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 000000000000..7966f74df305 --- /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/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs index 4bacc904174f..daadcd9d79a9 100644 --- a/crates/project-model/src/cargo_workspace.rs +++ b/crates/project-model/src/cargo_workspace.rs @@ -300,8 +300,6 @@ pub struct CargoMetadataConfig { pub extra_args: Vec, /// Extra env vars to set when invoking the cargo command pub extra_env: FxHashMap>, - /// The target dir for this workspace load. - pub target_dir: Utf8PathBuf, /// What kind of metadata are we fetching: workspace, rustc, or sysroot. pub kind: &'static str, /// The toolchain version, if known. @@ -317,188 +315,6 @@ struct PackageMetadata { } impl CargoWorkspace { - /// Fetches the metadata for the given `cargo_toml` manifest. - /// A successful result may contain another metadata error if the initial fetching failed but - /// the `--no-deps` retry succeeded. - /// - /// The sysroot is used to set the `RUSTUP_TOOLCHAIN` env var when invoking cargo - /// to ensure that the rustup proxy uses the correct toolchain. - pub fn fetch_metadata( - cargo_toml: &ManifestPath, - current_dir: &AbsPath, - config: &CargoMetadataConfig, - sysroot: &Sysroot, - no_deps: bool, - locked: bool, - progress: &dyn Fn(String), - ) -> anyhow::Result<(cargo_metadata::Metadata, Option)> { - let res = Self::fetch_metadata_( - cargo_toml, - current_dir, - config, - sysroot, - no_deps, - locked, - progress, - ); - if let Ok((_, Some(ref e))) = res { - tracing::warn!( - %cargo_toml, - ?e, - "`cargo metadata` failed, but retry with `--no-deps` succeeded" - ); - } - res - } - - fn fetch_metadata_( - cargo_toml: &ManifestPath, - current_dir: &AbsPath, - config: &CargoMetadataConfig, - sysroot: &Sysroot, - no_deps: bool, - locked: bool, - progress: &dyn Fn(String), - ) -> anyhow::Result<(cargo_metadata::Metadata, Option)> { - let cargo = sysroot.tool(Tool::Cargo, current_dir, &config.extra_env); - let mut meta = MetadataCommand::new(); - meta.cargo_path(cargo.get_program()); - cargo.get_envs().for_each(|(var, val)| _ = meta.env(var, val.unwrap_or_default())); - meta.manifest_path(cargo_toml.to_path_buf()); - match &config.features { - CargoFeatures::All => { - meta.features(CargoOpt::AllFeatures); - } - CargoFeatures::Selected { features, no_default_features } => { - if *no_default_features { - meta.features(CargoOpt::NoDefaultFeatures); - } - if !features.is_empty() { - meta.features(CargoOpt::SomeFeatures(features.clone())); - } - } - } - meta.current_dir(current_dir); - - let mut other_options = vec![]; - // cargo metadata only supports a subset of flags of what cargo usually accepts, and usually - // the only relevant flags for metadata here are unstable ones, so we pass those along - // but nothing else - let mut extra_args = config.extra_args.iter(); - while let Some(arg) = extra_args.next() { - if arg == "-Z" { - if let Some(arg) = extra_args.next() { - other_options.push("-Z".to_owned()); - other_options.push(arg.to_owned()); - } - } - } - - if !config.targets.is_empty() { - other_options.extend( - config.targets.iter().flat_map(|it| ["--filter-platform".to_owned(), it.clone()]), - ); - } - if no_deps { - other_options.push("--no-deps".to_owned()); - } - - let mut using_lockfile_copy = false; - // The manifest is a rust file, so this means its a script manifest - if cargo_toml.is_rust_manifest() { - other_options.push("-Zscript".to_owned()); - } else if config - .toolchain_version - .as_ref() - .is_some_and(|v| *v >= MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH) - { - let lockfile = <_ as AsRef>::as_ref(cargo_toml).with_extension("lock"); - let target_lockfile = config - .target_dir - .join("rust-analyzer") - .join("metadata") - .join(config.kind) - .join("Cargo.lock"); - match std::fs::copy(&lockfile, &target_lockfile) { - Ok(_) => { - using_lockfile_copy = true; - other_options.push("--lockfile-path".to_owned()); - other_options.push(target_lockfile.to_string()); - } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - // There exists no lockfile yet - using_lockfile_copy = true; - other_options.push("--lockfile-path".to_owned()); - other_options.push(target_lockfile.to_string()); - } - Err(e) => { - tracing::warn!( - "Failed to copy lock file from `{lockfile}` to `{target_lockfile}`: {e}", - ); - } - } - } - if using_lockfile_copy { - other_options.push("-Zunstable-options".to_owned()); - meta.env("RUSTC_BOOTSTRAP", "1"); - } - // No need to lock it if we copied the lockfile, we won't modify the original after all/ - // This way cargo cannot error out on us if the lockfile requires updating. - if !using_lockfile_copy && locked { - other_options.push("--locked".to_owned()); - } - meta.other_options(other_options); - - // FIXME: Fetching metadata is a slow process, as it might require - // calling crates.io. We should be reporting progress here, but it's - // unclear whether cargo itself supports it. - progress("cargo metadata: started".to_owned()); - - let res = (|| -> anyhow::Result<(_, _)> { - let mut errored = false; - let output = - spawn_with_streaming_output(meta.cargo_command(), &mut |_| (), &mut |line| { - errored = errored || line.starts_with("error") || line.starts_with("warning"); - if errored { - progress("cargo metadata: ?".to_owned()); - return; - } - progress(format!("cargo metadata: {line}")); - })?; - if !output.status.success() { - progress(format!("cargo metadata: failed {}", output.status)); - let error = cargo_metadata::Error::CargoMetadata { - stderr: String::from_utf8(output.stderr)?, - } - .into(); - if !no_deps { - // If we failed to fetch metadata with deps, try again without them. - // This makes r-a still work partially when offline. - if let Ok((metadata, _)) = Self::fetch_metadata_( - cargo_toml, - current_dir, - config, - sysroot, - true, - locked, - progress, - ) { - return Ok((metadata, Some(error))); - } - } - return Err(error); - } - let stdout = from_utf8(&output.stdout)? - .lines() - .find(|line| line.starts_with('{')) - .ok_or(cargo_metadata::Error::NoJson)?; - Ok((cargo_metadata::MetadataCommand::parse(stdout)?, None)) - })() - .with_context(|| format!("Failed to run `{:?}`", meta.cargo_command())); - progress("cargo metadata: finished".to_owned()); - res - } - pub fn new( mut meta: cargo_metadata::Metadata, ws_manifest_path: ManifestPath, @@ -733,3 +549,214 @@ impl CargoWorkspace { self.requires_rustc_private } } + +pub(crate) struct FetchMetadata { + command: cargo_metadata::MetadataCommand, + lockfile_path: Option, + kind: &'static str, + no_deps: bool, + no_deps_result: anyhow::Result, + other_options: Vec, +} + +impl FetchMetadata { + /// Builds a command to fetch metadata for the given `cargo_toml` manifest. + /// + /// Performs a lightweight pre-fetch using the `--no-deps` option, + /// available via [`FetchMetadata::no_deps_metadata`], to gather basic + /// information such as the `target-dir`. + /// + /// The provided sysroot is used to set the `RUSTUP_TOOLCHAIN` + /// environment variable when invoking Cargo, ensuring that the + /// rustup proxy selects the correct toolchain. + pub(crate) fn new( + cargo_toml: &ManifestPath, + current_dir: &AbsPath, + config: &CargoMetadataConfig, + sysroot: &Sysroot, + no_deps: bool, + ) -> Self { + let cargo = sysroot.tool(Tool::Cargo, current_dir, &config.extra_env); + let mut command = MetadataCommand::new(); + command.cargo_path(cargo.get_program()); + cargo.get_envs().for_each(|(var, val)| _ = command.env(var, val.unwrap_or_default())); + command.manifest_path(cargo_toml.to_path_buf()); + match &config.features { + CargoFeatures::All => { + command.features(CargoOpt::AllFeatures); + } + CargoFeatures::Selected { features, no_default_features } => { + if *no_default_features { + command.features(CargoOpt::NoDefaultFeatures); + } + if !features.is_empty() { + command.features(CargoOpt::SomeFeatures(features.clone())); + } + } + } + command.current_dir(current_dir); + + let mut needs_nightly = false; + let mut other_options = vec![]; + // cargo metadata only supports a subset of flags of what cargo usually accepts, and usually + // the only relevant flags for metadata here are unstable ones, so we pass those along + // but nothing else + let mut extra_args = config.extra_args.iter(); + while let Some(arg) = extra_args.next() { + if arg == "-Z" { + if let Some(arg) = extra_args.next() { + needs_nightly = true; + other_options.push("-Z".to_owned()); + other_options.push(arg.to_owned()); + } + } + } + + let mut lockfile_path = None; + if cargo_toml.is_rust_manifest() { + needs_nightly = true; + other_options.push("-Zscript".to_owned()); + } else if config + .toolchain_version + .as_ref() + .is_some_and(|v| *v >= MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH) + { + lockfile_path = Some(<_ as AsRef>::as_ref(cargo_toml).with_extension("lock")); + } + + if !config.targets.is_empty() { + other_options.extend( + config.targets.iter().flat_map(|it| ["--filter-platform".to_owned(), it.clone()]), + ); + } + + command.other_options(other_options.clone()); + + if needs_nightly { + command.env("RUSTC_BOOTSTRAP", "1"); + } + + // Pre-fetch basic metadata using `--no-deps`, which: + // - avoids fetching registries like crates.io, + // - skips dependency resolution and does not modify lockfiles, + // - and thus doesn't require progress reporting or copying lockfiles. + // + // Useful as a fast fallback to extract info like `target-dir`. + let cargo_command; + let no_deps_result = if no_deps { + command.no_deps(); + cargo_command = command.cargo_command(); + command.exec() + } else { + let mut no_deps_command = command.clone(); + no_deps_command.no_deps(); + cargo_command = no_deps_command.cargo_command(); + no_deps_command.exec() + } + .with_context(|| format!("Failed to run `{cargo_command:?}`")); + + Self { command, lockfile_path, kind: config.kind, no_deps, no_deps_result, other_options } + } + + pub(crate) fn no_deps_metadata(&self) -> Option<&cargo_metadata::Metadata> { + self.no_deps_result.as_ref().ok() + } + + /// Executes the metadata-fetching command. + /// + /// A successful result may still contain a metadata error if the full fetch failed, + /// but the fallback `--no-deps` pre-fetch succeeded during command construction. + pub(crate) fn exec( + self, + target_dir: &Utf8Path, + locked: bool, + progress: &dyn Fn(String), + ) -> anyhow::Result<(cargo_metadata::Metadata, Option)> { + let Self { mut command, lockfile_path, kind, no_deps, no_deps_result, mut other_options } = + self; + + if no_deps { + return no_deps_result.map(|m| (m, None)); + } + + let mut using_lockfile_copy = false; + // The manifest is a rust file, so this means its a script manifest + if let Some(lockfile) = lockfile_path { + let target_lockfile = + target_dir.join("rust-analyzer").join("metadata").join(kind).join("Cargo.lock"); + match std::fs::copy(&lockfile, &target_lockfile) { + Ok(_) => { + using_lockfile_copy = true; + other_options.push("--lockfile-path".to_owned()); + other_options.push(target_lockfile.to_string()); + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + // There exists no lockfile yet + using_lockfile_copy = true; + other_options.push("--lockfile-path".to_owned()); + other_options.push(target_lockfile.to_string()); + } + Err(e) => { + tracing::warn!( + "Failed to copy lock file from `{lockfile}` to `{target_lockfile}`: {e}", + ); + } + } + } + if using_lockfile_copy { + other_options.push("-Zunstable-options".to_owned()); + command.env("RUSTC_BOOTSTRAP", "1"); + } + // No need to lock it if we copied the lockfile, we won't modify the original after all/ + // This way cargo cannot error out on us if the lockfile requires updating. + if !using_lockfile_copy && locked { + other_options.push("--locked".to_owned()); + } + command.other_options(other_options); + + // FIXME: Fetching metadata is a slow process, as it might require + // calling crates.io. We should be reporting progress here, but it's + // unclear whether cargo itself supports it. + progress("cargo metadata: started".to_owned()); + + let res = (|| -> anyhow::Result<(_, _)> { + let mut errored = false; + let output = + spawn_with_streaming_output(command.cargo_command(), &mut |_| (), &mut |line| { + errored = errored || line.starts_with("error") || line.starts_with("warning"); + if errored { + progress("cargo metadata: ?".to_owned()); + return; + } + progress(format!("cargo metadata: {line}")); + })?; + if !output.status.success() { + progress(format!("cargo metadata: failed {}", output.status)); + let error = cargo_metadata::Error::CargoMetadata { + stderr: String::from_utf8(output.stderr)?, + } + .into(); + if !no_deps { + // If we failed to fetch metadata with deps, return pre-fetched result without them. + // This makes r-a still work partially when offline. + if let Ok(metadata) = no_deps_result { + tracing::warn!( + ?error, + "`cargo metadata` failed and returning succeeded result with `--no-deps`" + ); + return Ok((metadata, Some(error))); + } + } + return Err(error); + } + let stdout = from_utf8(&output.stdout)? + .lines() + .find(|line| line.starts_with('{')) + .ok_or(cargo_metadata::Error::NoJson)?; + Ok((cargo_metadata::MetadataCommand::parse(stdout)?, None)) + })() + .with_context(|| format!("Failed to run `{:?}`", command.cargo_command())); + progress("cargo metadata: finished".to_owned()); + res + } +} diff --git a/crates/project-model/src/env.rs b/crates/project-model/src/env.rs index 9e0415c3b39c..d281492fc98c 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" { + + for (key, entry) in env_json { + let serde_json::Value::Object(entry) = entry else { continue; - } - if let Some(suffix) = env.get(key) { - env.insert(key, base.join(suffix).to_string()); - } - } - env -} + }; + let Some(value) = entry.get("value").and_then(|v| v.as_str()) else { + continue; + }; -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"); + 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 436af64cf132..3bf3d06e6b1a 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/sysroot.rs b/crates/project-model/src/sysroot.rs index 9f19260d3099..9781c46737d8 100644 --- a/crates/project-model/src/sysroot.rs +++ b/crates/project-model/src/sysroot.rs @@ -9,14 +9,15 @@ use std::{env, fs, ops::Not, path::Path, process::Command}; use anyhow::{Result, format_err}; use itertools::Itertools; -use paths::{AbsPath, AbsPathBuf, Utf8PathBuf}; +use paths::{AbsPath, AbsPathBuf, Utf8Path, Utf8PathBuf}; use rustc_hash::FxHashMap; use stdx::format_to; use toolchain::{Tool, probe_for_binary}; use crate::{ CargoWorkspace, ManifestPath, ProjectJson, RustSourceWorkspaceConfig, - cargo_workspace::CargoMetadataConfig, utf8_stdout, + cargo_workspace::{CargoMetadataConfig, FetchMetadata}, + utf8_stdout, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -211,6 +212,7 @@ impl Sysroot { sysroot_source_config: &RustSourceWorkspaceConfig, no_deps: bool, current_dir: &AbsPath, + target_dir: &Utf8Path, progress: &dyn Fn(String), ) -> Option { assert!(matches!(self.workspace, RustLibSrcWorkspace::Empty), "workspace already loaded"); @@ -224,6 +226,7 @@ impl Sysroot { match self.load_library_via_cargo( &library_manifest, current_dir, + target_dir, cargo_config, no_deps, progress, @@ -319,6 +322,7 @@ impl Sysroot { &self, library_manifest: &ManifestPath, current_dir: &AbsPath, + target_dir: &Utf8Path, cargo_config: &CargoMetadataConfig, no_deps: bool, progress: &dyn Fn(String), @@ -331,16 +335,11 @@ impl Sysroot { Some("nightly".to_owned()), ); - let (mut res, _) = CargoWorkspace::fetch_metadata( - library_manifest, - current_dir, - &cargo_config, - self, - no_deps, - // Make sure we never attempt to write to the sysroot - true, - progress, - )?; + // Make sure we never attempt to write to the sysroot + let locked = true; + let (mut res, _) = + FetchMetadata::new(library_manifest, current_dir, &cargo_config, self, no_deps) + .exec(target_dir, locked, progress)?; // Patch out `rustc-std-workspace-*` crates to point to the real crates. // This is done prior to `CrateGraph` construction to prevent de-duplication logic from failing. diff --git a/crates/project-model/src/tests.rs b/crates/project-model/src/tests.rs index f229e9a650d0..ed72520f40d4 100644 --- a/crates/project-model/src/tests.rs +++ b/crates/project-model/src/tests.rs @@ -239,8 +239,13 @@ fn smoke_test_real_sysroot_cargo() { ); let cwd = AbsPathBuf::assert_utf8(temp_dir().join("smoke_test_real_sysroot_cargo")); std::fs::create_dir_all(&cwd).unwrap(); - let loaded_sysroot = - sysroot.load_workspace(&RustSourceWorkspaceConfig::default_cargo(), false, &cwd, &|_| ()); + let loaded_sysroot = sysroot.load_workspace( + &RustSourceWorkspaceConfig::default_cargo(), + false, + &cwd, + &Utf8PathBuf::default(), + &|_| (), + ); if let Some(loaded_sysroot) = loaded_sysroot { sysroot.set_workspace(loaded_sysroot); } diff --git a/crates/project-model/src/toolchain_info/rustc_cfg.rs b/crates/project-model/src/toolchain_info/rustc_cfg.rs index a77f76797fca..6e06e88bf7ac 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 a4d0ec69537a..a28f468e692d 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 f6ab85321978..9f12ededb613 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 91ba85985914..357053d8e825 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 43db84b4fa36..677f29e3c60a 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_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, - }, + cargo_config_file, + 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}, 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) @@ -282,10 +282,24 @@ impl ProjectWorkspace { .ok() .flatten(); + let fetch_metadata = FetchMetadata::new( + cargo_toml, + workspace_dir, + &CargoMetadataConfig { + features: features.clone(), + targets: targets.clone(), + extra_args: extra_args.clone(), + extra_env: extra_env.clone(), + toolchain_version: toolchain.clone(), + kind: "workspace", + }, + &sysroot, + *no_deps, + ); let target_dir = config .target_dir .clone() - .or_else(|| cargo_config_build_target_dir(cargo_toml, extra_env, &sysroot)) + .or_else(|| fetch_metadata.no_deps_metadata().map(|m| m.target_directory.clone())) .unwrap_or_else(|| workspace_dir.join("target").into()); // We spawn a bunch of processes to query various information about the workspace's @@ -319,7 +333,7 @@ impl ProjectWorkspace { }; rustc_dir.and_then(|rustc_dir| { info!(workspace = %cargo_toml, rustc_dir = %rustc_dir, "Using rustc source"); - match CargoWorkspace::fetch_metadata( + match FetchMetadata::new( &rustc_dir, workspace_dir, &CargoMetadataConfig { @@ -327,15 +341,12 @@ impl ProjectWorkspace { targets: targets.clone(), extra_args: extra_args.clone(), extra_env: extra_env.clone(), - target_dir: target_dir.clone(), toolchain_version: toolchain.clone(), kind: "rustc-dev" }, &sysroot, *no_deps, - true, - progress, - ) { + ).exec(&target_dir, true, progress) { Ok((meta, _error)) => { let workspace = CargoWorkspace::new( meta, @@ -364,40 +375,22 @@ impl ProjectWorkspace { }) }); - let cargo_metadata = s.spawn(|| { - CargoWorkspace::fetch_metadata( - cargo_toml, - workspace_dir, - &CargoMetadataConfig { - features: features.clone(), - targets: targets.clone(), - extra_args: extra_args.clone(), - extra_env: extra_env.clone(), - target_dir: target_dir.clone(), - toolchain_version: toolchain.clone(), - kind: "workspace", - }, - &sysroot, - *no_deps, - false, - progress, - ) - }); + let cargo_metadata = s.spawn(|| fetch_metadata.exec(&target_dir, false, progress)); let loaded_sysroot = s.spawn(|| { sysroot.load_workspace( &RustSourceWorkspaceConfig::CargoMetadata(sysroot_metadata_config( config, &targets, toolchain.clone(), - target_dir.clone(), )), config.no_deps, workspace_dir, + &target_dir, progress, ) }); 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 +469,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 @@ -502,6 +493,7 @@ impl ProjectWorkspace { &RustSourceWorkspaceConfig::Json(*sysroot_project), config.no_deps, project_root, + &target_dir, progress, ) } else { @@ -510,10 +502,10 @@ impl ProjectWorkspace { config, &targets, toolchain.clone(), - target_dir, )), config.no_deps, project_root, + &target_dir, progress, ) } @@ -554,7 +546,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 +556,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( @@ -571,17 +564,17 @@ impl ProjectWorkspace { config, &targets, toolchain.clone(), - target_dir.clone(), )), config.no_deps, dir, + &target_dir, &|_| (), ); if let Some(loaded_sysroot) = loaded_sysroot { sysroot.set_workspace(loaded_sysroot); } - let cargo_script = CargoWorkspace::fetch_metadata( + let fetch_metadata = FetchMetadata::new( detached_file, dir, &CargoMetadataConfig { @@ -589,25 +582,26 @@ impl ProjectWorkspace { targets, extra_args: config.extra_args.clone(), extra_env: config.extra_env.clone(), - target_dir, toolchain_version: toolchain.clone(), kind: "detached-file", }, &sysroot, config.no_deps, - false, - &|_| (), - ) - .ok() - .map(|(ws, error)| { - let cargo_config_extra_env = - cargo_config_env(detached_file, &config.extra_env, &sysroot); - ( - CargoWorkspace::new(ws, detached_file.clone(), cargo_config_extra_env, false), - WorkspaceBuildScripts::default(), - error.map(Arc::new), - ) - }); + ); + let target_dir = config + .target_dir + .clone() + .or_else(|| fetch_metadata.no_deps_metadata().map(|m| m.target_directory.clone())) + .unwrap_or_else(|| dir.join("target").into()); + let cargo_script = + fetch_metadata.exec(&target_dir, false, &|_| ()).ok().map(|(ws, error)| { + 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(), + error.map(Arc::new), + ) + }); Ok(ProjectWorkspace { kind: ProjectWorkspaceKind::DetachedFile { @@ -1889,15 +1883,33 @@ fn sysroot_metadata_config( config: &CargoConfig, targets: &[String], toolchain_version: Option, - target_dir: Utf8PathBuf, ) -> CargoMetadataConfig { CargoMetadataConfig { features: Default::default(), targets: targets.to_vec(), extra_args: Default::default(), extra_env: config.extra_env.clone(), - target_dir, toolchain_version, 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() +} diff --git a/crates/rust-analyzer/src/cli/rustc_tests.rs b/crates/rust-analyzer/src/cli/rustc_tests.rs index f97bf832442e..30ac93fb6f83 100644 --- a/crates/rust-analyzer/src/cli/rustc_tests.rs +++ b/crates/rust-analyzer/src/cli/rustc_tests.rs @@ -9,6 +9,7 @@ use hir::{ChangeWithProcMacros, Crate}; use ide::{AnalysisHost, DiagnosticCode, DiagnosticsConfig}; use ide_db::base_db; use itertools::Either; +use paths::Utf8PathBuf; use profile::StopWatch; use project_model::toolchain_info::{QueryConfig, target_data_layout}; use project_model::{ @@ -79,6 +80,7 @@ impl Tester { &RustSourceWorkspaceConfig::default_cargo(), false, &path, + &Utf8PathBuf::default(), &|_| (), ); if let Some(loaded_sysroot) = loaded_sysroot {