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