pex 2.59.5__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 (111) 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_39c0488.pf_fragment +0 -0
  6. pex/docs/html/_pagefind/fragment/en_3eeaaf4.pf_fragment +0 -0
  7. pex/docs/html/_pagefind/fragment/en_a1dde36.pf_fragment +0 -0
  8. pex/docs/html/_pagefind/fragment/en_a755644.pf_fragment +0 -0
  9. pex/docs/html/_pagefind/fragment/en_b16e3bd.pf_fragment +0 -0
  10. pex/docs/html/_pagefind/fragment/{en_ecf679c.pf_fragment → en_c5d35a7.pf_fragment} +0 -0
  11. pex/docs/html/_pagefind/fragment/en_ec62bd2.pf_fragment +0 -0
  12. pex/docs/html/_pagefind/fragment/en_f32628f.pf_fragment +0 -0
  13. pex/docs/html/_pagefind/index/{en_974dc5a.pf_index → en_b211695.pf_index} +0 -0
  14. pex/docs/html/_pagefind/pagefind-entry.json +1 -1
  15. pex/docs/html/_pagefind/pagefind.en_e8a49380e5.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 +20 -380
  32. pex/pep_427.py +736 -248
  33. pex/pex_builder.py +4 -4
  34. pex/pex_info.py +8 -3
  35. pex/resolve/venv_resolver.py +46 -25
  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.0.dist-info}/METADATA +4 -4
  85. {pex-2.59.5.dist-info → pex-2.60.0.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_fb971c7.pf_fragment +0 -0
  92. pex/docs/html/_pagefind/fragment/en_fd8f242.pf_fragment +0 -0
  93. pex/docs/html/_pagefind/pagefind.en_3549188bce.pf_meta +0 -0
  94. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.dist-info/INSTALLER +0 -1
  95. pex/vendor/_vendored/appdirs/appdirs-1.4.4.dist-info/INSTALLER +0 -1
  96. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.dist-info/INSTALLER +0 -1
  97. pex/vendor/_vendored/packaging_20_9/packaging-20.9.dist-info/INSTALLER +0 -1
  98. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.dist-info/INSTALLER +0 -1
  99. pex/vendor/_vendored/packaging_21_3/packaging-21.3.dist-info/INSTALLER +0 -1
  100. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.dist-info/INSTALLER +0 -1
  101. pex/vendor/_vendored/packaging_24_0/packaging-24.0.dist-info/INSTALLER +0 -1
  102. pex/vendor/_vendored/packaging_25_0/packaging-25.0.dist-info/INSTALLER +0 -1
  103. pex/vendor/_vendored/pip/pip-20.3.4.dist-info/INSTALLER +0 -1
  104. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.dist-info/INSTALLER +0 -1
  105. pex/vendor/_vendored/toml/toml-0.10.2.dist-info/INSTALLER +0 -1
  106. pex/vendor/_vendored/tomli/tomli-2.0.1.dist-info/INSTALLER +0 -1
  107. {pex-2.59.5.dist-info → pex-2.60.0.dist-info}/WHEEL +0 -0
  108. {pex-2.59.5.dist-info → pex-2.60.0.dist-info}/entry_points.txt +0 -0
  109. {pex-2.59.5.dist-info → pex-2.60.0.dist-info}/licenses/LICENSE +0 -0
  110. {pex-2.59.5.dist-info → pex-2.60.0.dist-info}/pylock/pylock.toml +0 -0
  111. {pex-2.59.5.dist-info → pex-2.60.0.dist-info}/top_level.txt +0 -0
pex/pep_376.py CHANGED
@@ -5,33 +5,17 @@ from __future__ import absolute_import
5
5
 
6
6
  import base64
7
7
  import csv
8
- import errno
9
8
  import hashlib
10
- import itertools
11
- import json
12
9
  import os
13
- import shutil
14
10
  from fileinput import FileInput
15
11
 
16
12
  from pex import hashing
17
- from pex.common import (
18
- CopyMode,
19
- is_pyc_dir,
20
- is_pyc_file,
21
- safe_mkdir,
22
- safe_open,
23
- safe_relative_symlink,
24
- )
13
+ from pex.common import safe_open
25
14
  from pex.compatibility import PY2
26
- from pex.fs import safe_link
27
- from pex.interpreter import PythonInterpreter
28
- from pex.typing import TYPE_CHECKING, cast
29
- from pex.util import CacheHelper
30
- from pex.venv.virtualenv import Virtualenv
31
- from pex.wheel import WHEEL, Wheel, WheelMetadataLoadError
15
+ from pex.typing import TYPE_CHECKING
32
16
 
33
17
  if TYPE_CHECKING:
34
- from typing import IO, Callable, Iterable, Iterator, Optional, Protocol, Text, Tuple, Union
18
+ from typing import IO, Callable, Iterable, Iterator, Optional, Protocol, Text, Union
35
19
 
36
20
  import attr # vendor:skip
37
21
 
@@ -81,39 +65,6 @@ class Hash(object):
81
65
  return self.value
82
66
 
83
67
 
84
- def find_and_replace_path_components(
85
- path, # type: Text
86
- find, # type: str
87
- replace, # type: str
88
- ):
89
- # type: (...) -> Text
90
- """Replace components of `path` that are exactly `find` with `replace`.
91
-
92
- >>> find_and_replace_path_components("foo/bar/baz", "bar", "spam")
93
- foo/spam/baz
94
- >>>
95
- """
96
- if not find or not replace:
97
- raise ValueError(
98
- "Both find and replace must be non-empty strings. Given find={find!r} "
99
- "replace={replace!r}".format(find=find, replace=replace)
100
- )
101
- if not path:
102
- return path
103
-
104
- components = []
105
- head = path
106
- while head:
107
- new_head, tail = os.path.split(head)
108
- if new_head == head:
109
- components.append(head)
110
- break
111
- components.append(tail)
112
- head = new_head
113
- components.reverse()
114
- return os.path.join(*(replace if component == find else component for component in components))
115
-
116
-
117
68
  @attr.s(frozen=True)
118
69
  class InstalledFile(object):
119
70
  """The record of a single installed file from a PEP 376 RECORD file.
@@ -121,336 +72,23 @@ class InstalledFile(object):
121
72
  See: https://peps.python.org/pep-0376/#record
122
73
  """
123
74
 
124
- _PYTHON_VER_PLACEHOLDER = "pythonX.Y"
125
-
126
- @staticmethod
127
- def _python_ver(interpreter=None):
128
- # type: (Optional[PythonInterpreter]) -> str
129
- python = interpreter or PythonInterpreter.get()
130
- return "python{major}.{minor}".format(major=python.version[0], minor=python.version[1])
131
-
132
- @classmethod
133
- def normalized_path(
134
- cls,
135
- path, # type: Text
136
- interpreter=None, # type: Optional[PythonInterpreter]
137
- ):
138
- # type: (...) -> Text
139
- return find_and_replace_path_components(
140
- path, cls._python_ver(interpreter=interpreter), cls._PYTHON_VER_PLACEHOLDER
141
- )
142
-
143
- @classmethod
144
- def denormalized_path(
145
- cls,
146
- path, # type: str
147
- interpreter=None, # type: Optional[PythonInterpreter]
148
- ):
149
- # type: (...) -> Text
150
- return find_and_replace_path_components(
151
- path, cls._PYTHON_VER_PLACEHOLDER, cls._python_ver(interpreter=interpreter)
152
- )
153
-
154
75
  path = attr.ib() # type: Text
155
76
  hash = attr.ib(default=None) # type: Optional[Hash]
156
77
  size = attr.ib(default=None) # type: Optional[int]
157
78
 
158
79
 
159
- @attr.s(frozen=True)
160
- class InstalledWheel(object):
161
- class LoadError(Exception):
162
- """Indicates an installed wheel was not loadable at a particular path."""
163
-
164
- _LAYOUT_JSON_FILENAME = ".layout.json"
165
-
166
- @classmethod
167
- def layout_file(cls, prefix_dir):
168
- # type: (str) -> str
169
- return os.path.join(prefix_dir, cls._LAYOUT_JSON_FILENAME)
170
-
171
- @classmethod
172
- def save(
173
- cls,
174
- prefix_dir, # type: str
175
- stash_dir, # type: str
176
- record_relpath, # type: Text
177
- root_is_purelib, # type: bool
178
- ):
179
- # type: (...) -> InstalledWheel
180
-
181
- # We currently need the installed wheel chroot hash for PEX-INFO / boot purposes. It is
182
- # expensive to calculate; so we do it here 1 time when saving the installed wheel.
183
- fingerprint = CacheHelper.dir_hash(prefix_dir, hasher=hashlib.sha256)
184
-
185
- layout = {
186
- "stash_dir": stash_dir,
187
- "record_relpath": record_relpath,
188
- "fingerprint": fingerprint,
189
- "root_is_purelib": root_is_purelib,
190
- }
191
- with open(cls.layout_file(prefix_dir), "w") as fp:
192
- json.dump(layout, fp, sort_keys=True)
193
- return cls(
194
- prefix_dir=prefix_dir,
195
- stash_dir=stash_dir,
196
- record_relpath=record_relpath,
197
- fingerprint=fingerprint,
198
- root_is_purelib=root_is_purelib,
199
- )
200
-
201
- @classmethod
202
- def load(cls, prefix_dir):
203
- # type: (str) -> InstalledWheel
204
- layout_file = cls.layout_file(prefix_dir)
205
- try:
206
- with open(layout_file) as fp:
207
- layout = json.load(fp)
208
- except (IOError, OSError) as e:
209
- raise cls.LoadError(
210
- "Failed to load an installed wheel layout from {layout_file}: {err}".format(
211
- layout_file=layout_file, err=e
212
- )
213
- )
214
- if not isinstance(layout, dict):
215
- raise cls.LoadError(
216
- "The installed wheel layout file at {layout_file} must contain a single top-level "
217
- "object, found: {value}.".format(layout_file=layout_file, value=layout)
218
- )
219
- stash_dir = layout.get("stash_dir")
220
- record_relpath = layout.get("record_relpath")
221
- if not stash_dir or not record_relpath:
222
- raise cls.LoadError(
223
- "The installed wheel layout file at {layout_file} must contain an object with both "
224
- "`stash_dir` and `record_relpath` attributes, found: {value}".format(
225
- layout_file=layout_file, value=layout
226
- )
227
- )
228
-
229
- fingerprint = layout.get("fingerprint")
230
-
231
- # N.B.: Caching root_is_purelib was not part of the original InstalledWheel layout data; so
232
- # we materialize the property if needed to support older installed wheel chroots.
233
- root_is_purelib = layout.get("root_is_purelib")
234
- if root_is_purelib is None:
235
- try:
236
- wheel = WHEEL.load(prefix_dir)
237
- except WheelMetadataLoadError as e:
238
- raise cls.LoadError(
239
- "Failed to determine if installed wheel at {location} is platform-specific: "
240
- "{err}".format(location=prefix_dir, err=e)
241
- )
242
- root_is_purelib = wheel.root_is_purelib
243
-
244
- return cls(
245
- prefix_dir=prefix_dir,
246
- stash_dir=cast(str, stash_dir),
247
- record_relpath=cast(str, record_relpath),
248
- fingerprint=cast("Optional[str]", fingerprint),
249
- root_is_purelib=root_is_purelib,
250
- )
251
-
252
- prefix_dir = attr.ib() # type: str
253
- stash_dir = attr.ib() # type: str
254
- record_relpath = attr.ib() # type: Text
255
- fingerprint = attr.ib() # type: Optional[str]
256
- root_is_purelib = attr.ib() # type: bool
257
-
258
- def wheel_file_name(self):
259
- # type: () -> str
260
- return Wheel.load(self.prefix_dir).wheel_file_name
261
-
262
- def stashed_path(self, *components):
263
- # type: (*str) -> str
264
- return os.path.join(self.prefix_dir, self.stash_dir, *components)
265
-
266
- @staticmethod
267
- def create_installed_file(
268
- path, # type: Text
269
- dest_dir, # type: str
270
- ):
271
- # type: (...) -> InstalledFile
272
- hasher = hashlib.sha256()
273
- hashing.file_hash(path, digest=hasher)
274
- return InstalledFile(
275
- path=os.path.relpath(path, dest_dir),
276
- hash=Hash.create(hasher),
277
- size=os.stat(path).st_size,
278
- )
279
-
280
- def _create_record(
281
- self,
282
- dst, # type: Text
283
- installed_files, # type: Iterable[InstalledFile]
284
- ):
285
- # type: (...) -> None
286
- Record.write(
287
- dst=os.path.join(dst, self.record_relpath),
288
- installed_files=[
289
- # The RECORD entry should never include hash or size; so we replace any such entry
290
- # with an un-hashed and un-sized one.
291
- InstalledFile(self.record_relpath, hash=None, size=None)
292
- if installed_file.path == self.record_relpath
293
- else installed_file
294
- for installed_file in installed_files
295
- ],
296
- )
297
-
298
- def reinstall_flat(
299
- self,
300
- target_dir, # type: str
301
- copy_mode=CopyMode.LINK, # type: CopyMode.Value
302
- ):
303
- # type: (...) -> Iterator[Tuple[Text, Text]]
304
- """Re-installs the installed wheel in a flat target directory.
305
-
306
- N.B.: A record of reinstalled files is returned in the form of an iterator that must be
307
- consumed to drive the installation to completion.
308
-
309
- If there is an error re-installing a file due to it already existing in the target
310
- directory, the error is suppressed, and it's expected that the caller detects this by
311
- comparing the record of installed files against those installed previously.
312
-
313
- :return: An iterator over src -> dst pairs.
314
- """
315
- installed_files = [InstalledFile(self.record_relpath)]
316
- for src, dst in itertools.chain(
317
- self._reinstall_stash(dest_dir=target_dir, link=copy_mode is not CopyMode.COPY),
318
- self._reinstall_site_packages(target_dir, copy_mode=copy_mode),
319
- ):
320
- installed_files.append(self.create_installed_file(path=dst, dest_dir=target_dir))
321
- yield src, dst
322
-
323
- self._create_record(target_dir, installed_files)
324
-
325
- def reinstall_venv(
326
- self,
327
- venv, # type: Virtualenv
328
- copy_mode=CopyMode.LINK, # type: CopyMode.Value
329
- rel_extra_path=None, # type: Optional[str]
330
- ):
331
- # type: (...) -> Iterator[Tuple[Text, Text]]
332
- """Re-installs the installed wheel in a venv.
333
-
334
- N.B.: A record of reinstalled files is returned in the form of an iterator that must be
335
- consumed to drive the installation to completion.
336
-
337
- If there is an error re-installing a file due to it already existing in the destination
338
- venv, the error is suppressed, and it's expected that the caller detects this by comparing
339
- the record of installed files against those installed previously.
340
-
341
- :return: An iterator over src -> dst pairs.
342
- """
343
-
344
- site_packages_dir = venv.purelib if self.root_is_purelib else venv.platlib
345
- site_packages_dir = (
346
- os.path.join(site_packages_dir, rel_extra_path) if rel_extra_path else site_packages_dir
347
- )
348
-
349
- installed_files = [InstalledFile(self.record_relpath)]
350
- for src, dst in itertools.chain(
351
- self._reinstall_stash(
352
- dest_dir=venv.venv_dir,
353
- interpreter=venv.interpreter,
354
- link=copy_mode is not CopyMode.COPY,
355
- ),
356
- self._reinstall_site_packages(site_packages_dir, copy_mode=copy_mode),
357
- ):
358
- installed_files.append(self.create_installed_file(path=dst, dest_dir=site_packages_dir))
359
- yield src, dst
360
-
361
- self._create_record(site_packages_dir, installed_files)
362
-
363
- def _reinstall_stash(
364
- self,
365
- dest_dir, # type: str
366
- interpreter=None, # type: Optional[PythonInterpreter]
367
- link=True, # type: bool
368
- ):
369
- # type: (...) -> Iterator[Tuple[Text, Text]]
370
-
371
- stash_abs_path = os.path.join(self.prefix_dir, self.stash_dir)
372
- for root, dirs, files in os.walk(stash_abs_path, topdown=True, followlinks=True):
373
- dir_created = False
374
- for f in files:
375
- src = os.path.join(root, f)
376
- src_relpath = os.path.relpath(src, stash_abs_path)
377
- dst = InstalledFile.denormalized_path(
378
- path=os.path.join(dest_dir, src_relpath), interpreter=interpreter
379
- )
380
- if not dir_created:
381
- safe_mkdir(os.path.dirname(dst))
382
- dir_created = True
383
- try:
384
- # We only try to link regular files since linking a symlink on Linux can produce
385
- # another symlink, which leaves open the possibility the src target could later
386
- # go missing leaving the dst dangling.
387
- if link and not os.path.islink(src):
388
- try:
389
- safe_link(src, dst)
390
- continue
391
- except OSError as e:
392
- if e.errno != errno.EXDEV:
393
- raise e
394
- link = False
395
- shutil.copy(src, dst)
396
- except (IOError, OSError) as e:
397
- if e.errno != errno.EEXIST:
398
- raise e
399
- finally:
400
- yield src, dst
401
-
402
- def _reinstall_site_packages(
403
- self,
404
- site_packages_dir, # type: str
405
- copy_mode=CopyMode.LINK, # type: CopyMode.Value
406
- ):
407
- # type: (...) -> Iterator[Tuple[Text, Text]]
408
-
409
- link = copy_mode is CopyMode.LINK
410
- for root, dirs, files in os.walk(self.prefix_dir, topdown=True, followlinks=True):
411
- if root == self.prefix_dir:
412
- dirs[:] = [d for d in dirs if not is_pyc_dir(d) and d != self.stash_dir]
413
- files[:] = [
414
- f for f in files if not is_pyc_file(f) and f != self._LAYOUT_JSON_FILENAME
415
- ]
416
-
417
- traverse = set(dirs)
418
- for path, is_dir in itertools.chain(
419
- zip(dirs, itertools.repeat(True)), zip(files, itertools.repeat(False))
420
- ):
421
- src_entry = os.path.join(root, path)
422
- dst_entry = os.path.join(
423
- site_packages_dir, os.path.relpath(src_entry, self.prefix_dir)
424
- )
425
- try:
426
- if copy_mode is CopyMode.SYMLINK and not (
427
- src_entry.endswith(".dist-info") and os.path.isdir(src_entry)
428
- ):
429
- safe_relative_symlink(src_entry, dst_entry)
430
- traverse.discard(path)
431
- elif is_dir:
432
- safe_mkdir(dst_entry)
433
- else:
434
- # We only try to link regular files since linking a symlink on Linux can
435
- # produce another symlink, which leaves open the possibility the src_entry
436
- # target could later go missing leaving the dst_entry dangling.
437
- if link and not os.path.islink(src_entry):
438
- try:
439
- safe_link(src_entry, dst_entry)
440
- continue
441
- except OSError as e:
442
- if e.errno != errno.EXDEV:
443
- raise e
444
- link = False
445
- shutil.copy(src_entry, dst_entry)
446
- except (IOError, OSError) as e:
447
- if e.errno != errno.EEXIST:
448
- raise e
449
- finally:
450
- if not is_dir:
451
- yield src_entry, dst_entry
452
-
453
- dirs[:] = list(traverse)
80
+ def create_installed_file(
81
+ path, # type: Text
82
+ dest_dir, # type: str
83
+ ):
84
+ # type: (...) -> InstalledFile
85
+ hasher = hashlib.sha256()
86
+ hashing.file_hash(path, digest=hasher)
87
+ return InstalledFile(
88
+ path=os.path.relpath(path, dest_dir),
89
+ hash=Hash.create(hasher),
90
+ size=os.stat(path).st_size,
91
+ )
454
92
 
455
93
 
456
94
  class RecordError(Exception):
@@ -483,10 +121,11 @@ class Record(object):
483
121
  cls,
484
122
  fp, # type: IO
485
123
  installed_files, # type: Iterable[InstalledFile]
124
+ eol="\n", # type: str
486
125
  ):
487
126
  # type: (...) -> None
488
- csv_writer = csv.writer(fp, delimiter=",", quotechar='"', lineterminator="\n")
489
- for installed_file in sorted(installed_files, key=lambda installed: installed.path):
127
+ csv_writer = csv.writer(fp, delimiter=",", quotechar='"', lineterminator=eol)
128
+ for installed_file in installed_files:
490
129
  csv_writer.writerow(attr.astuple(installed_file, recurse=False))
491
130
 
492
131
  @classmethod
@@ -494,13 +133,14 @@ class Record(object):
494
133
  cls,
495
134
  dst, # type: Text
496
135
  installed_files, # type: Iterable[InstalledFile]
136
+ eol="\n", # type: str
497
137
  ):
498
138
  # type: (...) -> None
499
139
 
500
140
  # The RECORD is a csv file with the path to each installed file in the 1st column.
501
141
  # See: https://peps.python.org/pep-0376/#record
502
142
  with safe_open(dst, "wb" if PY2 else "w") as fp:
503
- cls.write_fp(fp, installed_files)
143
+ cls.write_fp(fp, installed_files, eol=eol)
504
144
 
505
145
  @classmethod
506
146
  def read(