1'''Wheels support.''' 2 3from distutils.util import get_platform 4import email 5import itertools 6import os 7import re 8import zipfile 9 10from pkg_resources import Distribution, PathMetadata, parse_version 11from setuptools.extern.six import PY3 12from setuptools import Distribution as SetuptoolsDistribution 13from setuptools import pep425tags 14from setuptools.command.egg_info import write_requirements 15 16 17WHEEL_NAME = re.compile( 18 r"""^(?P<project_name>.+?)-(?P<version>\d.*?) 19 ((-(?P<build>\d.*?))?-(?P<py_version>.+?)-(?P<abi>.+?)-(?P<platform>.+?) 20 )\.whl$""", 21re.VERBOSE).match 22 23NAMESPACE_PACKAGE_INIT = '''\ 24try: 25 __import__('pkg_resources').declare_namespace(__name__) 26except ImportError: 27 __path__ = __import__('pkgutil').extend_path(__path__, __name__) 28''' 29 30 31def unpack(src_dir, dst_dir): 32 '''Move everything under `src_dir` to `dst_dir`, and delete the former.''' 33 for dirpath, dirnames, filenames in os.walk(src_dir): 34 subdir = os.path.relpath(dirpath, src_dir) 35 for f in filenames: 36 src = os.path.join(dirpath, f) 37 dst = os.path.join(dst_dir, subdir, f) 38 os.renames(src, dst) 39 for n, d in reversed(list(enumerate(dirnames))): 40 src = os.path.join(dirpath, d) 41 dst = os.path.join(dst_dir, subdir, d) 42 if not os.path.exists(dst): 43 # Directory does not exist in destination, 44 # rename it and prune it from os.walk list. 45 os.renames(src, dst) 46 del dirnames[n] 47 # Cleanup. 48 for dirpath, dirnames, filenames in os.walk(src_dir, topdown=True): 49 assert not filenames 50 os.rmdir(dirpath) 51 52 53class Wheel(object): 54 55 def __init__(self, filename): 56 match = WHEEL_NAME(os.path.basename(filename)) 57 if match is None: 58 raise ValueError('invalid wheel name: %r' % filename) 59 self.filename = filename 60 for k, v in match.groupdict().items(): 61 setattr(self, k, v) 62 63 def tags(self): 64 '''List tags (py_version, abi, platform) supported by this wheel.''' 65 return itertools.product(self.py_version.split('.'), 66 self.abi.split('.'), 67 self.platform.split('.')) 68 69 def is_compatible(self): 70 '''Is the wheel is compatible with the current platform?''' 71 supported_tags = pep425tags.get_supported() 72 return next((True for t in self.tags() if t in supported_tags), False) 73 74 def egg_name(self): 75 return Distribution( 76 project_name=self.project_name, version=self.version, 77 platform=(None if self.platform == 'any' else get_platform()), 78 ).egg_name() + '.egg' 79 80 def install_as_egg(self, destination_eggdir): 81 '''Install wheel as an egg directory.''' 82 with zipfile.ZipFile(self.filename) as zf: 83 dist_basename = '%s-%s' % (self.project_name, self.version) 84 dist_info = '%s.dist-info' % dist_basename 85 dist_data = '%s.data' % dist_basename 86 def get_metadata(name): 87 with zf.open('%s/%s' % (dist_info, name)) as fp: 88 value = fp.read().decode('utf-8') if PY3 else fp.read() 89 return email.parser.Parser().parsestr(value) 90 wheel_metadata = get_metadata('WHEEL') 91 dist_metadata = get_metadata('METADATA') 92 # Check wheel format version is supported. 93 wheel_version = parse_version(wheel_metadata.get('Wheel-Version')) 94 if not parse_version('1.0') <= wheel_version < parse_version('2.0dev0'): 95 raise ValueError('unsupported wheel format version: %s' % wheel_version) 96 # Extract to target directory. 97 os.mkdir(destination_eggdir) 98 zf.extractall(destination_eggdir) 99 # Convert metadata. 100 dist_info = os.path.join(destination_eggdir, dist_info) 101 dist = Distribution.from_location( 102 destination_eggdir, dist_info, 103 metadata=PathMetadata(destination_eggdir, dist_info) 104 ) 105 # Note: we need to evaluate and strip markers now, 106 # as we can't easily convert back from the syntax: 107 # foobar; "linux" in sys_platform and extra == 'test' 108 def raw_req(req): 109 req.marker = None 110 return str(req) 111 install_requires = list(sorted(map(raw_req, dist.requires()))) 112 extras_require = { 113 extra: list(sorted( 114 req 115 for req in map(raw_req, dist.requires((extra,))) 116 if req not in install_requires 117 )) 118 for extra in dist.extras 119 } 120 egg_info = os.path.join(destination_eggdir, 'EGG-INFO') 121 os.rename(dist_info, egg_info) 122 os.rename(os.path.join(egg_info, 'METADATA'), 123 os.path.join(egg_info, 'PKG-INFO')) 124 setup_dist = SetuptoolsDistribution(attrs=dict( 125 install_requires=install_requires, 126 extras_require=extras_require, 127 )) 128 write_requirements(setup_dist.get_command_obj('egg_info'), 129 None, os.path.join(egg_info, 'requires.txt')) 130 # Move data entries to their correct location. 131 dist_data = os.path.join(destination_eggdir, dist_data) 132 dist_data_scripts = os.path.join(dist_data, 'scripts') 133 if os.path.exists(dist_data_scripts): 134 egg_info_scripts = os.path.join(destination_eggdir, 135 'EGG-INFO', 'scripts') 136 os.mkdir(egg_info_scripts) 137 for entry in os.listdir(dist_data_scripts): 138 # Remove bytecode, as it's not properly handled 139 # during easy_install scripts install phase. 140 if entry.endswith('.pyc'): 141 os.unlink(os.path.join(dist_data_scripts, entry)) 142 else: 143 os.rename(os.path.join(dist_data_scripts, entry), 144 os.path.join(egg_info_scripts, entry)) 145 os.rmdir(dist_data_scripts) 146 for subdir in filter(os.path.exists, ( 147 os.path.join(dist_data, d) 148 for d in ('data', 'headers', 'purelib', 'platlib') 149 )): 150 unpack(subdir, destination_eggdir) 151 if os.path.exists(dist_data): 152 os.rmdir(dist_data) 153 # Fix namespace packages. 154 namespace_packages = os.path.join(egg_info, 'namespace_packages.txt') 155 if os.path.exists(namespace_packages): 156 with open(namespace_packages) as fp: 157 namespace_packages = fp.read().split() 158 for mod in namespace_packages: 159 mod_dir = os.path.join(destination_eggdir, *mod.split('.')) 160 mod_init = os.path.join(mod_dir, '__init__.py') 161 if os.path.exists(mod_dir) and not os.path.exists(mod_init): 162 with open(mod_init, 'w') as fp: 163 fp.write(NAMESPACE_PACKAGE_INIT) 164