Skip to content
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
34 changes: 26 additions & 8 deletions crates/uv-build-backend/src/wheel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,12 +596,17 @@ impl ZipDirectoryWriter {
}

/// Add a file with the given name and return a writer for it.
fn new_writer<'slf>(&'slf mut self, path: &str) -> Result<Box<dyn Write + 'slf>, Error> {
// TODO(konsti): We need to preserve permissions, at least the executable bit.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice

self.writer.start_file(
path,
zip::write::FileOptions::default().compression_method(self.compression),
)?;
fn new_writer<'slf>(
&'slf mut self,
path: &str,
executable_bit: bool,
) -> Result<Box<dyn Write + 'slf>, Error> {
// 644 is the default of the zip crate.
let permissions = if executable_bit { 775 } else { 664 };
let options = zip::write::FileOptions::default()
.unix_permissions(permissions)
.compression_method(self.compression);
self.writer.start_file(path, options)?;
Ok(Box::new(&mut self.writer))
}
}
Expand All @@ -626,7 +631,16 @@ impl DirectoryWriter for ZipDirectoryWriter {
fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> {
trace!("Adding {} from {}", path, file.user_display());
let mut reader = BufReader::new(File::open(file)?);
let mut writer = self.new_writer(path)?;
// Preserve the executable bit, especially for scripts
#[cfg(unix)]
let executable_bit = {
use std::os::unix::fs::PermissionsExt;
file.metadata()?.permissions().mode() & 0o111 != 0
};
// Windows has no executable bit
#[cfg(not(unix))]
let executable_bit = false;
let mut writer = self.new_writer(path, executable_bit)?;
let record = write_hashed(path, &mut reader, &mut writer)?;
drop(writer);
self.record.push(record);
Expand All @@ -644,7 +658,11 @@ impl DirectoryWriter for ZipDirectoryWriter {
let record_path = format!("{dist_info_dir}/RECORD");
trace!("Adding {record_path}");
let record = mem::take(&mut self.record);
write_record(&mut self.new_writer(&record_path)?, dist_info_dir, record)?;
write_record(
&mut self.new_writer(&record_path, false)?,
dist_info_dir,
record,
)?;

trace!("Adding central directory");
self.writer.finish()?;
Expand Down
64 changes: 64 additions & 0 deletions crates/uv/tests/it/build_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,67 @@ fn built_by_uv_editable() -> Result<()> {

Ok(())
}

#[cfg(unix)]
#[test]
fn preserve_executable_bit() -> Result<()> {
use std::io::Write;

let context = TestContext::new("3.12");

let project_dir = context.temp_dir.path().join("preserve_executable_bit");
context
.init()
.arg("--build-backend")
.arg("uv")
.arg("--preview")
.arg(&project_dir)
.assert()
.success();

fs_err::OpenOptions::new()
.write(true)
.append(true)
.open(project_dir.join("pyproject.toml"))?
.write_all(
indoc! {r#"
[tool.uv.build-backend.data]
scripts = "scripts"
"#}
.as_bytes(),
)?;

fs_err::create_dir(project_dir.join("scripts"))?;
fs_err::write(
project_dir.join("scripts").join("greet.sh"),
indoc! {r#"
echo "Hi from the shell"
"#},
)?;

context
.build_backend()
.arg("build-wheel")
.arg(context.temp_dir.path())
.current_dir(project_dir)
.assert()
.success();

let wheel = context
.temp_dir
.path()
.join("preserve_executable_bit-0.1.0-py3-none-any.whl");
context.pip_install().arg(wheel).assert().success();

uv_snapshot!(Command::new("greet.sh")
.env(EnvVars::PATH, venv_bin_path(&context.venv)), @r###"
success: true
exit_code: 0
----- stdout -----
Hi from the shell

----- stderr -----
"###);

Ok(())
}