Skip to content

Commit 557e750

Browse files
authored
Build backend: Preserve executable bit (#10027)
Fixes #9968
1 parent afdcea6 commit 557e750

File tree

2 files changed

+90
-8
lines changed

2 files changed

+90
-8
lines changed

crates/uv-build-backend/src/wheel.rs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -596,12 +596,17 @@ impl ZipDirectoryWriter {
596596
}
597597

598598
/// Add a file with the given name and return a writer for it.
599-
fn new_writer<'slf>(&'slf mut self, path: &str) -> Result<Box<dyn Write + 'slf>, Error> {
600-
// TODO(konsti): We need to preserve permissions, at least the executable bit.
601-
self.writer.start_file(
602-
path,
603-
zip::write::FileOptions::default().compression_method(self.compression),
604-
)?;
599+
fn new_writer<'slf>(
600+
&'slf mut self,
601+
path: &str,
602+
executable_bit: bool,
603+
) -> Result<Box<dyn Write + 'slf>, Error> {
604+
// 644 is the default of the zip crate.
605+
let permissions = if executable_bit { 775 } else { 664 };
606+
let options = zip::write::FileOptions::default()
607+
.unix_permissions(permissions)
608+
.compression_method(self.compression);
609+
self.writer.start_file(path, options)?;
605610
Ok(Box::new(&mut self.writer))
606611
}
607612
}
@@ -626,7 +631,16 @@ impl DirectoryWriter for ZipDirectoryWriter {
626631
fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> {
627632
trace!("Adding {} from {}", path, file.user_display());
628633
let mut reader = BufReader::new(File::open(file)?);
629-
let mut writer = self.new_writer(path)?;
634+
// Preserve the executable bit, especially for scripts
635+
#[cfg(unix)]
636+
let executable_bit = {
637+
use std::os::unix::fs::PermissionsExt;
638+
file.metadata()?.permissions().mode() & 0o111 != 0
639+
};
640+
// Windows has no executable bit
641+
#[cfg(not(unix))]
642+
let executable_bit = false;
643+
let mut writer = self.new_writer(path, executable_bit)?;
630644
let record = write_hashed(path, &mut reader, &mut writer)?;
631645
drop(writer);
632646
self.record.push(record);
@@ -644,7 +658,11 @@ impl DirectoryWriter for ZipDirectoryWriter {
644658
let record_path = format!("{dist_info_dir}/RECORD");
645659
trace!("Adding {record_path}");
646660
let record = mem::take(&mut self.record);
647-
write_record(&mut self.new_writer(&record_path)?, dist_info_dir, record)?;
661+
write_record(
662+
&mut self.new_writer(&record_path, false)?,
663+
dist_info_dir,
664+
record,
665+
)?;
648666

649667
trace!("Adding central directory");
650668
self.writer.finish()?;

crates/uv/tests/it/build_backend.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,67 @@ fn built_by_uv_editable() -> Result<()> {
214214

215215
Ok(())
216216
}
217+
218+
#[cfg(unix)]
219+
#[test]
220+
fn preserve_executable_bit() -> Result<()> {
221+
use std::io::Write;
222+
223+
let context = TestContext::new("3.12");
224+
225+
let project_dir = context.temp_dir.path().join("preserve_executable_bit");
226+
context
227+
.init()
228+
.arg("--build-backend")
229+
.arg("uv")
230+
.arg("--preview")
231+
.arg(&project_dir)
232+
.assert()
233+
.success();
234+
235+
fs_err::OpenOptions::new()
236+
.write(true)
237+
.append(true)
238+
.open(project_dir.join("pyproject.toml"))?
239+
.write_all(
240+
indoc! {r#"
241+
[tool.uv.build-backend.data]
242+
scripts = "scripts"
243+
"#}
244+
.as_bytes(),
245+
)?;
246+
247+
fs_err::create_dir(project_dir.join("scripts"))?;
248+
fs_err::write(
249+
project_dir.join("scripts").join("greet.sh"),
250+
indoc! {r#"
251+
echo "Hi from the shell"
252+
"#},
253+
)?;
254+
255+
context
256+
.build_backend()
257+
.arg("build-wheel")
258+
.arg(context.temp_dir.path())
259+
.current_dir(project_dir)
260+
.assert()
261+
.success();
262+
263+
let wheel = context
264+
.temp_dir
265+
.path()
266+
.join("preserve_executable_bit-0.1.0-py3-none-any.whl");
267+
context.pip_install().arg(wheel).assert().success();
268+
269+
uv_snapshot!(Command::new("greet.sh")
270+
.env(EnvVars::PATH, venv_bin_path(&context.venv)), @r###"
271+
success: true
272+
exit_code: 0
273+
----- stdout -----
274+
Hi from the shell
275+
276+
----- stderr -----
277+
"###);
278+
279+
Ok(())
280+
}

0 commit comments

Comments
 (0)