• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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