diff options
author | Nate Coraor <nate@bx.psu.edu> | 2016-02-02 17:54:55 -0500 |
---|---|---|
committer | Nate Coraor <nate@bx.psu.edu> | 2016-02-02 17:54:55 -0500 |
commit | facb3a9f80af9e8f13eb121ff27d20a8c5f8b57a (patch) | |
tree | 6891a52b3e6e0b321ff1d5aa8741a44e3b0645f8 | |
parent | e46f1f3eeee4b260fb3aa9f14c438da30d40426f (diff) | |
parent | 93de2423f9ce7ad926767f2cb9459bcb9f883e87 (diff) | |
download | wheel-facb3a9f80af9e8f13eb121ff27d20a8c5f8b57a.tar.gz |
Merged in ajdiaz/wheel (pull request #60)
Fix bdist_wheel to accept --plat-tag
-rw-r--r-- | README.txt | 9 | ||||
-rw-r--r-- | wheel/archive.py | 26 | ||||
-rw-r--r-- | wheel/bdist_wheel.py | 24 | ||||
-rw-r--r-- | wheel/metadata.py | 9 | ||||
-rw-r--r-- | wheel/pep425tags.py | 87 | ||||
-rw-r--r-- | wheel/test/test_wheelfile.py | 71 |
6 files changed, 195 insertions, 31 deletions
@@ -39,3 +39,12 @@ Unlike .egg, wheel will be a fully-documented standard at the binary level that is truly easy to install even if you do not want to use the reference implementation. + +Code of Conduct +--------------- + +Everyone interacting in the wheel project's codebases, issue trackers, chat +rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. + +.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ + diff --git a/wheel/archive.py b/wheel/archive.py index 225d295..f4dd617 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('SOURCE_DATE_EPOCH') + if timestamp is None: + date_time = None + else: + date_time = time.gmtime(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,15 @@ 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.gmtime(st.st_mtime) + date_time = mtime[0:6] + zinfo = zipfile.ZipInfo(path, date_time) + zinfo.external_attr = 0o100644 << 16 + 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 +68,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 6f25948..e858649 100644 --- a/wheel/bdist_wheel.py +++ b/wheel/bdist_wheel.py @@ -33,7 +33,7 @@ from distutils.sysconfig import get_python_version from distutils import log as logger -from .pep425tags import get_abbr_impl, get_impl_ver +from .pep425tags import get_abbr_impl, get_impl_ver, get_abi_tag from .util import native, open_for_csv from .archive import archive_wheelfile from .pkginfo import read_pkg_info, write_pkg_info @@ -154,14 +154,8 @@ class bdist_wheel(Command): plat_name = plat_name.replace('-', '_').replace('.', '_') impl_name = get_abbr_impl() impl_ver = get_impl_ver() - # PEP 3149 -- no SOABI in Py 2 - # For PyPy? - # "pp%s%s" % (sys.pypy_version_info.major, - # sys.pypy_version_info.minor) - abi_tag = sysconfig.get_config_vars().get('SOABI', 'none') - if abi_tag.startswith('cpython-'): - abi_tag = 'cp' + abi_tag.split('-')[1] - + # PEP 3149 + abi_tag = str(get_abi_tag()).lower() tag = (impl_name + impl_ver, abi_tag, plat_name) # XXX switch to this alternate implementation for non-pure: assert tag == supported_tags[0] @@ -211,7 +205,7 @@ class bdist_wheel(Command): if os.name == 'nt': # win32 barfs if any of these are ''; could be '.'? # (distutils.command.install:change_roots bug) - basedir_observed = os.path.join(self.data_dir, '..') + basedir_observed = os.path.normpath(os.path.join(self.data_dir, '..')) self.install_libbase = self.install_lib = basedir_observed setattr(install, @@ -389,9 +383,11 @@ class bdist_wheel(Command): 'not-zip-safe',))) # delete dependency_links if it is only whitespace - dependency_links = os.path.join(distinfo_path, 'dependency_links.txt') - if not open(dependency_links, 'r').read().strip(): - adios(dependency_links) + dependency_links_path = os.path.join(distinfo_path, 'dependency_links.txt') + with open(dependency_links_path, 'r') as dependency_links_file: + dependency_links = dependency_links_file.read().strip() + if not dependency_links: + adios(dependency_links_path) write_pkg_info(os.path.join(distinfo_path, 'METADATA'), pkg_info) @@ -421,7 +417,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/pep425tags.py b/wheel/pep425tags.py index 2cf2230..2fe8510 100644 --- a/wheel/pep425tags.py +++ b/wheel/pep425tags.py @@ -1,6 +1,7 @@ """Generate and work with PEP 425 Compatibility Tags.""" import sys +import warnings try: import sysconfig @@ -10,6 +11,14 @@ except ImportError: # pragma nocover import distutils.util +def get_config_var(var): + try: + return sysconfig.get_config_var(var) + except IOError as e: # pip Issue #1074 + warnings.warn("{0}".format(e), RuntimeWarning) + return None + + def get_abbr_impl(): """Return abbreviated implementation name.""" if hasattr(sys, 'pypy_version_info'): @@ -25,12 +34,69 @@ def get_abbr_impl(): def get_impl_ver(): """Return implementation version.""" - impl_ver = sysconfig.get_config_var("py_version_nodot") - if not impl_ver: - impl_ver = ''.join(map(str, sys.version_info[:2])) + impl_ver = get_config_var("py_version_nodot") + if not impl_ver or get_abbr_impl() == 'pp': + impl_ver = ''.join(map(str, get_impl_version_info())) return impl_ver +def get_impl_version_info(): + """Return sys.version_info-like tuple for use in decrementing the minor + version.""" + if get_abbr_impl() == 'pp': + # as per https://github.com/pypa/pip/issues/2882 + return (sys.version_info[0], sys.pypy_version_info.major, + sys.pypy_version_info.minor) + else: + return sys.version_info[0], sys.version_info[1] + + +def get_flag(var, fallback, expected=True, warn=True): + """Use a fallback method for determining SOABI flags if the needed config + var is unset or unavailable.""" + val = get_config_var(var) + if val is None: + if warn: + warnings.warn("Config variable '{0}' is unset, Python ABI tag may " + "be incorrect".format(var), RuntimeWarning, 2) + return fallback() + return val == expected + + +def get_abi_tag(): + """Return the ABI tag based on SOABI (if available) or emulate SOABI + (CPython 2, PyPy).""" + soabi = get_config_var('SOABI') + impl = get_abbr_impl() + if not soabi and impl in ('cp', 'pp') and hasattr(sys, 'maxunicode'): + d = '' + m = '' + u = '' + if get_flag('Py_DEBUG', + lambda: hasattr(sys, 'gettotalrefcount'), + warn=(impl == 'cp')): + d = 'd' + if get_flag('WITH_PYMALLOC', + lambda: impl == 'cp', + warn=(impl == 'cp')): + m = 'm' + if get_flag('Py_UNICODE_SIZE', + lambda: sys.maxunicode == 0x10ffff, + expected=4, + warn=(impl == 'cp' and + sys.version_info < (3, 3))) \ + and sys.version_info < (3, 3): + u = 'u' + abi = '%s%s%s%s%s' % (impl, get_impl_ver(), d, m, u) + elif soabi and soabi.startswith('cpython-'): + abi = 'cp' + soabi.split('-')[1] + elif soabi: + abi = soabi.replace('.', '_').replace('-', '_') + else: + abi = None + return abi + + def get_platform(): """Return our platform name 'win32', 'linux_x86_64'""" # XXX remove distutils dependency @@ -49,18 +115,19 @@ def get_supported(versions=None): # Versions must be given with respect to the preference if versions is None: versions = [] - major = sys.version_info[0] + version_info = get_impl_version_info() + major = version_info[:-1] # Support all previous minor Python versions. - for minor in range(sys.version_info[1], -1, -1): - versions.append(''.join(map(str, (major, minor)))) + for minor in range(version_info[-1], -1, -1): + versions.append(''.join(map(str, major + (minor,)))) impl = get_abbr_impl() abis = [] - soabi = sysconfig.get_config_var('SOABI') - if soabi and soabi.startswith('cpython-'): - abis[0:0] = ['cp' + soabi.split('-')[1]] + abi = get_abi_tag() + if abi: + abis[0:0] = [abi] abi3s = set() import imp @@ -96,5 +163,3 @@ def get_supported(versions=None): supported.append(('py%s' % (version[0]), 'none', 'any')) return supported - - diff --git a/wheel/test/test_wheelfile.py b/wheel/test/test_wheelfile.py index e362ceb..59bbb4c 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,36 @@ 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('SOURCE_DATE_EPOCH', '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) + +def test_zipfile_attributes(): + # With the change from ZipFile.write() to .writestr(), we need to manually + # set member attributes. Per existing tradition file permissions are forced + # to 0o644, although in the future we may want to preserve executable bits. + with temporary_directory() as tempdir: + path = os.path.join(tempdir, 'foo') + with codecs.open(path, 'w', encoding='utf-8') as fp: + fp.write('foo\n') + zip_base_name = os.path.join(tempdir, 'dummy') + 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.external_attr == 0o100644 << 16 |