]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | """Wheels support.""" |
2 | ||
3 | import email | |
4 | import itertools | |
5 | import os | |
6 | import posixpath | |
7 | import re | |
8 | import zipfile | |
9 | import contextlib | |
10 | ||
11 | from distutils.util import get_platform | |
12 | ||
13 | import pkg_resources | |
14 | import setuptools | |
15 | from pkg_resources import parse_version | |
16 | from setuptools.extern.packaging.tags import sys_tags | |
17 | from setuptools.extern.packaging.utils import canonicalize_name | |
18 | from setuptools.command.egg_info import write_requirements | |
19 | from setuptools.archive_util import _unpack_zipfile_obj | |
20 | ||
21 | ||
22 | WHEEL_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).match | |
27 | ||
28 | NAMESPACE_PACKAGE_INIT = \ | |
29 | "__import__('pkg_resources').declare_namespace(__name__)\n" | |
30 | ||
31 | ||
32 | def unpack(src_dir, dst_dir): | |
33 | '''Move everything under `src_dir` to `dst_dir`, and delete the former.''' | |
34 | for dirpath, dirnames, filenames in os.walk(src_dir): | |
35 | subdir = os.path.relpath(dirpath, src_dir) | |
36 | for f in filenames: | |
37 | src = os.path.join(dirpath, f) | |
38 | dst = os.path.join(dst_dir, subdir, f) | |
39 | os.renames(src, dst) | |
40 | for n, d in reversed(list(enumerate(dirnames))): | |
41 | src = os.path.join(dirpath, d) | |
42 | dst = os.path.join(dst_dir, subdir, d) | |
43 | if not os.path.exists(dst): | |
44 | # Directory does not exist in destination, | |
45 | # rename it and prune it from os.walk list. | |
46 | os.renames(src, dst) | |
47 | del dirnames[n] | |
48 | # Cleanup. | |
49 | for dirpath, dirnames, filenames in os.walk(src_dir, topdown=True): | |
50 | assert not filenames | |
51 | os.rmdir(dirpath) | |
52 | ||
53 | ||
54 | @contextlib.contextmanager | |
55 | def disable_info_traces(): | |
56 | """ | |
57 | Temporarily disable info traces. | |
58 | """ | |
59 | from distutils import log | |
60 | saved = log.set_threshold(log.WARN) | |
61 | try: | |
62 | yield | |
63 | finally: | |
64 | log.set_threshold(saved) | |
65 | ||
66 | ||
67 | class Wheel: | |
68 | ||
69 | def __init__(self, filename): | |
70 | match = WHEEL_NAME(os.path.basename(filename)) | |
71 | if match is None: | |
72 | raise ValueError('invalid wheel name: %r' % filename) | |
73 | self.filename = filename | |
74 | for k, v in match.groupdict().items(): | |
75 | setattr(self, k, v) | |
76 | ||
77 | def tags(self): | |
78 | '''List tags (py_version, abi, platform) supported by this wheel.''' | |
79 | return itertools.product( | |
80 | self.py_version.split('.'), | |
81 | self.abi.split('.'), | |
82 | self.platform.split('.'), | |
83 | ) | |
84 | ||
85 | def is_compatible(self): | |
86 | '''Is the wheel is compatible with the current platform?''' | |
87 | supported_tags = set( | |
88 | (t.interpreter, t.abi, t.platform) for t in sys_tags()) | |
89 | return next((True for t in self.tags() if t in supported_tags), False) | |
90 | ||
91 | def egg_name(self): | |
92 | return pkg_resources.Distribution( | |
93 | project_name=self.project_name, version=self.version, | |
94 | platform=(None if self.platform == 'any' else get_platform()), | |
95 | ).egg_name() + '.egg' | |
96 | ||
97 | def get_dist_info(self, zf): | |
98 | # find the correct name of the .dist-info dir in the wheel file | |
99 | for member in zf.namelist(): | |
100 | dirname = posixpath.dirname(member) | |
101 | if (dirname.endswith('.dist-info') and | |
102 | canonicalize_name(dirname).startswith( | |
103 | canonicalize_name(self.project_name))): | |
104 | return dirname | |
105 | raise ValueError("unsupported wheel format. .dist-info not found") | |
106 | ||
107 | def install_as_egg(self, destination_eggdir): | |
108 | '''Install wheel as an egg directory.''' | |
109 | with zipfile.ZipFile(self.filename) as zf: | |
110 | self._install_as_egg(destination_eggdir, zf) | |
111 | ||
112 | def _install_as_egg(self, destination_eggdir, zf): | |
113 | dist_basename = '%s-%s' % (self.project_name, self.version) | |
114 | dist_info = self.get_dist_info(zf) | |
115 | dist_data = '%s.data' % dist_basename | |
116 | egg_info = os.path.join(destination_eggdir, 'EGG-INFO') | |
117 | ||
118 | self._convert_metadata(zf, destination_eggdir, dist_info, egg_info) | |
119 | self._move_data_entries(destination_eggdir, dist_data) | |
120 | self._fix_namespace_packages(egg_info, destination_eggdir) | |
121 | ||
122 | @staticmethod | |
123 | def _convert_metadata(zf, destination_eggdir, dist_info, egg_info): | |
124 | def get_metadata(name): | |
125 | with zf.open(posixpath.join(dist_info, name)) as fp: | |
126 | value = fp.read().decode('utf-8') | |
127 | return email.parser.Parser().parsestr(value) | |
128 | ||
129 | wheel_metadata = get_metadata('WHEEL') | |
130 | # Check wheel format version is supported. | |
131 | wheel_version = parse_version(wheel_metadata.get('Wheel-Version')) | |
132 | wheel_v1 = ( | |
133 | parse_version('1.0') <= wheel_version < parse_version('2.0dev0') | |
134 | ) | |
135 | if not wheel_v1: | |
136 | raise ValueError( | |
137 | 'unsupported wheel format version: %s' % wheel_version) | |
138 | # Extract to target directory. | |
139 | _unpack_zipfile_obj(zf, destination_eggdir) | |
140 | # Convert metadata. | |
141 | dist_info = os.path.join(destination_eggdir, dist_info) | |
142 | dist = pkg_resources.Distribution.from_location( | |
143 | destination_eggdir, dist_info, | |
144 | metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info), | |
145 | ) | |
146 | ||
147 | # Note: Evaluate and strip markers now, | |
148 | # as it's difficult to convert back from the syntax: | |
149 | # foobar; "linux" in sys_platform and extra == 'test' | |
150 | def raw_req(req): | |
151 | req.marker = None | |
152 | return str(req) | |
153 | install_requires = list(map(raw_req, dist.requires())) | |
154 | extras_require = { | |
155 | extra: [ | |
156 | req | |
157 | for req in map(raw_req, dist.requires((extra,))) | |
158 | if req not in install_requires | |
159 | ] | |
160 | for extra in dist.extras | |
161 | } | |
162 | os.rename(dist_info, egg_info) | |
163 | os.rename( | |
164 | os.path.join(egg_info, 'METADATA'), | |
165 | os.path.join(egg_info, 'PKG-INFO'), | |
166 | ) | |
167 | setup_dist = setuptools.Distribution( | |
168 | attrs=dict( | |
169 | install_requires=install_requires, | |
170 | extras_require=extras_require, | |
171 | ), | |
172 | ) | |
173 | with disable_info_traces(): | |
174 | write_requirements( | |
175 | setup_dist.get_command_obj('egg_info'), | |
176 | None, | |
177 | os.path.join(egg_info, 'requires.txt'), | |
178 | ) | |
179 | ||
180 | @staticmethod | |
181 | def _move_data_entries(destination_eggdir, dist_data): | |
182 | """Move data entries to their correct location.""" | |
183 | dist_data = os.path.join(destination_eggdir, dist_data) | |
184 | dist_data_scripts = os.path.join(dist_data, 'scripts') | |
185 | if os.path.exists(dist_data_scripts): | |
186 | egg_info_scripts = os.path.join( | |
187 | destination_eggdir, 'EGG-INFO', 'scripts') | |
188 | os.mkdir(egg_info_scripts) | |
189 | for entry in os.listdir(dist_data_scripts): | |
190 | # Remove bytecode, as it's not properly handled | |
191 | # during easy_install scripts install phase. | |
192 | if entry.endswith('.pyc'): | |
193 | os.unlink(os.path.join(dist_data_scripts, entry)) | |
194 | else: | |
195 | os.rename( | |
196 | os.path.join(dist_data_scripts, entry), | |
197 | os.path.join(egg_info_scripts, entry), | |
198 | ) | |
199 | os.rmdir(dist_data_scripts) | |
200 | for subdir in filter(os.path.exists, ( | |
201 | os.path.join(dist_data, d) | |
202 | for d in ('data', 'headers', 'purelib', 'platlib') | |
203 | )): | |
204 | unpack(subdir, destination_eggdir) | |
205 | if os.path.exists(dist_data): | |
206 | os.rmdir(dist_data) | |
207 | ||
208 | @staticmethod | |
209 | def _fix_namespace_packages(egg_info, destination_eggdir): | |
210 | namespace_packages = os.path.join( | |
211 | egg_info, 'namespace_packages.txt') | |
212 | if os.path.exists(namespace_packages): | |
213 | with open(namespace_packages) as fp: | |
214 | namespace_packages = fp.read().split() | |
215 | for mod in namespace_packages: | |
216 | mod_dir = os.path.join(destination_eggdir, *mod.split('.')) | |
217 | mod_init = os.path.join(mod_dir, '__init__.py') | |
218 | if not os.path.exists(mod_dir): | |
219 | os.mkdir(mod_dir) | |
220 | if not os.path.exists(mod_init): | |
221 | with open(mod_init, 'w') as fp: | |
222 | fp.write(NAMESPACE_PACKAGE_INIT) |