diff --git a/setup.cfg b/setup.cfg index 1d718540..c6e4e7aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,6 +34,7 @@ package_dir= packages = find: python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* setup_requires = setuptools >= 40.9.0 +install_requires = packaging >= 20.2 zip_safe = False [options.packages.find] diff --git a/src/wheel/bdist_wheel.py b/src/wheel/bdist_wheel.py index 5bc92650..bc29a06f 100644 --- a/src/wheel/bdist_wheel.py +++ b/src/wheel/bdist_wheel.py @@ -4,28 +4,29 @@ A wheel is a built archive format. """ +import distutils import os import shutil import stat import sys import re +import warnings from collections import OrderedDict from email.generator import Generator from distutils.core import Command -from distutils.sysconfig import get_python_version +from distutils.sysconfig import get_config_var from distutils import log as logger from glob import iglob from shutil import rmtree -from warnings import warn from zipfile import ZIP_DEFLATED, ZIP_STORED +from packaging import tags import pkg_resources -from .pep425tags import get_abbr_impl, get_impl_ver, get_abi_tag, get_platform from .pkginfo import write_pkg_info +from .macosx_libfile import calculate_macosx_platform_tag from .metadata import pkginfo_to_metadata from .wheelfile import WheelFile -from . import pep425tags from . import __version__ as wheel_version @@ -35,6 +36,70 @@ PY_LIMITED_API_PATTERN = r'cp3\d' +def python_tag(): + return 'py{}'.format(sys.version_info[0]) + + +def get_platform(archive_root): + """Return our platform name 'win32', 'linux_x86_64'""" + # XXX remove distutils dependency + result = distutils.util.get_platform() + if result.startswith("macosx") and archive_root is not None: + result = calculate_macosx_platform_tag(archive_root, result) + if result == "linux_x86_64" and sys.maxsize == 2147483647: + # pip pull request #3497 + result = "linux_i686" + return result + + +def get_flag(var, fallback, expected=True, warn=True): + """Use a fallback value 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 = tags.interpreter_name() + if not soabi and impl in ('cp', 'pp') and hasattr(sys, 'maxunicode'): + d = '' + m = '' + u = '' + if get_flag('Py_DEBUG', + hasattr(sys, 'gettotalrefcount'), + warn=(impl == 'cp')): + d = 'd' + if get_flag('WITH_PYMALLOC', + impl == 'cp', + warn=(impl == 'cp' and + sys.version_info < (3, 8))) \ + and sys.version_info < (3, 8): + m = 'm' + if get_flag('Py_UNICODE_SIZE', + 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, tags.interpreter_version(), 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 safer_name(name): return safe_name(name).replace('-', '_') @@ -88,7 +153,7 @@ class bdist_wheel(Command): .format(', '.join(supported_compressions))), ('python-tag=', None, "Python implementation compatibility tag" - " (default: py%s)" % get_impl_ver()[0]), + " (default: '%s')" % (python_tag())), ('build-number=', None, "Build number for this particular version. " "As specified in PEP-0427, this must start with a digit. " @@ -116,7 +181,7 @@ def initialize_options(self): self.group = None self.universal = False self.compression = 'deflated' - self.python_tag = 'py' + get_impl_ver()[0] + self.python_tag = python_tag() self.build_number = None self.py_limited_api = False self.plat_name_supplied = False @@ -178,6 +243,12 @@ def get_tag(self): if self.plat_name and not self.plat_name.startswith("macosx"): plat_name = self.plat_name else: + # on macosx always limit the platform name to comply with any + # c-extension modules in bdist_dir, since the user can specify + # a higher MACOSX_DEPLOYMENT_TARGET via tools like CMake + + # on other platforms, and on macosx if there are no c-extension + # modules, use the default platform name. plat_name = get_platform(self.bdist_dir) if plat_name in ('linux-x86_64', 'linux_x86_64') and sys.maxsize == 2147483647: @@ -192,8 +263,8 @@ def get_tag(self): impl = self.python_tag tag = (impl, 'none', plat_name) else: - impl_name = get_abbr_impl() - impl_ver = get_impl_ver() + impl_name = tags.interpreter_name() + impl_ver = tags.interpreter_version() impl = impl_name + impl_ver # We don't work on CPython 3.1, 3.0. if self.py_limited_api and (impl_name + impl_ver).startswith('cp3'): @@ -202,12 +273,8 @@ def get_tag(self): else: abi_tag = str(get_abi_tag()).lower() tag = (impl, abi_tag, plat_name) - supported_tags = pep425tags.get_supported( - self.bdist_dir, - supplied_platform=plat_name if self.plat_name_supplied else None) - # XXX switch to this alternate implementation for non-pure: - if not self.py_limited_api: - assert tag == supported_tags[0], "%s != %s" % (tag, supported_tags[0]) + supported_tags = [(t.interpreter, t.abi, t.platform) + for t in tags.sys_tags()] assert tag in supported_tags, "would build wheel with unsupported tag {}".format(tag) return tag @@ -286,7 +353,9 @@ def run(self): # Add to 'Distribution.dist_files' so that the "upload" command works getattr(self.distribution, 'dist_files', []).append( - ('bdist_wheel', get_python_version(), wheel_path)) + ('bdist_wheel', + '{}.{}'.format(*sys.version_info[:2]), # like 3.7 + wheel_path)) if not self.keep_temp: logger.info('removing %s', self.bdist_dir) @@ -330,8 +399,8 @@ def license_paths(self): }) if 'license_file' in metadata: - warn('The "license_file" option is deprecated. Use "license_files" instead.', - DeprecationWarning) + warnings.warn('The "license_file" option is deprecated. Use ' + '"license_files" instead.', DeprecationWarning) files.add(metadata['license_file'][1]) if 'license_file' not in metadata and 'license_files' not in metadata: diff --git a/src/wheel/macosx_libfile.py b/src/wheel/macosx_libfile.py index a9036886..9141f269 100644 --- a/src/wheel/macosx_libfile.py +++ b/src/wheel/macosx_libfile.py @@ -36,6 +36,7 @@ """ import ctypes +import os import sys """here the needed const and struct from mach-o header files""" @@ -339,3 +340,71 @@ def parse_version(version): y = (version & 0x0000ff00) >> 8 z = (version & 0x000000ff) return x, y, z + + +def calculate_macosx_platform_tag(archive_root, platform_tag): + """ + Calculate proper macosx platform tag basing on files which are included to wheel + + Example platform tag `macosx-10.14-x86_64` + """ + prefix, base_version, suffix = platform_tag.split('-') + base_version = tuple([int(x) for x in base_version.split(".")]) + if len(base_version) >= 2: + base_version = base_version[0:2] + + assert len(base_version) == 2 + if "MACOSX_DEPLOYMENT_TARGET" in os.environ: + deploy_target = tuple([int(x) for x in os.environ[ + "MACOSX_DEPLOYMENT_TARGET"].split(".")]) + if len(deploy_target) >= 2: + deploy_target = deploy_target[0:2] + if deploy_target < base_version: + sys.stderr.write( + "[WARNING] MACOSX_DEPLOYMENT_TARGET is set to a lower value ({}) than the " + "version on which the Python interpreter was compiled ({}), and will be " + "ignored.\n".format('.'.join(str(x) for x in deploy_target), + '.'.join(str(x) for x in base_version)) + ) + else: + base_version = deploy_target + + assert len(base_version) == 2 + start_version = base_version + versions_dict = {} + for (dirpath, dirnames, filenames) in os.walk(archive_root): + for filename in filenames: + if filename.endswith('.dylib') or filename.endswith('.so'): + lib_path = os.path.join(dirpath, filename) + min_ver = extract_macosx_min_system_version(lib_path) + if min_ver is not None: + versions_dict[lib_path] = min_ver[0:2] + + if len(versions_dict) > 0: + base_version = max(base_version, max(versions_dict.values())) + + # macosx platform tag do not support minor bugfix release + fin_base_version = "_".join([str(x) for x in base_version]) + if start_version < base_version: + problematic_files = [k for k, v in versions_dict.items() if v > start_version] + problematic_files = "\n".join(problematic_files) + if len(problematic_files) == 1: + files_form = "this file" + else: + files_form = "these files" + error_message = \ + "[WARNING] This wheel needs a higher macOS version than {} " \ + "To silence this warning, set MACOSX_DEPLOYMENT_TARGET to at least " +\ + fin_base_version + " or recreate " + files_form + " with lower " \ + "MACOSX_DEPLOYMENT_TARGET: \n" + problematic_files + + if "MACOSX_DEPLOYMENT_TARGET" in os.environ: + error_message = error_message.format("is set in MACOSX_DEPLOYMENT_TARGET variable.") + else: + error_message = error_message.format( + "the version your Python interpreter is compiled against.") + + sys.stderr.write(error_message) + + platform_tag = prefix + "_" + fin_base_version + "_" + suffix + return platform_tag diff --git a/src/wheel/pep425tags.py b/src/wheel/pep425tags.py deleted file mode 100644 index 0c257636..00000000 --- a/src/wheel/pep425tags.py +++ /dev/null @@ -1,261 +0,0 @@ -"""Generate and work with PEP 425 Compatibility Tags.""" - -import distutils.util -import platform -import sys -import os -import sysconfig -import warnings - -from .macosx_libfile import extract_macosx_min_system_version - - -try: - from importlib.machinery import all_suffixes as get_all_suffixes -except ImportError: - from imp import get_suffixes - - def get_all_suffixes(): - return [suffix[0] for suffix in get_suffixes()] - - -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.""" - impl = platform.python_implementation() - if impl == 'PyPy': - return 'pp' - elif impl == 'Jython': - return 'jy' - elif impl == 'IronPython': - return 'ip' - elif impl == 'CPython': - return 'cp' - - raise LookupError('Unknown Python implementation: ' + impl) - - -def get_impl_ver(): - """Return implementation version.""" - impl_ver = get_config_var("py_version_nodot") - if not impl_ver: - 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.""" - 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' and - sys.version_info < (3, 8))) \ - and sys.version_info < (3, 8): - 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 calculate_macosx_platform_tag(archive_root, platform_tag): - """ - Calculate proper macosx platform tag basing on files which are included to wheel - - Example platform tag `macosx-10.14-x86_64` - """ - prefix, base_version, suffix = platform_tag.split('-') - base_version = tuple([int(x) for x in base_version.split(".")]) - if len(base_version) >= 2: - base_version = base_version[0:2] - - assert len(base_version) == 2 - if "MACOSX_DEPLOYMENT_TARGET" in os.environ: - deploy_target = tuple([int(x) for x in os.environ[ - "MACOSX_DEPLOYMENT_TARGET"].split(".")]) - if len(deploy_target) >= 2: - deploy_target = deploy_target[0:2] - if deploy_target < base_version: - sys.stderr.write( - "[WARNING] MACOSX_DEPLOYMENT_TARGET is set to a lower value ({}) than the " - "version on which the Python interpreter was compiled ({}), and will be " - "ignored.\n".format('.'.join(str(x) for x in deploy_target), - '.'.join(str(x) for x in base_version)) - ) - else: - base_version = deploy_target - - assert len(base_version) == 2 - start_version = base_version - versions_dict = {} - for (dirpath, dirnames, filenames) in os.walk(archive_root): - for filename in filenames: - if filename.endswith('.dylib') or filename.endswith('.so'): - lib_path = os.path.join(dirpath, filename) - min_ver = extract_macosx_min_system_version(lib_path) - if min_ver is not None: - versions_dict[lib_path] = min_ver[0:2] - - if len(versions_dict) > 0: - base_version = max(base_version, max(versions_dict.values())) - - # macosx platform tag do not support minor bugfix release - fin_base_version = "_".join([str(x) for x in base_version]) - if start_version < base_version: - problematic_files = [k for k, v in versions_dict.items() if v > start_version] - problematic_files = "\n".join(problematic_files) - if len(problematic_files) == 1: - files_form = "this file" - else: - files_form = "these files" - error_message = \ - "[WARNING] This wheel needs a higher macOS version than {} " \ - "To silence this warning, set MACOSX_DEPLOYMENT_TARGET to at least " +\ - fin_base_version + " or recreate " + files_form + " with lower " \ - "MACOSX_DEPLOYMENT_TARGET: \n" + problematic_files - - if "MACOSX_DEPLOYMENT_TARGET" in os.environ: - error_message = error_message.format("is set in MACOSX_DEPLOYMENT_TARGET variable.") - else: - error_message = error_message.format( - "the version your Python interpreter is compiled against.") - - sys.stderr.write(error_message) - - platform_tag = prefix + "_" + fin_base_version + "_" + suffix - return platform_tag - - -def get_platform(archive_root): - """Return our platform name 'win32', 'linux_x86_64'""" - # XXX remove distutils dependency - result = distutils.util.get_platform() - if result.startswith("macosx") and archive_root is not None: - result = calculate_macosx_platform_tag(archive_root, result) - result = result.replace('.', '_').replace('-', '_') - if result == "linux_x86_64" and sys.maxsize == 2147483647: - # pip pull request #3497 - result = "linux_i686" - - return result - - -def get_supported(archive_root, versions=None, supplied_platform=None): - """Return a list of supported tags for each version specified in - `versions`. - - :param versions: a list of string versions, of the form ["33", "32"], - or None. The first version will be assumed to support our ABI. - """ - supported = [] - - # Versions must be given with respect to the preference - if versions is None: - versions = [] - version_info = get_impl_version_info() - major = version_info[:-1] - # Support all previous minor Python versions. - for minor in range(version_info[-1], -1, -1): - versions.append(''.join(map(str, major + (minor,)))) - - impl = get_abbr_impl() - - abis = [] - - abi = get_abi_tag() - if abi: - abis[0:0] = [abi] - - abi3s = set() - for suffix in get_all_suffixes(): - if suffix.startswith('.abi'): - abi3s.add(suffix.split('.', 2)[1]) - - abis.extend(sorted(list(abi3s))) - - abis.append('none') - - platforms = [] - if supplied_platform: - platforms.append(supplied_platform) - platforms.append(get_platform(archive_root)) - - # Current version, current API (built specifically for our Python): - for abi in abis: - for arch in platforms: - supported.append(('%s%s' % (impl, versions[0]), abi, arch)) - - # abi3 modules compatible with older version of Python - for version in versions[1:]: - # abi3 was introduced in Python 3.2 - if version in ('31', '30'): - break - for abi in abi3s: # empty set if not Python 3 - for arch in platforms: - supported.append(("%s%s" % (impl, version), abi, arch)) - - # No abi / arch, but requires our implementation: - for i, version in enumerate(versions): - supported.append(('%s%s' % (impl, version), 'none', 'any')) - if i == 0: - # Tagged specifically as being cross-version compatible - # (with just the major version specified) - supported.append(('%s%s' % (impl, versions[0][0]), 'none', 'any')) - - # Major Python version + platform; e.g. binaries not using the Python API - for arch in platforms: - supported.append(('py%s' % (versions[0][0]), 'none', arch)) - - # No abi / arch, generic Python - for i, version in enumerate(versions): - supported.append(('py%s' % (version,), 'none', 'any')) - if i == 0: - supported.append(('py%s' % (version[0]), 'none', 'any')) - - return supported diff --git a/tests/test_macosx_libfile.py b/tests/test_macosx_libfile.py index 7bd696d8..35cff83b 100644 --- a/tests/test_macosx_libfile.py +++ b/tests/test_macosx_libfile.py @@ -3,7 +3,7 @@ import distutils.util from wheel.macosx_libfile import extract_macosx_min_system_version -from wheel.pep425tags import get_platform +from wheel.bdist_wheel import get_platform def test_read_from_dylib():