Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/project-model/src/cargo_config_file.rs')
| -rw-r--r-- | crates/project-model/src/cargo_config_file.rs | 227 |
1 files changed, 198 insertions, 29 deletions
diff --git a/crates/project-model/src/cargo_config_file.rs b/crates/project-model/src/cargo_config_file.rs index a1e7ed0923..5d6e5fd648 100644 --- a/crates/project-model/src/cargo_config_file.rs +++ b/crates/project-model/src/cargo_config_file.rs @@ -1,37 +1,135 @@ -//! Read `.cargo/config.toml` as a JSON object -use paths::{Utf8Path, Utf8PathBuf}; +//! Read `.cargo/config.toml` as a TOML table +use paths::{AbsPath, Utf8Path, Utf8PathBuf}; use rustc_hash::FxHashMap; +use toml::{ + Spanned, + de::{DeTable, DeValue}, +}; use toolchain::Tool; use crate::{ManifestPath, Sysroot, utf8_stdout}; -pub(crate) type CargoConfigFile = serde_json::Map<String, serde_json::Value>; - -pub(crate) fn read( - manifest: &ManifestPath, - extra_env: &FxHashMap<String, Option<String>>, - sysroot: &Sysroot, -) -> Option<CargoConfigFile> { - let mut cargo_config = sysroot.tool(Tool::Cargo, manifest.parent(), extra_env); - cargo_config - .args(["-Z", "unstable-options", "config", "get", "--format", "json"]) - .env("RUSTC_BOOTSTRAP", "1"); - if manifest.is_rust_manifest() { - cargo_config.arg("-Zscript"); - } - - tracing::debug!("Discovering cargo config by {:?}", cargo_config); - let json: serde_json::Map<String, serde_json::Value> = utf8_stdout(&mut cargo_config) - .inspect(|json| { - tracing::debug!("Discovered cargo config: {:?}", json); - }) - .inspect_err(|err| { - tracing::debug!("Failed to discover cargo config: {:?}", err); - }) - .ok() - .and_then(|stdout| serde_json::from_str(&stdout).ok())?; - - Some(json) +#[derive(Clone)] +pub struct CargoConfigFile(String); + +impl CargoConfigFile { + pub(crate) fn load( + manifest: &ManifestPath, + extra_env: &FxHashMap<String, Option<String>>, + sysroot: &Sysroot, + ) -> Option<Self> { + let mut cargo_config = sysroot.tool(Tool::Cargo, manifest.parent(), extra_env); + cargo_config + .args(["-Z", "unstable-options", "config", "get", "--format", "toml", "--show-origin"]) + .env("RUSTC_BOOTSTRAP", "1"); + if manifest.is_rust_manifest() { + cargo_config.arg("-Zscript"); + } + + tracing::debug!("Discovering cargo config by {cargo_config:?}"); + utf8_stdout(&mut cargo_config) + .inspect(|toml| { + tracing::debug!("Discovered cargo config: {toml:?}"); + }) + .inspect_err(|err| { + tracing::debug!("Failed to discover cargo config: {err:?}"); + }) + .ok() + .map(CargoConfigFile) + } + + pub(crate) fn read<'a>(&'a self) -> Option<CargoConfigFileReader<'a>> { + CargoConfigFileReader::new(&self.0) + } + + #[cfg(test)] + pub(crate) fn from_string_for_test(s: String) -> Self { + CargoConfigFile(s) + } +} + +pub(crate) struct CargoConfigFileReader<'a> { + toml_str: &'a str, + line_ends: Vec<usize>, + table: Spanned<DeTable<'a>>, +} + +impl<'a> CargoConfigFileReader<'a> { + fn new(toml_str: &'a str) -> Option<Self> { + let toml = DeTable::parse(toml_str) + .inspect_err(|err| tracing::debug!("Failed to parse cargo config into toml: {err:?}")) + .ok()?; + let mut last_line_end = 0; + let line_ends = toml_str + .lines() + .map(|l| { + last_line_end += l.len() + 1; + last_line_end + }) + .collect(); + + Some(CargoConfigFileReader { toml_str, table: toml, line_ends }) + } + + pub(crate) fn get_spanned( + &self, + accessor: impl IntoIterator<Item = &'a str>, + ) -> Option<&Spanned<DeValue<'a>>> { + let mut keys = accessor.into_iter(); + let mut val = self.table.get_ref().get(keys.next()?)?; + for key in keys { + let DeValue::Table(map) = val.get_ref() else { return None }; + val = map.get(key)?; + } + Some(val) + } + + pub(crate) fn get(&self, accessor: impl IntoIterator<Item = &'a str>) -> Option<&DeValue<'a>> { + self.get_spanned(accessor).map(|it| it.as_ref()) + } + + pub(crate) fn get_origin_root(&self, spanned: &Spanned<DeValue<'a>>) -> Option<&AbsPath> { + let span = spanned.span(); + + for &line_end in &self.line_ends { + if line_end < span.end { + continue; + } + + let after_span = &self.toml_str[span.end..line_end]; + + // table.key = "value" # /parent/.cargo/config.toml + // | | + // span.end line_end + let origin_path = after_span + .strip_prefix([',']) // strip trailing comma + .unwrap_or(after_span) + .trim_start() + .strip_prefix(['#']) + .and_then(|path| { + let path = path.trim(); + if path.starts_with("environment variable") + || path.starts_with("--config cli option") + { + None + } else { + Some(path) + } + }); + + return origin_path.and_then(|path| { + <&Utf8Path>::from(path) + .try_into() + .ok() + // Two levels up to the config file. + // See https://doc.rust-lang.org/cargo/reference/config.html#config-relative-paths + .and_then(AbsPath::parent) + .and_then(AbsPath::parent) + }); + } + + None + } } pub(crate) fn make_lockfile_copy( @@ -54,3 +152,74 @@ pub(crate) fn make_lockfile_copy( } } } + +#[test] +fn cargo_config_file_reader_works() { + #[cfg(target_os = "windows")] + let root = "C://ROOT"; + + #[cfg(not(target_os = "windows"))] + let root = "/ROOT"; + + let toml = format!( + r##" +alias.foo = "abc" +alias.bar = "🙂" # {root}/home/.cargo/config.toml +alias.sub-example = [ + "sub", # {root}/foo/.cargo/config.toml + "example", # {root}/❤️💛💙/💝/.cargo/config.toml +] +build.rustflags = [ + "--flag", # {root}/home/.cargo/config.toml + "env", # environment variable `CARGO_BUILD_RUSTFLAGS` + "cli", # --config cli option +] +env.CARGO_WORKSPACE_DIR.relative = true # {root}/home/.cargo/config.toml +env.CARGO_WORKSPACE_DIR.value = "" # {root}/home/.cargo/config.toml +"## + ); + + let reader = CargoConfigFileReader::new(&toml).unwrap(); + + let alias_foo = reader.get_spanned(["alias", "foo"]).unwrap(); + assert_eq!(alias_foo.as_ref().as_str().unwrap(), "abc"); + assert!(reader.get_origin_root(alias_foo).is_none()); + + let alias_bar = reader.get_spanned(["alias", "bar"]).unwrap(); + assert_eq!(alias_bar.as_ref().as_str().unwrap(), "🙂"); + assert_eq!(reader.get_origin_root(alias_bar).unwrap().as_str(), format!("{root}/home")); + + let alias_sub_example = reader.get_spanned(["alias", "sub-example"]).unwrap(); + assert!(reader.get_origin_root(alias_sub_example).is_none()); + let alias_sub_example = alias_sub_example.as_ref().as_array().unwrap(); + + assert_eq!(alias_sub_example[0].get_ref().as_str().unwrap(), "sub"); + assert_eq!( + reader.get_origin_root(&alias_sub_example[0]).unwrap().as_str(), + format!("{root}/foo") + ); + + assert_eq!(alias_sub_example[1].get_ref().as_str().unwrap(), "example"); + assert_eq!( + reader.get_origin_root(&alias_sub_example[1]).unwrap().as_str(), + format!("{root}/❤️💛💙/💝") + ); + + let build_rustflags = reader.get(["build", "rustflags"]).unwrap().as_array().unwrap(); + assert_eq!( + reader.get_origin_root(&build_rustflags[0]).unwrap().as_str(), + format!("{root}/home") + ); + assert!(reader.get_origin_root(&build_rustflags[1]).is_none()); + assert!(reader.get_origin_root(&build_rustflags[2]).is_none()); + + let env_cargo_workspace_dir = + reader.get(["env", "CARGO_WORKSPACE_DIR"]).unwrap().as_table().unwrap(); + let env_relative = &env_cargo_workspace_dir["relative"]; + assert!(env_relative.as_ref().as_bool().unwrap()); + assert_eq!(reader.get_origin_root(env_relative).unwrap().as_str(), format!("{root}/home")); + + let env_val = &env_cargo_workspace_dir["value"]; + assert_eq!(env_val.as_ref().as_str().unwrap(), ""); + assert_eq!(reader.get_origin_root(env_val).unwrap().as_str(), format!("{root}/home")); +} |