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