Skip to content

Commit 40bff69

Browse files
committed
refactor: own the interpreter runtime toolchain rules instead of rules_python's
Generated interpreter repos now instantiate a rules_py py_runtime_toolchain (py/private/interpreter/runtime.bzl, with a rules_py PyRuntimeInfo) instead of rules_python's py_runtime + py_runtime_pair + py_exec_tools_toolchain. One :runtime target per repo backs both toolchain registrations. Toolchain types are unchanged (@bazel_tools python type; rules_python's exec_tools type, with its exec_tools.exec_runtime/precompiler payload shape preserved), so rules_python-provisioned interpreters still satisfy whl_install/site-merge and rules_py still consumes user-registered rules_python py_runtime toolchains. This shrinks each interpreter repo's rules_python loads from 31 .bzl files to the 12 of the deliberately kept py_cc_toolchain chain, insulating provisioned toolchains from rules_python implementation churn, and leaves the runtime layer pinning no rules_python minimum version. New coverage: e2e/rules-python-interop (isolated module) pins that a module mixing both rulesets shares one rules_python-provisioned interpreter, including a $(PYTHON3) location diff_test; e2e/cases/rules-python-consumers pins that rules_python's own toolchain consumers (current_py_toolchain, exec-tools readers) work against rules_py-backed toolchains. Narrow breaks: - Generated interpreter repo targets are renamed: @python_X//:py3_runtime, :runtime_pair, and :exec_tools_toolchain are now a single @python_X//:runtime. Only direct label references need updating; toolchain resolution is unaffected. - rules_py-provisioned runtimes carry rules_py's PyRuntimeInfo, not rules_python's. Provider-indexed access (target[PyRuntimeInfo] with rules_python's symbol) on these targets or resolved toolchains breaks; duck-typed field access keeps working. As a consequence, rules_python's py_binary/py_test cannot execute on rules_py-provisioned toolchains (they read provider fields rules_py does not carry) — the same one-way interop direction #1231 establishes for PyInfo. - The exec-tools toolchain rules_py registers provides precompiler = None; enabling rules_python precompiling while @python_interpreters toolchains are registered first is unsupported.
1 parent c6441e5 commit 40bff69

18 files changed

Lines changed: 1797 additions & 76 deletions

File tree

.github/workflows/ci-workflows.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ jobs:
4545
- { path: "e2e/interpreter-runtime-metadata", slug: "e2e-interpreter-runtime-metadata", runner: "ubuntu-latest" }
4646
- { path: "e2e/interpreter-toolchain-settings", slug: "e2e-interpreter-toolchain-settings", runner: "ubuntu-latest" }
4747
- { path: "e2e/interpreter-input-validation", slug: "e2e-interpreter-input-validation", runner: "ubuntu-latest" }
48+
- { path: "e2e/rules-python-interop", slug: "e2e-rules-python-interop", runner: "ubuntu-latest" }
4849
- { path: "e2e/rules-proto-grpc-python", slug: "e2e-rules-proto-grpc-python", runner: "ubuntu-22.04-32core" }
4950
- { path: "examples/debugger", slug: "examples-debugger", runner: "ubuntu-latest" }
5051
- { path: "examples/dev_deps", slug: "examples-dev_deps", runner: "ubuntu-latest" }

docs/interpreter.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,17 @@ This interpreter provisioning is designed to coexist with `rules_python`:
231231
registration, so these interpreters work with all existing Python rules.
232232
- The `@rules_python//python/config_settings:python_version` flag is kept in
233233
sync with our own version flag via build transitions.
234-
- `py_runtime` and `py_runtime_pair` from `rules_python` are used to create
235-
the runtime providers.
234+
- Runtimes registered with `rules_python`'s `py_runtime` / `py_runtime_pair`
235+
(for example a system interpreter) remain usable by rules_py rules, which
236+
read the runtime fields structurally.
237+
- Build actions that run an interpreter (wheel installation, site-packages
238+
merging) resolve `@rules_python//python:exec_tools_toolchain_type`, so
239+
interpreters provisioned through either `interpreters.toolchain()` or
240+
`rules_python`'s `python.toolchain()` satisfy them.
241+
242+
Note that runtimes provisioned by `interpreters.toolchain()` carry rules_py's
243+
own runtime provider, not `rules_python`'s `PyRuntimeInfo``rules_python`'s
244+
rules cannot run on them.
236245

237246
You can migrate incrementally: replace `python.toolchain()` calls with
238247
`interpreters.toolchain()` and remove the `rules_python` interpreter
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
load("@aspect_rules_py//py:defs.bzl", "py_test")
2+
load(":exec_tools_facts.bzl", "exec_tools_facts", "with_python_version")
3+
4+
# rules_python reaches rules_py-provisioned interpreters only through
5+
# toolchain types, never through labels inside the generated repos — so the
6+
# repos' internal target names are private wiring. Pin that by running
7+
# rules_python's own consumer code against rules_py-backed toolchains.
8+
9+
# rules_python's current_py_toolchain field-reads the resolved standard
10+
# toolchain payload to expand $(PYTHON3). Pinned to 3.13 via the wrapper
11+
# below (data edges reset Python flags), it resolves the rules_py-provisioned
12+
# interpreter.
13+
genrule(
14+
name = "python3_var",
15+
outs = ["python3_var.txt"],
16+
cmd = "echo '$(PYTHON3)' > $@",
17+
toolchains = ["@rules_python//python:current_py_toolchain"],
18+
)
19+
20+
with_python_version(
21+
name = "python3_var_313",
22+
python_version = "3.13",
23+
deps = [":python3_var"],
24+
)
25+
26+
exec_tools_facts(
27+
name = "exec_tools_facts",
28+
)
29+
30+
py_test(
31+
name = "consumers_test",
32+
srcs = ["consumers_test.py"],
33+
data = [
34+
":exec_tools_facts",
35+
":python3_var_313",
36+
],
37+
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Asserts rules_python's toolchain consumers work against rules_py-provisioned
2+
toolchains, which they reach only via toolchain types — never via labels inside
3+
the generated interpreter repos.
4+
"""
5+
6+
import os
7+
8+
9+
def read(rel):
10+
path = os.path.join(
11+
os.environ["TEST_SRCDIR"],
12+
os.environ["TEST_WORKSPACE"],
13+
"rules-python-consumers",
14+
rel,
15+
)
16+
with open(path) as f:
17+
return f.read().strip()
18+
19+
20+
# rules_python's current_py_toolchain expanded $(PYTHON3) from the resolved
21+
# standard toolchain — backed by rules_py's provisioned 3.13 runtime.
22+
python3 = read("python3_var.txt")
23+
assert "python_interpreters+python_3_13" in python3, python3
24+
25+
# The exec-tools payload carries rules_python's expected shape: an exec
26+
# runtime from a rules_py-provisioned repo, and a precompiler field (None).
27+
facts = read("exec_tools_facts.txt").splitlines()
28+
assert "python_interpreters+" in facts[0], facts
29+
assert facts[1] == "None", facts
30+
31+
print("OK")
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Materialises the resolved exec-tools toolchain payload for assertion.
2+
3+
Reads the fields rules_python's consumers access — `exec_tools.exec_runtime`
4+
and `exec_tools.precompiler` — so the test pins that rules_py's registration
5+
under rules_python's toolchain type keeps their expected shape.
6+
"""
7+
8+
EXEC_TOOLS_TOOLCHAIN = "@rules_python//python:exec_tools_toolchain_type"
9+
10+
def _exec_tools_facts_impl(ctx):
11+
exec_tools = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN].exec_tools
12+
out = ctx.actions.declare_file(ctx.label.name + ".txt")
13+
ctx.actions.write(out, "{}\n{}\n".format(
14+
exec_tools.exec_runtime.interpreter.path,
15+
exec_tools.precompiler,
16+
))
17+
return [DefaultInfo(files = depset([out]))]
18+
19+
exec_tools_facts = rule(
20+
implementation = _exec_tools_facts_impl,
21+
toolchains = [EXEC_TOOLS_TOOLCHAIN],
22+
)
23+
24+
def _set_python_version_impl(settings, attr):
25+
# Both flags together so the uv constraints mismatch guard stays satisfied.
26+
return {
27+
"@aspect_rules_py//py/private/interpreter:python_version": attr.python_version,
28+
"@rules_python//python/config_settings:python_version": attr.python_version,
29+
}
30+
31+
_set_python_version = transition(
32+
implementation = _set_python_version_impl,
33+
inputs = [],
34+
outputs = [
35+
"@aspect_rules_py//py/private/interpreter:python_version",
36+
"@rules_python//python/config_settings:python_version",
37+
],
38+
)
39+
40+
def _with_python_version_impl(ctx):
41+
return [DefaultInfo(files = depset(transitive = [
42+
dep[DefaultInfo].files
43+
for dep in ctx.attr.deps
44+
]))]
45+
46+
with_python_version = rule(
47+
doc = """Forwards deps analyzed under an explicit Python version.
48+
49+
py_* data edges reset the Python flags to the caller baseline (#1294), so a
50+
data file that must be produced under a specific version pins it here rather
51+
than inheriting the consuming terminal's transition.""",
52+
implementation = _with_python_version_impl,
53+
attrs = {
54+
"deps": attr.label_list(mandatory = True),
55+
"python_version": attr.string(mandatory = True),
56+
"_allowlist_function_transition": attr.label(
57+
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
58+
),
59+
},
60+
cfg = _set_python_version,
61+
)

e2e/interpreter-runtime-metadata/BUILD.bazel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ load(":runtime_metadata_test.bzl", "runtime_metadata_test")
33
runtime_metadata_test(
44
name = "regular",
55
abi_flags = "",
6-
runtime = "@python_3_15_x86_64_unknown_linux_gnu//:py3_runtime",
6+
runtime = "@python_3_15_x86_64_unknown_linux_gnu//:runtime",
77
)
88

99
runtime_metadata_test(
1010
name = "freethreaded",
1111
abi_flags = "t",
12-
runtime = "@python_3_15_x86_64_unknown_linux_gnu_freethreaded//:py3_runtime",
12+
runtime = "@python_3_15_x86_64_unknown_linux_gnu_freethreaded//:runtime",
1313
)

e2e/interpreter-runtime-metadata/runtime_metadata_test.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Checks metadata on a provisioned PBS Python runtime."""
22

3-
load("@rules_python//python:py_runtime_info.bzl", "PyRuntimeInfo")
3+
load("@aspect_rules_py//py:defs.bzl", "PyRuntimeInfo")
44

55
def _assert_equal(description, expected, actual):
66
if actual != expected:
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
load("@aspect_rules_py//py:defs.bzl", "current_py_toolchain", rules_py_test = "py_test")
2+
load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
3+
load("@rules_python//python:py_test.bzl", rules_python_test = "py_test")
4+
5+
# The only interpreter in this module comes from rules_python (see
6+
# MODULE.bazel); each test asserts the resolved binary's repo path.
7+
8+
# Default toolchain resolution: no python_version set.
9+
rules_py_test(
10+
name = "rules_py_default_test",
11+
srcs = ["shared_interpreter_test.py"],
12+
main = "shared_interpreter_test.py",
13+
)
14+
15+
# Versioned resolution: the python_version transition also sets the
16+
# rules_python version flag, selecting rules_python's 3.12 toolchain.
17+
rules_py_test(
18+
name = "rules_py_versioned_test",
19+
srcs = ["shared_interpreter_test.py"],
20+
main = "shared_interpreter_test.py",
21+
python_version = "3.12",
22+
)
23+
24+
rules_python_test(
25+
name = "rules_python_test",
26+
srcs = ["shared_interpreter_test.py"],
27+
main = "shared_interpreter_test.py",
28+
)
29+
30+
# Build-time identity check: each ruleset's current_py_toolchain expands
31+
# $(PYTHON3) to its resolved toolchain's interpreter File.path; the diff
32+
# proves both toolchain resolutions yield the same artifact.
33+
34+
current_py_toolchain(
35+
name = "rules_py_current_toolchain",
36+
)
37+
38+
genrule(
39+
name = "rules_py_interpreter_path",
40+
outs = ["rules_py_interpreter_path.txt"],
41+
cmd = "echo '$(PYTHON3)' > $@",
42+
toolchains = [":rules_py_current_toolchain"],
43+
)
44+
45+
genrule(
46+
name = "rules_python_interpreter_path",
47+
outs = ["rules_python_interpreter_path.txt"],
48+
cmd = "echo '$(PYTHON3)' > $@",
49+
toolchains = ["@rules_python//python:current_py_toolchain"],
50+
)
51+
52+
diff_test(
53+
name = "same_interpreter_location_test",
54+
file1 = ":rules_py_interpreter_path",
55+
file2 = ":rules_python_interpreter_path",
56+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Interop: rules_py rules share the interpreter provisioned by rules_python.
2+
3+
This module deliberately provisions its only interpreter through rules_python's
4+
`python.toolchain()` — no `python_interpreters` — so rules_py and rules_python
5+
targets must both resolve, and run on, that single runtime.
6+
"""
7+
8+
module(name = "rules_python_interop")
9+
10+
bazel_dep(name = "aspect_rules_py")
11+
bazel_dep(name = "bazel_skylib", version = "1.4.2")
12+
bazel_dep(name = "rules_python", version = "1.9.0")
13+
14+
local_path_override(
15+
module_name = "aspect_rules_py",
16+
path = "../..",
17+
)
18+
19+
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
20+
python.toolchain(python_version = "3.12")

0 commit comments

Comments
 (0)