Skip to content

Commit 9098057

Browse files
authored
Warn when --platform resolves fail tag checks. (#2533)
The addition of a wheel tag compatibility check to the overall post-resolve check in #2512 regressed users of abbreviated `--platform` in some cases by failing PEX builds that would otherwise succeed and, later, actually work at runtime. Keep the spirit of #2512 by emitting a detailed warning at build time with remediation steps instead of failing the build outright. Fixes #2532
1 parent 5e689f9 commit 9098057

File tree

4 files changed

+176
-9
lines changed

4 files changed

+176
-9
lines changed

CHANGES.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Release Notes
22

3+
## 2.19.1
4+
5+
This release fixes a regression introduced by #2512 in the 2.19.0
6+
release when building PEXes using abbreviated `--platform` targets.
7+
Instead of failing certain builds that used to succeed, Pex now warns
8+
that the resulting PEX may fail at runtime and that
9+
`--complete-platform` should be used instead.
10+
11+
# Only warn when `--platform` resolves fail tag checks. (#2533)
12+
313
## 2.19.0
414

515
This release adds support for a new `--pre-resolved-dists` resolver as

pex/resolve/resolvers.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from abc import abstractmethod
99
from collections import OrderedDict, defaultdict
1010

11+
from pex import pex_warnings
1112
from pex.common import pluralize
1213
from pex.dependency_configuration import DependencyConfiguration
1314
from pex.dist_metadata import Distribution, Requirement
@@ -17,7 +18,7 @@
1718
from pex.pip.version import PipVersionValue
1819
from pex.resolve.lockfile.model import Lockfile
1920
from pex.sorted_tuple import SortedTuple
20-
from pex.targets import Target, Targets
21+
from pex.targets import AbbreviatedPlatform, Target, Targets
2122
from pex.typing import TYPE_CHECKING
2223

2324
if TYPE_CHECKING:
@@ -99,6 +100,7 @@ def check_resolve(
99100
resolved_distribution.distribution.metadata.project_name, []
100101
).append(resolved_distribution)
101102

103+
maybe_unsatisfied = defaultdict(list) # type: DefaultDict[Target, List[str]]
102104
unsatisfied = defaultdict(list) # type: DefaultDict[Target, List[str]]
103105
for resolved_distribution in itertools.chain.from_iterable(
104106
resolved_distributions_by_project_name.values()
@@ -129,14 +131,15 @@ def check_resolve(
129131
installed_requirement_dist.distribution
130132
for installed_requirement_dist in installed_requirement_dists
131133
]
132-
if not any(
133-
(
134-
requirement.specifier.contains(resolved_dist.version, prereleases=True)
135-
and target.wheel_applies(resolved_dist)
136-
)
134+
version_matches = any(
135+
requirement.specifier.contains(resolved_dist.version, prereleases=True)
137136
for resolved_dist in resolved_dists
138-
):
139-
unsatisfied[target].append(
137+
)
138+
tags_match = any(
139+
target.wheel_applies(resolved_dist) for resolved_dist in resolved_dists
140+
)
141+
if not version_matches or not tags_match:
142+
message = (
140143
"{dist} requires {requirement} but {count} incompatible {dists_were} "
141144
"resolved:\n {dists}".format(
142145
dist=dist,
@@ -149,6 +152,17 @@ def check_resolve(
149152
),
150153
)
151154
)
155+
if (
156+
version_matches
157+
and not tags_match
158+
and isinstance(target, AbbreviatedPlatform)
159+
):
160+
# We don't know for sure an abbreviated platform doesn't match a wheels tags
161+
# until we are running on that platform; so just warn for these instead of
162+
# hard erroring.
163+
maybe_unsatisfied[target].append(message)
164+
else:
165+
unsatisfied[target].append(message)
152166

153167
if unsatisfied:
154168
unsatisfieds = []
@@ -169,6 +183,31 @@ def check_resolve(
169183
)
170184
)
171185

186+
if maybe_unsatisfied:
187+
maybe_unsatisfieds = []
188+
for target, missing in maybe_unsatisfied.items():
189+
maybe_unsatisfieds.append(
190+
"{target} may not be compatible with:\n {missing}".format(
191+
target=target.render_description(), missing="\n ".join(missing)
192+
)
193+
)
194+
pex_warnings.warn(
195+
"The resolved distributions for {count} {targets} may not be compatible:\n"
196+
"{failures}\n"
197+
"\n"
198+
"Its generally advisable to use `--complete-platform` instead of `--platform` to\n"
199+
"ensure resolved distributions will be compatible with the target platform at\n"
200+
"runtime. For instructions on how to generate a `--complete-platform` see:\n"
201+
" https://docs.pex-tool.org/buildingpex.html#complete-platform ".format(
202+
count=len(maybe_unsatisfieds),
203+
targets=pluralize(maybe_unsatisfieds, "target"),
204+
failures="\n".join(
205+
"{index}: {failure}".format(index=index, failure=failure)
206+
for index, failure in enumerate(maybe_unsatisfieds, start=1)
207+
),
208+
)
209+
)
210+
172211

173212
@attr.s(frozen=True)
174213
class ResolveResult(object):

pex/version.py

Lines changed: 1 addition & 1 deletion
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.19.0"
4+
__version__ = "2.19.1"
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Copyright 2024 Pex project contributors.
2+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3+
4+
from __future__ import absolute_import
5+
6+
import os
7+
import subprocess
8+
from textwrap import dedent
9+
10+
from pex.typing import TYPE_CHECKING
11+
from testing import WheelBuilder
12+
from testing.docker import skip_unless_docker
13+
14+
if TYPE_CHECKING:
15+
from typing import Any
16+
17+
18+
@skip_unless_docker
19+
def test_resolved_wheel_tag_platform_mismatch_warns(
20+
tmpdir, # type: Any
21+
pex_project_dir, # type: str
22+
):
23+
# type: (...) -> None
24+
25+
context = os.path.join(str(tmpdir), "context")
26+
pex_wheel = WheelBuilder(pex_project_dir, wheel_dir=context).bdist()
27+
with open(os.path.join(context, "Dockerfile"), "w") as fp:
28+
fp.write(
29+
dedent(
30+
r"""
31+
FROM almalinux:8.10
32+
33+
RUN dnf install -y \
34+
python3.11-devel \
35+
gcc \
36+
make \
37+
libffi-devel
38+
39+
RUN mkdir /wheels
40+
COPY {pex_wheel} {pex_wheel}
41+
RUN python3.11 -mvenv /pex/venv && \
42+
/pex/venv/bin/pip install {pex_wheel} && \
43+
rm {pex_wheel}
44+
45+
ENV PATH=/pex/venv/bin:$PATH
46+
47+
RUN mkdir /work
48+
WORKDIR = /work
49+
""".format(
50+
pex_wheel=os.path.basename(pex_wheel)
51+
)
52+
)
53+
)
54+
subprocess.check_call(args=["docker", "build", "-t", "pex_test_issue_2532", context])
55+
56+
process = subprocess.Popen(
57+
args=[
58+
"docker",
59+
"run",
60+
"--rm",
61+
"pex_test_issue_2532",
62+
"bash",
63+
"-c",
64+
dedent(
65+
r"""
66+
pex \
67+
--python python3.11 \
68+
--platform manylinux_2_28_x86_64-cp-3.11.9-cp311 \
69+
cryptography==42.0.8 \
70+
cffi==1.16.0 \
71+
-o component_deps.pex
72+
./component_deps.pex -c 'import cffi; print(cffi.__file__)'
73+
"""
74+
),
75+
],
76+
stdout=subprocess.PIPE,
77+
stderr=subprocess.PIPE,
78+
)
79+
stdout, stderr = process.communicate()
80+
assert 0 == process.returncode
81+
82+
# N.B.: The tags calculated for manylinux_2_28_x86_64-cp-3.11.9-cp311 via `pip -v debug ...`
83+
# are:
84+
# ----
85+
# cp311-cp311-manylinux_2_28_x86_64
86+
# cp311-abi3-manylinux_2_28_x86_64
87+
# cp311-none-manylinux_2_28_x86_64
88+
# ...
89+
#
90+
# This does not match either of the wheel tags of:
91+
# + cp311-cp311-manylinux_2_17_x86_64
92+
# + cp311-cp311-manylinux_2014_x86_64
93+
#
94+
# Instead of failing the resolve check though, we should just see a warning since both of these
95+
# tags may be compatible at runtime, and, in fact, they are.
96+
error = stderr.decode("utf-8")
97+
assert (
98+
dedent(
99+
"""\
100+
PEXWarning: The resolved distributions for 1 target may not be compatible:
101+
1: abbreviated platform cp311-cp311-manylinux_2_28_x86_64 may not be compatible with:
102+
cryptography 42.0.8 requires cffi>=1.12; platform_python_implementation != "PyPy" but 2 incompatible dists were resolved:
103+
cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
104+
cffi-1.16.0-cp311-cp311-linux_x86_64.whl
105+
106+
Its generally advisable to use `--complete-platform` instead of `--platform` to
107+
ensure resolved distributions will be compatible with the target platform at
108+
runtime. For instructions on how to generate a `--complete-platform` see:
109+
https://docs.pex-tool.org/buildingpex.html#complete-platform
110+
"""
111+
).strip()
112+
in error
113+
), error
114+
115+
output = stdout.decode("utf-8")
116+
assert (
117+
"cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" in output
118+
), output

0 commit comments

Comments
 (0)