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