Unnamed repository; edit this file 'description' to name the repository.
fix: Parse cargo config files with origins
| -rw-r--r-- | crates/project-model/src/cargo_config_file.rs | 227 | ||||
| -rw-r--r-- | crates/project-model/src/env.rs | 103 | ||||
| -rw-r--r-- | crates/project-model/src/toolchain_info/target_tuple.rs | 48 | ||||
| -rw-r--r-- | crates/project-model/src/workspace.rs | 35 |
4 files changed, 287 insertions, 126 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")); +} diff --git a/crates/project-model/src/env.rs b/crates/project-model/src/env.rs index 8089155adf..51c447945c 100644 --- a/crates/project-model/src/env.rs +++ b/crates/project-model/src/env.rs @@ -3,7 +3,7 @@ use base_db::Env; use paths::Utf8Path; use rustc_hash::FxHashMap; -use crate::{ManifestPath, PackageData, TargetKind, cargo_config_file::CargoConfigFile}; +use crate::{PackageData, TargetKind, cargo_config_file::CargoConfigFile}; /// Recreates the compile-time environment variables that Cargo sets. /// @@ -61,46 +61,48 @@ pub(crate) fn inject_rustc_tool_env(env: &mut Env, cargo_name: &str, kind: Targe } pub(crate) fn cargo_config_env( - manifest: &ManifestPath, config: &Option<CargoConfigFile>, extra_env: &FxHashMap<String, Option<String>>, ) -> Env { + use toml::de::*; + let mut env = Env::default(); env.extend(extra_env.iter().filter_map(|(k, v)| v.as_ref().map(|v| (k.clone(), v.clone())))); - let Some(serde_json::Value::Object(env_json)) = config.as_ref().and_then(|c| c.get("env")) - else { + let Some(config_reader) = config.as_ref().and_then(|c| c.read()) else { + return env; + }; + let Some(env_toml) = config_reader.get(["env"]).and_then(|it| it.as_table()) else { return env; }; - // FIXME: The base here should be the parent of the `.cargo/config` file, not the manifest. - // But cargo does not provide this information. - let base = <_ as AsRef<Utf8Path>>::as_ref(manifest.parent()); - - for (key, entry) in env_json { - let value = match entry { - serde_json::Value::String(s) => s.clone(), - serde_json::Value::Object(entry) => { + for (key, entry) in env_toml { + let key = key.as_ref().as_ref(); + let value = match entry.as_ref() { + DeValue::String(s) => String::from(s.clone()), + DeValue::Table(entry) => { // Each entry MUST have a `value` key. - let Some(value) = entry.get("value").and_then(|v| v.as_str()) else { + let Some(map) = entry.get("value").and_then(|v| v.as_ref().as_str()) else { continue; }; // If the entry already exists in the environment AND the `force` key is not set to // true, then don't overwrite the value. if extra_env.get(key).is_some_and(Option::is_some) - && !entry.get("force").and_then(|v| v.as_bool()).unwrap_or(false) + && !entry.get("force").and_then(|v| v.as_ref().as_bool()).unwrap_or(false) { continue; } - if entry - .get("relative") - .and_then(|v| v.as_bool()) - .is_some_and(std::convert::identity) - { - base.join(value).to_string() + if let Some(base) = entry.get("relative").and_then(|v| { + if v.as_ref().as_bool().is_some_and(std::convert::identity) { + config_reader.get_origin_root(v) + } else { + None + } + }) { + base.join(map).to_string() } else { - value.to_owned() + map.to_owned() } } _ => continue, @@ -114,43 +116,30 @@ pub(crate) fn cargo_config_env( #[test] fn parse_output_cargo_config_env_works() { + use itertools::Itertools; + + let cwd = paths::AbsPathBuf::try_from( + paths::Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap(), + ) + .unwrap(); + let config_path = cwd.join(".cargo").join("config.toml"); let raw = r#" -{ - "env": { - "CARGO_WORKSPACE_DIR": { - "relative": true, - "value": "" - }, - "INVALID": { - "relative": "invalidbool", - "value": "../relative" - }, - "RELATIVE": { - "relative": true, - "value": "../relative" - }, - "TEST": { - "value": "test" - }, - "FORCED": { - "value": "test", - "force": true - }, - "UNFORCED": { - "value": "test", - "force": false - }, - "OVERWRITTEN": { - "value": "test" - }, - "NOT_AN_OBJECT": "value" - } -} +env.CARGO_WORKSPACE_DIR.relative = true +env.CARGO_WORKSPACE_DIR.value = "" +env.INVALID.relative = "invalidbool" +env.INVALID.value = "../relative" +env.RELATIVE.relative = true +env.RELATIVE.value = "../relative" +env.TEST.value = "test" +env.FORCED.value = "test" +env.FORCED.force = true +env.UNFORCED.value = "test" +env.UNFORCED.forced = false +env.OVERWRITTEN.value = "test" +env.NOT_AN_OBJECT = "value" "#; - let config: CargoConfigFile = serde_json::from_str(raw).unwrap(); - let cwd = paths::Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap(); - let manifest = paths::AbsPathBuf::assert(cwd.join("Cargo.toml")); - let manifest = ManifestPath::try_from(manifest).unwrap(); + let raw = raw.lines().map(|l| format!("{l} # {config_path}")).join("\n"); + let config = CargoConfigFile::from_string_for_test(raw); let extra_env = [ ("FORCED", Some("ignored")), ("UNFORCED", Some("newvalue")), @@ -160,7 +149,7 @@ fn parse_output_cargo_config_env_works() { .iter() .map(|(k, v)| (k.to_string(), v.map(ToString::to_string))) .collect(); - let env = cargo_config_env(&manifest, &Some(config), &extra_env); + let env = cargo_config_env(&Some(config), &extra_env); assert_eq!(env.get("CARGO_WORKSPACE_DIR").as_deref(), Some(cwd.join("").as_str())); assert_eq!(env.get("RELATIVE").as_deref(), Some(cwd.join("../relative").as_str())); assert_eq!(env.get("INVALID").as_deref(), Some("../relative")); diff --git a/crates/project-model/src/toolchain_info/target_tuple.rs b/crates/project-model/src/toolchain_info/target_tuple.rs index 9f12ededb6..12c64b5928 100644 --- a/crates/project-model/src/toolchain_info/target_tuple.rs +++ b/crates/project-model/src/toolchain_info/target_tuple.rs @@ -53,7 +53,7 @@ fn rustc_discover_host_tuple( } fn cargo_config_build_target(config: &CargoConfigFile) -> Option<Vec<String>> { - match parse_json_cargo_config_build_target(config) { + match parse_toml_cargo_config_build_target(config) { Ok(v) => v, Err(e) => { tracing::debug!("Failed to discover cargo config build target {e:?}"); @@ -63,18 +63,44 @@ fn cargo_config_build_target(config: &CargoConfigFile) -> Option<Vec<String>> { } // Parses `"build.target = [target-tuple, target-tuple, ...]"` or `"build.target = "target-tuple"` -fn parse_json_cargo_config_build_target( +fn parse_toml_cargo_config_build_target( config: &CargoConfigFile, ) -> anyhow::Result<Option<Vec<String>>> { - let target = config.get("build").and_then(|v| v.as_object()).and_then(|m| m.get("target")); - match target { - Some(serde_json::Value::String(s)) => Ok(Some(vec![s.to_owned()])), - Some(v) => serde_json::from_value(v.clone()) - .map(Option::Some) - .context("Failed to parse `build.target` as an array of target"), - // t`error: config value `build.target` is not set`, in which case we - // don't wanna log the error - None => Ok(None), + let Some(config_reader) = config.read() else { + return Ok(None); + }; + let Some(target) = config_reader.get_spanned(["build", "target"]) else { + return Ok(None); + }; + + // if the target ends with `.json`, join it to the config file's parent dir. + // See https://github.com/rust-lang/cargo/blob/f7acf448fc127df9a77c52cc2bba027790ac4931/src/cargo/core/compiler/compile_kind.rs#L171-L192 + let join_to_origin_if_json_path = |s: &str, spanned: &toml::Spanned<toml::de::DeValue<'_>>| { + if s.ends_with(".json") { + config_reader + .get_origin_root(spanned) + .map(|p| p.join(s).to_string()) + .unwrap_or_else(|| s.to_owned()) + } else { + s.to_owned() + } + }; + + let parse_err = "Failed to parse `build.target` as an array of target"; + + match target.as_ref() { + toml::de::DeValue::String(s) => { + Ok(Some(vec![join_to_origin_if_json_path(s.as_ref(), target)])) + } + toml::de::DeValue::Array(arr) => arr + .iter() + .map(|v| { + let s = v.as_ref().as_str().context(parse_err)?; + Ok(join_to_origin_if_json_path(s, v)) + }) + .collect::<anyhow::Result<_>>() + .map(Option::Some), + _ => Err(anyhow::anyhow!(parse_err)), } } diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs index f01daa82b6..4d56668cf2 100644 --- a/crates/project-model/src/workspace.rs +++ b/crates/project-model/src/workspace.rs @@ -27,7 +27,7 @@ use crate::{ ProjectJson, ProjectManifest, RustSourceWorkspaceConfig, Sysroot, TargetData, TargetKind, WorkspaceBuildScripts, build_dependencies::{BuildScriptOutput, ProcMacroDylibPath}, - cargo_config_file, + cargo_config_file::CargoConfigFile, cargo_workspace::{CargoMetadataConfig, DepKind, FetchMetadata, PackageData, RustLibSource}, env::{cargo_config_env, inject_cargo_env, inject_cargo_package_env, inject_rustc_tool_env}, project_json::{Crate, CrateArrayIdx}, @@ -268,7 +268,7 @@ impl ProjectWorkspace { tracing::info!(workspace = %cargo_toml, src_root = ?sysroot.rust_lib_src_root(), root = ?sysroot.root(), "Using sysroot"); progress("querying project metadata".to_owned()); - let config_file = cargo_config_file::read(cargo_toml, extra_env, &sysroot); + let config_file = CargoConfigFile::load(cargo_toml, extra_env, &sysroot); let config_file_ = config_file.clone(); let toolchain_config = QueryConfig::Cargo(&sysroot, cargo_toml, &config_file_); let targets = @@ -391,7 +391,6 @@ impl ProjectWorkspace { sysroot.load_workspace( &RustSourceWorkspaceConfig::CargoMetadata(sysroot_metadata_config( config, - workspace_dir, &targets, toolchain.clone(), )), @@ -402,9 +401,7 @@ impl ProjectWorkspace { .expect("failed to spawn thread"); let cargo_env = Builder::new() .name("ProjectWorkspace::cargo_env".to_owned()) - .spawn_scoped(s, move || { - cargo_config_env(cargo_toml, &config_file, &config.extra_env) - }) + .spawn_scoped(s, move || cargo_config_env(&config_file, &config.extra_env)) .expect("failed to spawn thread"); thread::Result::Ok(( rustc_cfg.join()?, @@ -503,7 +500,6 @@ impl ProjectWorkspace { sysroot.load_workspace( &RustSourceWorkspaceConfig::CargoMetadata(sysroot_metadata_config( config, - project_json.project_root(), &targets, toolchain.clone(), )), @@ -548,7 +544,7 @@ impl ProjectWorkspace { None => Sysroot::empty(), }; - let config_file = cargo_config_file::read(detached_file, &config.extra_env, &sysroot); + let config_file = CargoConfigFile::load(detached_file, &config.extra_env, &sysroot); let query_config = QueryConfig::Cargo(&sysroot, detached_file, &config_file); let toolchain = version::get(query_config, &config.extra_env).ok().flatten(); let targets = target_tuple::get(query_config, config.target.as_deref(), &config.extra_env) @@ -559,7 +555,6 @@ impl ProjectWorkspace { let loaded_sysroot = sysroot.load_workspace( &RustSourceWorkspaceConfig::CargoMetadata(sysroot_metadata_config( config, - dir, &targets, toolchain.clone(), )), @@ -585,8 +580,7 @@ impl ProjectWorkspace { config.no_deps, ); let cargo_script = fetch_metadata.exec(false, &|_| ()).ok().map(|(ws, error)| { - let cargo_config_extra_env = - cargo_config_env(detached_file, &config_file, &config.extra_env); + let cargo_config_extra_env = cargo_config_env(&config_file, &config.extra_env); ( CargoWorkspace::new(ws, detached_file.clone(), cargo_config_extra_env, false), WorkspaceBuildScripts::default(), @@ -1897,29 +1891,12 @@ fn add_dep_inner(graph: &mut CrateGraphBuilder, from: CrateBuilderId, dep: Depen fn sysroot_metadata_config( config: &CargoConfig, - current_dir: &AbsPath, targets: &[String], toolchain_version: Option<Version>, ) -> CargoMetadataConfig { - // We run `cargo metadata` on sysroot with sysroot dir as a working directory, but still pass - // the `targets` from the cargo config evaluated from the workspace's `current_dir`. - // So, we need to *canonicalize* those *might-be-relative-paths-to-custom-target-json-files*. - // - // See https://github.com/rust-lang/cargo/blob/f7acf448fc127df9a77c52cc2bba027790ac4931/src/cargo/core/compiler/compile_kind.rs#L171-L192 - let targets = targets - .iter() - .map(|target| { - if target.ends_with(".json") { - current_dir.join(target).to_string() - } else { - target.to_owned() - } - }) - .collect(); - CargoMetadataConfig { features: Default::default(), - targets, + targets: targets.to_vec(), extra_args: Default::default(), extra_env: config.extra_env.clone(), toolchain_version, |