Skip to content

Commit 6324646

Browse files
authored
Maintain file permissions (#1397)
1 parent 8648544 commit 6324646

File tree

4 files changed

+73
-20
lines changed

4 files changed

+73
-20
lines changed

backend/src/hatchling/builders/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def normalize_file_permissions(st_mode: int) -> int:
112112

113113
def set_zip_info_mode(zip_info: ZipInfo, mode: int = 0o644) -> None:
114114
"""
115+
https://github.com/python/cpython/blob/v3.12.3/Lib/zipfile/__init__.py#L574
115116
https://github.com/takluyver/flit/commit/3889583719888aef9f28baaa010e698cb7884904
116117
"""
117-
zip_info.external_attr = mode << 16
118+
zip_info.external_attr = (mode & 0xFFFF) << 16

backend/src/hatchling/builders/wheel.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def add_file(self, included_file: IncludedFile) -> tuple[str, str, str]:
9898

9999
# https://github.com/takluyver/flit/pull/66
100100
new_mode = normalize_file_permissions(file_stat.st_mode)
101-
set_zip_info_mode(zip_info, new_mode & 0xFFFF)
101+
set_zip_info_mode(zip_info, new_mode)
102102
if stat.S_ISDIR(file_stat.st_mode): # no cov
103103
zip_info.external_attr |= 0x10
104104
else:
@@ -123,9 +123,19 @@ def write_metadata(self, relative_path: str, contents: str | bytes) -> tuple[str
123123
relative_path = f'{self.metadata_directory}/{normalize_archive_path(relative_path)}'
124124
return self.write_file(relative_path, contents)
125125

126-
def write_shared_script(self, relative_path: str, contents: str | bytes) -> tuple[str, str, str]:
127-
relative_path = f'{self.shared_data_directory}/scripts/{normalize_archive_path(relative_path)}'
128-
return self.write_file(relative_path, contents)
126+
def write_shared_script(self, included_file: IncludedFile, contents: str | bytes) -> tuple[str, str, str]:
127+
relative_path = (
128+
f'{self.shared_data_directory}/scripts/{normalize_archive_path(included_file.distribution_path)}'
129+
)
130+
if sys.platform == 'win32':
131+
return self.write_file(relative_path, contents)
132+
133+
file_stat = os.stat(included_file.path)
134+
return self.write_file(
135+
relative_path,
136+
contents,
137+
mode=normalize_file_permissions(file_stat.st_mode) if self.reproducible else file_stat.st_mode,
138+
)
129139

130140
def add_shared_file(self, shared_file: IncludedFile) -> tuple[str, str, str]:
131141
shared_file.distribution_path = f'{self.shared_data_directory}/data/{shared_file.distribution_path}'
@@ -137,13 +147,22 @@ def add_extra_metadata_file(self, extra_metadata_file: IncludedFile) -> tuple[st
137147
)
138148
return self.add_file(extra_metadata_file)
139149

140-
def write_file(self, relative_path: str, contents: str | bytes) -> tuple[str, str, str]:
150+
def write_file(
151+
self,
152+
relative_path: str,
153+
contents: str | bytes,
154+
*,
155+
mode: int | None = None,
156+
) -> tuple[str, str, str]:
141157
if not isinstance(contents, bytes):
142158
contents = contents.encode('utf-8')
143159

144160
time_tuple = self.time_tuple or (2020, 2, 2, 0, 0, 0)
145161
zip_info = zipfile.ZipInfo(relative_path, time_tuple)
146-
set_zip_info_mode(zip_info)
162+
if mode is None:
163+
set_zip_info_mode(zip_info)
164+
else:
165+
set_zip_info_mode(zip_info, mode)
147166

148167
hash_obj = hashlib.sha256(contents)
149168
hash_digest = format_file_hash(hash_obj.digest())
@@ -628,7 +647,7 @@ def add_shared_scripts(self, archive: WheelArchive, records: RecordFile, build_d
628647
content.write(f.read())
629648
break
630649

631-
record = archive.write_shared_script(shared_script.distribution_path, content.getvalue())
650+
record = archive.write_shared_script(shared_script, content.getvalue())
632651
records.write(record)
633652

634653
def write_metadata(

docs/history/hatchling.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88

99
## Unreleased
1010

11+
***Fixed:***
12+
13+
- Maintain file permissions for `shared-scripts` option/`shared_scripts` build data of the `wheel` target
14+
1115
## [1.24.0](https://github.com/pypa/hatch/releases/tag/hatchling-v1.24.0) - 2024-04-16 ## {: #hatchling-v1.24.0 }
1216

1317
***Added:***

tests/backend/builders/test_wheel.py

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
from __future__ import annotations
2+
13
import os
24
import platform
35
import sys
46
import zipfile
7+
from typing import TYPE_CHECKING
58

69
import pytest
710
from packaging.tags import sys_tags
@@ -12,6 +15,9 @@
1215
from hatchling.metadata.spec import DEFAULT_METADATA_VERSION, get_core_metadata_constructors
1316
from hatchling.utils.constants import DEFAULT_BUILD_SCRIPT
1417

18+
if TYPE_CHECKING:
19+
from hatch.utils.fs import Path
20+
1521
# https://github.com/python/cpython/pull/26184
1622
fixed_pathlib_resolution = pytest.mark.skipif(
1723
sys.platform == 'win32' and (sys.version_info < (3, 8) or sys.implementation.name == 'pypy'),
@@ -23,6 +29,17 @@ def get_python_versions_tag():
2329
return '.'.join(f'py{major_version}' for major_version in get_known_python_major_versions())
2430

2531

32+
def extract_zip(zip_path: Path, target: Path) -> None:
33+
with zipfile.ZipFile(zip_path, 'r') as z:
34+
for name in z.namelist():
35+
member = z.getinfo(name)
36+
path = z.extract(member, target)
37+
if member.is_dir():
38+
os.chmod(path, 0o755)
39+
else:
40+
os.chmod(path, member.external_attr >> 16)
41+
42+
2643
def test_class():
2744
assert issubclass(WheelBuilder, BuilderInterface)
2845

@@ -1996,7 +2013,7 @@ def initialize(self, version, build_data):
19962013
)
19972014
helpers.assert_files(extraction_directory, expected_files)
19982015

1999-
def test_default_shared_scripts(self, hatch, helpers, temp_dir, config_file):
2016+
def test_default_shared_scripts(self, hatch, platform, helpers, temp_dir, config_file):
20002017
config_file.model.template.plugins['default']['src-layout'] = False
20012018
config_file.save()
20022019

@@ -2013,7 +2030,12 @@ def test_default_shared_scripts(self, hatch, helpers, temp_dir, config_file):
20132030
shared_data_path.ensure_dir_exists()
20142031

20152032
binary_contents = os.urandom(1024)
2016-
(shared_data_path / 'binary').write_bytes(binary_contents)
2033+
binary_file = shared_data_path / 'binary'
2034+
binary_file.write_bytes(binary_contents)
2035+
if not platform.windows:
2036+
expected_mode = 0o755
2037+
binary_file.chmod(expected_mode)
2038+
20172039
(shared_data_path / 'other_script.sh').write_text(
20182040
helpers.dedent(
20192041
"""
@@ -2085,10 +2107,7 @@ def test_default_shared_scripts(self, hatch, helpers, temp_dir, config_file):
20852107
assert expected_artifact == str(build_artifacts[0])
20862108

20872109
extraction_directory = temp_dir / '_archive'
2088-
extraction_directory.mkdir()
2089-
2090-
with zipfile.ZipFile(str(expected_artifact), 'r') as zip_archive:
2091-
zip_archive.extractall(str(extraction_directory))
2110+
extract_zip(expected_artifact, extraction_directory)
20922111

20932112
metadata_directory = f'{builder.project_id}.dist-info'
20942113
shared_data_directory = f'{builder.project_id}.data'
@@ -2101,7 +2120,11 @@ def test_default_shared_scripts(self, hatch, helpers, temp_dir, config_file):
21012120
)
21022121
helpers.assert_files(extraction_directory, expected_files)
21032122

2104-
def test_default_shared_scripts_from_build_data(self, hatch, helpers, temp_dir, config_file):
2123+
if not platform.windows:
2124+
extracted_binary = extraction_directory / shared_data_directory / 'scripts' / 'binary'
2125+
assert extracted_binary.stat().st_mode & 0o777 == expected_mode
2126+
2127+
def test_default_shared_scripts_from_build_data(self, hatch, platform, helpers, temp_dir, config_file):
21052128
config_file.model.template.plugins['default']['src-layout'] = False
21062129
config_file.save()
21072130

@@ -2118,7 +2141,12 @@ def test_default_shared_scripts_from_build_data(self, hatch, helpers, temp_dir,
21182141
shared_data_path.ensure_dir_exists()
21192142

21202143
binary_contents = os.urandom(1024)
2121-
(shared_data_path / 'binary').write_bytes(binary_contents)
2144+
binary_file = shared_data_path / 'binary'
2145+
binary_file.write_bytes(binary_contents)
2146+
if not platform.windows:
2147+
expected_mode = 0o755
2148+
binary_file.chmod(expected_mode)
2149+
21222150
(shared_data_path / 'other_script.sh').write_text(
21232151
helpers.dedent(
21242152
"""
@@ -2205,10 +2233,7 @@ def initialize(self, version, build_data):
22052233
assert expected_artifact == str(build_artifacts[0])
22062234

22072235
extraction_directory = temp_dir / '_archive'
2208-
extraction_directory.mkdir()
2209-
2210-
with zipfile.ZipFile(str(expected_artifact), 'r') as zip_archive:
2211-
zip_archive.extractall(str(extraction_directory))
2236+
extract_zip(expected_artifact, extraction_directory)
22122237

22132238
metadata_directory = f'{builder.project_id}.dist-info'
22142239
shared_data_directory = f'{builder.project_id}.data'
@@ -2221,6 +2246,10 @@ def initialize(self, version, build_data):
22212246
)
22222247
helpers.assert_files(extraction_directory, expected_files)
22232248

2249+
if not platform.windows:
2250+
extracted_binary = extraction_directory / shared_data_directory / 'scripts' / 'binary'
2251+
assert extracted_binary.stat().st_mode & 0o777 == expected_mode
2252+
22242253
def test_default_extra_metadata(self, hatch, helpers, temp_dir, config_file):
22252254
config_file.model.template.plugins['default']['src-layout'] = False
22262255
config_file.save()

0 commit comments

Comments
 (0)