Skip to content

gh-96867: Set zipapp execute bits from readable bits - #151970

Open
yuanx749 wants to merge 6 commits into
python:mainfrom
yuanx749:zipapp
Open

gh-96867: Set zipapp execute bits from readable bits#151970
yuanx749 wants to merge 6 commits into
python:mainfrom
yuanx749:zipapp

Conversation

@yuanx749

Copy link
Copy Markdown
Contributor

Fixes gh-96867.

This PR sets execute bits to match readable bits when adding a shebang, preserving the permissions allowed by the user's umask. It also handles pathlib.Path targets when copying an existing archive.

Tests cover archives created with different umask settings.

@pfmoore pfmoore self-assigned this Jul 30, 2026

@pfmoore pfmoore left a comment

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.

I've added some review comments below, but honestly I don't think this is the right fix in any case.

It shouldn't be zipapp's role to implement a function to (robustly) make a file executable. If that is useful functionality, it should be available in the os or pathlib libraries. We could then simply call that function.

Furthermore, the implementation here isn't even the recommended approach from #67679 (comment). Why did you choose this approach over that one? There are issues with that implementation (temporarily changing global state via umask is a bad idea in a multi-threaded program) but it does at least claim to have the benefit of exactly matching the behaviour of chmod +x.

Maybe you could propose the _make_executable function as a public function in os or pathlib, and then revisit this PR to simply use that function?

Comment thread Lib/zipapp.py
| (mode & stat.S_IRGRP) >> 2
| (mode & stat.S_IROTH) >> 2
)
os.chmod(path, mode | executable)

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.

This does not feel at all understandable. Why are we doing >> 2 bit operations instead of using the ST_IX* constants? Also, why are we checking S_IR* anyway? The docs just say that we make the file executable, not that we only make it executable when it's readable. I'm not totally opposed to this, I just want to see some justification - I'm not a Unix user, so I don't know what the typical expectation is when something is documented as "making a file executable".

Also, while I know this also applies to the existing code, the concept of an "executable bit" doesn't exist on Windows, so this code should be Unix-only. In fact the documentation explicitly says that this only happens on POSIX. We should fix this at the same time.

Comment thread Lib/zipapp.py

if interpreter and isinstance(new_archive, str):
os.chmod(new_archive, os.stat(new_archive).st_mode | stat.S_IEXEC)
if interpreter and isinstance(new_archive, (str, os.PathLike)):

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.

Thanks for spotting and fixing the discrepancy between this line and the test in _maybe_open.

Comment thread Lib/zipapp.py
f.write(shebang)


def _make_executable(path):

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.

I'd rather this function required that path is a pathlib.Path object, and used its methods, rather than using raw os calls.

That requires changing the call in _copy_archive, as the argument there could currently be a str. (Longer term, I'd prefer it if we consistently used Path objects rather than repeatedly testing for "str or PathLike", but that's something to handle in a future PR, not here.)

Comment thread Lib/test/test_zipapp.py
with os_helper.temp_umask(umask):
zipapp.create_archive(archive, target, interpreter='python')
self.assertEqual(stat.S_IMODE(target.stat().st_mode), expected_mode)

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.

Don't the existing tests also need fixing? At the very least, test_shebang_is_executable should assert that the newly created archive is readable, as well as executable, because if it's not, the test will fail.

Comment thread Lib/test/test_zipapp.py
source = self.tmpdir / 'source'
source.mkdir()
(source / '__main__.py').touch()
for umask, expected_mode in ((0o022, 0o755), (0o077, 0o700)):

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.

We shouldn't use octal constants here. They aren't understandable to readers who aren't familiar with the octal representation of Unix permissions (e.g., me...)

Comment thread Lib/test/test_zipapp.py
(source / '__main__.py').touch()
archive = self.tmpdir / 'source.pyz'
zipapp.create_archive(source, archive)
for umask, expected_mode in ((0o022, 0o755), (0o077, 0o700)):

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.

Same comment about octal constants here.

@bedevere-app

bedevere-app Bot commented Jul 30, 2026

Copy link
Copy Markdown

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

zipapp creates executable files with wrong permissions bits

2 participants