Skip to content
Open
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
5 changes: 4 additions & 1 deletion crates/project-model/src/build_dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 16 additions & 4 deletions crates/project-model/src/cargo_workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Utf8Path>>::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<Utf8Path>>::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() {
Expand Down
103 changes: 94 additions & 9 deletions crates/stdx/src/tempfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,47 @@ pub struct NamedTempFile {
_file: Option<File>,
path: PathBuf,
delete_on_drop: bool,
dir_to_delete: Option<PathBuf>,
}

impl NamedTempFile {
pub fn new(prefix: &str) -> io::Result<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<NamedTempFile> {
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]
Expand All @@ -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());
}
}
}

Expand All @@ -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 => {}
Expand All @@ -83,6 +112,28 @@ mod general_imp {
}
}
}

/// Creates a fresh, uniquely named temporary directory.
pub(super) fn create_dir(prefix: &str) -> io::Result<PathBuf> {
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(
Expand Down Expand Up @@ -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 })
}
}

Expand All @@ -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 })
}
}

Expand All @@ -159,6 +210,40 @@ mod imp {

pub(super) fn create(prefix: &str) -> io::Result<NamedTempFile> {
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();
}
}