diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 36875a89e..ee4d6b297 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -739,8 +739,8 @@ impl SourceBuild { // lock the output dir, but setuptools always writes to `build/` in the source tree, // regardless of whether its output dir is set to somewhere else. let canonical_source_path = self.source_tree.canonicalize()?; - let build_lock_path = - std::env::temp_dir().join(format!("uv-{}.lock", cache_digest(&canonical_source_path))); + let build_lock_path = uv_fs::locks_temp_dir()? + .join(format!("uv-{}.lock", cache_digest(&canonical_source_path))); let _lock = LockedFile::acquire(build_lock_path, self.source_tree.to_string_lossy()).await?; diff --git a/crates/uv-fs/src/lib.rs b/crates/uv-fs/src/lib.rs index dcc0f00b2..a9c4fd627 100644 --- a/crates/uv-fs/src/lib.rs +++ b/crates/uv-fs/src/lib.rs @@ -778,3 +778,69 @@ pub fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> std::io::Re } Ok(()) } + +/// The path to a shared, world-writeable directory for creating locks, to avoid polluting `/tmp`. +/// On Linux this is `/tmp/uv_locks`, if `$TMPDIR` is unset. +pub fn locks_temp_dir() -> std::io::Result { + let locks_dir_path = std::env::temp_dir().join("uv-locks"); + if !locks_dir_path.is_dir() { + create_world_writeable_dir(&locks_dir_path)?; + } + Ok(locks_dir_path) +} + +#[cfg(unix)] +fn create_world_writeable_dir(path: &Path) -> std::io::Result<()> { + // We need the new directory to have 0o777 permissions on Unix, but if we create it and then + // set those permissions, there's a small window in between where other processes could race in + // and get IO errors. You'd think we could use `DirBuilderExt::mode` from the standard library + // to set the permissions during creation, but the process-wide "umask" interferes with that, + // and we can't change our own umask without interfering other threads. Spawn a subprocess that + // can set its own umask before creating the dir for us. This is expensive, but we only need to + // do it the first time. The `-p` flag makes it not an error for multiple callers to do this + // concurrently. + let output = std::process::Command::new("sh") + .arg("-c") + // The -p flag makes it not an error for multiple callers to do this concurrently. The 1 at + // the top of 1777 is the "sticky bit", which prevents different users from deleting each + // other's files. It hardly matters for us in practice, but this is exactly the sort of use + // case it's intended for, and we might as well set it. + .arg(r#"umask 0 && mkdir -p -m 1777 -- "$1""#) + .arg("--") + .arg(path) + .output() + .unwrap(); + if !output.status.success() { + return Err(std::io::Error::other(format!( + "`mkdir {}` failed: {}", + path.to_string_lossy(), + String::from_utf8_lossy(&output.stderr), + ))); + } + Ok(()) +} + +#[cfg(windows)] +fn create_world_writeable_dir(path: &Path) -> std::io::Result<()> { + // Windows doesn't have Unix-style file permission. Just create the dir. + fs_err::create_dir_all(path) +} + +#[test] +fn test_create_world_writeable_dir() -> io::Result<()> { + let parent_dir = tempfile::tempdir()?; + let new_dir_path = parent_dir.path().join("foo"); + create_world_writeable_dir(&new_dir_path)?; + assert!(new_dir_path.exists()); + assert!(new_dir_path.is_dir()); + #[cfg(unix)] + { + // On Unix only, explicitly check the permissions mask of the new directory. + use std::os::unix::fs::PermissionsExt; + let metadata = fs_err::metadata(&new_dir_path)?; + assert_eq!(metadata.permissions().mode() & 0o777, 0o777); + } + // Create a file in the new directory, for good measure. + fs_err::File::create(new_dir_path.join("bar.txt"))?; + Ok(()) +} diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index b62633283..50e991f28 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -1,10 +1,10 @@ use std::borrow::Cow; use std::env::consts::ARCH; use std::fmt::{Display, Formatter}; +use std::io; use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus}; use std::sync::OnceLock; -use std::{env, io}; use configparser::ini::Ini; use fs_err as fs; @@ -625,7 +625,8 @@ impl Interpreter { } else { // Otherwise, use a global lockfile. LockedFile::acquire( - env::temp_dir().join(format!("uv-{}.lock", cache_digest(&self.sys_executable))), + uv_fs::locks_temp_dir()? + .join(format!("uv-{}.lock", cache_digest(&self.sys_executable))), self.sys_prefix.user_display(), ) .await diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index a3249b11a..440ce352e 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -722,21 +722,23 @@ impl ScriptInterpreter { match script { Pep723ItemRef::Script(script) => { LockedFile::acquire( - std::env::temp_dir().join(format!("uv-{}.lock", cache_digest(&script.path))), + uv_fs::locks_temp_dir()? + .join(format!("uv-{}.lock", cache_digest(&script.path))), script.path.simplified_display(), ) .await } Pep723ItemRef::Remote(.., url) => { LockedFile::acquire( - std::env::temp_dir().join(format!("uv-{}.lock", cache_digest(url))), + uv_fs::locks_temp_dir()?.join(format!("uv-{}.lock", cache_digest(url))), url.to_string(), ) .await } Pep723ItemRef::Stdin(metadata) => { LockedFile::acquire( - std::env::temp_dir().join(format!("uv-{}.lock", cache_digest(&metadata.raw))), + uv_fs::locks_temp_dir()? + .join(format!("uv-{}.lock", cache_digest(&metadata.raw))), "stdin".to_string(), ) .await @@ -989,7 +991,7 @@ impl ProjectInterpreter { /// Grab a file lock for the environment to prevent concurrent writes across processes. pub(crate) async fn lock(workspace: &Workspace) -> Result { LockedFile::acquire( - std::env::temp_dir().join(format!( + uv_fs::locks_temp_dir()?.join(format!( "uv-{}.lock", cache_digest(workspace.install_path()) )),