project_model/toolchain_info/
target_tuple.rs

1//! Functionality to discover the current build target(s).
2use std::path::Path;
3
4use anyhow::Context;
5use rustc_hash::FxHashMap;
6use toolchain::Tool;
7
8use crate::{
9    Sysroot, cargo_config_file::CargoConfigFile, toolchain_info::QueryConfig, utf8_stdout,
10};
11
12/// For cargo, runs `cargo -Zunstable-options config get build.target` to get the configured project target(s).
13/// For rustc, runs `rustc --print -vV` to get the host target.
14pub fn get(
15    config: QueryConfig<'_>,
16    target: Option<&str>,
17    extra_env: &FxHashMap<String, Option<String>>,
18) -> anyhow::Result<Vec<String>> {
19    let _p = tracing::info_span!("target_tuple::get").entered();
20    if let Some(target) = target {
21        return Ok(vec![target.to_owned()]);
22    }
23
24    let (sysroot, current_dir) = match config {
25        QueryConfig::Cargo(sysroot, cargo_toml, config_file) => {
26            match config_file.as_ref().and_then(cargo_config_build_target) {
27                Some(it) => return Ok(it),
28                None => (sysroot, cargo_toml.parent().as_ref()),
29            }
30        }
31        QueryConfig::Rustc(sysroot, current_dir) => (sysroot, current_dir),
32    };
33    rustc_discover_host_tuple(extra_env, sysroot, current_dir).map(|it| vec![it])
34}
35
36fn rustc_discover_host_tuple(
37    extra_env: &FxHashMap<String, Option<String>>,
38    sysroot: &Sysroot,
39    current_dir: &Path,
40) -> anyhow::Result<String> {
41    let mut cmd = sysroot.tool(Tool::Rustc, current_dir, extra_env);
42    cmd.arg("-vV");
43    let stdout = utf8_stdout(&mut cmd)
44        .with_context(|| format!("unable to discover host platform via `{cmd:?}`"))?;
45    let field = "host: ";
46    let target = stdout.lines().find_map(|l| l.strip_prefix(field));
47    if let Some(target) = target {
48        Ok(target.to_owned())
49    } else {
50        // If we fail to resolve the host platform, it's not the end of the world.
51        Err(anyhow::format_err!("rustc -vV did not report host platform, got:\n{}", stdout))
52    }
53}
54
55fn cargo_config_build_target(config: &CargoConfigFile) -> Option<Vec<String>> {
56    match parse_toml_cargo_config_build_target(config) {
57        Ok(v) => v,
58        Err(e) => {
59            tracing::debug!("Failed to discover cargo config build target {e:?}");
60            None
61        }
62    }
63}
64
65// Parses `"build.target = [target-tuple, target-tuple, ...]"` or `"build.target = "target-tuple"`
66fn parse_toml_cargo_config_build_target(
67    config: &CargoConfigFile,
68) -> anyhow::Result<Option<Vec<String>>> {
69    let Some(config_reader) = config.read() else {
70        return Ok(None);
71    };
72    let Some(target) = config_reader.get_spanned(["build", "target"]) else {
73        return Ok(None);
74    };
75
76    // if the target ends with `.json`, join it to the config file's parent dir.
77    // See https://github.com/rust-lang/cargo/blob/f7acf448fc127df9a77c52cc2bba027790ac4931/src/cargo/core/compiler/compile_kind.rs#L171-L192
78    let join_to_origin_if_json_path = |s: &str, spanned: &toml::Spanned<toml::de::DeValue<'_>>| {
79        if s.ends_with(".json") {
80            config_reader
81                .get_origin_root(spanned)
82                .map(|p| p.join(s).to_string())
83                .unwrap_or_else(|| s.to_owned())
84        } else {
85            s.to_owned()
86        }
87    };
88
89    let parse_err = "Failed to parse `build.target` as an array of target";
90
91    match target.as_ref() {
92        toml::de::DeValue::String(s) => {
93            Ok(Some(vec![join_to_origin_if_json_path(s.as_ref(), target)]))
94        }
95        toml::de::DeValue::Array(arr) => arr
96            .iter()
97            .map(|v| {
98                let s = v.as_ref().as_str().context(parse_err)?;
99                Ok(join_to_origin_if_json_path(s, v))
100            })
101            .collect::<anyhow::Result<_>>()
102            .map(Option::Some),
103        _ => Err(anyhow::anyhow!(parse_err)),
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use paths::{AbsPathBuf, Utf8PathBuf};
110
111    use crate::{ManifestPath, Sysroot};
112
113    use super::*;
114
115    #[test]
116    fn cargo() {
117        let manifest_path = concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml");
118        let sysroot = Sysroot::empty();
119        let manifest_path =
120            ManifestPath::try_from(AbsPathBuf::assert(Utf8PathBuf::from(manifest_path))).unwrap();
121        let cfg = QueryConfig::Cargo(&sysroot, &manifest_path, &None);
122        assert!(get(cfg, None, &FxHashMap::default()).is_ok());
123    }
124
125    #[test]
126    fn rustc() {
127        let sysroot = Sysroot::empty();
128        let cfg = QueryConfig::Rustc(&sysroot, env!("CARGO_MANIFEST_DIR").as_ref());
129        assert!(get(cfg, None, &FxHashMap::default()).is_ok());
130    }
131}