target_spec/
spec.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    Error, Platform, Triple,
6    errors::{ExpressionParseError, PlainStringParseError},
7};
8use cfg_expr::{Expression, Predicate};
9use std::{borrow::Cow, fmt, str::FromStr, sync::Arc};
10
11/// A parsed target specification or triple string, as found in a `Cargo.toml` file.
12///
13/// ## Expressions and triple strings
14///
15/// Cargo supports [platform-specific
16/// dependencies](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies).
17/// These dependencies can be specified in one of two ways:
18///
19/// ```toml
20/// # 1. As Rust-like `#[cfg]` syntax.
21/// [target.'cfg(all(unix, target_arch = "x86_64"))'.dependencies]
22/// native = { path = "native/x86_64" }
23///
24/// # 2. Listing out the full target triple.
25/// [target.x86_64-pc-windows-gnu.dependencies]
26/// winhttp = "0.4.0"
27/// ```
28///
29/// ### The `cfg()` syntax
30///
31/// The first [`cfg()` syntax](https://doc.rust-lang.org/reference/conditional-compilation.html) is
32/// represented via the [`TargetSpec::Expression`] variant. This variant represents an arbitrary
33/// expression against certain properties of the target. To evaluate a [`Platform`] against this
34/// variant, `target-spec` builds an expression tree (currently via
35/// [`cfg-expr`](https://github.com/EmbarkStudios/cfg-expr)).
36///
37/// ### Plain string syntax
38///
39/// The second syntax, listing just the name of a platform, is represented via the
40/// [`TargetSpec::PlainString`] variant. In target-spec's model, the contained data is arbitrary
41/// and used only for string comparisons. For example,
42/// `TargetSpec::PlainString("x86_64-pc-windows-gnu")` will match the `x86_64-pc-windows-gnu`
43/// platform.
44///
45/// `target-spec` checks that the string looks sufficiently like a triple. This check is duplicated
46/// from Cargo's own check and is implemented in [`Self::looks_like_plain_string`].
47///
48/// ## Examples
49///
50/// ```
51/// use target_spec::{Platform, TargetFeatures, TargetSpec};
52///
53/// let i686_windows = Platform::new("i686-pc-windows-gnu", TargetFeatures::Unknown).unwrap();
54/// let x86_64_mac = Platform::new("x86_64-apple-darwin", TargetFeatures::none()).unwrap();
55/// let i686_linux = Platform::new(
56///     "i686-unknown-linux-gnu",
57///     TargetFeatures::features(["sse2"].iter().copied()),
58/// ).unwrap();
59///
60/// let spec: TargetSpec = "cfg(any(windows, target_arch = \"x86_64\"))".parse().unwrap();
61/// assert_eq!(spec.eval(&i686_windows), Some(true), "i686 Windows");
62/// assert_eq!(spec.eval(&x86_64_mac), Some(true), "x86_64 MacOS");
63/// assert_eq!(spec.eval(&i686_linux), Some(false), "i686 Linux (should not match)");
64///
65/// let spec: TargetSpec = "cfg(any(target_feature = \"sse2\", target_feature = \"sse\"))".parse().unwrap();
66/// assert_eq!(spec.eval(&i686_windows), None, "i686 Windows features are unknown");
67/// assert_eq!(spec.eval(&x86_64_mac), Some(false), "x86_64 MacOS matches no features");
68/// assert_eq!(spec.eval(&i686_linux), Some(true), "i686 Linux matches some features");
69/// ```
70#[derive(Clone, Debug)]
71pub enum TargetSpec {
72    /// A complex expression.
73    ///
74    /// Parsed from strings like `"cfg(any(windows, target_arch = \"x86_64\"))"`.
75    Expression(TargetSpecExpression),
76
77    /// A plain string representing a triple.
78    ///
79    /// This string hasn't been validated because it may represent a custom platform. To validate
80    /// this string, use [`Self::is_known`].
81    PlainString(TargetSpecPlainString),
82}
83
84impl TargetSpec {
85    /// Creates a new target spec from a string.
86    pub fn new(input: impl Into<Cow<'static, str>>) -> Result<Self, Error> {
87        let input = input.into();
88
89        if Self::looks_like_expression(&input) {
90            match TargetSpecExpression::new(&input) {
91                Ok(expression) => Ok(Self::Expression(expression)),
92                Err(error) => Err(Error::InvalidExpression(error)),
93            }
94        } else {
95            match TargetSpecPlainString::new(input) {
96                Ok(plain_str) => Ok(Self::PlainString(plain_str)),
97                Err(error) => Err(Error::InvalidTargetSpecString(error)),
98            }
99        }
100    }
101
102    /// Returns true if the input will be parsed as a target expression.
103    ///
104    /// In other words, returns true if the input begins with `"cfg("`.
105    pub fn looks_like_expression(input: &str) -> bool {
106        input.starts_with("cfg(")
107    }
108
109    /// Returns true if the input will be understood to be a plain string.
110    ///
111    /// This check is borrowed from
112    /// [`cargo-platform`](https://github.com/rust-lang/cargo/blob/5febbe5587b74108165f748e79a4f8badbdf5e0e/crates/cargo-platform/src/lib.rs#L40-L63).
113    ///
114    /// Note that this currently accepts an empty string. This matches Cargo's behavior as of Rust
115    /// 1.70.
116    pub fn looks_like_plain_string(input: &str) -> bool {
117        TargetSpecPlainString::validate(input).is_ok()
118    }
119
120    /// Returns true if an input looks like it's known and understood.
121    ///
122    /// * For [`Self::Expression`], the inner [`TargetSpecExpression`] has already been parsed as
123    ///   valid, so this returns true.
124    /// * For [`Self::PlainString`], this returns true if the string matches a builtin triple or has
125    ///   heuristically been determined to be valid.
126    ///
127    /// This method does not take into account custom platforms. If you know about custom platforms,
128    /// consider checking against those as well.
129    pub fn is_known(&self) -> bool {
130        match self {
131            TargetSpec::PlainString(plain_str) => {
132                Triple::new(plain_str.as_str().to_owned()).is_ok()
133            }
134            TargetSpec::Expression(_) => true,
135        }
136    }
137
138    /// Evaluates this specification against the given platform.
139    ///
140    /// Returns `Some(true)` if there's a match, `Some(false)` if there's none, or `None` if the
141    /// result of the evaluation is unknown (typically found if target features are involved).
142    #[inline]
143    pub fn eval(&self, platform: &Platform) -> Option<bool> {
144        match self {
145            TargetSpec::PlainString(plain_str) => Some(platform.triple_str() == plain_str.as_str()),
146            TargetSpec::Expression(expr) => expr.eval(platform),
147        }
148    }
149}
150
151impl FromStr for TargetSpec {
152    type Err = Error;
153
154    fn from_str(input: &str) -> Result<Self, Self::Err> {
155        Self::new(input.to_owned())
156    }
157}
158
159impl fmt::Display for TargetSpec {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        match self {
162            TargetSpec::Expression(expr) => write!(f, "{expr}"),
163            TargetSpec::PlainString(plain_str) => write!(f, "{plain_str}"),
164        }
165    }
166}
167
168/// A target expression, parsed from a string beginning with `cfg(`.
169///
170/// For more information, see [`TargetSpec`].
171#[derive(Clone, Debug)]
172pub struct TargetSpecExpression {
173    inner: Arc<Expression>,
174}
175
176impl TargetSpecExpression {
177    /// Creates a new `TargetSpecExpression` from a string beginning with `cfg(`.
178    ///
179    /// Returns an error if the string could not be parsed, or if the string contains a predicate
180    /// that wasn't understood by `target-spec`.
181    pub fn new(input: &str) -> Result<Self, ExpressionParseError> {
182        let expr = Expression::parse(input).map_err(|err| ExpressionParseError::new(input, err))?;
183        Ok(Self {
184            inner: Arc::new(expr),
185        })
186    }
187
188    /// Returns the string that was parsed into `self`.
189    #[inline]
190    pub fn expression_str(&self) -> &str {
191        self.inner.original()
192    }
193
194    /// Evaluates this expression against the given platform.
195    ///
196    /// Returns `Some(true)` if there's a match, `Some(false)` if there's none, or `None` if the
197    /// result of the evaluation is unknown (typically found if target features are involved).
198    pub fn eval(&self, platform: &Platform) -> Option<bool> {
199        self.inner.eval(|pred| {
200            match pred {
201                Predicate::Target(target) => Some(platform.triple().matches(target)),
202                Predicate::TargetFeature(feature) => platform.target_features().matches(feature),
203                Predicate::Test | Predicate::DebugAssertions | Predicate::ProcMacro => {
204                    // Known families that always evaluate to false. See
205                    // https://docs.rs/cargo-platform/0.1.1/src/cargo_platform/lib.rs.html#76.
206                    Some(false)
207                }
208                Predicate::Feature(_) => {
209                    // NOTE: This is not supported by Cargo which always evaluates this to false. See
210                    // https://github.com/rust-lang/cargo/issues/7442 for more details.
211                    Some(false)
212                }
213                Predicate::Flag(flag) => {
214                    // This returns false by default but true in some cases.
215                    Some(platform.has_flag(flag))
216                }
217                Predicate::KeyValue { .. } => {
218                    // This is always interpreted by Cargo as false.
219                    Some(false)
220                }
221            }
222        })
223    }
224}
225
226impl FromStr for TargetSpecExpression {
227    type Err = ExpressionParseError;
228
229    fn from_str(input: &str) -> Result<Self, Self::Err> {
230        Self::new(input)
231    }
232}
233
234impl fmt::Display for TargetSpecExpression {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        f.write_str(self.expression_str())
237    }
238}
239
240/// A plain string as contained within a [`TargetSpec::PlainString`].
241///
242/// For more information, see [`TargetSpec`].
243#[derive(Clone, Debug)]
244pub struct TargetSpecPlainString(Cow<'static, str>);
245
246impl TargetSpecPlainString {
247    /// Creates a new instance of `TargetSpecPlainString`, after validating it.
248    pub fn new(input: impl Into<Cow<'static, str>>) -> Result<Self, PlainStringParseError> {
249        let input = input.into();
250        Self::validate(&input)?;
251        Ok(Self(input))
252    }
253
254    /// Returns the string as parsed.
255    pub fn as_str(&self) -> &str {
256        &self.0
257    }
258
259    fn validate(input: &str) -> Result<(), PlainStringParseError> {
260        if let Some((char_index, c)) = input
261            .char_indices()
262            .find(|&(_, c)| !(c.is_alphanumeric() || c == '_' || c == '-' || c == '.'))
263        {
264            Err(PlainStringParseError::new(input.to_owned(), char_index, c))
265        } else {
266            Ok(())
267        }
268    }
269}
270
271impl FromStr for TargetSpecPlainString {
272    type Err = PlainStringParseError;
273
274    fn from_str(input: &str) -> Result<Self, Self::Err> {
275        Self::new(input.to_owned())
276    }
277}
278
279impl fmt::Display for TargetSpecPlainString {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        f.write_str(&self.0)
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use cfg_expr::{
289        Predicate, TargetPredicate,
290        targets::{Abi, Arch, Family, Os},
291    };
292
293    #[test]
294    fn test_triple() {
295        let res = TargetSpec::new("x86_64-apple-darwin");
296        assert!(matches!(
297            res,
298            Ok(TargetSpec::PlainString(triple)) if triple.as_str() == "x86_64-apple-darwin"
299        ));
300    }
301
302    #[test]
303    fn test_single() {
304        let expr = match TargetSpec::new("cfg(windows)").unwrap() {
305            TargetSpec::PlainString(triple) => {
306                panic!("expected expression, got triple: {:?}", triple)
307            }
308            TargetSpec::Expression(expr) => expr,
309        };
310        assert_eq!(
311            expr.inner.predicates().collect::<Vec<_>>(),
312            vec![Predicate::Target(TargetPredicate::Family(Family::windows))],
313        );
314    }
315
316    #[test]
317    fn test_target_abi() {
318        let expr =
319            match TargetSpec::new("cfg(any(target_arch = \"wasm32\", target_abi = \"unknown\"))")
320                .unwrap()
321            {
322                TargetSpec::PlainString(triple) => {
323                    panic!("expected expression, got triple: {:?}", triple)
324                }
325                TargetSpec::Expression(expr) => expr,
326            };
327
328        assert_eq!(
329            expr.inner.predicates().collect::<Vec<_>>(),
330            vec![
331                Predicate::Target(TargetPredicate::Arch(Arch("wasm32".into()))),
332                Predicate::Target(TargetPredicate::Abi(Abi("unknown".into()))),
333            ],
334        );
335    }
336
337    #[test]
338    fn test_not() {
339        assert!(matches!(
340            TargetSpec::new("cfg(not(windows))"),
341            Ok(TargetSpec::Expression(_))
342        ));
343    }
344
345    #[test]
346    fn test_testequal() {
347        let expr = match TargetSpec::new("cfg(target_os = \"windows\")").unwrap() {
348            TargetSpec::PlainString(triple) => {
349                panic!("expected spec, got triple: {:?}", triple)
350            }
351            TargetSpec::Expression(expr) => expr,
352        };
353
354        assert_eq!(
355            expr.inner.predicates().collect::<Vec<_>>(),
356            vec![Predicate::Target(TargetPredicate::Os(Os::windows))],
357        );
358    }
359
360    #[test]
361    fn test_identifier_like_triple() {
362        // This used to be "x86_64-pc-darwin", but target-lexicon can parse that.
363        let spec = TargetSpec::new("cannotbeknown")
364            .expect("triples that look like identifiers should parse correctly");
365        assert!(!spec.is_known(), "spec isn't known");
366    }
367
368    #[test]
369    fn test_triple_string_identifier() {
370        // We generally trust that unicode-ident is correct. Just do some basic checks.
371        let valid = ["", "foo", "foo-bar", "foo_baz", "-foo", "quux-"];
372        let invalid = ["foo+bar", "foo bar", " "];
373        for input in valid {
374            assert!(
375                TargetSpec::looks_like_plain_string(input),
376                "`{input}` looks like triple string"
377            );
378        }
379        for input in invalid {
380            assert!(
381                !TargetSpec::looks_like_plain_string(input),
382                "`{input}` does not look like triple string"
383            );
384        }
385    }
386
387    #[test]
388    fn test_unknown_triple() {
389        // This used to be "x86_64-pc-darwin", but target-lexicon can parse that.
390        let err = TargetSpec::new("cannotbeknown!!!")
391            .expect_err("unknown triples should parse correctly");
392        assert!(matches!(
393            err,
394            Error::InvalidTargetSpecString(s) if s.input == "cannotbeknown!!!"
395        ));
396    }
397
398    #[test]
399    fn test_unknown_flag() {
400        let expr = match TargetSpec::new("cfg(foo)").unwrap() {
401            TargetSpec::PlainString(triple) => {
402                panic!("expected spec, got triple: {:?}", triple)
403            }
404            TargetSpec::Expression(expr) => expr,
405        };
406
407        assert_eq!(
408            expr.inner.predicates().collect::<Vec<_>>(),
409            vec![Predicate::Flag("foo")],
410        );
411    }
412
413    #[test]
414    fn test_unknown_predicate() {
415        let expr = match TargetSpec::new("cfg(bogus_key = \"bogus_value\")")
416            .expect("unknown predicate should parse")
417        {
418            TargetSpec::PlainString(triple) => {
419                panic!("expected spec, got triple: {:?}", triple)
420            }
421            TargetSpec::Expression(expr) => expr,
422        };
423        assert_eq!(
424            expr.inner.predicates().collect::<Vec<_>>(),
425            vec![Predicate::KeyValue {
426                key: "bogus_key",
427                val: "bogus_value"
428            }],
429        );
430
431        let platform = Platform::build_target().unwrap();
432        // This should always evaluate to false.
433        assert_eq!(expr.eval(&platform), Some(false));
434
435        let expr = TargetSpec::new("cfg(not(bogus_key = \"bogus_value\"))")
436            .expect("unknown predicate should parse");
437        // This is a cfg(not()), so it should always evaluate to true.
438        assert_eq!(expr.eval(&platform), Some(true));
439    }
440
441    #[test]
442    fn test_extra() {
443        let res = TargetSpec::new("cfg(unix)this-is-extra");
444        res.expect_err("extra content at the end");
445    }
446
447    #[test]
448    fn test_incomplete() {
449        // This fails because the ) at the end is missing.
450        let res = TargetSpec::new("cfg(not(unix)");
451        res.expect_err("missing ) at the end");
452    }
453}