alpm_types/version/
buildtool.rs

1//! Build tool related version handling.
2
3use std::{
4    fmt::{Display, Formatter},
5    str::FromStr,
6};
7
8use serde::Serialize;
9
10#[cfg(doc)]
11use crate::BuildTool;
12use crate::{Architecture, Error, FullVersion, MinimalVersion, Version};
13
14/// The version and optional architecture of a build tool.
15///
16/// [`BuildToolVersion`] is used in conjunction with [`BuildTool`] to denote the specific build tool
17/// a package is built with.
18/// [`BuildToolVersion`] distinguishes between two types of representations:
19///
20/// - the one used by [makepkg], which relies on [`MinimalVersion`]
21/// - and the one used by [pkgctl] (devtools), which relies on [`FullVersion`] and the
22///   [`Architecture`] of the build tool.
23///
24/// For more information refer to the `buildtoolver` keyword in [BUILDINFOv2].
25///
26/// ## Examples
27/// ```
28/// use std::str::FromStr;
29///
30/// use alpm_types::{Architecture, BuildToolVersion, FullVersion, MinimalVersion};
31///
32/// # fn main() -> testresult::TestResult {
33/// // Representation used by makepkg
34/// assert_eq!(
35///     BuildToolVersion::from_str("1.0.0")?,
36///     BuildToolVersion::Makepkg(MinimalVersion::from_str("1.0.0")?)
37/// );
38/// assert_eq!(
39///     BuildToolVersion::from_str("1:1.0.0")?,
40///     BuildToolVersion::Makepkg(MinimalVersion::from_str("1:1.0.0")?)
41/// );
42///
43/// // Representation used by pkgctl
44/// assert_eq!(
45///     BuildToolVersion::from_str("1.0.0-1-any")?,
46///     BuildToolVersion::DevTools {
47///         version: FullVersion::from_str("1.0.0-1")?,
48///         architecture: Architecture::from_str("any")?
49///     }
50/// );
51/// assert_eq!(
52///     BuildToolVersion::from_str("1:1.0.0-1-any")?,
53///     BuildToolVersion::DevTools {
54///         version: FullVersion::from_str("1:1.0.0-1")?,
55///         architecture: Architecture::from_str("any")?
56///     }
57/// );
58/// # Ok(())
59/// # }
60/// ```
61///
62/// [BUILDINFOv2]: https://alpm.archlinux.page/specifications/BUILDINFOv2.5.html
63/// [makepkg]: https://man.archlinux.org/man/makepkg.8
64/// [pkgctl]: https://man.archlinux.org/man/pkgctl.1
65#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
66pub enum BuildToolVersion {
67    /// The version representation used by [makepkg].
68    ///
69    /// [makepkg]: https://man.archlinux.org/man/makepkg.8
70    Makepkg(MinimalVersion),
71    /// The version representation used by [pkgctl] (devtools).
72    ///
73    /// [pkgctl]: https://man.archlinux.org/man/pkgctl.1
74    DevTools {
75        /// The (_full_ or _full with epoch_) version of the build tool.
76        version: FullVersion,
77        /// The architecture of the build tool.
78        architecture: Architecture,
79    },
80}
81
82impl BuildToolVersion {
83    /// Returns the optional [`Architecture`].
84    ///
85    /// # Note
86    ///
87    /// If `self` is a [`BuildToolVersion::Makepkg`] this method always returns [`None`].
88    pub fn architecture(&self) -> Option<Architecture> {
89        if let Self::DevTools {
90            version: _,
91            architecture,
92        } = self
93        {
94            Some(*architecture)
95        } else {
96            None
97        }
98    }
99
100    /// Returns a [`Version`] that matches the underlying [`MinimalVersion`] or [`FullVersion`].
101    pub fn version(&self) -> Version {
102        match self {
103            Self::Makepkg(version) => Version::from(version),
104            Self::DevTools {
105                version,
106                architecture: _,
107            } => Version::from(version),
108        }
109    }
110}
111
112impl FromStr for BuildToolVersion {
113    type Err = Error;
114    /// Creates a [`BuildToolVersion`] from a string slice.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if
119    ///
120    /// - `s` contains no '-' and `s` is not a valid [`MinimalVersion`],
121    /// - or `s` contains at least one '-' and after splitting on the right most occurrence, either
122    ///   the left-hand side is not a valid [`FullVersion`] or the right hand side is not a valid
123    ///   [`Architecture`].
124    fn from_str(s: &str) -> Result<Self, Self::Err> {
125        match s.rsplit_once('-') {
126            Some((version, architecture)) => Ok(BuildToolVersion::DevTools {
127                version: FullVersion::from_str(version)?,
128                architecture: Architecture::from_str(architecture)?,
129            }),
130            None => Ok(BuildToolVersion::Makepkg(MinimalVersion::from_str(s)?)),
131        }
132    }
133}
134
135impl Display for BuildToolVersion {
136    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
137        match self {
138            Self::Makepkg(version) => write!(f, "{version}"),
139            Self::DevTools {
140                version,
141                architecture,
142            } => write!(f, "{version}-{architecture}"),
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use rstest::rstest;
150    use testresult::TestResult;
151
152    use super::*;
153
154    /// Ensure that valid strings are correctly parsed as [`BuildToolVersion`] and invalid ones lead
155    /// to an [`Error`].
156    #[rstest]
157    #[case::devtools_full(
158        "1.0.0-1-any",
159        Ok(BuildToolVersion::DevTools{version: FullVersion::from_str("1.0.0-1")?, architecture: Architecture::from_str("any")?}),
160    )]
161    #[case::devtools_full_with_epoch(
162        "1:1.0.0-1-any",
163        Ok(BuildToolVersion::DevTools{version: FullVersion::from_str("1:1.0.0-1")?, architecture: Architecture::from_str("any")?}),
164    )]
165    #[case::makepkg_minimal(
166        "1.0.0",
167        Ok(BuildToolVersion::Makepkg(MinimalVersion::from_str("1.0.0")?)),
168    )]
169    #[case::makepkg_minimal_with_epoch(
170        "1:1.0.0",
171        Ok(BuildToolVersion::Makepkg(MinimalVersion::from_str("1:1.0.0")?)),
172    )]
173    #[case::minimal_version_with_architecture("1.0.0-any",
174        Err(Error::ParseError(
175            "1.0.0\n^\nexpected alpm-pkgver string, followed by a '-' and an alpm-pkgrel string".to_string()
176        ))
177     )]
178    #[case::minimal_version_with_epoch_and_architecture(
179        "1:1.0.0-any",
180        Err(Error::ParseError(
181            "1:1.0.0\n  ^\nexpected alpm-pkgver string, followed by a '-' and an alpm-pkgrel string".to_string()
182        ))
183    )]
184    #[case::full_version_with_bogus_architecture("1.0.0-1-foo", Err(strum::ParseError::VariantNotFound.into()))]
185    fn valid_buildtoolver_new(
186        #[case] input: &str,
187        #[case] expected: Result<BuildToolVersion, Error>,
188    ) -> TestResult {
189        let parse_result = BuildToolVersion::from_str(input);
190        assert_eq!(
191            parse_result, expected,
192            "Expected '{expected:?}' when parsing '{input}' but got '{parse_result:?}'"
193        );
194
195        Ok(())
196    }
197
198    /// Ensures that [`BuildToolVersion::from_str`] fails on invalid version strings with specific
199    /// errors.
200    #[rstest]
201    #[case::minimal_version_with_architecture(
202        "1.0.0-any",
203        Error::ParseError(
204            "1.0.0\n^\nexpected alpm-pkgver string, followed by a '-' and an alpm-pkgrel string".to_string()
205        )
206    )]
207    #[case::minimal_version_with_invalid_architecture(
208        "1.0.0-foo",
209        Error::ParseError(
210            "1.0.0\n^\nexpected alpm-pkgver string, followed by a '-' and an alpm-pkgrel string".to_string()
211        )
212    )]
213    #[case::full_version_with_invalid_architecture("1.0.0-1-foo", strum::ParseError::VariantNotFound.into())]
214    fn invalid_buildtoolver_new(#[case] buildtoolver: &str, #[case] expected: Error) {
215        assert_eq!(
216            BuildToolVersion::from_str(buildtoolver),
217            Err(expected),
218            "Expected error during parse of buildtoolver '{buildtoolver}'"
219        );
220    }
221
222    #[rstest]
223    #[case(".1.0.0-1-any", "invalid first pkgver character")]
224    fn invalid_buildtoolver_badpkgver(#[case] buildtoolver: &str, #[case] err_snippet: &str) {
225        let Err(Error::ParseError(err_msg)) = BuildToolVersion::from_str(buildtoolver) else {
226            panic!("'{buildtoolver}' erroneously parsed as BuildToolVersion")
227        };
228        assert!(
229            err_msg.contains(err_snippet),
230            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
231        );
232    }
233}