Skip to main content

cargo_feature_combinations/
target.rs

1use color_eyre::eyre::{self, WrapErr};
2use std::process::Command;
3
4/// A Rust target triple.
5#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
6pub struct TargetTriple(pub String);
7
8impl TargetTriple {
9    /// Borrow this target triple as a string.
10    #[must_use]
11    pub fn as_str(&self) -> &str {
12        &self.0
13    }
14}
15
16impl std::fmt::Display for TargetTriple {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        f.write_str(&self.0)
19    }
20}
21
22/// Where a package's effective target came from in the precedence chain.
23///
24/// Carried per package-target assignment so that injection and output
25/// decisions stay local to the assignment instead of relying on plan-wide
26/// boolean drift (`is_configured`, `should_inject`, `is_multi_target`).
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum TargetSource {
29    /// Explicit Cargo CLI `--target <triple>` (already forwarded to Cargo).
30    Cli,
31    /// Package-level `targets` list (cargo-fc must inject `--target`).
32    PackageConfig,
33    /// Workspace-level `targets` list (cargo-fc must inject `--target`).
34    WorkspaceConfig,
35    /// `CARGO_BUILD_TARGET` (Cargo sees the env var; no injection needed).
36    CargoBuildTargetEnv,
37    /// Host target from `rustc -vV` (keep current no-`--target` behavior).
38    Host,
39}
40
41impl TargetSource {
42    /// Whether cargo-fc must inject a `--target <triple>` flag for this source.
43    #[must_use]
44    pub fn should_inject_target_arg(self) -> bool {
45        matches!(self, Self::PackageConfig | Self::WorkspaceConfig)
46    }
47}
48
49/// A concrete target together with where it came from.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct EffectiveTarget {
52    /// The concrete target triple.
53    pub triple: TargetTriple,
54    /// Where this target was selected from.
55    pub source: TargetSource,
56}
57
58/// Adapter providing the ambient target environment for planning.
59///
60/// Abstracted behind a trait so that target planning is unit-testable without
61/// reading the real environment or invoking `rustc`.
62pub trait TargetEnvironment {
63    /// The `CARGO_BUILD_TARGET` triple, if set and non-empty.
64    fn cargo_build_target(&self) -> Option<String>;
65    /// The host target triple from `rustc -vV`.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if the host target cannot be determined.
70    fn host_target(&self) -> eyre::Result<TargetTriple>;
71}
72
73/// Production [`TargetEnvironment`] backed by the process environment and
74/// `rustc -vV`.
75#[derive(Debug, Default, Clone, Copy)]
76pub struct RustcTargetEnvironment;
77
78impl TargetEnvironment for RustcTargetEnvironment {
79    fn cargo_build_target(&self) -> Option<String> {
80        let triple = std::env::var("CARGO_BUILD_TARGET").ok()?;
81        let triple = triple.trim();
82        if triple.is_empty() {
83            None
84        } else {
85            Some(triple.to_string())
86        }
87    }
88
89    fn host_target(&self) -> eyre::Result<TargetTriple> {
90        host_triple()
91    }
92}
93
94/// Detect the host target triple via `rustc -vV`.
95///
96/// # Errors
97///
98/// Returns an error if `rustc` cannot be invoked or its output cannot be
99/// parsed.
100pub fn host_triple() -> eyre::Result<TargetTriple> {
101    let output = Command::new("rustc")
102        .arg("-vV")
103        .output()
104        .wrap_err("failed to invoke rustc to detect host target")?;
105
106    if !output.status.success() {
107        eyre::bail!("rustc -vV failed with exit code {:?}", output.status.code());
108    }
109
110    let stdout = String::from_utf8_lossy(&output.stdout);
111    for line in stdout.lines() {
112        if let Some(host) = line.strip_prefix("host: ") {
113            let host = host.trim();
114            if host.is_empty() {
115                continue;
116            }
117            return Ok(TargetTriple(host.to_string()));
118        }
119    }
120
121    eyre::bail!("could not parse host target triple from `rustc -vV`")
122}
123
124/// Parse an explicit Cargo `--target <triple>` / `--target=<triple>` flag from
125/// forwarded args, considering only arguments **before** `--`.
126///
127/// Arguments after `--` belong to the test binary or run target and must not
128/// affect cargo-fc target planning, e.g. `cargo fc run -- --target value`.
129/// # Errors
130///
131/// Returns an error when more than one explicit target is present. Cargo
132/// accepts repeated targets for some commands, but cargo-fc currently plans
133/// one explicit target; silently choosing one would make summaries misleading.
134pub fn parse_cli_target(cargo_args: &[String]) -> eyre::Result<Option<String>> {
135    let mut target = None;
136    let mut it = cargo_args.iter();
137    while let Some(arg) = it.next() {
138        if arg == "--" {
139            break;
140        }
141        if arg == "--target"
142            && let Some(v) = it.next()
143        {
144            if v == "--" {
145                break;
146            }
147            set_cli_target(&mut target, v.clone())?;
148            continue;
149        }
150        if let Some(v) = arg.strip_prefix("--target=")
151            && !v.is_empty()
152        {
153            set_cli_target(&mut target, v.to_string())?;
154        }
155    }
156    Ok(target)
157}
158
159fn set_cli_target(target: &mut Option<String>, value: String) -> eyre::Result<()> {
160    if let Some(existing) = target {
161        eyre::bail!(
162            "cargo-fc supports only one explicit --target at a time; got `{existing}` and `{value}`"
163        );
164    }
165    *target = Some(value);
166    Ok(())
167}
168
169#[cfg(test)]
170mod test {
171    use super::parse_cli_target;
172
173    fn owned(values: &[&str]) -> Vec<String> {
174        values.iter().copied().map(String::from).collect()
175    }
176
177    #[test]
178    fn parse_cli_target_separate_value() {
179        assert_eq!(
180            parse_cli_target(&owned(&["check", "--target", "wasm32-unknown-unknown"])).unwrap(),
181            Some("wasm32-unknown-unknown".to_string())
182        );
183    }
184
185    #[test]
186    fn parse_cli_target_equals_form() {
187        assert_eq!(
188            parse_cli_target(&owned(&["check", "--target=wasm32-unknown-unknown"])).unwrap(),
189            Some("wasm32-unknown-unknown".to_string())
190        );
191    }
192
193    #[test]
194    fn parse_cli_target_ignores_value_after_double_dash() {
195        // `cargo fc run -- --target value-for-the-binary` must not be treated as
196        // cargo's target triple.
197        assert_eq!(
198            parse_cli_target(&owned(&["run", "--", "--target", "binary-arg"])).unwrap(),
199            None
200        );
201    }
202
203    #[test]
204    fn parse_cli_target_does_not_consume_double_dash_as_value() {
205        assert_eq!(
206            parse_cli_target(&owned(&["check", "--target", "--"])).unwrap(),
207            None
208        );
209    }
210
211    #[test]
212    fn parse_cli_target_before_double_dash_still_parsed() {
213        assert_eq!(
214            parse_cli_target(&owned(&[
215                "run",
216                "--target",
217                "x86_64-unknown-linux-gnu",
218                "--",
219                "--target",
220                "binary-arg",
221            ]))
222            .unwrap(),
223            Some("x86_64-unknown-linux-gnu".to_string())
224        );
225    }
226
227    #[test]
228    fn parse_cli_target_absent() {
229        assert_eq!(
230            parse_cli_target(&owned(&["check", "--all-features"])).unwrap(),
231            None
232        );
233    }
234
235    #[test]
236    fn parse_cli_target_rejects_repeated_targets() {
237        let err = parse_cli_target(&owned(&["check", "--target=a", "--target", "b"]))
238            .expect_err("repeated targets should fail");
239
240        assert!(err.to_string().contains("only one explicit --target"));
241    }
242}