1use std::{
11 collections::{BTreeSet, HashMap},
12 ffi::OsString,
13 fs,
14 path::{Component, Path, PathBuf},
15 process::Command,
16 str::FromStr,
17};
18
19use cargo_config2::cargo_home_with_cwd;
20use cargo_platform::{Cfg, CfgExpr};
21use serde::{Deserialize, Serialize};
22
23use crate::{
24 rust_cargo_config_model::{
25 CargoConfigDefinition, CargoConfigKind, CargoConfigValue, load_cargo_configuration,
26 },
27 rust_test_runner::CargoTestInvocation,
28};
29
30#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
31#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
32pub enum RustCargoRunnerProgram {
33 SearchPath { value: String },
34 Absolute { value: PathBuf },
35 WorkspaceRelative { value: PathBuf },
36}
37
38impl RustCargoRunnerProgram {
39 pub fn resolve(&self, workspace: &Path) -> PathBuf {
40 match self {
41 Self::SearchPath { value } => PathBuf::from(value),
42 Self::Absolute { value } => value.clone(),
43 Self::WorkspaceRelative { value } => workspace.join(value),
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
49#[serde(rename_all = "camelCase", deny_unknown_fields)]
50pub struct RustCargoUnderlyingRunner {
51 pub program: RustCargoRunnerProgram,
52 pub arguments: Vec<String>,
53}
54
55impl RustCargoUnderlyingRunner {
56 pub fn resolve(&self, workspace: &Path) -> RustCargoResolvedRunner {
57 RustCargoResolvedRunner {
58 program: self.program.resolve(workspace),
59 arguments: self.arguments.clone(),
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
65#[serde(rename_all = "camelCase", deny_unknown_fields)]
66pub struct RustCargoResolvedRunner {
67 pub program: PathBuf,
68 pub arguments: Vec<String>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
72#[serde(rename_all = "camelCase", deny_unknown_fields)]
73pub struct RustCargoTargetRunnerPlan {
74 pub target: String,
75 pub underlying_runner: Option<RustCargoUnderlyingRunner>,
76}
77
78impl RustCargoTargetRunnerPlan {
79 pub fn resolve(&self, workspace: &Path) -> RustCargoResolvedTargetRunner {
80 RustCargoResolvedTargetRunner {
81 target: self.target.clone(),
82 underlying_runner: self
83 .underlying_runner
84 .as_ref()
85 .map(|runner| runner.resolve(workspace)),
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
91#[serde(rename_all = "camelCase", deny_unknown_fields)]
92pub struct RustCargoResolvedTargetRunner {
93 pub target: String,
94 pub underlying_runner: Option<RustCargoResolvedRunner>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
98#[serde(rename_all = "camelCase", deny_unknown_fields)]
99pub struct RustCargoRunnerPlan {
100 pub compiler: RustCargoCompilerCommandPlan,
101 pub targets: Vec<RustCargoTargetRunnerPlan>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
105#[serde(rename_all = "camelCase", deny_unknown_fields)]
106pub struct RustCargoCompilerCommandPlan {
107 pub rustc: RustCargoRunnerProgram,
108 pub rustc_wrapper: Option<RustCargoRunnerProgram>,
109 pub rustc_workspace_wrapper: Option<RustCargoRunnerProgram>,
110}
111
112impl RustCargoCompilerCommandPlan {
113 fn resolved_programs(&self, workspace: &Path) -> Vec<PathBuf> {
114 self.rustc_wrapper
115 .iter()
116 .chain(self.rustc_workspace_wrapper.iter())
117 .chain(std::iter::once(&self.rustc))
118 .map(|program| program.resolve(workspace))
119 .collect()
120 }
121
122 fn command(&self, workspace: &Path) -> Command {
123 let programs = self.resolved_programs(workspace);
124 let mut command = Command::new(&programs[0]);
125 command.args(&programs[1..]);
126 command
127 }
128}
129
130#[derive(Debug, Clone)]
131struct CargoModelInputs {
132 cargo_home: Option<PathBuf>,
133 environment: HashMap<String, OsString>,
134 host_override: Option<String>,
135}
136
137impl CargoModelInputs {
138 fn ambient(root: &Path) -> Self {
139 Self {
140 cargo_home: cargo_home_with_cwd(root),
141 environment: std::env::vars_os()
142 .filter_map(|(key, value)| key.into_string().ok().map(|key| (key, value)))
143 .collect(),
144 host_override: None,
145 }
146 }
147
148 fn environment(&self, key: &str) -> Option<&OsString> {
149 self.environment.get(key)
150 }
151}
152
153#[derive(Debug)]
154pub enum RustCargoConfigurationError {
155 Io { path: PathBuf, reason: String },
156 Invalid(String),
157 Unsupported(String),
158}
159
160impl std::fmt::Display for RustCargoConfigurationError {
161 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 match self {
163 Self::Io { path, reason } => write!(formatter, "{}: {reason}", path.display()),
164 Self::Invalid(reason) => write!(formatter, "invalid Cargo configuration: {reason}"),
165 Self::Unsupported(reason) => formatter.write_str(reason),
166 }
167 }
168}
169
170impl std::error::Error for RustCargoConfigurationError {}
171
172fn io_error(path: &Path, error: impl std::fmt::Display) -> RustCargoConfigurationError {
173 RustCargoConfigurationError::Io {
174 path: path.to_owned(),
175 reason: error.to_string(),
176 }
177}
178
179fn command_targets(
180 invocation: &CargoTestInvocation,
181) -> Result<Vec<String>, RustCargoConfigurationError> {
182 let command_position = invocation.command_position().ok_or_else(|| {
183 RustCargoConfigurationError::Invalid("Cargo test runner subcommand is missing".into())
184 })?;
185 toolchain_selector(invocation, command_position)?;
186 if invocation.arguments[..command_position]
187 .iter()
188 .any(|argument| argument == "-Z" || argument.starts_with("-Z"))
189 {
190 return Err(RustCargoConfigurationError::Unsupported(
191 "Cargo runner composition does not yet resolve pre-subcommand -Z configuration semantics exactly"
192 .into(),
193 ));
194 }
195 let mut targets = Vec::new();
196 let mut index = 0;
197 while index < invocation.arguments.len() {
198 let argument = &invocation.arguments[index];
199 if argument == "--config" {
200 if invocation.arguments.get(index + 1).is_none() {
201 return Err(RustCargoConfigurationError::Invalid(
202 "--config has no value".into(),
203 ));
204 }
205 index += 2;
206 continue;
207 }
208 if let Some(value) = argument.strip_prefix("--config=") {
209 if value.is_empty() {
210 return Err(RustCargoConfigurationError::Invalid(
211 "--config has no value".into(),
212 ));
213 }
214 index += 1;
215 continue;
216 }
217 if argument == "--target" {
218 let target = invocation.arguments.get(index + 1).ok_or_else(|| {
219 RustCargoConfigurationError::Invalid("--target has no value".into())
220 })?;
221 targets.push(target.clone());
222 index += 2;
223 continue;
224 }
225 if let Some(target) = argument.strip_prefix("--target=") {
226 if target.is_empty() {
227 return Err(RustCargoConfigurationError::Invalid(
228 "--target has no value".into(),
229 ));
230 }
231 targets.push(target.to_owned());
232 }
233 index += 1;
234 }
235 Ok(targets)
236}
237
238fn command_config_arguments(
239 invocation: &CargoTestInvocation,
240) -> Result<Vec<String>, RustCargoConfigurationError> {
241 let mut values = Vec::new();
242 let mut index = 0;
243 while index < invocation.arguments.len() {
244 let argument = &invocation.arguments[index];
245 if argument == "--config" {
246 let value = invocation.arguments.get(index + 1).ok_or_else(|| {
247 RustCargoConfigurationError::Invalid("--config has no value".into())
248 })?;
249 values.push(value.clone());
250 index += 2;
251 continue;
252 }
253 if let Some(value) = argument.strip_prefix("--config=") {
254 if value.is_empty() {
255 return Err(RustCargoConfigurationError::Invalid(
256 "--config has no value".into(),
257 ));
258 }
259 values.push(value.to_owned());
260 }
261 index += 1;
262 }
263 Ok(values)
264}
265
266fn toolchain_selector(
267 invocation: &CargoTestInvocation,
268 test_position: usize,
269) -> Result<Option<&str>, RustCargoConfigurationError> {
270 let prefix = &invocation.arguments[..test_position];
271 let selectors = prefix
272 .iter()
273 .enumerate()
274 .filter(|(_, argument)| argument.starts_with('+'))
275 .collect::<Vec<_>>();
276 match selectors.as_slice() {
277 [] => Ok(None),
278 [(0, selector)] if selector.len() > 1 => Ok(Some(&selector[1..])),
279 [(0, _)] => Err(RustCargoConfigurationError::Invalid(
280 "the rustup toolchain selector is empty".into(),
281 )),
282 _ => Err(RustCargoConfigurationError::Invalid(
283 "the rustup +toolchain selector must be the first and only selector before the Cargo subcommand"
284 .into(),
285 )),
286 }
287}
288
289fn command_stdout(
290 program: &Path,
291 arguments: &[&str],
292 operation: &str,
293) -> Result<String, RustCargoConfigurationError> {
294 let output = Command::new(program)
295 .args(arguments)
296 .output()
297 .map_err(|error| io_error(program, error))?;
298 if !output.status.success() {
299 return Err(RustCargoConfigurationError::Invalid(format!(
300 "{} failed while {operation} with status {}: {}",
301 program.display(),
302 output
303 .status
304 .code()
305 .map_or_else(|| "signal".into(), |value| value.to_string()),
306 String::from_utf8_lossy(&output.stderr).trim()
307 )));
308 }
309 if !output.stderr.is_empty() {
310 return Err(RustCargoConfigurationError::Invalid(format!(
311 "{} wrote unexpected stderr while {operation}: {}",
312 program.display(),
313 String::from_utf8_lossy(&output.stderr).trim()
314 )));
315 }
316 String::from_utf8(output.stdout).map_err(|_| {
317 RustCargoConfigurationError::Invalid(format!(
318 "{} produced non-UTF-8 output while {operation}",
319 program.display()
320 ))
321 })
322}
323
324fn rustup_program(cargo: &Path) -> PathBuf {
325 cargo
326 .parent()
327 .filter(|parent| !parent.as_os_str().is_empty())
328 .map_or_else(
329 || PathBuf::from(format!("rustup{}", std::env::consts::EXE_SUFFIX)),
330 |parent| parent.join(format!("rustup{}", std::env::consts::EXE_SUFFIX)),
331 )
332}
333
334fn selected_cargo_program(
335 invocation: &CargoTestInvocation,
336) -> Result<OsString, RustCargoConfigurationError> {
337 let command_position = invocation.command_position().ok_or_else(|| {
338 RustCargoConfigurationError::Invalid("Cargo test runner subcommand is missing".into())
339 })?;
340 let Some(selector) = toolchain_selector(invocation, command_position)? else {
341 return Ok(invocation.program.clone().into());
342 };
343 let cargo_proxy = which::which(&invocation.program).map_err(|error| {
344 RustCargoConfigurationError::Invalid(format!(
345 "could not resolve the Cargo proxy {}: {error}",
346 invocation.program
347 ))
348 })?;
349 let rustup = rustup_program(&cargo_proxy);
350 let selected = command_stdout(
351 &rustup,
352 &["which", "--toolchain", selector, "cargo"],
353 "resolving the explicit rustup toolchain's Cargo",
354 )?;
355 let selected = PathBuf::from(selected.trim());
356 if !selected.is_absolute() {
357 return Err(RustCargoConfigurationError::Invalid(format!(
358 "rustup returned a non-absolute Cargo path for +{selector}: {}",
359 selected.display()
360 )));
361 }
362 let selected = fs::canonicalize(&selected).map_err(|error| io_error(&selected, error))?;
363 let metadata = fs::symlink_metadata(&selected).map_err(|error| io_error(&selected, error))?;
364 if !metadata.file_type().is_file() {
365 return Err(RustCargoConfigurationError::Invalid(format!(
366 "rustup returned a non-regular Cargo path for +{selector}: {}",
367 selected.display()
368 )));
369 }
370 let proxy_version = command_stdout(
371 &cargo_proxy,
372 &[&format!("+{selector}"), "-Vv"],
373 "verifying the explicit rustup Cargo selection",
374 )?;
375 let selected_version = command_stdout(
376 &selected,
377 &["-Vv"],
378 "verifying the resolved Cargo executable",
379 )?;
380 if proxy_version != selected_version {
381 return Err(RustCargoConfigurationError::Invalid(format!(
382 "the +{selector} Cargo proxy and rustup's selected Cargo executable disagree"
383 )));
384 }
385 Ok(selected.into_os_string())
386}
387
388fn model_target_config<'a>(
389 model: &'a CargoConfigValue,
390 target: &str,
391) -> Option<&'a CargoConfigValue> {
392 let target_table = model.at(&["target"])?.table()?;
393 if let Some(value) = target_table.get(target) {
394 return Some(value);
395 }
396 let mut value = model.at(&["target"])?;
397 for component in target.split('.') {
398 value = value.table()?.get(component)?;
399 }
400 Some(value)
401}
402
403fn model_exact_runner<'a>(
404 model: &'a CargoConfigValue,
405 target: &str,
406) -> Option<&'a CargoConfigValue> {
407 model_target_config(model, target)?.at(&["runner"])
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
411enum ModelTargetKind {
412 Tuple,
413 Json,
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
417struct ModelTarget {
418 kind: ModelTargetKind,
419 name: String,
420 cargo_argument: String,
421}
422
423fn model_targets(
424 root: &Path,
425 model: &CargoConfigValue,
426 command_targets: Vec<String>,
427 host: &str,
428 inputs: &CargoModelInputs,
429) -> Result<Vec<ModelTarget>, RustCargoConfigurationError> {
430 let convert = |target: &str| {
431 let target = target.trim();
432 if target.is_empty() {
433 return Err(RustCargoConfigurationError::Invalid(
434 "Cargo target was empty".into(),
435 ));
436 }
437 let target = if target == "host-tuple" { host } else { target };
438 if target.ends_with(".json") {
439 let requested = Path::new(target);
440 let requested = if requested.is_absolute() {
441 requested.to_owned()
442 } else {
443 root.join(requested)
444 };
445 let path = fs::canonicalize(&requested).map_err(|error| io_error(&requested, error))?;
446 let name = path
447 .file_stem()
448 .and_then(|stem| stem.to_str())
449 .filter(|stem| !stem.is_empty())
450 .ok_or_else(|| {
451 RustCargoConfigurationError::Invalid(format!(
452 "Cargo target specification has no UTF-8 file stem: {}",
453 path.display()
454 ))
455 })?;
456 let cargo_argument = path.to_str().ok_or_else(|| {
457 RustCargoConfigurationError::Invalid(format!(
458 "Cargo target specification path is not UTF-8: {}",
459 path.display()
460 ))
461 })?;
462 return Ok(ModelTarget {
463 kind: ModelTargetKind::Json,
464 name: name.to_owned(),
465 cargo_argument: cargo_argument.to_owned(),
466 });
467 }
468 Ok(ModelTarget {
469 kind: ModelTargetKind::Tuple,
470 name: target.to_owned(),
471 cargo_argument: target.to_owned(),
472 })
473 };
474 let targets = if !command_targets.is_empty() {
475 command_targets
476 } else if let Some(target) = inputs.environment("CARGO_BUILD_TARGET") {
477 let target = target.clone().into_string().map_err(|_| {
478 RustCargoConfigurationError::Invalid("CARGO_BUILD_TARGET is not UTF-8".into())
479 })?;
480 vec![target]
481 } else if let Some(targets) = model.at(&["build", "target"]) {
482 targets
483 .string_list()
484 .ok_or_else(|| {
485 RustCargoConfigurationError::Invalid(
486 "build.target must be a string or string array".into(),
487 )
488 })?
489 .into_iter()
490 .map(str::to_owned)
491 .collect()
492 } else {
493 vec![host.to_owned()]
494 };
495 let targets = targets
496 .iter()
497 .map(|target| convert(target))
498 .collect::<Result<BTreeSet<_>, _>>()
499 .map(BTreeSet::into_iter)
500 .map(Iterator::collect::<Vec<_>>)?;
501 let mut names = HashMap::new();
502 for target in &targets {
503 if let Some(first) = names.insert(&target.name, &target.cargo_argument) {
504 return Err(RustCargoConfigurationError::Invalid(format!(
505 "Cargo targets {first} and {} have the same configuration identity {:?}",
506 target.cargo_argument, target.name
507 )));
508 }
509 }
510 Ok(targets)
511}
512
513fn environment_runner(
514 target: &str,
515 inputs: &CargoModelInputs,
516) -> Result<Option<CargoConfigValue>, RustCargoConfigurationError> {
517 let mut key = target.replace(['-', '.'], "_");
518 key.make_ascii_uppercase();
519 let key = format!("CARGO_TARGET_{key}_RUNNER");
520 let Some(value) = inputs.environment(&key) else {
521 return Ok(None);
522 };
523 let value = value
524 .clone()
525 .into_string()
526 .map_err(|_| RustCargoConfigurationError::Invalid(format!("{key} is not UTF-8")))?;
527 Ok(Some(CargoConfigValue {
528 kind: CargoConfigKind::String(value),
529 definition: CargoConfigDefinition::Environment(key),
530 }))
531}
532
533fn environment_tool(
534 root: &Path,
535 key: &str,
536 inputs: &CargoModelInputs,
537 empty_disables: bool,
538 non_utf8_is_absent: bool,
539) -> Result<Option<Option<RustCargoRunnerProgram>>, RustCargoConfigurationError> {
540 let Some(value) = inputs.environment(key) else {
541 return Ok(None);
542 };
543 let value = match value.clone().into_string() {
544 Ok(value) => value,
545 Err(_) if non_utf8_is_absent => return Ok(None),
546 Err(_) => {
547 return Err(RustCargoConfigurationError::Invalid(format!(
548 "{key} is not UTF-8"
549 )));
550 }
551 };
552 if empty_disables && value.is_empty() {
553 return Ok(Some(None));
554 }
555 let path = if value.contains('/') || value.contains('\\') {
556 root.join(value)
557 } else {
558 PathBuf::from(value)
559 };
560 Ok(Some(Some(runner_program(root, path)?)))
561}
562
563fn model_tool(
564 root: &Path,
565 value: &CargoConfigValue,
566 empty_disables: bool,
567) -> Result<Option<RustCargoRunnerProgram>, RustCargoConfigurationError> {
568 let path = value
569 .program_path(root)
570 .map_err(|error| RustCargoConfigurationError::Invalid(error.to_string()))?;
571 if empty_disables && path.as_os_str().is_empty() {
572 return Ok(None);
573 }
574 runner_program(root, path).map(Some)
575}
576
577fn selected_compiler_tool(
578 root: &Path,
579 model: &CargoConfigValue,
580 inputs: &CargoModelInputs,
581 model_key: &str,
582 direct_environment: &str,
583 cargo_environment: &str,
584 empty_disables: bool,
585) -> Result<Option<RustCargoRunnerProgram>, RustCargoConfigurationError> {
586 let configured = model.at(&["build", model_key]);
587 if let Some(selected) =
588 environment_tool(root, direct_environment, inputs, empty_disables, true)?
589 {
590 return Ok(selected);
591 }
592 if configured.is_some_and(|value| {
593 matches!(
594 value.definition,
595 CargoConfigDefinition::CliFile(_) | CargoConfigDefinition::CliValue
596 )
597 }) {
598 return model_tool(root, configured.expect("checked above"), empty_disables);
599 }
600 if let Some(selected) =
601 environment_tool(root, cargo_environment, inputs, empty_disables, false)?
602 {
603 return Ok(selected);
604 }
605 configured
606 .map(|value| model_tool(root, value, empty_disables))
607 .transpose()
608 .map(Option::flatten)
609}
610
611fn compiler_command_plan(
612 root: &Path,
613 model: &CargoConfigValue,
614 default_rustc: RustCargoRunnerProgram,
615 inputs: &CargoModelInputs,
616) -> Result<RustCargoCompilerCommandPlan, RustCargoConfigurationError> {
617 let rustc = selected_compiler_tool(
618 root,
619 model,
620 inputs,
621 "rustc",
622 "RUSTC",
623 "CARGO_BUILD_RUSTC",
624 false,
625 )?
626 .unwrap_or(default_rustc);
627 let rustc_wrapper = selected_compiler_tool(
628 root,
629 model,
630 inputs,
631 "rustc-wrapper",
632 "RUSTC_WRAPPER",
633 "CARGO_BUILD_RUSTC_WRAPPER",
634 true,
635 )?;
636 let rustc_workspace_wrapper = selected_compiler_tool(
637 root,
638 model,
639 inputs,
640 "rustc-workspace-wrapper",
641 "RUSTC_WORKSPACE_WRAPPER",
642 "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER",
643 true,
644 )?;
645 Ok(RustCargoCompilerCommandPlan {
646 rustc,
647 rustc_wrapper,
648 rustc_workspace_wrapper,
649 })
650}
651
652fn compiler_host(
653 root: &Path,
654 compiler: &RustCargoCompilerCommandPlan,
655) -> Result<String, RustCargoConfigurationError> {
656 let output = compiler
657 .command(root)
658 .arg("-vV")
659 .output()
660 .map_err(|error| io_error(&compiler.rustc.resolve(root), error))?;
661 if !output.status.success() {
662 return Err(RustCargoConfigurationError::Invalid(format!(
663 "Cargo's configured compiler command failed while resolving its host with status {}: {}",
664 output
665 .status
666 .code()
667 .map_or_else(|| "signal".into(), |value| value.to_string()),
668 String::from_utf8_lossy(&output.stderr).trim()
669 )));
670 }
671 let stdout = String::from_utf8(output.stdout).map_err(|_| {
672 RustCargoConfigurationError::Invalid(
673 "Cargo's configured compiler command produced non-UTF-8 verbose version output".into(),
674 )
675 })?;
676 stdout
677 .lines()
678 .find_map(|line| line.strip_prefix("host: "))
679 .filter(|host| !host.is_empty())
680 .map(str::to_owned)
681 .ok_or_else(|| {
682 RustCargoConfigurationError::Invalid(
683 "Cargo's configured compiler command did not report a host in rustc -vV output"
684 .into(),
685 )
686 })
687}
688
689fn parsed_model_runner(
690 root: &Path,
691 runner: &CargoConfigValue,
692) -> Result<RustCargoUnderlyingRunner, RustCargoConfigurationError> {
693 let (program, arguments) = runner
694 .program_and_arguments(root)
695 .map_err(|error| RustCargoConfigurationError::Invalid(error.to_string()))?;
696 Ok(RustCargoUnderlyingRunner {
697 program: runner_program(root, program)?,
698 arguments,
699 })
700}
701
702fn target_cfg(
703 configuration_root: &Path,
704 execution_root: &Path,
705 compiler: &RustCargoCompilerCommandPlan,
706 target: &ModelTarget,
707) -> Result<Vec<Cfg>, RustCargoConfigurationError> {
708 let target_argument = match target.kind {
709 ModelTargetKind::Tuple => PathBuf::from(&target.cargo_argument),
710 ModelTargetKind::Json => {
711 let source = Path::new(&target.cargo_argument);
712 source.strip_prefix(configuration_root).map_or_else(
713 |_| source.to_owned(),
714 |relative| execution_root.join(relative),
715 )
716 }
717 };
718 let output = compiler
719 .command(execution_root)
720 .args(["--print", "cfg", "--target"])
721 .arg(&target_argument)
722 .output()
723 .map_err(|error| io_error(&compiler.rustc.resolve(execution_root), error))?;
724 if !output.status.success() {
725 return Err(RustCargoConfigurationError::Invalid(format!(
726 "Cargo's configured compiler command failed while resolving cfg values for target {} with status {}: {}",
727 target.cargo_argument,
728 output
729 .status
730 .code()
731 .map_or_else(|| "signal".into(), |value| value.to_string()),
732 String::from_utf8_lossy(&output.stderr).trim()
733 )));
734 }
735 let stdout = String::from_utf8(output.stdout).map_err(|_| {
736 RustCargoConfigurationError::Invalid(format!(
737 "Cargo's configured compiler command produced non-UTF-8 cfg output for target {}",
738 target.cargo_argument
739 ))
740 })?;
741 stdout
742 .lines()
743 .filter(|line| !line.trim().is_empty())
744 .map(|line| {
745 Cfg::from_str(line.trim()).map_err(|error| {
746 RustCargoConfigurationError::Invalid(format!(
747 "rustc emitted an invalid cfg value {line:?}: {error}"
748 ))
749 })
750 })
751 .collect()
752}
753
754fn cfg_runner<'a>(
755 model: &'a CargoConfigValue,
756 cfg: &[Cfg],
757) -> Result<Option<&'a CargoConfigValue>, RustCargoConfigurationError> {
758 let Some(targets) = model.at(&["target"]).and_then(CargoConfigValue::table) else {
759 return Ok(None);
760 };
761 let mut matches = Vec::new();
762 for (key, target) in targets {
763 let Some(expression) = key
764 .strip_prefix("cfg(")
765 .and_then(|key| key.strip_suffix(')'))
766 else {
767 continue;
768 };
769 let expression = CfgExpr::from_str(expression).map_err(|error| {
770 RustCargoConfigurationError::Invalid(format!(
771 "invalid Cargo target cfg key {key:?}: {error}"
772 ))
773 })?;
774 if expression.matches(cfg)
775 && let Some(runner) = target.at(&["runner"])
776 {
777 matches.push((key, runner));
778 }
779 }
780 match matches.as_slice() {
781 [] => Ok(None),
782 [(_, runner)] => Ok(Some(*runner)),
783 [(first, _), (second, _), ..] => Err(RustCargoConfigurationError::Invalid(format!(
784 "several matching instances of target.'cfg(..)'.runner: {first} and {second}"
785 ))),
786 }
787}
788
789fn model_runner(
790 configuration_root: &Path,
791 execution_root: &Path,
792 model: &CargoConfigValue,
793 compiler: &RustCargoCompilerCommandPlan,
794 target: &ModelTarget,
795 inputs: &CargoModelInputs,
796) -> Result<Option<RustCargoUnderlyingRunner>, RustCargoConfigurationError> {
797 let exact = model_exact_runner(model, &target.name);
798 let environment = environment_runner(&target.name, inputs)?;
799 let selected = match exact {
800 Some(value)
801 if matches!(
802 value.definition,
803 CargoConfigDefinition::CliFile(_) | CargoConfigDefinition::CliValue
804 ) =>
805 {
806 Some(value)
807 }
808 _ => environment.as_ref().or(exact),
809 };
810 if let Some(runner) = selected {
811 return parsed_model_runner(configuration_root, runner).map(Some);
812 }
813 let cfg = target_cfg(configuration_root, execution_root, compiler, target)?;
814 cfg_runner(model, &cfg)?
815 .map(|runner| parsed_model_runner(configuration_root, runner))
816 .transpose()
817}
818
819fn runner_program(
820 root: &Path,
821 path: PathBuf,
822) -> Result<RustCargoRunnerProgram, RustCargoConfigurationError> {
823 if path.is_absolute() {
824 if let Ok(relative) = path.strip_prefix(root) {
825 if relative.as_os_str().is_empty()
826 || relative
827 .components()
828 .any(|component| !matches!(component, Component::Normal(_)))
829 {
830 return Err(RustCargoConfigurationError::Invalid(format!(
831 "runner path is not a regular workspace-relative path: {}",
832 path.display()
833 )));
834 }
835 return Ok(RustCargoRunnerProgram::WorkspaceRelative {
836 value: relative.to_owned(),
837 });
838 }
839 return Ok(RustCargoRunnerProgram::Absolute { value: path });
840 }
841 if path.components().count() == 1 {
842 let value = path.into_os_string().into_string().map_err(|_| {
843 RustCargoConfigurationError::Invalid("runner search path is not UTF-8".into())
844 })?;
845 return Ok(RustCargoRunnerProgram::SearchPath { value });
846 }
847 Err(RustCargoConfigurationError::Invalid(format!(
848 "Cargo returned an unresolved relative runner path: {}",
849 path.display()
850 )))
851}
852
853fn resolve_with_inputs(
854 root: &Path,
855 execution_root: &Path,
856 invocation: &CargoTestInvocation,
857 model_inputs: CargoModelInputs,
858) -> Result<RustCargoRunnerPlan, RustCargoConfigurationError> {
859 let root = fs::canonicalize(root).map_err(|error| io_error(root, error))?;
860 let execution_root =
861 fs::canonicalize(execution_root).map_err(|error| io_error(execution_root, error))?;
862 let command_targets = command_targets(invocation)?;
863 let command_config = command_config_arguments(invocation)?;
864 let command_position = invocation.command_position().ok_or_else(|| {
865 RustCargoConfigurationError::Invalid("Cargo test runner subcommand is missing".into())
866 })?;
867 let explicit_toolchain = toolchain_selector(invocation, command_position)?.is_some();
868 let selected_cargo = selected_cargo_program(invocation)?;
869 let model = load_cargo_configuration(&root, model_inputs.cargo_home.clone(), &command_config)
870 .map_err(|error| RustCargoConfigurationError::Invalid(error.to_string()))?;
871 let selected_cargo_path = PathBuf::from(&selected_cargo);
872 let default_rustc = if explicit_toolchain {
873 selected_cargo_path
874 .parent()
875 .filter(|parent| !parent.as_os_str().is_empty())
876 .map(|parent| parent.join(format!("rustc{}", std::env::consts::EXE_SUFFIX)))
877 .filter(|candidate| candidate.is_file())
878 .map(|candidate| runner_program(&root, candidate))
879 .transpose()?
880 .unwrap_or_else(|| RustCargoRunnerProgram::SearchPath {
881 value: format!("rustc{}", std::env::consts::EXE_SUFFIX),
882 })
883 } else {
884 RustCargoRunnerProgram::SearchPath {
885 value: format!("rustc{}", std::env::consts::EXE_SUFFIX),
886 }
887 };
888 let compiler = compiler_command_plan(&root, &model, default_rustc, &model_inputs)?;
889 let host = model_inputs
890 .host_override
891 .clone()
892 .map(Ok)
893 .unwrap_or_else(|| compiler_host(&execution_root, &compiler))?;
894 let targets = model_targets(&root, &model, command_targets, &host, &model_inputs)?;
895 let targets = targets
896 .iter()
897 .map(|target| {
898 Ok(RustCargoTargetRunnerPlan {
899 target: target.name.clone(),
900 underlying_runner: model_runner(
901 &root,
902 &execution_root,
903 &model,
904 &compiler,
905 target,
906 &model_inputs,
907 )?,
908 })
909 })
910 .collect::<Result<Vec<_>, RustCargoConfigurationError>>()?;
911 Ok(RustCargoRunnerPlan { compiler, targets })
912}
913
914pub(crate) fn resolve_cargo_runner_plan(
915 root: &Path,
916 execution_root: &Path,
917 invocation: &CargoTestInvocation,
918) -> Result<RustCargoRunnerPlan, RustCargoConfigurationError> {
919 resolve_with_inputs(
920 root,
921 execution_root,
922 invocation,
923 CargoModelInputs::ambient(root),
924 )
925}
926
927#[cfg(test)]
928mod tests {
929 use std::{
930 sync::atomic::{AtomicU64, Ordering},
931 time::{SystemTime, UNIX_EPOCH},
932 };
933
934 use super::*;
935
936 static FIXTURE_ID: AtomicU64 = AtomicU64::new(0);
937
938 fn fixture() -> PathBuf {
939 let root = std::env::temp_dir().join(format!(
940 "supercov-cargo-configuration-{}-{}-{}",
941 std::process::id(),
942 SystemTime::now()
943 .duration_since(UNIX_EPOCH)
944 .unwrap()
945 .as_nanos(),
946 FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
947 ));
948 fs::create_dir_all(root.join(".cargo/bin with spaces")).unwrap();
949 root
950 }
951
952 fn invocation(arguments: &[&str]) -> CargoTestInvocation {
953 CargoTestInvocation {
954 program: "cargo".into(),
955 kind: crate::rust_test_runner::RustCargoCommandKind::CargoTest,
956 arguments: arguments.iter().map(|value| (*value).into()).collect(),
957 runner_arguments: Vec::new(),
958 }
959 }
960
961 fn model_inputs() -> CargoModelInputs {
962 model_inputs_with([])
963 }
964
965 fn model_inputs_with<const N: usize>(
966 additional: [(OsString, OsString); N],
967 ) -> CargoModelInputs {
968 CargoModelInputs {
969 cargo_home: None,
970 environment: additional
971 .into_iter()
972 .filter_map(|(key, value)| key.into_string().ok().map(|key| (key, value)))
973 .collect(),
974 host_override: Some("aarch64-apple-darwin".into()),
975 }
976 }
977
978 #[test]
979 fn compiler_tools_follow_cargo_cli_and_environment_precedence() {
980 let root = fixture();
981 fs::write(
982 root.join(".cargo/config.toml"),
983 concat!(
984 "[build]\n",
985 "rustc=\"./file-rustc\"\n",
986 "rustc-wrapper=\"./file-wrapper\"\n",
987 "rustc-workspace-wrapper=\"./file-workspace-wrapper\"\n",
988 ),
989 )
990 .unwrap();
991 let cli = [
992 "build.rustc=\"./cli-rustc\"".into(),
993 "build.rustc-wrapper=\"./cli-wrapper\"".into(),
994 "build.rustc-workspace-wrapper=\"./cli-workspace-wrapper\"".into(),
995 ];
996 let model = load_cargo_configuration(&root, None, &cli).unwrap();
997 let plan = compiler_command_plan(
998 &root,
999 &model,
1000 RustCargoRunnerProgram::SearchPath {
1001 value: "rustc".into(),
1002 },
1003 &model_inputs_with([
1004 (OsString::from("RUSTC"), OsString::from("./direct-rustc")),
1005 (
1006 OsString::from("CARGO_BUILD_RUSTC_WRAPPER"),
1007 OsString::from("./cargo-wrapper"),
1008 ),
1009 (OsString::from("RUSTC_WORKSPACE_WRAPPER"), OsString::new()),
1010 ]),
1011 )
1012 .unwrap();
1013 assert_eq!(
1014 plan.rustc,
1015 RustCargoRunnerProgram::WorkspaceRelative {
1016 value: "direct-rustc".into()
1017 }
1018 );
1019 assert_eq!(
1020 plan.rustc_wrapper,
1021 Some(RustCargoRunnerProgram::WorkspaceRelative {
1022 value: "cli-wrapper".into()
1023 })
1024 );
1025 assert_eq!(plan.rustc_workspace_wrapper, None);
1026 fs::remove_dir_all(root).unwrap();
1027 }
1028
1029 #[cfg(unix)]
1030 #[test]
1031 fn direct_non_utf8_tool_environment_falls_back_but_config_environment_is_invalid() {
1032 use std::os::unix::ffi::OsStringExt;
1033
1034 let root = fixture();
1035 fs::write(root.join(".cargo/config.toml"), "").unwrap();
1036 let model =
1037 load_cargo_configuration(&root, None, &["build.rustc=\"./cli-rustc\"".into()]).unwrap();
1038 let plan = compiler_command_plan(
1039 &root,
1040 &model,
1041 RustCargoRunnerProgram::SearchPath {
1042 value: "rustc".into(),
1043 },
1044 &model_inputs_with([(OsString::from("RUSTC"), OsString::from_vec(vec![0xff]))]),
1045 )
1046 .unwrap();
1047 assert_eq!(
1048 plan.rustc,
1049 RustCargoRunnerProgram::WorkspaceRelative {
1050 value: "cli-rustc".into()
1051 }
1052 );
1053
1054 let empty_model = load_cargo_configuration(&root, None, &[]).unwrap();
1055 let error = compiler_command_plan(
1056 &root,
1057 &empty_model,
1058 RustCargoRunnerProgram::SearchPath {
1059 value: "rustc".into(),
1060 },
1061 &model_inputs_with([(
1062 OsString::from("CARGO_BUILD_RUSTC"),
1063 OsString::from_vec(vec![0xff]),
1064 )]),
1065 )
1066 .unwrap_err()
1067 .to_string();
1068 assert!(error.contains("CARGO_BUILD_RUSTC is not UTF-8"), "{error}");
1069 fs::remove_dir_all(root).unwrap();
1070 }
1071
1072 #[test]
1073 fn every_compiler_wrapper_layer_is_preserved_in_the_compiler_plan() {
1074 let root = fixture();
1075 fs::write(
1076 root.join(".cargo/config.toml"),
1077 "[build]\nrustc-wrapper=\"wrapper\"\n",
1078 )
1079 .unwrap();
1080 let model = load_cargo_configuration(&root, None, &[]).unwrap();
1081 let plan = compiler_command_plan(
1082 &root,
1083 &model,
1084 RustCargoRunnerProgram::SearchPath {
1085 value: "rustc".into(),
1086 },
1087 &model_inputs(),
1088 )
1089 .unwrap();
1090 assert_eq!(
1091 plan.rustc_wrapper,
1092 Some(RustCargoRunnerProgram::SearchPath {
1093 value: "wrapper".into()
1094 })
1095 );
1096
1097 fs::write(root.join(".cargo/config.toml"), "").unwrap();
1098 let model = load_cargo_configuration(
1099 &root,
1100 None,
1101 &["build.rustc-workspace-wrapper=\"workspace-wrapper\"".into()],
1102 )
1103 .unwrap();
1104 let plan = compiler_command_plan(
1105 &root,
1106 &model,
1107 RustCargoRunnerProgram::SearchPath {
1108 value: "rustc".into(),
1109 },
1110 &model_inputs(),
1111 )
1112 .unwrap();
1113 assert_eq!(
1114 plan.rustc_workspace_wrapper,
1115 Some(RustCargoRunnerProgram::SearchPath {
1116 value: "workspace-wrapper".into()
1117 })
1118 );
1119
1120 let model = load_cargo_configuration(&root, None, &[]).unwrap();
1121 let plan = compiler_command_plan(
1122 &root,
1123 &model,
1124 RustCargoRunnerProgram::SearchPath {
1125 value: "rustc".into(),
1126 },
1127 &model_inputs_with([(
1128 OsString::from("RUSTC_WRAPPER"),
1129 OsString::from("environment-wrapper"),
1130 )]),
1131 )
1132 .unwrap();
1133 assert_eq!(
1134 plan.rustc_wrapper,
1135 Some(RustCargoRunnerProgram::SearchPath {
1136 value: "environment-wrapper".into()
1137 })
1138 );
1139 fs::remove_dir_all(root).unwrap();
1140 }
1141
1142 #[cfg(unix)]
1143 #[test]
1144 fn included_build_rustc_drives_host_and_cfg_runner_selection() {
1145 use std::os::unix::fs::PermissionsExt;
1146
1147 let root = fixture();
1148 let rustc = which::which("rustc").unwrap();
1149 let proxy = root.join("compiler-proxy");
1150 let log = root.join("compiler.log");
1151 fs::write(
1152 &proxy,
1153 format!(
1154 "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"{}\"\nexec \"{}\" \"$@\"\n",
1155 log.display(),
1156 rustc.display()
1157 ),
1158 )
1159 .unwrap();
1160 fs::set_permissions(&proxy, fs::Permissions::from_mode(0o755)).unwrap();
1161 fs::write(
1162 root.join(".cargo/compiler.toml"),
1163 concat!(
1164 "[build]\nrustc=\"./compiler-proxy\"\n",
1165 "[target.'cfg(unix)']\nrunner=[\"cfg-runner\",\"--cfg\"]\n",
1166 ),
1167 )
1168 .unwrap();
1169 fs::write(
1170 root.join(".cargo/config.toml"),
1171 "include=[\"compiler.toml\"]\n",
1172 )
1173 .unwrap();
1174 let mut inputs = model_inputs();
1175 inputs.host_override = None;
1176 let plan = resolve_with_inputs(&root, &root, &invocation(&["test"]), inputs).unwrap();
1177 assert_eq!(
1178 plan.compiler.rustc,
1179 RustCargoRunnerProgram::WorkspaceRelative {
1180 value: "compiler-proxy".into()
1181 }
1182 );
1183 assert_eq!(
1184 plan.targets[0].underlying_runner,
1185 Some(RustCargoUnderlyingRunner {
1186 program: RustCargoRunnerProgram::SearchPath {
1187 value: "cfg-runner".into()
1188 },
1189 arguments: vec!["--cfg".into()]
1190 })
1191 );
1192 let invocations = fs::read_to_string(&log).unwrap();
1193 assert!(invocations.lines().any(|line| line == "-vV"));
1194 assert!(
1195 invocations
1196 .lines()
1197 .any(|line| line.contains("--print cfg --target"))
1198 );
1199 fs::remove_dir_all(root).unwrap();
1200 }
1201
1202 #[test]
1203 fn exact_target_beats_cfg_and_environment_beats_both() {
1204 let root = fixture();
1205 fs::write(
1206 root.join(".cargo/config.toml"),
1207 concat!(
1208 "[target.'cfg(target_vendor = \"apple\")']\n",
1209 "runner=[\"cfg-runner\",\"--cfg\"]\n",
1210 "[target.aarch64-apple-darwin]\n",
1211 "runner=\"exact-runner --exact\"\n",
1212 ),
1213 )
1214 .unwrap();
1215 let plan =
1216 resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs()).unwrap();
1217 assert_eq!(
1218 plan.targets[0].underlying_runner,
1219 Some(RustCargoUnderlyingRunner {
1220 program: RustCargoRunnerProgram::SearchPath {
1221 value: "exact-runner".into(),
1222 },
1223 arguments: vec!["--exact".into()],
1224 })
1225 );
1226 let plan = resolve_with_inputs(
1227 &root,
1228 &root,
1229 &invocation(&["test"]),
1230 model_inputs_with([(
1231 OsString::from("CARGO_TARGET_AARCH64_APPLE_DARWIN_RUNNER"),
1232 OsString::from("environment-runner --environment"),
1233 )]),
1234 )
1235 .unwrap();
1236 assert_eq!(
1237 plan.targets[0].underlying_runner,
1238 Some(RustCargoUnderlyingRunner {
1239 program: RustCargoRunnerProgram::SearchPath {
1240 value: "environment-runner".into(),
1241 },
1242 arguments: vec!["--environment".into()],
1243 })
1244 );
1245 fs::remove_dir_all(root).unwrap();
1246 }
1247
1248 #[test]
1249 fn resolves_exact_array_runner_and_relocates_workspace_program() {
1250 let root = fixture();
1251 fs::write(
1252 root.join(".cargo/config.toml"),
1253 "[target.aarch64-apple-darwin]\nrunner=[\"bin with spaces/runner\",\"--fixed\"]\n",
1254 )
1255 .unwrap();
1256 let plan =
1257 resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs()).unwrap();
1258 assert_eq!(plan.targets[0].target, "aarch64-apple-darwin");
1259 assert_eq!(
1260 plan.targets[0].underlying_runner,
1261 Some(RustCargoUnderlyingRunner {
1262 program: RustCargoRunnerProgram::WorkspaceRelative {
1263 value: PathBuf::from("bin with spaces/runner")
1264 },
1265 arguments: vec!["--fixed".into()],
1266 })
1267 );
1268 fs::remove_dir_all(root).unwrap();
1269 }
1270
1271 #[test]
1272 fn ordinary_files_use_cargo_195_runner_parsing_and_duplicate_cfg_rules() {
1273 let root = fixture();
1274 fs::write(
1275 root.join(".cargo/config.toml"),
1276 "[target.aarch64-apple-darwin]\nrunner=\"runner\\t--one\\n--two\"\n",
1277 )
1278 .unwrap();
1279 let plan =
1280 resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs()).unwrap();
1281 assert_eq!(
1282 plan.targets[0].underlying_runner,
1283 Some(RustCargoUnderlyingRunner {
1284 program: RustCargoRunnerProgram::SearchPath {
1285 value: "runner".into()
1286 },
1287 arguments: vec!["--one".into(), "--two".into()]
1288 })
1289 );
1290 fs::write(
1291 root.join(".cargo/config.toml"),
1292 concat!(
1293 "[target.'cfg(target_vendor = \"apple\")']\nrunner=\"vendor-cfg\"\n",
1294 "[target.'cfg(target_os = \"macos\")']\nrunner=\"os-cfg\"\n",
1295 ),
1296 )
1297 .unwrap();
1298 let error = resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs())
1299 .unwrap_err()
1300 .to_string();
1301 assert!(error.contains("several matching instances"), "{error}");
1302 fs::remove_dir_all(root).unwrap();
1303 }
1304
1305 #[test]
1306 fn cargo_duplicate_and_host_tuple_targets_collapse_before_runner_selection() {
1307 let root = fixture();
1308 fs::write(
1309 root.join(".cargo/config.toml"),
1310 "[target.aarch64-apple-darwin]\nrunner=\"runner\"\n",
1311 )
1312 .unwrap();
1313 let plan = resolve_with_inputs(
1314 &root,
1315 &root,
1316 &invocation(&[
1317 "test",
1318 "--target=host-tuple",
1319 "--target=aarch64-apple-darwin",
1320 "--target=aarch64-apple-darwin",
1321 ]),
1322 model_inputs(),
1323 )
1324 .unwrap();
1325 assert_eq!(plan.targets[0].target, "aarch64-apple-darwin");
1326 assert_eq!(
1327 plan.targets[0].underlying_runner,
1328 Some(RustCargoUnderlyingRunner {
1329 program: RustCargoRunnerProgram::SearchPath {
1330 value: "runner".into()
1331 },
1332 arguments: Vec::new()
1333 })
1334 );
1335 fs::remove_dir_all(root).unwrap();
1336 }
1337
1338 #[test]
1339 fn custom_target_paths_are_project_relative_and_name_collisions_fail_closed() {
1340 let root = fixture();
1341 fs::create_dir_all(root.join("targets/first")).unwrap();
1342 fs::create_dir_all(root.join("targets/second")).unwrap();
1343 fs::write(root.join("targets/first/custom.json"), "{}").unwrap();
1344 fs::write(root.join("targets/second/custom.json"), "{}").unwrap();
1345 let model = CargoConfigValue {
1346 kind: CargoConfigKind::Table(std::collections::BTreeMap::new()),
1347 definition: CargoConfigDefinition::BuiltIn,
1348 };
1349 let inputs = model_inputs();
1350 let target = model_targets(
1351 &root,
1352 &model,
1353 vec!["targets/first/custom.json".into()],
1354 "aarch64-apple-darwin",
1355 &inputs,
1356 )
1357 .unwrap();
1358 assert_eq!(target[0].name, "custom");
1359 assert_eq!(
1360 Path::new(&target[0].cargo_argument),
1361 fs::canonicalize(root.join("targets/first/custom.json")).unwrap()
1362 );
1363 let error = model_targets(
1364 &root,
1365 &model,
1366 vec![
1367 "targets/first/custom.json".into(),
1368 "targets/second/custom.json".into(),
1369 ],
1370 "aarch64-apple-darwin",
1371 &inputs,
1372 )
1373 .unwrap_err()
1374 .to_string();
1375 assert!(error.contains("same configuration identity"), "{error}");
1376 fs::remove_dir_all(root).unwrap();
1377 }
1378
1379 #[test]
1380 fn direct_cargo_path_keeps_cargos_default_rustc_search_semantics() {
1381 let root = fixture();
1382 fs::write(root.join(".cargo/config.toml"), "").unwrap();
1383 let cargo = which::which("cargo").unwrap();
1384 let plan = resolve_with_inputs(
1385 &root,
1386 &root,
1387 &CargoTestInvocation {
1388 program: cargo.to_string_lossy().into_owned(),
1389 kind: crate::rust_test_runner::RustCargoCommandKind::CargoTest,
1390 arguments: vec!["test".into()],
1391 runner_arguments: Vec::new(),
1392 },
1393 model_inputs(),
1394 )
1395 .unwrap();
1396 assert_eq!(
1397 plan.compiler.rustc,
1398 RustCargoRunnerProgram::SearchPath {
1399 value: format!("rustc{}", std::env::consts::EXE_SUFFIX)
1400 }
1401 );
1402 fs::remove_dir_all(root).unwrap();
1403 }
1404
1405 #[test]
1406 fn resolves_an_explicit_installed_rustup_toolchain_before_target_configuration() {
1407 let root = fixture();
1408 let rustc = Command::new("rustc")
1411 .args(["+1.95.0", "-vV"])
1412 .output()
1413 .unwrap();
1414 assert!(
1415 rustc.status.success(),
1416 "{}",
1417 String::from_utf8_lossy(&rustc.stderr)
1418 );
1419 let host = "aarch64-apple-darwin";
1425 fs::write(
1426 root.join(".cargo/config.toml"),
1427 format!("[target.{host}]\nrunner=[\"selected-runner\",\"--selected\"]\n"),
1428 )
1429 .unwrap();
1430 let plan = resolve_with_inputs(
1431 &root,
1432 &root,
1433 &invocation(&["+1.95.0", "test"]),
1434 model_inputs(),
1435 )
1436 .unwrap();
1437 let rustc_executable = format!("rustc{}", std::env::consts::EXE_SUFFIX);
1438 assert!(matches!(
1439 &plan.compiler.rustc,
1440 RustCargoRunnerProgram::Absolute { value }
1441 if value.file_name().and_then(|name| name.to_str())
1442 == Some(rustc_executable.as_str())
1443 ));
1444 assert_eq!(plan.targets[0].target, host);
1445 assert_eq!(
1446 plan.targets[0].underlying_runner,
1447 Some(RustCargoUnderlyingRunner {
1448 program: RustCargoRunnerProgram::SearchPath {
1449 value: "selected-runner".into(),
1450 },
1451 arguments: vec!["--selected".into()],
1452 })
1453 );
1454 fs::remove_dir_all(root).unwrap();
1455 }
1456
1457 #[test]
1458 fn resolves_include_cli_cfg_and_multiple_target_runners_but_rejects_open_surfaces() {
1459 let root = fixture();
1460 fs::write(
1461 root.join(".cargo/extra.toml"),
1462 "[target.aarch64-apple-darwin]\nrunner=[\"included\",\"--included\"]\n",
1463 )
1464 .unwrap();
1465 fs::write(
1466 root.join(".cargo/config.toml"),
1467 "include=[\"extra.toml\"]\n",
1468 )
1469 .unwrap();
1470 assert_eq!(
1471 resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs(),)
1472 .unwrap()
1473 .targets[0]
1474 .underlying_runner,
1475 Some(RustCargoUnderlyingRunner {
1476 program: RustCargoRunnerProgram::SearchPath {
1477 value: "included".into()
1478 },
1479 arguments: vec!["--included".into()]
1480 })
1481 );
1482 assert_eq!(
1483 resolve_with_inputs(
1484 &root,
1485 &root,
1486 &invocation(&[
1487 "test",
1488 "--config",
1489 "target.aarch64-apple-darwin.runner=[\"cli\",\"--cli\"]",
1490 ]),
1491 model_inputs_with([(
1492 OsString::from("CARGO_TARGET_AARCH64_APPLE_DARWIN_RUNNER"),
1493 OsString::from("environment"),
1494 )]),
1495 )
1496 .unwrap()
1497 .targets[0]
1498 .underlying_runner,
1499 Some(RustCargoUnderlyingRunner {
1500 program: RustCargoRunnerProgram::SearchPath {
1501 value: "cli".into()
1502 },
1503 arguments: vec!["--cli".into()]
1504 })
1505 );
1506 let multi_target_environment = [
1507 (
1508 OsString::from("CARGO_TARGET_A_RUNNER"),
1509 OsString::from("runner-a"),
1510 ),
1511 (
1512 OsString::from("CARGO_TARGET_B_RUNNER"),
1513 OsString::from("runner-b"),
1514 ),
1515 ];
1516 let plan = resolve_with_inputs(
1517 &root,
1518 &root,
1519 &invocation(&["test", "--target=a", "--target=b"]),
1520 model_inputs_with(multi_target_environment),
1521 );
1522 let plan = plan.unwrap();
1523 assert_eq!(
1524 plan.targets
1525 .iter()
1526 .map(|target| target.target.as_str())
1527 .collect::<Vec<_>>(),
1528 ["a", "b"]
1529 );
1530 assert_eq!(
1531 plan.targets
1532 .iter()
1533 .map(|target| {
1534 target
1535 .underlying_runner
1536 .as_ref()
1537 .map(|runner| &runner.program)
1538 })
1539 .collect::<Vec<_>>(),
1540 [
1541 Some(&RustCargoRunnerProgram::SearchPath {
1542 value: "runner-a".into()
1543 }),
1544 Some(&RustCargoRunnerProgram::SearchPath {
1545 value: "runner-b".into()
1546 })
1547 ]
1548 );
1549 fs::write(
1550 root.join(".cargo/extra.toml"),
1551 "[target.'cfg(target_vendor = \"apple\")']\nrunner=\"included-cfg\"\n",
1552 )
1553 .unwrap();
1554 assert_eq!(
1555 resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs(),)
1556 .unwrap()
1557 .targets[0]
1558 .underlying_runner,
1559 Some(RustCargoUnderlyingRunner {
1560 program: RustCargoRunnerProgram::SearchPath {
1561 value: "included-cfg".into()
1562 },
1563 arguments: Vec::new()
1564 })
1565 );
1566 fs::write(
1567 root.join(".cargo/extra.toml"),
1568 concat!(
1569 "[target.'cfg(target_vendor = \"apple\")']\nrunner=\"vendor-cfg\"\n",
1570 "[target.'cfg(target_os = \"macos\")']\nrunner=\"os-cfg\"\n",
1571 ),
1572 )
1573 .unwrap();
1574 let error = resolve_with_inputs(&root, &root, &invocation(&["test"]), model_inputs())
1575 .unwrap_err()
1576 .to_string();
1577 assert!(error.contains("several matching instances"), "{error}");
1578 assert!(
1579 command_targets(&invocation(&["--quiet", "+nightly", "test"]))
1580 .unwrap_err()
1581 .to_string()
1582 .contains("must be the first")
1583 );
1584 assert!(
1585 resolve_with_inputs(
1586 &root,
1587 &root,
1588 &invocation(&["-Ztarget-applies-to-host", "test"]),
1589 model_inputs(),
1590 )
1591 .unwrap_err()
1592 .to_string()
1593 .contains("-Z configuration semantics")
1594 );
1595 fs::remove_dir_all(root).unwrap();
1596 }
1597}