123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- """
- Create a wheel (.whl) distribution.
- A wheel is a built archive format.
- """
- import csv
- import hashlib
- import os
- import subprocess
- import warnings
- import shutil
- import json
- import wheel
- try:
- import sysconfig
- except ImportError: # pragma nocover
- # Python < 2.7
- import distutils.sysconfig as sysconfig
- import pkg_resources
- safe_name = pkg_resources.safe_name
- safe_version = pkg_resources.safe_version
- from shutil import rmtree
- from email.generator import Generator
- from distutils.util import get_platform
- from distutils.core import Command
- from distutils.sysconfig import get_python_version
- from distutils import log as logger
- 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
- from .metadata import pkginfo_to_dict
- from . import pep425tags, metadata
- 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]),
- ]
- 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.distinfo_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.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())
- # 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
- val = wheel['universal'][1].strip()
- if val.lower() in ('1', 'true', 'yes'):
- self.universal = True
- @property
- def wheel_dist_name(self):
- """Return distribution full name with - replaced with _"""
- return '-'.join((safer_name(self.distribution.get_name()),
- safer_version(self.distribution.get_version())))
- 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()
- 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()
- # PEP 3149
- abi_tag = str(get_abi_tag()).lower()
- tag = (impl_name + impl_ver, 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:
- assert tag == supported_tags[0]
- return tag
- def get_archive_basename(self):
- """Return archive name without extension"""
- impl_tag, abi_tag, plat_tag = self.get_tag()
- archive_basename = "%s-%s-%s-%s" % (
- self.wheel_dist_name,
- impl_tag,
- abi_tag,
- plat_tag)
- return archive_basename
- 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')
- archive_basename = self.get_archive_basename()
- pseudoinstall_root = os.path.join(self.dist_dir, archive_basename)
- 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'))
- self.distinfo_dir = os.path.join(self.bdist_dir,
- '%s.dist-info' % self.wheel_dist_name)
- self.egg2dist(self.egginfo_dir,
- self.distinfo_dir)
- self.write_wheelfile(self.distinfo_dir)
- self.write_record(self.bdist_dir, self.distinfo_dir)
- # Make the archive
- if not os.path.exists(self.dist_dir):
- os.makedirs(self.dist_dir)
- wheel_name = archive_wheelfile(pseudoinstall_root, archive_root)
- # Sign the archive
- if 'WHEEL_TOOL' in os.environ:
- subprocess.call([os.environ['WHEEL_TOOL'], 'sign', wheel_name])
- # Add to 'Distribution.dist_files' so that the "upload" command works
- getattr(self.distribution, 'dist_files', []).append(
- ('bdist_wheel', get_python_version(), wheel_name))
- if not self.keep_temp:
- if self.dry_run:
- logger.info('removing %s', self.bdist_dir)
- else:
- 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()
- # 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
- def _pkginfo_to_metadata(self, egg_info_path, pkginfo_path):
- return metadata.pkginfo_to_metadata(egg_info_path, pkginfo_path)
- def license_file(self):
- """Return license filename from a license-file key in setup.cfg, or None."""
- metadata = self.distribution.get_option_dict('metadata')
- if not 'license_file' in metadata:
- return None
- return metadata['license_file'][1]
- def setupcfg_requirements(self):
- """Generate requirements from setup.cfg as
- ('Requires-Dist', 'requirement; qualifier') tuples. From a metadata
- section in setup.cfg:
- [metadata]
- provides-extra = extra1
- extra2
- requires-dist = requirement; qualifier
- another; qualifier2
- unqualified
- Yields
- ('Provides-Extra', 'extra1'),
- ('Provides-Extra', 'extra2'),
- ('Requires-Dist', 'requirement; qualifier'),
- ('Requires-Dist', 'another; qualifier2'),
- ('Requires-Dist', 'unqualified')
- """
- metadata = self.distribution.get_option_dict('metadata')
- # our .ini parser folds - to _ in key names:
- for key, title in (('provides_extra', 'Provides-Extra'),
- ('requires_dist', 'Requires-Dist')):
- if not key in metadata:
- continue
- field = metadata[key]
- for line in field[1].splitlines():
- line = line.strip()
- if not line:
- continue
- yield (title, line)
- def add_requirements(self, metadata_path):
- """Add additional requirements from setup.cfg to file metadata_path"""
- additional = list(self.setupcfg_requirements())
- if not additional: return
- pkg_info = read_pkg_info(metadata_path)
- if 'Provides-Extra' in pkg_info or 'Requires-Dist' in pkg_info:
- warnings.warn('setup.cfg requirements overwrite values from setup.py')
- del pkg_info['Provides-Extra']
- del pkg_info['Requires-Dist']
- for k, v in additional:
- pkg_info[k] = v
- write_pkg_info(metadata_path, pkg_info)
- 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 = self._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 = self._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: set(('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)
- # XXX deprecated. Still useful for current distribute/setuptools.
- metadata_path = os.path.join(distinfo_path, 'METADATA')
- self.add_requirements(metadata_path)
- # XXX intentionally a different path than the PEP.
- metadata_json_path = os.path.join(distinfo_path, 'metadata.json')
- pymeta = pkginfo_to_dict(metadata_path,
- distribution=self.distribution)
- if 'description' in pymeta:
- description_filename = 'DESCRIPTION.rst'
- description_text = pymeta.pop('description')
- description_path = os.path.join(distinfo_path,
- description_filename)
- with open(description_path, "wb") as description_file:
- description_file.write(description_text.encode('utf-8'))
- pymeta['extensions']['python.details']['document_names']['description'] = description_filename
- # XXX heuristically copy any LICENSE/LICENSE.txt?
- license = self.license_file()
- if license:
- license_filename = 'LICENSE.txt'
- shutil.copy(license, os.path.join(self.distinfo_dir, license_filename))
- pymeta['extensions']['python.details']['document_names']['license'] = license_filename
- with open(metadata_json_path, "w") as metadata_json:
- json.dump(pymeta, metadata_json, sort_keys=True)
- adios(egginfo_path)
- def write_record(self, bdist_dir, distinfo_dir):
- from wheel.util import urlsafe_b64encode
- record_path = os.path.join(distinfo_dir, 'RECORD')
- record_relpath = os.path.relpath(record_path, bdist_dir)
- def walk():
- for dir, dirs, files in os.walk(bdist_dir):
- dirs.sort()
- for f in sorted(files):
- yield os.path.join(dir, f)
- def skip(path):
- """Wheel hashes every possible file."""
- return (path == record_relpath)
- with open_for_csv(record_path, 'w+') as record_file:
- writer = csv.writer(record_file)
- for path in walk():
- relpath = os.path.relpath(path, bdist_dir)
- if skip(relpath):
- hash = ''
- size = ''
- else:
- with open(path, 'rb') as f:
- data = f.read()
- digest = hashlib.sha256(data).digest()
- hash = 'sha256=' + native(urlsafe_b64encode(digest))
- size = len(data)
- record_path = os.path.relpath(
- path, bdist_dir).replace(os.path.sep, '/')
- writer.writerow((record_path, hash, size))
|