1use crate::{
7 cargo_config::{CargoConfig, CargoConfigSource, CargoConfigs, DiscoveredConfig, Runner},
8 errors::TargetRunnerError,
9 platform::BuildPlatforms,
10};
11use camino::{Utf8Path, Utf8PathBuf};
12use nextest_metadata::BuildPlatform;
13use std::fmt;
14use target_spec::Platform;
15
16#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct TargetRunner {
20 host: Option<PlatformRunner>,
21 target: Option<PlatformRunner>,
22}
23
24impl TargetRunner {
25 pub fn new(
29 configs: &CargoConfigs,
30 build_platforms: &BuildPlatforms,
31 ) -> Result<Self, TargetRunnerError> {
32 let host = PlatformRunner::by_precedence(configs, &build_platforms.host.platform)?;
33 let target = match &build_platforms.target {
34 Some(target) => PlatformRunner::by_precedence(configs, &target.triple.platform)?,
35 None => host.clone(),
36 };
37
38 Ok(Self { host, target })
39 }
40
41 pub fn empty() -> Self {
43 Self {
44 host: None,
45 target: None,
46 }
47 }
48
49 #[inline]
51 pub fn target(&self) -> Option<&PlatformRunner> {
52 self.target.as_ref()
53 }
54
55 #[inline]
57 pub fn host(&self) -> Option<&PlatformRunner> {
58 self.host.as_ref()
59 }
60
61 #[inline]
63 pub fn for_build_platform(&self, build_platform: BuildPlatform) -> Option<&PlatformRunner> {
64 match build_platform {
65 BuildPlatform::Target => self.target(),
66 BuildPlatform::Host => self.host(),
67 }
68 }
69
70 #[inline]
72 pub fn all_build_platforms(&self) -> [(BuildPlatform, Option<&PlatformRunner>); 2] {
73 [
74 (BuildPlatform::Target, self.target()),
75 (BuildPlatform::Host, self.host()),
76 ]
77 }
78}
79
80#[derive(Clone, Debug, Eq, PartialEq)]
84pub struct PlatformRunner {
85 runner_binary: Utf8PathBuf,
86 args: Vec<String>,
87 source: PlatformRunnerSource,
88}
89
90impl PlatformRunner {
91 pub fn debug_new(
93 runner_binary: Utf8PathBuf,
94 args: Vec<String>,
95 source: PlatformRunnerSource,
96 ) -> Self {
97 Self {
98 runner_binary,
99 args,
100 source,
101 }
102 }
103
104 fn by_precedence(
105 configs: &CargoConfigs,
106 platform: &Platform,
107 ) -> Result<Option<Self>, TargetRunnerError> {
108 Self::find_config(configs, platform)
109 }
110
111 #[doc(hidden)]
116 pub fn find_config(
117 configs: &CargoConfigs,
118 target: &Platform,
119 ) -> Result<Option<Self>, TargetRunnerError> {
120 for discovered_config in configs.discovered_configs() {
125 match discovered_config {
126 DiscoveredConfig::CliOption { config, source }
127 | DiscoveredConfig::File { config, source } => {
128 if let Some(runner) =
129 Self::from_cli_option_or_file(target, config, source, configs.cwd())?
130 {
131 return Ok(Some(runner));
132 }
133 }
134 DiscoveredConfig::Env => {
135 if let Some(tr) = Self::from_env(Self::runner_env_var(target), configs.cwd())? {
138 return Ok(Some(tr));
139 }
140 }
141 }
142 }
143
144 Ok(None)
145 }
146
147 fn from_cli_option_or_file(
148 target: &target_spec::Platform,
149 config: &CargoConfig,
150 source: &CargoConfigSource,
151 cwd: &Utf8Path,
152 ) -> Result<Option<Self>, TargetRunnerError> {
153 if let Some(targets) = &config.target {
154 if let Some(parent) = targets.get(target.triple_str()) {
156 if let Some(runner) = &parent.runner {
157 return Ok(Some(Self::parse_runner(
158 PlatformRunnerSource::CargoConfig {
159 source: source.clone(),
160 target_table: target.triple_str().into(),
161 },
162 runner.clone(),
163 cwd,
164 )?));
165 }
166 }
167
168 for (cfg, runner) in targets.iter().filter_map(|(k, v)| match &v.runner {
173 Some(runner) if k.starts_with("cfg(") => Some((k, runner)),
174 _ => None,
175 }) {
176 let expr = match target_spec::TargetSpecExpression::new(cfg) {
178 Ok(expr) => expr,
179 Err(_err) => continue,
180 };
181
182 if expr.eval(target) == Some(true) {
183 return Ok(Some(Self::parse_runner(
184 PlatformRunnerSource::CargoConfig {
185 source: source.clone(),
186 target_table: cfg.clone(),
187 },
188 runner.clone(),
189 cwd,
190 )?));
191 }
192 }
193 }
194
195 Ok(None)
196 }
197
198 fn from_env(env_key: String, cwd: &Utf8Path) -> Result<Option<Self>, TargetRunnerError> {
199 if let Some(runner_var) = std::env::var_os(&env_key) {
200 let runner = runner_var
201 .into_string()
202 .map_err(|_osstr| TargetRunnerError::InvalidEnvironmentVar(env_key.clone()))?;
203 Self::parse_runner(
204 PlatformRunnerSource::Env(env_key),
205 Runner::Simple(runner),
206 cwd,
207 )
208 .map(Some)
209 } else {
210 Ok(None)
211 }
212 }
213
214 #[doc(hidden)]
216 pub fn runner_env_var(target: &Platform) -> String {
217 let triple_str = target.triple_str().to_ascii_uppercase().replace('-', "_");
218 format!("CARGO_TARGET_{triple_str}_RUNNER")
219 }
220
221 fn parse_runner(
222 source: PlatformRunnerSource,
223 runner: Runner,
224 cwd: &Utf8Path,
225 ) -> Result<Self, TargetRunnerError> {
226 let (runner_binary, args) = match runner {
227 Runner::Simple(value) => {
228 let mut runner_iter = value.split_whitespace();
231
232 let runner_binary =
233 runner_iter
234 .next()
235 .ok_or_else(|| TargetRunnerError::BinaryNotSpecified {
236 key: source.clone(),
237 value: value.clone(),
238 })?;
239 let args = runner_iter.map(String::from).collect();
240 (
241 Self::normalize_runner(runner_binary, source.resolve_dir(cwd)),
242 args,
243 )
244 }
245 Runner::List(mut values) => {
246 if values.is_empty() {
247 return Err(TargetRunnerError::BinaryNotSpecified {
248 key: source,
249 value: String::new(),
250 });
251 } else {
252 let runner_binary = values.remove(0);
253 (
254 Self::normalize_runner(&runner_binary, source.resolve_dir(cwd)),
255 values,
256 )
257 }
258 }
259 };
260
261 Ok(Self {
262 runner_binary,
263 args,
264 source,
265 })
266 }
267
268 fn normalize_runner(runner_binary: &str, resolve_dir: &Utf8Path) -> Utf8PathBuf {
270 let is_path =
271 runner_binary.contains('/') || (cfg!(windows) && runner_binary.contains('\\'));
272 if is_path {
273 resolve_dir.join(runner_binary)
274 } else {
275 runner_binary.into()
277 }
278 }
279
280 #[inline]
287 pub fn binary(&self) -> &str {
288 self.runner_binary.as_str()
289 }
290
291 #[inline]
293 pub fn args(&self) -> impl Iterator<Item = &str> {
294 self.args.iter().map(AsRef::as_ref)
295 }
296
297 #[inline]
299 pub fn source(&self) -> &PlatformRunnerSource {
300 &self.source
301 }
302}
303
304#[derive(Clone, Debug, PartialEq, Eq)]
308pub enum PlatformRunnerSource {
309 Env(String),
311
312 CargoConfig {
315 source: CargoConfigSource,
317
318 target_table: String,
324 },
325}
326
327impl PlatformRunnerSource {
328 fn resolve_dir<'a>(&'a self, cwd: &'a Utf8Path) -> &'a Utf8Path {
330 match self {
331 Self::Env(_) => cwd,
332 Self::CargoConfig { source, .. } => source.resolve_dir(cwd),
333 }
334 }
335}
336
337impl fmt::Display for PlatformRunnerSource {
338 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339 match self {
340 Self::Env(var) => {
341 write!(f, "environment variable `{var}`")
342 }
343 Self::CargoConfig {
344 source: CargoConfigSource::CliOption,
345 target_table,
346 } => {
347 write!(f, "`target.{target_table}.runner` specified by `--config`")
348 }
349 Self::CargoConfig {
350 source: CargoConfigSource::File(path),
351 target_table,
352 } => {
353 write!(f, "`target.{target_table}.runner` within `{path}`")
354 }
355 }
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use camino_tempfile::Utf8TempDir;
363 use color_eyre::eyre::{Context, Result};
364 use target_spec::TargetFeatures;
365
366 #[test]
367 fn test_find_config() {
368 let dir = setup_temp_dir().unwrap();
369 let dir_path = dir.path().canonicalize_utf8().unwrap();
370 let dir_foo_path = dir_path.join("foo");
371 let dir_foo_bar_path = dir_foo_path.join("bar");
372
373 assert_eq!(
377 find_config(
378 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
379 &[],
380 &dir_foo_bar_path,
381 &dir_path,
382 ),
383 Some(PlatformRunner {
384 runner_binary: "wine".into(),
385 args: vec!["--test-arg".into()],
386 source: PlatformRunnerSource::CargoConfig {
387 source: CargoConfigSource::File(dir_path.join("foo/bar/.cargo/config.toml")),
388 target_table: "x86_64-pc-windows-msvc".into()
389 },
390 }),
391 );
392
393 assert_eq!(
394 find_config(
395 Platform::new("x86_64-pc-windows-gnu", TargetFeatures::Unknown).unwrap(),
396 &[],
397 &dir_foo_bar_path,
398 &dir_path,
399 ),
400 Some(PlatformRunner {
401 runner_binary: "wine2".into(),
402 args: vec![],
403 source: PlatformRunnerSource::CargoConfig {
404 source: CargoConfigSource::File(dir_path.join("foo/bar/.cargo/config.toml")),
405 target_table: "cfg(windows)".into()
406 },
407 }),
408 );
409
410 assert_eq!(
411 find_config(
412 Platform::new("x86_64-unknown-linux-gnu", TargetFeatures::Unknown).unwrap(),
413 &[],
414 &dir_foo_bar_path,
415 &dir_path,
416 ),
417 Some(PlatformRunner {
418 runner_binary: dir_path.join("unix-runner"),
419 args: vec![],
420 source: PlatformRunnerSource::CargoConfig {
421 source: CargoConfigSource::File(dir_path.join(".cargo/config")),
422 target_table: "cfg(unix)".into()
423 },
424 }),
425 );
426
427 assert_eq!(
431 find_config(
432 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
433 &[],
434 &dir_foo_path,
435 &dir_path,
436 ),
437 Some(PlatformRunner {
438 runner_binary: dir_path.join("../parent-wine"),
439 args: vec![],
440 source: PlatformRunnerSource::CargoConfig {
441 source: CargoConfigSource::File(dir_path.join(".cargo/config")),
442 target_table: "x86_64-pc-windows-msvc".into()
443 },
444 }),
445 );
446
447 assert_eq!(
448 find_config(
449 Platform::new("x86_64-pc-windows-gnu", TargetFeatures::Unknown).unwrap(),
450 &[],
451 &dir_foo_path,
452 &dir_path,
453 ),
454 None,
455 );
456
457 assert_eq!(
461 find_config(
462 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
463 &[],
464 &dir_path,
465 &dir_path,
466 ),
467 Some(PlatformRunner {
468 runner_binary: dir_path.join("../parent-wine"),
469 args: vec![],
470 source: PlatformRunnerSource::CargoConfig {
471 source: CargoConfigSource::File(dir_path.join(".cargo/config")),
472 target_table: "x86_64-pc-windows-msvc".into()
473 },
474 }),
475 );
476
477 assert_eq!(
478 find_config(
479 Platform::new("x86_64-pc-windows-gnu", TargetFeatures::Unknown).unwrap(),
480 &[],
481 &dir_path,
482 &dir_path,
483 ),
484 None,
485 );
486
487 assert_eq!(
491 find_config(
492 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
493 &["target.'cfg(windows)'.runner='windows-runner'"],
494 &dir_path,
495 &dir_path,
496 ),
497 Some(PlatformRunner {
498 runner_binary: "windows-runner".into(),
499 args: vec![],
500 source: PlatformRunnerSource::CargoConfig {
501 source: CargoConfigSource::CliOption,
502 target_table: "cfg(windows)".into()
503 },
504 }),
505 );
506
507 assert_eq!(
508 find_config(
509 Platform::new("x86_64-pc-windows-gnu", TargetFeatures::Unknown).unwrap(),
510 &["target.'cfg(windows)'.runner='windows-runner'"],
511 &dir_path,
512 &dir_path,
513 ),
514 Some(PlatformRunner {
515 runner_binary: "windows-runner".into(),
516 args: vec![],
517 source: PlatformRunnerSource::CargoConfig {
518 source: CargoConfigSource::CliOption,
519 target_table: "cfg(windows)".into()
520 },
521 }),
522 );
523
524 assert_eq!(
526 find_config(
527 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
528 &["target.'cfg(unix)'.runner='unix-runner'"],
529 &dir_path,
530 &dir_path,
531 ),
532 Some(PlatformRunner {
533 runner_binary: dir_path.join("../parent-wine"),
534 args: vec![],
535 source: PlatformRunnerSource::CargoConfig {
536 source: CargoConfigSource::File(dir_path.join(".cargo/config")),
537 target_table: "x86_64-pc-windows-msvc".into()
538 },
539 }),
540 );
541
542 assert_eq!(
543 find_config(
544 Platform::new("x86_64-pc-windows-gnu", TargetFeatures::Unknown).unwrap(),
545 &["target.'cfg(unix)'.runner='unix-runner'"],
546 &dir_path,
547 &dir_path,
548 ),
549 None,
550 );
551
552 assert_eq!(
554 find_config(
555 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
556 &[
557 "target.'cfg(windows)'.runner='windows-runner'",
558 "target.'cfg(all())'.runner='all-runner'"
559 ],
560 &dir_path,
561 &dir_path,
562 ),
563 Some(PlatformRunner {
564 runner_binary: "windows-runner".into(),
565 args: vec![],
566 source: PlatformRunnerSource::CargoConfig {
567 source: CargoConfigSource::CliOption,
568 target_table: "cfg(windows)".into()
569 },
570 }),
571 );
572
573 assert_eq!(
574 find_config(
575 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
576 &[
577 "target.'cfg(all())'.runner='./all-runner'",
578 "target.'cfg(windows)'.runner='windows-runner'",
579 ],
580 &dir_path,
581 &dir_path,
582 ),
583 Some(PlatformRunner {
584 runner_binary: dir_path.join("all-runner"),
585 args: vec![],
586 source: PlatformRunnerSource::CargoConfig {
587 source: CargoConfigSource::CliOption,
588 target_table: "cfg(all())".into()
589 },
590 }),
591 );
592 }
593
594 fn setup_temp_dir() -> Result<Utf8TempDir> {
595 let dir = camino_tempfile::Builder::new()
596 .tempdir()
597 .wrap_err("error creating tempdir")?;
598
599 std::fs::create_dir_all(dir.path().join(".cargo"))
600 .wrap_err("error creating .cargo subdir")?;
601 std::fs::create_dir_all(dir.path().join("foo/bar/.cargo"))
602 .wrap_err("error creating foo/bar/.cargo subdir")?;
603
604 std::fs::write(dir.path().join(".cargo/config"), CARGO_CONFIG_CONTENTS)
605 .wrap_err("error writing .cargo/config")?;
606 std::fs::write(
607 dir.path().join("foo/bar/.cargo/config.toml"),
608 FOO_BAR_CARGO_CONFIG_CONTENTS,
609 )
610 .wrap_err("error writing foo/bar/.cargo/config.toml")?;
611
612 Ok(dir)
613 }
614
615 fn find_config(
616 platform: Platform,
617 cli_configs: &[&str],
618 cwd: &Utf8Path,
619 terminate_search_at: &Utf8Path,
620 ) -> Option<PlatformRunner> {
621 let configs =
622 CargoConfigs::new_with_isolation(cli_configs, cwd, terminate_search_at, Vec::new())
623 .unwrap();
624 PlatformRunner::find_config(&configs, &platform).unwrap()
625 }
626
627 static CARGO_CONFIG_CONTENTS: &str = r#"
628 [target.x86_64-pc-windows-msvc]
629 runner = "../parent-wine"
630
631 [target.'cfg(unix)']
632 runner = "./unix-runner"
633 "#;
634
635 static FOO_BAR_CARGO_CONFIG_CONTENTS: &str = r#"
636 [target.x86_64-pc-windows-msvc]
637 runner = ["wine", "--test-arg"]
638
639 [target.'cfg(windows)']
640 runner = "wine2"
641 "#;
642}