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 fn by_precedence(
92 configs: &CargoConfigs,
93 platform: &Platform,
94 ) -> Result<Option<Self>, TargetRunnerError> {
95 Self::find_config(configs, platform)
96 }
97
98 #[doc(hidden)]
103 pub fn find_config(
104 configs: &CargoConfigs,
105 target: &Platform,
106 ) -> Result<Option<Self>, TargetRunnerError> {
107 for discovered_config in configs.discovered_configs() {
112 match discovered_config {
113 DiscoveredConfig::CliOption { config, source }
114 | DiscoveredConfig::File { config, source } => {
115 if let Some(runner) =
116 Self::from_cli_option_or_file(target, config, source, configs.cwd())?
117 {
118 return Ok(Some(runner));
119 }
120 }
121 DiscoveredConfig::Env => {
122 if let Some(tr) = Self::from_env(Self::runner_env_var(target), configs.cwd())? {
125 return Ok(Some(tr));
126 }
127 }
128 }
129 }
130
131 Ok(None)
132 }
133
134 fn from_cli_option_or_file(
135 target: &target_spec::Platform,
136 config: &CargoConfig,
137 source: &CargoConfigSource,
138 cwd: &Utf8Path,
139 ) -> Result<Option<Self>, TargetRunnerError> {
140 if let Some(targets) = &config.target {
141 if let Some(parent) = targets.get(target.triple_str()) {
143 if let Some(runner) = &parent.runner {
144 return Ok(Some(Self::parse_runner(
145 PlatformRunnerSource::CargoConfig {
146 source: source.clone(),
147 target_table: target.triple_str().into(),
148 },
149 runner.clone(),
150 cwd,
151 )?));
152 }
153 }
154
155 for (cfg, runner) in targets.iter().filter_map(|(k, v)| match &v.runner {
160 Some(runner) if k.starts_with("cfg(") => Some((k, runner)),
161 _ => None,
162 }) {
163 let expr = match target_spec::TargetSpecExpression::new(cfg) {
165 Ok(expr) => expr,
166 Err(_err) => continue,
167 };
168
169 if expr.eval(target) == Some(true) {
170 return Ok(Some(Self::parse_runner(
171 PlatformRunnerSource::CargoConfig {
172 source: source.clone(),
173 target_table: cfg.clone(),
174 },
175 runner.clone(),
176 cwd,
177 )?));
178 }
179 }
180 }
181
182 Ok(None)
183 }
184
185 fn from_env(env_key: String, cwd: &Utf8Path) -> Result<Option<Self>, TargetRunnerError> {
186 if let Some(runner_var) = std::env::var_os(&env_key) {
187 let runner = runner_var
188 .into_string()
189 .map_err(|_osstr| TargetRunnerError::InvalidEnvironmentVar(env_key.clone()))?;
190 Self::parse_runner(
191 PlatformRunnerSource::Env(env_key),
192 Runner::Simple(runner),
193 cwd,
194 )
195 .map(Some)
196 } else {
197 Ok(None)
198 }
199 }
200
201 #[doc(hidden)]
203 pub fn runner_env_var(target: &Platform) -> String {
204 let triple_str = target.triple_str().to_ascii_uppercase().replace('-', "_");
205 format!("CARGO_TARGET_{triple_str}_RUNNER")
206 }
207
208 fn parse_runner(
209 source: PlatformRunnerSource,
210 runner: Runner,
211 cwd: &Utf8Path,
212 ) -> Result<Self, TargetRunnerError> {
213 let (runner_binary, args) = match runner {
214 Runner::Simple(value) => {
215 let mut runner_iter = value.split_whitespace();
218
219 let runner_binary =
220 runner_iter
221 .next()
222 .ok_or_else(|| TargetRunnerError::BinaryNotSpecified {
223 key: source.clone(),
224 value: value.clone(),
225 })?;
226 let args = runner_iter.map(String::from).collect();
227 (
228 Self::normalize_runner(runner_binary, source.resolve_dir(cwd)),
229 args,
230 )
231 }
232 Runner::List(mut values) => {
233 if values.is_empty() {
234 return Err(TargetRunnerError::BinaryNotSpecified {
235 key: source,
236 value: String::new(),
237 });
238 } else {
239 let runner_binary = values.remove(0);
240 (
241 Self::normalize_runner(&runner_binary, source.resolve_dir(cwd)),
242 values,
243 )
244 }
245 }
246 };
247
248 Ok(Self {
249 runner_binary,
250 args,
251 source,
252 })
253 }
254
255 fn normalize_runner(runner_binary: &str, resolve_dir: &Utf8Path) -> Utf8PathBuf {
257 let is_path =
258 runner_binary.contains('/') || (cfg!(windows) && runner_binary.contains('\\'));
259 if is_path {
260 resolve_dir.join(runner_binary)
261 } else {
262 runner_binary.into()
264 }
265 }
266
267 #[inline]
274 pub fn binary(&self) -> &str {
275 self.runner_binary.as_str()
276 }
277
278 #[inline]
280 pub fn args(&self) -> impl Iterator<Item = &str> {
281 self.args.iter().map(AsRef::as_ref)
282 }
283
284 #[inline]
286 pub fn source(&self) -> &PlatformRunnerSource {
287 &self.source
288 }
289}
290
291#[derive(Clone, Debug, PartialEq, Eq)]
295pub enum PlatformRunnerSource {
296 Env(String),
298
299 CargoConfig {
302 source: CargoConfigSource,
304
305 target_table: String,
311 },
312}
313
314impl PlatformRunnerSource {
315 fn resolve_dir<'a>(&'a self, cwd: &'a Utf8Path) -> &'a Utf8Path {
317 match self {
318 Self::Env(_) => cwd,
319 Self::CargoConfig { source, .. } => source.resolve_dir(cwd),
320 }
321 }
322}
323
324impl fmt::Display for PlatformRunnerSource {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 match self {
327 Self::Env(var) => {
328 write!(f, "environment variable `{var}`")
329 }
330 Self::CargoConfig {
331 source: CargoConfigSource::CliOption,
332 target_table,
333 } => {
334 write!(f, "`target.{target_table}.runner` specified by `--config`")
335 }
336 Self::CargoConfig {
337 source: CargoConfigSource::File(path),
338 target_table,
339 } => {
340 write!(f, "`target.{target_table}.runner` within `{path}`")
341 }
342 }
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use camino_tempfile::Utf8TempDir;
350 use color_eyre::eyre::{Context, Result};
351 use target_spec::TargetFeatures;
352
353 #[test]
354 fn test_find_config() {
355 let dir = setup_temp_dir().unwrap();
356 let dir_path = dir.path().canonicalize_utf8().unwrap();
357 let dir_foo_path = dir_path.join("foo");
358 let dir_foo_bar_path = dir_foo_path.join("bar");
359
360 assert_eq!(
364 find_config(
365 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
366 &[],
367 &dir_foo_bar_path,
368 &dir_path,
369 ),
370 Some(PlatformRunner {
371 runner_binary: "wine".into(),
372 args: vec!["--test-arg".into()],
373 source: PlatformRunnerSource::CargoConfig {
374 source: CargoConfigSource::File(dir_path.join("foo/bar/.cargo/config.toml")),
375 target_table: "x86_64-pc-windows-msvc".into()
376 },
377 }),
378 );
379
380 assert_eq!(
381 find_config(
382 Platform::new("x86_64-pc-windows-gnu", TargetFeatures::Unknown).unwrap(),
383 &[],
384 &dir_foo_bar_path,
385 &dir_path,
386 ),
387 Some(PlatformRunner {
388 runner_binary: "wine2".into(),
389 args: vec![],
390 source: PlatformRunnerSource::CargoConfig {
391 source: CargoConfigSource::File(dir_path.join("foo/bar/.cargo/config.toml")),
392 target_table: "cfg(windows)".into()
393 },
394 }),
395 );
396
397 assert_eq!(
398 find_config(
399 Platform::new("x86_64-unknown-linux-gnu", TargetFeatures::Unknown).unwrap(),
400 &[],
401 &dir_foo_bar_path,
402 &dir_path,
403 ),
404 Some(PlatformRunner {
405 runner_binary: dir_path.join("unix-runner"),
406 args: vec![],
407 source: PlatformRunnerSource::CargoConfig {
408 source: CargoConfigSource::File(dir_path.join(".cargo/config")),
409 target_table: "cfg(unix)".into()
410 },
411 }),
412 );
413
414 assert_eq!(
418 find_config(
419 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
420 &[],
421 &dir_foo_path,
422 &dir_path,
423 ),
424 Some(PlatformRunner {
425 runner_binary: dir_path.join("../parent-wine"),
426 args: vec![],
427 source: PlatformRunnerSource::CargoConfig {
428 source: CargoConfigSource::File(dir_path.join(".cargo/config")),
429 target_table: "x86_64-pc-windows-msvc".into()
430 },
431 }),
432 );
433
434 assert_eq!(
435 find_config(
436 Platform::new("x86_64-pc-windows-gnu", TargetFeatures::Unknown).unwrap(),
437 &[],
438 &dir_foo_path,
439 &dir_path,
440 ),
441 None,
442 );
443
444 assert_eq!(
448 find_config(
449 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
450 &[],
451 &dir_path,
452 &dir_path,
453 ),
454 Some(PlatformRunner {
455 runner_binary: dir_path.join("../parent-wine"),
456 args: vec![],
457 source: PlatformRunnerSource::CargoConfig {
458 source: CargoConfigSource::File(dir_path.join(".cargo/config")),
459 target_table: "x86_64-pc-windows-msvc".into()
460 },
461 }),
462 );
463
464 assert_eq!(
465 find_config(
466 Platform::new("x86_64-pc-windows-gnu", TargetFeatures::Unknown).unwrap(),
467 &[],
468 &dir_path,
469 &dir_path,
470 ),
471 None,
472 );
473
474 assert_eq!(
478 find_config(
479 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
480 &["target.'cfg(windows)'.runner='windows-runner'"],
481 &dir_path,
482 &dir_path,
483 ),
484 Some(PlatformRunner {
485 runner_binary: "windows-runner".into(),
486 args: vec![],
487 source: PlatformRunnerSource::CargoConfig {
488 source: CargoConfigSource::CliOption,
489 target_table: "cfg(windows)".into()
490 },
491 }),
492 );
493
494 assert_eq!(
495 find_config(
496 Platform::new("x86_64-pc-windows-gnu", TargetFeatures::Unknown).unwrap(),
497 &["target.'cfg(windows)'.runner='windows-runner'"],
498 &dir_path,
499 &dir_path,
500 ),
501 Some(PlatformRunner {
502 runner_binary: "windows-runner".into(),
503 args: vec![],
504 source: PlatformRunnerSource::CargoConfig {
505 source: CargoConfigSource::CliOption,
506 target_table: "cfg(windows)".into()
507 },
508 }),
509 );
510
511 assert_eq!(
513 find_config(
514 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
515 &["target.'cfg(unix)'.runner='unix-runner'"],
516 &dir_path,
517 &dir_path,
518 ),
519 Some(PlatformRunner {
520 runner_binary: dir_path.join("../parent-wine"),
521 args: vec![],
522 source: PlatformRunnerSource::CargoConfig {
523 source: CargoConfigSource::File(dir_path.join(".cargo/config")),
524 target_table: "x86_64-pc-windows-msvc".into()
525 },
526 }),
527 );
528
529 assert_eq!(
530 find_config(
531 Platform::new("x86_64-pc-windows-gnu", TargetFeatures::Unknown).unwrap(),
532 &["target.'cfg(unix)'.runner='unix-runner'"],
533 &dir_path,
534 &dir_path,
535 ),
536 None,
537 );
538
539 assert_eq!(
541 find_config(
542 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
543 &[
544 "target.'cfg(windows)'.runner='windows-runner'",
545 "target.'cfg(all())'.runner='all-runner'"
546 ],
547 &dir_path,
548 &dir_path,
549 ),
550 Some(PlatformRunner {
551 runner_binary: "windows-runner".into(),
552 args: vec![],
553 source: PlatformRunnerSource::CargoConfig {
554 source: CargoConfigSource::CliOption,
555 target_table: "cfg(windows)".into()
556 },
557 }),
558 );
559
560 assert_eq!(
561 find_config(
562 Platform::new("x86_64-pc-windows-msvc", TargetFeatures::Unknown).unwrap(),
563 &[
564 "target.'cfg(all())'.runner='./all-runner'",
565 "target.'cfg(windows)'.runner='windows-runner'",
566 ],
567 &dir_path,
568 &dir_path,
569 ),
570 Some(PlatformRunner {
571 runner_binary: dir_path.join("all-runner"),
572 args: vec![],
573 source: PlatformRunnerSource::CargoConfig {
574 source: CargoConfigSource::CliOption,
575 target_table: "cfg(all())".into()
576 },
577 }),
578 );
579 }
580
581 fn setup_temp_dir() -> Result<Utf8TempDir> {
582 let dir = camino_tempfile::Builder::new()
583 .tempdir()
584 .wrap_err("error creating tempdir")?;
585
586 std::fs::create_dir_all(dir.path().join(".cargo"))
587 .wrap_err("error creating .cargo subdir")?;
588 std::fs::create_dir_all(dir.path().join("foo/bar/.cargo"))
589 .wrap_err("error creating foo/bar/.cargo subdir")?;
590
591 std::fs::write(dir.path().join(".cargo/config"), CARGO_CONFIG_CONTENTS)
592 .wrap_err("error writing .cargo/config")?;
593 std::fs::write(
594 dir.path().join("foo/bar/.cargo/config.toml"),
595 FOO_BAR_CARGO_CONFIG_CONTENTS,
596 )
597 .wrap_err("error writing foo/bar/.cargo/config.toml")?;
598
599 Ok(dir)
600 }
601
602 fn find_config(
603 platform: Platform,
604 cli_configs: &[&str],
605 cwd: &Utf8Path,
606 terminate_search_at: &Utf8Path,
607 ) -> Option<PlatformRunner> {
608 let configs =
609 CargoConfigs::new_with_isolation(cli_configs, cwd, terminate_search_at, Vec::new())
610 .unwrap();
611 PlatformRunner::find_config(&configs, &platform).unwrap()
612 }
613
614 static CARGO_CONFIG_CONTENTS: &str = r#"
615 [target.x86_64-pc-windows-msvc]
616 runner = "../parent-wine"
617
618 [target.'cfg(unix)']
619 runner = "./unix-runner"
620 "#;
621
622 static FOO_BAR_CARGO_CONFIG_CONTENTS: &str = r#"
623 [target.x86_64-pc-windows-msvc]
624 runner = ["wine", "--test-arg"]
625
626 [target.'cfg(windows)']
627 runner = "wine2"
628 "#;
629}