Skip to content

Commit 2defdec

Browse files
committed
fix: migrate py_pex_binary to use providers
1 parent b24f045 commit 2defdec

11 files changed

Lines changed: 366 additions & 84 deletions

File tree

e2e/cases/pex-interpreter-exclusion/BUILD.bazel

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_pex_binary", "py_test")
1+
load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_pex_binary", "py_test", "py_unpacked_wheel")
22

33
py_binary(
44
name = "app",
@@ -27,6 +27,63 @@ py_pex_binary(
2727
python_interpreter_constraints = [],
2828
)
2929

30+
# Default constraints are stamped from the binary's version (3.13), not the pex
31+
# rule's toolchain (workspace default 3.11).
32+
py_pex_binary(
33+
name = "app_mismatch_constrained_pex",
34+
binary = ":app_mismatch",
35+
)
36+
37+
# A wheel reached only through a `filegroup(srcs = [...])` data wrapper. The
38+
# closure aspect walks data → filegroup but not the filegroup's `srcs`, so the
39+
# wheel must still be recognized as a distribution (--dependency) rather than
40+
# shipped as loose --source content.
41+
#
42+
# A hand-built wheel (not a uv-hub wheel): its py_unpacked_wheel carries no
43+
# dep_group constraint, so it survives the data edge's dep_group reset — a uv
44+
# wheel would be pruned there before the packaging logic is even exercised.
45+
genrule(
46+
name = "distfg_whl",
47+
srcs = [
48+
"wheel/distfg/__init__.py",
49+
"wheel/METADATA",
50+
],
51+
outs = ["distfg-1.0-py3-none-any.whl"],
52+
cmd = " ".join([
53+
"$(execpath @bazel_tools//tools/zip:zipper) c $@",
54+
"distfg/__init__.py=$(execpath wheel/distfg/__init__.py)",
55+
"distfg-1.0.dist-info/METADATA=$(execpath wheel/METADATA)",
56+
]),
57+
tools = ["@bazel_tools//tools/zip:zipper"],
58+
)
59+
60+
py_unpacked_wheel(
61+
name = "distfg",
62+
src = ":distfg-1.0-py3-none-any.whl",
63+
top_levels = [
64+
"distfg",
65+
"distfg-1.0.dist-info",
66+
],
67+
)
68+
69+
filegroup(
70+
name = "wrapped_wheel",
71+
srcs = [":distfg"],
72+
)
73+
74+
py_binary(
75+
name = "app_wrapped_wheel",
76+
srcs = ["app.py"],
77+
data = [":wrapped_wheel"],
78+
main = "app.py",
79+
)
80+
81+
py_pex_binary(
82+
name = "app_wrapped_wheel_pex",
83+
binary = ":app_wrapped_wheel",
84+
python_interpreter_constraints = [],
85+
)
86+
3087
py_test(
3188
name = "test_pex_excludes_interpreter",
3289
srcs = ["test_pex_excludes_interpreter.py"],
@@ -36,3 +93,17 @@ py_test(
3693
],
3794
main = "test_pex_excludes_interpreter.py",
3895
)
96+
97+
py_test(
98+
name = "test_pex_constraint_version",
99+
srcs = ["test_pex_constraint_version.py"],
100+
data = [":app_mismatch_constrained_pex"],
101+
main = "test_pex_constraint_version.py",
102+
)
103+
104+
py_test(
105+
name = "test_pex_wheel_under_filegroup",
106+
srcs = ["test_pex_wheel_under_filegroup.py"],
107+
data = [":app_wrapped_wheel_pex"],
108+
main = "test_pex_wheel_under_filegroup.py",
109+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""The default python_interpreter_constraints must be stamped from the binary's
2+
own interpreter version, not the pex rule's toolchain.
3+
4+
app_mismatch pins python_version 3.13 while this workspace's default is 3.11.
5+
With the default constraint (`CPython=={major}.{minor}.*`) the PEX must advertise
6+
3.13 — the interpreter it was actually built for — otherwise it would refuse to
7+
run on its own target interpreter.
8+
"""
9+
10+
import json
11+
import os
12+
import zipfile
13+
from pathlib import Path
14+
15+
pex = (
16+
Path(os.environ["RUNFILES_DIR"])
17+
/ "_main/pex-interpreter-exclusion/app_mismatch_constrained_pex.pex"
18+
)
19+
with zipfile.ZipFile(pex) as zf:
20+
info = json.loads(zf.read("PEX-INFO"))
21+
22+
assert info["interpreter_constraints"] == ["CPython==3.13.*"], info[
23+
"interpreter_constraints"
24+
]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""A wheel reached only through a `filegroup(srcs = [...])` data wrapper must
2+
still be packaged as a PEX distribution, not as loose source.
3+
4+
app_wrapped_wheel depends on the distfg wheel via `data = [":wrapped_wheel"]`,
5+
a filegroup whose `srcs` hold the wheel. The closure aspect walks the `data`
6+
edge to the filegroup but not the filegroup's `srcs`; if the wheel's
7+
PyWheelsInfo is missed there, its install tree is shipped as loose `--source`
8+
content and never appears among the PEX distributions.
9+
"""
10+
11+
import json
12+
import os
13+
import zipfile
14+
from pathlib import Path
15+
16+
pex = (
17+
Path(os.environ["RUNFILES_DIR"])
18+
/ "_main/pex-interpreter-exclusion/app_wrapped_wheel_pex.pex"
19+
)
20+
with zipfile.ZipFile(pex) as zf:
21+
info = json.loads(zf.read("PEX-INFO"))
22+
names = zf.namelist()
23+
24+
distributions = info.get("distributions", {})
25+
assert any("distfg" in key for key in distributions), (
26+
"distfg wheel was not packaged as a PEX distribution",
27+
list(distributions),
28+
)
29+
30+
# The distribution lives under `.deps/`, not as loose source files.
31+
loose = [name for name in names if "distfg" in name and not name.startswith(".deps/")]
32+
assert not loose, loose[:10]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Metadata-Version: 2.1
2+
Name: distfg
3+
Version: 1.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
VALUE = "distfg"

py/private/BUILD.bazel

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@ bzl_library(
4747
name = "py_image_layer",
4848
srcs = ["py_image_layer.bzl"],
4949
deps = [
50+
":providers",
5051
":py_info",
5152
":py_info_interop",
53+
"//py/private/py_venv:types",
54+
"//py/private/toolchain:types",
5255
"@bazel_lib//lib:transitions",
5356
"@tar.bzl//tar:mtree",
5457
"@tar.bzl//tar:tar",
@@ -120,8 +123,9 @@ bzl_library(
120123
name = "py_pex_binary",
121124
srcs = ["py_pex_binary.bzl"],
122125
deps = [
126+
":providers",
123127
":py_info",
124-
":py_semantics",
128+
"//py/private/py_venv:types",
125129
"//py/private/toolchain:types",
126130
],
127131
)

py/private/py_image_layer.bzl

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ Sharing model:
3535
load("//py/private:providers.bzl", "PyWheelsInfo")
3636
load("//py/private:py_info.bzl", "PyInfo")
3737
load("//py/private:py_info_interop.bzl", "has_py_info")
38-
load("//py/private/toolchain:types.bzl", "PY_TOOLCHAIN")
38+
load("//py/private/py_venv:types.bzl", "PY_VENV_KINDS")
39+
load("//py/private/toolchain:types.bzl", "PY_TOOLCHAIN", "interpreter_files_and_version")
3940

4041
_TAR_TOOLCHAIN = "@tar.bzl//tar/toolchain:type"
4142

@@ -182,8 +183,6 @@ _LayerInfo = provider(
182183
},
183184
)
184185

185-
_PY_VENV_KINDS = ("py_venv", "_py_venv", "_py_venv_lib")
186-
187186
def _collect_from_deps(ctx, provider):
188187
"""Walk deps/data/actual/venv and return a list of provider values from each matching dep."""
189188
results = []
@@ -307,11 +306,9 @@ def _layer_aspect_impl(target, ctx):
307306
# (NOT ctx.toolchains) is how the binary reads it back, which correctly
308307
# follows the binary's target-platform transition.
309308
if platform_common.ToolchainInfo in target:
310-
py3 = getattr(target[platform_common.ToolchainInfo], "py3_runtime", None)
311-
if py3 == None or py3.files == None:
309+
interp_depset, _ = interpreter_files_and_version(target)
310+
if interp_depset == None:
312311
return []
313-
direct = [py3.interpreter] if getattr(py3, "interpreter", None) != None else []
314-
interp_depset = depset(direct = direct, transitive = [py3.files])
315312
plan = ctx.attr._layer_tier[PyLayerTierInfo]
316313
interp_group = plan.interpreter_group
317314
interp_layer = None
@@ -399,7 +396,7 @@ def _layer_aspect_impl(target, ctx):
399396

400397
# Skip PyInfo deps (including wheel-leaf targets, which also emit PyInfo) —
401398
# they self-capture via the aspect.
402-
if kind not in _PY_VENV_KINDS and has_py_info(target) and not is_binary:
399+
if kind not in PY_VENV_KINDS and has_py_info(target) and not is_binary:
403400
own_parts = [target[DefaultInfo].files]
404401
for attr_name in ("data", "deps"):
405402
attr_val = getattr(ctx.rule.attr, attr_name, None)
@@ -424,7 +421,7 @@ def _layer_aspect_impl(target, ctx):
424421
else:
425422
own_source.append(own_depset)
426423

427-
if kind in _PY_VENV_KINDS:
424+
if kind in PY_VENV_KINDS:
428425
if PY_TOOLCHAIN in ctx.rule.toolchains:
429426
py_tc = ctx.rule.toolchains[PY_TOOLCHAIN]
430427
if _LayerInfo in py_tc:

0 commit comments

Comments
 (0)