reproducibly 0.0.11__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.11.dist-info → reproducibly-0.0.15.dist-info}/METADATA +10 -7
- reproducibly-0.0.15.dist-info/RECORD +5 -0
- {reproducibly-0.0.11.dist-info → reproducibly-0.0.15.dist-info}/WHEEL +1 -1
- reproducibly.py +59 -60
- reproducibly-0.0.11.dist-info/RECORD +0 -5
- {reproducibly-0.0.11.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,15 +25,15 @@ 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
|
34
35
|
from cibuildwheel.__main__ import build_in_directory
|
35
36
|
from cibuildwheel.options import CommandLineArguments
|
36
|
-
from packaging.requirements import Requirement
|
37
37
|
from pyproject_hooks import default_subprocess_runner
|
38
38
|
|
39
39
|
# [[[cog import cog ; from pathlib import Path ]]]
|
@@ -52,12 +52,12 @@ from pyproject_hooks import default_subprocess_runner
|
|
52
52
|
# cog.outl("# ///")
|
53
53
|
# ]]]
|
54
54
|
# /// script
|
55
|
-
# requires-python = ">=3.
|
55
|
+
# requires-python = ">=3.13"
|
56
56
|
# dependencies = [
|
57
|
-
# "build==1.2.
|
58
|
-
# "cibuildwheel==2.
|
59
|
-
# "packaging==24.
|
60
|
-
# "pyproject_hooks==1.
|
57
|
+
# "build==1.2.2.post1",
|
58
|
+
# "cibuildwheel==2.23.2",
|
59
|
+
# "packaging==24.2",
|
60
|
+
# "pyproject_hooks==1.2.0",
|
61
61
|
# ]
|
62
62
|
# ///
|
63
63
|
# [[[end]]]
|
@@ -68,20 +68,11 @@ from pyproject_hooks import default_subprocess_runner
|
|
68
68
|
# - Built distributions are typically zip files
|
69
69
|
# - The default date for this script is the earliest date supported by both
|
70
70
|
# - The minimum date value supported by zip files, is documented in
|
71
|
-
# <https://github.com/python/cpython/blob/3.
|
71
|
+
# <https://github.com/python/cpython/blob/3.13/Lib/zipfile.py>.
|
72
72
|
EARLIEST = datetime(1980, 1, 1, 0, 0, 0).timestamp() # 315532800.0
|
73
73
|
|
74
74
|
|
75
|
-
|
76
|
-
# [[[cog
|
77
|
-
# for line in Path("constraints.txt").read_text().splitlines():
|
78
|
-
# cog.outl(f'"{line}",')
|
79
|
-
# ]]]
|
80
|
-
"wheel==0.44.0",
|
81
|
-
# [[[end]]]
|
82
|
-
}
|
83
|
-
|
84
|
-
__version__ = "0.0.11"
|
75
|
+
__version__ = "0.0.15"
|
85
76
|
|
86
77
|
|
87
78
|
def _build(
|
@@ -96,15 +87,15 @@ def _build(
|
|
96
87
|
srcdir,
|
97
88
|
runner=default_subprocess_runner,
|
98
89
|
)
|
99
|
-
env.install(
|
100
|
-
env.install(
|
90
|
+
env.install(builder.build_system_requires)
|
91
|
+
env.install(builder.get_requires_for_build(distribution))
|
101
92
|
built = builder.build(distribution, output)
|
102
93
|
return output / built
|
103
94
|
|
104
95
|
|
105
96
|
def _extract_to_empty_directory(sdist: Path, directory: str) -> Path:
|
106
|
-
with
|
107
|
-
t.extractall(directory)
|
97
|
+
with TarFile.open(sdist) as t:
|
98
|
+
t.extractall(directory, filter="data")
|
108
99
|
return next(Path(directory).iterdir())
|
109
100
|
|
110
101
|
|
@@ -112,14 +103,12 @@ def _cibuildwheel(sdist: Path, output: Path) -> Path:
|
|
112
103
|
"""Call the cibuildwheel API
|
113
104
|
|
114
105
|
Returns the path to the built distribution"""
|
115
|
-
filename = Path("constraints.txt")
|
116
106
|
with (
|
117
107
|
ModifiedEnvironment(
|
118
|
-
CIBW_DEPENDENCY_VERSIONS=str(filename),
|
119
108
|
CIBW_BUILD_FRONTEND="build",
|
120
109
|
CIBW_CONTAINER_ENGINE="podman",
|
121
110
|
CIBW_ENVIRONMENT_PASS_LINUX="SOURCE_DATE_EPOCH",
|
122
|
-
CIBW_ENVIRONMENT=
|
111
|
+
CIBW_ENVIRONMENT="PIP_TIMEOUT=150",
|
123
112
|
),
|
124
113
|
TemporaryDirectory() as directory,
|
125
114
|
):
|
@@ -129,7 +118,6 @@ def _cibuildwheel(sdist: Path, output: Path) -> Path:
|
|
129
118
|
args.output_dir = Path(directory).resolve()
|
130
119
|
args.platform = None
|
131
120
|
with chdir(directory): # output maybe a relative path
|
132
|
-
filename.write_text("\n".join(CONSTRAINTS) + "\n")
|
133
121
|
build_in_directory(args)
|
134
122
|
wheel = next(args.output_dir.glob("*.whl"))
|
135
123
|
output.joinpath(wheel.name).unlink(missing_ok=True)
|
@@ -172,13 +160,13 @@ class Builder(Enum):
|
|
172
160
|
@nonmember
|
173
161
|
@staticmethod
|
174
162
|
def which(archive: Path) -> "Builder":
|
175
|
-
with
|
163
|
+
with TarFile.open(archive, "r:gz") as tar:
|
176
164
|
c = any(i.name.endswith(".c") for i in tar.getmembers())
|
177
165
|
return Builder.cibuildwheel if c else Builder.build
|
178
166
|
|
179
167
|
|
180
|
-
def
|
181
|
-
"""Cleanse
|
168
|
+
def cleanse_sdist(path_: Path, mtime: float) -> int:
|
169
|
+
"""Cleanse a single source distribution
|
182
170
|
|
183
171
|
- Set all uids and gids to zero
|
184
172
|
- Set all unames and gnames to root
|
@@ -186,22 +174,22 @@ def cleanse_metadata(path_: Path, mtime: float) -> int:
|
|
186
174
|
- Set modified time for .tar inside .gz
|
187
175
|
- Set modified time for files inside the .tar
|
188
176
|
- Remove group and other write permissions for files inside the .tar
|
177
|
+
- Set the compression level to zero i.e. no compression
|
189
178
|
"""
|
190
|
-
|
179
|
+
filename = path_.absolute()
|
191
180
|
|
192
181
|
mtime = max(mtime, EARLIEST)
|
193
182
|
|
194
|
-
with TemporaryDirectory() as
|
195
|
-
with
|
196
|
-
tar.extractall(path=
|
183
|
+
with TemporaryDirectory() as path:
|
184
|
+
with TarFile.open(filename) as tar:
|
185
|
+
tar.extractall(path=path, filter="data")
|
197
186
|
|
198
|
-
|
199
|
-
(extracted,) = Path(
|
200
|
-
uncompressed = f"{extracted}.tar"
|
187
|
+
filename.unlink(missing_ok=True)
|
188
|
+
(extracted,) = Path(path).iterdir()
|
201
189
|
|
202
|
-
prefix =
|
190
|
+
prefix = path.removeprefix("/") + "/"
|
203
191
|
|
204
|
-
def filter_(tarinfo:
|
192
|
+
def filter_(tarinfo: TarInfo) -> TarInfo:
|
205
193
|
tarinfo.mtime = int(mtime)
|
206
194
|
tarinfo.uid = tarinfo.gid = 0
|
207
195
|
tarinfo.uname = tarinfo.gname = "root"
|
@@ -209,19 +197,25 @@ def cleanse_metadata(path_: Path, mtime: float) -> int:
|
|
209
197
|
tarinfo.path = tarinfo.path.removeprefix(prefix)
|
210
198
|
return tarinfo
|
211
199
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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:
|
217
211
|
copyfileobj(tar, file)
|
218
|
-
utime(
|
212
|
+
utime(filename, (mtime, mtime))
|
219
213
|
return 0
|
220
214
|
|
221
215
|
|
222
216
|
def latest_modification_time(archive: Path) -> str:
|
223
217
|
"""Latest modification time for a gzipped tarfile as a string"""
|
224
|
-
with
|
218
|
+
with TarFile.open(archive, "r:gz") as tar:
|
225
219
|
latest = max(member.mtime for member in tar.getmembers())
|
226
220
|
return "{:.0f}".format(latest)
|
227
221
|
|
@@ -257,21 +251,24 @@ def key(input_: bytes | ZipInfo) -> tuple[int, list[str | list]]:
|
|
257
251
|
return (group, breadth_first_key(item))
|
258
252
|
|
259
253
|
|
260
|
-
def
|
261
|
-
"""
|
262
|
-
after = set()
|
263
|
-
for replacement in constraints:
|
264
|
-
name = Requirement(replacement).name
|
265
|
-
for i in before:
|
266
|
-
after.add(replacement if Requirement(i).name == name else i)
|
267
|
-
return after
|
254
|
+
def fix_zip_members(path: Path, umask: int = 0o022) -> Path:
|
255
|
+
"""Apply fixes to members in a zip file
|
268
256
|
|
257
|
+
Processes the zip file in place. Path is both the source and destination, a
|
258
|
+
temporary working copy is made.
|
269
259
|
|
270
|
-
|
271
|
-
|
260
|
+
- Apply a umask to each member
|
261
|
+
- Change to compression level zero
|
272
262
|
|
273
|
-
|
274
|
-
|
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
|
+
"""
|
275
272
|
operand = ~(umask << 16)
|
276
273
|
|
277
274
|
with TemporaryDirectory() as directory:
|
@@ -280,6 +277,8 @@ def zipumask(path: Path, umask: int = 0o022) -> Path:
|
|
280
277
|
for member in original.infolist():
|
281
278
|
data = original.read(member)
|
282
279
|
member.external_attr = member.external_attr & operand
|
280
|
+
member.compress_type = ZIP_DEFLATED
|
281
|
+
member.compress_level = 0
|
283
282
|
destination.writestr(member, data)
|
284
283
|
path.unlink()
|
285
284
|
move(copy, path) # can't rename as /tmp may be a different device
|
@@ -377,7 +376,7 @@ def main(arguments: list[str] | None = None) -> int:
|
|
377
376
|
date = float(environ["SOURCE_DATE_EPOCH"])
|
378
377
|
else:
|
379
378
|
date = latest_commit_time(repository)
|
380
|
-
|
379
|
+
cleanse_sdist(sdist, date)
|
381
380
|
for sdist in parsed["sdists"]:
|
382
381
|
with ModifiedEnvironment(SOURCE_DATE_EPOCH=latest_modification_time(sdist)):
|
383
382
|
if Builder.which(sdist) == Builder.cibuildwheel:
|
@@ -386,7 +385,7 @@ def main(arguments: list[str] | None = None) -> int:
|
|
386
385
|
with TemporaryDirectory() as directory:
|
387
386
|
srcdir = _extract_to_empty_directory(sdist, directory)
|
388
387
|
built = _build(srcdir, parsed["output"], "wheel")
|
389
|
-
_sortwheel(
|
388
|
+
fix_zip_members(_sortwheel(built))
|
390
389
|
return 0
|
391
390
|
|
392
391
|
|
@@ -1,5 +0,0 @@
|
|
1
|
-
reproducibly.py,sha256=0HzRBIu4mk68d9cl3IB3tXsjOLfKySOtckG43wF4DHU,13407
|
2
|
-
reproducibly-0.0.11.dist-info/METADATA,sha256=TsvqUUQUMC_nEy4lZr48izwic1aV2ckp061JN8NP9Io,2852
|
3
|
-
reproducibly-0.0.11.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
4
|
-
reproducibly-0.0.11.dist-info/entry_points.txt,sha256=J4fRzmY7XffHnoo9etzvgeph8np598MPpIy3jpnGlfk,50
|
5
|
-
reproducibly-0.0.11.dist-info/RECORD,,
|
File without changes
|