diff --git a/crates/project-model/src/build_dependencies.rs b/crates/project-model/src/build_dependencies.rs index 664665625f..dec1ffc72d 100644 --- a/crates/project-model/src/build_dependencies.rs +++ b/crates/project-model/src/build_dependencies.rs @@ -16,12 +16,13 @@ use la_arena::ArenaMap; use paths::{AbsPath, AbsPathBuf, Utf8PathBuf}; use rustc_hash::{FxHashMap, FxHashSet}; use serde::Deserialize as _; -use stdx::{always, never}; +use stdx::never; use toolchain::Tool; use crate::{ CargoConfig, CargoFeatures, CargoWorkspace, InvocationStrategy, ManifestPath, Package, Sysroot, - TargetKind, utf8_stdout, + TargetKind, cargo_config_file::make_lockfile_copy, + cargo_workspace::MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH, utf8_stdout, }; /// Output of the build script and proc-macro building steps for a workspace. @@ -77,7 +78,7 @@ impl WorkspaceBuildScripts { let current_dir = workspace.workspace_root(); let allowed_features = workspace.workspace_features(); - let cmd = Self::build_command( + let (_guard, cmd) = Self::build_command( config, &allowed_features, workspace.manifest_path(), @@ -98,7 +99,7 @@ impl WorkspaceBuildScripts { ) -> io::Result> { assert_eq!(config.invocation_strategy, InvocationStrategy::Once); - let cmd = Self::build_command( + let (_guard, cmd) = Self::build_command( config, &Default::default(), // This is not gonna be used anyways, so just construct a dummy here @@ -379,10 +380,6 @@ impl WorkspaceBuildScripts { progress(format!( "building compile-time-deps: proc-macro {name} built" )); - always!( - data.proc_macro_dylib_path == ProcMacroDylibPath::NotBuilt, - "received multiple compiler artifacts for the same package: {message:?}" - ); if data.proc_macro_dylib_path == ProcMacroDylibPath::NotBuilt { data.proc_macro_dylib_path = ProcMacroDylibPath::NotProcMacro; } @@ -434,14 +431,15 @@ impl WorkspaceBuildScripts { current_dir: &AbsPath, sysroot: &Sysroot, toolchain: Option<&semver::Version>, - ) -> io::Result { + ) -> io::Result<(Option, Command)> { match config.run_build_script_command.as_deref() { Some([program, args @ ..]) => { let mut cmd = toolchain::command(program, current_dir, &config.extra_env); cmd.args(args); - Ok(cmd) + Ok((None, cmd)) } _ => { + let mut requires_unstable_options = false; let mut cmd = sysroot.tool(Tool::Cargo, current_dir, &config.extra_env); cmd.args(["check", "--quiet", "--workspace", "--message-format=json"]); @@ -457,7 +455,19 @@ impl WorkspaceBuildScripts { if let Some(target) = &config.target { cmd.args(["--target", target]); } - + let mut temp_dir_guard = None; + if toolchain + .is_some_and(|v| *v >= MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH) + { + let lockfile_path = + <_ as AsRef>::as_ref(manifest_path).with_extension("lock"); + if let Some((temp_dir, target_lockfile)) = make_lockfile_copy(&lockfile_path) { + temp_dir_guard = Some(temp_dir); + cmd.arg("--lockfile-path"); + cmd.arg(target_lockfile.as_str()); + requires_unstable_options = true; + } + } match &config.features { CargoFeatures::All => { cmd.arg("--all-features"); @@ -479,6 +489,7 @@ impl WorkspaceBuildScripts { } if manifest_path.is_rust_manifest() { + requires_unstable_options = true; cmd.arg("-Zscript"); } @@ -488,7 +499,7 @@ impl WorkspaceBuildScripts { // available in current toolchain's cargo, use it to build compile time deps only. const COMP_TIME_DEPS_MIN_TOOLCHAIN_VERSION: semver::Version = semver::Version { major: 1, - minor: 89, + minor: 189, patch: 0, pre: semver::Prerelease::EMPTY, build: semver::BuildMetadata::EMPTY, @@ -498,8 +509,7 @@ impl WorkspaceBuildScripts { toolchain.is_some_and(|v| *v >= COMP_TIME_DEPS_MIN_TOOLCHAIN_VERSION); if cargo_comp_time_deps_available { - cmd.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly"); - cmd.arg("-Zunstable-options"); + requires_unstable_options = true; cmd.arg("--compile-time-deps"); // we can pass this unconditionally, because we won't actually build the // binaries, and as such, this will succeed even on targets without libtest @@ -522,7 +532,11 @@ impl WorkspaceBuildScripts { cmd.env("RA_RUSTC_WRAPPER", "1"); } } - Ok(cmd) + if requires_unstable_options { + cmd.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly"); + cmd.arg("-Zunstable-options"); + } + Ok((temp_dir_guard, cmd)) } } } diff --git a/crates/project-model/src/cargo_config_file.rs b/crates/project-model/src/cargo_config_file.rs index 7966f74df3..a1e7ed0923 100644 --- a/crates/project-model/src/cargo_config_file.rs +++ b/crates/project-model/src/cargo_config_file.rs @@ -1,4 +1,5 @@ //! Read `.cargo/config.toml` as a JSON object +use paths::{Utf8Path, Utf8PathBuf}; use rustc_hash::FxHashMap; use toolchain::Tool; @@ -32,3 +33,24 @@ pub(crate) fn read( Some(json) } + +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 + } + } +} diff --git a/crates/project-model/src/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs index 767a4aade0..b5f68b2435 100644 --- a/crates/project-model/src/cargo_workspace.rs +++ b/crates/project-model/src/cargo_workspace.rs @@ -15,16 +15,18 @@ use span::Edition; use stdx::process::spawn_with_streaming_output; use toolchain::Tool; +use crate::cargo_config_file::make_lockfile_copy; use crate::{CfgOverrides, InvocationStrategy}; use crate::{ManifestPath, Sysroot}; -const MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH: semver::Version = semver::Version { - major: 1, - minor: 82, - patch: 0, - pre: semver::Prerelease::EMPTY, - build: semver::BuildMetadata::EMPTY, -}; +pub(crate) const MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH: semver::Version = + semver::Version { + major: 1, + minor: 82, + patch: 0, + pre: semver::Prerelease::EMPTY, + build: semver::BuildMetadata::EMPTY, + }; /// [`CargoWorkspace`] represents the logical structure of, well, a Cargo /// workspace. It pretty closely mirrors `cargo metadata` output. @@ -552,8 +554,10 @@ impl CargoWorkspace { pub(crate) struct FetchMetadata { command: cargo_metadata::MetadataCommand, + #[expect(dead_code)] manifest_path: ManifestPath, lockfile_path: Option, + #[expect(dead_code)] kind: &'static str, no_deps: bool, no_deps_result: anyhow::Result, @@ -634,7 +638,7 @@ impl FetchMetadata { command.other_options(other_options.clone()); if needs_nightly { - command.env("RUSTC_BOOTSTRAP", "1"); + command.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly"); } // Pre-fetch basic metadata using `--no-deps`, which: @@ -681,11 +685,12 @@ impl FetchMetadata { locked: bool, progress: &dyn Fn(String), ) -> anyhow::Result<(cargo_metadata::Metadata, Option)> { + _ = target_dir; let Self { mut command, - manifest_path, + manifest_path: _, lockfile_path, - kind, + kind: _, no_deps, no_deps_result, mut other_options, @@ -696,54 +701,18 @@ impl FetchMetadata { } let mut using_lockfile_copy = false; - let mut _temp_dir_guard = None; - // The manifest is a rust file, so this means its a script manifest - if let Some(lockfile) = lockfile_path { - _temp_dir_guard = temp_dir::TempDir::with_prefix("rust-analyzer").ok(); - let target_lockfile = _temp_dir_guard - .and_then(|tmp| tmp.path().join("Cargo.lock").try_into().ok()) - .unwrap_or_else(|| { - // When multiple workspaces share the same target dir, they might overwrite into a - // single lockfile path. - // See https://github.com/rust-lang/rust-analyzer/issues/20189#issuecomment-3073520255 - let manifest_path_hash = std::hash::BuildHasher::hash_one( - &std::hash::BuildHasherDefault::::default(), - &manifest_path, - ); - let disambiguator = format!( - "{}_{manifest_path_hash}", - manifest_path.components().nth_back(1).map_or("", |c| c.as_str()) - ); - - target_dir - .join("rust-analyzer") - .join("metadata") - .join(kind) - .join(disambiguator) - .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}", - ); - } - } + let mut _temp_dir_guard; + if let Some(lockfile) = lockfile_path + && let Some((temp_dir, target_lockfile)) = make_lockfile_copy(&lockfile) + { + _temp_dir_guard = temp_dir; + other_options.push("--lockfile-path".to_owned()); + other_options.push(target_lockfile.to_string()); + using_lockfile_copy = true; } if using_lockfile_copy { + command.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly"); 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. @@ -752,13 +721,11 @@ impl FetchMetadata { } 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; + tracing::debug!("Running `{:?}`", command.cargo_command()); let output = spawn_with_streaming_output(command.cargo_command(), &mut |_| (), &mut |line| { errored = errored || line.starts_with("error") || line.starts_with("warning");