368 lines
14 KiB
Python
368 lines
14 KiB
Python
"""
|
|
Create a wheel (.whl) distribution.
|
|
|
|
A wheel is a built archive format.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import re
|
|
from email.generator import Generator
|
|
from distutils.core import Command
|
|
from distutils.sysconfig import get_python_version
|
|
from distutils import log as logger
|
|
from glob import iglob
|
|
from shutil import rmtree
|
|
from warnings import warn
|
|
|
|
import pkg_resources
|
|
|
|
from .pep425tags import get_abbr_impl, get_impl_ver, get_abi_tag, get_platform
|
|
from .pkginfo import write_pkg_info
|
|
from .metadata import pkginfo_to_metadata
|
|
from .wheelfile import WheelFile
|
|
from . import pep425tags
|
|
from . import __version__ as wheel_version
|
|
|
|
|
|
safe_name = pkg_resources.safe_name
|
|
safe_version = pkg_resources.safe_version
|
|
|
|
PY_LIMITED_API_PATTERN = r'cp3\d'
|
|
|
|
|
|
def safer_name(name):
|
|
return safe_name(name).replace('-', '_')
|
|
|
|
|
|
def safer_version(version):
|
|
return safe_version(version).replace('-', '_')
|
|
|
|
|
|
class bdist_wheel(Command):
|
|
|
|
description = 'create a wheel distribution'
|
|
|
|
user_options = [('bdist-dir=', 'b',
|
|
"temporary directory for creating the distribution"),
|
|
('plat-name=', 'p',
|
|
"platform name to embed in generated filenames "
|
|
"(default: %s)" % get_platform()),
|
|
('keep-temp', 'k',
|
|
"keep the pseudo-installation tree around after " +
|
|
"creating the distribution archive"),
|
|
('dist-dir=', 'd',
|
|
"directory to put final built distributions in"),
|
|
('skip-build', None,
|
|
"skip rebuilding everything (for testing/debugging)"),
|
|
('relative', None,
|
|
"build the archive using relative paths"
|
|
"(default: false)"),
|
|
('owner=', 'u',
|
|
"Owner name used when creating a tar file"
|
|
" [default: current user]"),
|
|
('group=', 'g',
|
|
"Group name used when creating a tar file"
|
|
" [default: current group]"),
|
|
('universal', None,
|
|
"make a universal wheel"
|
|
" (default: false)"),
|
|
('python-tag=', None,
|
|
"Python implementation compatibility tag"
|
|
" (default: py%s)" % get_impl_ver()[0]),
|
|
('build-number=', None,
|
|
"Build number for this particular version. "
|
|
"As specified in PEP-0427, this must start with a digit. "
|
|
"[default: None]"),
|
|
('py-limited-api=', None,
|
|
"Python tag (cp32|cp33|cpNN) for abi3 wheel tag"
|
|
" (default: false)"),
|
|
]
|
|
|
|
boolean_options = ['keep-temp', 'skip-build', 'relative', 'universal']
|
|
|
|
def initialize_options(self):
|
|
self.bdist_dir = None
|
|
self.data_dir = None
|
|
self.plat_name = None
|
|
self.plat_tag = None
|
|
self.format = 'zip'
|
|
self.keep_temp = False
|
|
self.dist_dir = None
|
|
self.egginfo_dir = None
|
|
self.root_is_pure = None
|
|
self.skip_build = None
|
|
self.relative = False
|
|
self.owner = None
|
|
self.group = None
|
|
self.universal = False
|
|
self.python_tag = 'py' + get_impl_ver()[0]
|
|
self.build_number = None
|
|
self.py_limited_api = False
|
|
self.plat_name_supplied = False
|
|
|
|
def finalize_options(self):
|
|
if self.bdist_dir is None:
|
|
bdist_base = self.get_finalized_command('bdist').bdist_base
|
|
self.bdist_dir = os.path.join(bdist_base, 'wheel')
|
|
|
|
self.data_dir = self.wheel_dist_name + '.data'
|
|
self.plat_name_supplied = self.plat_name is not None
|
|
|
|
need_options = ('dist_dir', 'plat_name', 'skip_build')
|
|
|
|
self.set_undefined_options('bdist',
|
|
*zip(need_options, need_options))
|
|
|
|
self.root_is_pure = not (self.distribution.has_ext_modules()
|
|
or self.distribution.has_c_libraries())
|
|
|
|
if self.py_limited_api and not re.match(PY_LIMITED_API_PATTERN, self.py_limited_api):
|
|
raise ValueError("py-limited-api must match '%s'" % PY_LIMITED_API_PATTERN)
|
|
|
|
# Support legacy [wheel] section for setting universal
|
|
wheel = self.distribution.get_option_dict('wheel')
|
|
if 'universal' in wheel:
|
|
# please don't define this in your global configs
|
|
logger.warn('The [wheel] section is deprecated. Use [bdist_wheel] instead.')
|
|
val = wheel['universal'][1].strip()
|
|
if val.lower() in ('1', 'true', 'yes'):
|
|
self.universal = True
|
|
|
|
if self.build_number is not None and not self.build_number[:1].isdigit():
|
|
raise ValueError("Build tag (build-number) must start with a digit.")
|
|
|
|
@property
|
|
def wheel_dist_name(self):
|
|
"""Return distribution full name with - replaced with _"""
|
|
components = (safer_name(self.distribution.get_name()),
|
|
safer_version(self.distribution.get_version()))
|
|
if self.build_number:
|
|
components += (self.build_number,)
|
|
return '-'.join(components)
|
|
|
|
def get_tag(self):
|
|
# bdist sets self.plat_name if unset, we should only use it for purepy
|
|
# wheels if the user supplied it.
|
|
if self.plat_name_supplied:
|
|
plat_name = self.plat_name
|
|
elif self.root_is_pure:
|
|
plat_name = 'any'
|
|
else:
|
|
plat_name = self.plat_name or get_platform()
|
|
if plat_name in ('linux-x86_64', 'linux_x86_64') and sys.maxsize == 2147483647:
|
|
plat_name = 'linux_i686'
|
|
plat_name = plat_name.replace('-', '_').replace('.', '_')
|
|
|
|
if self.root_is_pure:
|
|
if self.universal:
|
|
impl = 'py2.py3'
|
|
else:
|
|
impl = self.python_tag
|
|
tag = (impl, 'none', plat_name)
|
|
else:
|
|
impl_name = get_abbr_impl()
|
|
impl_ver = get_impl_ver()
|
|
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'):
|
|
impl = self.py_limited_api
|
|
abi_tag = 'abi3'
|
|
else:
|
|
abi_tag = str(get_abi_tag()).lower()
|
|
tag = (impl, abi_tag, plat_name)
|
|
supported_tags = pep425tags.get_supported(
|
|
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])
|
|
assert tag in supported_tags, "would build wheel with unsupported tag {}".format(tag)
|
|
return tag
|
|
|
|
def run(self):
|
|
build_scripts = self.reinitialize_command('build_scripts')
|
|
build_scripts.executable = 'python'
|
|
|
|
if not self.skip_build:
|
|
self.run_command('build')
|
|
|
|
install = self.reinitialize_command('install',
|
|
reinit_subcommands=True)
|
|
install.root = self.bdist_dir
|
|
install.compile = False
|
|
install.skip_build = self.skip_build
|
|
install.warn_dir = False
|
|
|
|
# A wheel without setuptools scripts is more cross-platform.
|
|
# Use the (undocumented) `no_ep` option to setuptools'
|
|
# install_scripts command to avoid creating entry point scripts.
|
|
install_scripts = self.reinitialize_command('install_scripts')
|
|
install_scripts.no_ep = True
|
|
|
|
# Use a custom scheme for the archive, because we have to decide
|
|
# at installation time which scheme to use.
|
|
for key in ('headers', 'scripts', 'data', 'purelib', 'platlib'):
|
|
setattr(install,
|
|
'install_' + key,
|
|
os.path.join(self.data_dir, key))
|
|
|
|
basedir_observed = ''
|
|
|
|
if os.name == 'nt':
|
|
# win32 barfs if any of these are ''; could be '.'?
|
|
# (distutils.command.install:change_roots bug)
|
|
basedir_observed = os.path.normpath(os.path.join(self.data_dir, '..'))
|
|
self.install_libbase = self.install_lib = basedir_observed
|
|
|
|
setattr(install,
|
|
'install_purelib' if self.root_is_pure else 'install_platlib',
|
|
basedir_observed)
|
|
|
|
logger.info("installing to %s", self.bdist_dir)
|
|
|
|
self.run_command('install')
|
|
|
|
impl_tag, abi_tag, plat_tag = self.get_tag()
|
|
archive_basename = "{}-{}-{}-{}".format(self.wheel_dist_name, impl_tag, abi_tag, plat_tag)
|
|
if not self.relative:
|
|
archive_root = self.bdist_dir
|
|
else:
|
|
archive_root = os.path.join(
|
|
self.bdist_dir,
|
|
self._ensure_relative(install.install_base))
|
|
|
|
self.set_undefined_options('install_egg_info', ('target', 'egginfo_dir'))
|
|
distinfo_dirname = '{}-{}.dist-info'.format(
|
|
safer_name(self.distribution.get_name()),
|
|
safer_version(self.distribution.get_version()))
|
|
distinfo_dir = os.path.join(self.bdist_dir, distinfo_dirname)
|
|
self.egg2dist(self.egginfo_dir, distinfo_dir)
|
|
|
|
self.write_wheelfile(distinfo_dir)
|
|
|
|
# Make the archive
|
|
if not os.path.exists(self.dist_dir):
|
|
os.makedirs(self.dist_dir)
|
|
|
|
wheel_path = os.path.join(self.dist_dir, archive_basename + '.whl')
|
|
with WheelFile(wheel_path, 'w') as wf:
|
|
wf.write_files(archive_root)
|
|
|
|
# Add to 'Distribution.dist_files' so that the "upload" command works
|
|
getattr(self.distribution, 'dist_files', []).append(
|
|
('bdist_wheel', get_python_version(), wheel_path))
|
|
|
|
if not self.keep_temp:
|
|
logger.info('removing %s', self.bdist_dir)
|
|
if not self.dry_run:
|
|
rmtree(self.bdist_dir)
|
|
|
|
def write_wheelfile(self, wheelfile_base, generator='bdist_wheel (' + wheel_version + ')'):
|
|
from email.message import Message
|
|
msg = Message()
|
|
msg['Wheel-Version'] = '1.0' # of the spec
|
|
msg['Generator'] = generator
|
|
msg['Root-Is-Purelib'] = str(self.root_is_pure).lower()
|
|
if self.build_number is not None:
|
|
msg['Build'] = self.build_number
|
|
|
|
# Doesn't work for bdist_wininst
|
|
impl_tag, abi_tag, plat_tag = self.get_tag()
|
|
for impl in impl_tag.split('.'):
|
|
for abi in abi_tag.split('.'):
|
|
for plat in plat_tag.split('.'):
|
|
msg['Tag'] = '-'.join((impl, abi, plat))
|
|
|
|
wheelfile_path = os.path.join(wheelfile_base, 'WHEEL')
|
|
logger.info('creating %s', wheelfile_path)
|
|
with open(wheelfile_path, 'w') as f:
|
|
Generator(f, maxheaderlen=0).flatten(msg)
|
|
|
|
def _ensure_relative(self, path):
|
|
# copied from dir_util, deleted
|
|
drive, path = os.path.splitdrive(path)
|
|
if path[0:1] == os.sep:
|
|
path = drive + path[1:]
|
|
return path
|
|
|
|
@property
|
|
def license_paths(self):
|
|
metadata = self.distribution.get_option_dict('metadata')
|
|
files = set()
|
|
patterns = sorted({
|
|
option for option in metadata.get('license_files', ('', ''))[1].split()
|
|
})
|
|
|
|
if 'license_file' in metadata:
|
|
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:
|
|
patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')
|
|
|
|
for pattern in patterns:
|
|
for path in iglob(pattern):
|
|
if path not in files and os.path.isfile(path):
|
|
logger.info('adding license file "%s" (matched pattern "%s")', path, pattern)
|
|
files.add(path)
|
|
|
|
return files
|
|
|
|
def egg2dist(self, egginfo_path, distinfo_path):
|
|
"""Convert an .egg-info directory into a .dist-info directory"""
|
|
def adios(p):
|
|
"""Appropriately delete directory, file or link."""
|
|
if os.path.exists(p) and not os.path.islink(p) and os.path.isdir(p):
|
|
shutil.rmtree(p)
|
|
elif os.path.exists(p):
|
|
os.unlink(p)
|
|
|
|
adios(distinfo_path)
|
|
|
|
if not os.path.exists(egginfo_path):
|
|
# There is no egg-info. This is probably because the egg-info
|
|
# file/directory is not named matching the distribution name used
|
|
# to name the archive file. Check for this case and report
|
|
# accordingly.
|
|
import glob
|
|
pat = os.path.join(os.path.dirname(egginfo_path), '*.egg-info')
|
|
possible = glob.glob(pat)
|
|
err = "Egg metadata expected at %s but not found" % (egginfo_path,)
|
|
if possible:
|
|
alt = os.path.basename(possible[0])
|
|
err += " (%s found - possible misnamed archive file?)" % (alt,)
|
|
|
|
raise ValueError(err)
|
|
|
|
if os.path.isfile(egginfo_path):
|
|
# .egg-info is a single file
|
|
pkginfo_path = egginfo_path
|
|
pkg_info = pkginfo_to_metadata(egginfo_path, egginfo_path)
|
|
os.mkdir(distinfo_path)
|
|
else:
|
|
# .egg-info is a directory
|
|
pkginfo_path = os.path.join(egginfo_path, 'PKG-INFO')
|
|
pkg_info = pkginfo_to_metadata(egginfo_path, pkginfo_path)
|
|
|
|
# ignore common egg metadata that is useless to wheel
|
|
shutil.copytree(egginfo_path, distinfo_path,
|
|
ignore=lambda x, y: {'PKG-INFO', 'requires.txt', 'SOURCES.txt',
|
|
'not-zip-safe'}
|
|
)
|
|
|
|
# delete dependency_links if it is only whitespace
|
|
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)
|
|
|
|
for license_path in self.license_paths:
|
|
filename = os.path.basename(license_path)
|
|
shutil.copy(license_path, os.path.join(distinfo_path, filename))
|
|
|
|
adios(egginfo_path)
|