project_model/
cargo_config_file.rs1use 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 let origin_path = after_span
105 .strip_prefix([',']) .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 .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 WithFlag,
144 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 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}