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_376.py CHANGED
@@ -5,33 +5,18 @@ 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
9
+ import io
12
10
  import os
13
- import shutil
14
11
  from fileinput import FileInput
15
12
 
16
13
  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
- )
14
+ from pex.common import safe_open
25
15
  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
16
+ from pex.typing import TYPE_CHECKING
32
17
 
33
18
  if TYPE_CHECKING:
34
- from typing import IO, Callable, Iterable, Iterator, Optional, Protocol, Text, Tuple, Union
19
+ from typing import IO, Callable, Iterable, Iterator, Optional, Protocol, Text, Union
35
20
 
36
21
  import attr # vendor:skip
37
22
 
@@ -81,39 +66,6 @@ class Hash(object):
81
66
  return self.value
82
67
 
83
68
 
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
69
  @attr.s(frozen=True)
118
70
  class InstalledFile(object):
119
71
  """The record of a single installed file from a PEP 376 RECORD file.
@@ -121,336 +73,23 @@ class InstalledFile(object):
121
73
  See: https://peps.python.org/pep-0376/#record
122
74
  """
123
75
 
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
76
  path = attr.ib() # type: Text
155
77
  hash = attr.ib(default=None) # type: Optional[Hash]
156
78
  size = attr.ib(default=None) # type: Optional[int]
157
79
 
158
80
 
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)
81
+ def create_installed_file(
82
+ path, # type: Text
83
+ dest_dir, # type: str
84
+ ):
85
+ # type: (...) -> InstalledFile
86
+ hasher = hashlib.sha256()
87
+ hashing.file_hash(path, digest=hasher)
88
+ return InstalledFile(
89
+ path=os.path.relpath(path, dest_dir),
90
+ hash=Hash.create(hasher),
91
+ size=os.stat(path).st_size,
92
+ )
454
93
 
455
94
 
456
95
  class RecordError(Exception):
@@ -483,24 +122,42 @@ class Record(object):
483
122
  cls,
484
123
  fp, # type: IO
485
124
  installed_files, # type: Iterable[InstalledFile]
125
+ eol="\n", # type: str
486
126
  ):
487
127
  # 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):
128
+ csv_writer = csv.writer(fp, delimiter=",", quotechar='"', lineterminator=eol)
129
+ for installed_file in installed_files:
490
130
  csv_writer.writerow(attr.astuple(installed_file, recurse=False))
491
131
 
132
+ @classmethod
133
+ def write_bytes(
134
+ cls,
135
+ installed_files, # type: Iterable[InstalledFile]
136
+ eol="\n", # type: str
137
+ ):
138
+ # type: (...) -> bytes
139
+ if PY2:
140
+ record_fp = io.BytesIO()
141
+ cls.write_fp(fp=record_fp, installed_files=installed_files, eol=eol)
142
+ return record_fp.getvalue()
143
+ else:
144
+ record_fp = io.StringIO()
145
+ cls.write_fp(fp=record_fp, installed_files=installed_files, eol=eol)
146
+ return record_fp.getvalue().encode("utf-8")
147
+
492
148
  @classmethod
493
149
  def write(
494
150
  cls,
495
151
  dst, # type: Text
496
152
  installed_files, # type: Iterable[InstalledFile]
153
+ eol="\n", # type: str
497
154
  ):
498
155
  # type: (...) -> None
499
156
 
500
157
  # The RECORD is a csv file with the path to each installed file in the 1st column.
501
158
  # See: https://peps.python.org/pep-0376/#record
502
159
  with safe_open(dst, "wb" if PY2 else "w") as fp:
503
- cls.write_fp(fp, installed_files)
160
+ cls.write_fp(fp, installed_files, eol=eol)
504
161
 
505
162
  @classmethod
506
163
  def read(