pex 2.64.1__py2.py3-none-any.whl → 2.69.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pex might be problematic. Click here for more details.
- pex/bin/pex.py +2 -1
- pex/build_backend/configuration.py +5 -5
- pex/build_backend/wrap.py +2 -19
- pex/cli/commands/lock.py +4 -2
- pex/cli/commands/run.py +10 -11
- pex/cli/pex.py +11 -4
- pex/dist_metadata.py +29 -2
- pex/docs/html/_pagefind/fragment/en_4250138.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_7125dad.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_785d562.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_8e94bb8.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/{en_17782b6.pf_fragment → en_a0396bb.pf_fragment} +0 -0
- pex/docs/html/_pagefind/fragment/en_a8a3588.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_c07d988.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_d718411.pf_fragment +0 -0
- pex/docs/html/_pagefind/index/en_a2e3c5e.pf_index +0 -0
- pex/docs/html/_pagefind/pagefind-entry.json +1 -1
- pex/docs/html/_pagefind/pagefind.en_4ce1afa9e3.pf_meta +0 -0
- pex/docs/html/_static/documentation_options.js +1 -1
- pex/docs/html/api/vars.html +5 -5
- pex/docs/html/buildingpex.html +5 -5
- pex/docs/html/genindex.html +5 -5
- pex/docs/html/index.html +5 -5
- pex/docs/html/recipes.html +5 -5
- pex/docs/html/scie.html +5 -5
- pex/docs/html/search.html +5 -5
- pex/docs/html/whatispex.html +5 -5
- pex/hashing.py +71 -9
- pex/interpreter_constraints.py +1 -1
- pex/jobs.py +13 -6
- pex/pep_376.py +21 -6
- pex/pep_427.py +30 -8
- pex/pex_builder.py +1 -4
- pex/pip/local_project.py +6 -14
- pex/pip/tool.py +3 -3
- pex/pip/vcs.py +93 -44
- pex/pip/version.py +7 -0
- pex/resolve/configured_resolve.py +13 -5
- pex/resolve/lock_downloader.py +1 -0
- pex/resolve/locker.py +30 -14
- pex/resolve/lockfile/create.py +2 -7
- pex/resolve/pre_resolved_resolver.py +1 -7
- pex/resolve/project.py +233 -47
- pex/resolve/resolver_configuration.py +1 -1
- pex/resolve/resolver_options.py +14 -9
- pex/resolve/venv_resolver.py +221 -65
- pex/resolver.py +59 -55
- pex/scie/__init__.py +40 -1
- pex/scie/model.py +2 -0
- pex/scie/science.py +25 -3
- pex/sdist.py +219 -0
- pex/version.py +1 -1
- pex/wheel.py +16 -12
- {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/METADATA +4 -4
- {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/RECORD +60 -59
- {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/entry_points.txt +1 -0
- pex/docs/html/_pagefind/fragment/en_1048255.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_3f7efc3.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_40667cd.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_55ee2f4.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_d6d92dd.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_d834316.pf_fragment +0 -0
- pex/docs/html/_pagefind/fragment/en_ec2ce54.pf_fragment +0 -0
- pex/docs/html/_pagefind/index/en_17effb2.pf_index +0 -0
- pex/docs/html/_pagefind/pagefind.en_49ec86cf86.pf_meta +0 -0
- {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/WHEEL +0 -0
- {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/licenses/LICENSE +0 -0
- {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/pylock/pylock.toml +0 -0
- {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/top_level.txt +0 -0
pex/docs/html/whatispex.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="prefetch" href="_static/pex-logo-dark.png" as="image">
|
|
9
9
|
|
|
10
10
|
<link rel="shortcut icon" href="_static/pex-icon.png"><!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25 -->
|
|
11
|
-
<title>What are .pex files? - Pex Docs (v2.
|
|
11
|
+
<title>What are .pex files? - Pex Docs (v2.69.0)</title>
|
|
12
12
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
|
13
13
|
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
|
14
14
|
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
</label>
|
|
164
164
|
</div>
|
|
165
165
|
<div class="header-center">
|
|
166
|
-
<a href="index.html"><div class="brand">Pex Docs (v2.
|
|
166
|
+
<a href="index.html"><div class="brand">Pex Docs (v2.69.0)</div></a>
|
|
167
167
|
</div>
|
|
168
168
|
<div class="header-right">
|
|
169
169
|
<div class="theme-toggle-container theme-toggle-header">
|
|
@@ -322,12 +322,12 @@ build executable .pex files. This is described more thoroughly in
|
|
|
322
322
|
</div>
|
|
323
323
|
<div class="right-details">
|
|
324
324
|
<div class="icons">
|
|
325
|
-
<a class="muted-link " href="https://pypi.org/project/pex/2.
|
|
325
|
+
<a class="muted-link " href="https://pypi.org/project/pex/2.69.0/" aria-label="PyPI"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
|
|
326
326
|
<path d="M439.8 200.5c-7.7-30.9-22.3-54.2-53.4-54.2h-40.1v47.4c0 36.8-31.2 67.8-66.8 67.8H172.7c-29.2 0-53.4 25-53.4 54.3v101.8c0 29 25.2 46 53.4 54.3 33.8 9.9 66.3 11.7 106.8 0 26.9-7.8 53.4-23.5 53.4-54.3v-40.7H226.2v-13.6h160.2c31.1 0 42.6-21.7 53.4-54.2 11.2-33.5 10.7-65.7 0-108.6zM286.2 404c11.1 0 20.1 9.1 20.1 20.3 0 11.3-9 20.4-20.1 20.4-11 0-20.1-9.2-20.1-20.4.1-11.3 9.1-20.3 20.1-20.3zM167.8 248.1h106.8c29.7 0 53.4-24.5 53.4-54.3V91.9c0-29-24.4-50.7-53.4-55.6-35.8-5.9-74.7-5.6-106.8.1-45.2 8-53.4 24.7-53.4 55.6v40.7h106.9v13.6h-147c-31.1 0-58.3 18.7-66.8 54.2-9.8 40.7-10.2 66.1 0 108.6 7.6 31.6 25.7 54.2 56.8 54.2H101v-48.8c0-35.3 30.5-66.4 66.8-66.4zm-6.7-142.6c-11.1 0-20.1-9.1-20.1-20.3.1-11.3 9-20.4 20.1-20.4 11 0 20.1 9.2 20.1 20.4s-9 20.3-20.1 20.3z">
|
|
327
327
|
</path>
|
|
328
328
|
</svg>
|
|
329
329
|
</a>
|
|
330
|
-
<a class="muted-link " href="https://github.com/pex-tool/pex/releases/download/v2.
|
|
330
|
+
<a class="muted-link " href="https://github.com/pex-tool/pex/releases/download/v2.69.0/pex" aria-label="Download"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 640 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
|
|
331
331
|
<path d="M537.6 226.6c4.1-10.7 6.4-22.4 6.4-34.6 0-53-43-96-96-96-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32c-88.4 0-160 71.6-160 160 0 2.7.1 5.4.2 8.1C40.2 219.8 0 273.2 0 336c0 79.5 64.5 144 144 144h368c70.7 0 128-57.3 128-128 0-61.9-44-113.6-102.4-125.4zm-132.9 88.7L299.3 420.7c-6.2 6.2-16.4 6.2-22.6 0L171.3 315.3c-10.1-10.1-2.9-27.3 11.3-27.3H248V176c0-8.8 7.2-16 16-16h48c8.8 0 16 7.2 16 16v112h65.4c14.2 0 21.4 17.2 11.3 27.3z">
|
|
332
332
|
</path>
|
|
333
333
|
</svg>
|
|
@@ -371,7 +371,7 @@ build executable .pex files. This is described more thoroughly in
|
|
|
371
371
|
|
|
372
372
|
</aside>
|
|
373
373
|
</div>
|
|
374
|
-
</div><script src="_static/documentation_options.js?v=
|
|
374
|
+
</div><script src="_static/documentation_options.js?v=525daa63"></script>
|
|
375
375
|
<script src="_static/doctools.js?v=9bcbadda"></script>
|
|
376
376
|
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
|
377
377
|
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
pex/hashing.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright 2022 Pex project contributors.
|
|
2
2
|
# Licensed under the Apache License, Version 2.0 (see LICENSE).
|
|
3
3
|
|
|
4
|
-
from __future__ import absolute_import
|
|
4
|
+
from __future__ import absolute_import, print_function
|
|
5
5
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import os
|
|
@@ -241,10 +241,12 @@ def dir_hash(
|
|
|
241
241
|
def iter_files():
|
|
242
242
|
# type: () -> Iterator[Text]
|
|
243
243
|
for root, dirs, files in os.walk(top, followlinks=True):
|
|
244
|
-
dirs[:] = [
|
|
244
|
+
dirs[:] = [
|
|
245
|
+
d for d in dirs if dir_filter(os.path.relpath(os.path.join(root, d), top))
|
|
246
|
+
]
|
|
245
247
|
for f in files:
|
|
246
248
|
path = os.path.join(root, f)
|
|
247
|
-
if file_filter(path):
|
|
249
|
+
if file_filter(os.path.relpath(path, top)):
|
|
248
250
|
yield path
|
|
249
251
|
|
|
250
252
|
file_paths = sorted(iter_files())
|
|
@@ -278,16 +280,26 @@ def zip_hash(
|
|
|
278
280
|
else zf.namelist()
|
|
279
281
|
)
|
|
280
282
|
|
|
281
|
-
dirs =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
dirs = set()
|
|
284
|
+
for name in namelist:
|
|
285
|
+
if name.endswith("/"):
|
|
286
|
+
dirname = name.rstrip("/")
|
|
287
|
+
else:
|
|
288
|
+
dirname = os.path.dirname(name)
|
|
289
|
+
while dirname:
|
|
290
|
+
dirs.add(dirname)
|
|
291
|
+
dirname = os.path.dirname(dirname)
|
|
292
|
+
|
|
293
|
+
accept_dirs = frozenset(
|
|
294
|
+
d for d in dirs if dir_filter(os.path.relpath(d, relpath) if relpath else d)
|
|
295
|
+
)
|
|
296
|
+
reject_dirs = tuple("{dir}/".format(dir=path) for path in (dirs - accept_dirs))
|
|
285
297
|
accept_files = sorted(
|
|
286
298
|
name
|
|
287
299
|
for name in namelist
|
|
288
300
|
if not name.endswith("/")
|
|
289
|
-
and not
|
|
290
|
-
and file_filter(os.path.
|
|
301
|
+
and not name.startswith(reject_dirs)
|
|
302
|
+
and file_filter(os.path.relpath(name, relpath) if relpath else name)
|
|
291
303
|
)
|
|
292
304
|
|
|
293
305
|
hashed_names = (
|
|
@@ -297,3 +309,53 @@ def zip_hash(
|
|
|
297
309
|
|
|
298
310
|
for filename in accept_files:
|
|
299
311
|
update_hash(zf.open(filename, "r"), digest)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
if __name__ == "__main__":
|
|
315
|
+
import sys
|
|
316
|
+
import zipfile
|
|
317
|
+
from argparse import ArgumentParser
|
|
318
|
+
|
|
319
|
+
from pex.common import is_pyc_dir, is_pyc_file
|
|
320
|
+
|
|
321
|
+
parser = ArgumentParser()
|
|
322
|
+
parser.add_argument(
|
|
323
|
+
"--exclude-dir",
|
|
324
|
+
dest="exclude_dirs",
|
|
325
|
+
action="append",
|
|
326
|
+
default=[],
|
|
327
|
+
)
|
|
328
|
+
parser.add_argument("--zip-relpath")
|
|
329
|
+
parser.add_argument("paths", nargs="+")
|
|
330
|
+
|
|
331
|
+
options = parser.parse_args()
|
|
332
|
+
exclude_dirs = frozenset(options.exclude_dirs)
|
|
333
|
+
|
|
334
|
+
for path in options.paths:
|
|
335
|
+
digest = Sha256()
|
|
336
|
+
if zipfile.is_zipfile(path):
|
|
337
|
+
zip_hash(
|
|
338
|
+
path,
|
|
339
|
+
digest=digest,
|
|
340
|
+
relpath=options.zip_relpath,
|
|
341
|
+
dir_filter=(
|
|
342
|
+
lambda dir_path: (
|
|
343
|
+
not is_pyc_dir(dir_path) and os.path.basename(dir_path) not in exclude_dirs
|
|
344
|
+
)
|
|
345
|
+
),
|
|
346
|
+
file_filter=lambda f: not is_pyc_file(f),
|
|
347
|
+
)
|
|
348
|
+
elif os.path.isdir(path):
|
|
349
|
+
dir_hash(
|
|
350
|
+
path,
|
|
351
|
+
digest=digest,
|
|
352
|
+
dir_filter=(
|
|
353
|
+
lambda dir_path: (
|
|
354
|
+
not is_pyc_dir(dir_path) and os.path.basename(dir_path) not in exclude_dirs
|
|
355
|
+
)
|
|
356
|
+
),
|
|
357
|
+
file_filter=lambda f: not is_pyc_file(f),
|
|
358
|
+
)
|
|
359
|
+
else:
|
|
360
|
+
print("Can only hash zip files or directories. Skipping file", path, file=sys.stderr)
|
|
361
|
+
print(path, digest.hexdigest(), file=sys.stdout)
|
pex/interpreter_constraints.py
CHANGED
|
@@ -375,7 +375,7 @@ COMPATIBLE_PYTHON_VERSIONS = (
|
|
|
375
375
|
PythonVersion(Lifecycle.EOL, 3, 6, 15),
|
|
376
376
|
PythonVersion(Lifecycle.EOL, 3, 7, 17),
|
|
377
377
|
PythonVersion(Lifecycle.EOL, 3, 8, 20),
|
|
378
|
-
PythonVersion(Lifecycle.
|
|
378
|
+
PythonVersion(Lifecycle.EOL, 3, 9, 25),
|
|
379
379
|
PythonVersion(Lifecycle.STABLE, 3, 10, 19),
|
|
380
380
|
PythonVersion(Lifecycle.STABLE, 3, 11, 14),
|
|
381
381
|
PythonVersion(Lifecycle.STABLE, 3, 12, 12),
|
pex/jobs.py
CHANGED
|
@@ -724,6 +724,19 @@ def iter_map_parallel(
|
|
|
724
724
|
if not input_items:
|
|
725
725
|
return
|
|
726
726
|
|
|
727
|
+
# We want each of the job slots to process MULTIPROCESSING_DEFAULT_MIN_AVERAGE_LOAD on
|
|
728
|
+
# average in order to overcome multiprocessing overheads. If we don't need at least 2 slots, we
|
|
729
|
+
# are unlikely to get any boost from multiprocessing.
|
|
730
|
+
needed_slots = len(input_items) // min_average_load
|
|
731
|
+
if needed_slots < 2:
|
|
732
|
+
for item in input_items:
|
|
733
|
+
yield function(item)
|
|
734
|
+
return
|
|
735
|
+
|
|
736
|
+
# Of course, if there are fewer available cores than that or the user has pinned max jobs lower,
|
|
737
|
+
# we clamp to that.
|
|
738
|
+
pool_size = min(needed_slots, _sanitize_max_jobs(max_jobs))
|
|
739
|
+
|
|
727
740
|
if costing_function is not None:
|
|
728
741
|
# We ensure no job slot is so unlucky as to get all the biggest jobs and thus become an
|
|
729
742
|
# un-necessarily long pole by sorting based on cost. Some examples to illustrate the effect
|
|
@@ -740,12 +753,6 @@ def iter_map_parallel(
|
|
|
740
753
|
#
|
|
741
754
|
input_items.sort(key=costing_function, reverse=True)
|
|
742
755
|
|
|
743
|
-
# We want each of the job slots above to process MULTIPROCESSING_DEFAULT_MIN_AVERAGE_LOAD on
|
|
744
|
-
# average in order to overcome multiprocessing overheads. Of course, if there are fewer
|
|
745
|
-
# available cores than that or the user has pinned max jobs lower, we clamp to that. Finally, we
|
|
746
|
-
# always want at least two slots to ensure we process input items in parallel.
|
|
747
|
-
pool_size = max(2, min(len(input_items) // min_average_load, _sanitize_max_jobs(max_jobs)))
|
|
748
|
-
|
|
749
756
|
apply_function = functools.partial(_apply_function, function)
|
|
750
757
|
|
|
751
758
|
slots = defaultdict(list) # type: DefaultDict[int, List[float]]
|
pex/pep_376.py
CHANGED
|
@@ -78,6 +78,14 @@ class InstalledFile(object):
|
|
|
78
78
|
size = attr.ib(default=None) # type: Optional[int]
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
@attr.s(frozen=True)
|
|
82
|
+
class InstalledDirectory(object):
|
|
83
|
+
# N.B.: Although directory entries should not exist in a RECORD, they have been seen in the
|
|
84
|
+
# wild; so we're forced to deal with them. See: https://github.com/pex-tool/pex/issues/2998
|
|
85
|
+
|
|
86
|
+
dir_info = attr.ib() # type: InstalledFile
|
|
87
|
+
|
|
88
|
+
|
|
81
89
|
def create_installed_file(
|
|
82
90
|
path, # type: Text
|
|
83
91
|
dest_dir, # type: str
|
|
@@ -121,18 +129,21 @@ class Record(object):
|
|
|
121
129
|
def write_fp(
|
|
122
130
|
cls,
|
|
123
131
|
fp, # type: IO
|
|
124
|
-
installed_files, # type: Iterable[InstalledFile]
|
|
132
|
+
installed_files, # type: Iterable[Union[InstalledFile, InstalledDirectory]]
|
|
125
133
|
eol="\n", # type: str
|
|
126
134
|
):
|
|
127
135
|
# type: (...) -> None
|
|
128
136
|
csv_writer = csv.writer(fp, delimiter=",", quotechar='"', lineterminator=eol)
|
|
129
137
|
for installed_file in installed_files:
|
|
130
|
-
|
|
138
|
+
if isinstance(installed_file, InstalledDirectory):
|
|
139
|
+
csv_writer.writerow(attr.astuple(installed_file.dir_info, recurse=False))
|
|
140
|
+
else:
|
|
141
|
+
csv_writer.writerow(attr.astuple(installed_file, recurse=False))
|
|
131
142
|
|
|
132
143
|
@classmethod
|
|
133
144
|
def write_bytes(
|
|
134
145
|
cls,
|
|
135
|
-
installed_files, # type: Iterable[InstalledFile]
|
|
146
|
+
installed_files, # type: Iterable[Union[InstalledFile, InstalledDirectory]]
|
|
136
147
|
eol="\n", # type: str
|
|
137
148
|
):
|
|
138
149
|
# type: (...) -> bytes
|
|
@@ -149,7 +160,7 @@ class Record(object):
|
|
|
149
160
|
def write(
|
|
150
161
|
cls,
|
|
151
162
|
dst, # type: Text
|
|
152
|
-
installed_files, # type: Iterable[InstalledFile]
|
|
163
|
+
installed_files, # type: Iterable[Union[InstalledFile, InstalledDirectory]]
|
|
153
164
|
eol="\n", # type: str
|
|
154
165
|
):
|
|
155
166
|
# type: (...) -> None
|
|
@@ -165,7 +176,7 @@ class Record(object):
|
|
|
165
176
|
lines, # type: Union[FileInput[Text], Iterator[Text]]
|
|
166
177
|
exclude=None, # type: Optional[Callable[[Text], bool]]
|
|
167
178
|
):
|
|
168
|
-
# type: (...) -> Iterator[InstalledFile]
|
|
179
|
+
# type: (...) -> Iterator[Union[InstalledFile, InstalledDirectory]]
|
|
169
180
|
|
|
170
181
|
# The RECORD is a csv file with the path to each installed file in the 1st column.
|
|
171
182
|
# See: https://peps.python.org/pep-0376/#record
|
|
@@ -177,7 +188,11 @@ class Record(object):
|
|
|
177
188
|
continue
|
|
178
189
|
file_hash = Hash(fingerprint) if fingerprint else None
|
|
179
190
|
size = int(file_size) if file_size else None
|
|
180
|
-
|
|
191
|
+
installed_file = InstalledFile(path=path, hash=file_hash, size=size)
|
|
192
|
+
if path.endswith("/"):
|
|
193
|
+
yield InstalledDirectory(dir_info=installed_file)
|
|
194
|
+
else:
|
|
195
|
+
yield installed_file
|
|
181
196
|
|
|
182
197
|
project_name = attr.ib() # type: str
|
|
183
198
|
version = attr.ib() # type: str
|
pex/pep_427.py
CHANGED
|
@@ -36,7 +36,7 @@ from pex.exceptions import production_assert, reportable_unexpected_error_msg
|
|
|
36
36
|
from pex.executables import chmod_plus_x, is_python_script
|
|
37
37
|
from pex.installed_wheel import InstalledWheel
|
|
38
38
|
from pex.interpreter import PythonInterpreter
|
|
39
|
-
from pex.pep_376 import InstalledFile, Record, create_installed_file
|
|
39
|
+
from pex.pep_376 import InstalledDirectory, InstalledFile, Record, create_installed_file
|
|
40
40
|
from pex.pep_440 import Version
|
|
41
41
|
from pex.pep_503 import ProjectName
|
|
42
42
|
from pex.sysconfig import SCRIPT_DIR, SysPlatform
|
|
@@ -775,11 +775,24 @@ def create_whl(
|
|
|
775
775
|
)
|
|
776
776
|
else:
|
|
777
777
|
for installed_file in Record.read(lines=iter(record_data.decode("utf-8").splitlines())):
|
|
778
|
-
|
|
778
|
+
path = (
|
|
779
|
+
installed_file.dir_info.path
|
|
780
|
+
if isinstance(installed_file, InstalledDirectory)
|
|
781
|
+
else installed_file.path
|
|
782
|
+
)
|
|
783
|
+
src = os.path.join(whl_chroot, path)
|
|
784
|
+
if not os.path.exists(src):
|
|
785
|
+
production_assert(
|
|
786
|
+
isinstance(installed_file, InstalledDirectory),
|
|
787
|
+
"The wheel entry {filename} is unexpectedly missing from {source}.",
|
|
788
|
+
filename=path,
|
|
789
|
+
source=wheel_to_create.source,
|
|
790
|
+
)
|
|
791
|
+
safe_mkdir(src)
|
|
779
792
|
if use_system_time:
|
|
780
|
-
zip_fp.write(src,
|
|
793
|
+
zip_fp.write(src, path)
|
|
781
794
|
else:
|
|
782
|
-
zip_fp.write_deterministic(src,
|
|
795
|
+
zip_fp.write_deterministic(src, path)
|
|
783
796
|
return wheel_path
|
|
784
797
|
|
|
785
798
|
|
|
@@ -869,7 +882,11 @@ def install_wheel(
|
|
|
869
882
|
eol = "\r\n" if record_lines[0].endswith("\r\n") else "\n"
|
|
870
883
|
|
|
871
884
|
if not record_data or any(
|
|
872
|
-
os.path.isabs(
|
|
885
|
+
os.path.isabs(
|
|
886
|
+
installed_file.dir_info.path
|
|
887
|
+
if isinstance(installed_file, InstalledDirectory)
|
|
888
|
+
else installed_file.path
|
|
889
|
+
)
|
|
873
890
|
for installed_file in Record.read(lines=iter(record_lines))
|
|
874
891
|
):
|
|
875
892
|
prefix = "The RECORD in {whl}".format(whl=os.path.basename(whl))
|
|
@@ -935,10 +952,15 @@ def install_wheel(
|
|
|
935
952
|
requested_relpath = wheel.metadata_path("REQUESTED")
|
|
936
953
|
zip_metadata_relpath = wheel.pex_metadata_path(ZipMetadata.FILENAME)
|
|
937
954
|
|
|
938
|
-
installed_files = [] # type: List[InstalledFile]
|
|
955
|
+
installed_files = [] # type: List[Union[InstalledFile, InstalledDirectory]]
|
|
939
956
|
provenance = [] # type: List[Tuple[Text, Text]]
|
|
940
957
|
symlinked = set() # type: Set[Text]
|
|
941
|
-
for
|
|
958
|
+
for installed_file_or_dir in Record.read(lines=iter(record_data.decode("utf-8").splitlines())):
|
|
959
|
+
if isinstance(installed_file_or_dir, InstalledDirectory):
|
|
960
|
+
installed_files.append(installed_file_or_dir)
|
|
961
|
+
continue
|
|
962
|
+
|
|
963
|
+
installed_file = installed_file_or_dir
|
|
942
964
|
if installed_file.path == record_relpath:
|
|
943
965
|
record_eol = _detect_record_eol(os.path.join(wheel.location, installed_file.path))
|
|
944
966
|
installed_files.append(InstalledFile(path=record_relpath, hash=None, size=None))
|
|
@@ -1051,7 +1073,7 @@ def install_wheel(
|
|
|
1051
1073
|
py_files = [
|
|
1052
1074
|
os.path.join(dest, installed_file.path)
|
|
1053
1075
|
for installed_file in installed_files
|
|
1054
|
-
if installed_file.path.endswith(".py")
|
|
1076
|
+
if isinstance(installed_file, InstalledFile) and installed_file.path.endswith(".py")
|
|
1055
1077
|
]
|
|
1056
1078
|
process = subprocess.Popen(
|
|
1057
1079
|
args=args + py_files, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
pex/pex_builder.py
CHANGED
|
@@ -470,10 +470,7 @@ class PEXBuilder(object):
|
|
|
470
470
|
def _prepare_code(self):
|
|
471
471
|
chroot_path = self._chroot.path()
|
|
472
472
|
self._pex_info.code_hash = CacheHelper.pex_code_hash(
|
|
473
|
-
chroot_path,
|
|
474
|
-
exclude_dirs=tuple(
|
|
475
|
-
os.path.join(chroot_path, d) for d in (layout.BOOTSTRAP_DIR, layout.DEPS_DIR)
|
|
476
|
-
),
|
|
473
|
+
chroot_path, exclude_dirs=(layout.BOOTSTRAP_DIR, layout.DEPS_DIR)
|
|
477
474
|
)
|
|
478
475
|
self._pex_info.pex_hash = hashlib.sha1(self._pex_info.dump().encode("utf-8")).hexdigest()
|
|
479
476
|
self._chroot.write(self._pex_info.dump().encode("utf-8"), PexInfo.PATH, label="manifest")
|
pex/pip/local_project.py
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
from __future__ import absolute_import
|
|
5
5
|
|
|
6
6
|
import os.path
|
|
7
|
-
import tarfile
|
|
8
7
|
|
|
9
|
-
from pex import hashing
|
|
8
|
+
from pex import hashing, sdist
|
|
10
9
|
from pex.build_system import pep_517
|
|
11
10
|
from pex.common import temporary_dir
|
|
12
11
|
from pex.pip.version import PipVersionValue
|
|
@@ -33,25 +32,18 @@ def digest_local_project(
|
|
|
33
32
|
# type: (...) -> Union[str, Error]
|
|
34
33
|
with TRACER.timed("Fingerprinting local project at {directory}".format(directory=directory)):
|
|
35
34
|
with temporary_dir() as td:
|
|
36
|
-
|
|
35
|
+
sdist_path_or_error = pep_517.build_sdist(
|
|
37
36
|
project_directory=directory,
|
|
38
37
|
dist_dir=os.path.join(td, "dists"),
|
|
39
38
|
pip_version=pip_version,
|
|
40
39
|
target=target,
|
|
41
40
|
resolver=resolver,
|
|
42
41
|
)
|
|
43
|
-
if isinstance(
|
|
44
|
-
return
|
|
45
|
-
|
|
42
|
+
if isinstance(sdist_path_or_error, Error):
|
|
43
|
+
return sdist_path_or_error
|
|
44
|
+
sdist_path = sdist_path_or_error
|
|
46
45
|
|
|
47
46
|
extract_dir = dest_dir or os.path.join(td, "extracted")
|
|
48
|
-
|
|
49
|
-
tf.extractall(extract_dir)
|
|
50
|
-
listing = os.listdir(extract_dir)
|
|
51
|
-
assert len(listing) == 1, (
|
|
52
|
-
"Expected sdist generated for {directory} to contain one top-level directory, "
|
|
53
|
-
"found:\n{listing}".format(directory=directory, listing="\n".join(listing))
|
|
54
|
-
)
|
|
55
|
-
project_dir = os.path.join(extract_dir, listing[0])
|
|
47
|
+
project_dir = sdist.extract_tarball(sdist_path, dest_dir=extract_dir)
|
|
56
48
|
hashing.dir_hash(directory=project_dir, digest=digest)
|
|
57
49
|
return os.path.join(extract_dir, project_dir)
|
pex/pip/tool.py
CHANGED
|
@@ -552,8 +552,7 @@ class Pip(object):
|
|
|
552
552
|
)
|
|
553
553
|
return Job(command=command, process=process, finalizer=finalizer, context="pip")
|
|
554
554
|
|
|
555
|
-
|
|
556
|
-
def _iter_build_configuration_options(build_configuration):
|
|
555
|
+
def _iter_build_configuration_options(self, build_configuration):
|
|
557
556
|
# type: (BuildConfiguration) -> Iterator[str]
|
|
558
557
|
|
|
559
558
|
# N.B.: BuildConfiguration maintains invariants that ensure --only-binary, --no-binary,
|
|
@@ -576,7 +575,8 @@ class Pip(object):
|
|
|
576
575
|
if build_configuration.prefer_older_binary:
|
|
577
576
|
yield "--prefer-binary"
|
|
578
577
|
|
|
579
|
-
|
|
578
|
+
# N.B.: In 25.3 `--use-pep517` became the default and only option.
|
|
579
|
+
if build_configuration.use_pep517 is not None and self.version < PipVersion.v25_3:
|
|
580
580
|
yield "--use-pep517" if build_configuration.use_pep517 else "--no-use-pep517"
|
|
581
581
|
|
|
582
582
|
if not build_configuration.build_isolation:
|
pex/pip/vcs.py
CHANGED
|
@@ -3,26 +3,42 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import absolute_import
|
|
5
5
|
|
|
6
|
-
import glob
|
|
7
6
|
import os
|
|
8
7
|
import re
|
|
9
8
|
|
|
10
9
|
from pex import hashing
|
|
11
10
|
from pex.artifact_url import VCS, Fingerprint
|
|
12
|
-
from pex.common import is_pyc_dir, is_pyc_file
|
|
11
|
+
from pex.common import is_pyc_dir, is_pyc_file
|
|
12
|
+
from pex.exceptions import reportable_unexpected_error_msg
|
|
13
13
|
from pex.hashing import Sha256
|
|
14
14
|
from pex.pep_440 import Version
|
|
15
15
|
from pex.pep_503 import ProjectName
|
|
16
16
|
from pex.result import Error, try_
|
|
17
|
-
from pex.tracer import TRACER
|
|
18
17
|
from pex.typing import TYPE_CHECKING
|
|
19
18
|
|
|
20
19
|
if TYPE_CHECKING:
|
|
21
|
-
|
|
20
|
+
# N.B.: The `re.Pattern` type is not available in all Python versions Pex supports.
|
|
21
|
+
from re import Pattern # type: ignore[attr-defined]
|
|
22
|
+
from typing import Callable, Optional, Text, Tuple, Union
|
|
22
23
|
|
|
23
24
|
from pex.hashing import HintedDigest
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
def _project_name_re(project_name):
|
|
28
|
+
# type: (ProjectName) -> str
|
|
29
|
+
return project_name.normalized.replace("-", "[-_.]+")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _built_source_dist_pattern(project_name):
|
|
33
|
+
# type: (ProjectName) -> Pattern
|
|
34
|
+
return re.compile(
|
|
35
|
+
r"(?P<project_name>{project_name_re})-(?P<version>.+)\.zip".format(
|
|
36
|
+
project_name_re=_project_name_re(project_name)
|
|
37
|
+
),
|
|
38
|
+
re.IGNORECASE,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
26
42
|
def _find_built_source_dist(
|
|
27
43
|
build_dir, # type: str
|
|
28
44
|
project_name, # type: ProjectName
|
|
@@ -34,12 +50,7 @@ def _find_built_source_dist(
|
|
|
34
50
|
# encoded in: `pip._internal.req.req_install.InstallRequirement.archive`.
|
|
35
51
|
|
|
36
52
|
listing = os.listdir(build_dir)
|
|
37
|
-
pattern =
|
|
38
|
-
r"{project_name}-(?P<version>.+)\.zip".format(
|
|
39
|
-
project_name=project_name.normalized.replace("-", "[-_.]+")
|
|
40
|
-
),
|
|
41
|
-
re.IGNORECASE,
|
|
42
|
-
)
|
|
53
|
+
pattern = _built_source_dist_pattern(project_name)
|
|
43
54
|
for name in listing:
|
|
44
55
|
match = pattern.match(name)
|
|
45
56
|
if match and Version(match.group("version")) == version:
|
|
@@ -58,23 +69,66 @@ def _find_built_source_dist(
|
|
|
58
69
|
|
|
59
70
|
def fingerprint_downloaded_vcs_archive(
|
|
60
71
|
download_dir, # type: str
|
|
61
|
-
project_name, # type:
|
|
62
|
-
version, # type:
|
|
72
|
+
project_name, # type: ProjectName
|
|
73
|
+
version, # type: Version
|
|
63
74
|
vcs, # type: VCS.Value
|
|
64
75
|
):
|
|
65
76
|
# type: (...) -> Tuple[Fingerprint, str]
|
|
66
77
|
|
|
67
78
|
archive_path = try_(
|
|
68
|
-
_find_built_source_dist(
|
|
69
|
-
build_dir=download_dir, project_name=ProjectName(project_name), version=Version(version)
|
|
70
|
-
)
|
|
79
|
+
_find_built_source_dist(build_dir=download_dir, project_name=project_name, version=version)
|
|
71
80
|
)
|
|
72
81
|
digest = Sha256()
|
|
73
|
-
digest_vcs_archive(archive_path=archive_path, vcs=vcs, digest=digest)
|
|
82
|
+
digest_vcs_archive(project_name=project_name, archive_path=archive_path, vcs=vcs, digest=digest)
|
|
74
83
|
return Fingerprint.from_digest(digest), archive_path
|
|
75
84
|
|
|
76
85
|
|
|
86
|
+
def _vcs_dir_filter(
|
|
87
|
+
vcs, # type: VCS.Value
|
|
88
|
+
project_name, # type: ProjectName
|
|
89
|
+
):
|
|
90
|
+
# type: (...) -> Callable[[Text], bool]
|
|
91
|
+
|
|
92
|
+
# Ignore VCS control directories for the purposes of fingerprinting the version controlled
|
|
93
|
+
# source tree. VCS control directories can contain non-reproducible content (Git at least
|
|
94
|
+
# has files that contain timestamps).
|
|
95
|
+
#
|
|
96
|
+
# We cannot prune these directories from the source archive directly unfortunately since
|
|
97
|
+
# some build processes use VCS version information to derive their version numbers (C.F.:
|
|
98
|
+
# https://pypi.org/project/setuptools-scm/). As such, we'll get a stable fingerprint, but be
|
|
99
|
+
# forced to re-build a wheel each time the VCS requirement is re-locked later, even when it
|
|
100
|
+
# hashes the same.
|
|
101
|
+
vcs_control_dir = ".{vcs}".format(vcs=vcs)
|
|
102
|
+
|
|
103
|
+
# N.B.: If the VCS project uses setuptools as its build backend, depending on the version of
|
|
104
|
+
# Pip used, the VCS checkout can have a `<project name>.egg-info/` directory littering its root
|
|
105
|
+
# left over from Pip generating project metadata to determine version and dependencies. No other
|
|
106
|
+
# well known build-backend has this problem at this time (checked hatchling, poetry-core,
|
|
107
|
+
# pdm-backend and uv_build).
|
|
108
|
+
# C.F.: https://github.com/pypa/pip/pull/13602
|
|
109
|
+
egg_info_dir_re = re.compile(
|
|
110
|
+
r"^{project_name_re}\.egg-info$".format(project_name_re=_project_name_re(project_name)),
|
|
111
|
+
re.IGNORECASE,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def vcs_dir_filter(dir_path):
|
|
115
|
+
# type: (Text) -> bool
|
|
116
|
+
if is_pyc_dir(dir_path):
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
base_dir_name = dir_path.split(os.sep)[0]
|
|
120
|
+
return base_dir_name != vcs_control_dir and not egg_info_dir_re.match(base_dir_name)
|
|
121
|
+
|
|
122
|
+
return vcs_dir_filter
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _vcs_file_filter(vcs):
|
|
126
|
+
# type: (VCS.Value) -> Callable[[Text], bool]
|
|
127
|
+
return lambda f: not is_pyc_file(f)
|
|
128
|
+
|
|
129
|
+
|
|
77
130
|
def digest_vcs_archive(
|
|
131
|
+
project_name, # type: ProjectName
|
|
78
132
|
archive_path, # type: str
|
|
79
133
|
vcs, # type: VCS.Value
|
|
80
134
|
digest, # type: HintedDigest
|
|
@@ -84,22 +138,32 @@ def digest_vcs_archive(
|
|
|
84
138
|
# All VCS requirements are prepared as zip archives as encoded in:
|
|
85
139
|
# `pip._internal.req.req_install.InstallRequirement.archive` and the archive is already offset
|
|
86
140
|
# by a subdirectory (if any).
|
|
87
|
-
with TRACER.timed(
|
|
88
|
-
"Digesting {archive} {vcs} archive".format(archive=os.path.basename(archive_path), vcs=vcs)
|
|
89
|
-
), temporary_dir() as chroot, open_zip(archive_path) as archive:
|
|
90
|
-
# TODO(John Sirois): Consider implementing zip_hash to avoid the extractall.
|
|
91
|
-
archive.extractall(chroot)
|
|
92
141
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
142
|
+
# The zip archives created by Pip have a single project name top-level directory housing
|
|
143
|
+
# the full clone. We look for that to get a consistent clone hash with a bare clone.
|
|
144
|
+
match = _built_source_dist_pattern(project_name).match(os.path.basename(archive_path))
|
|
145
|
+
if match is None:
|
|
146
|
+
raise AssertionError(
|
|
147
|
+
reportable_unexpected_error_msg(
|
|
148
|
+
"Failed to determine the project name prefix for the VCS zip {zip} with expected "
|
|
149
|
+
"canonical project name {project_name}".format(
|
|
150
|
+
zip=archive_path, project_name=project_name
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
top_dir = match.group("project_name")
|
|
98
155
|
|
|
99
|
-
|
|
156
|
+
hashing.zip_hash(
|
|
157
|
+
zip_path=archive_path,
|
|
158
|
+
digest=digest,
|
|
159
|
+
relpath=top_dir,
|
|
160
|
+
dir_filter=_vcs_dir_filter(vcs, project_name),
|
|
161
|
+
file_filter=_vcs_file_filter(vcs),
|
|
162
|
+
)
|
|
100
163
|
|
|
101
164
|
|
|
102
165
|
def digest_vcs_repo(
|
|
166
|
+
project_name, # type: ProjectName
|
|
103
167
|
repo_path, # type: str
|
|
104
168
|
vcs, # type: VCS.Value
|
|
105
169
|
digest, # type: HintedDigest
|
|
@@ -107,24 +171,9 @@ def digest_vcs_repo(
|
|
|
107
171
|
):
|
|
108
172
|
# type: (...) -> None
|
|
109
173
|
|
|
110
|
-
# Ignore VCS control directories for the purposes of fingerprinting the version controlled
|
|
111
|
-
# source tree. VCS control directories can contain non-reproducible content (Git at least
|
|
112
|
-
# has files that contain timestamps).
|
|
113
|
-
#
|
|
114
|
-
# We cannot prune these directories from the source archive directly unfortunately since
|
|
115
|
-
# some build processes use VCS version information to derive their version numbers (C.F.:
|
|
116
|
-
# https://pypi.org/project/setuptools-scm/). As such, we'll get a stable fingerprint, but be
|
|
117
|
-
# forced to re-build a wheel each time the VCS requirement is re-locked later, even when it
|
|
118
|
-
# hashes the same.
|
|
119
|
-
vcs_control_dir = ".{vcs}".format(vcs=vcs)
|
|
120
|
-
|
|
121
174
|
hashing.dir_hash(
|
|
122
175
|
directory=os.path.join(repo_path, subdirectory) if subdirectory else repo_path,
|
|
123
176
|
digest=digest,
|
|
124
|
-
dir_filter=(
|
|
125
|
-
|
|
126
|
-
not is_pyc_dir(dir_path) and os.path.basename(dir_path) != vcs_control_dir
|
|
127
|
-
)
|
|
128
|
-
),
|
|
129
|
-
file_filter=lambda f: not is_pyc_file(f),
|
|
177
|
+
dir_filter=_vcs_dir_filter(vcs, project_name),
|
|
178
|
+
file_filter=_vcs_file_filter(vcs),
|
|
130
179
|
)
|
pex/pip/version.py
CHANGED
|
@@ -369,6 +369,13 @@ class PipVersion(Enum["PipVersionValue"]):
|
|
|
369
369
|
requires_python=">=3.9,<3.16",
|
|
370
370
|
)
|
|
371
371
|
|
|
372
|
+
v25_3 = PipVersionValue(
|
|
373
|
+
version="25.3",
|
|
374
|
+
setuptools_version="80.9.0",
|
|
375
|
+
wheel_version="0.45.1",
|
|
376
|
+
requires_python=">=3.9,<3.16",
|
|
377
|
+
)
|
|
378
|
+
|
|
372
379
|
VENDORED = v20_3_4_patched
|
|
373
380
|
LATEST = LatestPipVersion()
|
|
374
381
|
LATEST_COMPATIBLE = LatestCompatiblePipVersion()
|