From 693e66f8179a96b7f6ac533ed83b10449e76c48f Mon Sep 17 00:00:00 2001 From: Wulian233 <1055917385@qq.com> Date: Mon, 30 Dec 2024 20:02:34 +0800 Subject: [PATCH 1/3] finish --- Doc/whatsnew/3.14.rst | 6 ++++ Lib/test/test_zipfile/test_core.py | 31 ++++++++++++++++++- Lib/zipfile/__init__.py | 6 +++- ...4-12-30-19-53-14.gh-issue-91279.EeOJk1.rst | 3 ++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-12-30-19-53-14.gh-issue-91279.EeOJk1.rst diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 53415bb09bf080..0a23178e93d0f0 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -670,6 +670,12 @@ zipinfo (Contributed by Bénédikt Tran in :gh:`123424`.) +* :meth:`zipfile.ZipFile.writestr` now respect ``SOURCE_DATE_EPOCH`` that + distributions can set centrally and have build tools consume this in order + to produce reproducible output. + + (Contributed by Jiahao Li in :gh:`91279`.) + .. Add improved modules above alphabetically, not here at the end. Optimizations diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index 49f39b9337df85..f202896fa837b4 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -19,7 +19,7 @@ from random import randint, random, randbytes from test import archiver_tests -from test.support import script_helper +from test.support import script_helper, os_helper from test.support import ( findfile, requires_zlib, requires_bz2, requires_lzma, captured_stdout, captured_stderr, requires_subprocess @@ -1781,6 +1781,35 @@ def test_writestr_extended_local_header_issue1202(self): zinfo.flag_bits |= zipfile._MASK_USE_DATA_DESCRIPTOR # Include an extended local header. orig_zip.writestr(zinfo, data) + def test_write_with_source_date_epoch(self): + with os_helper.EnvironmentVarGuard() as env: + # Set the SOURCE_DATE_EPOCH environment variable to a specific timestamp + env['SOURCE_DATE_EPOCH'] = "1727440508" + + with zipfile.ZipFile(TESTFN, "w") as zf: + zf.writestr("test_source_date_epoch.txt", "Testing SOURCE_DATE_EPOCH") + + with zipfile.ZipFile(TESTFN, "r") as zf: + zip_info = zf.getinfo("test_source_date_epoch.txt") + get_time = time.gmtime(int(os.environ['SOURCE_DATE_EPOCH']))[:6] + # Compare each element of the date_time tuple + # Allow for a 1-second difference + for z_time, g_time in zip(zip_info.date_time, get_time): + self.assertAlmostEqual(z_time, g_time, delta=1) + + def test_write_without_source_date_epoch(self): + if 'SOURCE_DATE_EPOCH' in os.environ: + del os.environ['SOURCE_DATE_EPOCH'] + + with zipfile.ZipFile(TESTFN, "w") as zf: + zf.writestr("test_no_source_date_epoch.txt", "Testing without SOURCE_DATE_EPOCH") + + with zipfile.ZipFile(TESTFN, "r") as zf: + zip_info = zf.getinfo("test_no_source_date_epoch.txt") + current_time = time.gmtime()[:6] + for z_time, c_time in zip(zip_info.date_time, current_time): + self.assertAlmostEqual(z_time, c_time, delta=1) + def test_close(self): """Check that the zipfile is closed after the 'with' block.""" with zipfile.ZipFile(TESTFN2, "w") as zipfp: diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 052ef47b8f6598..49e3199e66214e 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -614,7 +614,11 @@ def _for_archive(self, archive: ZipFile) -> Self: Return self. """ - self.date_time = time.localtime(time.time())[:6] + # gh-91279: Set the SOURCE_DATE_EPOCH to a specific timestamp + self.epoch = os.environ.get('SOURCE_DATE_EPOCH') + self.get_time = int(self.epoch) if self.epoch else time.time() + self.date_time = time.gmtime(self.get_time)[:6] + self.compress_type = archive.compression self.compress_level = archive.compresslevel if self.filename.endswith('/'): # pragma: no cover diff --git a/Misc/NEWS.d/next/Library/2024-12-30-19-53-14.gh-issue-91279.EeOJk1.rst b/Misc/NEWS.d/next/Library/2024-12-30-19-53-14.gh-issue-91279.EeOJk1.rst new file mode 100644 index 00000000000000..30ee2ea5efd069 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-12-30-19-53-14.gh-issue-91279.EeOJk1.rst @@ -0,0 +1,3 @@ +:meth:`zipfile.ZipFile.writestr` now respect ``SOURCE_DATE_EPOCH`` that +distributions can set centrally and have build tools consume this in order +to produce reproducible output. From d07b4adc28e1904af79fa1d24da10ef3171ef756 Mon Sep 17 00:00:00 2001 From: Wulian <1055917385@qq.com> Date: Mon, 30 Dec 2024 20:52:12 +0800 Subject: [PATCH 2/3] Update __init__.py --- Lib/zipfile/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 49e3199e66214e..44c55d626bb90b 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -615,9 +615,9 @@ def _for_archive(self, archive: ZipFile) -> Self: Return self. """ # gh-91279: Set the SOURCE_DATE_EPOCH to a specific timestamp - self.epoch = os.environ.get('SOURCE_DATE_EPOCH') - self.get_time = int(self.epoch) if self.epoch else time.time() - self.date_time = time.gmtime(self.get_time)[:6] + epoch = os.environ.get('SOURCE_DATE_EPOCH') + get_time = int(epoch) if epoch else time.time() + self.date_time = time.gmtime(get_time)[:6] self.compress_type = archive.compression self.compress_level = archive.compresslevel From 54a711689da3403c9f8b7e24249733ccf01930e2 Mon Sep 17 00:00:00 2001 From: Wulian233 <1055917385@qq.com> Date: Wed, 1 Jan 2025 15:23:44 +0800 Subject: [PATCH 3/3] using time.localtime --- Lib/test/test_zipfile/test_core.py | 6 +++--- Lib/zipfile/__init__.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index f202896fa837b4..73effdc23bf9f0 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -1784,14 +1784,14 @@ def test_writestr_extended_local_header_issue1202(self): def test_write_with_source_date_epoch(self): with os_helper.EnvironmentVarGuard() as env: # Set the SOURCE_DATE_EPOCH environment variable to a specific timestamp - env['SOURCE_DATE_EPOCH'] = "1727440508" + env['SOURCE_DATE_EPOCH'] = "1735715999" with zipfile.ZipFile(TESTFN, "w") as zf: zf.writestr("test_source_date_epoch.txt", "Testing SOURCE_DATE_EPOCH") with zipfile.ZipFile(TESTFN, "r") as zf: zip_info = zf.getinfo("test_source_date_epoch.txt") - get_time = time.gmtime(int(os.environ['SOURCE_DATE_EPOCH']))[:6] + get_time = time.localtime(int(os.environ['SOURCE_DATE_EPOCH']))[:6] # Compare each element of the date_time tuple # Allow for a 1-second difference for z_time, g_time in zip(zip_info.date_time, get_time): @@ -1806,7 +1806,7 @@ def test_write_without_source_date_epoch(self): with zipfile.ZipFile(TESTFN, "r") as zf: zip_info = zf.getinfo("test_no_source_date_epoch.txt") - current_time = time.gmtime()[:6] + current_time = time.localtime()[:6] for z_time, c_time in zip(zip_info.date_time, current_time): self.assertAlmostEqual(z_time, c_time, delta=1) diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 44c55d626bb90b..5515f164a4a2f2 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -617,7 +617,7 @@ def _for_archive(self, archive: ZipFile) -> Self: # gh-91279: Set the SOURCE_DATE_EPOCH to a specific timestamp epoch = os.environ.get('SOURCE_DATE_EPOCH') get_time = int(epoch) if epoch else time.time() - self.date_time = time.gmtime(get_time)[:6] + self.date_time = time.localtime(get_time)[:6] self.compress_type = archive.compression self.compress_level = archive.compresslevel