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}