//! 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}; #[derive(Clone)] pub struct CargoConfigFile(String); impl CargoConfigFile { pub(crate) fn load( 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", "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::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, table: Spanned>, } impl<'a> CargoConfigFileReader<'a> { fn new(toml_str: &'a str) -> Option { 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, ) -> Option<&Spanned>> { 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) -> Option<&DeValue<'a>> { self.get_spanned(accessor).map(|it| it.as_ref()) } pub(crate) fn get_origin_root(&self, spanned: &Spanned>) -> 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) struct LockfileCopy { pub(crate) path: Utf8PathBuf, pub(crate) usage: LockfileUsage, _temp_dir: temp_dir::TempDir, } pub(crate) enum LockfileUsage { /// Rust [1.82.0, 1.95.0). `cargo --lockfile-path ` WithFlag, /// Rust >= 1.95.0. `CARGO_RESOLVER_LOCKFILE_PATH= cargo ` WithEnvVar, } pub(crate) fn make_lockfile_copy( toolchain_version: &semver::Version, lockfile_path: &Utf8Path, ) -> Option { const MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH_FLAG: semver::Version = semver::Version { major: 1, minor: 82, patch: 0, pre: semver::Prerelease::EMPTY, build: semver::BuildMetadata::EMPTY, }; const MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH_ENV: semver::Version = semver::Version { major: 1, minor: 95, patch: 0, pre: semver::Prerelease::EMPTY, build: semver::BuildMetadata::EMPTY, }; let usage = if *toolchain_version >= MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH_ENV { LockfileUsage::WithEnvVar } else if *toolchain_version >= MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH_FLAG { LockfileUsage::WithFlag } else { return None; }; let temp_dir = temp_dir::TempDir::with_prefix("rust-analyzer").ok()?; let path: Utf8PathBuf = temp_dir.path().join("Cargo.lock").try_into().ok()?; let path = match std::fs::copy(lockfile_path, &path) { Ok(_) => { tracing::debug!("Copied lock file from `{}` to `{}`", lockfile_path, path); path } // lockfile does not yet exist, so we can just create a new one in the temp dir Err(e) if e.kind() == std::io::ErrorKind::NotFound => path, Err(e) => { tracing::warn!("Failed to copy lock file from `{lockfile_path}` to `{path}`: {e}",); return None; } }; Some(LockfileCopy { path, usage, _temp_dir: temp_dir }) } #[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")); }