Skip to content

Commit 6b1cff8

Browse files
committed
ENH: make sdist archives reproducible
1 parent 251c323 commit 6b1cff8

File tree

3 files changed

+39
-3
lines changed

3 files changed

+39
-3
lines changed

mesonpy/__init__.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import tarfile
3232
import tempfile
3333
import textwrap
34-
import time
3534
import typing
3635
import warnings
3736

@@ -872,6 +871,7 @@ def sdist(self, directory: Path) -> pathlib.Path:
872871
meson_dist_name = f'{self._meson_name}-{self._meson_version}'
873872
meson_dist_path = pathlib.Path(self._build_dir, 'meson-dist', f'{meson_dist_name}.tar.gz')
874873
sdist_path = pathlib.Path(directory, f'{dist_name}.tar.gz')
874+
pyproject_toml_mtime = 0
875875

876876
with tarfile.open(meson_dist_path, 'r:gz') as meson_dist, mesonpy._util.create_targz(sdist_path) as sdist:
877877
for member in meson_dist.getmembers():
@@ -898,6 +898,9 @@ def sdist(self, directory: Path) -> pathlib.Path:
898898
stem = member.name.split('/', 1)[1]
899899
member.name = '/'.join((dist_name, stem))
900900

901+
if stem == 'pyproject.toml':
902+
pyproject_toml_mtime = member.mtime
903+
901904
# Reset owner and group to root:root. This mimics what
902905
# 'git archive' does and makes the sdist reproducible upon
903906
# being built by different users.
@@ -910,7 +913,23 @@ def sdist(self, directory: Path) -> pathlib.Path:
910913
member = tarfile.TarInfo(f'{dist_name}/PKG-INFO')
911914
member.uid = member.gid = 0
912915
member.uname = member.gname = 'root'
913-
member.mtime = time.time()
916+
917+
# Set the 'PKG-INFO' modification time to the modification time of
918+
# 'pyproject.toml' in the archive generated by 'meson dist'. In
919+
# turn this is the last commit time, unless touched by a dist
920+
# script. This makes the sdist reproducible upon being built at
921+
# different times, when dist scripts are not used, which should be
922+
# the majority of cases.
923+
#
924+
# Note that support for dynamic version in project metadata allows
925+
# the version to depend on the build time. Therefore, setting the
926+
# 'PKG-INFO' modification time to the 'pyproject.toml'
927+
# modification time can be seen as not strictly correct. However,
928+
# the sdist standard does not dictate which modification time to
929+
# use for 'PKG-INFO'. This choice allows to make the sdist
930+
# byte-for-byte reproducible in the most common case.
931+
member.mtime = pyproject_toml_mtime
932+
914933
metadata = bytes(self._metadata.as_rfc822())
915934
member.size = len(metadata)
916935
sdist.addfile(member, io.BytesIO(metadata))

mesonpy/_util.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ def create_targz(path: Path) -> Iterator[tarfile.TarFile]:
3737
os.makedirs(os.path.dirname(path), exist_ok=True)
3838
file = typing.cast(IO[bytes], gzip.GzipFile(
3939
path,
40-
mode='wb',
40+
mode='w',
41+
# Set the stream last modification time to 0. This mimics
42+
# what 'git archive' does and makes the archives byte-for-byte
43+
# reproducible.
44+
mtime=0,
4145
))
4246
tar = tarfile.TarFile(
4347
mode='w',

tests/test_sdist.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sys
1010
import tarfile
1111
import textwrap
12+
import time
1213

1314
import pytest
1415

@@ -205,3 +206,15 @@ def test_long_path(sdist_long_path):
205206
'long_path-1.0.0/meson.build',
206207
'long_path-1.0.0/pyproject.toml'
207208
}
209+
210+
211+
def test_reproducible(package_pure, tmp_path):
212+
t1 = time.time()
213+
sdist_path_a = mesonpy.build_sdist(tmp_path / 'a')
214+
t2 = time.time()
215+
# Ensure that the two sdists are build at least one second apart.
216+
time.sleep(max(t1 + 1.0 - t2, 0.0))
217+
sdist_path_b = mesonpy.build_sdist(tmp_path / 'b')
218+
219+
assert sdist_path_a == sdist_path_b
220+
assert tmp_path.joinpath('a', sdist_path_a).read_bytes() == tmp_path.joinpath('b', sdist_path_b).read_bytes()

0 commit comments

Comments
 (0)