summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNate Coraor <nate@bx.psu.edu>2016-02-02 17:54:55 -0500
committerNate Coraor <nate@bx.psu.edu>2016-02-02 17:54:55 -0500
commitfacb3a9f80af9e8f13eb121ff27d20a8c5f8b57a (patch)
tree6891a52b3e6e0b321ff1d5aa8741a44e3b0645f8
parente46f1f3eeee4b260fb3aa9f14c438da30d40426f (diff)
parent93de2423f9ce7ad926767f2cb9459bcb9f883e87 (diff)
downloadwheel-facb3a9f80af9e8f13eb121ff27d20a8c5f8b57a.tar.gz
Merged in ajdiaz/wheel (pull request #60)
Fix bdist_wheel to accept --plat-tag
-rw-r--r--README.txt9
-rw-r--r--wheel/archive.py26
-rw-r--r--wheel/bdist_wheel.py24
-rw-r--r--wheel/metadata.py9
-rw-r--r--wheel/pep425tags.py87
-rw-r--r--wheel/test/test_wheelfile.py71
6 files changed, 195 insertions, 31 deletions
diff --git a/README.txt b/README.txt
index 4b14821..7b37ad9 100644
--- a/README.txt
+++ b/README.txt
@@ -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