From 42e342c5f81a64cf696d7c3dd6eb5168f42431b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 13 Mar 2023 18:16:42 +0200 Subject: [PATCH 1/5] Fixed `wheel unpack` not setting the executable bit for executable files in the archive Fixes #505. --- docs/news.rst | 1 + src/wheel/cli/unpack.py | 12 +++++++++++- tests/cli/test_unpack.py | 23 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) 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..17ef0fa0 100644 --- a/src/wheel/cli/unpack.py +++ b/src/wheel/cli/unpack.py @@ -1,5 +1,7 @@ from __future__ import annotations +import os +import stat from pathlib import Path from ..wheelfile import WheelFile @@ -14,10 +16,18 @@ def unpack(path: str, dest: str = ".") -> None: :param path: The path to the wheel. :param dest: Destination directory (default to current directory). """ + umask = os.umask(0) + os.umask(umask) with WheelFile(path) as wf: 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: + extracted_path = destination / zinfo.filename + wf.extract(zinfo, extracted_path) + + # Set the executable bit if it was set in the archive + if stat.S_IMODE(zinfo.external_attr >> 16 & 0o111): + extracted_path.chmod(0o777 & ~umask | 0o111) print("OK") diff --git a/tests/cli/test_unpack.py b/tests/cli/test_unpack.py index e6a38bb9..8cf223d8 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,20 @@ 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_text("test script") + script_path.chmod(0o777) + with WheelFile(wheel_path, "w") as wf: + wf.write(str(script_path), "script") + + script_path.unlink() + script_path = script_path.parent / "test-1.0" / "script" + unpack(str(wheel_path), str(tmp_path)) + assert stat.S_IMODE(script_path.stat().st_mode) & 0o111 From 933d556f34ee84833c5d1a02d2cccc66c2a6bebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 13 Mar 2023 18:32:19 +0200 Subject: [PATCH 2/5] Fixed EncodingWarning --- tests/cli/test_unpack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cli/test_unpack.py b/tests/cli/test_unpack.py index 8cf223d8..e614aa98 100644 --- a/tests/cli/test_unpack.py +++ b/tests/cli/test_unpack.py @@ -24,7 +24,7 @@ def test_unpack(wheel_paths, tmp_path): 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_text("test script") + script_path.write_bytes(b"test script") script_path.chmod(0o777) with WheelFile(wheel_path, "w") as wf: wf.write(str(script_path), "script") From aa24fc4f2aa87ff720ab610d2fdf8af54d48a891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 13 Mar 2023 21:59:41 +0200 Subject: [PATCH 3/5] Fixed unpack() unpacking files to wrong paths --- src/wheel/cli/unpack.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/wheel/cli/unpack.py b/src/wheel/cli/unpack.py index 17ef0fa0..e84e7902 100644 --- a/src/wheel/cli/unpack.py +++ b/src/wheel/cli/unpack.py @@ -23,11 +23,10 @@ def unpack(path: str, dest: str = ".") -> None: destination = Path(dest) / namever print(f"Unpacking to: {destination}...", end="", flush=True) for zinfo in wf.filelist: - extracted_path = destination / zinfo.filename - wf.extract(zinfo, extracted_path) + wf.extract(zinfo, destination) # Set the executable bit if it was set in the archive if stat.S_IMODE(zinfo.external_attr >> 16 & 0o111): - extracted_path.chmod(0o777 & ~umask | 0o111) + destination.joinpath(zinfo.filename).chmod(0o777 & ~umask | 0o111) print("OK") From ee19d11aa6cc6ed1bf1940e86a44cecf1acf4214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 13 Mar 2023 22:40:00 +0200 Subject: [PATCH 4/5] Set the same permissions on output as in the archive --- src/wheel/cli/unpack.py | 10 +++------- tests/cli/test_unpack.py | 9 +++++---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/wheel/cli/unpack.py b/src/wheel/cli/unpack.py index e84e7902..ddbad959 100644 --- a/src/wheel/cli/unpack.py +++ b/src/wheel/cli/unpack.py @@ -1,7 +1,5 @@ from __future__ import annotations -import os -import stat from pathlib import Path from ..wheelfile import WheelFile @@ -16,8 +14,6 @@ def unpack(path: str, dest: str = ".") -> None: :param path: The path to the wheel. :param dest: Destination directory (default to current directory). """ - umask = os.umask(0) - os.umask(umask) with WheelFile(path) as wf: namever = wf.parsed_filename.group("namever") destination = Path(dest) / namever @@ -25,8 +21,8 @@ def unpack(path: str, dest: str = ".") -> None: for zinfo in wf.filelist: wf.extract(zinfo, destination) - # Set the executable bit if it was set in the archive - if stat.S_IMODE(zinfo.external_attr >> 16 & 0o111): - destination.joinpath(zinfo.filename).chmod(0o777 & ~umask | 0o111) + # Set permissions to the same values as they were set in the archive + 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 e614aa98..ae584af0 100644 --- a/tests/cli/test_unpack.py +++ b/tests/cli/test_unpack.py @@ -25,11 +25,12 @@ 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(0o777) + script_path.chmod(0o755) with WheelFile(wheel_path, "w") as wf: - wf.write(str(script_path), "script") + wf.write(str(script_path), "nested/script") script_path.unlink() - script_path = script_path.parent / "test-1.0" / "script" + script_path = tmp_path / "test-1.0" / "nested" / "script" unpack(str(wheel_path), str(tmp_path)) - assert stat.S_IMODE(script_path.stat().st_mode) & 0o111 + assert not script_path.is_dir() + assert stat.S_IMODE(script_path.stat().st_mode) == 0o755 From 9d0249e031d077c7b7fff8a2ad9c061382aae2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 13 Mar 2023 22:46:16 +0200 Subject: [PATCH 5/5] Link to the CPython bug --- src/wheel/cli/unpack.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wheel/cli/unpack.py b/src/wheel/cli/unpack.py index ddbad959..d48840e6 100644 --- a/src/wheel/cli/unpack.py +++ b/src/wheel/cli/unpack.py @@ -22,6 +22,8 @@ def unpack(path: str, dest: str = ".") -> None: 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)