From 8dad658beadd57cd812510fa01b94d456aaad621 Mon Sep 17 00:00:00 2001 From: Shoyu Vanilla Date: Sun, 6 Jul 2025 00:24:42 +0900 Subject: [PATCH] chore: Cleanup cargo config queries --- crates/project-model/src/cargo_config_file.rs | 34 +++++ crates/project-model/src/env.rs | 139 +++++++----------- crates/project-model/src/lib.rs | 5 +- .../src/toolchain_info/rustc_cfg.rs | 4 +- .../src/toolchain_info/target_data_layout.rs | 4 +- .../src/toolchain_info/target_tuple.rs | 50 ++++--- .../src/toolchain_info/version.rs | 4 +- crates/project-model/src/workspace.rs | 46 ++++-- 8 files changed, 152 insertions(+), 134 deletions(-) create mode 100644 crates/project-model/src/cargo_config_file.rs diff --git a/crates/project-model/src/cargo_config_file.rs b/crates/project-model/src/cargo_config_file.rs new file mode 100644 index 0000000000..7966f74df3 --- /dev/null +++ b/crates/project-model/src/cargo_config_file.rs @@ -0,0 +1,34 @@ +//! Read `.cargo/config.toml` as a JSON object +use rustc_hash::FxHashMap; +use toolchain::Tool; + +use crate::{ManifestPath, Sysroot, utf8_stdout}; + +pub(crate) type CargoConfigFile = serde_json::Map; + +pub(crate) fn read( + manifest: &ManifestPath, + extra_env: &FxHashMap>, + sysroot: &Sysroot, +) -> Option { + 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 = 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) +} diff --git a/crates/project-model/src/env.rs b/crates/project-model/src/env.rs index 9e0415c3b3..d281492fc9 100644 --- a/crates/project-model/src/env.rs +++ b/crates/project-model/src/env.rs @@ -1,10 +1,9 @@ //! Cargo-like environment variables injection. use base_db::Env; -use paths::{Utf8Path, Utf8PathBuf}; -use rustc_hash::FxHashMap; +use paths::Utf8Path; use toolchain::Tool; -use crate::{ManifestPath, PackageData, Sysroot, TargetKind, utf8_stdout}; +use crate::{ManifestPath, PackageData, TargetKind, cargo_config_file::CargoConfigFile}; /// Recreates the compile-time environment variables that Cargo sets. /// @@ -61,104 +60,68 @@ pub(crate) fn inject_rustc_tool_env(env: &mut Env, cargo_name: &str, kind: Targe env.set("CARGO_CRATE_NAME", cargo_name.replace('-', "_")); } -pub(crate) fn cargo_config_env( - manifest: &ManifestPath, - extra_env: &FxHashMap>, - sysroot: &Sysroot, -) -> Env { - let mut cargo_config = sysroot.tool(Tool::Cargo, manifest.parent(), extra_env); - cargo_config - .args(["-Z", "unstable-options", "config", "get", "env"]) - .env("RUSTC_BOOTSTRAP", "1"); - if manifest.is_rust_manifest() { - cargo_config.arg("-Zscript"); - } - // if successful we receive `env.key.value = "value" per entry - tracing::debug!("Discovering cargo config env by {:?}", cargo_config); - utf8_stdout(&mut cargo_config) - .map(|stdout| parse_output_cargo_config_env(manifest, &stdout)) - .inspect(|env| { - tracing::debug!("Discovered cargo config env: {:?}", env); - }) - .inspect_err(|err| { - tracing::debug!("Failed to discover cargo config env: {:?}", err); - }) - .unwrap_or_default() -} - -fn parse_output_cargo_config_env(manifest: &ManifestPath, stdout: &str) -> Env { +pub(crate) fn cargo_config_env(manifest: &ManifestPath, config: &Option) -> Env { let mut env = Env::default(); - let mut relatives = vec![]; - for (key, val) in - stdout.lines().filter_map(|l| l.strip_prefix("env.")).filter_map(|l| l.split_once(" = ")) - { - let val = val.trim_matches('"').to_owned(); - if let Some((key, modifier)) = key.split_once('.') { - match modifier { - "relative" => relatives.push((key, val)), - "value" => _ = env.insert(key, val), - _ => { - tracing::warn!( - "Unknown modifier in cargo config env: {}, expected `relative` or `value`", - modifier - ); - continue; - } - } - } else { - env.insert(key, val); - } - } + let Some(serde_json::Value::Object(env_json)) = config.as_ref().and_then(|c| c.get("env")) + 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>::as_ref(manifest.parent()); - for (key, relative) in relatives { - if relative != "true" { - continue; - } - if let Some(suffix) = env.get(key) { - env.insert(key, base.join(suffix).to_string()); - } - } - env -} -pub(crate) fn cargo_config_build_target_dir( - manifest: &ManifestPath, - extra_env: &FxHashMap>, - sysroot: &Sysroot, -) -> Option { - let mut cargo_config = sysroot.tool(Tool::Cargo, manifest.parent(), extra_env); - cargo_config - .args(["-Z", "unstable-options", "config", "get", "build.target-dir"]) - .env("RUSTC_BOOTSTRAP", "1"); - if manifest.is_rust_manifest() { - cargo_config.arg("-Zscript"); + for (key, entry) in env_json { + let serde_json::Value::Object(entry) = entry else { + continue; + }; + let Some(value) = entry.get("value").and_then(|v| v.as_str()) else { + continue; + }; + + let value = if entry + .get("relative") + .and_then(|v| v.as_bool()) + .is_some_and(std::convert::identity) + { + base.join(value).to_string() + } else { + value.to_owned() + }; + env.insert(key, value); } - utf8_stdout(&mut cargo_config) - .map(|stdout| { - Utf8Path::new(stdout.trim_start_matches("build.target-dir = ").trim_matches('"')) - .to_owned() - }) - .ok() + + env } #[test] fn parse_output_cargo_config_env_works() { - let stdout = r#" -env.CARGO_WORKSPACE_DIR.relative = true -env.CARGO_WORKSPACE_DIR.value = "" -env.RELATIVE.relative = true -env.RELATIVE.value = "../relative" -env.INVALID.relative = invalidbool -env.INVALID.value = "../relative" -env.TEST.value = "test" -"# - .trim(); + let raw = r#" +{ + "env": { + "CARGO_WORKSPACE_DIR": { + "relative": true, + "value": "" + }, + "INVALID": { + "relative": "invalidbool", + "value": "../relative" + }, + "RELATIVE": { + "relative": true, + "value": "../relative" + }, + "TEST": { + "value": "test" + } + } +} +"#; + 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 env = parse_output_cargo_config_env(&manifest, stdout); + let env = cargo_config_env(&manifest, &Some(config)); 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/lib.rs b/crates/project-model/src/lib.rs index 436af64cf1..3bf3d06e6b 100644 --- a/crates/project-model/src/lib.rs +++ b/crates/project-model/src/lib.rs @@ -24,7 +24,7 @@ pub mod toolchain_info { use std::path::Path; - use crate::{ManifestPath, Sysroot}; + use crate::{ManifestPath, Sysroot, cargo_config_file::CargoConfigFile}; #[derive(Copy, Clone)] pub enum QueryConfig<'a> { @@ -32,11 +32,12 @@ pub mod toolchain_info { Rustc(&'a Sysroot, &'a Path), /// Attempt to use cargo to query the desired information, honoring cargo configurations. /// If this fails, falls back to invoking `rustc` directly. - Cargo(&'a Sysroot, &'a ManifestPath), + Cargo(&'a Sysroot, &'a ManifestPath, &'a Option), } } mod build_dependencies; +mod cargo_config_file; mod cargo_workspace; mod env; mod manifest_path; diff --git a/crates/project-model/src/toolchain_info/rustc_cfg.rs b/crates/project-model/src/toolchain_info/rustc_cfg.rs index a77f76797f..6e06e88bf7 100644 --- a/crates/project-model/src/toolchain_info/rustc_cfg.rs +++ b/crates/project-model/src/toolchain_info/rustc_cfg.rs @@ -63,7 +63,7 @@ fn rustc_print_cfg( ) -> anyhow::Result { const RUSTC_ARGS: [&str; 2] = ["--print", "cfg"]; let (sysroot, current_dir) = match config { - QueryConfig::Cargo(sysroot, cargo_toml) => { + QueryConfig::Cargo(sysroot, cargo_toml, _) => { let mut cmd = sysroot.tool(Tool::Cargo, cargo_toml.parent(), extra_env); cmd.args(["rustc", "-Z", "unstable-options"]).args(RUSTC_ARGS); if let Some(target) = target { @@ -109,7 +109,7 @@ mod tests { let sysroot = Sysroot::empty(); let manifest_path = ManifestPath::try_from(AbsPathBuf::assert(Utf8PathBuf::from(manifest_path))).unwrap(); - let cfg = QueryConfig::Cargo(&sysroot, &manifest_path); + let cfg = QueryConfig::Cargo(&sysroot, &manifest_path, &None); assert_ne!(get(cfg, None, &FxHashMap::default()), vec![]); } diff --git a/crates/project-model/src/toolchain_info/target_data_layout.rs b/crates/project-model/src/toolchain_info/target_data_layout.rs index a4d0ec6953..a28f468e69 100644 --- a/crates/project-model/src/toolchain_info/target_data_layout.rs +++ b/crates/project-model/src/toolchain_info/target_data_layout.rs @@ -20,7 +20,7 @@ pub fn get( }) }; let (sysroot, current_dir) = match config { - QueryConfig::Cargo(sysroot, cargo_toml) => { + QueryConfig::Cargo(sysroot, cargo_toml, _) => { let mut cmd = sysroot.tool(Tool::Cargo, cargo_toml.parent(), extra_env); cmd.env("RUSTC_BOOTSTRAP", "1"); cmd.args(["rustc", "-Z", "unstable-options"]).args(RUSTC_ARGS).args([ @@ -66,7 +66,7 @@ mod tests { let sysroot = Sysroot::empty(); let manifest_path = ManifestPath::try_from(AbsPathBuf::assert(Utf8PathBuf::from(manifest_path))).unwrap(); - let cfg = QueryConfig::Cargo(&sysroot, &manifest_path); + let cfg = QueryConfig::Cargo(&sysroot, &manifest_path, &None); assert!(get(cfg, None, &FxHashMap::default()).is_ok()); } diff --git a/crates/project-model/src/toolchain_info/target_tuple.rs b/crates/project-model/src/toolchain_info/target_tuple.rs index f6ab853219..9f12ededb6 100644 --- a/crates/project-model/src/toolchain_info/target_tuple.rs +++ b/crates/project-model/src/toolchain_info/target_tuple.rs @@ -5,7 +5,9 @@ use anyhow::Context; use rustc_hash::FxHashMap; use toolchain::Tool; -use crate::{ManifestPath, Sysroot, toolchain_info::QueryConfig, utf8_stdout}; +use crate::{ + Sysroot, cargo_config_file::CargoConfigFile, toolchain_info::QueryConfig, utf8_stdout, +}; /// For cargo, runs `cargo -Zunstable-options config get build.target` to get the configured project target(s). /// For rustc, runs `rustc --print -vV` to get the host target. @@ -20,8 +22,8 @@ pub fn get( } let (sysroot, current_dir) = match config { - QueryConfig::Cargo(sysroot, cargo_toml) => { - match cargo_config_build_target(cargo_toml, extra_env, sysroot) { + QueryConfig::Cargo(sysroot, cargo_toml, config_file) => { + match config_file.as_ref().and_then(cargo_config_build_target) { Some(it) => return Ok(it), None => (sysroot, cargo_toml.parent().as_ref()), } @@ -50,30 +52,30 @@ fn rustc_discover_host_tuple( } } -fn cargo_config_build_target( - cargo_toml: &ManifestPath, - extra_env: &FxHashMap>, - sysroot: &Sysroot, -) -> Option> { - let mut cmd = sysroot.tool(Tool::Cargo, cargo_toml.parent(), extra_env); - cmd.current_dir(cargo_toml.parent()).env("RUSTC_BOOTSTRAP", "1"); - cmd.args(["-Z", "unstable-options", "config", "get", "build.target"]); - // if successful we receive `build.target = "target-tuple"` - // or `build.target = ["", ..]` - // this might be `error: config value `build.target` is not set` in which case we - // don't wanna log the error - utf8_stdout(&mut cmd).and_then(parse_output_cargo_config_build_target).ok() +fn cargo_config_build_target(config: &CargoConfigFile) -> Option> { + match parse_json_cargo_config_build_target(config) { + Ok(v) => v, + Err(e) => { + tracing::debug!("Failed to discover cargo config build target {e:?}"); + None + } + } } // Parses `"build.target = [target-tuple, target-tuple, ...]"` or `"build.target = "target-tuple"` -fn parse_output_cargo_config_build_target(stdout: String) -> anyhow::Result> { - let trimmed = stdout.trim_start_matches("build.target = ").trim_matches('"'); - - if !trimmed.starts_with('[') { - return Ok([trimmed.to_owned()].to_vec()); +fn parse_json_cargo_config_build_target( + config: &CargoConfigFile, +) -> anyhow::Result>> { + 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), } - - serde_json::from_str(trimmed).context("Failed to parse `build.target` as an array of target") } #[cfg(test)] @@ -90,7 +92,7 @@ mod tests { let sysroot = Sysroot::empty(); let manifest_path = ManifestPath::try_from(AbsPathBuf::assert(Utf8PathBuf::from(manifest_path))).unwrap(); - let cfg = QueryConfig::Cargo(&sysroot, &manifest_path); + let cfg = QueryConfig::Cargo(&sysroot, &manifest_path, &None); assert!(get(cfg, None, &FxHashMap::default()).is_ok()); } diff --git a/crates/project-model/src/toolchain_info/version.rs b/crates/project-model/src/toolchain_info/version.rs index 91ba859859..357053d8e8 100644 --- a/crates/project-model/src/toolchain_info/version.rs +++ b/crates/project-model/src/toolchain_info/version.rs @@ -12,7 +12,7 @@ pub(crate) fn get( extra_env: &FxHashMap>, ) -> Result, anyhow::Error> { let (mut cmd, prefix) = match config { - QueryConfig::Cargo(sysroot, cargo_toml) => { + QueryConfig::Cargo(sysroot, cargo_toml, _) => { (sysroot.tool(Tool::Cargo, cargo_toml.parent(), extra_env), "cargo ") } QueryConfig::Rustc(sysroot, current_dir) => { @@ -44,7 +44,7 @@ mod tests { let sysroot = Sysroot::empty(); let manifest_path = ManifestPath::try_from(AbsPathBuf::assert(Utf8PathBuf::from(manifest_path))).unwrap(); - let cfg = QueryConfig::Cargo(&sysroot, &manifest_path); + let cfg = QueryConfig::Cargo(&sysroot, &manifest_path, &None); assert!(get(cfg, &FxHashMap::default()).is_ok()); } diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs index 43db84b4fa..801c2abdfc 100644 --- a/crates/project-model/src/workspace.rs +++ b/crates/project-model/src/workspace.rs @@ -25,11 +25,9 @@ use crate::{ ProjectJson, ProjectManifest, RustSourceWorkspaceConfig, Sysroot, TargetData, TargetKind, WorkspaceBuildScripts, build_dependencies::BuildScriptOutput, + cargo_config_file, cargo_workspace::{CargoMetadataConfig, DepKind, PackageData, RustLibSource}, - env::{ - cargo_config_build_target_dir, cargo_config_env, inject_cargo_env, - inject_cargo_package_env, inject_rustc_tool_env, - }, + env::{cargo_config_env, inject_cargo_env, inject_cargo_package_env, inject_rustc_tool_env}, project_json::{Crate, CrateArrayIdx}, sysroot::RustLibSrcWorkspace, toolchain_info::{QueryConfig, rustc_cfg, target_data_layout, target_tuple, version}, @@ -270,7 +268,9 @@ 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 toolchain_config = QueryConfig::Cargo(&sysroot, cargo_toml); + let config_file = cargo_config_file::read(cargo_toml, extra_env, &sysroot); + let config_file_ = config_file.clone(); + let toolchain_config = QueryConfig::Cargo(&sysroot, cargo_toml, &config_file_); let targets = target_tuple::get(toolchain_config, target.as_deref(), extra_env).unwrap_or_default(); let toolchain = version::get(toolchain_config, extra_env) @@ -285,7 +285,7 @@ impl ProjectWorkspace { let target_dir = config .target_dir .clone() - .or_else(|| cargo_config_build_target_dir(cargo_toml, extra_env, &sysroot)) + .or_else(|| cargo_target_dir(cargo_toml, extra_env, &sysroot)) .unwrap_or_else(|| workspace_dir.join("target").into()); // We spawn a bunch of processes to query various information about the workspace's @@ -397,7 +397,7 @@ impl ProjectWorkspace { ) }); let cargo_config_extra_env = - s.spawn(|| cargo_config_env(cargo_toml, extra_env, &sysroot)); + s.spawn(move || cargo_config_env(cargo_toml, &config_file)); thread::Result::Ok(( rustc_cfg.join()?, data_layout.join()?, @@ -476,9 +476,7 @@ impl ProjectWorkspace { let target_dir = config .target_dir .clone() - .or_else(|| { - cargo_config_build_target_dir(project_json.manifest()?, &config.extra_env, &sysroot) - }) + .or_else(|| cargo_target_dir(project_json.manifest()?, &config.extra_env, &sysroot)) .unwrap_or_else(|| project_root.join("target").into()); // We spawn a bunch of processes to query various information about the workspace's @@ -554,7 +552,8 @@ impl ProjectWorkspace { None => Sysroot::empty(), }; - let query_config = QueryConfig::Cargo(&sysroot, detached_file); + let config_file = cargo_config_file::read(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) .unwrap_or_default(); @@ -563,7 +562,7 @@ impl ProjectWorkspace { let target_dir = config .target_dir .clone() - .or_else(|| cargo_config_build_target_dir(detached_file, &config.extra_env, &sysroot)) + .or_else(|| cargo_target_dir(detached_file, &config.extra_env, &sysroot)) .unwrap_or_else(|| dir.join("target").into()); let loaded_sysroot = sysroot.load_workspace( @@ -600,8 +599,7 @@ impl ProjectWorkspace { ) .ok() .map(|(ws, error)| { - let cargo_config_extra_env = - cargo_config_env(detached_file, &config.extra_env, &sysroot); + let cargo_config_extra_env = cargo_config_env(detached_file, &config_file); ( CargoWorkspace::new(ws, detached_file.clone(), cargo_config_extra_env, false), WorkspaceBuildScripts::default(), @@ -1901,3 +1899,23 @@ fn sysroot_metadata_config( kind: "sysroot", } } + +fn cargo_target_dir( + manifest: &ManifestPath, + extra_env: &FxHashMap>, + sysroot: &Sysroot, +) -> Option { + let cargo = sysroot.tool(Tool::Cargo, manifest.parent(), extra_env); + let mut meta = cargo_metadata::MetadataCommand::new(); + meta.cargo_path(cargo.get_program()); + meta.manifest_path(manifest); + // `--no-deps` doesn't (over)write lockfiles as it doesn't do any package resolve. + // So we can use it to get `target_directory` before copying lockfiles + let mut other_options = vec!["--no-deps".to_owned()]; + if manifest.is_rust_manifest() { + meta.env("RUSTC_BOOTSTRAP", "1"); + other_options.push("-Zscript".to_owned()); + } + meta.other_options(other_options); + meta.exec().map(|m| m.target_directory).ok() +}