diff --git a/crates/install-wheel-rs/src/linker.rs b/crates/install-wheel-rs/src/linker.rs index 9b72be62e..ff43ba4f3 100644 --- a/crates/install-wheel-rs/src/linker.rs +++ b/crates/install-wheel-rs/src/linker.rs @@ -597,7 +597,7 @@ fn symlink_wheel_files( // The `RECORD` file is modified during installation, so we copy it instead of symlinking. if path.ends_with("RECORD") { - fs::copy(path, &out_path)?; + synchronized_copy(path, &out_path, locks)?; count += 1; continue; } diff --git a/crates/install-wheel-rs/src/wheel.rs b/crates/install-wheel-rs/src/wheel.rs index 820ef49e2..5cccbdb0e 100644 --- a/crates/install-wheel-rs/src/wheel.rs +++ b/crates/install-wheel-rs/src/wheel.rs @@ -327,11 +327,14 @@ pub(crate) fn write_script_entrypoints( // Make the launcher executable. #[cfg(unix)] { + use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; - fs::set_permissions( - site_packages.join(entrypoint_relative), - std::fs::Permissions::from_mode(0o755), - )?; + + let path = site_packages.join(entrypoint_relative); + let permissions = fs::metadata(&path)?.permissions(); + if permissions.mode() & 0o111 != 0o111 { + fs::set_permissions(path, Permissions::from_mode(permissions.mode() | 0o111))?; + } } } } @@ -534,20 +537,64 @@ fn install_script( ) })?; fs::remove_file(&path)?; + + // Make the script executable. We just created the file, so we can set permissions directly. + #[cfg(unix)] + { + use std::fs::Permissions; + use std::os::unix::fs::PermissionsExt; + + let permissions = fs::metadata(&script_absolute)?.permissions(); + if permissions.mode() & 0o111 != 0o111 { + fs::set_permissions( + script_absolute, + Permissions::from_mode(permissions.mode() | 0o111), + )?; + } + } + Some(size_and_encoded_hash) } else { - // reading and writing is slow especially for large binaries, so we move them instead + // Reading and writing is slow (especially for large binaries), so we move them instead, if + // we can. This also retains the file permissions. We _can't_ move (and must copy) if the + // file permissions need to be changed, since we might not own the file. drop(script); - fs::rename(&path, &script_absolute)?; + + #[cfg(unix)] + { + use std::fs::Permissions; + use std::os::unix::fs::PermissionsExt; + + let permissions = fs::metadata(&path)?.permissions(); + + if permissions.mode() & 0o111 == 0o111 { + // If the permissions are already executable, we don't need to change them. + fs::rename(&path, &script_absolute)?; + } else { + // If we have to modify the permissions, copy the file, since we might not own it. + warn!( + "Copying script from {} to {} (permissions: {:o})", + path.simplified_display(), + script_absolute.simplified_display(), + permissions.mode() + ); + + uv_fs::copy_atomic_sync(&path, &script_absolute)?; + + fs::set_permissions( + script_absolute, + Permissions::from_mode(permissions.mode() | 0o111), + )?; + } + } + + #[cfg(not(unix))] + { + fs::rename(&path, &script_absolute)?; + } + None }; - #[cfg(unix)] - { - use std::fs::Permissions; - use std::os::unix::fs::PermissionsExt; - - fs::set_permissions(&script_absolute, Permissions::from_mode(0o755))?; - } // Find the existing entry in the `RECORD`. let relative_to_site_packages = path diff --git a/crates/uv-extract/src/stream.rs b/crates/uv-extract/src/stream.rs index f89767e93..3e3625c18 100644 --- a/crates/uv-extract/src/stream.rs +++ b/crates/uv-extract/src/stream.rs @@ -87,11 +87,13 @@ pub async fn unzip( let path = target.join(path); let permissions = fs_err::tokio::metadata(&path).await?.permissions(); - fs_err::tokio::set_permissions( - &path, - Permissions::from_mode(permissions.mode() | 0o111), - ) - .await?; + if permissions.mode() & 0o111 != 0o111 { + fs_err::tokio::set_permissions( + &path, + Permissions::from_mode(permissions.mode() | 0o111), + ) + .await?; + } } } } @@ -137,11 +139,13 @@ async fn untar_in>( if has_any_executable_bit != 0 { if let Some(path) = crate::tar::unpacked_at(dst.as_ref(), &file.path()?) { let permissions = fs_err::tokio::metadata(&path).await?.permissions(); - fs_err::tokio::set_permissions( - &path, - Permissions::from_mode(permissions.mode() | 0o111), - ) - .await?; + if permissions.mode() & 0o111 != 0o111 { + fs_err::tokio::set_permissions( + &path, + Permissions::from_mode(permissions.mode() | 0o111), + ) + .await?; + } } } } diff --git a/crates/uv-extract/src/sync.rs b/crates/uv-extract/src/sync.rs index 4460d7974..77a7945a3 100644 --- a/crates/uv-extract/src/sync.rs +++ b/crates/uv-extract/src/sync.rs @@ -69,10 +69,12 @@ pub fn unzip( let has_any_executable_bit = mode & 0o111; if has_any_executable_bit != 0 { let permissions = fs_err::metadata(&path)?.permissions(); - fs_err::set_permissions( - &path, - Permissions::from_mode(permissions.mode() | 0o111), - )?; + if permissions.mode() & 0o111 != 0o111 { + fs_err::set_permissions( + &path, + Permissions::from_mode(permissions.mode() | 0o111), + )?; + } } } } diff --git a/crates/uv-fs/src/lib.rs b/crates/uv-fs/src/lib.rs index 619f87b0b..58c73ef12 100644 --- a/crates/uv-fs/src/lib.rs +++ b/crates/uv-fs/src/lib.rs @@ -161,6 +161,23 @@ pub fn write_atomic_sync(path: impl AsRef, data: impl AsRef<[u8]>) -> std: Ok(()) } +/// Copy `from` to `to` atomically using a temporary file and atomic rename. +pub fn copy_atomic_sync(from: impl AsRef, to: impl AsRef) -> std::io::Result<()> { + let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?; + fs_err::copy(from.as_ref(), &temp_file)?; + temp_file.persist(&to).map_err(|err| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Failed to persist temporary file to {}: {}", + to.user_display(), + err.error + ), + ) + })?; + Ok(()) +} + /// Rename a file, retrying (on Windows) if it fails due to transient operating system errors. #[cfg(feature = "tokio")] pub async fn rename_with_retry(