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