Repository: takluyver/flit
Pull request #66 (2016-01-17 10:12:52)

Normalise permission bits to 755 or 644
Apparently, the only permission bit git tracks is executable or not. So this flattens the permission bits to 755 for executable files, or 644 for non-executable, matching the default umask on Debian systems.

I still want to think about this:
- Does wheel installation preserve any more permission bits? If not,
  this is definitely fine.
- Do other version control systems store more permissions?
- Is there a reason you might want other permissions on files installed
  from wheels, e.g. to make a file not world readable? It seems redundant
  when packages come from a public index, but maybe for privately
  installed packages it makes sense.



Patch:
diff --git a/flit/wheel.py b/flit/wheel.py
index 16c044b..f6d0194 100644
--- a/flit/wheel.py
+++ b/flit/wheel.py
@@ -7,6 +7,7 @@ import io
 import logging
 import os
 import re
+import stat
 import sys
 import tempfile
 from types import SimpleNamespace
@@ -89,6 +90,17 @@ class WheelBuilder:
         else:
             # Set timestamps in zipfile for reproducible build
             zinfo = zipfile.ZipInfo(rel_path, self.source_time_stamp)
+        
+        # Normalize permission bits to either 755 (executable) or 644
+        st_mode = os.stat(full_path).st_mode
+        new_mode = (st_mode | 0o644) & ~0o133  # 644 permissions, higher bits unchanged
+        if st_mode & 0o100:
+            # If executable, 644 -> 755
+            new_mode |= 0o111
+        zinfo.external_attr = (new_mode & 0xFFFF) << 16      # Unix attributes
+
+        if stat.S_ISDIR(st_mode):
+            zinfo.external_attr |= 0x10  # MS-DOS directory flag
 
         hashsum = hashlib.sha256()
         with open(full_path, 'rb') as src, self.wheel_zip.open(zinfo, 'w') as dst:


Test patch:
diff --git a/tests/test_wheel.py b/tests/test_wheel.py
index 250c803..3505cdf 100644
--- a/tests/test_wheel.py
+++ b/tests/test_wheel.py
@@ -1,7 +1,9 @@
 import configparser
+import os
 from pathlib import Path
 import shutil
 import tempfile
+from unittest import skipIf
 import zipfile
 
 import pytest
@@ -65,3 +67,30 @@ def test_wheel_builder():
 
         assert zipfile.is_zipfile(str(target))
         assert wb.wheel_filename == 'package1-0.1-py2.py3-none-any.whl'
+
+@skipIf(os.name == 'nt', 'Windows does not preserve necessary permissions')
+def test_permissions_normed():
+    with tempfile.TemporaryDirectory() as td:
+        shutil.copy(str(samples_dir / 'module1.py'), td)
+        shutil.copy(str(samples_dir / 'module1-pkg.ini'), td)
+
+        Path(td, 'module1.py').chmod(0o620)
+        wheel_main(Path(td, 'module1-pkg.ini'))
+
+        whl = Path(td, 'dist', 'module1-0.1-py2.py3-none-any.whl')
+        assert_isfile(whl)
+        with zipfile.ZipFile(str(whl)) as zf:
+            info = zf.getinfo('module1.py')
+            perms = (info.external_attr >> 16) & 0o777
+            assert perms == 0o644, oct(perms)
+        whl.unlink()
+
+        # This time with executable bit set
+        Path(td, 'module1.py').chmod(0o720)
+        wheel_main(Path(td, 'module1-pkg.ini'))
+
+        assert_isfile(whl)
+        with zipfile.ZipFile(str(whl)) as zf:
+            info = zf.getinfo('module1.py')
+            perms = (info.external_attr >> 16) & 0o777
+            assert perms == 0o755, oct(perms)