Skip to content

fix: Copy lockfiles into target directory before invoking cargo metadata #20018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 59 additions & 8 deletions crates/project-model/src/cargo_workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use anyhow::Context;
use base_db::Env;
use cargo_metadata::{CargoOpt, MetadataCommand};
use la_arena::{Arena, Idx};
use paths::{AbsPath, AbsPathBuf, Utf8PathBuf};
use paths::{AbsPath, AbsPathBuf, Utf8Path, Utf8PathBuf};
use rustc_hash::{FxHashMap, FxHashSet};
use serde_derive::Deserialize;
use serde_json::from_value;
Expand All @@ -18,6 +18,14 @@ use toolchain::Tool;
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,
};

/// [`CargoWorkspace`] represents the logical structure of, well, a Cargo
/// workspace. It pretty closely mirrors `cargo metadata` output.
///
Expand Down Expand Up @@ -291,6 +299,13 @@ pub struct CargoMetadataConfig {
pub extra_args: Vec<String>,
/// Extra env vars to set when invoking the cargo command
pub extra_env: FxHashMap<String, Option<String>>,
/// 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.
/// Used to conditionally enable unstable cargo features.
pub toolchain_version: Option<semver::Version>,
}

// Deserialize helper for the cargo metadata
Expand Down Expand Up @@ -383,17 +398,53 @@ impl CargoWorkspace {
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() {
// Deliberately don't set up RUSTC_BOOTSTRAP or a nightly override here, the user should
// opt into it themselves.
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<Utf8Path>>::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 locked {
other_options.push("--locked".to_owned());
if using_lockfile_copy {
other_options.push("-Zunstable-options".to_owned());
meta.env("RUSTC_BOOTSTRAP", "1");
}
if no_deps {
other_options.push("--no-deps".to_owned());
// 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);

Expand Down Expand Up @@ -427,8 +478,8 @@ impl CargoWorkspace {
current_dir,
config,
sysroot,
locked,
true,
locked,
Comment on lines -430 to +482
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bools considered evil strikes again

progress,
) {
return Ok((metadata, Some(error)));
Expand Down
8 changes: 7 additions & 1 deletion crates/project-model/src/manifest_path.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! See [`ManifestPath`].
use std::{borrow::Borrow, fmt, ops};

use paths::{AbsPath, AbsPathBuf};
use paths::{AbsPath, AbsPathBuf, Utf8Path};

/// More or less [`AbsPathBuf`] with non-None parent.
///
Expand Down Expand Up @@ -78,6 +78,12 @@ impl AsRef<std::ffi::OsStr> for ManifestPath {
}
}

impl AsRef<Utf8Path> for ManifestPath {
fn as_ref(&self) -> &Utf8Path {
self.file.as_ref()
}
}

impl Borrow<AbsPath> for ManifestPath {
fn borrow(&self) -> &AbsPath {
self.file.borrow()
Expand Down
54 changes: 35 additions & 19 deletions crates/project-model/src/sysroot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! but we can't process `.rlib` and need source code instead. The source code
//! is typically installed with `rustup component add rust-src` command.

use core::fmt;
use std::{env, fs, ops::Not, path::Path, process::Command};

use anyhow::{Result, format_err};
Expand Down Expand Up @@ -34,6 +35,19 @@ pub enum RustLibSrcWorkspace {
Empty,
}

impl fmt::Display for RustLibSrcWorkspace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RustLibSrcWorkspace::Workspace(ws) => write!(f, "workspace {}", ws.workspace_root()),
RustLibSrcWorkspace::Json(json) => write!(f, "json {}", json.manifest_or_root()),
RustLibSrcWorkspace::Stitched(stitched) => {
write!(f, "stitched with {} crates", stitched.crates.len())
}
RustLibSrcWorkspace::Empty => write!(f, "empty"),
}
}
}

impl Sysroot {
pub const fn empty() -> Sysroot {
Sysroot {
Expand Down Expand Up @@ -195,6 +209,7 @@ impl Sysroot {
pub fn load_workspace(
&self,
sysroot_source_config: &RustSourceWorkspaceConfig,
current_dir: &AbsPath,
progress: &dyn Fn(String),
) -> Option<RustLibSrcWorkspace> {
assert!(matches!(self.workspace, RustLibSrcWorkspace::Empty), "workspace already loaded");
Expand All @@ -205,10 +220,16 @@ impl Sysroot {
if let RustSourceWorkspaceConfig::CargoMetadata(cargo_config) = sysroot_source_config {
let library_manifest = ManifestPath::try_from(src_root.join("Cargo.toml")).unwrap();
if fs::metadata(&library_manifest).is_ok() {
if let Some(loaded) =
self.load_library_via_cargo(library_manifest, src_root, cargo_config, progress)
{
return Some(loaded);
match self.load_library_via_cargo(
&library_manifest,
current_dir,
cargo_config,
progress,
) {
Ok(loaded) => return Some(loaded),
Err(e) => {
tracing::error!("`cargo metadata` failed on `{library_manifest}` : {e}")
}
}
}
tracing::debug!("Stitching sysroot library: {src_root}");
Expand Down Expand Up @@ -294,11 +315,11 @@ impl Sysroot {

fn load_library_via_cargo(
&self,
library_manifest: ManifestPath,
rust_lib_src_dir: &AbsPathBuf,
library_manifest: &ManifestPath,
current_dir: &AbsPath,
cargo_config: &CargoMetadataConfig,
progress: &dyn Fn(String),
) -> Option<RustLibSrcWorkspace> {
) -> Result<RustLibSrcWorkspace> {
tracing::debug!("Loading library metadata: {library_manifest}");
let mut cargo_config = cargo_config.clone();
// the sysroot uses `public-dependency`, so we make cargo think it's a nightly
Expand All @@ -307,22 +328,16 @@ impl Sysroot {
Some("nightly".to_owned()),
);

let (mut res, _) = match CargoWorkspace::fetch_metadata(
&library_manifest,
rust_lib_src_dir,
let (mut res, _) = CargoWorkspace::fetch_metadata(
library_manifest,
current_dir,
&cargo_config,
self,
false,
// Make sure we never attempt to write to the sysroot
true,
progress,
) {
Ok(it) => it,
Err(e) => {
tracing::error!("`cargo metadata` failed on `{library_manifest}` : {e}");
return None;
}
};
)?;

// 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.
Expand Down Expand Up @@ -373,8 +388,9 @@ impl Sysroot {
res.packages.remove(idx);
});

let cargo_workspace = CargoWorkspace::new(res, library_manifest, Default::default(), true);
Some(RustLibSrcWorkspace::Workspace(cargo_workspace))
let cargo_workspace =
CargoWorkspace::new(res, library_manifest.clone(), Default::default(), true);
Ok(RustLibSrcWorkspace::Workspace(cargo_workspace))
}
}

Expand Down
12 changes: 10 additions & 2 deletions crates/project-model/src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::env::temp_dir;

use base_db::{CrateGraphBuilder, ProcMacroPaths};
use cargo_metadata::Metadata;
use cfg::{CfgAtom, CfgDiff};
Expand Down Expand Up @@ -235,12 +237,18 @@ fn smoke_test_real_sysroot_cargo() {
AbsPath::assert(Utf8Path::new(env!("CARGO_MANIFEST_DIR"))),
&Default::default(),
);
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(), &|_| ());
sysroot.load_workspace(&RustSourceWorkspaceConfig::default_cargo(), &cwd, &|_| ());
if let Some(loaded_sysroot) = loaded_sysroot {
sysroot.set_workspace(loaded_sysroot);
}
assert!(matches!(sysroot.workspace(), RustLibSrcWorkspace::Workspace(_)));
assert!(
matches!(sysroot.workspace(), RustLibSrcWorkspace::Workspace(_)),
"got {}",
sysroot.workspace()
);
let project_workspace = ProjectWorkspace {
kind: ProjectWorkspaceKind::Cargo {
cargo: cargo_workspace,
Expand Down
Loading