pex 2.59.4__py2.py3-none-any.whl → 2.60.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pex might be problematic. Click here for more details.
- pex/build_backend/wrap.py +25 -4
- pex/cache/dirs.py +14 -4
- pex/cli/commands/lock.py +8 -5
- pex/common.py +57 -7
- pex/compatibility.py +1 -1
- pex/dist_metadata.py +48 -6
- pex/docs/html/_pagefind/fragment/en_39c0488.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_3eeaaf4.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_a1dde36.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_a755644.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_b16e3bd.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/{en_e323b0a.pf_fragment → en_c5d35a7.pf_fragment} +0 -0
- pex/docs/html/_pagefind/fragment/en_ec62bd2.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_f32628f.pf_fragment +0 -0
- pex/docs/html/_pagefind/index/{en_9894162.pf_index → en_b211695.pf_index} +0 -0
- pex/docs/html/_pagefind/pagefind-entry.json +1 -1
- pex/docs/html/_pagefind/pagefind.en_e8a49380e5.pf_meta +0 -0
- pex/docs/html/_static/documentation_options.js +1 -1
- pex/docs/html/api/vars.html +5 -5
- pex/docs/html/buildingpex.html +5 -5
- pex/docs/html/genindex.html +5 -5
- pex/docs/html/index.html +5 -5
- pex/docs/html/recipes.html +5 -5
- pex/docs/html/scie.html +5 -5
- pex/docs/html/search.html +5 -5
- pex/docs/html/whatispex.html +5 -5
- pex/entry_points_txt.py +98 -0
- pex/environment.py +13 -10
- pex/finders.py +1 -1
- pex/installed_wheel.py +127 -0
- pex/interpreter.py +17 -5
- pex/interpreter_constraints.py +4 -4
- pex/pep_376.py +37 -385
- pex/pep_427.py +736 -248
- pex/pex_builder.py +4 -4
- pex/pex_info.py +8 -3
- pex/resolve/venv_resolver.py +98 -23
- pex/resolver.py +10 -3
- pex/sysconfig.py +5 -3
- pex/third_party/__init__.py +1 -1
- pex/tools/commands/repository.py +47 -24
- pex/vendor/__init__.py +4 -9
- pex/vendor/__main__.py +62 -41
- pex/vendor/_vendored/ansicolors/.layout.json +1 -1
- pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.dist-info/RECORD +11 -0
- pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.pex-info/original-whl-info.json +1 -0
- pex/vendor/_vendored/appdirs/.layout.json +1 -1
- pex/vendor/_vendored/appdirs/appdirs-1.4.4.dist-info/RECORD +7 -0
- pex/vendor/_vendored/appdirs/appdirs-1.4.4.pex-info/original-whl-info.json +1 -0
- pex/vendor/_vendored/attrs/.layout.json +1 -1
- pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.dist-info/RECORD +37 -0
- pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.pex-info/original-whl-info.json +1 -0
- pex/vendor/_vendored/packaging_20_9/.layout.json +1 -1
- pex/vendor/_vendored/packaging_20_9/packaging-20.9.dist-info/RECORD +20 -0
- pex/vendor/_vendored/packaging_20_9/packaging-20.9.pex-info/original-whl-info.json +1 -0
- pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.dist-info/RECORD +7 -0
- pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.pex-info/original-whl-info.json +1 -0
- pex/vendor/_vendored/packaging_21_3/.layout.json +1 -1
- pex/vendor/_vendored/packaging_21_3/packaging-21.3.dist-info/RECORD +20 -0
- pex/vendor/_vendored/packaging_21_3/packaging-21.3.pex-info/original-whl-info.json +1 -0
- pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.dist-info/RECORD +18 -0
- pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.pex-info/original-whl-info.json +1 -0
- pex/vendor/_vendored/packaging_24_0/.layout.json +1 -1
- pex/vendor/_vendored/packaging_24_0/packaging-24.0.dist-info/RECORD +22 -0
- pex/vendor/_vendored/packaging_24_0/packaging-24.0.pex-info/original-whl-info.json +1 -0
- pex/vendor/_vendored/packaging_25_0/.layout.json +1 -1
- pex/vendor/_vendored/packaging_25_0/packaging-25.0.dist-info/RECORD +24 -0
- pex/vendor/_vendored/packaging_25_0/packaging-25.0.pex-info/original-whl-info.json +1 -0
- pex/vendor/_vendored/pip/.layout.json +1 -1
- pex/vendor/_vendored/pip/pip/_vendor/certifi/cacert.pem +63 -1
- pex/vendor/_vendored/pip/pip-20.3.4.dist-info/RECORD +388 -0
- pex/vendor/_vendored/pip/pip-20.3.4.pex-info/original-whl-info.json +1 -0
- pex/vendor/_vendored/setuptools/.layout.json +1 -1
- pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.dist-info/RECORD +107 -0
- pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.pex-info/original-whl-info.json +1 -0
- pex/vendor/_vendored/toml/.layout.json +1 -1
- pex/vendor/_vendored/toml/toml-0.10.2.dist-info/RECORD +11 -0
- pex/vendor/_vendored/toml/toml-0.10.2.pex-info/original-whl-info.json +1 -0
- pex/vendor/_vendored/tomli/.layout.json +1 -1
- pex/vendor/_vendored/tomli/tomli-2.0.1.dist-info/RECORD +10 -0
- pex/vendor/_vendored/tomli/tomli-2.0.1.pex-info/original-whl-info.json +1 -0
- pex/venv/installer.py +9 -5
- pex/version.py +1 -1
- pex/wheel.py +79 -15
- pex/whl.py +67 -0
- pex/windows/__init__.py +14 -11
- {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/METADATA +4 -4
- {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/RECORD +93 -77
- pex/docs/html/_pagefind/fragment/en_144b803.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_1df1379.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_2c6c6cb.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_a916b1c.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_b33e5d4.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_c1c571a.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_fda06e7.pf_fragment +0 -0
- pex/docs/html/_pagefind/pagefind.en_84c8322e7a.pf_meta +0 -0
- pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.dist-info/INSTALLER +0 -1
- pex/vendor/_vendored/appdirs/appdirs-1.4.4.dist-info/INSTALLER +0 -1
- pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.dist-info/INSTALLER +0 -1
- pex/vendor/_vendored/packaging_20_9/packaging-20.9.dist-info/INSTALLER +0 -1
- pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.dist-info/INSTALLER +0 -1
- pex/vendor/_vendored/packaging_21_3/packaging-21.3.dist-info/INSTALLER +0 -1
- pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.dist-info/INSTALLER +0 -1
- pex/vendor/_vendored/packaging_24_0/packaging-24.0.dist-info/INSTALLER +0 -1
- pex/vendor/_vendored/packaging_25_0/packaging-25.0.dist-info/INSTALLER +0 -1
- pex/vendor/_vendored/pip/pip-20.3.4.dist-info/INSTALLER +0 -1
- pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.dist-info/INSTALLER +0 -1
- pex/vendor/_vendored/toml/toml-0.10.2.dist-info/INSTALLER +0 -1
- pex/vendor/_vendored/tomli/tomli-2.0.1.dist-info/INSTALLER +0 -1
- {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/WHEEL +0 -0
- {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/entry_points.txt +0 -0
- {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/licenses/LICENSE +0 -0
- {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/pylock/pylock.toml +0 -0
- {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/top_level.txt +0 -0
pex/pep_427.py
CHANGED
|
@@ -3,53 +3,66 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import absolute_import, print_function
|
|
5
5
|
|
|
6
|
+
import errno
|
|
6
7
|
import itertools
|
|
8
|
+
import json
|
|
7
9
|
import os.path
|
|
8
10
|
import re
|
|
9
11
|
import shutil
|
|
10
12
|
import subprocess
|
|
11
13
|
import sys
|
|
14
|
+
import time
|
|
12
15
|
import zipfile
|
|
13
|
-
|
|
14
|
-
from
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
|
|
17
|
+
from pex import pex_warnings
|
|
18
|
+
from pex.common import (
|
|
19
|
+
CopyMode,
|
|
20
|
+
ZipFileType,
|
|
21
|
+
deterministic_walk,
|
|
22
|
+
open_zip,
|
|
23
|
+
safe_copy,
|
|
24
|
+
safe_mkdir,
|
|
25
|
+
safe_mkdtemp,
|
|
26
|
+
safe_open,
|
|
27
|
+
safe_relative_symlink,
|
|
28
|
+
touch,
|
|
26
29
|
)
|
|
30
|
+
from pex.compatibility import commonpath, string
|
|
31
|
+
from pex.dist_metadata import DistMetadata, Distribution, MetadataFiles
|
|
32
|
+
from pex.entry_points_txt import install_scripts
|
|
27
33
|
from pex.enum import Enum
|
|
28
|
-
from pex.exceptions import reportable_unexpected_error_msg
|
|
29
|
-
from pex.executables import chmod_plus_x
|
|
34
|
+
from pex.exceptions import production_assert, reportable_unexpected_error_msg
|
|
35
|
+
from pex.executables import chmod_plus_x, is_python_script
|
|
36
|
+
from pex.installed_wheel import InstalledWheel
|
|
30
37
|
from pex.interpreter import PythonInterpreter
|
|
31
|
-
from pex.
|
|
32
|
-
from pex.
|
|
38
|
+
from pex.pep_376 import InstalledFile, Record, create_installed_file
|
|
39
|
+
from pex.pep_440 import Version
|
|
33
40
|
from pex.pep_503 import ProjectName
|
|
34
|
-
from pex.sysconfig import SCRIPT_DIR
|
|
41
|
+
from pex.sysconfig import SCRIPT_DIR, SysPlatform
|
|
35
42
|
from pex.typing import TYPE_CHECKING, cast
|
|
43
|
+
from pex.venv.virtualenv import Virtualenv
|
|
36
44
|
from pex.wheel import Wheel
|
|
37
45
|
|
|
38
46
|
if TYPE_CHECKING:
|
|
39
47
|
from typing import ( # noqa
|
|
48
|
+
Any,
|
|
40
49
|
Callable,
|
|
41
50
|
DefaultDict,
|
|
51
|
+
Dict,
|
|
42
52
|
Iterable,
|
|
43
53
|
Iterator,
|
|
44
54
|
List,
|
|
45
55
|
Mapping,
|
|
46
56
|
Optional,
|
|
57
|
+
Set,
|
|
47
58
|
Text,
|
|
48
59
|
Tuple,
|
|
49
60
|
Union,
|
|
50
61
|
)
|
|
51
62
|
|
|
52
63
|
import attr # vendor:skip
|
|
64
|
+
|
|
65
|
+
from pex.installed_wheel import InstalledWheel # noqa
|
|
53
66
|
else:
|
|
54
67
|
from pex.third_party import attr
|
|
55
68
|
|
|
@@ -69,38 +82,131 @@ class InstallableType(Enum["InstallableType.Value"]):
|
|
|
69
82
|
InstallableType.seal()
|
|
70
83
|
|
|
71
84
|
|
|
85
|
+
def _headers_install_path_for_wheel(
|
|
86
|
+
base, # type: str
|
|
87
|
+
wheel, # type: Wheel
|
|
88
|
+
):
|
|
89
|
+
# type: (...) -> str
|
|
90
|
+
|
|
91
|
+
major = "X" # type: Any
|
|
92
|
+
minor = "Y" # type: Any
|
|
93
|
+
compatible_python_versions = tuple(frozenset(wheel.iter_compatible_python_versions()))
|
|
94
|
+
if len(compatible_python_versions) == 1 and len(compatible_python_versions[0]) >= 2:
|
|
95
|
+
major, minor = compatible_python_versions[0][:2]
|
|
96
|
+
|
|
97
|
+
return _headers_install_path(base, version=(major, minor), project_name=wheel.project_name)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _headers_install_path(
|
|
101
|
+
base, # type: str
|
|
102
|
+
version, # type: Tuple[Any, Any]
|
|
103
|
+
project_name, # type: ProjectName
|
|
104
|
+
):
|
|
105
|
+
# type: (...) -> str
|
|
106
|
+
|
|
107
|
+
# N.B.: You'd think sysconfig_paths["include"] would be the right answer here but both
|
|
108
|
+
# `pip`, and by emulation, `uv pip`, use `<venv>/include/site/pythonX.Y/<project name>`.
|
|
109
|
+
#
|
|
110
|
+
# The "mess" is admitted and described at length here:
|
|
111
|
+
# + https://discuss.python.org/t/clarification-on-a-wheels-header-data/9305
|
|
112
|
+
# + https://discuss.python.org/t/deprecating-the-headers-wheel-data-key/23712
|
|
113
|
+
#
|
|
114
|
+
# Both discussions died out with no path resolved to clean up the mess.
|
|
115
|
+
|
|
116
|
+
return os.path.join(
|
|
117
|
+
base,
|
|
118
|
+
"include",
|
|
119
|
+
"site",
|
|
120
|
+
"python{major}.{minor}".format(major=version[0], minor=version[1]),
|
|
121
|
+
project_name.raw,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
72
125
|
@attr.s(frozen=True)
|
|
73
126
|
class InstallPaths(object):
|
|
74
127
|
|
|
75
128
|
CHROOT_STASH = ".prefix"
|
|
76
|
-
PATHS = "purelib", "platlib", "headers", "scripts", "data"
|
|
77
129
|
|
|
78
130
|
@classmethod
|
|
79
131
|
def chroot(
|
|
80
132
|
cls,
|
|
81
133
|
destination, # type: str
|
|
82
|
-
|
|
134
|
+
wheel, # type: Wheel
|
|
83
135
|
):
|
|
84
136
|
# type: (...) -> InstallPaths
|
|
85
137
|
base = os.path.join(destination, cls.CHROOT_STASH)
|
|
138
|
+
|
|
86
139
|
return cls(
|
|
87
140
|
purelib=destination,
|
|
88
141
|
platlib=destination,
|
|
89
|
-
headers=
|
|
142
|
+
headers=_headers_install_path_for_wheel(base, wheel),
|
|
90
143
|
scripts=os.path.join(base, SCRIPT_DIR),
|
|
91
144
|
data=base,
|
|
145
|
+
path_names=("headers", "scripts", "data", "purelib", "platlib"),
|
|
92
146
|
)
|
|
93
147
|
|
|
94
148
|
@classmethod
|
|
95
|
-
def interpreter(
|
|
96
|
-
|
|
149
|
+
def interpreter(
|
|
150
|
+
cls,
|
|
151
|
+
interpreter, # type: PythonInterpreter
|
|
152
|
+
project_name, # type: ProjectName
|
|
153
|
+
):
|
|
154
|
+
# type: (...) -> InstallPaths
|
|
97
155
|
sysconfig_paths = interpreter.identity.paths
|
|
98
156
|
return cls(
|
|
99
157
|
purelib=sysconfig_paths["purelib"],
|
|
100
158
|
platlib=sysconfig_paths["platlib"],
|
|
101
|
-
headers=
|
|
159
|
+
headers=_headers_install_path(
|
|
160
|
+
interpreter.prefix,
|
|
161
|
+
version=(interpreter.version[0], interpreter.version[1]),
|
|
162
|
+
project_name=project_name,
|
|
163
|
+
),
|
|
102
164
|
scripts=sysconfig_paths["scripts"],
|
|
103
165
|
data=sysconfig_paths["data"],
|
|
166
|
+
path_names=("purelib", "platlib", "headers", "scripts", "data"),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def flat(
|
|
171
|
+
cls,
|
|
172
|
+
destination, # type: str
|
|
173
|
+
wheel, # type: Wheel
|
|
174
|
+
):
|
|
175
|
+
# type: (...) -> InstallPaths
|
|
176
|
+
return cls(
|
|
177
|
+
purelib=destination,
|
|
178
|
+
platlib=destination,
|
|
179
|
+
headers=_headers_install_path_for_wheel(destination, wheel),
|
|
180
|
+
scripts=os.path.join(destination, SCRIPT_DIR),
|
|
181
|
+
data=destination,
|
|
182
|
+
path_names=("headers", "scripts", "data", "purelib", "platlib"),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def wheel(
|
|
187
|
+
cls,
|
|
188
|
+
destination, # type: str
|
|
189
|
+
project_name, # type: ProjectName
|
|
190
|
+
version, # type: Version
|
|
191
|
+
):
|
|
192
|
+
# type: (...) -> InstallPaths
|
|
193
|
+
|
|
194
|
+
data = os.path.join(
|
|
195
|
+
destination,
|
|
196
|
+
"{project_name}-{version}.data".format(
|
|
197
|
+
# N.B.: We don't use the canonical form since it goes to lowercase.
|
|
198
|
+
project_name=re.sub(r"[-_.]+", "_", project_name.raw),
|
|
199
|
+
# N.B.: We don't use the canonical form since it drop trailing zero segments.
|
|
200
|
+
version=version.raw.replace("-", "_"),
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
return cls(
|
|
204
|
+
purelib=destination,
|
|
205
|
+
platlib=destination,
|
|
206
|
+
headers=os.path.join(data, "headers"),
|
|
207
|
+
scripts=os.path.join(data, "scripts"),
|
|
208
|
+
data=os.path.join(data, "data"),
|
|
209
|
+
path_names=("headers", "scripts", "data", "purelib", "platlib"),
|
|
104
210
|
)
|
|
105
211
|
|
|
106
212
|
purelib = attr.ib() # type: str
|
|
@@ -108,6 +214,7 @@ class InstallPaths(object):
|
|
|
108
214
|
headers = attr.ib() # type: str
|
|
109
215
|
scripts = attr.ib() # type: str
|
|
110
216
|
data = attr.ib() # type: str
|
|
217
|
+
_path_names = attr.ib() # type: Tuple[str, ...]
|
|
111
218
|
|
|
112
219
|
def __getitem__(self, item):
|
|
113
220
|
# type: (Text) -> str
|
|
@@ -123,37 +230,237 @@ class InstallPaths(object):
|
|
|
123
230
|
return self.data
|
|
124
231
|
raise KeyError("Not a known install path: {item}".format(item=item))
|
|
125
232
|
|
|
233
|
+
def __iter__(self):
|
|
234
|
+
# type: () -> Iterator[Tuple[str, str]]
|
|
235
|
+
for path_name in self._path_names:
|
|
236
|
+
yield path_name, self[path_name]
|
|
237
|
+
|
|
126
238
|
def __str__(self):
|
|
127
239
|
# type: () -> str
|
|
128
240
|
return "\n".join(
|
|
129
|
-
"{path}={value}".format(path=
|
|
241
|
+
"{path}={value}".format(path=path_name, value=value) for path_name, value in self
|
|
130
242
|
)
|
|
131
243
|
|
|
132
244
|
|
|
133
245
|
@attr.s(frozen=True)
|
|
134
|
-
class
|
|
246
|
+
class ZipEntryInfo(object):
|
|
135
247
|
@classmethod
|
|
136
|
-
def
|
|
248
|
+
def from_zip_info(
|
|
137
249
|
cls,
|
|
138
|
-
|
|
139
|
-
|
|
250
|
+
zip_info, # type: zipfile.ZipInfo
|
|
251
|
+
normalize_file_stat=False, # type: bool
|
|
140
252
|
):
|
|
141
|
-
# type: (...) ->
|
|
253
|
+
# type: (...) -> ZipEntryInfo
|
|
142
254
|
return cls(
|
|
143
|
-
|
|
144
|
-
|
|
255
|
+
filename=zip_info.filename,
|
|
256
|
+
date_time=zip_info.date_time,
|
|
257
|
+
external_attr=(
|
|
258
|
+
ZipFileType.from_zip_info(zip_info).deterministic_external_attr
|
|
259
|
+
if normalize_file_stat
|
|
260
|
+
else zip_info.external_attr
|
|
261
|
+
),
|
|
145
262
|
)
|
|
146
263
|
|
|
147
264
|
@classmethod
|
|
148
|
-
def
|
|
149
|
-
# type: (
|
|
150
|
-
|
|
265
|
+
def from_json(cls, data):
|
|
266
|
+
# type: (Any) -> ZipEntryInfo
|
|
267
|
+
|
|
268
|
+
if not isinstance(data, list) or not len(data) == 3:
|
|
269
|
+
raise ValueError(
|
|
270
|
+
"Invalid ZipEntryInfo JSON data. Expected a 3-item list, given {value} of type "
|
|
271
|
+
"{type}.".format(value=data, type=type(data))
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
filename, date_time, external_attr = data
|
|
275
|
+
if not isinstance(filename, string):
|
|
276
|
+
raise ValueError(
|
|
277
|
+
"Invalid ZipEntryInfo JSON data. Expected a `filename` string property; found "
|
|
278
|
+
"{value} of type {type}.".format(value=filename, type=type(filename))
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if (
|
|
282
|
+
not isinstance(date_time, list)
|
|
283
|
+
or not len(date_time) == 6
|
|
284
|
+
or not all(isinstance(component, int) for component in date_time)
|
|
285
|
+
):
|
|
286
|
+
raise ValueError(
|
|
287
|
+
"Invalid ZipEntryInfo JSON data. Expected a `date_time` list of six integers "
|
|
288
|
+
"property; found {value} of type {type}.".format(
|
|
289
|
+
value=date_time, type=type(date_time)
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if not isinstance(external_attr, int):
|
|
294
|
+
raise ValueError(
|
|
295
|
+
"Invalid ZipEntryInfo JSON data. Expected an `external_attr` integer property; "
|
|
296
|
+
"found {value} of type {type}.".format(
|
|
297
|
+
value=external_attr, type=type(external_attr)
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return cls(
|
|
302
|
+
filename=filename,
|
|
303
|
+
date_time=cast("Tuple[int, int, int, int, int, int]", tuple(date_time)),
|
|
304
|
+
external_attr=external_attr,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
filename = attr.ib() # type: Text
|
|
308
|
+
date_time = attr.ib() # type: Tuple[int, int, int, int, int, int]
|
|
309
|
+
external_attr = attr.ib() # type: int
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def is_dir(self):
|
|
313
|
+
# type: () -> bool
|
|
314
|
+
return self.filename.endswith("/")
|
|
315
|
+
|
|
316
|
+
def date_time_as_struct_time(self):
|
|
317
|
+
# type: () -> time.struct_time
|
|
318
|
+
return time.struct_time(self.date_time + (0, 0, -1))
|
|
319
|
+
|
|
320
|
+
def external_attr_as_stat_mode(self):
|
|
321
|
+
# type: () -> int
|
|
322
|
+
return self.external_attr >> 16
|
|
323
|
+
|
|
324
|
+
def to_json(self):
|
|
325
|
+
# type: () -> Any
|
|
326
|
+
return self.filename, self.date_time, self.external_attr
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@attr.s(frozen=True)
|
|
330
|
+
class ZipMetadata(object):
|
|
331
|
+
FILENAME = "original-whl-info.json"
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def from_zip(
|
|
335
|
+
cls,
|
|
336
|
+
filename, # type: str
|
|
337
|
+
info_list, # type: Iterable[zipfile.ZipInfo]
|
|
338
|
+
normalize_file_stat=False, # type: bool
|
|
339
|
+
):
|
|
340
|
+
# type: (...) -> ZipMetadata
|
|
341
|
+
return cls(
|
|
342
|
+
filename=os.path.basename(filename),
|
|
343
|
+
entry_info=tuple(
|
|
344
|
+
ZipEntryInfo.from_zip_info(zip_info, normalize_file_stat=normalize_file_stat)
|
|
345
|
+
for zip_info in info_list
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
@classmethod
|
|
350
|
+
def read(cls, wheel):
|
|
351
|
+
# type: (Wheel) -> Optional[ZipMetadata]
|
|
352
|
+
|
|
353
|
+
data = wheel.read_pex_metadata(cls.FILENAME)
|
|
354
|
+
if not data:
|
|
355
|
+
return None
|
|
356
|
+
zip_metadata = json.loads(data)
|
|
357
|
+
if not isinstance(zip_metadata, dict):
|
|
358
|
+
raise ValueError(
|
|
359
|
+
"Invalid ZipMetadata JSON data. Expected an object; found "
|
|
360
|
+
"{value} of type {type}.".format(value=zip_metadata, type=type(zip_metadata))
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
filename = zip_metadata.pop("filename", None)
|
|
364
|
+
if not isinstance(filename, string):
|
|
365
|
+
raise ValueError(
|
|
366
|
+
"Invalid ZipMetadata JSON data. Expected an object with a string-valued 'filename' "
|
|
367
|
+
"property; instead found {value} of type {type}.".format(
|
|
368
|
+
value=zip_metadata, type=type(zip_metadata)
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
entries = zip_metadata.pop("entries", None)
|
|
373
|
+
if not isinstance(entries, list):
|
|
374
|
+
raise ValueError(
|
|
375
|
+
"Invalid ZipMetadata JSON data. Expected an object with a list-valued 'entries' "
|
|
376
|
+
"property; instead found {value} of type {type}.".format(
|
|
377
|
+
value=zip_metadata, type=type(zip_metadata)
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if zip_metadata:
|
|
382
|
+
raise ValueError(
|
|
383
|
+
"Invalid ZipMetadata JSON data. Unrecognized object keys: {keys}".format(
|
|
384
|
+
keys=", ".join(zip_metadata)
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return cls(
|
|
389
|
+
filename=filename,
|
|
390
|
+
entry_info=tuple(ZipEntryInfo.from_json(zip_entry_info) for zip_entry_info in entries),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
filename = attr.ib() # type: str
|
|
394
|
+
entry_info = attr.ib() # type: Tuple[ZipEntryInfo, ...]
|
|
395
|
+
|
|
396
|
+
def __iter__(self):
|
|
397
|
+
# type: () -> Iterator[ZipEntryInfo]
|
|
398
|
+
return iter(self.entry_info)
|
|
399
|
+
|
|
400
|
+
def write(
|
|
401
|
+
self,
|
|
402
|
+
dest, # type: str
|
|
403
|
+
wheel, # type: Wheel
|
|
404
|
+
):
|
|
405
|
+
# type: (...) -> str
|
|
406
|
+
path = os.path.join(dest, wheel.pex_metadata_path(self.FILENAME))
|
|
407
|
+
with safe_open(path, "w") as fp:
|
|
408
|
+
json.dump(
|
|
409
|
+
{
|
|
410
|
+
"filename": self.filename,
|
|
411
|
+
"entries": [entry_info.to_json() for entry_info in self.entry_info],
|
|
412
|
+
},
|
|
413
|
+
fp,
|
|
414
|
+
sort_keys=True,
|
|
415
|
+
separators=(",", ":"),
|
|
416
|
+
)
|
|
417
|
+
return path
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@attr.s(frozen=True)
|
|
421
|
+
class InstallableWheel(object):
|
|
422
|
+
@classmethod
|
|
423
|
+
def from_whl(
|
|
424
|
+
cls,
|
|
425
|
+
whl, # type: Union[str, Wheel]
|
|
426
|
+
install_paths=None, # type: Optional[InstallPaths]
|
|
427
|
+
):
|
|
428
|
+
# type: (...) -> InstallableWheel
|
|
429
|
+
wheel = whl if isinstance(whl, Wheel) else Wheel.load(whl)
|
|
430
|
+
zip_metadata = ZipMetadata.read(wheel)
|
|
431
|
+
return cls(wheel=wheel, install_paths=install_paths, zip_metadata=zip_metadata)
|
|
432
|
+
|
|
433
|
+
@classmethod
|
|
434
|
+
def from_installed_wheel(cls, installed_wheel):
|
|
435
|
+
# type: (InstalledWheel) -> InstallableWheel
|
|
436
|
+
wheel = Wheel.load(installed_wheel.prefix_dir)
|
|
437
|
+
return cls.from_whl(
|
|
438
|
+
whl=wheel, install_paths=InstallPaths.chroot(installed_wheel.prefix_dir, wheel=wheel)
|
|
439
|
+
)
|
|
151
440
|
|
|
152
441
|
wheel = attr.ib() # type: Wheel
|
|
153
442
|
is_whl = attr.ib(init=False) # type: bool
|
|
154
443
|
install_paths = attr.ib(default=None) # type: Optional[InstallPaths]
|
|
444
|
+
zip_metadata = attr.ib(default=None) # type: Optional[ZipMetadata]
|
|
445
|
+
|
|
446
|
+
def record_zip_metadata(self, dest):
|
|
447
|
+
# type: (str) -> Optional[str]
|
|
448
|
+
if self.zip_metadata:
|
|
449
|
+
return self.zip_metadata.write(dest, self.wheel)
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def project_name(self):
|
|
454
|
+
# type: () -> ProjectName
|
|
455
|
+
return self.wheel.project_name
|
|
456
|
+
|
|
457
|
+
@property
|
|
458
|
+
def version(self):
|
|
459
|
+
# type: () -> Version
|
|
460
|
+
return self.wheel.version
|
|
155
461
|
|
|
156
462
|
def __attrs_post_init__(self):
|
|
463
|
+
# type: () -> None
|
|
157
464
|
is_whl = zipfile.is_zipfile(self.wheel.location)
|
|
158
465
|
|
|
159
466
|
if is_whl and self.install_paths:
|
|
@@ -174,6 +481,18 @@ class InstallableWheel(object):
|
|
|
174
481
|
|
|
175
482
|
object.__setattr__(self, "is_whl", is_whl)
|
|
176
483
|
|
|
484
|
+
def iter_applicable_install_paths(self):
|
|
485
|
+
# type: () -> Iterator[Tuple[str, str]]
|
|
486
|
+
if self.install_paths:
|
|
487
|
+
for path_name, path in self.install_paths:
|
|
488
|
+
if path_name == "purelib":
|
|
489
|
+
if not self.root_is_purelib:
|
|
490
|
+
continue
|
|
491
|
+
elif path_name == "platlib":
|
|
492
|
+
if self.root_is_purelib:
|
|
493
|
+
continue
|
|
494
|
+
yield path_name, path
|
|
495
|
+
|
|
177
496
|
@property
|
|
178
497
|
def location(self):
|
|
179
498
|
# type: () -> str
|
|
@@ -202,7 +521,7 @@ class InstallableWheel(object):
|
|
|
202
521
|
@property
|
|
203
522
|
def wheel_file_name(self):
|
|
204
523
|
# type: () -> str
|
|
205
|
-
return self.wheel.wheel_file_name
|
|
524
|
+
return self.zip_metadata.filename if self.zip_metadata else self.wheel.wheel_file_name
|
|
206
525
|
|
|
207
526
|
def dist_metadata(self):
|
|
208
527
|
# type: () -> DistMetadata
|
|
@@ -212,16 +531,92 @@ class InstallableWheel(object):
|
|
|
212
531
|
# type: (*str) -> str
|
|
213
532
|
return self.wheel.metadata_path(*components)
|
|
214
533
|
|
|
534
|
+
def distribution(self):
|
|
535
|
+
# type: () -> Distribution
|
|
536
|
+
return Distribution(location=self.location, metadata=self.dist_metadata())
|
|
537
|
+
|
|
538
|
+
def pex_metadata_path(self, *components):
|
|
539
|
+
# type: (*str) -> str
|
|
540
|
+
return self.wheel.pex_metadata_path(*components)
|
|
541
|
+
|
|
215
542
|
|
|
216
543
|
class WheelInstallError(WheelError):
|
|
217
544
|
"""Indicates an error installing a `.whl` file."""
|
|
218
545
|
|
|
219
546
|
|
|
547
|
+
def reinstall_flat(
|
|
548
|
+
installed_wheel, # type: InstalledWheel
|
|
549
|
+
target_dir, # type: str
|
|
550
|
+
copy_mode=CopyMode.LINK, # type: CopyMode.Value
|
|
551
|
+
):
|
|
552
|
+
# type: (...) -> Iterator[Tuple[Text, Text]]
|
|
553
|
+
"""Re-installs the installed wheel in a flat target directory.
|
|
554
|
+
|
|
555
|
+
N.B.: A record of reinstalled files is returned in the form of an iterator that must be
|
|
556
|
+
consumed to drive the installation to completion.
|
|
557
|
+
|
|
558
|
+
If there is an error re-installing a file due to it already existing in the target
|
|
559
|
+
directory, the error is suppressed, and it's expected that the caller detects this by
|
|
560
|
+
comparing the record of installed files against those installed previously.
|
|
561
|
+
|
|
562
|
+
:return: An iterator over src -> dst pairs.
|
|
563
|
+
"""
|
|
564
|
+
for src, dst in install_wheel_flat(
|
|
565
|
+
wheel=InstallableWheel.from_installed_wheel(installed_wheel),
|
|
566
|
+
destination=target_dir,
|
|
567
|
+
copy_mode=copy_mode,
|
|
568
|
+
):
|
|
569
|
+
yield src, dst
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def reinstall_venv(
|
|
573
|
+
installed_wheel, # type: InstalledWheel
|
|
574
|
+
venv, # type: Virtualenv
|
|
575
|
+
copy_mode=CopyMode.LINK, # type: CopyMode.Value
|
|
576
|
+
rel_extra_path=None, # type: Optional[str]
|
|
577
|
+
):
|
|
578
|
+
# type: (...) -> Iterator[Tuple[Text, Text]]
|
|
579
|
+
"""Re-installs the installed wheel in a venv.
|
|
580
|
+
|
|
581
|
+
N.B.: A record of reinstalled files is returned in the form of an iterator that must be
|
|
582
|
+
consumed to drive the installation to completion.
|
|
583
|
+
|
|
584
|
+
If there is an error re-installing a file due to it already existing in the destination
|
|
585
|
+
venv, the error is suppressed, and it's expected that the caller detects this by comparing
|
|
586
|
+
the record of installed files against those installed previously.
|
|
587
|
+
|
|
588
|
+
:return: An iterator over src -> dst pairs.
|
|
589
|
+
"""
|
|
590
|
+
|
|
591
|
+
for src, dst in install_wheel_interpreter(
|
|
592
|
+
wheel=InstallableWheel.from_installed_wheel(installed_wheel),
|
|
593
|
+
interpreter=venv.interpreter,
|
|
594
|
+
copy_mode=copy_mode,
|
|
595
|
+
rel_extra_path=rel_extra_path,
|
|
596
|
+
compile=False,
|
|
597
|
+
):
|
|
598
|
+
yield src, dst
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def repack(
|
|
602
|
+
installed_wheel, # type: InstalledWheel
|
|
603
|
+
dest_dir, # type: str
|
|
604
|
+
use_system_time=False, # type: bool
|
|
605
|
+
override_wheel_file_name=None, # type: Optional[str]
|
|
606
|
+
):
|
|
607
|
+
# type: (...) -> str
|
|
608
|
+
return create_whl(
|
|
609
|
+
wheel=InstallableWheel.from_installed_wheel(installed_wheel),
|
|
610
|
+
destination=dest_dir,
|
|
611
|
+
use_system_time=use_system_time,
|
|
612
|
+
override_wheel_file_name=override_wheel_file_name,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
|
|
220
616
|
def install_wheel_chroot(
|
|
221
617
|
wheel, # type: Union[str, InstallableWheel]
|
|
222
618
|
destination, # type: str
|
|
223
|
-
|
|
224
|
-
requested=True, # type: bool
|
|
619
|
+
normalize_file_stat=False, # type: bool
|
|
225
620
|
):
|
|
226
621
|
# type: (...) -> InstalledWheel
|
|
227
622
|
|
|
@@ -230,17 +625,16 @@ def install_wheel_chroot(
|
|
|
230
625
|
)
|
|
231
626
|
install_wheel(
|
|
232
627
|
wheel_to_install,
|
|
233
|
-
InstallPaths.chroot(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
compile=compile,
|
|
237
|
-
requested=requested,
|
|
628
|
+
InstallPaths.chroot(destination, wheel=wheel_to_install.wheel),
|
|
629
|
+
record_entry_info=True,
|
|
630
|
+
normalize_file_stat=normalize_file_stat,
|
|
238
631
|
)
|
|
239
632
|
|
|
240
633
|
record_relpath = wheel_to_install.metadata_files.metadata_file_rel_path("RECORD")
|
|
241
634
|
assert (
|
|
242
635
|
record_relpath is not None
|
|
243
636
|
), "The {module}.install_wheel function should always create a RECORD.".format(module=__name__)
|
|
637
|
+
|
|
244
638
|
return InstalledWheel.save(
|
|
245
639
|
prefix_dir=destination,
|
|
246
640
|
stash_dir=InstallPaths.CHROOT_STASH,
|
|
@@ -252,167 +646,324 @@ def install_wheel_chroot(
|
|
|
252
646
|
def install_wheel_interpreter(
|
|
253
647
|
wheel, # type: Union[str, InstallableWheel]
|
|
254
648
|
interpreter, # type: PythonInterpreter
|
|
649
|
+
copy_mode=CopyMode.LINK, # type: CopyMode.Value
|
|
650
|
+
rel_extra_path=None, # type: Optional[str]
|
|
255
651
|
compile=True, # type: bool
|
|
256
652
|
requested=True, # type: bool
|
|
257
653
|
):
|
|
258
|
-
# type: (...) ->
|
|
654
|
+
# type: (...) -> Tuple[Tuple[Text, Text], ...]
|
|
259
655
|
|
|
260
656
|
wheel_to_install = (
|
|
261
657
|
wheel if isinstance(wheel, InstallableWheel) else InstallableWheel.from_whl(wheel)
|
|
262
658
|
)
|
|
263
|
-
install_wheel(
|
|
659
|
+
return install_wheel(
|
|
264
660
|
wheel_to_install,
|
|
265
|
-
InstallPaths.interpreter(
|
|
661
|
+
InstallPaths.interpreter(
|
|
662
|
+
interpreter, project_name=wheel_to_install.metadata_files.metadata.project_name
|
|
663
|
+
),
|
|
664
|
+
copy_mode=copy_mode,
|
|
266
665
|
interpreter=interpreter,
|
|
666
|
+
rel_extra_path=rel_extra_path,
|
|
267
667
|
compile=compile,
|
|
268
668
|
requested=requested,
|
|
669
|
+
record_entry_info=True,
|
|
269
670
|
)
|
|
270
671
|
|
|
271
672
|
|
|
673
|
+
def install_wheel_flat(
|
|
674
|
+
wheel, # type: Union[str, InstallableWheel]
|
|
675
|
+
destination, # type: str
|
|
676
|
+
copy_mode=CopyMode.LINK, # type: CopyMode.Value
|
|
677
|
+
compile=False, # type: bool
|
|
678
|
+
):
|
|
679
|
+
# type: (...) -> Tuple[Tuple[Text, Text], ...]
|
|
680
|
+
|
|
681
|
+
wheel_to_install = (
|
|
682
|
+
wheel if isinstance(wheel, InstallableWheel) else InstallableWheel.from_whl(wheel)
|
|
683
|
+
)
|
|
684
|
+
return install_wheel(
|
|
685
|
+
wheel_to_install,
|
|
686
|
+
InstallPaths.flat(destination, wheel=wheel_to_install.wheel),
|
|
687
|
+
copy_mode=copy_mode,
|
|
688
|
+
compile=compile,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def create_whl(
|
|
693
|
+
wheel, # type: Union[str, InstallableWheel]
|
|
694
|
+
destination, # type: str
|
|
695
|
+
compile=False, # type: bool
|
|
696
|
+
use_system_time=False, # type: bool
|
|
697
|
+
override_wheel_file_name=None, # type: Optional[str]
|
|
698
|
+
):
|
|
699
|
+
# type: (...) -> str
|
|
700
|
+
|
|
701
|
+
if not isinstance(wheel, InstallableWheel) and zipfile.is_zipfile(wheel):
|
|
702
|
+
wheel_dst = os.path.join(destination, os.path.basename(wheel))
|
|
703
|
+
safe_copy(wheel, wheel_dst)
|
|
704
|
+
return wheel_dst
|
|
705
|
+
|
|
706
|
+
wheel_to_create = (
|
|
707
|
+
wheel if isinstance(wheel, InstallableWheel) else InstallableWheel.from_whl(wheel)
|
|
708
|
+
)
|
|
709
|
+
whl_file_name = override_wheel_file_name or wheel_to_create.wheel_file_name
|
|
710
|
+
whl_chroot = os.path.join(safe_mkdtemp(prefix="pex_create_whl."), whl_file_name)
|
|
711
|
+
install_wheel(
|
|
712
|
+
wheel_to_create,
|
|
713
|
+
InstallPaths.wheel(
|
|
714
|
+
destination=whl_chroot,
|
|
715
|
+
project_name=wheel_to_create.project_name,
|
|
716
|
+
version=wheel_to_create.version,
|
|
717
|
+
),
|
|
718
|
+
compile=compile,
|
|
719
|
+
install_entry_point_scripts=False,
|
|
720
|
+
)
|
|
721
|
+
record_data = Wheel.load(whl_chroot).metadata_files.read("RECORD")
|
|
722
|
+
if record_data is None:
|
|
723
|
+
raise AssertionError(reportable_unexpected_error_msg())
|
|
724
|
+
|
|
725
|
+
wheel_path = os.path.join(destination, whl_file_name)
|
|
726
|
+
with open_zip(wheel_path, "w") as zip_fp:
|
|
727
|
+
if use_system_time and wheel_to_create.zip_metadata:
|
|
728
|
+
for zip_entry_info in wheel_to_create.zip_metadata:
|
|
729
|
+
src = os.path.join(whl_chroot, zip_entry_info.filename)
|
|
730
|
+
if not os.path.exists(src):
|
|
731
|
+
production_assert(
|
|
732
|
+
zip_entry_info.is_dir,
|
|
733
|
+
"The wheel entry {filename} is unexpectedly missing from {source}.",
|
|
734
|
+
filename=zip_entry_info.filename,
|
|
735
|
+
source=wheel_to_create.source,
|
|
736
|
+
)
|
|
737
|
+
safe_mkdir(src)
|
|
738
|
+
zip_fp.write_ex(
|
|
739
|
+
src,
|
|
740
|
+
zip_entry_info.filename,
|
|
741
|
+
date_time=zip_entry_info.date_time_as_struct_time(),
|
|
742
|
+
file_mode=zip_entry_info.external_attr_as_stat_mode(),
|
|
743
|
+
)
|
|
744
|
+
else:
|
|
745
|
+
for installed_file in Record.read(lines=iter(record_data.decode("utf-8").splitlines())):
|
|
746
|
+
src = os.path.join(whl_chroot, installed_file.path)
|
|
747
|
+
if use_system_time:
|
|
748
|
+
zip_fp.write(src, installed_file.path)
|
|
749
|
+
else:
|
|
750
|
+
zip_fp.write_deterministic(src, installed_file.path)
|
|
751
|
+
return wheel_path
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _detect_record_eol(path):
|
|
755
|
+
# type: (Text) -> str
|
|
756
|
+
|
|
757
|
+
with open(path, "rb") as fp:
|
|
758
|
+
line = fp.readline()
|
|
759
|
+
return "\r\n" if line.endswith(b"\r\n") else "\n"
|
|
760
|
+
|
|
761
|
+
|
|
272
762
|
def install_wheel(
|
|
273
763
|
wheel, # type: InstallableWheel
|
|
274
764
|
install_paths, # type: InstallPaths
|
|
765
|
+
copy_mode=CopyMode.LINK, # type: CopyMode.Value
|
|
275
766
|
interpreter=None, # type: Optional[PythonInterpreter]
|
|
767
|
+
rel_extra_path=None, # type: Optional[str]
|
|
276
768
|
compile=False, # type: bool
|
|
277
769
|
requested=True, # type: bool
|
|
770
|
+
install_entry_point_scripts=True, # type: bool
|
|
771
|
+
record_entry_info=False, # type: bool
|
|
772
|
+
normalize_file_stat=False, # type: bool
|
|
278
773
|
):
|
|
279
|
-
# type: (...) ->
|
|
774
|
+
# type: (...) -> Tuple[Tuple[Text, Text], ...]
|
|
280
775
|
|
|
281
776
|
# See: https://packaging.python.org/en/latest/specifications/binary-distribution-format/#installing-a-wheel-distribution-1-0-py32-none-any-whl
|
|
282
|
-
dest = install_paths.purelib if wheel.root_is_purelib else install_paths.platlib
|
|
283
777
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def record_files(
|
|
293
|
-
root_dir, # type: Text
|
|
294
|
-
names, # type: Iterable[Text]
|
|
295
|
-
):
|
|
296
|
-
# type: (...) -> None
|
|
297
|
-
for name in sorted(names):
|
|
298
|
-
if is_pyc_file(name):
|
|
299
|
-
# These files are both optional to RECORD and should never be present in wheels
|
|
300
|
-
# anyway per the spec.
|
|
301
|
-
continue
|
|
302
|
-
file_abspath = os.path.join(root_dir, name)
|
|
303
|
-
if record_relpath == name:
|
|
304
|
-
# We'll generate a new RECORD below as needed.
|
|
305
|
-
os.unlink(file_abspath)
|
|
306
|
-
continue
|
|
307
|
-
installed_files.append(
|
|
308
|
-
InstalledWheel.create_installed_file(path=file_abspath, dest_dir=dest)
|
|
309
|
-
)
|
|
778
|
+
dest = install_paths.purelib if wheel.root_is_purelib else install_paths.platlib
|
|
779
|
+
if rel_extra_path:
|
|
780
|
+
dest = os.path.join(dest, rel_extra_path)
|
|
781
|
+
if wheel.root_is_purelib:
|
|
782
|
+
install_paths = attr.evolve(install_paths, purelib=dest)
|
|
783
|
+
else:
|
|
784
|
+
install_paths = attr.evolve(install_paths, platlib=dest)
|
|
310
785
|
|
|
311
786
|
if wheel.is_whl:
|
|
312
|
-
|
|
787
|
+
whl = wheel.location
|
|
788
|
+
zip_metadata = None # type: Optional[ZipMetadata]
|
|
789
|
+
with open_zip(whl) as zf:
|
|
313
790
|
# 1. Unpack
|
|
314
791
|
zf.extractall(dest)
|
|
792
|
+
if record_entry_info:
|
|
793
|
+
zip_metadata = ZipMetadata.from_zip(
|
|
794
|
+
filename=whl, info_list=zf.infolist(), normalize_file_stat=normalize_file_stat
|
|
795
|
+
)
|
|
796
|
+
|
|
315
797
|
# TODO(John Sirois): Consider verifying signatures.
|
|
316
798
|
# N.B.: Pip does not and its also not clear what good this does. A zip can be easily
|
|
317
799
|
# poked on a per-entry basis allowing forging a RECORD entry and its associated file.
|
|
318
800
|
# Only an outer fingerprint of the whole wheel really solves this sort of tampering.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
try:
|
|
332
|
-
dest_dir = install_paths[entry]
|
|
333
|
-
except KeyError as e:
|
|
334
|
-
raise WheelInstallError(
|
|
335
|
-
"The wheel at {wheel_path} is invalid and cannot be installed: "
|
|
336
|
-
"{err}".format(wheel_path=wheel.source, err=e)
|
|
337
|
-
)
|
|
338
|
-
entry_path = os.path.join(data_path, entry)
|
|
339
|
-
copied = [dst for _, dst in iter_copytree(entry_path, dest_dir)]
|
|
340
|
-
if copied and "scripts" == entry:
|
|
341
|
-
for script in copied:
|
|
342
|
-
chmod_plus_x(script)
|
|
343
|
-
if interpreter:
|
|
344
|
-
with closing(
|
|
345
|
-
FileInput(files=copied, inplace=True, mode="rb")
|
|
346
|
-
) as script_fi:
|
|
347
|
-
for line in cast("Iterator[bytes]", script_fi):
|
|
348
|
-
buffer = get_stdout_bytes_buffer()
|
|
349
|
-
if script_fi.isfirstline() and re.match(br"^#!pythonw?", line):
|
|
350
|
-
_, _, shebang_args = line.partition(b" ")
|
|
351
|
-
buffer.write(
|
|
352
|
-
"{shebang}\n".format(
|
|
353
|
-
shebang=interpreter.shebang(
|
|
354
|
-
args=shebang_args.decode("utf-8")
|
|
355
|
-
)
|
|
356
|
-
).encode("utf-8")
|
|
357
|
-
)
|
|
358
|
-
else:
|
|
359
|
-
# N.B.: These lines include the newline already.
|
|
360
|
-
buffer.write(cast(bytes, line))
|
|
361
|
-
|
|
362
|
-
record_files(
|
|
363
|
-
root_dir=dest_dir,
|
|
364
|
-
names=[
|
|
365
|
-
os.path.relpath(os.path.join(root, f), entry_path)
|
|
366
|
-
for root, _, files in os.walk(entry_path)
|
|
367
|
-
for f in files
|
|
368
|
-
],
|
|
369
|
-
)
|
|
370
|
-
shutil.rmtree(data_path)
|
|
371
|
-
elif wheel.install_paths:
|
|
801
|
+
|
|
802
|
+
wheel = InstallableWheel(
|
|
803
|
+
wheel=Wheel.load(dest, project_name=wheel.project_name),
|
|
804
|
+
install_paths=InstallPaths.wheel(
|
|
805
|
+
dest, project_name=wheel.project_name, version=wheel.version
|
|
806
|
+
),
|
|
807
|
+
zip_metadata=zip_metadata,
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
# Deal with bad whl `RECORD`s. We happen to hit one from selenium-4.1.2-py3-none-any.whl
|
|
811
|
+
# in our tests. The selenium >=4,<4.1.3 wheels are all published with absolute paths for
|
|
812
|
+
# all the .py file RECORD entries. The .dist-info and .data entries are fine though.
|
|
372
813
|
record_data = wheel.metadata_files.read("RECORD")
|
|
373
|
-
if not record_data:
|
|
374
|
-
raise WheelInstallError(
|
|
375
|
-
"Cannot re-install installed wheel for {source} because it has no installation "
|
|
376
|
-
"RECORD metadata.".format(source=wheel.source)
|
|
377
|
-
)
|
|
378
814
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
)
|
|
397
|
-
break
|
|
398
|
-
else:
|
|
399
|
-
raise WheelInstallError(
|
|
400
|
-
"Encountered a file from {source} with no identifiable target install path: "
|
|
401
|
-
"{file}".format(source=wheel.source, file=installed_file.path)
|
|
815
|
+
record_lines = [] # type: List[Text]
|
|
816
|
+
eol = os.sep
|
|
817
|
+
if record_data:
|
|
818
|
+
record_lines = record_data.decode("utf-8").splitlines(
|
|
819
|
+
True # N.B. no kw in 2.7: keepends=True
|
|
820
|
+
)
|
|
821
|
+
eol = "\r\n" if record_lines[0].endswith("\r\n") else "\n"
|
|
822
|
+
|
|
823
|
+
if not record_data or any(
|
|
824
|
+
os.path.isabs(installed_file.path)
|
|
825
|
+
for installed_file in Record.read(lines=iter(record_lines))
|
|
826
|
+
):
|
|
827
|
+
prefix = "The RECORD in {whl}".format(whl=os.path.basename(whl))
|
|
828
|
+
suffix = "so wheel re-packing will not be round-trippable."
|
|
829
|
+
if not record_data:
|
|
830
|
+
pex_warnings.warn(
|
|
831
|
+
"{the_record} is missing; {and_so}.".format(the_record=prefix, and_so=suffix)
|
|
402
832
|
)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
InstalledFile(
|
|
408
|
-
path=os.path.relpath(dst_file, dest),
|
|
409
|
-
hash=installed_file.hash,
|
|
410
|
-
size=installed_file.size,
|
|
833
|
+
else:
|
|
834
|
+
pex_warnings.warn(
|
|
835
|
+
"{the_record} has at least some invalid entries with absolute paths; "
|
|
836
|
+
"{and_so}.".format(the_record=prefix, and_so=suffix)
|
|
411
837
|
)
|
|
838
|
+
# Write a minimal repaired record to drive the spread operation below.
|
|
839
|
+
Record.write(
|
|
840
|
+
dst=os.path.join(dest, wheel.metadata_path("RECORD")),
|
|
841
|
+
installed_files=[
|
|
842
|
+
InstalledFile(os.path.relpath(os.path.join(root, path), dest))
|
|
843
|
+
for root, _, files in deterministic_walk(dest)
|
|
844
|
+
for path in files
|
|
845
|
+
],
|
|
846
|
+
eol=eol,
|
|
412
847
|
)
|
|
413
|
-
|
|
848
|
+
|
|
849
|
+
if not wheel.install_paths:
|
|
414
850
|
raise AssertionError(reportable_unexpected_error_msg())
|
|
415
851
|
|
|
852
|
+
record_data = wheel.metadata_files.read("RECORD")
|
|
853
|
+
if not record_data:
|
|
854
|
+
raise WheelInstallError(
|
|
855
|
+
"Cannot re-install installed wheel for {source} because it has no installation "
|
|
856
|
+
"RECORD metadata.".format(source=wheel.source)
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
# 2. Spread
|
|
860
|
+
entry_points = wheel.distribution().get_entry_map()
|
|
861
|
+
script_names = frozenset(
|
|
862
|
+
SysPlatform.CURRENT.binary_name(script)
|
|
863
|
+
for script in itertools.chain.from_iterable(
|
|
864
|
+
entry_points.get(key, {}) for key in ("console_scripts", "gui_scripts")
|
|
865
|
+
)
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
def is_entry_point_script(script_path):
|
|
869
|
+
# type: (Text) -> bool
|
|
870
|
+
return os.path.basename(script_path) in script_names
|
|
871
|
+
|
|
872
|
+
record_relpath = wheel.metadata_path("RECORD")
|
|
873
|
+
record_eol = os.linesep
|
|
874
|
+
|
|
875
|
+
dist_info_dir_relpath = wheel.metadata_path()
|
|
876
|
+
pex_info_dir_relpath = wheel.pex_metadata_path()
|
|
877
|
+
installer_relpath = wheel.metadata_path("INSTALLER")
|
|
878
|
+
requested_relpath = wheel.metadata_path("REQUESTED")
|
|
879
|
+
zip_metadata_relpath = wheel.pex_metadata_path(ZipMetadata.FILENAME)
|
|
880
|
+
|
|
881
|
+
installed_files = [] # type: List[InstalledFile]
|
|
882
|
+
provenance = [] # type: List[Tuple[Text, Text]]
|
|
883
|
+
symlinked = set() # type: Set[Text]
|
|
884
|
+
for installed_file in Record.read(lines=iter(record_data.decode("utf-8").splitlines())):
|
|
885
|
+
if installed_file.path == record_relpath:
|
|
886
|
+
record_eol = _detect_record_eol(os.path.join(wheel.location, installed_file.path))
|
|
887
|
+
installed_files.append(InstalledFile(path=record_relpath, hash=None, size=None))
|
|
888
|
+
# We'll generate these metadata files below as needed.
|
|
889
|
+
continue
|
|
890
|
+
if installed_file.path in (installer_relpath, requested_relpath, zip_metadata_relpath):
|
|
891
|
+
# We'll generate these metadata files below as needed.
|
|
892
|
+
continue
|
|
893
|
+
|
|
894
|
+
if not compile and installed_file.path.endswith(".pyc"):
|
|
895
|
+
continue
|
|
896
|
+
|
|
897
|
+
src_file = os.path.realpath(os.path.join(wheel.location, installed_file.path))
|
|
898
|
+
dst_components = None # type: Optional[Tuple[Text, Text, bool]]
|
|
899
|
+
for path_name, installed_path in wheel.iter_applicable_install_paths():
|
|
900
|
+
installed_path = os.path.realpath(installed_path)
|
|
901
|
+
if installed_path == commonpath((installed_path, src_file)):
|
|
902
|
+
rewrite_script = False
|
|
903
|
+
if "scripts" == path_name:
|
|
904
|
+
if is_entry_point_script(src_file):
|
|
905
|
+
# This entry point script will be installed afresh below as needed.
|
|
906
|
+
break
|
|
907
|
+
rewrite_script = interpreter is not None and is_python_script(
|
|
908
|
+
src_file, check_executable=False
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
dst_rel_path = os.path.relpath(src_file, installed_path)
|
|
912
|
+
dst_components = path_name, dst_rel_path, rewrite_script
|
|
913
|
+
break
|
|
914
|
+
else:
|
|
915
|
+
raise WheelInstallError(
|
|
916
|
+
"Encountered a file from {source} with no identifiable target install path: "
|
|
917
|
+
"{file}".format(source=wheel.source, file=installed_file.path)
|
|
918
|
+
)
|
|
919
|
+
if dst_components:
|
|
920
|
+
dst_path_name, dst_rel_path, rewrite_script = dst_components
|
|
921
|
+
dst_file = os.path.join(install_paths[dst_path_name], dst_rel_path)
|
|
922
|
+
if rewrite_script and interpreter is not None:
|
|
923
|
+
with open(src_file, mode="rb") as in_fp, safe_open(dst_file, "wb") as out_fp:
|
|
924
|
+
first_line = in_fp.readline()
|
|
925
|
+
if first_line and re.match(br"^#!pythonw?", first_line):
|
|
926
|
+
_, _, shebang_args = first_line.partition(b" ")
|
|
927
|
+
encoding_line = ""
|
|
928
|
+
next_line = in_fp.readline()
|
|
929
|
+
# See: https://peps.python.org/pep-0263/
|
|
930
|
+
if next_line and re.match(
|
|
931
|
+
br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)", next_line
|
|
932
|
+
):
|
|
933
|
+
encoding_line = str(next_line.decode("ascii"))
|
|
934
|
+
out_fp.write(
|
|
935
|
+
"{shebang}\n".format(
|
|
936
|
+
shebang=interpreter.shebang(
|
|
937
|
+
args=shebang_args.decode("utf-8"), encoding_line=encoding_line
|
|
938
|
+
)
|
|
939
|
+
).encode("utf-8")
|
|
940
|
+
)
|
|
941
|
+
if not encoding_line and next_line:
|
|
942
|
+
out_fp.write(next_line)
|
|
943
|
+
shutil.copyfileobj(in_fp, out_fp)
|
|
944
|
+
chmod_plus_x(out_fp.name)
|
|
945
|
+
elif copy_mode is CopyMode.SYMLINK:
|
|
946
|
+
top_level = dst_rel_path.split(os.sep)[0]
|
|
947
|
+
if top_level in (dist_info_dir_relpath, pex_info_dir_relpath):
|
|
948
|
+
safe_relative_symlink(src_file, dst_file)
|
|
949
|
+
elif top_level not in symlinked:
|
|
950
|
+
top_level_src = os.path.join(wheel.install_paths[dst_path_name], top_level)
|
|
951
|
+
top_level_dst = os.path.join(install_paths[dst_path_name], top_level)
|
|
952
|
+
try:
|
|
953
|
+
safe_relative_symlink(top_level_src, top_level_dst)
|
|
954
|
+
symlinked.add(top_level)
|
|
955
|
+
except OSError as e:
|
|
956
|
+
if e.errno != errno.EEXIST:
|
|
957
|
+
raise
|
|
958
|
+
else:
|
|
959
|
+
safe_mkdir(os.path.dirname(dst_file))
|
|
960
|
+
if copy_mode is CopyMode.LINK:
|
|
961
|
+
safe_copy(src_file, dst_file, overwrite=False)
|
|
962
|
+
if not os.path.exists(dst_file):
|
|
963
|
+
shutil.copy(src_file, dst_file)
|
|
964
|
+
installed_files.append(create_installed_file(path=dst_file, dest_dir=dest))
|
|
965
|
+
provenance.append((src_file, dst_file))
|
|
966
|
+
|
|
416
967
|
if compile:
|
|
417
968
|
args = [
|
|
418
969
|
interpreter.binary if interpreter else sys.executable,
|
|
@@ -440,93 +991,30 @@ def install_wheel(
|
|
|
440
991
|
file = InstalledFile(path=os.path.relpath(os.path.join(root, f), dest))
|
|
441
992
|
installed_files.append(file)
|
|
442
993
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
with safe_open(os.path.join(dest, wheel.metadata_path("INSTALLER")), "w") as fp:
|
|
451
|
-
print("pex", file=fp)
|
|
452
|
-
installed_files.append(InstalledWheel.create_installed_file(path=fp.name, dest_dir=dest))
|
|
994
|
+
if install_entry_point_scripts:
|
|
995
|
+
for script_src, script_abspath in install_scripts(
|
|
996
|
+
install_paths.scripts, entry_points, interpreter, overwrite=False
|
|
997
|
+
):
|
|
998
|
+
installed_files.append(create_installed_file(path=script_abspath, dest_dir=dest))
|
|
999
|
+
provenance.append((script_src, script_abspath))
|
|
453
1000
|
|
|
454
1001
|
if interpreter:
|
|
455
|
-
# Finalize a proper venv install with
|
|
1002
|
+
# Finalize a proper venv install with INSTALLER and REQUESTED (if it was).
|
|
1003
|
+
with safe_open(os.path.join(dest, installer_relpath), "w") as fp:
|
|
1004
|
+
print("pex", file=fp)
|
|
1005
|
+
installed_files.append(create_installed_file(path=fp.name, dest_dir=dest))
|
|
456
1006
|
if requested:
|
|
457
|
-
requested_path = os.path.join(dest,
|
|
1007
|
+
requested_path = os.path.join(dest, requested_relpath)
|
|
458
1008
|
touch(requested_path)
|
|
459
|
-
installed_files.append(
|
|
460
|
-
InstalledWheel.create_installed_file(path=requested_path, dest_dir=dest)
|
|
461
|
-
)
|
|
462
|
-
|
|
463
|
-
installed_files.append(InstalledFile(path=record_relpath, hash=None, size=None))
|
|
464
|
-
Record.write(dst=record_abspath, installed_files=installed_files)
|
|
1009
|
+
installed_files.append(create_installed_file(path=requested_path, dest_dir=dest))
|
|
465
1010
|
|
|
1011
|
+
if record_entry_info:
|
|
1012
|
+
zip_metadata_path = wheel.record_zip_metadata(dest)
|
|
1013
|
+
if zip_metadata_path:
|
|
1014
|
+
installed_files.append(create_installed_file(path=zip_metadata_path, dest_dir=dest))
|
|
466
1015
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
interpreter=None, # type: Optional[PythonInterpreter]
|
|
471
|
-
):
|
|
472
|
-
# type: (...) -> Iterator[str]
|
|
1016
|
+
Record.write(
|
|
1017
|
+
dst=os.path.join(dest, record_relpath), installed_files=installed_files, eol=record_eol
|
|
1018
|
+
)
|
|
473
1019
|
|
|
474
|
-
|
|
475
|
-
for named_entry_point, gui in itertools.chain.from_iterable(
|
|
476
|
-
((value, gui) for value in entry_points.get(key, {}).values())
|
|
477
|
-
for key, gui in (("console_scripts", False), ("gui_scripts", True))
|
|
478
|
-
):
|
|
479
|
-
entry_point = named_entry_point.entry_point
|
|
480
|
-
if isinstance(entry_point, CallableEntryPoint):
|
|
481
|
-
script = dedent(
|
|
482
|
-
"""\
|
|
483
|
-
{shebang}
|
|
484
|
-
# -*- coding: utf-8 -*-
|
|
485
|
-
import importlib
|
|
486
|
-
import sys
|
|
487
|
-
|
|
488
|
-
entry_point = importlib.import_module({modname!r})
|
|
489
|
-
for attr in {attrs!r}:
|
|
490
|
-
entry_point = getattr(entry_point, attr)
|
|
491
|
-
|
|
492
|
-
if __name__ == "__main__":
|
|
493
|
-
import os
|
|
494
|
-
pex_root_fallback = os.environ.get("_PEX_ROOT_FALLBACK")
|
|
495
|
-
if pex_root_fallback:
|
|
496
|
-
import atexit
|
|
497
|
-
import shutil
|
|
498
|
-
|
|
499
|
-
atexit.register(shutil.rmtree, pex_root_fallback, True)
|
|
500
|
-
|
|
501
|
-
sys.exit(entry_point())
|
|
502
|
-
"""
|
|
503
|
-
).format(shebang=shebang, modname=entry_point.module, attrs=entry_point.attrs)
|
|
504
|
-
else:
|
|
505
|
-
script = dedent(
|
|
506
|
-
"""\
|
|
507
|
-
{shebang}
|
|
508
|
-
# -*- coding: utf-8 -*-
|
|
509
|
-
import runpy
|
|
510
|
-
import sys
|
|
511
|
-
|
|
512
|
-
if __name__ == "__main__":
|
|
513
|
-
import os
|
|
514
|
-
pex_root_fallback = os.environ.get("_PEX_ROOT_FALLBACK")
|
|
515
|
-
if pex_root_fallback:
|
|
516
|
-
import atexit
|
|
517
|
-
import shutil
|
|
518
|
-
|
|
519
|
-
atexit.register(shutil.rmtree, pex_root_fallback, True)
|
|
520
|
-
|
|
521
|
-
runpy.run_module({modname!r}, run_name="__main__", alter_sys=True)
|
|
522
|
-
sys.exit(0)
|
|
523
|
-
"""
|
|
524
|
-
).format(shebang=shebang, modname=entry_point.module)
|
|
525
|
-
script_abspath = os.path.join(install_paths.scripts, named_entry_point.name)
|
|
526
|
-
if WINDOWS:
|
|
527
|
-
script_abspath = windows.create_script(script_abspath, script, gui=gui)
|
|
528
|
-
else:
|
|
529
|
-
with safe_open(script_abspath, "w") as fp:
|
|
530
|
-
fp.write(script)
|
|
531
|
-
chmod_plus_x(fp.name)
|
|
532
|
-
yield script_abspath
|
|
1020
|
+
return tuple(provenance)
|