Skip to main content

alpm_types/version/
buildtool.rs

1//! Build tool related version handling.
2
3use std::{
4    fmt::{Display, Formatter},
5    str::FromStr,
6};
7
8use alpm_parsers::traits::{AlpmParser, ParserUntil};
9#[cfg(feature = "serde")]
10use serde::Serialize;
11use winnow::{
12    Parser,
13    combinator::opt,
14    error::{ContextError, ErrMode, StrContext, StrContextValue},
15    prelude::ModalResult,
16};
17
18#[cfg(doc)]
19use crate::BuildTool;
20use crate::{Architecture, Error, FullVersion, MinimalVersion, Version};
21
22/// The version and optional architecture of a build tool.
23///
24/// [`BuildToolVersion`] is used in conjunction with [`BuildTool`] to denote the specific build tool
25/// a package is built with.
26/// [`BuildToolVersion`] distinguishes between two types of representations:
27///
28/// - the one used by [makepkg], which relies on [`MinimalVersion`]
29/// - and the one used by [pkgctl] (devtools), which relies on [`FullVersion`] and the
30///   [`Architecture`] of the build tool.
31///
32/// For more information refer to the `buildtoolver` keyword in [BUILDINFOv2].
33///
34/// ## Examples
35/// ```
36/// use std::str::FromStr;
37///
38/// use alpm_types::{Architecture, BuildToolVersion, FullVersion, MinimalVersion};
39///
40/// # fn main() -> testresult::TestResult {
41/// // Representation used by makepkg
42/// assert_eq!(
43///     BuildToolVersion::from_str("1.0.0")?,
44///     BuildToolVersion::Makepkg(MinimalVersion::from_str("1.0.0")?)
45/// );
46/// assert_eq!(
47///     BuildToolVersion::from_str("1:1.0.0")?,
48///     BuildToolVersion::Makepkg(MinimalVersion::from_str("1:1.0.0")?)
49/// );
50///
51/// // Representation used by pkgctl
52/// assert_eq!(
53///     BuildToolVersion::from_str("1.0.0-1-any")?,
54///     BuildToolVersion::DevTools {
55///         version: FullVersion::from_str("1.0.0-1")?,
56///         architecture: Architecture::from_str("any")?
57///     }
58/// );
59/// assert_eq!(
60///     BuildToolVersion::from_str("1:1.0.0-1-any")?,
61///     BuildToolVersion::DevTools {
62///         version: FullVersion::from_str("1:1.0.0-1")?,
63///         architecture: Architecture::from_str("any")?
64///     }
65/// );
66/// # Ok(())
67/// # }
68/// ```
69///
70/// [BUILDINFOv2]: https://alpm.archlinux.page/specifications/BUILDINFOv2.5.html
71/// [makepkg]: https://man.archlinux.org/man/makepkg.8
72/// [pkgctl]: https://man.archlinux.org/man/pkgctl.1
73#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
74#[cfg_attr(feature = "serde", derive(Serialize))]
75pub enum BuildToolVersion {
76    /// The version representation used by [makepkg].
77    ///
78    /// [makepkg]: https://man.archlinux.org/man/makepkg.8
79    Makepkg(MinimalVersion),
80    /// The version representation used by [pkgctl] (devtools).
81    ///
82    /// [pkgctl]: https://man.archlinux.org/man/pkgctl.1
83    DevTools {
84        /// The (_full_ or _full with epoch_) version of the build tool.
85        version: FullVersion,
86        /// The architecture of the build tool.
87        architecture: Architecture,
88    },
89}
90
91impl BuildToolVersion {
92    /// Returns the optional [`Architecture`].
93    ///
94    /// # Note
95    ///
96    /// If `self` is a [`BuildToolVersion::Makepkg`] this method always returns [`None`].
97    pub fn architecture(&self) -> Option<Architecture> {
98        if let Self::DevTools {
99            version: _,
100            architecture,
101        } = self
102        {
103            Some(architecture.clone())
104        } else {
105            None
106        }
107    }
108
109    /// Returns a [`Version`] that matches the underlying [`MinimalVersion`] or [`FullVersion`].
110    pub fn version(&self) -> Version {
111        match self {
112            Self::Makepkg(version) => Version::from(version),
113            Self::DevTools {
114                version,
115                architecture: _,
116            } => Version::from(version),
117        }
118    }
119}
120
121impl AlpmParser for BuildToolVersion {
122    /// Recognizes a [`BuildToolVersion`] in a string slice.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if `input` does not begin with a [build tool version].
127    ///
128    /// [build tool version]: https://alpm.archlinux.page/specifications/BUILDINFOv2.5.html#buildtoolver
129    fn parser(input: &mut &str) -> ModalResult<Self> {
130        // The start can either be:
131        // - A minimal version (no pkgrel, thereby shorter)
132        // - A full version together with an `-` and an architecture.
133        //
134        // Since the FullVersion is longer, we can use it to determine what kind of input we can
135        // expect.
136        let full_version = opt(FullVersion::parser).parse_next(input)?;
137
138        if let Some(version) = full_version {
139            "-".context(StrContext::Label("buildtool version"))
140                .context(StrContext::Expected(StrContextValue::Description(
141                    "'-' delimiter between full alpm-package-version and alpm-architecture",
142                )))
143                .parse_next(input)?;
144
145            let architecture = Architecture::parser.parse_next(input)?;
146            return Ok(BuildToolVersion::DevTools {
147                version,
148                architecture,
149            });
150        }
151
152        let minimal_version =  MinimalVersion::parser
153            .context(StrContext::Label("buildtool version"))
154            .context(StrContext::Expected(StrContextValue::Description("a stand-alone minimal alpm-package-version")))
155            .context(StrContext::Expected(StrContextValue::Description("or a full alpm-package-version together with an alpm-architecture, delimited by a '-'")))
156            .parse_next(input)?;
157
158        Ok(BuildToolVersion::Makepkg(minimal_version))
159    }
160
161    fn delimiter_error_context<'a, O, P>(
162        parser: P,
163    ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
164    where
165        P: Parser<&'a str, O, ErrMode<ContextError>>,
166    {
167        parser
168            .context(StrContext::Label("buildtool version"))
169            .context(StrContext::Expected(StrContextValue::Description("a stand-alone minimal alpm-package-version")))
170            .context(StrContext::Expected(StrContextValue::Description("or a full alpm-package-version together with an alpm-architecture, delimited by a '-'")))
171    }
172}
173
174impl FromStr for BuildToolVersion {
175    type Err = Error;
176    /// Creates a [`BuildToolVersion`] from a string slice.
177    ///
178    /// Delegates to [`BuildToolVersion::parser`].
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if [`BuildToolVersion::parser`] fails.
183    fn from_str(s: &str) -> Result<Self, Self::Err> {
184        Ok(Self::parser_until_eof.parse(s)?)
185    }
186}
187
188impl Display for BuildToolVersion {
189    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
190        match self {
191            Self::Makepkg(version) => write!(f, "{version}"),
192            Self::DevTools {
193                version,
194                architecture,
195            } => write!(f, "{version}-{architecture}"),
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use insta::assert_snapshot;
203    use rstest::rstest;
204    use testresult::TestResult;
205
206    use super::*;
207    use crate::configure_insta;
208
209    /// Ensure that valid strings are correctly parsed as [`BuildToolVersion`] and invalid ones lead
210    /// to an [`Error`].
211    #[rstest]
212    #[case::devtools_full(
213        "1.0.0-1-any",
214        BuildToolVersion::DevTools{version: FullVersion::from_str("1.0.0-1")?, architecture: Architecture::from_str("any")?},
215    )]
216    #[case::devtools_full_with_epoch(
217        "1:1.0.0-1-any",
218        BuildToolVersion::DevTools{version: FullVersion::from_str("1:1.0.0-1")?, architecture: Architecture::from_str("any")?},
219    )]
220    #[case::makepkg_minimal(
221        "1.0.0",
222        BuildToolVersion::Makepkg(MinimalVersion::from_str("1.0.0")?),
223    )]
224    #[case::makepkg_minimal_with_epoch(
225        "1:1.0.0",
226        BuildToolVersion::Makepkg(MinimalVersion::from_str("1:1.0.0")?),
227    )]
228    fn valid_buildtool_version(
229        #[case] input: &str,
230        #[case] expected: BuildToolVersion,
231    ) -> TestResult {
232        let version = match BuildToolVersion::from_str(input) {
233            Ok(version) => version,
234            Err(err) => {
235                panic!("Expected BuildToolVersion parsing of string {input} to succeed:\n{err}")
236            }
237        };
238
239        assert_eq!(
240            version, expected,
241            "Expected '{expected:#?}' when parsing '{input}' but got '{version:#?}'"
242        );
243
244        Ok(())
245    }
246
247    #[rstest]
248    #[case::full_version_with_architecture("1.0.0-any")]
249    #[case::minimal_version_with_epoch_and_architecture("1:1.0.0-any")]
250    #[case::bad_package_version("ß-1-any")]
251    fn invalid_buildtool_version(#[case] input: &str) -> TestResult {
252        let err = match BuildToolVersion::from_str(input) {
253            Err(err) => err,
254            Ok(_) => {
255                panic!("Expected BuildToolVersion parsing of string {input} to fail")
256            }
257        };
258
259        let (test_name, _guard) = configure_insta();
260        assert_snapshot!(test_name, err.to_string());
261
262        Ok(())
263    }
264}