Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed wheel unpack not setting the executable bit #514

Merged
merged 5 commits into from
Mar 13, 2023
Merged

Fixed wheel unpack not setting the executable bit #514

merged 5 commits into from
Mar 13, 2023

Conversation

agronholm
Copy link
Contributor

Fixes #505.

@codecov
Copy link

codecov bot commented Mar 13, 2023

Codecov Report

Patch coverage: 100.00% and project coverage change: +0.07 🎉

Comparison is base (26d2e0d) 72.76% compared to head (9d0249e) 72.83%.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #514      +/-   ##
==========================================
+ Coverage   72.76%   72.83%   +0.07%     
==========================================
  Files          13       13              
  Lines        1061     1064       +3     
==========================================
+ Hits          772      775       +3     
  Misses        289      289              
Impacted Files Coverage Δ
src/wheel/cli/unpack.py 100.00% <100.00%> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@agronholm agronholm requested a review from henryiii March 13, 2023 16:52
@henryiii
Copy link
Contributor

This seems to have been a zipfile bug since at least 2012, python/cpython#59999.

@agronholm
Copy link
Contributor Author

I copied this technique from pip. Would you say this is good for merging?

@henryiii
Copy link
Contributor

I think so, looks okay, but planning to check it in fully about an hour. I think this code path might get hit in the retagging command.

@henryiii
Copy link
Contributor

Ah, never mind, that was an older version of the tags PR. The merged one doesn't go through this code.

Something weird is happening, though. I tried this on something I thought had a script (based on a GitHub code search), and it seems spyder-5.4.2/spyder-5.4.2.data/data/share/applications/spyder.desktop is now unpacking to spyder-5.4.2/spyder-5.4.2.data/data/share/applications/spyder.desktop/spyder-5.4.2.data/data/share/applications/spyder.desktop.

@agronholm
Copy link
Contributor Author

Can you tell me how to repro?

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
Copy link
Contributor

@henryiii henryiii Mar 13, 2023

Choose a reason for hiding this comment

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

zinfo already has the filename path, so this is duplicating the path, I think. a/b/c.txt -> a/b/a/b/c.txt.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, that makes no sense, as destination remains the same, only zinfo.filename keeps changing. I tested extracting Django-3.2.16-py3-none-any.whl (which happened to be lying around) with both this branch and main, and I didn't see any difference.

Copy link
Contributor

@henryiii henryiii Mar 13, 2023

Choose a reason for hiding this comment

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

You are setting destination with the filename, so now it has both. I think you want wf.extract(zinfo, destination) here. Extract extracts "a/b.txt" from argument 1 to argument 2, keeping the original relative path. Flatting this requires workarounds, see https://stackoverflow.com/a/47632134 for example. Are you sure you were using this branch?

tree Django-4.1.7/Django-4.1.7.dist-info
Django-4.1.7/Django-4.1.7.dist-info
├── AUTHORS
│   └── Django-4.1.7.dist-info
│       └── AUTHORS
├── LICENSE
│   └── Django-4.1.7.dist-info
│       └── LICENSE
├── LICENSE.python
│   └── Django-4.1.7.dist-info
│       └── LICENSE.python
├── METADATA
│   └── Django-4.1.7.dist-info
│       └── METADATA
├── RECORD
│   └── Django-4.1.7.dist-info
│       └── RECORD
├── WHEEL
│   └── Django-4.1.7.dist-info
│       └── WHEEL
├── entry_points.txt
│   └── Django-4.1.7.dist-info
│       └── entry_points.txt
└── top_level.txt
    └── Django-4.1.7.dist-info
        └── top_level.txt

Copy link
Contributor

@henryiii henryiii Mar 13, 2023

Choose a reason for hiding this comment

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

However, this only affects the non-package files, like .dist-info. The package files (for both packages) seem fine. Nevermind, happening to both.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On a deeper look, I saw the problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I basically pushed almost the exact same fix right as you posted that comment 😄

@henryiii
Copy link
Contributor

Repro:

diff --git a/tests/cli/test_unpack.py b/tests/cli/test_unpack.py
index e614aa9..9cd4a94 100644
--- a/tests/cli/test_unpack.py
+++ b/tests/cli/test_unpack.py
@@ -27,9 +27,9 @@ def test_unpack_executable_bit(tmp_path):
     script_path.write_bytes(b"test script")
     script_path.chmod(0o777)
     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

Fix:

diff --git a/src/wheel/cli/unpack.py b/src/wheel/cli/unpack.py
index 17ef0fa..fc3ffa4 100644
--- a/src/wheel/cli/unpack.py
+++ b/src/wheel/cli/unpack.py
@@ -23,11 +23,11 @@ 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 = destination / zinfo.filename
                 extracted_path.chmod(0o777 & ~umask | 0o111)

     print("OK")

@henryiii
Copy link
Contributor

henryiii commented Mar 13, 2023

:D I had the fix almost immediately, getting the repro was the hard part. For some reason I was convinced wf.write(str(script_path), "script") was writing the word "script" to script_path inside the wheel, I think...

I'd recommend adding the patch for the tests, though, just to cover this case. Or a second test / parametrization with a nested file. I pulled your fix, passes my test.

@agronholm
Copy link
Contributor Author

I tried that repro patch against 933d556, but it didn't fail.

@henryiii
Copy link
Contributor

Checked it on pygal (from https://cs.github.com/?scopeName=All+repos&scope=&q=path%3Asetup.py+%22scripts%3D%22):

$ pip download --no-deps pygal
$ wheel unpack pygal-3.0.0-py2.py3-none-any.whl
$ ls -l pygal-3.0.0/pygal-3.0.0.data/scripts/pygal_gen.py
-rw-r--r--  1 henryschreiner  staff  2316 Mar 13 16:09 pygal-3.0.0/pygal-3.0.0.data/scripts/pygal_gen.py
$ gh pr checkout 514
$ wheel unpack pygal-3.0.0-py2.py3-none-any.whl
$ ls -l pygal-3.0.0/pygal-3.0.0.data/scripts/pygal_gen.py
-rwxr-xr-x  1 henryschreiner  staff  2316 Mar 13 16:08 pygal-3.0.0/pygal-3.0.0.data/scripts/pygal_gen.py*

Looks better! What about if all x's aren't set, though? This only checks for 111?

@agronholm
Copy link
Contributor Author

Looks better! What about if all x's aren't set, though? This only checks for 111?

No, it checks for any x (otherwise it would check zinfo.external_attr >> 16 & 0o111 == 0o111 and not what it does now (zinfo.external_attr >> 16 & 0o111 ).

@agronholm
Copy link
Contributor Author

What about the repro though? Can you get it to fail against 933d556?

@henryiii
Copy link
Contributor

henryiii commented Mar 13, 2023

No, it's not failing. Not sure why, but I had a mistake when I first wrote the test, then didn't go back and check the original after I fixed the test after writing the patch. Not sure why it's passing, though.

I know it checks for any x, but it looks like it sets all of them if one of them was set? Shouldn't it try to keep the original setting? (Pip's extraction probably doesn't care, but that's not trying to support round trips, just installing)

@agronholm
Copy link
Contributor Author

Pip doesn't respect the original setting, but I guess wheel unpack is used for a different purpose, so I suppose I could just forego that umask stuff and set the exact same executable pattern on output.

@henryiii
Copy link
Contributor

henryiii commented Mar 13, 2023

Got it: add

assert not script_path.is_dir()

(I think maybe to the original, tested with the nesting). Directories are "executable". :)

@henryiii
Copy link
Contributor

Yes, pip's goal is different, see my (maybe too slow) edit above. :) Unpack should replicate the exact original permission bits. It's just a bug in CPython that it doesn't work via extractall.

Copy link
Contributor

@henryiii henryiii left a comment

Choose a reason for hiding this comment

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

I'm happy with it now. :)

@agronholm agronholm merged commit 934fe17 into main Mar 13, 2023
@agronholm agronholm deleted the fix-505 branch March 13, 2023 20:52
@agronholm
Copy link
Contributor Author

Thanks for the review! It was really helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

wheel unpack doesn't preserve permissions
2 participants