Skip to content

Commit 40c17ed

Browse files
committed
fix: Parse cargo config files with origins
1 parent 3052b76 commit 40c17ed

File tree

4 files changed

+268
-126
lines changed

4 files changed

+268
-126
lines changed
Lines changed: 179 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,131 @@
1-
//! Read `.cargo/config.toml` as a JSON object
2-
use paths::{Utf8Path, Utf8PathBuf};
1+
//! Read `.cargo/config.toml` as a TOML table
2+
use paths::{AbsPath, Utf8Path, Utf8PathBuf};
33
use rustc_hash::FxHashMap;
4+
use toml::{
5+
Spanned,
6+
de::{DeTable, DeValue},
7+
};
48
use toolchain::Tool;
59

610
use crate::{ManifestPath, Sysroot, utf8_stdout};
711

8-
pub(crate) type CargoConfigFile = serde_json::Map<String, serde_json::Value>;
9-
10-
pub(crate) fn read(
11-
manifest: &ManifestPath,
12-
extra_env: &FxHashMap<String, Option<String>>,
13-
sysroot: &Sysroot,
14-
) -> Option<CargoConfigFile> {
15-
let mut cargo_config = sysroot.tool(Tool::Cargo, manifest.parent(), extra_env);
16-
cargo_config
17-
.args(["-Z", "unstable-options", "config", "get", "--format", "json"])
18-
.env("RUSTC_BOOTSTRAP", "1");
19-
if manifest.is_rust_manifest() {
20-
cargo_config.arg("-Zscript");
21-
}
22-
23-
tracing::debug!("Discovering cargo config by {:?}", cargo_config);
24-
let json: serde_json::Map<String, serde_json::Value> = utf8_stdout(&mut cargo_config)
25-
.inspect(|json| {
26-
tracing::debug!("Discovered cargo config: {:?}", json);
27-
})
28-
.inspect_err(|err| {
29-
tracing::debug!("Failed to discover cargo config: {:?}", err);
30-
})
31-
.ok()
32-
.and_then(|stdout| serde_json::from_str(&stdout).ok())?;
33-
34-
Some(json)
12+
#[derive(Clone)]
13+
pub struct CargoConfigFile(String);
14+
15+
impl 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+
51+
pub(crate) struct CargoConfigFileReader<'a> {
52+
toml_str: &'a str,
53+
line_ends: Vec<usize>,
54+
table: Spanned<DeTable<'a>>,
55+
}
56+
57+
impl<'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 line_ends = toml_str.lines().fold(vec![], |mut acc, l| {
63+
acc.push(acc.last().copied().unwrap_or(0_usize) + l.len() + 1);
64+
acc
65+
});
66+
67+
Some(CargoConfigFileReader { toml_str, table: toml, line_ends })
68+
}
69+
70+
pub(crate) fn get_spanned(
71+
&self,
72+
accessor: impl IntoIterator<Item = &'a str>,
73+
) -> Option<&Spanned<DeValue<'a>>> {
74+
let mut keys = accessor.into_iter();
75+
let mut val = self.table.get_ref().get(keys.next()?)?;
76+
for key in keys {
77+
let DeValue::Table(map) = val.get_ref() else { return None };
78+
val = map.get(key)?;
79+
}
80+
Some(val)
81+
}
82+
83+
pub(crate) fn get(&self, accessor: impl IntoIterator<Item = &'a str>) -> Option<&DeValue<'a>> {
84+
self.get_spanned(accessor).map(|it| it.as_ref())
85+
}
86+
87+
pub(crate) fn get_origin_root(&self, spanned: &Spanned<DeValue<'a>>) -> Option<&AbsPath> {
88+
let span = spanned.span();
89+
90+
for &line_end in &self.line_ends {
91+
if line_end < span.end {
92+
continue;
93+
}
94+
95+
let after_span = &self.toml_str[span.end..line_end];
96+
97+
// table.key = "value" # /parent/.cargo/config.toml
98+
// | |
99+
// span.end end
100+
let origin_path = after_span
101+
.strip_prefix([',']) // strip trailing comma
102+
.unwrap_or(after_span)
103+
.trim_start()
104+
.strip_prefix(['#'])
105+
.and_then(|path| {
106+
let path = path.trim();
107+
if path.starts_with("environment variable")
108+
|| path.starts_with("--config cli option")
109+
{
110+
None
111+
} else {
112+
Some(path)
113+
}
114+
});
115+
116+
return origin_path.and_then(|path| {
117+
<&Utf8Path>::from(path)
118+
.try_into()
119+
.ok()
120+
// Two levels up to the config file.
121+
// See https://doc.rust-lang.org/cargo/reference/config.html#config-relative-paths
122+
.and_then(AbsPath::parent)
123+
.and_then(AbsPath::parent)
124+
});
125+
}
126+
127+
None
128+
}
35129
}
36130

37131
pub(crate) fn make_lockfile_copy(
@@ -54,3 +148,59 @@ pub(crate) fn make_lockfile_copy(
54148
}
55149
}
56150
}
151+
152+
#[test]
153+
fn cargo_config_file_reader_works() {
154+
let toml = r##"
155+
alias.foo = "abc"
156+
alias.bar = "🙂" # /ROOT/home/.cargo/config.toml
157+
alias.sub-example = [
158+
"sub", # /ROOT/foo/.cargo/config.toml
159+
"example", # /ROOT/bar/.cargo/config.toml
160+
]
161+
build.rustflags = [
162+
"--flag", # /ROOT/home/.cargo/config.toml
163+
"env", # environment variable `CARGO_BUILD_RUSTFLAGS`
164+
"cli", # --config cli option
165+
]
166+
env.CARGO_WORKSPACE_DIR.relative = true # /ROOT/home/.cargo/config.toml
167+
env.CARGO_WORKSPACE_DIR.value = "" # /ROOT/home/.cargo/config.toml
168+
"##;
169+
#[cfg(target_os = "windows")]
170+
let toml = &toml.replace("/ROOT", "C:/");
171+
172+
let reader = CargoConfigFileReader::new(toml).unwrap();
173+
174+
let alias_foo = reader.get_spanned(["alias", "foo"]).unwrap();
175+
assert_eq!(alias_foo.as_ref().as_str().unwrap(), "abc");
176+
assert!(reader.get_origin_root(alias_foo).is_none());
177+
178+
let alias_bar = reader.get_spanned(["alias", "bar"]).unwrap();
179+
assert_eq!(alias_bar.as_ref().as_str().unwrap(), "🙂");
180+
assert_eq!(reader.get_origin_root(alias_bar).unwrap().as_str(), "/ROOT/home");
181+
182+
let alias_sub_example = reader.get_spanned(["alias", "sub-example"]).unwrap();
183+
assert!(reader.get_origin_root(alias_sub_example).is_none());
184+
let alias_sub_example = alias_sub_example.as_ref().as_array().unwrap();
185+
186+
assert_eq!(alias_sub_example[0].get_ref().as_str().unwrap(), "sub");
187+
assert_eq!(reader.get_origin_root(&alias_sub_example[0]).unwrap().as_str(), "/ROOT/foo");
188+
189+
assert_eq!(alias_sub_example[1].get_ref().as_str().unwrap(), "example");
190+
assert_eq!(reader.get_origin_root(&alias_sub_example[1]).unwrap().as_str(), "/ROOT/bar");
191+
192+
let build_rustflags = reader.get(["build", "rustflags"]).unwrap().as_array().unwrap();
193+
assert_eq!(reader.get_origin_root(&build_rustflags[0]).unwrap().as_str(), "/ROOT/home");
194+
assert!(reader.get_origin_root(&build_rustflags[1]).is_none());
195+
assert!(reader.get_origin_root(&build_rustflags[2]).is_none());
196+
197+
let env_cargo_workspace_dir =
198+
reader.get(["env", "CARGO_WORKSPACE_DIR"]).unwrap().as_table().unwrap();
199+
let env_relative = &env_cargo_workspace_dir["relative"];
200+
assert!(env_relative.as_ref().as_bool().unwrap());
201+
assert_eq!(reader.get_origin_root(env_relative).unwrap().as_str(), "/ROOT/home");
202+
203+
let env_val = &env_cargo_workspace_dir["value"];
204+
assert_eq!(env_val.as_ref().as_str().unwrap(), "");
205+
assert_eq!(reader.get_origin_root(env_val).unwrap().as_str(), "/ROOT/home");
206+
}

crates/project-model/src/env.rs

Lines changed: 46 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
//! Cargo-like environment variables injection.
22
use base_db::Env;
3-
use paths::Utf8Path;
43
use rustc_hash::FxHashMap;
54
use toolchain::Tool;
65

7-
use crate::{ManifestPath, PackageData, TargetKind, cargo_config_file::CargoConfigFile};
6+
use crate::{PackageData, TargetKind, cargo_config_file::CargoConfigFile};
87

98
/// Recreates the compile-time environment variables that Cargo sets.
109
///
@@ -62,46 +61,48 @@ pub(crate) fn inject_rustc_tool_env(env: &mut Env, cargo_name: &str, kind: Targe
6261
}
6362

6463
pub(crate) fn cargo_config_env(
65-
manifest: &ManifestPath,
6664
config: &Option<CargoConfigFile>,
6765
extra_env: &FxHashMap<String, Option<String>>,
6866
) -> Env {
67+
use toml::de::*;
68+
6969
let mut env = Env::default();
7070
env.extend(extra_env.iter().filter_map(|(k, v)| v.as_ref().map(|v| (k.clone(), v.clone()))));
7171

72-
let Some(serde_json::Value::Object(env_json)) = config.as_ref().and_then(|c| c.get("env"))
73-
else {
72+
let Some(config_reader) = config.as_ref().and_then(|c| c.read()) else {
73+
return env;
74+
};
75+
let Some(env_toml) = config_reader.get(["env"]).and_then(|it| it.as_table()) else {
7476
return env;
7577
};
7678

77-
// FIXME: The base here should be the parent of the `.cargo/config` file, not the manifest.
78-
// But cargo does not provide this information.
79-
let base = <_ as AsRef<Utf8Path>>::as_ref(manifest.parent());
80-
81-
for (key, entry) in env_json {
82-
let value = match entry {
83-
serde_json::Value::String(s) => s.clone(),
84-
serde_json::Value::Object(entry) => {
79+
for (key, entry) in env_toml {
80+
let key = key.as_ref().as_ref();
81+
let value = match entry.as_ref() {
82+
DeValue::String(s) => String::from(s.clone()),
83+
DeValue::Table(entry) => {
8584
// Each entry MUST have a `value` key.
86-
let Some(value) = entry.get("value").and_then(|v| v.as_str()) else {
85+
let Some(map) = entry.get("value").and_then(|v| v.as_ref().as_str()) else {
8786
continue;
8887
};
8988
// If the entry already exists in the environment AND the `force` key is not set to
9089
// true, then don't overwrite the value.
9190
if extra_env.get(key).is_some_and(Option::is_some)
92-
&& !entry.get("force").and_then(|v| v.as_bool()).unwrap_or(false)
91+
&& !entry.get("force").and_then(|v| v.as_ref().as_bool()).unwrap_or(false)
9392
{
9493
continue;
9594
}
9695

97-
if entry
98-
.get("relative")
99-
.and_then(|v| v.as_bool())
100-
.is_some_and(std::convert::identity)
101-
{
102-
base.join(value).to_string()
96+
if let Some(base) = entry.get("relative").and_then(|v| {
97+
if v.as_ref().as_bool().is_some_and(std::convert::identity) {
98+
config_reader.get_origin_root(v)
99+
} else {
100+
None
101+
}
102+
}) {
103+
base.join(map).to_string()
103104
} else {
104-
value.to_owned()
105+
map.to_owned()
105106
}
106107
}
107108
_ => continue,
@@ -115,43 +116,30 @@ pub(crate) fn cargo_config_env(
115116

116117
#[test]
117118
fn parse_output_cargo_config_env_works() {
119+
use itertools::Itertools;
120+
121+
let cwd = paths::AbsPathBuf::try_from(
122+
paths::Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap(),
123+
)
124+
.unwrap();
125+
let config_path = cwd.join(".cargo").join("config.toml");
118126
let raw = r#"
119-
{
120-
"env": {
121-
"CARGO_WORKSPACE_DIR": {
122-
"relative": true,
123-
"value": ""
124-
},
125-
"INVALID": {
126-
"relative": "invalidbool",
127-
"value": "../relative"
128-
},
129-
"RELATIVE": {
130-
"relative": true,
131-
"value": "../relative"
132-
},
133-
"TEST": {
134-
"value": "test"
135-
},
136-
"FORCED": {
137-
"value": "test",
138-
"force": true
139-
},
140-
"UNFORCED": {
141-
"value": "test",
142-
"force": false
143-
},
144-
"OVERWRITTEN": {
145-
"value": "test"
146-
},
147-
"NOT_AN_OBJECT": "value"
148-
}
149-
}
127+
env.CARGO_WORKSPACE_DIR.relative = true
128+
env.CARGO_WORKSPACE_DIR.value = ""
129+
env.INVALID.relative = "invalidbool"
130+
env.INVALID.value = "../relative"
131+
env.RELATIVE.relative = true
132+
env.RELATIVE.value = "../relative"
133+
env.TEST.value = "test"
134+
env.FORCED.value = "test"
135+
env.FORCED.force = true
136+
env.UNFORCED.value = "test"
137+
env.UNFORCED.forced = false
138+
env.OVERWRITTEN.value = "test"
139+
env.NOT_AN_OBJECT = "value"
150140
"#;
151-
let config: CargoConfigFile = serde_json::from_str(raw).unwrap();
152-
let cwd = paths::Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
153-
let manifest = paths::AbsPathBuf::assert(cwd.join("Cargo.toml"));
154-
let manifest = ManifestPath::try_from(manifest).unwrap();
141+
let raw = raw.lines().map(|l| format!("{l} # {config_path}")).join("\n");
142+
let config = CargoConfigFile::from_string_for_test(raw);
155143
let extra_env = [
156144
("FORCED", Some("ignored")),
157145
("UNFORCED", Some("newvalue")),
@@ -161,7 +149,7 @@ fn parse_output_cargo_config_env_works() {
161149
.iter()
162150
.map(|(k, v)| (k.to_string(), v.map(ToString::to_string)))
163151
.collect();
164-
let env = cargo_config_env(&manifest, &Some(config), &extra_env);
152+
let env = cargo_config_env(&Some(config), &extra_env);
165153
assert_eq!(env.get("CARGO_WORKSPACE_DIR").as_deref(), Some(cwd.join("").as_str()));
166154
assert_eq!(env.get("RELATIVE").as_deref(), Some(cwd.join("../relative").as_str()));
167155
assert_eq!(env.get("INVALID").as_deref(), Some("../relative"));

0 commit comments

Comments
 (0)