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.

Files changed (114) hide show
  1. pex/build_backend/wrap.py +25 -4
  2. pex/cache/dirs.py +14 -4
  3. pex/cli/commands/lock.py +8 -5
  4. pex/common.py +57 -7
  5. pex/compatibility.py +1 -1
  6. pex/dist_metadata.py +48 -6
  7. pex/docs/html/_pagefind/fragment/en_39c0488.pf_fragment +0 -0
  8. pex/docs/html/_pagefind/fragment/en_3eeaaf4.pf_fragment +0 -0
  9. pex/docs/html/_pagefind/fragment/en_a1dde36.pf_fragment +0 -0
  10. pex/docs/html/_pagefind/fragment/en_a755644.pf_fragment +0 -0
  11. pex/docs/html/_pagefind/fragment/en_b16e3bd.pf_fragment +0 -0
  12. pex/docs/html/_pagefind/fragment/{en_e323b0a.pf_fragment → en_c5d35a7.pf_fragment} +0 -0
  13. pex/docs/html/_pagefind/fragment/en_ec62bd2.pf_fragment +0 -0
  14. pex/docs/html/_pagefind/fragment/en_f32628f.pf_fragment +0 -0
  15. pex/docs/html/_pagefind/index/{en_9894162.pf_index → en_b211695.pf_index} +0 -0
  16. pex/docs/html/_pagefind/pagefind-entry.json +1 -1
  17. pex/docs/html/_pagefind/pagefind.en_e8a49380e5.pf_meta +0 -0
  18. pex/docs/html/_static/documentation_options.js +1 -1
  19. pex/docs/html/api/vars.html +5 -5
  20. pex/docs/html/buildingpex.html +5 -5
  21. pex/docs/html/genindex.html +5 -5
  22. pex/docs/html/index.html +5 -5
  23. pex/docs/html/recipes.html +5 -5
  24. pex/docs/html/scie.html +5 -5
  25. pex/docs/html/search.html +5 -5
  26. pex/docs/html/whatispex.html +5 -5
  27. pex/entry_points_txt.py +98 -0
  28. pex/environment.py +13 -10
  29. pex/finders.py +1 -1
  30. pex/installed_wheel.py +127 -0
  31. pex/interpreter.py +17 -5
  32. pex/interpreter_constraints.py +4 -4
  33. pex/pep_376.py +37 -385
  34. pex/pep_427.py +736 -248
  35. pex/pex_builder.py +4 -4
  36. pex/pex_info.py +8 -3
  37. pex/resolve/venv_resolver.py +98 -23
  38. pex/resolver.py +10 -3
  39. pex/sysconfig.py +5 -3
  40. pex/third_party/__init__.py +1 -1
  41. pex/tools/commands/repository.py +47 -24
  42. pex/vendor/__init__.py +4 -9
  43. pex/vendor/__main__.py +62 -41
  44. pex/vendor/_vendored/ansicolors/.layout.json +1 -1
  45. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.dist-info/RECORD +11 -0
  46. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.pex-info/original-whl-info.json +1 -0
  47. pex/vendor/_vendored/appdirs/.layout.json +1 -1
  48. pex/vendor/_vendored/appdirs/appdirs-1.4.4.dist-info/RECORD +7 -0
  49. pex/vendor/_vendored/appdirs/appdirs-1.4.4.pex-info/original-whl-info.json +1 -0
  50. pex/vendor/_vendored/attrs/.layout.json +1 -1
  51. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.dist-info/RECORD +37 -0
  52. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.pex-info/original-whl-info.json +1 -0
  53. pex/vendor/_vendored/packaging_20_9/.layout.json +1 -1
  54. pex/vendor/_vendored/packaging_20_9/packaging-20.9.dist-info/RECORD +20 -0
  55. pex/vendor/_vendored/packaging_20_9/packaging-20.9.pex-info/original-whl-info.json +1 -0
  56. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.dist-info/RECORD +7 -0
  57. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.pex-info/original-whl-info.json +1 -0
  58. pex/vendor/_vendored/packaging_21_3/.layout.json +1 -1
  59. pex/vendor/_vendored/packaging_21_3/packaging-21.3.dist-info/RECORD +20 -0
  60. pex/vendor/_vendored/packaging_21_3/packaging-21.3.pex-info/original-whl-info.json +1 -0
  61. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.dist-info/RECORD +18 -0
  62. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.pex-info/original-whl-info.json +1 -0
  63. pex/vendor/_vendored/packaging_24_0/.layout.json +1 -1
  64. pex/vendor/_vendored/packaging_24_0/packaging-24.0.dist-info/RECORD +22 -0
  65. pex/vendor/_vendored/packaging_24_0/packaging-24.0.pex-info/original-whl-info.json +1 -0
  66. pex/vendor/_vendored/packaging_25_0/.layout.json +1 -1
  67. pex/vendor/_vendored/packaging_25_0/packaging-25.0.dist-info/RECORD +24 -0
  68. pex/vendor/_vendored/packaging_25_0/packaging-25.0.pex-info/original-whl-info.json +1 -0
  69. pex/vendor/_vendored/pip/.layout.json +1 -1
  70. pex/vendor/_vendored/pip/pip/_vendor/certifi/cacert.pem +63 -1
  71. pex/vendor/_vendored/pip/pip-20.3.4.dist-info/RECORD +388 -0
  72. pex/vendor/_vendored/pip/pip-20.3.4.pex-info/original-whl-info.json +1 -0
  73. pex/vendor/_vendored/setuptools/.layout.json +1 -1
  74. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.dist-info/RECORD +107 -0
  75. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.pex-info/original-whl-info.json +1 -0
  76. pex/vendor/_vendored/toml/.layout.json +1 -1
  77. pex/vendor/_vendored/toml/toml-0.10.2.dist-info/RECORD +11 -0
  78. pex/vendor/_vendored/toml/toml-0.10.2.pex-info/original-whl-info.json +1 -0
  79. pex/vendor/_vendored/tomli/.layout.json +1 -1
  80. pex/vendor/_vendored/tomli/tomli-2.0.1.dist-info/RECORD +10 -0
  81. pex/vendor/_vendored/tomli/tomli-2.0.1.pex-info/original-whl-info.json +1 -0
  82. pex/venv/installer.py +9 -5
  83. pex/version.py +1 -1
  84. pex/wheel.py +79 -15
  85. pex/whl.py +67 -0
  86. pex/windows/__init__.py +14 -11
  87. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/METADATA +4 -4
  88. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/RECORD +93 -77
  89. pex/docs/html/_pagefind/fragment/en_144b803.pf_fragment +0 -0
  90. pex/docs/html/_pagefind/fragment/en_1df1379.pf_fragment +0 -0
  91. pex/docs/html/_pagefind/fragment/en_2c6c6cb.pf_fragment +0 -0
  92. pex/docs/html/_pagefind/fragment/en_a916b1c.pf_fragment +0 -0
  93. pex/docs/html/_pagefind/fragment/en_b33e5d4.pf_fragment +0 -0
  94. pex/docs/html/_pagefind/fragment/en_c1c571a.pf_fragment +0 -0
  95. pex/docs/html/_pagefind/fragment/en_fda06e7.pf_fragment +0 -0
  96. pex/docs/html/_pagefind/pagefind.en_84c8322e7a.pf_meta +0 -0
  97. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.dist-info/INSTALLER +0 -1
  98. pex/vendor/_vendored/appdirs/appdirs-1.4.4.dist-info/INSTALLER +0 -1
  99. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.dist-info/INSTALLER +0 -1
  100. pex/vendor/_vendored/packaging_20_9/packaging-20.9.dist-info/INSTALLER +0 -1
  101. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.dist-info/INSTALLER +0 -1
  102. pex/vendor/_vendored/packaging_21_3/packaging-21.3.dist-info/INSTALLER +0 -1
  103. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.dist-info/INSTALLER +0 -1
  104. pex/vendor/_vendored/packaging_24_0/packaging-24.0.dist-info/INSTALLER +0 -1
  105. pex/vendor/_vendored/packaging_25_0/packaging-25.0.dist-info/INSTALLER +0 -1
  106. pex/vendor/_vendored/pip/pip-20.3.4.dist-info/INSTALLER +0 -1
  107. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.dist-info/INSTALLER +0 -1
  108. pex/vendor/_vendored/toml/toml-0.10.2.dist-info/INSTALLER +0 -1
  109. pex/vendor/_vendored/tomli/tomli-2.0.1.dist-info/INSTALLER +0 -1
  110. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/WHEEL +0 -0
  111. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/entry_points.txt +0 -0
  112. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/licenses/LICENSE +0 -0
  113. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/pylock/pylock.toml +0 -0
  114. {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
- from contextlib import closing
14
- from fileinput import FileInput
15
- from textwrap import dedent
16
-
17
- from pex import pex_warnings, windows
18
- from pex.common import is_pyc_file, iter_copytree, open_zip, safe_mkdir, safe_open, touch
19
- from pex.compatibility import commonpath, get_stdout_bytes_buffer, safe_commonpath
20
- from pex.dist_metadata import (
21
- CallableEntryPoint,
22
- DistMetadata,
23
- Distribution,
24
- MetadataFiles,
25
- NamedEntryPoint,
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.os import WINDOWS
32
- from pex.pep_376 import InstalledFile, InstalledWheel, Record
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
- project_name, # type: ProjectName
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=os.path.join(base, "include", "site", "pythonX.Y", project_name.raw),
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(cls, interpreter):
96
- # type: (PythonInterpreter) -> InstallPaths
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=sysconfig_paths["include"],
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=item, value=self[item]) for item in InstallPaths.PATHS
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 InstallableWheel(object):
246
+ class ZipEntryInfo(object):
135
247
  @classmethod
136
- def from_installation(
248
+ def from_zip_info(
137
249
  cls,
138
- interpreter, # type: PythonInterpreter
139
- distribution, # type: Distribution
250
+ zip_info, # type: zipfile.ZipInfo
251
+ normalize_file_stat=False, # type: bool
140
252
  ):
141
- # type: (...) -> InstallableWheel
253
+ # type: (...) -> ZipEntryInfo
142
254
  return cls(
143
- wheel=Wheel.from_distribution(distribution),
144
- install_paths=InstallPaths.interpreter(interpreter),
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 from_whl(cls, whl):
149
- # type: (str) -> InstallableWheel
150
- return cls(wheel=Wheel.load(whl))
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
- compile=False, # type: bool
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
- destination, project_name=wheel_to_install.metadata_files.metadata.project_name
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: (...) -> None
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(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: (...) -> None
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
- record_relpath = wheel.metadata_path("RECORD")
285
- record_abspath = os.path.join(dest, record_relpath)
286
-
287
- data_rel_path = wheel.data_dir
288
- data_path = os.path.join(dest, data_rel_path)
289
-
290
- installed_files = [] # type: List[InstalledFile]
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
- with open_zip(wheel.location) as zf:
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
- record_files(
320
- root_dir=dest,
321
- names=[
322
- name
323
- for name in zf.namelist()
324
- if not name.endswith("/")
325
- and data_rel_path != safe_commonpath((data_rel_path, name))
326
- ],
327
- )
328
- if os.path.isdir(data_path):
329
- # 2. Spread
330
- for entry in sorted(os.listdir(data_path)):
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
- for installed_file in Record.read(iter(record_data.decode("utf-8").splitlines())):
380
- if installed_file.path == record_relpath:
381
- # We'll generate a new RECORD below as needed.
382
- continue
383
- src_file = os.path.realpath(os.path.join(wheel.location, installed_file.path))
384
- dst_file = os.path.realpath(os.path.join(dest, installed_file.path))
385
- if dest == commonpath((dest, dst_file)):
386
- safe_mkdir(os.path.dirname(dst_file))
387
- shutil.copy(src_file, dst_file)
388
- installed_files.append(installed_file)
389
- continue
390
-
391
- for path in InstallPaths.PATHS:
392
- installed_path = wheel.install_paths[path]
393
- if installed_path == commonpath((installed_path, src_file)):
394
- dst_file = os.path.join(
395
- install_paths[path], os.path.relpath(src_file, installed_path)
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
- safe_mkdir(os.path.dirname(dst_file))
405
- shutil.copy(src_file, dst_file)
406
- installed_files.append(
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
- else:
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
- dist = Distribution(location=dest, metadata=wheel.dist_metadata())
444
- entry_points = dist.get_entry_map()
445
- installed_files.extend(
446
- InstalledWheel.create_installed_file(path=script_abspath, dest_dir=dest)
447
- for script_abspath in install_scripts(install_paths, entry_points, interpreter)
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 REQUESTED and a RECORD to support uninstalling.
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, wheel.metadata_path("REQUESTED"))
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
- def install_scripts(
468
- install_paths, # type: InstallPaths
469
- entry_points, # type: Mapping[str, Mapping[str, NamedEntryPoint]]
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
- shebang = interpreter.shebang() if interpreter else "#!python"
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)