Skip to content

Commit ce30d98

Browse files
authored
Accept more foreign --platform "YOLO-mode" wheels. (#2607)
Previously, when speculatively building a wheel from an sdist for a foreign platform target, the wheel tags needed to match the foreign platform target's tags exactly in all cases. For `--complete-platform` this makes sense, we have complete information about the foreign platform target's tags, but for an abbreviated `--platform` it does not since we have abbreviated information that is not enough to know a tag mismatch definitively signals the wheel will never work on the foreign platform target. Now only those definitive cases (e.g.: the speculative wheel is Linux but the foreign abbreviated platform is macOS) are rejected. This will let some eventually failing wheels though, but the warning added in #2533 already covers this risk.
1 parent e1c9c8d commit ce30d98

8 files changed

+114
-58
lines changed

CHANGES.md

+14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Release Notes
22

3+
## 2.24.2
4+
5+
This release fixes a long-standing bug in "YOLO-mode" foreign platform
6+
speculative wheel builds. Previously if the speculatively built wheel
7+
had tags that did not match the foreign platform, the process errored
8+
pre-emptively. This was correct for complete foreign platforms, where
9+
all tag information is known, but not for all cases of abbreviated
10+
platforms, where the failure was overly aggressive in some cases. Now
11+
foreign abbreviated platform speculative builds are only rejected when
12+
there is enough information to be sure the speculatively built wheel
13+
definitely cannot work on the foreign abbreviated platform.
14+
15+
* Accept more foreign `--platform` "YOLO-mode" wheels. (#2607)
16+
317
## 2.24.1
418

519
This release fixes `pex3 cache prune` handling of cached Pips.

docker/user/create_docker_image_user.sh

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
set -xeuo pipefail
44

55
if (( $# != 4 )); then
6-
echo >2 "Usage $0 <user> <uid> <group> <gid>"
6+
echo >&2 "Usage $0 <user> <uid> <group> <gid>"
77
exit 1
88
fi
99

1010
uid=$2
1111
gid=$4
1212

13+
if id -un ubuntu; then
14+
userdel -r ubuntu
15+
fi
16+
1317
if ! id -g ${gid} >/dev/null; then
1418
group=$3
1519
addgroup --gid=${gid} ${group} >&2

pex/environment.py

+12-14
Original file line numberDiff line numberDiff line change
@@ -527,20 +527,18 @@ def _root_requirements_iter(
527527
)
528528
unavailable_dists = self._unavailable_dists_by_project_name.get(project_name)
529529
if unavailable_dists:
530-
message += (
531-
"\nFound {count} {distributions} for {project_name} that do not apply:\n"
532-
"{unavailable_dists}".format(
533-
count=len(unavailable_dists),
534-
distributions=pluralize(unavailable_dists, "distribution"),
535-
project_name=project_name,
536-
unavailable_dists="\n".join(
537-
"{index}.) {message}".format(
538-
index=index,
539-
message=unavailable_dist.render_message(self._target),
540-
)
541-
for index, unavailable_dist in enumerate(unavailable_dists, start=1)
542-
),
543-
)
530+
message += "\nFound {count} {distributions} for {project_name} that {does} not apply:\n" "{unavailable_dists}".format(
531+
count=len(unavailable_dists),
532+
distributions=pluralize(unavailable_dists, "distribution"),
533+
project_name=project_name,
534+
does="does" if len(unavailable_dists) == 1 else "do",
535+
unavailable_dists="\n".join(
536+
"{index}.) {message}".format(
537+
index=index,
538+
message=unavailable_dist.render_message(self._target),
539+
)
540+
for index, unavailable_dist in enumerate(unavailable_dists, start=1)
541+
),
544542
)
545543
raise ResolveError(message)
546544
candidates = [

pex/resolver.py

+47-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from pex.network_configuration import NetworkConfiguration
2727
from pex.orderedset import OrderedSet
2828
from pex.pep_376 import InstalledWheel
29+
from pex.pep_425 import CompatibilityTags
2930
from pex.pep_427 import InstallableType, WheelError, install_wheel_chroot
3031
from pex.pep_503 import ProjectName
3132
from pex.pip.download_observer import DownloadObserver
@@ -44,6 +45,7 @@
4445
check_resolve,
4546
)
4647
from pex.targets import AbbreviatedPlatform, CompletePlatform, LocalInterpreter, Target, Targets
48+
from pex.third_party.packaging.tags import Tag
4749
from pex.tracer import TRACER
4850
from pex.typing import TYPE_CHECKING
4951
from pex.util import CacheHelper
@@ -53,6 +55,7 @@
5355
from typing import (
5456
DefaultDict,
5557
Dict,
58+
FrozenSet,
5659
Iterable,
5760
Iterator,
5861
List,
@@ -368,7 +371,50 @@ def finalize_build(self, check_compatible=True):
368371
wheel_path = wheels[0]
369372
if check_compatible and self.request.target.is_foreign:
370373
wheel = Distribution.load(wheel_path)
371-
if not self.request.target.wheel_applies(wheel):
374+
wheel_tag_match = self.request.target.wheel_applies(wheel)
375+
incompatible = isinstance(self.request.target, CompletePlatform) and not wheel_tag_match
376+
if (
377+
not incompatible
378+
and not wheel_tag_match
379+
and isinstance(self.request.target, AbbreviatedPlatform)
380+
):
381+
382+
def collect_platforms(tags):
383+
# type: (Iterable[Tag]) -> Tuple[FrozenSet[str], bool]
384+
platforms = [] # type: List[str]
385+
is_linux = False
386+
for tag in tags:
387+
platforms.append(tag.platform)
388+
if "linux" in tag.platform:
389+
is_linux = True
390+
return frozenset(platforms), is_linux
391+
392+
wheel_platform_tags, is_linux_wheel = collect_platforms(
393+
CompatibilityTags.from_wheel(wheel.location)
394+
)
395+
abbreviated_target_platform_tags, is_linux_abbreviated_target = collect_platforms(
396+
self.request.target.supported_tags
397+
)
398+
# N.B.: We can't say much about whether an abbreviated platform will match in the
399+
# end unless the platform is a mismatch (i.e. linux vs mac). We check only for that
400+
# sort of mismatch here. Further, we don't wade into manylinux compatibility and
401+
# just consider a locally built linux wheel may match a linux target.
402+
if not (is_linux_wheel and is_linux_abbreviated_target):
403+
if is_linux_wheel ^ is_linux_abbreviated_target:
404+
incompatible = True
405+
else:
406+
common_platforms = abbreviated_target_platform_tags.intersection(
407+
wheel_platform_tags
408+
)
409+
if not common_platforms:
410+
incompatible = True
411+
elif common_platforms == frozenset(["any"]):
412+
# N.B.: In the "any" platform case, we know we have complete information
413+
# about the foreign abbreviated target platform (the `pip debug` command
414+
# we run to learn compatible tags has enough information to give us all
415+
# the "any" tags accurately); so we can expect an exact wheel tag match.
416+
incompatible = not wheel_tag_match
417+
if incompatible:
372418
raise ValueError(
373419
"No pre-built wheel was available for {project_name} {version}.{eol}"
374420
"Successfully built the wheel {wheel} from the sdist {sdist} but it is not "

pex/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Copyright 2015 Pex project contributors.
22
# Licensed under the Apache License, Version 2.0 (see LICENSE).
33

4-
__version__ = "2.24.1"
4+
__version__ = "2.24.2"

tests/integration/resolve/test_issue_2532.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def test_resolved_wheel_tag_platform_mismatch_warns(
4545
ENV PATH=/pex/venv/bin:$PATH
4646
4747
RUN mkdir /work
48-
WORKDIR = /work
48+
WORKDIR /work
4949
""".format(
5050
pex_wheel=os.path.basename(pex_wheel)
5151
)
@@ -77,7 +77,8 @@ def test_resolved_wheel_tag_platform_mismatch_warns(
7777
stderr=subprocess.PIPE,
7878
)
7979
stdout, stderr = process.communicate()
80-
assert 0 == process.returncode
80+
error = stderr.decode("utf-8")
81+
assert 0 == process.returncode, error
8182

8283
# N.B.: The tags calculated for manylinux_2_28_x86_64-cp-3.11.9-cp311 via `pip -v debug ...`
8384
# are:
@@ -93,7 +94,6 @@ def test_resolved_wheel_tag_platform_mismatch_warns(
9394
#
9495
# Instead of failing the resolve check though, we should just see a warning since both of these
9596
# tags may be compatible at runtime, and, in fact, they are.
96-
error = stderr.decode("utf-8")
9797
assert (
9898
dedent(
9999
"""\

tests/integration/test_issue_996.py

+30-36
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,19 @@
33

44
import multiprocessing
55
import os
6+
import subprocess
67

7-
from pex.common import temporary_dir
88
from pex.interpreter import PythonInterpreter
99
from pex.typing import TYPE_CHECKING
10-
from testing import (
11-
PY39,
12-
PY310,
13-
IntegResults,
14-
ensure_python_interpreter,
15-
make_env,
16-
run_pex_command,
17-
run_simple_pex,
18-
)
10+
from testing import PY39, PY310, IntegResults, ensure_python_interpreter, make_env, run_pex_command
11+
from testing.pytest.tmp import Tempdir
1912

2013
if TYPE_CHECKING:
2114
from typing import List
2215

2316

24-
def test_resolve_local_platform():
25-
# type: () -> None
17+
def test_resolve_local_platform(tmpdir):
18+
# type: (Tempdir) -> None
2619
python39 = ensure_python_interpreter(PY39)
2720
python310 = ensure_python_interpreter(PY310)
2821
pex_python_path = os.pathsep.join((python39, python310))
@@ -35,27 +28,28 @@ def create_platform_pex(args):
3528
env=make_env(PEX_PYTHON_PATH=pex_python_path),
3629
)
3730

38-
with temporary_dir() as td:
39-
pex_file = os.path.join(td, "pex_file")
40-
41-
# N.B.: We use psutil since only an sdist is available for linux and osx and the
42-
# distribution has no dependencies.
43-
args = ["psutil==5.7.0", "-o", pex_file]
44-
45-
# By default, no --platforms are resolved and so distributions must be available in binary
46-
# form.
47-
results = create_platform_pex(args)
48-
results.assert_failure()
49-
50-
# If --platform resolution is enabled however, we should be able to find a corresponding
51-
# local interpreter to perform a full-featured resolve with.
52-
results = create_platform_pex(["--resolve-local-platforms"] + args)
53-
results.assert_success()
54-
55-
output, returncode = run_simple_pex(
56-
pex=pex_file,
57-
args=("-c", "import psutil; print(psutil.cpu_count())"),
58-
interpreter=PythonInterpreter.from_binary(python310),
59-
)
60-
assert 0 == returncode
61-
assert int(output.strip()) >= multiprocessing.cpu_count()
31+
pex_file = tmpdir.join("pex_file")
32+
33+
# N.B.: We use psutil since only an sdist is available for linux and osx and the
34+
# distribution has no dependencies.
35+
build_args = ["psutil==5.7.0", "-o", pex_file]
36+
check_args = [python310, pex_file, "-c", "import psutil; print(psutil.cpu_count())"]
37+
check_env = make_env(PEX_PYTHON_PATH=python310, PEX_VERBOSE=1)
38+
39+
# By default, no --platforms are resolved and so the yolo build process will produce a wheel
40+
# using Python 3.9, which is not compatible with Python 3.10.
41+
create_platform_pex(build_args).assert_success()
42+
process = subprocess.Popen(check_args, env=check_env, stderr=subprocess.PIPE)
43+
_, stderr = process.communicate()
44+
error = stderr.decode("utf-8")
45+
assert process.returncode != 0, error
46+
assert (
47+
"Found 1 distribution for psutil that does not apply:\n"
48+
" 1.) The wheel tags for psutil 5.7.0 are "
49+
) in error
50+
51+
# If --platform resolution is enabled however, we should be able to find a corresponding
52+
# local interpreter to perform a full-featured resolve with.
53+
create_platform_pex(["--resolve-local-platforms"] + build_args).assert_success()
54+
output = subprocess.check_output(check_args, env=check_env).decode("utf-8")
55+
assert int(output.strip()) >= multiprocessing.cpu_count()

tests/integration/test_pex_bootstrapper.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -393,14 +393,14 @@ def test_boot_resolve_fail(
393393
r"No interpreter compatible with the requested constraints was found:\n"
394394
r"\n"
395395
r" A distribution for psutil could not be resolved for {py39_exe}.\n"
396-
r" Found 1 distribution for psutil that do not apply:\n"
396+
r" Found 1 distribution for psutil that does not apply:\n"
397397
r" 1\.\) The wheel tags for psutil 5\.9\.0 are .+ which do not match the supported tags "
398398
r"of {py39_exe}:\n"
399399
r" cp39-cp39-.+\n"
400400
r" ... \d+ more ...\n"
401401
r"\n"
402402
r" A distribution for psutil could not be resolved for {py310_exe}.\n"
403-
r" Found 1 distribution for psutil that do not apply:\n"
403+
r" Found 1 distribution for psutil that does not apply:\n"
404404
r" 1\.\) The wheel tags for psutil 5\.9\.0 are .+ which do not match the supported tags "
405405
r"of {py310_exe}:\n"
406406
r" cp310-cp310-.+\n"

0 commit comments

Comments
 (0)