Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion e2e/cases/pex-interpreter-exclusion/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_pex_binary", "py_test")
load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_pex_binary", "py_test", "py_unpacked_wheel")

py_binary(
name = "app",
Expand Down Expand Up @@ -27,6 +27,63 @@ py_pex_binary(
python_interpreter_constraints = [],
)

# Default constraints are stamped from the binary's version (3.13), not the pex
# rule's toolchain (workspace default 3.11).
py_pex_binary(
name = "app_mismatch_constrained_pex",
binary = ":app_mismatch",
)

# A wheel reached only through a `filegroup(srcs = [...])` data wrapper. The
# closure aspect walks data → filegroup but not the filegroup's `srcs`, so the
# wheel must still be recognized as a distribution (--dependency) rather than
# shipped as loose --source content.
#
# A hand-built wheel (not a uv-hub wheel): its py_unpacked_wheel carries no
# dep_group constraint, so it survives the data edge's dep_group reset — a uv
# wheel would be pruned there before the packaging logic is even exercised.
genrule(
name = "distfg_whl",
srcs = [
"wheel/distfg/__init__.py",
"wheel/METADATA",
],
outs = ["distfg-1.0-py3-none-any.whl"],
cmd = " ".join([
"$(execpath @bazel_tools//tools/zip:zipper) c $@",
"distfg/__init__.py=$(execpath wheel/distfg/__init__.py)",
"distfg-1.0.dist-info/METADATA=$(execpath wheel/METADATA)",
]),
tools = ["@bazel_tools//tools/zip:zipper"],
)

py_unpacked_wheel(
name = "distfg",
src = ":distfg-1.0-py3-none-any.whl",
top_levels = [
"distfg",
"distfg-1.0.dist-info",
],
)

filegroup(
name = "wrapped_wheel",
srcs = [":distfg"],
)

py_binary(
name = "app_wrapped_wheel",
srcs = ["app.py"],
data = [":wrapped_wheel"],
main = "app.py",
)

py_pex_binary(
name = "app_wrapped_wheel_pex",
binary = ":app_wrapped_wheel",
python_interpreter_constraints = [],
)

py_test(
name = "test_pex_excludes_interpreter",
srcs = ["test_pex_excludes_interpreter.py"],
Expand All @@ -36,3 +93,17 @@ py_test(
],
main = "test_pex_excludes_interpreter.py",
)

py_test(
name = "test_pex_constraint_version",
srcs = ["test_pex_constraint_version.py"],
data = [":app_mismatch_constrained_pex"],
main = "test_pex_constraint_version.py",
)

py_test(
name = "test_pex_wheel_under_filegroup",
srcs = ["test_pex_wheel_under_filegroup.py"],
data = [":app_wrapped_wheel_pex"],
main = "test_pex_wheel_under_filegroup.py",
)
24 changes: 24 additions & 0 deletions e2e/cases/pex-interpreter-exclusion/test_pex_constraint_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""The default python_interpreter_constraints must be stamped from the binary's
own interpreter version, not the pex rule's toolchain.

app_mismatch pins python_version 3.13 while this workspace's default is 3.11.
With the default constraint (`CPython=={major}.{minor}.*`) the PEX must advertise
3.13 — the interpreter it was actually built for — otherwise it would refuse to
run on its own target interpreter.
"""

import json
import os
import zipfile
from pathlib import Path

pex = (
Path(os.environ["RUNFILES_DIR"])
/ "_main/pex-interpreter-exclusion/app_mismatch_constrained_pex.pex"
)
with zipfile.ZipFile(pex) as zf:
info = json.loads(zf.read("PEX-INFO"))

assert info["interpreter_constraints"] == ["CPython==3.13.*"], info[
"interpreter_constraints"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""A wheel reached only through a `filegroup(srcs = [...])` data wrapper must
still be packaged as a PEX distribution, not as loose source.

app_wrapped_wheel depends on the distfg wheel via `data = [":wrapped_wheel"]`,
a filegroup whose `srcs` hold the wheel. The closure aspect walks the `data`
edge to the filegroup but not the filegroup's `srcs`; if the wheel's
PyWheelsInfo is missed there, its install tree is shipped as loose `--source`
content and never appears among the PEX distributions.
"""

import json
import os
import zipfile
from pathlib import Path

pex = (
Path(os.environ["RUNFILES_DIR"])
/ "_main/pex-interpreter-exclusion/app_wrapped_wheel_pex.pex"
)
with zipfile.ZipFile(pex) as zf:
info = json.loads(zf.read("PEX-INFO"))
names = zf.namelist()

distributions = info.get("distributions", {})
assert any("distfg" in key for key in distributions), (
"distfg wheel was not packaged as a PEX distribution",
list(distributions),
)

# The distribution lives under `.deps/`, not as loose source files.
loose = [name for name in names if "distfg" in name and not name.startswith(".deps/")]
assert not loose, loose[:10]
3 changes: 3 additions & 0 deletions e2e/cases/pex-interpreter-exclusion/wheel/METADATA
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Metadata-Version: 2.1
Name: distfg
Version: 1.0
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VALUE = "distfg"
6 changes: 5 additions & 1 deletion py/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ bzl_library(
name = "py_image_layer",
srcs = ["py_image_layer.bzl"],
deps = [
":providers",
":py_info",
":py_info_interop",
"//py/private/py_venv:types",
"//py/private/toolchain:types",
"@bazel_lib//lib:transitions",
"@tar.bzl//tar:mtree",
"@tar.bzl//tar:tar",
Expand Down Expand Up @@ -142,8 +145,9 @@ bzl_library(
name = "py_pex_binary",
srcs = ["py_pex_binary.bzl"],
deps = [
":providers",
":py_info",
":py_semantics",
"//py/private/py_venv:types",
"//py/private/toolchain:types",
],
)
Expand Down
15 changes: 6 additions & 9 deletions py/private/py_image_layer.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ Sharing model:
load("//py/private:providers.bzl", "PyWheelsInfo")
load("//py/private:py_info.bzl", "PyInfo")
load("//py/private:py_info_interop.bzl", "has_py_info")
load("//py/private/toolchain:types.bzl", "PY_TOOLCHAIN")
load("//py/private/py_venv:types.bzl", "PY_VENV_KINDS")
load("//py/private/toolchain:types.bzl", "PY_TOOLCHAIN", "interpreter_files_and_version")

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

Expand Down Expand Up @@ -182,8 +183,6 @@ _LayerInfo = provider(
},
)

_PY_VENV_KINDS = ("py_venv", "_py_venv", "_py_venv_lib")

def _collect_from_deps(ctx, provider):
"""Walk deps/data/actual/venv and return a list of provider values from each matching dep."""
results = []
Expand Down Expand Up @@ -307,11 +306,9 @@ def _layer_aspect_impl(target, ctx):
# (NOT ctx.toolchains) is how the binary reads it back, which correctly
# follows the binary's target-platform transition.
if platform_common.ToolchainInfo in target:
py3 = getattr(target[platform_common.ToolchainInfo], "py3_runtime", None)
if py3 == None or py3.files == None:
interp_depset, _ = interpreter_files_and_version(target)
if interp_depset == None:
return []
direct = [py3.interpreter] if getattr(py3, "interpreter", None) != None else []
interp_depset = depset(direct = direct, transitive = [py3.files])
plan = ctx.attr._layer_tier[PyLayerTierInfo]
interp_group = plan.interpreter_group
interp_layer = None
Expand Down Expand Up @@ -399,7 +396,7 @@ def _layer_aspect_impl(target, ctx):

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

if kind in _PY_VENV_KINDS:
if kind in PY_VENV_KINDS:
if PY_TOOLCHAIN in ctx.rule.toolchains:
py_tc = ctx.rule.toolchains[PY_TOOLCHAIN]
if _LayerInfo in py_tc:
Expand Down
Loading
Loading