codekingpro/portable-devtools
114k
1"""Wheels support."""2 3import email4import itertools5import functools6import os7import posixpath8import re9import zipfile10import contextlib11 12from distutils.util import get_platform13 14import setuptools15from setuptools.extern.packaging.version import Version as parse_version16from setuptools.extern.packaging.tags import sys_tags17from setuptools.extern.packaging.utils import canonicalize_name18from setuptools.command.egg_info import write_requirements, _egg_basename19from setuptools.archive_util import _unpack_zipfile_obj20 21 22WHEEL_NAME = re.compile(23 r"""^(?P<project_name>.+?)-(?P<version>\d.*?)24 ((-(?P<build>\d.*?))?-(?P<py_version>.+?)-(?P<abi>.+?)-(?P<platform>.+?)25 )\.whl$""",26 re.VERBOSE,27).match28 29NAMESPACE_PACKAGE_INIT = "__import__('pkg_resources').declare_namespace(__name__)\n"30 31 32@functools.lru_cache(maxsize=None)33def _get_supported_tags():34 # We calculate the supported tags only once, otherwise calling35 # this method on thousands of wheels takes seconds instead of36 # milliseconds.37 return {(t.interpreter, t.abi, t.platform) for t in sys_tags()}38 39 40def unpack(src_dir, dst_dir):41 """Move everything under `src_dir` to `dst_dir`, and delete the former."""42 for dirpath, dirnames, filenames in os.walk(src_dir):43 subdir = os.path.relpath(dirpath, src_dir)44 for f in filenames:45 src = os.path.join(dirpath, f)46 dst = os.path.join(dst_dir, subdir, f)47 os.renames(src, dst)48 for n, d in reversed(list(enumerate(dirnames))):49 src = os.path.join(dirpath, d)50 dst = os.path.join(dst_dir, subdir, d)51 if not os.path.exists(dst):52 # Directory does not exist in destination,53 # rename it and prune it from os.walk list.54 os.renames(src, dst)55 del dirnames[n]56 # Cleanup.57 for dirpath, dirnames, filenames in os.walk(src_dir, topdown=True):58 assert not filenames59 os.rmdir(dirpath)60 61 62@contextlib.contextmanager63def disable_info_traces():64 """65 Temporarily disable info traces.66 """67 from distutils import log68 69 saved = log.set_threshold(log.WARN)70 try:71 yield72 finally:73 log.set_threshold(saved)74 75 76class Wheel:77 def __init__(self, filename):78 match = WHEEL_NAME(os.path.basename(filename))79 if match is None:80 raise ValueError('invalid wheel name: %r' % filename)81 self.filename = filename82 for k, v in match.groupdict().items():83 setattr(self, k, v)84 85 def tags(self):86 """List tags (py_version, abi, platform) supported by this wheel."""87 return itertools.product(88 self.py_version.split('.'),89 self.abi.split('.'),90 self.platform.split('.'),91 )92 93 def is_compatible(self):94 """Is the wheel compatible with the current platform?"""95 return next((True for t in self.tags() if t in _get_supported_tags()), False)96 97 def egg_name(self):98 return (99 _egg_basename(100 self.project_name,101 self.version,102 platform=(None if self.platform == 'any' else get_platform()),103 )104 + ".egg"105 )106 107 def get_dist_info(self, zf):108 # find the correct name of the .dist-info dir in the wheel file109 for member in zf.namelist():110 dirname = posixpath.dirname(member)111 if dirname.endswith('.dist-info') and canonicalize_name(dirname).startswith(112 canonicalize_name(self.project_name)113 ):114 return dirname115 raise ValueError("unsupported wheel format. .dist-info not found")116 117 def install_as_egg(self, destination_eggdir):118 """Install wheel as an egg directory."""119 with zipfile.ZipFile(self.filename) as zf:120 self._install_as_egg(destination_eggdir, zf)121 122 def _install_as_egg(self, destination_eggdir, zf):123 dist_basename = '%s-%s' % (self.project_name, self.version)124 dist_info = self.get_dist_info(zf)125 dist_data = '%s.data' % dist_basename126 egg_info = os.path.join(destination_eggdir, 'EGG-INFO')127 128 self._convert_metadata(zf, destination_eggdir, dist_info, egg_info)129 self._move_data_entries(destination_eggdir, dist_data)130 self._fix_namespace_packages(egg_info, destination_eggdir)131 132 @staticmethod133 def _convert_metadata(zf, destination_eggdir, dist_info, egg_info):134 import pkg_resources135 136 def get_metadata(name):137 with zf.open(posixpath.join(dist_info, name)) as fp:138 value = fp.read().decode('utf-8')139 return email.parser.Parser().parsestr(value)140 141 wheel_metadata = get_metadata('WHEEL')142 # Check wheel format version is supported.143 wheel_version = parse_version(wheel_metadata.get('Wheel-Version'))144 wheel_v1 = parse_version('1.0') <= wheel_version < parse_version('2.0dev0')145 if not wheel_v1:146 raise ValueError('unsupported wheel format version: %s' % wheel_version)147 # Extract to target directory.148 _unpack_zipfile_obj(zf, destination_eggdir)149 # Convert metadata.150 dist_info = os.path.join(destination_eggdir, dist_info)151 dist = pkg_resources.Distribution.from_location(152 destination_eggdir,153 dist_info,154 metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info),155 )156 157 # Note: Evaluate and strip markers now,158 # as it's difficult to convert back from the syntax:159 # foobar; "linux" in sys_platform and extra == 'test'160 def raw_req(req):161 req.marker = None162 return str(req)163 164 install_requires = list(map(raw_req, dist.requires()))165 extras_require = {166 extra: [167 req168 for req in map(raw_req, dist.requires((extra,)))169 if req not in install_requires170 ]171 for extra in dist.extras172 }173 os.rename(dist_info, egg_info)174 os.rename(175 os.path.join(egg_info, 'METADATA'),176 os.path.join(egg_info, 'PKG-INFO'),177 )178 setup_dist = setuptools.Distribution(179 attrs=dict(180 install_requires=install_requires,181 extras_require=extras_require,182 ),183 )184 with disable_info_traces():185 write_requirements(186 setup_dist.get_command_obj('egg_info'),187 None,188 os.path.join(egg_info, 'requires.txt'),189 )190 191 @staticmethod192 def _move_data_entries(destination_eggdir, dist_data):193 """Move data entries to their correct location."""194 dist_data = os.path.join(destination_eggdir, dist_data)195 dist_data_scripts = os.path.join(dist_data, 'scripts')196 if os.path.exists(dist_data_scripts):197 egg_info_scripts = os.path.join(destination_eggdir, 'EGG-INFO', 'scripts')198 os.mkdir(egg_info_scripts)199 for entry in os.listdir(dist_data_scripts):200 # Remove bytecode, as it's not properly handled201 # during easy_install scripts install phase.202 if entry.endswith('.pyc'):203 os.unlink(os.path.join(dist_data_scripts, entry))204 else:205 os.rename(206 os.path.join(dist_data_scripts, entry),207 os.path.join(egg_info_scripts, entry),208 )209 os.rmdir(dist_data_scripts)210 for subdir in filter(211 os.path.exists,212 (213 os.path.join(dist_data, d)214 for d in ('data', 'headers', 'purelib', 'platlib')215 ),216 ):217 unpack(subdir, destination_eggdir)218 if os.path.exists(dist_data):219 os.rmdir(dist_data)220 221 @staticmethod222 def _fix_namespace_packages(egg_info, destination_eggdir):223 namespace_packages = os.path.join(egg_info, 'namespace_packages.txt')224 if os.path.exists(namespace_packages):225 with open(namespace_packages) as fp:226 namespace_packages = fp.read().split()227 for mod in namespace_packages:228 mod_dir = os.path.join(destination_eggdir, *mod.split('.'))229 mod_init = os.path.join(mod_dir, '__init__.py')230 if not os.path.exists(mod_dir):231 os.mkdir(mod_dir)232 if not os.path.exists(mod_init):233 with open(mod_init, 'w') as fp:234 fp.write(NAMESPACE_PACKAGE_INIT)235 