Skip to main content

nextest_runner/cargo_config/
target_triple.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use super::ExtractedCustomPlatform;
5use crate::{
6    cargo_config::{CargoConfigSource, CargoConfigs, DiscoveredConfig},
7    errors::{RustBuildMetaParseError, TargetTripleError},
8};
9use bstr::ByteSlice;
10use camino::{Utf8Path, Utf8PathBuf};
11use std::{fmt, process::Command};
12use target_spec::{CustomSource, Platform, TargetFeatures, summaries::PlatformSummary};
13
14/// Represents a target triple that's being cross-compiled against.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct TargetTriple {
17    /// The target platform being built.
18    pub platform: Platform,
19
20    /// The source the triple came from.
21    pub source: TargetTripleSource,
22
23    /// The place where the target definition was obtained from.
24    pub location: TargetDefinitionLocation,
25}
26
27impl TargetTriple {
28    /// Converts a `PlatformSummary` that was output by `TargetTriple::serialize` back to a target triple.
29    /// This target triple is assumed to originate from a build-metadata config.
30    pub fn deserialize(
31        platform: Option<PlatformSummary>,
32    ) -> Result<Option<TargetTriple>, RustBuildMetaParseError> {
33        let Some(summary) = platform else {
34            return Ok(None);
35        };
36        let platform = summary.to_platform()?;
37        let location = match platform.custom_source() {
38            None => TargetDefinitionLocation::Builtin,
39            Some(CustomSource::Json(json)) => {
40                TargetDefinitionLocation::MetadataCustom(json.to_owned())
41            }
42            Some(CustomSource::Cfg(_)) => TargetDefinitionLocation::RustcCfgCustom,
43            // CustomSource is non_exhaustive.
44            Some(_) => {
45                return Err(RustBuildMetaParseError::Unsupported {
46                    message: format!(
47                        "custom platform {:?} is defined in a way that this version of nextest \
48                         does not understand; try upgrading nextest, and if that doesn't help, \
49                         please report a bug",
50                        platform.triple_str(),
51                    ),
52                });
53            }
54        };
55        Ok(Some(TargetTriple {
56            platform,
57            source: TargetTripleSource::Metadata,
58            location,
59        }))
60    }
61
62    /// Converts a string that was output by older versions of nextest back to a target triple.
63    pub fn deserialize_str(
64        triple_str: Option<String>,
65    ) -> Result<Option<TargetTriple>, target_spec::Error> {
66        triple_str
67            .map(|triple_str| {
68                Ok(TargetTriple {
69                    platform: Platform::new(triple_str, TargetFeatures::Unknown)?,
70                    source: TargetTripleSource::Metadata,
71                    location: TargetDefinitionLocation::Builtin,
72                })
73            })
74            .transpose()
75    }
76
77    /// Returns the target triple being built as a string to pass into downstream Cargo arguments,
78    /// such as `cargo metadata --filter-platform`.
79    ///
80    /// For custom target triples, this will be a path to a file ending with `.json`. Nextest may
81    /// temporarily extract the target triple, in which case a `Utf8TempFile` is returned.
82    pub fn to_cargo_target_arg(&self) -> Result<CargoTargetArg, TargetTripleError> {
83        match &self.location {
84            // The determination for heuristic targets may not be quite right.
85            TargetDefinitionLocation::Builtin | TargetDefinitionLocation::Heuristic => Ok(
86                CargoTargetArg::Builtin(self.platform.triple_str().to_string()),
87            ),
88            TargetDefinitionLocation::DirectPath(path)
89            | TargetDefinitionLocation::RustTargetPath(path) => {
90                Ok(CargoTargetArg::Path(path.clone()))
91            }
92            TargetDefinitionLocation::MetadataCustom(json) => CargoTargetArg::from_custom_json(
93                self.platform.triple_str(),
94                json,
95                self.source.clone(),
96            ),
97            TargetDefinitionLocation::RustcCfgCustom => Ok(CargoTargetArg::RustcCfg(
98                self.platform.triple_str().to_string(),
99            )),
100        }
101    }
102
103    /// Find the target triple being built.
104    ///
105    /// This does so by looking at, in order:
106    ///
107    /// 1. the passed in --target CLI option
108    /// 2. the CARGO_BUILD_TARGET env var
109    /// 3. build.target in Cargo config files
110    ///
111    /// The `host_platform` is used to resolve the special "host-tuple" target, which resolves to
112    /// the host platform.
113    pub fn find(
114        cargo_configs: &CargoConfigs,
115        target_cli_option: Option<&str>,
116        host_platform: &Platform,
117    ) -> Result<Option<Self>, TargetTripleError> {
118        // First, look at the CLI option passed in.
119        if let Some(triple_str_or_path) = target_cli_option {
120            let ret = Self::resolve_triple(
121                triple_str_or_path,
122                TargetTripleSource::CliOption,
123                cargo_configs.cwd(),
124                cargo_configs.target_paths(),
125                host_platform,
126            )?;
127            return Ok(Some(ret));
128        }
129
130        // Finally, look at the cargo configs.
131        Self::from_cargo_configs(cargo_configs, host_platform)
132    }
133
134    /// The environment variable used for target searches
135    pub const CARGO_BUILD_TARGET_ENV: &'static str = "CARGO_BUILD_TARGET";
136
137    fn from_env(
138        cwd: &Utf8Path,
139        target_paths: &[Utf8PathBuf],
140        host_platform: &Platform,
141    ) -> Result<Option<Self>, TargetTripleError> {
142        if let Some(triple_val) = std::env::var_os(Self::CARGO_BUILD_TARGET_ENV) {
143            let triple = triple_val
144                .into_string()
145                .map_err(|_osstr| TargetTripleError::InvalidEnvironmentVar)?;
146            let ret = Self::resolve_triple(
147                &triple,
148                TargetTripleSource::Env,
149                cwd,
150                target_paths,
151                host_platform,
152            )?;
153            Ok(Some(ret))
154        } else {
155            Ok(None)
156        }
157    }
158
159    fn from_cargo_configs(
160        cargo_configs: &CargoConfigs,
161        host_platform: &Platform,
162    ) -> Result<Option<Self>, TargetTripleError> {
163        for discovered_config in cargo_configs.discovered_configs() {
164            match discovered_config {
165                DiscoveredConfig::CliOption { config, source }
166                | DiscoveredConfig::File { config, source } => {
167                    if let Some(triple) = &config.build.target {
168                        let resolve_dir = source.resolve_dir(cargo_configs.cwd());
169                        let source = TargetTripleSource::CargoConfig {
170                            source: source.clone(),
171                        };
172                        let ret = Self::resolve_triple(
173                            triple,
174                            source,
175                            resolve_dir,
176                            cargo_configs.target_paths(),
177                            host_platform,
178                        )?;
179                        return Ok(Some(ret));
180                    }
181                }
182                DiscoveredConfig::Env => {
183                    // Look at the CARGO_BUILD_TARGET env var.
184                    if let Some(triple) = Self::from_env(
185                        cargo_configs.cwd(),
186                        cargo_configs.target_paths(),
187                        host_platform,
188                    )? {
189                        return Ok(Some(triple));
190                    }
191                }
192            }
193        }
194
195        Ok(None)
196    }
197
198    /// Resolves triples passed in over the command line using the algorithm described here:
199    /// https://github.com/rust-lang/rust/blob/2d0aa57684e10f7b3d3fe740ee18d431181583ad/compiler/rustc_target/src/spec/mod.rs#L11C11-L20
200    /// https://github.com/rust-lang/rust/blob/f217411bacbe943ead9dfca93a91dff0753c2a96/compiler/rustc_session/src/config.rs#L2065-L2079
201    fn resolve_triple(
202        triple_str_or_path: &str,
203        source: TargetTripleSource,
204        // This is typically the cwd but in case of a triple specified in a config file is resolved
205        // with respect to that.
206        resolve_dir: &Utf8Path,
207        target_paths: &[Utf8PathBuf],
208        host_platform: &Platform,
209    ) -> Result<Self, TargetTripleError> {
210        // Handle "host-tuple" special case: resolve to the host platform.
211        if triple_str_or_path == "host-tuple" {
212            return Ok(Self {
213                platform: host_platform.clone(),
214                source,
215                location: TargetDefinitionLocation::Builtin,
216            });
217        }
218
219        if triple_str_or_path.ends_with(".json") {
220            return Self::custom_from_path(triple_str_or_path.as_ref(), source, resolve_dir);
221        }
222
223        // Is this a builtin (non-heuristic)?
224        if let Ok(platform) =
225            Platform::new_strict(triple_str_or_path.to_owned(), TargetFeatures::Unknown)
226        {
227            return Ok(Self {
228                platform,
229                source,
230                location: TargetDefinitionLocation::Builtin,
231            });
232        }
233
234        // Now look for this triple through all the paths in RUST_TARGET_PATH.
235        let triple_filename = {
236            let mut triple_str = triple_str_or_path.to_owned();
237            triple_str.push_str(".json");
238            Utf8PathBuf::from(triple_str)
239        };
240
241        for dir in target_paths {
242            let path = dir.join(&triple_filename);
243            if path.is_file() {
244                let path = path.canonicalize_utf8().map_err(|error| {
245                    TargetTripleError::TargetPathReadError {
246                        source: source.clone(),
247                        path,
248                        error,
249                    }
250                })?;
251                return Self::load_file(
252                    triple_str_or_path,
253                    &path,
254                    source,
255                    TargetDefinitionLocation::RustTargetPath(path.clone()),
256                );
257            }
258        }
259
260        if let Ok(stdout) = Command::new(std::env::var("RUSTC").unwrap_or("rustc".to_owned()))
261            .arg("--print=cfg")
262            .arg("--target")
263            .arg(triple_str_or_path)
264            .output()
265            .map(|out| out.stdout)
266            && let Ok(cfg_text) = stdout.to_str()
267            && let Ok(platform) = Platform::new_custom_cfg(
268                triple_str_or_path.to_owned(),
269                cfg_text,
270                TargetFeatures::Unknown,
271            )
272        {
273            return Ok(Self {
274                platform,
275                source,
276                location: TargetDefinitionLocation::RustcCfgCustom,
277            });
278        }
279
280        // TODO: search in rustlib. This isn't documented and we need to implement searching for
281        // rustlib:
282        // https://github.com/rust-lang/rust/blob/2d0aa57684e10f7b3d3fe740ee18d431181583ad/compiler/rustc_target/src/spec/mod.rs#L2789-L2799.
283
284        // As a last-ditch effort, use a heuristic approach.
285        let platform = Platform::new(triple_str_or_path.to_owned(), TargetFeatures::Unknown)
286            .map_err(|error| TargetTripleError::TargetSpecError {
287                source: source.clone(),
288                error,
289            })?;
290        Ok(Self {
291            platform,
292            source,
293            location: TargetDefinitionLocation::Heuristic,
294        })
295    }
296
297    /// Converts a path ending with `.json` to a custom target triple.
298    pub(super) fn custom_from_path(
299        path: &Utf8Path,
300        source: TargetTripleSource,
301        resolve_dir: &Utf8Path,
302    ) -> Result<Self, TargetTripleError> {
303        assert_eq!(
304            path.extension(),
305            Some("json"),
306            "path {path} must end with .json",
307        );
308        let path = resolve_dir.join(path);
309        let canonicalized_path =
310            path.canonicalize_utf8()
311                .map_err(|error| TargetTripleError::TargetPathReadError {
312                    source: source.clone(),
313                    path,
314                    error,
315                })?;
316        // Strip the ".json" at the end.
317        let triple_str = canonicalized_path
318            .file_stem()
319            .expect("target path must not be empty")
320            .to_owned();
321        Self::load_file(
322            &triple_str,
323            &canonicalized_path,
324            source,
325            TargetDefinitionLocation::DirectPath(canonicalized_path.clone()),
326        )
327    }
328
329    fn load_file(
330        triple_str: &str,
331        path: &Utf8Path,
332        source: TargetTripleSource,
333        location: TargetDefinitionLocation,
334    ) -> Result<Self, TargetTripleError> {
335        let contents = std::fs::read_to_string(path).map_err(|error| {
336            TargetTripleError::TargetPathReadError {
337                source: source.clone(),
338                path: path.to_owned(),
339                error,
340            }
341        })?;
342        let platform =
343            Platform::new_custom(triple_str.to_owned(), &contents, TargetFeatures::Unknown)
344                .map_err(|error| TargetTripleError::TargetSpecError {
345                    source: source.clone(),
346                    error,
347                })?;
348        Ok(Self {
349            platform,
350            source,
351            location,
352        })
353    }
354}
355
356/// Cargo argument for downstream commands.
357///
358/// If it is necessary to run a Cargo command with a target triple, this enum provides the right
359/// invocation. Create it with [`TargetTriple::to_cargo_target_arg`].
360///
361/// The `Display` impl of this type produces the argument to provide after `--target`, or `cargo
362/// metadata --filter-platform`.
363#[derive(Debug)]
364pub enum CargoTargetArg {
365    /// The target triple is a builtin.
366    Builtin(String),
367
368    /// The target triple is a JSON file at this path.
369    Path(Utf8PathBuf),
370
371    /// The target triple was extracted from metadata and stored in a temporary directory.
372    Extracted(ExtractedCustomPlatform),
373
374    /// The target triple was resolved via `rustc --print=cfg`.
375    RustcCfg(String),
376}
377
378impl CargoTargetArg {
379    fn from_custom_json(
380        triple_str: &str,
381        json: &str,
382        source: TargetTripleSource,
383    ) -> Result<Self, TargetTripleError> {
384        let extracted = ExtractedCustomPlatform::new(triple_str, json, source)?;
385        Ok(Self::Extracted(extracted))
386    }
387}
388
389impl fmt::Display for CargoTargetArg {
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        match self {
392            Self::Builtin(triple) => {
393                write!(f, "{triple}")
394            }
395            Self::Path(path) => {
396                write!(f, "{path}")
397            }
398            Self::Extracted(extracted) => {
399                write!(f, "{}", extracted.path())
400            }
401            Self::RustcCfg(triple) => {
402                write!(f, "{triple}")
403            }
404        }
405    }
406}
407
408/// The place where a target triple's configuration was picked up from.
409///
410/// This is the type of [`TargetTriple::source`].
411#[derive(Clone, Debug, PartialEq, Eq)]
412pub enum TargetTripleSource {
413    /// The target triple was defined by the --target CLI option.
414    CliOption,
415
416    /// The target triple was defined by the `CARGO_BUILD_TARGET` env var.
417    Env,
418
419    /// The target triple was defined through a `.cargo/config.toml` or `.cargo/config` file, or a
420    /// `--config` CLI option.
421    CargoConfig {
422        /// The source of the configuration.
423        source: CargoConfigSource,
424    },
425
426    /// The target triple was defined through a metadata file provided using the --archive-file or
427    /// the `--binaries-metadata` CLI option.
428    Metadata,
429}
430
431impl fmt::Display for TargetTripleSource {
432    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433        match self {
434            Self::CliOption => {
435                write!(f, "--target <option>")
436            }
437            Self::Env => {
438                write!(f, "environment variable `CARGO_BUILD_TARGET`")
439            }
440            Self::CargoConfig {
441                source: CargoConfigSource::CliOption,
442            } => {
443                write!(f, "`build.target` specified by `--config`")
444            }
445
446            Self::CargoConfig {
447                source: CargoConfigSource::File(path),
448            } => {
449                write!(f, "`build.target` within `{path}`")
450            }
451            Self::Metadata => {
452                write!(f, "--archive-file or --binaries-metadata option")
453            }
454        }
455    }
456}
457
458/// The location a target triple's definition was obtained from.
459#[derive(Clone, Debug, Eq, PartialEq)]
460pub enum TargetDefinitionLocation {
461    /// The target triple was a builtin.
462    Builtin,
463
464    /// The definition was obtained from a file on disk -- the triple string ended with .json.
465    DirectPath(Utf8PathBuf),
466
467    /// The definition was obtained from a file in `RUST_TARGET_PATH`.
468    RustTargetPath(Utf8PathBuf),
469
470    /// The definition was obtained heuristically.
471    Heuristic,
472
473    /// A custom definition was stored in metadata. The string is the JSON of the custom target.
474    MetadataCustom(String),
475
476    /// The custom cfg retrieved by calling `rustc --print=cfg`.
477    RustcCfgCustom,
478}
479
480impl fmt::Display for TargetDefinitionLocation {
481    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
482        match self {
483            Self::Builtin => {
484                write!(f, "target was builtin")
485            }
486            Self::DirectPath(path) => {
487                write!(f, "definition obtained from file at path `{path}`")
488            }
489            Self::RustTargetPath(path) => {
490                write!(f, "definition obtained from RUST_TARGET_PATH: `{path}`")
491            }
492            Self::Heuristic => {
493                write!(f, "definition obtained heuristically")
494            }
495            Self::MetadataCustom(_) => {
496                write!(f, "custom definition stored in metadata")
497            }
498            Self::RustcCfgCustom => {
499                write!(f, "custom definition from the rustc cfg")
500            }
501        }
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508    use crate::cargo_config::test_helpers::{custom_platform, setup_temp_dir};
509
510    #[test]
511    fn test_find_target_triple() {
512        let dir = setup_temp_dir().unwrap();
513        let dir_path = Utf8PathBuf::try_from(dir.path().canonicalize().unwrap()).unwrap();
514        let dir_foo_path = dir_path.join("foo");
515        let dir_foo_bar_path = dir_foo_path.join("bar");
516        let dir_foo_bar_custom1_path = dir_foo_bar_path.join("custom1");
517        let dir_foo_bar_custom2_path = dir_foo_bar_path.join("custom2");
518        let custom_target_dir = dir.path().join("custom-target");
519        let custom_target_path = dir
520            .path()
521            .join("custom-target/my-target.json")
522            .canonicalize_utf8()
523            .expect("path exists");
524
525        // Test reading from config files
526        assert_eq!(
527            find_target_triple(&[], None, &dir_foo_bar_path, &dir_path),
528            Some(TargetTriple {
529                platform: platform("x86_64-unknown-linux-gnu"),
530                source: TargetTripleSource::CargoConfig {
531                    source: CargoConfigSource::File(dir_path.join("foo/bar/.cargo/config.toml")),
532                },
533                location: TargetDefinitionLocation::Builtin,
534            }),
535        );
536
537        assert_eq!(
538            find_target_triple(&[], None, &dir_foo_path, &dir_path),
539            Some(TargetTriple {
540                platform: platform("x86_64-pc-windows-msvc"),
541                source: TargetTripleSource::CargoConfig {
542                    source: CargoConfigSource::File(dir_path.join("foo/.cargo/config")),
543                },
544                location: TargetDefinitionLocation::Builtin,
545            }),
546        );
547
548        assert_eq!(
549            find_target_triple(&[], None, &dir_foo_bar_custom2_path, &dir_path),
550            Some(TargetTriple {
551                platform: custom_platform(),
552                source: TargetTripleSource::CargoConfig {
553                    source: CargoConfigSource::File(
554                        dir_path.join("foo/bar/custom2/.cargo/config.toml")
555                    ),
556                },
557                location: TargetDefinitionLocation::DirectPath(custom_target_path.clone()),
558            })
559        );
560
561        assert_eq!(
562            find_target_triple_with_paths(
563                &[],
564                None,
565                &dir_foo_bar_custom1_path,
566                &dir_path,
567                vec![custom_target_dir]
568            ),
569            Some(TargetTriple {
570                platform: custom_platform(),
571                source: TargetTripleSource::CargoConfig {
572                    source: CargoConfigSource::File(
573                        dir_path.join("foo/bar/custom1/.cargo/config.toml")
574                    ),
575                },
576                location: TargetDefinitionLocation::RustTargetPath(custom_target_path.clone()),
577            })
578        );
579
580        assert_eq!(
581            find_target_triple(
582                &["build.target=\"aarch64-unknown-linux-gnu\""],
583                None,
584                &dir_foo_bar_path,
585                &dir_path
586            ),
587            Some(TargetTriple {
588                platform: platform("aarch64-unknown-linux-gnu"),
589                source: TargetTripleSource::CargoConfig {
590                    source: CargoConfigSource::CliOption,
591                },
592                location: TargetDefinitionLocation::Builtin,
593            })
594        );
595
596        // --config arguments are followed left to right.
597        assert_eq!(
598            find_target_triple(
599                &[
600                    "build.target=\"aarch64-unknown-linux-gnu\"",
601                    "build.target=\"x86_64-unknown-linux-musl\""
602                ],
603                None,
604                &dir_foo_bar_path,
605                &dir_path
606            ),
607            Some(TargetTriple {
608                platform: platform("aarch64-unknown-linux-gnu"),
609                source: TargetTripleSource::CargoConfig {
610                    source: CargoConfigSource::CliOption,
611                },
612                location: TargetDefinitionLocation::Builtin,
613            })
614        );
615
616        // --config arguments are resolved wrt the current dir.
617        assert_eq!(
618            find_target_triple(
619                &["build.target=\"../../custom-target/my-target.json\"",],
620                None,
621                &dir_foo_bar_path,
622                &dir_path
623            ),
624            Some(TargetTriple {
625                platform: custom_platform(),
626                source: TargetTripleSource::CargoConfig {
627                    source: CargoConfigSource::CliOption,
628                },
629                location: TargetDefinitionLocation::DirectPath(custom_target_path.clone()),
630            })
631        );
632
633        // --config is preferred over the environment.
634        assert_eq!(
635            find_target_triple(
636                &["build.target=\"aarch64-unknown-linux-gnu\"",],
637                Some("aarch64-pc-windows-msvc"),
638                &dir_foo_bar_path,
639                &dir_path
640            ),
641            Some(TargetTriple {
642                platform: platform("aarch64-unknown-linux-gnu"),
643                source: TargetTripleSource::CargoConfig {
644                    source: CargoConfigSource::CliOption,
645                },
646                location: TargetDefinitionLocation::Builtin,
647            })
648        );
649
650        // The environment is preferred over local paths.
651        assert_eq!(
652            find_target_triple(
653                &[],
654                Some("aarch64-pc-windows-msvc"),
655                &dir_foo_bar_path,
656                &dir_path
657            ),
658            Some(TargetTriple {
659                platform: platform("aarch64-pc-windows-msvc"),
660                source: TargetTripleSource::Env,
661                location: TargetDefinitionLocation::Builtin,
662            })
663        );
664
665        // --config <path> should be parsed correctly. Config files passed in via --config currently
666        // come after keys and values passed in via --config, and before the environment (this
667        // didn't used to be the case in older versions of Rust, but is now the case as of Rust 1.68
668        // with https://github.com/rust-lang/cargo/pull/11077).
669        assert_eq!(
670            find_target_triple(&["extra-config.toml"], None, &dir_foo_path, &dir_path),
671            Some(TargetTriple {
672                platform: platform("aarch64-unknown-linux-gnu"),
673                source: TargetTripleSource::CargoConfig {
674                    source: CargoConfigSource::File(dir_foo_path.join("extra-config.toml")),
675                },
676                location: TargetDefinitionLocation::Builtin,
677            })
678        );
679        assert_eq!(
680            find_target_triple(
681                &["extra-config.toml"],
682                Some("aarch64-pc-windows-msvc"),
683                &dir_foo_path,
684                &dir_path
685            ),
686            Some(TargetTriple {
687                platform: platform("aarch64-unknown-linux-gnu"),
688                source: TargetTripleSource::CargoConfig {
689                    source: CargoConfigSource::File(dir_foo_path.join("extra-config.toml")),
690                },
691                location: TargetDefinitionLocation::Builtin,
692            })
693        );
694        assert_eq!(
695            find_target_triple(
696                &[
697                    "../extra-config.toml",
698                    "build.target=\"x86_64-unknown-linux-musl\"",
699                ],
700                None,
701                &dir_foo_bar_path,
702                &dir_path
703            ),
704            Some(TargetTriple {
705                platform: platform("x86_64-unknown-linux-musl"),
706                source: TargetTripleSource::CargoConfig {
707                    source: CargoConfigSource::CliOption,
708                },
709                location: TargetDefinitionLocation::Builtin,
710            })
711        );
712        assert_eq!(
713            find_target_triple(
714                &[
715                    "build.target=\"x86_64-unknown-linux-musl\"",
716                    "extra-config.toml",
717                ],
718                None,
719                &dir_foo_path,
720                &dir_path
721            ),
722            Some(TargetTriple {
723                platform: platform("x86_64-unknown-linux-musl"),
724                source: TargetTripleSource::CargoConfig {
725                    source: CargoConfigSource::CliOption,
726                },
727                location: TargetDefinitionLocation::Builtin,
728            })
729        );
730        // Config paths passed over the command line are resolved according to the directory they're
731        // in. (To test this, run the test from dir/foo/bar -- extra-custom-config should be
732        // resolved according to dir/foo).
733        assert_eq!(
734            find_target_triple(
735                &["../extra-custom-config.toml"],
736                None,
737                &dir_foo_bar_path,
738                &dir_path
739            ),
740            Some(TargetTriple {
741                platform: custom_platform(),
742                source: TargetTripleSource::CargoConfig {
743                    source: CargoConfigSource::File(dir_foo_path.join("extra-custom-config.toml")),
744                },
745                location: TargetDefinitionLocation::DirectPath(custom_target_path),
746            })
747        );
748
749        assert_eq!(find_target_triple(&[], None, &dir_path, &dir_path), None);
750    }
751
752    fn find_target_triple(
753        cli_configs: &[&str],
754        env: Option<&str>,
755        start_search_at: &Utf8Path,
756        terminate_search_at: &Utf8Path,
757    ) -> Option<TargetTriple> {
758        find_target_triple_with_paths(
759            cli_configs,
760            env,
761            start_search_at,
762            terminate_search_at,
763            Vec::new(),
764        )
765    }
766
767    fn find_target_triple_with_paths(
768        cli_configs: &[&str],
769        env: Option<&str>,
770        start_search_at: &Utf8Path,
771        terminate_search_at: &Utf8Path,
772        target_paths: Vec<Utf8PathBuf>,
773    ) -> Option<TargetTriple> {
774        find_target_triple_impl(
775            cli_configs,
776            None,
777            env,
778            start_search_at,
779            terminate_search_at,
780            target_paths,
781            &dummy_host_platform(),
782        )
783    }
784
785    fn find_target_triple_with_host(
786        cli_configs: &[&str],
787        target_cli_option: Option<&str>,
788        env: Option<&str>,
789        start_search_at: &Utf8Path,
790        terminate_search_at: &Utf8Path,
791        host_platform: &Platform,
792    ) -> Option<TargetTriple> {
793        find_target_triple_impl(
794            cli_configs,
795            target_cli_option,
796            env,
797            start_search_at,
798            terminate_search_at,
799            Vec::new(),
800            host_platform,
801        )
802    }
803
804    fn find_target_triple_impl(
805        cli_configs: &[&str],
806        target_cli_option: Option<&str>,
807        env: Option<&str>,
808        start_search_at: &Utf8Path,
809        terminate_search_at: &Utf8Path,
810        target_paths: Vec<Utf8PathBuf>,
811        host_platform: &Platform,
812    ) -> Option<TargetTriple> {
813        let configs = CargoConfigs::new_with_isolation(
814            cli_configs,
815            start_search_at,
816            terminate_search_at,
817            target_paths,
818        )
819        .unwrap();
820        if let Some(env) = env {
821            // SAFETY:
822            // https://nexte.st/docs/configuration/env-vars/#altering-the-environment-within-tests
823            unsafe { std::env::set_var("CARGO_BUILD_TARGET", env) };
824        }
825        let ret = TargetTriple::find(&configs, target_cli_option, host_platform).unwrap();
826        // SAFETY:
827        // https://nexte.st/docs/configuration/env-vars/#altering-the-environment-within-tests
828        unsafe { std::env::remove_var("CARGO_BUILD_TARGET") };
829        ret
830    }
831
832    #[test]
833    fn test_host_tuple() {
834        // Create a temp dir with a .cargo/config.toml that has build.target = "host-tuple".
835        let dir = camino_tempfile::Builder::new()
836            .tempdir()
837            .expect("error creating tempdir");
838        let dir_path = Utf8PathBuf::try_from(dir.path().canonicalize().unwrap()).unwrap();
839
840        std::fs::create_dir_all(dir.path().join(".cargo")).expect("error creating .cargo subdir");
841        std::fs::write(
842            dir.path().join(".cargo/config.toml"),
843            r#"
844                [build]
845                target = "host-tuple"
846            "#,
847        )
848        .expect("error writing .cargo/config.toml");
849
850        let host_platform = platform("aarch64-apple-darwin");
851
852        // Test --target host-tuple (CLI option).
853        assert_eq!(
854            find_target_triple_with_host(
855                &[],
856                Some("host-tuple"),
857                None,
858                &dir_path,
859                &dir_path,
860                &host_platform,
861            ),
862            Some(TargetTriple {
863                platform: platform("aarch64-apple-darwin"),
864                source: TargetTripleSource::CliOption,
865                location: TargetDefinitionLocation::Builtin,
866            })
867        );
868
869        // Test --config build.target="host-tuple".
870        assert_eq!(
871            find_target_triple_with_host(
872                &["build.target=\"host-tuple\""],
873                None,
874                None,
875                &dir_path,
876                &dir_path,
877                &host_platform,
878            ),
879            Some(TargetTriple {
880                platform: platform("aarch64-apple-darwin"),
881                source: TargetTripleSource::CargoConfig {
882                    source: CargoConfigSource::CliOption,
883                },
884                location: TargetDefinitionLocation::Builtin,
885            })
886        );
887
888        // Test CARGO_BUILD_TARGET=host-tuple (env var).
889        assert_eq!(
890            find_target_triple_with_host(
891                &[],
892                None,
893                Some("host-tuple"),
894                &dir_path,
895                &dir_path,
896                &host_platform,
897            ),
898            Some(TargetTriple {
899                platform: platform("aarch64-apple-darwin"),
900                source: TargetTripleSource::Env,
901                location: TargetDefinitionLocation::Builtin,
902            })
903        );
904
905        // Test .cargo/config.toml with build.target = "host-tuple".
906        assert_eq!(
907            find_target_triple_with_host(&[], None, None, &dir_path, &dir_path, &host_platform),
908            Some(TargetTriple {
909                platform: platform("aarch64-apple-darwin"),
910                source: TargetTripleSource::CargoConfig {
911                    source: CargoConfigSource::File(dir_path.join(".cargo/config.toml")),
912                },
913                location: TargetDefinitionLocation::Builtin,
914            })
915        );
916    }
917
918    fn platform(triple_str: &str) -> Platform {
919        Platform::new(triple_str.to_owned(), TargetFeatures::Unknown).expect("triple str is valid")
920    }
921
922    fn dummy_host_platform() -> Platform {
923        Platform::new(
924            "x86_64-unknown-linux-gnu".to_owned(),
925            TargetFeatures::Unknown,
926        )
927        .unwrap()
928    }
929}