cargo_feature_combinations/
target.rs1use color_eyre::eyre::{self, WrapErr};
2use std::process::Command;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
6pub struct TargetTriple(pub String);
7
8impl TargetTriple {
9 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum TargetSource {
29 Cli,
31 PackageConfig,
33 WorkspaceConfig,
35 CargoBuildTargetEnv,
37 Host,
39}
40
41impl TargetSource {
42 #[must_use]
44 pub fn should_inject_target_arg(self) -> bool {
45 matches!(self, Self::PackageConfig | Self::WorkspaceConfig)
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct EffectiveTarget {
52 pub triple: TargetTriple,
54 pub source: TargetSource,
56}
57
58pub trait TargetEnvironment {
63 fn cargo_build_target(&self) -> Option<String>;
65 fn host_target(&self) -> eyre::Result<TargetTriple>;
71}
72
73#[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
94pub 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
124pub 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 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}