blob: 5d6e5fd648b38dc5b68e3b5a96efc0a6d2bda591 [file] [log] [blame]
//! 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<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(
lockfile_path: &Utf8Path,
) -> Option<(temp_dir::TempDir, Utf8PathBuf)> {
let temp_dir = temp_dir::TempDir::with_prefix("rust-analyzer").ok()?;
let target_lockfile = temp_dir.path().join("Cargo.lock").try_into().ok()?;
match std::fs::copy(lockfile_path, &target_lockfile) {
Ok(_) => {
tracing::debug!("Copied lock file from `{}` to `{}`", lockfile_path, target_lockfile);
Some((temp_dir, target_lockfile))
}
// 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 => Some((temp_dir, target_lockfile)),
Err(e) => {
tracing::warn!(
"Failed to copy lock file from `{lockfile_path}` to `{target_lockfile}`: {e}",
);
None
}
}
}
#[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"));
}