123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- """Wheels support."""
- from distutils.util import get_platform
- from distutils import log
- import email
- import itertools
- import os
- import posixpath
- import re
- import zipfile
- import pkg_resources
- import setuptools
- from pkg_resources import parse_version
- from setuptools.extern.packaging.tags import sys_tags
- from setuptools.extern.packaging.utils import canonicalize_name
- from setuptools.extern.six import PY3
- from setuptools.command.egg_info import write_requirements
- __metaclass__ = type
- WHEEL_NAME = re.compile(
- r"""^(?P<project_name>.+?)-(?P<version>\d.*?)
- ((-(?P<build>\d.*?))?-(?P<py_version>.+?)-(?P<abi>.+?)-(?P<platform>.+?)
- )\.whl$""",
- re.VERBOSE).match
- NAMESPACE_PACKAGE_INIT = '''\
- try:
- __import__('pkg_resources').declare_namespace(__name__)
- except ImportError:
- __path__ = __import__('pkgutil').extend_path(__path__, __name__)
- '''
- def unpack(src_dir, dst_dir):
- '''Move everything under `src_dir` to `dst_dir`, and delete the former.'''
- for dirpath, dirnames, filenames in os.walk(src_dir):
- subdir = os.path.relpath(dirpath, src_dir)
- for f in filenames:
- src = os.path.join(dirpath, f)
- dst = os.path.join(dst_dir, subdir, f)
- os.renames(src, dst)
- for n, d in reversed(list(enumerate(dirnames))):
- src = os.path.join(dirpath, d)
- dst = os.path.join(dst_dir, subdir, d)
- if not os.path.exists(dst):
- # Directory does not exist in destination,
- # rename it and prune it from os.walk list.
- os.renames(src, dst)
- del dirnames[n]
- # Cleanup.
- for dirpath, dirnames, filenames in os.walk(src_dir, topdown=True):
- assert not filenames
- os.rmdir(dirpath)
- class Wheel:
- def __init__(self, filename):
- match = WHEEL_NAME(os.path.basename(filename))
- if match is None:
- raise ValueError('invalid wheel name: %r' % filename)
- self.filename = filename
- for k, v in match.groupdict().items():
- setattr(self, k, v)
- def tags(self):
- '''List tags (py_version, abi, platform) supported by this wheel.'''
- return itertools.product(
- self.py_version.split('.'),
- self.abi.split('.'),
- self.platform.split('.'),
- )
- def is_compatible(self):
- '''Is the wheel is compatible with the current platform?'''
- supported_tags = set((t.interpreter, t.abi, t.platform) for t in sys_tags())
- return next((True for t in self.tags() if t in supported_tags), False)
- def egg_name(self):
- return pkg_resources.Distribution(
- project_name=self.project_name, version=self.version,
- platform=(None if self.platform == 'any' else get_platform()),
- ).egg_name() + '.egg'
- def get_dist_info(self, zf):
- # find the correct name of the .dist-info dir in the wheel file
- for member in zf.namelist():
- dirname = posixpath.dirname(member)
- if (dirname.endswith('.dist-info') and
- canonicalize_name(dirname).startswith(
- canonicalize_name(self.project_name))):
- return dirname
- raise ValueError("unsupported wheel format. .dist-info not found")
- def install_as_egg(self, destination_eggdir):
- '''Install wheel as an egg directory.'''
- with zipfile.ZipFile(self.filename) as zf:
- self._install_as_egg(destination_eggdir, zf)
- def _install_as_egg(self, destination_eggdir, zf):
- dist_basename = '%s-%s' % (self.project_name, self.version)
- dist_info = self.get_dist_info(zf)
- dist_data = '%s.data' % dist_basename
- egg_info = os.path.join(destination_eggdir, 'EGG-INFO')
- self._convert_metadata(zf, destination_eggdir, dist_info, egg_info)
- self._move_data_entries(destination_eggdir, dist_data)
- self._fix_namespace_packages(egg_info, destination_eggdir)
- @staticmethod
- def _convert_metadata(zf, destination_eggdir, dist_info, egg_info):
- def get_metadata(name):
- with zf.open(posixpath.join(dist_info, name)) as fp:
- value = fp.read().decode('utf-8') if PY3 else fp.read()
- return email.parser.Parser().parsestr(value)
- wheel_metadata = get_metadata('WHEEL')
- # Check wheel format version is supported.
- wheel_version = parse_version(wheel_metadata.get('Wheel-Version'))
- wheel_v1 = (
- parse_version('1.0') <= wheel_version < parse_version('2.0dev0')
- )
- if not wheel_v1:
- raise ValueError(
- 'unsupported wheel format version: %s' % wheel_version)
- # Extract to target directory.
- os.mkdir(destination_eggdir)
- zf.extractall(destination_eggdir)
- # Convert metadata.
- dist_info = os.path.join(destination_eggdir, dist_info)
- dist = pkg_resources.Distribution.from_location(
- destination_eggdir, dist_info,
- metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info),
- )
- # Note: Evaluate and strip markers now,
- # as it's difficult to convert back from the syntax:
- # foobar; "linux" in sys_platform and extra == 'test'
- def raw_req(req):
- req.marker = None
- return str(req)
- install_requires = list(sorted(map(raw_req, dist.requires())))
- extras_require = {
- extra: sorted(
- req
- for req in map(raw_req, dist.requires((extra,)))
- if req not in install_requires
- )
- for extra in dist.extras
- }
- os.rename(dist_info, egg_info)
- os.rename(
- os.path.join(egg_info, 'METADATA'),
- os.path.join(egg_info, 'PKG-INFO'),
- )
- setup_dist = setuptools.Distribution(
- attrs=dict(
- install_requires=install_requires,
- extras_require=extras_require,
- ),
- )
- # Temporarily disable info traces.
- log_threshold = log._global_log.threshold
- log.set_threshold(log.WARN)
- try:
- write_requirements(
- setup_dist.get_command_obj('egg_info'),
- None,
- os.path.join(egg_info, 'requires.txt'),
- )
- finally:
- log.set_threshold(log_threshold)
- @staticmethod
- def _move_data_entries(destination_eggdir, dist_data):
- """Move data entries to their correct location."""
- dist_data = os.path.join(destination_eggdir, dist_data)
- dist_data_scripts = os.path.join(dist_data, 'scripts')
- if os.path.exists(dist_data_scripts):
- egg_info_scripts = os.path.join(
- destination_eggdir, 'EGG-INFO', 'scripts')
- os.mkdir(egg_info_scripts)
- for entry in os.listdir(dist_data_scripts):
- # Remove bytecode, as it's not properly handled
- # during easy_install scripts install phase.
- if entry.endswith('.pyc'):
- os.unlink(os.path.join(dist_data_scripts, entry))
- else:
- os.rename(
- os.path.join(dist_data_scripts, entry),
- os.path.join(egg_info_scripts, entry),
- )
- os.rmdir(dist_data_scripts)
- for subdir in filter(os.path.exists, (
- os.path.join(dist_data, d)
- for d in ('data', 'headers', 'purelib', 'platlib')
- )):
- unpack(subdir, destination_eggdir)
- if os.path.exists(dist_data):
- os.rmdir(dist_data)
- @staticmethod
- def _fix_namespace_packages(egg_info, destination_eggdir):
- namespace_packages = os.path.join(
- egg_info, 'namespace_packages.txt')
- if os.path.exists(namespace_packages):
- with open(namespace_packages) as fp:
- namespace_packages = fp.read().split()
- for mod in namespace_packages:
- mod_dir = os.path.join(destination_eggdir, *mod.split('.'))
- mod_init = os.path.join(mod_dir, '__init__.py')
- if not os.path.exists(mod_dir):
- os.mkdir(mod_dir)
- if not os.path.exists(mod_init):
- with open(mod_init, 'w') as fp:
- fp.write(NAMESPACE_PACKAGE_INIT)
|