Skip to main content

project_model/
cargo_config_file.rs

1//! Read `.cargo/config.toml` as a TOML table
2use paths::{AbsPath, Utf8Path, Utf8PathBuf};
3use rustc_hash::FxHashMap;
4use toml::{
5    Spanned,
6    de::{DeTable, DeValue},
7};
8use toolchain::Tool;
9
10use crate::{ManifestPath, Sysroot, utf8_stdout};
11
12#[derive(Clone)]
13pub struct CargoConfigFile(String);
14
15impl CargoConfigFile {
16    pub(crate) fn load(
17        manifest: &ManifestPath,
18        extra_env: &FxHashMap<String, Option<String>>,
19        sysroot: &Sysroot,
20    ) -> Option<Self> {
21        let mut cargo_config = sysroot.tool(Tool::Cargo, manifest.parent(), extra_env);
22        cargo_config
23            .args(["-Z", "unstable-options", "config", "get", "--format", "toml", "--show-origin"])
24            .env("RUSTC_BOOTSTRAP", "1");
25        if manifest.is_rust_manifest() {
26            cargo_config.arg("-Zscript");
27        }
28
29        tracing::debug!("Discovering cargo config by {cargo_config:?}");
30        utf8_stdout(&mut cargo_config)
31            .inspect(|toml| {
32                tracing::debug!("Discovered cargo config: {toml:?}");
33            })
34            .inspect_err(|err| {
35                tracing::debug!("Failed to discover cargo config: {err:?}");
36            })
37            .ok()
38            .map(CargoConfigFile)
39    }
40
41    pub(crate) fn read<'a>(&'a self) -> Option<CargoConfigFileReader<'a>> {
42        CargoConfigFileReader::new(&self.0)
43    }
44
45    #[cfg(test)]
46    pub(crate) fn from_string_for_test(s: String) -> Self {
47        CargoConfigFile(s)
48    }
49}
50
51pub(crate) struct CargoConfigFileReader<'a> {
52    toml_str: &'a str,
53    line_ends: Vec<usize>,
54    table: Spanned<DeTable<'a>>,
55}
56
57impl<'a> CargoConfigFileReader<'a> {
58    fn new(toml_str: &'a str) -> Option<Self> {
59        let toml = DeTable::parse(toml_str)
60            .inspect_err(|err| tracing::debug!("Failed to parse cargo config into toml: {err:?}"))
61            .ok()?;
62        let mut last_line_end = 0;
63        let line_ends = toml_str
64            .lines()
65            .map(|l| {
66                last_line_end += l.len() + 1;
67                last_line_end
68            })
69            .collect();
70
71        Some(CargoConfigFileReader { toml_str, table: toml, line_ends })
72    }
73
74    pub(crate) fn get_spanned(
75        &self,
76        accessor: impl IntoIterator<Item = &'a str>,
77    ) -> Option<&Spanned<DeValue<'a>>> {
78        let mut keys = accessor.into_iter();
79        let mut val = self.table.get_ref().get(keys.next()?)?;
80        for key in keys {
81            let DeValue::Table(map) = val.get_ref() else { return None };
82            val = map.get(key)?;
83        }
84        Some(val)
85    }
86
87    pub(crate) fn get(&self, accessor: impl IntoIterator<Item = &'a str>) -> Option<&DeValue<'a>> {
88        self.get_spanned(accessor).map(|it| it.as_ref())
89    }
90
91    pub(crate) fn get_origin_root(&self, spanned: &Spanned<DeValue<'a>>) -> Option<&AbsPath> {
92        let span = spanned.span();
93
94        for &line_end in &self.line_ends {
95            if line_end < span.end {
96                continue;
97            }
98
99            let after_span = &self.toml_str[span.end..line_end];
100
101            // table.key = "value" # /parent/.cargo/config.toml
102            //                   |                            |
103            //                   span.end                     line_end
104            let origin_path = after_span
105                .strip_prefix([',']) // strip trailing comma
106                .unwrap_or(after_span)
107                .trim_start()
108                .strip_prefix(['#'])
109                .and_then(|path| {
110                    let path = path.trim();
111                    if path.starts_with("environment variable")
112                        || path.starts_with("--config cli option")
113                    {
114                        None
115                    } else {
116                        Some(path)
117                    }
118                });
119
120            return origin_path.and_then(|path| {
121                <&Utf8Path>::from(path)
122                    .try_into()
123                    .ok()
124                    // Two levels up to the config file.
125                    // See https://doc.rust-lang.org/cargo/reference/config.html#config-relative-paths
126                    .and_then(AbsPath::parent)
127                    .and_then(AbsPath::parent)
128            });
129        }
130
131        None
132    }
133}
134
135pub(crate) struct LockfileCopy {
136    pub(crate) path: Utf8PathBuf,
137    pub(crate) usage: LockfileUsage,
138    _temp_dir: temp_dir::TempDir,
139}
140
141pub(crate) enum LockfileUsage {
142    /// Rust [1.82.0, 1.95.0). `cargo <subcmd> --lockfile-path <lockfile path>`
143    WithFlag,
144    /// Rust >= 1.95.0. `CARGO_RESOLVER_LOCKFILE_PATH=<lockfile path> cargo <subcmd>`
145    WithEnvVar,
146}
147
148pub(crate) fn make_lockfile_copy(
149    toolchain_version: &semver::Version,
150    lockfile_path: &Utf8Path,
151) -> Option<LockfileCopy> {
152    const MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH_FLAG: semver::Version =
153        semver::Version {
154            major: 1,
155            minor: 82,
156            patch: 0,
157            pre: semver::Prerelease::EMPTY,
158            build: semver::BuildMetadata::EMPTY,
159        };
160
161    const MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH_ENV: semver::Version =
162        semver::Version {
163            major: 1,
164            minor: 95,
165            patch: 0,
166            pre: semver::Prerelease::EMPTY,
167            build: semver::BuildMetadata::EMPTY,
168        };
169
170    let usage = if *toolchain_version >= MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH_ENV {
171        LockfileUsage::WithEnvVar
172    } else if *toolchain_version >= MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH_FLAG {
173        LockfileUsage::WithFlag
174    } else {
175        return None;
176    };
177
178    let temp_dir = temp_dir::TempDir::with_prefix("rust-analyzer").ok()?;
179    let path: Utf8PathBuf = temp_dir.path().join("Cargo.lock").try_into().ok()?;
180    let path = match std::fs::copy(lockfile_path, &path) {
181        Ok(_) => {
182            tracing::debug!("Copied lock file from `{}` to `{}`", lockfile_path, path);
183            path
184        }
185        // lockfile does not yet exist, so we can just create a new one in the temp dir
186        Err(e) if e.kind() == std::io::ErrorKind::NotFound => path,
187        Err(e) => {
188            tracing::warn!("Failed to copy lock file from `{lockfile_path}` to `{path}`: {e}",);
189            return None;
190        }
191    };
192
193    Some(LockfileCopy { path, usage, _temp_dir: temp_dir })
194}
195
196#[test]
197fn cargo_config_file_reader_works() {
198    #[cfg(target_os = "windows")]
199    let root = "C://ROOT";
200
201    #[cfg(not(target_os = "windows"))]
202    let root = "/ROOT";
203
204    let toml = format!(
205        r##"
206alias.foo = "abc"
207alias.bar = "🙂" # {root}/home/.cargo/config.toml
208alias.sub-example = [
209    "sub", # {root}/foo/.cargo/config.toml
210    "example", # {root}/❤️💛💙/💝/.cargo/config.toml
211]
212build.rustflags = [
213    "--flag", # {root}/home/.cargo/config.toml
214    "env", # environment variable `CARGO_BUILD_RUSTFLAGS`
215    "cli", # --config cli option
216]
217env.CARGO_WORKSPACE_DIR.relative = true # {root}/home/.cargo/config.toml
218env.CARGO_WORKSPACE_DIR.value = "" # {root}/home/.cargo/config.toml
219"##
220    );
221
222    let reader = CargoConfigFileReader::new(&toml).unwrap();
223
224    let alias_foo = reader.get_spanned(["alias", "foo"]).unwrap();
225    assert_eq!(alias_foo.as_ref().as_str().unwrap(), "abc");
226    assert!(reader.get_origin_root(alias_foo).is_none());
227
228    let alias_bar = reader.get_spanned(["alias", "bar"]).unwrap();
229    assert_eq!(alias_bar.as_ref().as_str().unwrap(), "🙂");
230    assert_eq!(reader.get_origin_root(alias_bar).unwrap().as_str(), format!("{root}/home"));
231
232    let alias_sub_example = reader.get_spanned(["alias", "sub-example"]).unwrap();
233    assert!(reader.get_origin_root(alias_sub_example).is_none());
234    let alias_sub_example = alias_sub_example.as_ref().as_array().unwrap();
235
236    assert_eq!(alias_sub_example[0].get_ref().as_str().unwrap(), "sub");
237    assert_eq!(
238        reader.get_origin_root(&alias_sub_example[0]).unwrap().as_str(),
239        format!("{root}/foo")
240    );
241
242    assert_eq!(alias_sub_example[1].get_ref().as_str().unwrap(), "example");
243    assert_eq!(
244        reader.get_origin_root(&alias_sub_example[1]).unwrap().as_str(),
245        format!("{root}/❤️💛💙/💝")
246    );
247
248    let build_rustflags = reader.get(["build", "rustflags"]).unwrap().as_array().unwrap();
249    assert_eq!(
250        reader.get_origin_root(&build_rustflags[0]).unwrap().as_str(),
251        format!("{root}/home")
252    );
253    assert!(reader.get_origin_root(&build_rustflags[1]).is_none());
254    assert!(reader.get_origin_root(&build_rustflags[2]).is_none());
255
256    let env_cargo_workspace_dir =
257        reader.get(["env", "CARGO_WORKSPACE_DIR"]).unwrap().as_table().unwrap();
258    let env_relative = &env_cargo_workspace_dir["relative"];
259    assert!(env_relative.as_ref().as_bool().unwrap());
260    assert_eq!(reader.get_origin_root(env_relative).unwrap().as_str(), format!("{root}/home"));
261
262    let env_val = &env_cargo_workspace_dir["value"];
263    assert_eq!(env_val.as_ref().as_str().unwrap(), "");
264    assert_eq!(reader.get_origin_root(env_val).unwrap().as_str(), format!("{root}/home"));
265}