diff --git a/docs/news.rst b/docs/news.rst index 4961729e..2fbe136d 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -4,6 +4,7 @@ Release Notes **UNRELEASED** - Updated vendored ``packaging`` to 23.0 +- ``wheel unpack`` now preserves the executable attribute of extracted files - Fixed spaces in platform names not being converted to underscores (PR by David Tucker) - Fixed ``RECORD`` files in generated wheels missing the regular file attribute - Fixed ``DeprecationWarning`` about the use of the deprecated ``pkg_resources`` API diff --git a/src/wheel/cli/unpack.py b/src/wheel/cli/unpack.py index c6409d4b..d48840e6 100644 --- a/src/wheel/cli/unpack.py +++ b/src/wheel/cli/unpack.py @@ -18,6 +18,13 @@ def unpack(path: str, dest: str = ".") -> None: namever = wf.parsed_filename.group("namever") destination = Path(dest) / namever print(f"Unpacking to: {destination}...", end="", flush=True) - wf.extractall(destination) + for zinfo in wf.filelist: + wf.extract(zinfo, destination) + + # Set permissions to the same values as they were set in the archive + # We have to do this manually due to + # https://github.com/python/cpython/issues/59999 + permissions = zinfo.external_attr >> 16 & 0o777 + destination.joinpath(zinfo.filename).chmod(permissions) print("OK") diff --git a/tests/cli/test_unpack.py b/tests/cli/test_unpack.py index e6a38bb9..ae584af0 100644 --- a/tests/cli/test_unpack.py +++ b/tests/cli/test_unpack.py @@ -1,6 +1,12 @@ from __future__ import annotations +import platform +import stat + +import pytest + from wheel.cli.unpack import unpack +from wheel.wheelfile import WheelFile def test_unpack(wheel_paths, tmp_path): @@ -10,3 +16,21 @@ def test_unpack(wheel_paths, tmp_path): """ for wheel_path in wheel_paths: unpack(wheel_path, str(tmp_path)) + + +@pytest.mark.skipif( + platform.system() == "Windows", reason="Windows does not support the executable bit" +) +def test_unpack_executable_bit(tmp_path): + wheel_path = tmp_path / "test-1.0-py3-none-any.whl" + script_path = tmp_path / "script" + script_path.write_bytes(b"test script") + script_path.chmod(0o755) + with WheelFile(wheel_path, "w") as wf: + wf.write(str(script_path), "nested/script") + + script_path.unlink() + script_path = tmp_path / "test-1.0" / "nested" / "script" + unpack(str(wheel_path), str(tmp_path)) + assert not script_path.is_dir() + assert stat.S_IMODE(script_path.stat().st_mode) == 0o755