reproducibly 0.0.12__py3-none-any.whl → 0.0.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reproducibly-0.0.12.dist-info → reproducibly-0.0.15.dist-info}/METADATA +10 -7
- reproducibly-0.0.15.dist-info/RECORD +5 -0
- {reproducibly-0.0.12.dist-info → reproducibly-0.0.15.dist-info}/WHEEL +1 -1
- reproducibly.py +58 -36
- reproducibly-0.0.12.dist-info/RECORD +0 -5
- {reproducibly-0.0.12.dist-info → reproducibly-0.0.15.dist-info}/entry_points.txt +0 -0
@@ -1,16 +1,16 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: reproducibly
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.15
|
4
4
|
Summary: Reproducibly build Python packages
|
5
5
|
Author-email: Keith Maxwell <keith.maxwell@gmail.com>
|
6
|
-
Requires-Python: >=3.
|
6
|
+
Requires-Python: >=3.13
|
7
7
|
Description-Content-Type: text/markdown
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
9
|
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
10
|
-
Requires-Dist: build==1.2.
|
11
|
-
Requires-Dist: cibuildwheel==2.
|
12
|
-
Requires-Dist: packaging==24.
|
13
|
-
Requires-Dist: pyproject_hooks==1.
|
10
|
+
Requires-Dist: build==1.2.2.post1
|
11
|
+
Requires-Dist: cibuildwheel==2.23.2
|
12
|
+
Requires-Dist: packaging==24.2
|
13
|
+
Requires-Dist: pyproject_hooks==1.2.0
|
14
14
|
Project-URL: Homepage, https://github.com/maxwell-k/reproducibly/
|
15
15
|
Project-URL: Issues, https://github.com/maxwell-k/reproducibly/issues
|
16
16
|
|
@@ -54,6 +54,7 @@ features:
|
|
54
54
|
- Builds a source distribution (sdist) from a git repository
|
55
55
|
- Builds a wheel from a sdist
|
56
56
|
- Resets metadata like user and group names and ids to predictable values
|
57
|
+
- Uses no compression for predictable file hashes across Linux distributions
|
57
58
|
- By default uses the last commit date and time from git
|
58
59
|
- Respects SOURCE_DATE_EPOCH when building a sdist
|
59
60
|
- Single file script with inline script metadata or PyPI package
|
@@ -93,5 +94,7 @@ To run unit tests and integration tests:
|
|
93
94
|
README.md
|
94
95
|
Copyright 2023 Keith Maxwell
|
95
96
|
SPDX-License-Identifier: CC-BY-SA-4.0
|
97
|
+
|
98
|
+
vim: set filetype=markdown.dprint.cog.htmlCommentNoSpell :
|
96
99
|
-->
|
97
100
|
|
@@ -0,0 +1,5 @@
|
|
1
|
+
reproducibly.py,sha256=AgMNoWREhLsRyyXuA9BwMJSI99_i9B_ft4RwwilTobQ,13518
|
2
|
+
reproducibly-0.0.15.dist-info/METADATA,sha256=rOEfFbBwOe1F21wBsSSxht4xNYBMd2Hnf3AcO9wvBvI,2995
|
3
|
+
reproducibly-0.0.15.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
4
|
+
reproducibly-0.0.15.dist-info/entry_points.txt,sha256=J4fRzmY7XffHnoo9etzvgeph8np598MPpIy3jpnGlfk,50
|
5
|
+
reproducibly-0.0.15.dist-info/RECORD,,
|
reproducibly.py
CHANGED
@@ -5,6 +5,7 @@ features:
|
|
5
5
|
- Builds a source distribution (sdist) from a git repository
|
6
6
|
- Builds a wheel from a sdist
|
7
7
|
- Resets metadata like user and group names and ids to predictable values
|
8
|
+
- Uses no compression for predictable file hashes across Linux distributions
|
8
9
|
- By default uses the last commit date and time from git
|
9
10
|
- Respects SOURCE_DATE_EPOCH when building a sdist
|
10
11
|
- Single file script with inline script metadata or PyPI package
|
@@ -14,7 +15,6 @@ features:
|
|
14
15
|
# Copyright 2024 Keith Maxwell
|
15
16
|
# SPDX-License-Identifier: MPL-2.0
|
16
17
|
import gzip
|
17
|
-
import tarfile
|
18
18
|
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
19
19
|
from contextlib import chdir
|
20
20
|
from datetime import datetime
|
@@ -25,9 +25,10 @@ from shutil import copyfileobj, move
|
|
25
25
|
from stat import S_IWGRP, S_IWOTH
|
26
26
|
from subprocess import CalledProcessError, run
|
27
27
|
from sys import version_info
|
28
|
+
from tarfile import TarFile, TarInfo
|
28
29
|
from tempfile import TemporaryDirectory
|
29
30
|
from typing import cast, Literal, TypedDict
|
30
|
-
from zipfile import ZipFile, ZipInfo
|
31
|
+
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
31
32
|
|
32
33
|
from build import ProjectBuilder
|
33
34
|
from build.env import DefaultIsolatedEnv
|
@@ -51,12 +52,12 @@ from pyproject_hooks import default_subprocess_runner
|
|
51
52
|
# cog.outl("# ///")
|
52
53
|
# ]]]
|
53
54
|
# /// script
|
54
|
-
# requires-python = ">=3.
|
55
|
+
# requires-python = ">=3.13"
|
55
56
|
# dependencies = [
|
56
|
-
# "build==1.2.
|
57
|
-
# "cibuildwheel==2.
|
58
|
-
# "packaging==24.
|
59
|
-
# "pyproject_hooks==1.
|
57
|
+
# "build==1.2.2.post1",
|
58
|
+
# "cibuildwheel==2.23.2",
|
59
|
+
# "packaging==24.2",
|
60
|
+
# "pyproject_hooks==1.2.0",
|
60
61
|
# ]
|
61
62
|
# ///
|
62
63
|
# [[[end]]]
|
@@ -67,11 +68,11 @@ from pyproject_hooks import default_subprocess_runner
|
|
67
68
|
# - Built distributions are typically zip files
|
68
69
|
# - The default date for this script is the earliest date supported by both
|
69
70
|
# - The minimum date value supported by zip files, is documented in
|
70
|
-
# <https://github.com/python/cpython/blob/3.
|
71
|
+
# <https://github.com/python/cpython/blob/3.13/Lib/zipfile.py>.
|
71
72
|
EARLIEST = datetime(1980, 1, 1, 0, 0, 0).timestamp() # 315532800.0
|
72
73
|
|
73
74
|
|
74
|
-
__version__ = "0.0.
|
75
|
+
__version__ = "0.0.15"
|
75
76
|
|
76
77
|
|
77
78
|
def _build(
|
@@ -93,8 +94,8 @@ def _build(
|
|
93
94
|
|
94
95
|
|
95
96
|
def _extract_to_empty_directory(sdist: Path, directory: str) -> Path:
|
96
|
-
with
|
97
|
-
t.extractall(directory)
|
97
|
+
with TarFile.open(sdist) as t:
|
98
|
+
t.extractall(directory, filter="data")
|
98
99
|
return next(Path(directory).iterdir())
|
99
100
|
|
100
101
|
|
@@ -159,13 +160,13 @@ class Builder(Enum):
|
|
159
160
|
@nonmember
|
160
161
|
@staticmethod
|
161
162
|
def which(archive: Path) -> "Builder":
|
162
|
-
with
|
163
|
+
with TarFile.open(archive, "r:gz") as tar:
|
163
164
|
c = any(i.name.endswith(".c") for i in tar.getmembers())
|
164
165
|
return Builder.cibuildwheel if c else Builder.build
|
165
166
|
|
166
167
|
|
167
|
-
def
|
168
|
-
"""Cleanse
|
168
|
+
def cleanse_sdist(path_: Path, mtime: float) -> int:
|
169
|
+
"""Cleanse a single source distribution
|
169
170
|
|
170
171
|
- Set all uids and gids to zero
|
171
172
|
- Set all unames and gnames to root
|
@@ -173,22 +174,22 @@ def cleanse_metadata(path_: Path, mtime: float) -> int:
|
|
173
174
|
- Set modified time for .tar inside .gz
|
174
175
|
- Set modified time for files inside the .tar
|
175
176
|
- Remove group and other write permissions for files inside the .tar
|
177
|
+
- Set the compression level to zero i.e. no compression
|
176
178
|
"""
|
177
|
-
|
179
|
+
filename = path_.absolute()
|
178
180
|
|
179
181
|
mtime = max(mtime, EARLIEST)
|
180
182
|
|
181
|
-
with TemporaryDirectory() as
|
182
|
-
with
|
183
|
-
tar.extractall(path=
|
183
|
+
with TemporaryDirectory() as path:
|
184
|
+
with TarFile.open(filename) as tar:
|
185
|
+
tar.extractall(path=path, filter="data")
|
184
186
|
|
185
|
-
|
186
|
-
(extracted,) = Path(
|
187
|
-
uncompressed = f"{extracted}.tar"
|
187
|
+
filename.unlink(missing_ok=True)
|
188
|
+
(extracted,) = Path(path).iterdir()
|
188
189
|
|
189
|
-
prefix =
|
190
|
+
prefix = path.removeprefix("/") + "/"
|
190
191
|
|
191
|
-
def filter_(tarinfo:
|
192
|
+
def filter_(tarinfo: TarInfo) -> TarInfo:
|
192
193
|
tarinfo.mtime = int(mtime)
|
193
194
|
tarinfo.uid = tarinfo.gid = 0
|
194
195
|
tarinfo.uname = tarinfo.gname = "root"
|
@@ -196,19 +197,25 @@ def cleanse_metadata(path_: Path, mtime: float) -> int:
|
|
196
197
|
tarinfo.path = tarinfo.path.removeprefix(prefix)
|
197
198
|
return tarinfo
|
198
199
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
200
|
+
tar = f"{extracted}.tar"
|
201
|
+
with TarFile.open(tar, "w") as tarfile:
|
202
|
+
tarfile.add(extracted, filter=filter_)
|
203
|
+
|
204
|
+
with gzip.GzipFile(
|
205
|
+
filename=filename,
|
206
|
+
mode="wb",
|
207
|
+
mtime=mtime,
|
208
|
+
compresslevel=0,
|
209
|
+
) as file:
|
210
|
+
with open(tar, "rb") as tar:
|
204
211
|
copyfileobj(tar, file)
|
205
|
-
utime(
|
212
|
+
utime(filename, (mtime, mtime))
|
206
213
|
return 0
|
207
214
|
|
208
215
|
|
209
216
|
def latest_modification_time(archive: Path) -> str:
|
210
217
|
"""Latest modification time for a gzipped tarfile as a string"""
|
211
|
-
with
|
218
|
+
with TarFile.open(archive, "r:gz") as tar:
|
212
219
|
latest = max(member.mtime for member in tar.getmembers())
|
213
220
|
return "{:.0f}".format(latest)
|
214
221
|
|
@@ -244,11 +251,24 @@ def key(input_: bytes | ZipInfo) -> tuple[int, list[str | list]]:
|
|
244
251
|
return (group, breadth_first_key(item))
|
245
252
|
|
246
253
|
|
247
|
-
def
|
248
|
-
"""Apply
|
254
|
+
def fix_zip_members(path: Path, umask: int = 0o022) -> Path:
|
255
|
+
"""Apply fixes to members in a zip file
|
256
|
+
|
257
|
+
Processes the zip file in place. Path is both the source and destination, a
|
258
|
+
temporary working copy is made.
|
259
|
+
|
260
|
+
- Apply a umask to each member
|
261
|
+
- Change to compression level zero
|
249
262
|
|
250
|
-
|
251
|
-
|
263
|
+
When using the default deflate compression and comparing wheels created on
|
264
|
+
Ubuntu 24.04 and Fedora 40, minor differences in the size of the compressed
|
265
|
+
wheel were observed. For example:
|
266
|
+
|
267
|
+
│ -112 files, 909030 bytes uncompressed, 272160 bytes compressed: 70.1%
|
268
|
+
│ +112 files, 909030 bytes uncompressed, 271653 bytes compressed: 70.1%
|
269
|
+
|
270
|
+
As a solution this function uses compression level zero i.e. no compression.
|
271
|
+
"""
|
252
272
|
operand = ~(umask << 16)
|
253
273
|
|
254
274
|
with TemporaryDirectory() as directory:
|
@@ -257,6 +277,8 @@ def zipumask(path: Path, umask: int = 0o022) -> Path:
|
|
257
277
|
for member in original.infolist():
|
258
278
|
data = original.read(member)
|
259
279
|
member.external_attr = member.external_attr & operand
|
280
|
+
member.compress_type = ZIP_DEFLATED
|
281
|
+
member.compress_level = 0
|
260
282
|
destination.writestr(member, data)
|
261
283
|
path.unlink()
|
262
284
|
move(copy, path) # can't rename as /tmp may be a different device
|
@@ -354,7 +376,7 @@ def main(arguments: list[str] | None = None) -> int:
|
|
354
376
|
date = float(environ["SOURCE_DATE_EPOCH"])
|
355
377
|
else:
|
356
378
|
date = latest_commit_time(repository)
|
357
|
-
|
379
|
+
cleanse_sdist(sdist, date)
|
358
380
|
for sdist in parsed["sdists"]:
|
359
381
|
with ModifiedEnvironment(SOURCE_DATE_EPOCH=latest_modification_time(sdist)):
|
360
382
|
if Builder.which(sdist) == Builder.cibuildwheel:
|
@@ -363,7 +385,7 @@ def main(arguments: list[str] | None = None) -> int:
|
|
363
385
|
with TemporaryDirectory() as directory:
|
364
386
|
srcdir = _extract_to_empty_directory(sdist, directory)
|
365
387
|
built = _build(srcdir, parsed["output"], "wheel")
|
366
|
-
_sortwheel(
|
388
|
+
fix_zip_members(_sortwheel(built))
|
367
389
|
return 0
|
368
390
|
|
369
391
|
|
@@ -1,5 +0,0 @@
|
|
1
|
-
reproducibly.py,sha256=tz7vajj6XVutFugNhGm2oTsvxjSHPBlh4iArABMuCmU,12627
|
2
|
-
reproducibly-0.0.12.dist-info/METADATA,sha256=EriIkwvWD-e0r7lJmaXynD89B-A7L6RWW1KyteJFJoo,2852
|
3
|
-
reproducibly-0.0.12.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
4
|
-
reproducibly-0.0.12.dist-info/entry_points.txt,sha256=J4fRzmY7XffHnoo9etzvgeph8np598MPpIy3jpnGlfk,50
|
5
|
-
reproducibly-0.0.12.dist-info/RECORD,,
|
File without changes
|