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    /// Note that currently this only supports triples, not JSON files.
100    pub fn find(
101        cargo_configs: &CargoConfigs,
102        target_cli_option: Option<&str>,
103    ) -> Result<Option<Self>, TargetTripleError> {
104        // First, look at the CLI option passed in.
105        if let Some(triple_str_or_path) = target_cli_option {
106            let ret = Self::resolve_triple(
107                triple_str_or_path,
108                TargetTripleSource::CliOption,
109                cargo_configs.cwd(),
110                cargo_configs.target_paths(),
111            )?;
112            return Ok(Some(ret));
113        }
114
115        // Finally, look at the cargo configs.
116        Self::from_cargo_configs(cargo_configs)
117    }
118
119    /// The environment variable used for target searches
120    pub const CARGO_BUILD_TARGET_ENV: &'static str = "CARGO_BUILD_TARGET";
121
122    fn from_env(
123        cwd: &Utf8Path,
124        target_paths: &[Utf8PathBuf],
125    ) -> Result<Option<Self>, TargetTripleError> {
126        if let Some(triple_val) = std::env::var_os(Self::CARGO_BUILD_TARGET_ENV) {
127            let triple = triple_val
128                .into_string()
129                .map_err(|_osstr| TargetTripleError::InvalidEnvironmentVar)?;
130            let ret = Self::resolve_triple(&triple, TargetTripleSource::Env, cwd, target_paths)?;
131            Ok(Some(ret))
132        } else {
133            Ok(None)
134        }
135    }
136
137    fn from_cargo_configs(cargo_configs: &CargoConfigs) -> Result<Option<Self>, TargetTripleError> {
138        for discovered_config in cargo_configs.discovered_configs() {
139            match discovered_config {
140                DiscoveredConfig::CliOption { config, source }
141                | DiscoveredConfig::File { config, source } => {
142                    if let Some(triple) = &config.build.target {
143                        let resolve_dir = source.resolve_dir(cargo_configs.cwd());
144                        let source = TargetTripleSource::CargoConfig {
145                            source: source.clone(),
146                        };
147                        let ret = Self::resolve_triple(
148                            triple,
149                            source,
150                            resolve_dir,
151                            cargo_configs.target_paths(),
152                        )?;
153                        return Ok(Some(ret));
154                    }
155                }
156                DiscoveredConfig::Env => {
157                    // Look at the CARGO_BUILD_TARGET env var.
158                    if let Some(triple) =
159                        Self::from_env(cargo_configs.cwd(), cargo_configs.target_paths())?
160                    {
161                        return Ok(Some(triple));
162                    }
163                }
164            }
165        }
166
167        Ok(None)
168    }
169
170    /// Resolves triples passed in over the command line using the algorithm described here:
171    /// https://github.com/rust-lang/rust/blob/2d0aa57684e10f7b3d3fe740ee18d431181583ad/compiler/rustc_target/src/spec/mod.rs#L11C11-L20
172    /// https://github.com/rust-lang/rust/blob/f217411bacbe943ead9dfca93a91dff0753c2a96/compiler/rustc_session/src/config.rs#L2065-L2079
173    fn resolve_triple(
174        triple_str_or_path: &str,
175        source: TargetTripleSource,
176        // This is typically the cwd but in case of a triple specified in a config file is resolved
177        // with respect to that.
178        resolve_dir: &Utf8Path,
179        target_paths: &[Utf8PathBuf],
180    ) -> Result<Self, TargetTripleError> {
181        if triple_str_or_path.ends_with(".json") {
182            return Self::custom_from_path(triple_str_or_path.as_ref(), source, resolve_dir);
183        }
184
185        // Is this a builtin (non-heuristic)?
186        if let Ok(platform) =
187            Platform::new_strict(triple_str_or_path.to_owned(), TargetFeatures::Unknown)
188        {
189            return Ok(Self {
190                platform,
191                source,
192                location: TargetDefinitionLocation::Builtin,
193            });
194        }
195
196        // Now look for this triple through all the paths in RUST_TARGET_PATH.
197        let triple_filename = {
198            let mut triple_str = triple_str_or_path.to_owned();
199            triple_str.push_str(".json");
200            Utf8PathBuf::from(triple_str)
201        };
202
203        for dir in target_paths {
204            let path = dir.join(&triple_filename);
205            if path.is_file() {
206                let path = path.canonicalize_utf8().map_err(|error| {
207                    TargetTripleError::TargetPathReadError {
208                        source: source.clone(),
209                        path,
210                        error,
211                    }
212                })?;
213                return Self::load_file(
214                    triple_str_or_path,
215                    &path,
216                    source,
217                    TargetDefinitionLocation::RustTargetPath(path.clone()),
218                );
219            }
220        }
221
222        // TODO: search in rustlib. This isn't documented and we need to implement searching for
223        // rustlib:
224        // https://github.com/rust-lang/rust/blob/2d0aa57684e10f7b3d3fe740ee18d431181583ad/compiler/rustc_target/src/spec/mod.rs#L2789-L2799.
225
226        // As a last-ditch effort, use a heuristic approach.
227        let platform = Platform::new(triple_str_or_path.to_owned(), TargetFeatures::Unknown)
228            .map_err(|error| TargetTripleError::TargetSpecError {
229                source: source.clone(),
230                error,
231            })?;
232        Ok(Self {
233            platform,
234            source,
235            location: TargetDefinitionLocation::Heuristic,
236        })
237    }
238
239    /// Converts a path ending with `.json` to a custom target triple.
240    pub(super) fn custom_from_path(
241        path: &Utf8Path,
242        source: TargetTripleSource,
243        resolve_dir: &Utf8Path,
244    ) -> Result<Self, TargetTripleError> {
245        assert_eq!(
246            path.extension(),
247            Some("json"),
248            "path {path} must end with .json",
249        );
250        let path = resolve_dir.join(path);
251        let canonicalized_path =
252            path.canonicalize_utf8()
253                .map_err(|error| TargetTripleError::TargetPathReadError {
254                    source: source.clone(),
255                    path,
256                    error,
257                })?;
258        // Strip the ".json" at the end.
259        let triple_str = canonicalized_path
260            .file_stem()
261            .expect("target path must not be empty")
262            .to_owned();
263        Self::load_file(
264            &triple_str,
265            &canonicalized_path,
266            source,
267            TargetDefinitionLocation::DirectPath(canonicalized_path.clone()),
268        )
269    }
270
271    fn load_file(
272        triple_str: &str,
273        path: &Utf8Path,
274        source: TargetTripleSource,
275        location: TargetDefinitionLocation,
276    ) -> Result<Self, TargetTripleError> {
277        let contents = std::fs::read_to_string(path).map_err(|error| {
278            TargetTripleError::TargetPathReadError {
279                source: source.clone(),
280                path: path.to_owned(),
281                error,
282            }
283        })?;
284        let platform =
285            Platform::new_custom(triple_str.to_owned(), &contents, TargetFeatures::Unknown)
286                .map_err(|error| TargetTripleError::TargetSpecError {
287                    source: source.clone(),
288                    error,
289                })?;
290        Ok(Self {
291            platform,
292            source,
293            location,
294        })
295    }
296}
297
298/// Cargo argument for downstream commands.
299///
300/// If it is necessary to run a Cargo command with a target triple, this enum provides the right
301/// invocation. Create it with [`TargetTriple::to_cargo_target_arg`].
302///
303/// The `Display` impl of this type produces the argument to provide after `--target`, or `cargo
304/// metadata --filter-platform`.
305#[derive(Debug)]
306pub enum CargoTargetArg {
307    /// The target triple is a builtin.
308    Builtin(String),
309
310    /// The target triple is a JSON file at this path.
311    Path(Utf8PathBuf),
312
313    /// The target triple was extracted from metadata and stored in a temporary directory.
314    Extracted(ExtractedCustomPlatform),
315}
316
317impl CargoTargetArg {
318    fn from_custom_json(
319        triple_str: &str,
320        json: &str,
321        source: TargetTripleSource,
322    ) -> Result<Self, TargetTripleError> {
323        let extracted = ExtractedCustomPlatform::new(triple_str, json, source)?;
324        Ok(Self::Extracted(extracted))
325    }
326}
327
328impl fmt::Display for CargoTargetArg {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        match self {
331            Self::Builtin(triple) => {
332                write!(f, "{triple}")
333            }
334            Self::Path(path) => {
335                write!(f, "{path}")
336            }
337            Self::Extracted(extracted) => {
338                write!(f, "{}", extracted.path())
339            }
340        }
341    }
342}
343
344/// The place where a target triple's configuration was picked up from.
345///
346/// This is the type of [`TargetTriple::source`].
347#[derive(Clone, Debug, PartialEq, Eq)]
348pub enum TargetTripleSource {
349    /// The target triple was defined by the --target CLI option.
350    CliOption,
351
352    /// The target triple was defined by the `CARGO_BUILD_TARGET` env var.
353    Env,
354
355    /// The target triple was defined through a `.cargo/config.toml` or `.cargo/config` file, or a
356    /// `--config` CLI option.
357    CargoConfig {
358        /// The source of the configuration.
359        source: CargoConfigSource,
360    },
361
362    /// The target triple was defined through a metadata file provided using the --archive-file or
363    /// the `--binaries-metadata` CLI option.
364    Metadata,
365}
366
367impl fmt::Display for TargetTripleSource {
368    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
369        match self {
370            Self::CliOption => {
371                write!(f, "--target <option>")
372            }
373            Self::Env => {
374                write!(f, "environment variable `CARGO_BUILD_TARGET`")
375            }
376            Self::CargoConfig {
377                source: CargoConfigSource::CliOption,
378            } => {
379                write!(f, "`build.target` specified by `--config`")
380            }
381
382            Self::CargoConfig {
383                source: CargoConfigSource::File(path),
384            } => {
385                write!(f, "`build.target` within `{path}`")
386            }
387            Self::Metadata => {
388                write!(f, "--archive-file or --binaries-metadata option")
389            }
390        }
391    }
392}
393
394/// The location a target triple's definition was obtained from.
395#[derive(Clone, Debug, Eq, PartialEq)]
396pub enum TargetDefinitionLocation {
397    /// The target triple was a builtin.
398    Builtin,
399
400    /// The definition was obtained from a file on disk -- the triple string ended with .json.
401    DirectPath(Utf8PathBuf),
402
403    /// The definition was obtained from a file in `RUST_TARGET_PATH`.
404    RustTargetPath(Utf8PathBuf),
405
406    /// The definition was obtained heuristically.
407    Heuristic,
408
409    /// A custom definition was stored in metadata. The string is the JSON of the custom target.
410    MetadataCustom(String),
411}
412
413impl fmt::Display for TargetDefinitionLocation {
414    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415        match self {
416            Self::Builtin => {
417                write!(f, "target was builtin")
418            }
419            Self::DirectPath(path) => {
420                write!(f, "definition obtained from file at path `{path}`")
421            }
422            Self::RustTargetPath(path) => {
423                write!(f, "definition obtained from RUST_TARGET_PATH: `{path}`")
424            }
425            Self::Heuristic => {
426                write!(f, "definition obtained heuristically")
427            }
428            Self::MetadataCustom(_) => {
429                write!(f, "custom definition stored in metadata")
430            }
431        }
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::cargo_config::test_helpers::{custom_platform, setup_temp_dir};
439
440    #[test]
441    fn test_find_target_triple() {
442        let dir = setup_temp_dir().unwrap();
443        let dir_path = Utf8PathBuf::try_from(dir.path().canonicalize().unwrap()).unwrap();
444        let dir_foo_path = dir_path.join("foo");
445        let dir_foo_bar_path = dir_foo_path.join("bar");
446        let dir_foo_bar_custom1_path = dir_foo_bar_path.join("custom1");
447        let dir_foo_bar_custom2_path = dir_foo_bar_path.join("custom2");
448        let custom_target_dir = dir.path().join("custom-target");
449        let custom_target_path = dir
450            .path()
451            .join("custom-target/my-target.json")
452            .canonicalize_utf8()
453            .expect("path exists");
454
455        // Test reading from config files
456        assert_eq!(
457            find_target_triple(&[], None, &dir_foo_bar_path, &dir_path),
458            Some(TargetTriple {
459                platform: platform("x86_64-unknown-linux-gnu"),
460                source: TargetTripleSource::CargoConfig {
461                    source: CargoConfigSource::File(dir_path.join("foo/bar/.cargo/config.toml")),
462                },
463                location: TargetDefinitionLocation::Builtin,
464            }),
465        );
466
467        assert_eq!(
468            find_target_triple(&[], None, &dir_foo_path, &dir_path),
469            Some(TargetTriple {
470                platform: platform("x86_64-pc-windows-msvc"),
471                source: TargetTripleSource::CargoConfig {
472                    source: CargoConfigSource::File(dir_path.join("foo/.cargo/config")),
473                },
474                location: TargetDefinitionLocation::Builtin,
475            }),
476        );
477
478        assert_eq!(
479            find_target_triple(&[], None, &dir_foo_bar_custom2_path, &dir_path),
480            Some(TargetTriple {
481                platform: custom_platform(),
482                source: TargetTripleSource::CargoConfig {
483                    source: CargoConfigSource::File(
484                        dir_path.join("foo/bar/custom2/.cargo/config.toml")
485                    ),
486                },
487                location: TargetDefinitionLocation::DirectPath(custom_target_path.clone()),
488            })
489        );
490
491        assert_eq!(
492            find_target_triple_with_paths(
493                &[],
494                None,
495                &dir_foo_bar_custom1_path,
496                &dir_path,
497                vec![custom_target_dir]
498            ),
499            Some(TargetTriple {
500                platform: custom_platform(),
501                source: TargetTripleSource::CargoConfig {
502                    source: CargoConfigSource::File(
503                        dir_path.join("foo/bar/custom1/.cargo/config.toml")
504                    ),
505                },
506                location: TargetDefinitionLocation::RustTargetPath(custom_target_path.clone()),
507            })
508        );
509
510        assert_eq!(
511            find_target_triple(
512                &["build.target=\"aarch64-unknown-linux-gnu\""],
513                None,
514                &dir_foo_bar_path,
515                &dir_path
516            ),
517            Some(TargetTriple {
518                platform: platform("aarch64-unknown-linux-gnu"),
519                source: TargetTripleSource::CargoConfig {
520                    source: CargoConfigSource::CliOption,
521                },
522                location: TargetDefinitionLocation::Builtin,
523            })
524        );
525
526        // --config arguments are followed left to right.
527        assert_eq!(
528            find_target_triple(
529                &[
530                    "build.target=\"aarch64-unknown-linux-gnu\"",
531                    "build.target=\"x86_64-unknown-linux-musl\""
532                ],
533                None,
534                &dir_foo_bar_path,
535                &dir_path
536            ),
537            Some(TargetTriple {
538                platform: platform("aarch64-unknown-linux-gnu"),
539                source: TargetTripleSource::CargoConfig {
540                    source: CargoConfigSource::CliOption,
541                },
542                location: TargetDefinitionLocation::Builtin,
543            })
544        );
545
546        // --config arguments are resolved wrt the current dir.
547        assert_eq!(
548            find_target_triple(
549                &["build.target=\"../../custom-target/my-target.json\"",],
550                None,
551                &dir_foo_bar_path,
552                &dir_path
553            ),
554            Some(TargetTriple {
555                platform: custom_platform(),
556                source: TargetTripleSource::CargoConfig {
557                    source: CargoConfigSource::CliOption,
558                },
559                location: TargetDefinitionLocation::DirectPath(custom_target_path.clone()),
560            })
561        );
562
563        // --config is preferred over the environment.
564        assert_eq!(
565            find_target_triple(
566                &["build.target=\"aarch64-unknown-linux-gnu\"",],
567                Some("aarch64-pc-windows-msvc"),
568                &dir_foo_bar_path,
569                &dir_path
570            ),
571            Some(TargetTriple {
572                platform: platform("aarch64-unknown-linux-gnu"),
573                source: TargetTripleSource::CargoConfig {
574                    source: CargoConfigSource::CliOption,
575                },
576                location: TargetDefinitionLocation::Builtin,
577            })
578        );
579
580        // The environment is preferred over local paths.
581        assert_eq!(
582            find_target_triple(
583                &[],
584                Some("aarch64-pc-windows-msvc"),
585                &dir_foo_bar_path,
586                &dir_path
587            ),
588            Some(TargetTriple {
589                platform: platform("aarch64-pc-windows-msvc"),
590                source: TargetTripleSource::Env,
591                location: TargetDefinitionLocation::Builtin,
592            })
593        );
594
595        // --config <path> should be parsed correctly. Config files passed in via --config currently
596        // come after keys and values passed in via --config, and before the environment (this
597        // didn't used to be the case in older versions of Rust, but is now the case as of Rust 1.68
598        // with https://github.com/rust-lang/cargo/pull/11077).
599        assert_eq!(
600            find_target_triple(&["extra-config.toml"], None, &dir_foo_path, &dir_path),
601            Some(TargetTriple {
602                platform: platform("aarch64-unknown-linux-gnu"),
603                source: TargetTripleSource::CargoConfig {
604                    source: CargoConfigSource::File(dir_foo_path.join("extra-config.toml")),
605                },
606                location: TargetDefinitionLocation::Builtin,
607            })
608        );
609        assert_eq!(
610            find_target_triple(
611                &["extra-config.toml"],
612                Some("aarch64-pc-windows-msvc"),
613                &dir_foo_path,
614                &dir_path
615            ),
616            Some(TargetTriple {
617                platform: platform("aarch64-unknown-linux-gnu"),
618                source: TargetTripleSource::CargoConfig {
619                    source: CargoConfigSource::File(dir_foo_path.join("extra-config.toml")),
620                },
621                location: TargetDefinitionLocation::Builtin,
622            })
623        );
624        assert_eq!(
625            find_target_triple(
626                &[
627                    "../extra-config.toml",
628                    "build.target=\"x86_64-unknown-linux-musl\"",
629                ],
630                None,
631                &dir_foo_bar_path,
632                &dir_path
633            ),
634            Some(TargetTriple {
635                platform: platform("x86_64-unknown-linux-musl"),
636                source: TargetTripleSource::CargoConfig {
637                    source: CargoConfigSource::CliOption,
638                },
639                location: TargetDefinitionLocation::Builtin,
640            })
641        );
642        assert_eq!(
643            find_target_triple(
644                &[
645                    "build.target=\"x86_64-unknown-linux-musl\"",
646                    "extra-config.toml",
647                ],
648                None,
649                &dir_foo_path,
650                &dir_path
651            ),
652            Some(TargetTriple {
653                platform: platform("x86_64-unknown-linux-musl"),
654                source: TargetTripleSource::CargoConfig {
655                    source: CargoConfigSource::CliOption,
656                },
657                location: TargetDefinitionLocation::Builtin,
658            })
659        );
660        // Config paths passed over the command line are resolved according to the directory they're
661        // in. (To test this, run the test from dir/foo/bar -- extra-custom-config should be
662        // resolved according to dir/foo).
663        assert_eq!(
664            find_target_triple(
665                &["../extra-custom-config.toml"],
666                None,
667                &dir_foo_bar_path,
668                &dir_path
669            ),
670            Some(TargetTriple {
671                platform: custom_platform(),
672                source: TargetTripleSource::CargoConfig {
673                    source: CargoConfigSource::File(dir_foo_path.join("extra-custom-config.toml")),
674                },
675                location: TargetDefinitionLocation::DirectPath(custom_target_path),
676            })
677        );
678
679        assert_eq!(find_target_triple(&[], None, &dir_path, &dir_path), None);
680    }
681
682    fn find_target_triple(
683        cli_configs: &[&str],
684        env: Option<&str>,
685        start_search_at: &Utf8Path,
686        terminate_search_at: &Utf8Path,
687    ) -> Option<TargetTriple> {
688        find_target_triple_with_paths(
689            cli_configs,
690            env,
691            start_search_at,
692            terminate_search_at,
693            Vec::new(),
694        )
695    }
696
697    fn find_target_triple_with_paths(
698        cli_configs: &[&str],
699        env: Option<&str>,
700        start_search_at: &Utf8Path,
701        terminate_search_at: &Utf8Path,
702        target_paths: Vec<Utf8PathBuf>,
703    ) -> Option<TargetTriple> {
704        let configs = CargoConfigs::new_with_isolation(
705            cli_configs,
706            start_search_at,
707            terminate_search_at,
708            target_paths,
709        )
710        .unwrap();
711        if let Some(env) = env {
712            std::env::set_var("CARGO_BUILD_TARGET", env);
713        }
714        let ret = TargetTriple::from_cargo_configs(&configs).unwrap();
715        std::env::remove_var("CARGO_BUILD_TARGET");
716        ret
717    }
718
719    fn platform(triple_str: &str) -> Platform {
720        Platform::new(triple_str.to_owned(), TargetFeatures::Unknown).expect("triple str is valid")
721    }
722}