pex 2.59.5__py2.py3-none-any.whl → 2.60.1__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 (112) hide show
  1. pex/cache/dirs.py +14 -4
  2. pex/cli/commands/lock.py +8 -5
  3. pex/common.py +57 -7
  4. pex/dist_metadata.py +48 -6
  5. pex/docs/html/_pagefind/fragment/en_52292af.pf_fragment +0 -0
  6. pex/docs/html/_pagefind/fragment/en_6190e2d.pf_fragment +0 -0
  7. pex/docs/html/_pagefind/fragment/en_8d14bba.pf_fragment +0 -0
  8. pex/docs/html/_pagefind/fragment/en_9ba8f7b.pf_fragment +0 -0
  9. pex/docs/html/_pagefind/fragment/en_c350870.pf_fragment +0 -0
  10. pex/docs/html/_pagefind/fragment/en_cb99877.pf_fragment +0 -0
  11. pex/docs/html/_pagefind/fragment/en_cf3d25b.pf_fragment +0 -0
  12. pex/docs/html/_pagefind/fragment/en_df34874.pf_fragment +0 -0
  13. pex/docs/html/_pagefind/index/{en_974dc5a.pf_index → en_853e43e.pf_index} +0 -0
  14. pex/docs/html/_pagefind/pagefind-entry.json +1 -1
  15. pex/docs/html/_pagefind/pagefind.en_71c76562fd.pf_meta +0 -0
  16. pex/docs/html/_static/documentation_options.js +1 -1
  17. pex/docs/html/api/vars.html +5 -5
  18. pex/docs/html/buildingpex.html +5 -5
  19. pex/docs/html/genindex.html +5 -5
  20. pex/docs/html/index.html +5 -5
  21. pex/docs/html/recipes.html +5 -5
  22. pex/docs/html/scie.html +5 -5
  23. pex/docs/html/search.html +5 -5
  24. pex/docs/html/whatispex.html +5 -5
  25. pex/entry_points_txt.py +98 -0
  26. pex/environment.py +13 -10
  27. pex/finders.py +1 -1
  28. pex/installed_wheel.py +127 -0
  29. pex/interpreter.py +17 -5
  30. pex/interpreter_constraints.py +4 -4
  31. pex/pep_376.py +37 -380
  32. pex/pep_427.py +757 -246
  33. pex/pex_builder.py +4 -4
  34. pex/pex_info.py +8 -3
  35. pex/resolve/venv_resolver.py +46 -34
  36. pex/resolver.py +10 -3
  37. pex/sysconfig.py +5 -3
  38. pex/third_party/__init__.py +1 -1
  39. pex/tools/commands/repository.py +47 -24
  40. pex/vendor/__init__.py +1 -8
  41. pex/vendor/__main__.py +62 -41
  42. pex/vendor/_vendored/ansicolors/.layout.json +1 -1
  43. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.dist-info/RECORD +11 -0
  44. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.pex-info/original-whl-info.json +1 -0
  45. pex/vendor/_vendored/appdirs/.layout.json +1 -1
  46. pex/vendor/_vendored/appdirs/appdirs-1.4.4.dist-info/RECORD +7 -0
  47. pex/vendor/_vendored/appdirs/appdirs-1.4.4.pex-info/original-whl-info.json +1 -0
  48. pex/vendor/_vendored/attrs/.layout.json +1 -1
  49. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.dist-info/RECORD +37 -0
  50. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.pex-info/original-whl-info.json +1 -0
  51. pex/vendor/_vendored/packaging_20_9/.layout.json +1 -1
  52. pex/vendor/_vendored/packaging_20_9/packaging-20.9.dist-info/RECORD +20 -0
  53. pex/vendor/_vendored/packaging_20_9/packaging-20.9.pex-info/original-whl-info.json +1 -0
  54. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.dist-info/RECORD +7 -0
  55. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.pex-info/original-whl-info.json +1 -0
  56. pex/vendor/_vendored/packaging_21_3/.layout.json +1 -1
  57. pex/vendor/_vendored/packaging_21_3/packaging-21.3.dist-info/RECORD +20 -0
  58. pex/vendor/_vendored/packaging_21_3/packaging-21.3.pex-info/original-whl-info.json +1 -0
  59. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.dist-info/RECORD +18 -0
  60. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.pex-info/original-whl-info.json +1 -0
  61. pex/vendor/_vendored/packaging_24_0/.layout.json +1 -1
  62. pex/vendor/_vendored/packaging_24_0/packaging-24.0.dist-info/RECORD +22 -0
  63. pex/vendor/_vendored/packaging_24_0/packaging-24.0.pex-info/original-whl-info.json +1 -0
  64. pex/vendor/_vendored/packaging_25_0/.layout.json +1 -1
  65. pex/vendor/_vendored/packaging_25_0/packaging-25.0.dist-info/RECORD +24 -0
  66. pex/vendor/_vendored/packaging_25_0/packaging-25.0.pex-info/original-whl-info.json +1 -0
  67. pex/vendor/_vendored/pip/.layout.json +1 -1
  68. pex/vendor/_vendored/pip/pip-20.3.4.dist-info/RECORD +388 -0
  69. pex/vendor/_vendored/pip/pip-20.3.4.pex-info/original-whl-info.json +1 -0
  70. pex/vendor/_vendored/setuptools/.layout.json +1 -1
  71. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.dist-info/RECORD +107 -0
  72. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.pex-info/original-whl-info.json +1 -0
  73. pex/vendor/_vendored/toml/.layout.json +1 -1
  74. pex/vendor/_vendored/toml/toml-0.10.2.dist-info/RECORD +11 -0
  75. pex/vendor/_vendored/toml/toml-0.10.2.pex-info/original-whl-info.json +1 -0
  76. pex/vendor/_vendored/tomli/.layout.json +1 -1
  77. pex/vendor/_vendored/tomli/tomli-2.0.1.dist-info/RECORD +10 -0
  78. pex/vendor/_vendored/tomli/tomli-2.0.1.pex-info/original-whl-info.json +1 -0
  79. pex/venv/installer.py +9 -5
  80. pex/version.py +1 -1
  81. pex/wheel.py +79 -15
  82. pex/whl.py +67 -0
  83. pex/windows/__init__.py +14 -11
  84. {pex-2.59.5.dist-info → pex-2.60.1.dist-info}/METADATA +4 -4
  85. {pex-2.59.5.dist-info → pex-2.60.1.dist-info}/RECORD +90 -74
  86. pex/docs/html/_pagefind/fragment/en_34b3bf8.pf_fragment +0 -0
  87. pex/docs/html/_pagefind/fragment/en_3cefc8e.pf_fragment +0 -0
  88. pex/docs/html/_pagefind/fragment/en_44ba8a7.pf_fragment +0 -0
  89. pex/docs/html/_pagefind/fragment/en_8eb9a56.pf_fragment +0 -0
  90. pex/docs/html/_pagefind/fragment/en_db171fd.pf_fragment +0 -0
  91. pex/docs/html/_pagefind/fragment/en_ecf679c.pf_fragment +0 -0
  92. pex/docs/html/_pagefind/fragment/en_fb971c7.pf_fragment +0 -0
  93. pex/docs/html/_pagefind/fragment/en_fd8f242.pf_fragment +0 -0
  94. pex/docs/html/_pagefind/pagefind.en_3549188bce.pf_meta +0 -0
  95. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.dist-info/INSTALLER +0 -1
  96. pex/vendor/_vendored/appdirs/appdirs-1.4.4.dist-info/INSTALLER +0 -1
  97. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.dist-info/INSTALLER +0 -1
  98. pex/vendor/_vendored/packaging_20_9/packaging-20.9.dist-info/INSTALLER +0 -1
  99. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.dist-info/INSTALLER +0 -1
  100. pex/vendor/_vendored/packaging_21_3/packaging-21.3.dist-info/INSTALLER +0 -1
  101. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.dist-info/INSTALLER +0 -1
  102. pex/vendor/_vendored/packaging_24_0/packaging-24.0.dist-info/INSTALLER +0 -1
  103. pex/vendor/_vendored/packaging_25_0/packaging-25.0.dist-info/INSTALLER +0 -1
  104. pex/vendor/_vendored/pip/pip-20.3.4.dist-info/INSTALLER +0 -1
  105. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.dist-info/INSTALLER +0 -1
  106. pex/vendor/_vendored/toml/toml-0.10.2.dist-info/INSTALLER +0 -1
  107. pex/vendor/_vendored/tomli/tomli-2.0.1.dist-info/INSTALLER +0 -1
  108. {pex-2.59.5.dist-info → pex-2.60.1.dist-info}/WHEEL +0 -0
  109. {pex-2.59.5.dist-info → pex-2.60.1.dist-info}/entry_points.txt +0 -0
  110. {pex-2.59.5.dist-info → pex-2.60.1.dist-info}/licenses/LICENSE +0 -0
  111. {pex-2.59.5.dist-info → pex-2.60.1.dist-info}/pylock/pylock.toml +0 -0
  112. {pex-2.59.5.dist-info → pex-2.60.1.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,166 +646,346 @@ 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
+
762
+ def _iter_installed_files(
763
+ chroot, # type: str
764
+ exclude_rel_paths=(), # type: Iterable[str]
765
+ ):
766
+ # type: (...) -> Iterator[InstalledFile]
767
+ exclude = frozenset(exclude_rel_paths)
768
+ for root, _, files in deterministic_walk(chroot):
769
+ for path in files:
770
+ rel_path = os.path.relpath(os.path.join(root, path), chroot)
771
+ if rel_path in exclude:
772
+ continue
773
+ yield InstalledFile(rel_path)
774
+
775
+
272
776
  def install_wheel(
273
777
  wheel, # type: InstallableWheel
274
778
  install_paths, # type: InstallPaths
779
+ copy_mode=CopyMode.LINK, # type: CopyMode.Value
275
780
  interpreter=None, # type: Optional[PythonInterpreter]
781
+ rel_extra_path=None, # type: Optional[str]
276
782
  compile=False, # type: bool
277
783
  requested=True, # type: bool
784
+ install_entry_point_scripts=True, # type: bool
785
+ record_entry_info=False, # type: bool
786
+ normalize_file_stat=False, # type: bool
278
787
  ):
279
- # type: (...) -> None
788
+ # type: (...) -> Tuple[Tuple[Text, Text], ...]
280
789
 
281
790
  # 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
-
284
- record_relpath = wheel.metadata_path("RECORD")
285
- record_abspath = os.path.join(dest, record_relpath)
286
791
 
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
- )
792
+ dest = install_paths.purelib if wheel.root_is_purelib else install_paths.platlib
793
+ if rel_extra_path:
794
+ dest = os.path.join(dest, rel_extra_path)
795
+ if wheel.root_is_purelib:
796
+ install_paths = attr.evolve(install_paths, purelib=dest)
797
+ else:
798
+ install_paths = attr.evolve(install_paths, platlib=dest)
310
799
 
311
800
  if wheel.is_whl:
312
- with open_zip(wheel.location) as zf:
801
+ whl = wheel.location
802
+ zip_metadata = None # type: Optional[ZipMetadata]
803
+ with open_zip(whl) as zf:
313
804
  # 1. Unpack
314
805
  zf.extractall(dest)
806
+ if record_entry_info:
807
+ zip_metadata = ZipMetadata.from_zip(
808
+ filename=whl, info_list=zf.infolist(), normalize_file_stat=normalize_file_stat
809
+ )
810
+
315
811
  # TODO(John Sirois): Consider verifying signatures.
316
812
  # N.B.: Pip does not and its also not clear what good this does. A zip can be easily
317
813
  # poked on a per-entry basis allowing forging a RECORD entry and its associated file.
318
814
  # 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:
815
+
816
+ wheel = InstallableWheel(
817
+ wheel=Wheel.load(dest, project_name=wheel.project_name),
818
+ install_paths=InstallPaths.wheel(
819
+ dest, project_name=wheel.project_name, version=wheel.version
820
+ ),
821
+ zip_metadata=zip_metadata,
822
+ )
823
+
824
+ # Deal with bad whl `RECORD`s. We happen to hit one from selenium-4.1.2-py3-none-any.whl
825
+ # in our tests. The selenium >=4,<4.1.3 wheels are all published with absolute paths for
826
+ # all the .py file RECORD entries. The .dist-info and .data entries are fine though.
372
827
  record_data = wheel.metadata_files.read("RECORD")
373
- if not record_data:
828
+
829
+ record_lines = [] # type: List[Text]
830
+ eol = os.sep
831
+ if record_data:
832
+ record_lines = record_data.decode("utf-8").splitlines(
833
+ True # N.B. no kw in 2.7: keepends=True
834
+ )
835
+ eol = "\r\n" if record_lines[0].endswith("\r\n") else "\n"
836
+
837
+ if not record_data or any(
838
+ os.path.isabs(installed_file.path)
839
+ for installed_file in Record.read(lines=iter(record_lines))
840
+ ):
841
+ prefix = "The RECORD in {whl}".format(whl=os.path.basename(whl))
842
+ suffix = "so wheel re-packing will not be round-trippable."
843
+ if not record_data:
844
+ pex_warnings.warn(
845
+ "{the_record} is missing; {and_so}.".format(the_record=prefix, and_so=suffix)
846
+ )
847
+ else:
848
+ pex_warnings.warn(
849
+ "{the_record} has at least some invalid entries with absolute paths; "
850
+ "{and_so}.".format(the_record=prefix, and_so=suffix)
851
+ )
852
+ # Write a minimal repaired record to drive the spread operation below.
853
+ Record.write(
854
+ dst=os.path.join(dest, wheel.metadata_path("RECORD")),
855
+ installed_files=list(_iter_installed_files(dest)),
856
+ eol=eol,
857
+ )
858
+
859
+ if not wheel.install_paths:
860
+ raise AssertionError(reportable_unexpected_error_msg())
861
+
862
+ record_data = wheel.metadata_files.read("RECORD")
863
+ if not record_data:
864
+ try:
865
+ installed_wheel = InstalledWheel.load(wheel.location)
866
+ except InstalledWheel.LoadError:
374
867
  raise WheelInstallError(
375
- "Cannot re-install installed wheel for {source} because it has no installation "
376
- "RECORD metadata.".format(source=wheel.source)
868
+ "Cannot re-install wheel for {source} because it has no installation RECORD "
869
+ "metadata.".format(source=wheel.source)
870
+ )
871
+ else:
872
+ # This is a legacy installed wheel layout with no RECORD; so we concoct one
873
+ layout_file_rel_path = os.path.relpath(
874
+ installed_wheel.layout_file(wheel.location), wheel.location
875
+ )
876
+ record_data = Record.write_bytes(
877
+ installed_files=_iter_installed_files(
878
+ chroot=wheel.location, exclude_rel_paths=[layout_file_rel_path]
879
+ )
377
880
  )
378
881
 
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
882
+ # 2. Spread
883
+ entry_points = wheel.distribution().get_entry_map()
884
+ script_names = frozenset(
885
+ SysPlatform.CURRENT.binary_name(script)
886
+ for script in itertools.chain.from_iterable(
887
+ entry_points.get(key, {}) for key in ("console_scripts", "gui_scripts")
888
+ )
889
+ )
890
+
891
+ def is_entry_point_script(script_path):
892
+ # type: (Text) -> bool
893
+ return os.path.basename(script_path) in script_names
894
+
895
+ record_relpath = wheel.metadata_path("RECORD")
896
+ record_eol = os.linesep
390
897
 
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)
898
+ dist_info_dir_relpath = wheel.metadata_path()
899
+ pex_info_dir_relpath = wheel.pex_metadata_path()
900
+ installer_relpath = wheel.metadata_path("INSTALLER")
901
+ requested_relpath = wheel.metadata_path("REQUESTED")
902
+ zip_metadata_relpath = wheel.pex_metadata_path(ZipMetadata.FILENAME)
903
+
904
+ installed_files = [] # type: List[InstalledFile]
905
+ provenance = [] # type: List[Tuple[Text, Text]]
906
+ symlinked = set() # type: Set[Text]
907
+ for installed_file in Record.read(lines=iter(record_data.decode("utf-8").splitlines())):
908
+ if installed_file.path == record_relpath:
909
+ record_eol = _detect_record_eol(os.path.join(wheel.location, installed_file.path))
910
+ installed_files.append(InstalledFile(path=record_relpath, hash=None, size=None))
911
+ # We'll generate these metadata files below as needed.
912
+ continue
913
+ if installed_file.path in (installer_relpath, requested_relpath, zip_metadata_relpath):
914
+ # We'll generate these metadata files below as needed.
915
+ continue
916
+
917
+ if not compile and installed_file.path.endswith(".pyc"):
918
+ continue
919
+
920
+ src_file = os.path.realpath(os.path.join(wheel.location, installed_file.path))
921
+ dst_components = None # type: Optional[Tuple[Text, Text, bool]]
922
+ for path_name, installed_path in wheel.iter_applicable_install_paths():
923
+ installed_path = os.path.realpath(installed_path)
924
+ if installed_path == commonpath((installed_path, src_file)):
925
+ rewrite_script = False
926
+ if "scripts" == path_name:
927
+ if is_entry_point_script(src_file):
928
+ # This entry point script will be installed afresh below as needed.
929
+ break
930
+ rewrite_script = interpreter is not None and is_python_script(
931
+ src_file, check_executable=False
396
932
  )
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)
402
- )
403
933
 
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,
411
- )
934
+ dst_rel_path = os.path.relpath(src_file, installed_path)
935
+ dst_components = path_name, dst_rel_path, rewrite_script
936
+ break
937
+ else:
938
+ raise WheelInstallError(
939
+ "Encountered a file from {source} with no identifiable target install path: "
940
+ "{file}".format(source=wheel.source, file=installed_file.path)
412
941
  )
413
- else:
414
- raise AssertionError(reportable_unexpected_error_msg())
942
+ if dst_components:
943
+ dst_path_name, dst_rel_path, rewrite_script = dst_components
944
+ dst_file = os.path.join(install_paths[dst_path_name], dst_rel_path)
945
+ if rewrite_script and interpreter is not None:
946
+ with open(src_file, mode="rb") as in_fp, safe_open(dst_file, "wb") as out_fp:
947
+ first_line = in_fp.readline()
948
+ if first_line and re.match(br"^#!pythonw?", first_line):
949
+ _, _, shebang_args = first_line.partition(b" ")
950
+ encoding_line = ""
951
+ next_line = in_fp.readline()
952
+ # See: https://peps.python.org/pep-0263/
953
+ if next_line and re.match(
954
+ br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)", next_line
955
+ ):
956
+ encoding_line = str(next_line.decode("ascii"))
957
+ out_fp.write(
958
+ "{shebang}\n".format(
959
+ shebang=interpreter.shebang(
960
+ args=shebang_args.decode("utf-8"), encoding_line=encoding_line
961
+ )
962
+ ).encode("utf-8")
963
+ )
964
+ if not encoding_line and next_line:
965
+ out_fp.write(next_line)
966
+ shutil.copyfileobj(in_fp, out_fp)
967
+ chmod_plus_x(out_fp.name)
968
+ elif copy_mode is CopyMode.SYMLINK:
969
+ top_level = dst_rel_path.split(os.sep)[0]
970
+ if top_level in (dist_info_dir_relpath, pex_info_dir_relpath):
971
+ safe_relative_symlink(src_file, dst_file)
972
+ elif top_level not in symlinked:
973
+ top_level_src = os.path.join(wheel.install_paths[dst_path_name], top_level)
974
+ top_level_dst = os.path.join(install_paths[dst_path_name], top_level)
975
+ try:
976
+ safe_relative_symlink(top_level_src, top_level_dst)
977
+ symlinked.add(top_level)
978
+ except OSError as e:
979
+ if e.errno != errno.EEXIST:
980
+ raise
981
+ else:
982
+ safe_mkdir(os.path.dirname(dst_file))
983
+ if copy_mode is CopyMode.LINK:
984
+ safe_copy(src_file, dst_file, overwrite=False)
985
+ if not os.path.exists(dst_file):
986
+ shutil.copy(src_file, dst_file)
987
+ installed_files.append(create_installed_file(path=dst_file, dest_dir=dest))
988
+ provenance.append((src_file, dst_file))
415
989
 
416
990
  if compile:
417
991
  args = [
@@ -440,93 +1014,30 @@ def install_wheel(
440
1014
  file = InstalledFile(path=os.path.relpath(os.path.join(root, f), dest))
441
1015
  installed_files.append(file)
442
1016
 
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))
1017
+ if install_entry_point_scripts:
1018
+ for script_src, script_abspath in install_scripts(
1019
+ install_paths.scripts, entry_points, interpreter, overwrite=False
1020
+ ):
1021
+ installed_files.append(create_installed_file(path=script_abspath, dest_dir=dest))
1022
+ provenance.append((script_src, script_abspath))
453
1023
 
454
1024
  if interpreter:
455
- # Finalize a proper venv install with REQUESTED and a RECORD to support uninstalling.
1025
+ # Finalize a proper venv install with INSTALLER and REQUESTED (if it was).
1026
+ with safe_open(os.path.join(dest, installer_relpath), "w") as fp:
1027
+ print("pex", file=fp)
1028
+ installed_files.append(create_installed_file(path=fp.name, dest_dir=dest))
456
1029
  if requested:
457
- requested_path = os.path.join(dest, wheel.metadata_path("REQUESTED"))
1030
+ requested_path = os.path.join(dest, requested_relpath)
458
1031
  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)
1032
+ installed_files.append(create_installed_file(path=requested_path, dest_dir=dest))
465
1033
 
1034
+ if record_entry_info:
1035
+ zip_metadata_path = wheel.record_zip_metadata(dest)
1036
+ if zip_metadata_path:
1037
+ installed_files.append(create_installed_file(path=zip_metadata_path, dest_dir=dest))
466
1038
 
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]
1039
+ Record.write(
1040
+ dst=os.path.join(dest, record_relpath), installed_files=installed_files, eol=record_eol
1041
+ )
473
1042
 
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
1043
+ return tuple(provenance)