From 815fa76e5f8f48ea95e02e3a4afc1c2fd3792341 Mon Sep 17 00:00:00 2001 From: Angad Tendulkar Date: Fri, 28 Aug 2026 20:50:54 -0400 Subject: [PATCH 1/4] Fix NamedTempFile copies never succeeding and being unusable by child processes NamedTempFile::new opened the file with only `create_new(true)`, which the standard library rejects (creating a file requires write or append access), so every use of NamedTempFile - notably the Cargo.lock copies handed to `cargo metadata` and proc-macro dylib copies on Windows - silently failed. Additionally, on Linux the file was immediately unlinked and re-addressed as /proc/self/fd/N. That path is only meaningful inside the rust-analyzer process; a spawned cargo cannot read it (the fd is CLOEXEC, and /proc/self refers to the child's own fd table). Fix both in new_from_existing by copying into a fresh, uniquely named temporary directory while keeping the original file name. Keeping the name matters because cargo refuses lockfile paths not literally named "Cargo.lock". The copy is also made writable, since the source may live in a read-only toolchain installation (e.g. the nix store). Co-Authored-By: Claude Fable 5 --- crates/stdx/src/tempfile.rs | 106 +++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/crates/stdx/src/tempfile.rs b/crates/stdx/src/tempfile.rs index b67d4c46c39a..88d76910f51d 100644 --- a/crates/stdx/src/tempfile.rs +++ b/crates/stdx/src/tempfile.rs @@ -11,6 +11,7 @@ pub struct NamedTempFile { _file: Option, path: PathBuf, delete_on_drop: bool, + dir_to_delete: Option, } impl NamedTempFile { @@ -18,17 +19,39 @@ impl NamedTempFile { imp::create(prefix) } - /// Creates a new `NamedTempFile` that is a copy of an existing file. + /// Creates a new `NamedTempFile` that is a copy of an existing file, keeping its file + /// name by placing the copy in a fresh temporary directory. + /// + /// Unlike [`NamedTempFile::new`], the returned path is guaranteed to stay linked in the + /// filesystem until the value is dropped, so it can be handed to other processes. Some + /// consumers also require the exact file name to be preserved (e.g. Cargo insists that + /// a lockfile is named `Cargo.lock`), which the temporary directory provides. pub fn new_from_existing(prefix: &str, existing: &Path) -> io::Result { - let result = NamedTempFile::new(prefix)?; - std::fs::copy(existing, &result.path)?; - Ok(result) + let file_name = existing.file_name().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "existing file has no file name") + })?; + let dir = general_imp::create_dir(prefix)?; + let path = dir.join(file_name); + let copy = (|| { + std::fs::copy(existing, &path)?; + // The source may be read-only (e.g. a lockfile in a read-only toolchain + // installation); make the copy writable so consumers can update it. + let mut perms = std::fs::metadata(&path)?.permissions(); + #[allow(clippy::permissions_set_readonly_false)] + perms.set_readonly(false); + std::fs::set_permissions(&path, perms) + })(); + if let Err(e) = copy { + _ = std::fs::remove_dir_all(&dir); + return Err(e); + } + Ok(NamedTempFile { _file: None, path, delete_on_drop: true, dir_to_delete: Some(dir) }) } /// Creates a `NamedTempFile` from a path, without deleting it on drop. #[inline] pub fn from_path(path: PathBuf) -> NamedTempFile { - NamedTempFile { _file: None, path, delete_on_drop: false } + NamedTempFile { _file: None, path, delete_on_drop: false, dir_to_delete: None } } #[inline] @@ -42,6 +65,11 @@ impl Drop for NamedTempFile { if self.delete_on_drop && std::fs::remove_file(&self.path).is_err() { tracing::info!("cannot remove temporary file {}", self.path.display()); } + if let Some(dir) = &self.dir_to_delete + && std::fs::remove_dir(dir).is_err() + { + tracing::info!("cannot remove temporary directory {}", dir.display()); + } } } @@ -67,7 +95,8 @@ mod general_imp { INTERNAL_COUNTER.fetch_add(1, Ordering::AcqRel), )); let mut open_options = OpenOptions::new(); - open_options.create_new(true); + // `create_new` requires the file to be opened with write or append access. + open_options.write(true).create_new(true); options_callback(&mut open_options); match open_options.open(&path) { Err(e) if e.kind() == ErrorKind::AlreadyExists => {} @@ -83,6 +112,28 @@ mod general_imp { } } } + + /// Creates a fresh, uniquely named temporary directory. + pub(super) fn create_dir(prefix: &str) -> io::Result { + let temp_dir = std::env::temp_dir().canonicalize()?; + let pid = std::process::id(); + loop { + let path = temp_dir.join(format!( + "{prefix}{pid:x}-{:x}", + INTERNAL_COUNTER.fetch_add(1, Ordering::AcqRel), + )); + match std::fs::create_dir(&path) { + Err(e) if e.kind() == ErrorKind::AlreadyExists => {} + Err(e) => { + return Err(io::Error::new( + e.kind(), + format!("error creating directory {path:?}: {e}"), + )); + } + Ok(()) => return Ok(path), + } + } + } } #[cfg(any( @@ -125,7 +176,7 @@ mod imp { delete_on_drop = false; } } - Ok(NamedTempFile { _file: Some(file), path, delete_on_drop }) + Ok(NamedTempFile { _file: Some(file), path, delete_on_drop, dir_to_delete: None }) } } @@ -143,7 +194,7 @@ mod imp { options.attributes(FILE_ATTRIBUTE_TEMPORARY); options.custom_flags(FILE_FLAG_DELETE_ON_CLOSE); })?; - Ok(NamedTempFile { _file: Some(file), path, delete_on_drop: false }) + Ok(NamedTempFile { _file: Some(file), path, delete_on_drop: false, dir_to_delete: None }) } } @@ -159,6 +210,43 @@ mod imp { pub(super) fn create(prefix: &str) -> io::Result { let (file, path) = general_imp::create(prefix, |_| {})?; - Ok(NamedTempFile { _file: Some(file), path, delete_on_drop: true }) + Ok(NamedTempFile { _file: Some(file), path, delete_on_drop: true, dir_to_delete: None }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_creates_a_writable_file() { + let temp = NamedTempFile::new("stdx-test-new").unwrap(); + std::fs::write(temp.path(), b"hello").unwrap(); + } + + #[test] + fn new_from_existing_keeps_file_name_and_is_writable() { + let source = NamedTempFile::new_from_existing( + "stdx-test-source", + &{ + let dir = general_imp::create_dir("stdx-test-orig").unwrap(); + let path = dir.join("Cargo.lock"); + std::fs::write(&path, b"contents").unwrap(); + // Simulate a read-only source, like a lockfile in a read-only + // toolchain installation. + let mut perms = std::fs::metadata(&path).unwrap().permissions(); + perms.set_readonly(true); + std::fs::set_permissions(&path, perms).unwrap(); + path + }, + ) + .unwrap(); + // The copy keeps the file name so that consumers which require an exact + // name (Cargo insists on `Cargo.lock`) can use it. + assert_eq!(source.path().file_name().unwrap(), "Cargo.lock"); + assert_eq!(std::fs::read(source.path()).unwrap(), b"contents"); + // The path is a real, linked file that other processes could open, and + // the copy is writable even when the source was read-only. + std::fs::write(source.path(), b"updated").unwrap(); } } From c8daba4fc939b175891fc79f05a4bd59c31cb4df Mon Sep 17 00:00:00 2001 From: Angad Tendulkar Date: Fri, 28 Aug 2026 20:51:07 -0400 Subject: [PATCH 2/4] Search ancestor directories for the lockfile when fetching cargo metadata The rustc-dev dist component ships the compiler sources with `rustc-src/rust/compiler/rustc/Cargo.toml` as the entry manifest, but without a workspace root manifest next to the lockfile, which lives at `rustc-src/rust/Cargo.lock`. FetchMetadata only looked for a lockfile right next to the manifest, so for `rust-analyzer.rustc.source = "discover"` setups the metadata fetch ran with `--locked` and no usable lockfile. On read-only toolchain installations (e.g. rustup toolchains on nix) cargo then fails to create one, and rust-analyzer silently degrades to `--no-deps` metadata, dropping rustc_middle and friends from the crate graph entirely - rustc_private projects lose all type information for rustc crates. Walk up from the manifest to find the lockfile and reuse the existing lockfile-copy mechanism, which keeps cargo from touching the original. Co-Authored-By: Claude Fable 5 --- crates/project-model/src/cargo_workspace.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/project-model/src/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs index 9d2588380c22..7e8eae5763fa 100644 --- a/crates/project-model/src/cargo_workspace.rs +++ b/crates/project-model/src/cargo_workspace.rs @@ -704,10 +704,22 @@ impl FetchMetadata { if cargo_toml.is_rust_manifest() { other_options.push("-Zscript".to_owned()); } else if let Some(v) = config.toolchain_version.as_ref() { - lockfile_copy = make_lockfile_copy( - v, - &<_ as AsRef>::as_ref(cargo_toml).with_extension("lock"), - ); + // Search the manifest's directory and its ancestors for the lockfile. The + // manifest may be an entry point into a larger source distribution whose + // lockfile lives higher up. Notably, the `rustc-dev` component ships + // `rustc-src/rust/compiler/rustc/Cargo.toml` without a sibling lockfile or a + // workspace root manifest; its lockfile is at `rustc-src/rust/Cargo.lock`. Without + // it, `cargo metadata` tries to write a fresh lockfile next to the manifest, which + // fails in read-only toolchain installations and degrades us to `--no-deps` + // metadata, losing all dependency crates (e.g. `rustc_middle`). + let lockfile_path = <_ as AsRef>::as_ref(cargo_toml) + .ancestors() + .skip(1) + .map(|dir| dir.join("Cargo.lock")) + .find(|lock| lock.as_std_path().is_file()); + if let Some(lockfile_path) = lockfile_path { + lockfile_copy = make_lockfile_copy(v, &lockfile_path); + } } if !config.targets.is_empty() { From c31b0bce9f646a9d9d2a55edd1bed3ecf6d494ce Mon Sep 17 00:00:00 2001 From: Angad Tendulkar Date: Fri, 28 Aug 2026 20:51:08 -0400 Subject: [PATCH 3/4] Follow symlinks when discovering prebuilt rustc proc-macro dylibs The scan of the target libdir for rustc_macros & co. used DirEntry::file_type(), which does not follow symlinks. Toolchains assembled out of symlinks (e.g. by nix / oxalica's rust-overlay) link every dylib into the sysroot, so all proc-macro dylibs were skipped. As a result `rustc_queries!` never expanded for rustc_private projects and the macro-generated TyCtxt query getters (`tcx.mir_keys(())` etc.) did not resolve at all. Use fs::metadata, which traverses symlinks, instead. Co-Authored-By: Claude Fable 5 --- crates/project-model/src/build_dependencies.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/project-model/src/build_dependencies.rs b/crates/project-model/src/build_dependencies.rs index 8da0a574e764..40cf975e4522 100644 --- a/crates/project-model/src/build_dependencies.rs +++ b/crates/project-model/src/build_dependencies.rs @@ -211,7 +211,10 @@ impl WorkspaceBuildScripts { let proc_macro_dylibs: Vec<(String, AbsPathBuf)> = std::fs::read_dir(target_libdir)? .filter_map(|entry| { let dir_entry = entry.ok()?; - if dir_entry.file_type().ok()?.is_file() { + // Use `fs::metadata` rather than `DirEntry::file_type` so that symlinks + // are followed; sysroots assembled out of symlinks (e.g. by nix) link + // the proc-macro dylibs into the target libdir. + if std::fs::metadata(dir_entry.path()).ok()?.is_file() { let path = dir_entry.path(); let extension = path.extension()?; if extension == std::env::consts::DLL_EXTENSION { From 252d99c4fe77f7a7f88fce5482debe9beb0a9ef8 Mon Sep 17 00:00:00 2001 From: Angad Tendulkar Date: Fri, 28 Aug 2026 22:07:44 -0400 Subject: [PATCH 4/4] rustfmt --- crates/stdx/src/tempfile.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/stdx/src/tempfile.rs b/crates/stdx/src/tempfile.rs index 88d76910f51d..ff9e343abd52 100644 --- a/crates/stdx/src/tempfile.rs +++ b/crates/stdx/src/tempfile.rs @@ -226,20 +226,17 @@ mod tests { #[test] fn new_from_existing_keeps_file_name_and_is_writable() { - let source = NamedTempFile::new_from_existing( - "stdx-test-source", - &{ - let dir = general_imp::create_dir("stdx-test-orig").unwrap(); - let path = dir.join("Cargo.lock"); - std::fs::write(&path, b"contents").unwrap(); - // Simulate a read-only source, like a lockfile in a read-only - // toolchain installation. - let mut perms = std::fs::metadata(&path).unwrap().permissions(); - perms.set_readonly(true); - std::fs::set_permissions(&path, perms).unwrap(); - path - }, - ) + let source = NamedTempFile::new_from_existing("stdx-test-source", &{ + let dir = general_imp::create_dir("stdx-test-orig").unwrap(); + let path = dir.join("Cargo.lock"); + std::fs::write(&path, b"contents").unwrap(); + // Simulate a read-only source, like a lockfile in a read-only + // toolchain installation. + let mut perms = std::fs::metadata(&path).unwrap().permissions(); + perms.set_readonly(true); + std::fs::set_permissions(&path, perms).unwrap(); + path + }) .unwrap(); // The copy keeps the file name so that consumers which require an exact // name (Cargo insists on `Cargo.lock`) can use it.