diff options
author | Nate Coraor <nate@bx.psu.edu> | 2016-02-01 14:54:05 -0500 |
---|---|---|
committer | Nate Coraor <nate@bx.psu.edu> | 2016-02-01 14:54:05 -0500 |
commit | 145abb4ac83643011ab60988635d63d2616d932e (patch) | |
tree | 8f896d1fa275b807a9a05549134f0eb839182146 | |
parent | 044cedcce403c36ddc7644a32151f38252918dff (diff) | |
parent | 554df34d1afd8727a7405544801810e25cff5f6e (diff) | |
download | wheel-145abb4ac83643011ab60988635d63d2616d932e.tar.gz |
Merged in warsaw/wheel/issue143 (pull request #52)
Apply the Debian patch for reproducible wheel files, but instead of hardcoding
-rw-r--r-- | wheel/archive.py | 25 | ||||
-rw-r--r-- | wheel/bdist_wheel.py | 2 | ||||
-rw-r--r-- | wheel/metadata.py | 9 | ||||
-rw-r--r-- | wheel/test/test_wheelfile.py | 56 |
4 files changed, 85 insertions, 7 deletions
diff --git a/wheel/archive.py b/wheel/archive.py index 225d295..42ffee6 100644 --- a/wheel/archive.py +++ b/wheel/archive.py @@ -2,6 +2,8 @@ Archive tools for wheel. """ +import os +import time import logging import os.path import zipfile @@ -31,6 +33,15 @@ def make_wheelfile_inner(base_name, base_dir='.'): log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir) + # Some applications need reproducible .whl files, but they can't do this + # without forcing the timestamp of the individual ZipInfo objects. See + # issue #143. + timestamp = os.environ.get('WHEEL_FORCE_TIMESTAMP') + if timestamp is None: + date_time = None + else: + date_time = time.localtime(int(timestamp))[0:6] + # XXX support bz2, xz when available zip = zipfile.ZipFile(open(zip_filename, "wb+"), "w", compression=zipfile.ZIP_DEFLATED) @@ -38,8 +49,14 @@ def make_wheelfile_inner(base_name, base_dir='.'): score = {'WHEEL': 1, 'METADATA': 2, 'RECORD': 3} deferred = [] - def writefile(path): - zip.write(path, path) + def writefile(path, date_time): + if date_time is None: + st = os.stat(path) + mtime = time.localtime(st.st_mtime) + date_time = mtime[0:6] + zinfo = zipfile.ZipInfo(path, date_time) + with open(path, 'rb') as fp: + zip.writestr(zinfo, fp.read()) log.info("adding '%s'" % path) for dirpath, dirnames, filenames in os.walk(base_dir): @@ -50,11 +67,11 @@ def make_wheelfile_inner(base_name, base_dir='.'): if dirpath.endswith('.dist-info'): deferred.append((score.get(name, 0), path)) else: - writefile(path) + writefile(path, date_time) deferred.sort() for score, path in deferred: - writefile(path) + writefile(path, date_time) zip.close() diff --git a/wheel/bdist_wheel.py b/wheel/bdist_wheel.py index b8abaa0..2cb545d 100644 --- a/wheel/bdist_wheel.py +++ b/wheel/bdist_wheel.py @@ -404,7 +404,7 @@ class bdist_wheel(Command): pymeta['extensions']['python.details']['document_names']['license'] = license_filename with open(metadata_json_path, "w") as metadata_json: - json.dump(pymeta, metadata_json) + json.dump(pymeta, metadata_json, sort_keys=True) adios(egginfo_path) diff --git a/wheel/metadata.py b/wheel/metadata.py index 8756fde..b3cc65c 100644 --- a/wheel/metadata.py +++ b/wheel/metadata.py @@ -74,7 +74,14 @@ def handle_requires(metadata, pkg_info, key): if may_requires: metadata['run_requires'] = [] - for key, value in may_requires.items(): + def sort_key(item): + # Both condition and extra could be None, which can't be compared + # against strings in Python 3. + key, value = item + if key.condition is None: + return '' + return key.condition + for key, value in sorted(may_requires.items(), key=sort_key): may_requirement = OrderedDict((('requires', value),)) if key.extra: may_requirement['extra'] = key.extra diff --git a/wheel/test/test_wheelfile.py b/wheel/test/test_wheelfile.py index e362ceb..561f578 100644 --- a/wheel/test/test_wheelfile.py +++ b/wheel/test/test_wheelfile.py @@ -1,11 +1,48 @@ +import os import wheel.install +import wheel.archive import hashlib try: from StringIO import StringIO except ImportError: from io import BytesIO as StringIO +import codecs import zipfile import pytest +import shutil +import tempfile +from contextlib import contextmanager + +@contextmanager +def environ(key, value): + old_value = os.environ.get(key) + try: + os.environ[key] = value + yield + finally: + if old_value is None: + del os.environ[key] + else: + os.environ[key] = old_value + +@contextmanager +def temporary_directory(): + # tempfile.TemporaryDirectory doesn't exist in Python 2. + tempdir = tempfile.mkdtemp() + try: + yield tempdir + finally: + shutil.rmtree(tempdir) + +@contextmanager +def readable_zipfile(path): + # zipfile.ZipFile() isn't a context manager under Python 2. + zf = zipfile.ZipFile(path, 'r') + try: + yield zf + finally: + zf.close() + def test_verifying_zipfile(): if not hasattr(zipfile.ZipExtFile, '_update_crc'): @@ -66,4 +103,21 @@ def test_pop_zipfile(): zf = wheel.install.VerifyingZipFile(sio, 'r') assert len(zf.infolist()) == 1 -
\ No newline at end of file + +def test_zipfile_timestamp(): + # An environment variable can be used to influence the timestamp on + # TarInfo objects inside the zip. See issue #143. TemporaryDirectory is + # not a context manager under Python 3. + with temporary_directory() as tempdir: + for filename in ('one', 'two', 'three'): + path = os.path.join(tempdir, filename) + with codecs.open(path, 'w', encoding='utf-8') as fp: + fp.write(filename + '\n') + zip_base_name = os.path.join(tempdir, 'dummy') + # The earliest date representable in TarInfos, 1980-01-01 + with environ('WHEEL_FORCE_TIMESTAMP', '315576060'): + zip_filename = wheel.archive.make_wheelfile_inner( + zip_base_name, tempdir) + with readable_zipfile(zip_filename) as zf: + for info in zf.infolist(): + assert info.date_time[:3] == (1980, 1, 1) |