Skip to content

Commit 94107d7

Browse files
committed
feat(toolchain): expose interpreter files-to-run on PyRuntimeInfo
Rules that execute a py_runtime interpreter in an action need the interpreter executable together with its runfiles metadata. The existing PyRuntimeInfo fields identify the interpreter file and runtime files, but do not preserve the target's FilesToRunProvider for executable interpreter targets. Add PyRuntimeInfo.interpreter_files_to_run for runtimes created from an executable interpreter target, and validate that direct provider construction keeps the FilesToRunProvider executable aligned with the interpreter field. Direct file interpreters and platform runtimes continue to leave this field unset, preserving existing py_runtime behavior. Document the new public provider field and add focused analysis-test coverage for executable, file-only, platform, and invalid constructor cases.
1 parent c7efd79 commit 94107d7

6 files changed

Lines changed: 233 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ END_UNRELEASED_TEMPLATE
9797

9898
{#v0-0-0-added}
9999
### Added
100+
* (toolchain) Added {obj}`PyRuntimeInfo.interpreter_files_to_run` so action
101+
consumers can execute an in-build runtime interpreter with its runfiles.
100102
* (toolchain) Added {obj}`python.override.toolchain_target_settings` to allow
101103
adding `config_setting` labels to all registered toolchains.
102104
* (windows) Full venv support for Windows is available. Set

python/private/py_runtime_info.bzl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def _PyRuntimeInfo_init(
5555
interpreter_path = None,
5656
interpreter = None,
5757
files = None,
58+
interpreter_files_to_run = None,
5859
coverage_tool = None,
5960
coverage_files = None,
6061
pyc_tag = None,
@@ -74,6 +75,15 @@ def _PyRuntimeInfo_init(
7475
if interpreter_path and files != None:
7576
fail("cannot specify 'files' if 'interpreter_path' is given")
7677

78+
if interpreter_path and interpreter_files_to_run:
79+
fail("cannot specify 'interpreter_files_to_run' if 'interpreter_path' is given")
80+
81+
if interpreter_files_to_run:
82+
if not interpreter_files_to_run.executable:
83+
fail("'interpreter_files_to_run' must have an executable")
84+
if interpreter_files_to_run.executable != interpreter:
85+
fail("'interpreter_files_to_run.executable' must match 'interpreter'")
86+
7787
if (coverage_tool and not coverage_files) or (not coverage_tool and coverage_files):
7888
fail(
7989
"coverage_tool and coverage_files must both be set or neither must be set, " +
@@ -112,6 +122,7 @@ def _PyRuntimeInfo_init(
112122
"files": files,
113123
"implementation_name": implementation_name,
114124
"interpreter": interpreter,
125+
"interpreter_files_to_run": interpreter_files_to_run,
115126
"interpreter_path": interpreter_path,
116127
"interpreter_version_info": interpreter_version_info_struct_from_dict(interpreter_version_info),
117128
"pyc_tag": pyc_tag,
@@ -239,6 +250,19 @@ The Python implementation name (`sys.implementation.name`)
239250
If this is an in-build runtime, this field is a `File` representing the
240251
interpreter. Otherwise, this is `None`. Note that an in-build runtime can use
241252
either a prebuilt, checked-in interpreter or an interpreter built from source.
253+
""",
254+
"interpreter_files_to_run": """
255+
:type: None | FilesToRunProvider
256+
257+
The `FilesToRunProvider` for the interpreter target when this runtime was
258+
created from an executable target. This includes the interpreter executable and
259+
the runfiles metadata needed to use it as an action tool. Rules that execute the
260+
interpreter in an action should use this field so Bazel can stage the
261+
interpreter together with its runfiles. This is `None` for platform runtimes
262+
using `interpreter_path` and for file-only interpreter targets.
263+
264+
:::{versionadded} VERSION_NEXT_FEATURE
265+
:::
242266
""",
243267
"interpreter_path": """
244268
:type: str | None

python/private/py_runtime_rule.bzl

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,44 @@ def _py_runtime_impl(ctx):
3939
runfiles = ctx.runfiles()
4040

4141
hermetic = bool(interpreter)
42+
interpreter_files_to_run = None
4243
if not hermetic:
4344
if runtime_files:
4445
fail("if 'interpreter_path' is given then 'files' must be empty")
4546
if not paths.is_absolute(interpreter_path):
4647
fail("interpreter_path must be an absolute path")
4748
else:
4849
interpreter_di = interpreter[DefaultInfo]
49-
50-
if interpreter_di.files_to_run and interpreter_di.files_to_run.executable:
50+
interpreter_file = None
51+
52+
# Direct file targets also expose files_to_run.executable. They should
53+
# keep py_runtime's file-only behavior and not populate
54+
# interpreter_files_to_run. Rule targets have OutputGroupInfo; direct
55+
# file targets do not.
56+
is_file_target = OutputGroupInfo not in interpreter
57+
if _is_singleton_depset(interpreter_di.files):
58+
interpreter_file = interpreter_di.files.to_list()[0]
59+
60+
if is_file_target and interpreter_file:
61+
# Direct file label: use the file as the interpreter, but do not
62+
# treat it as an executable target with runfiles metadata.
63+
interpreter = interpreter_file
64+
elif interpreter_di.files_to_run and interpreter_di.files_to_run.executable:
65+
# Executable rule target: use the executable and preserve the full
66+
# FilesToRunProvider so action consumers can stage its runfiles.
5167
interpreter = interpreter_di.files_to_run.executable
68+
interpreter_files_to_run = interpreter_di.files_to_run
5269
runfiles = runfiles.merge(interpreter_di.default_runfiles)
5370

5471
runtime_files = depset(transitive = [
5572
interpreter_di.files,
5673
interpreter_di.default_runfiles.files,
5774
runtime_files,
5875
])
59-
elif _is_singleton_depset(interpreter_di.files):
60-
interpreter = interpreter_di.files.to_list()[0]
76+
elif interpreter_file:
77+
# Non-executable rule with exactly one output: preserve the
78+
# historical file-only interpreter behavior.
79+
interpreter = interpreter_file
6180
else:
6281
fail("interpreter must be an executable target or must produce exactly one file.")
6382

@@ -111,6 +130,7 @@ def _py_runtime_impl(ctx):
111130
py_runtime_info_kwargs = dict(
112131
interpreter_path = interpreter_path or None,
113132
interpreter = interpreter,
133+
interpreter_files_to_run = interpreter_files_to_run,
114134
files = runtime_files if hermetic else None,
115135
coverage_tool = coverage_tool,
116136
coverage_files = coverage_files,
@@ -119,6 +139,7 @@ def _py_runtime_impl(ctx):
119139
bootstrap_template = ctx.file.bootstrap_template,
120140
)
121141
builtin_py_runtime_info_kwargs = dict(py_runtime_info_kwargs)
142+
builtin_py_runtime_info_kwargs.pop("interpreter_files_to_run", None)
122143

123144
# There are all args that BuiltinPyRuntimeInfo doesn't support
124145
py_runtime_info_kwargs.update(dict(

tests/py_runtime/py_runtime_tests.bzl

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ def _test_in_build_interpreter_impl(env, target):
197197
info.python_version().equals("PY3")
198198
info.files().contains_predicate(matching.file_basename_equals("file1.txt"))
199199
info.interpreter().path().contains("fake_interpreter")
200+
env.expect.that_bool(info.actual.interpreter_files_to_run == None).equals(True)
200201

201202
_tests.append(_test_in_build_interpreter)
202203

@@ -227,6 +228,9 @@ def _test_interpreter_binary_with_multiple_outputs_impl(env, target):
227228
factory = py_runtime_info_subject,
228229
)
229230
py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter")
231+
py_runtime_info.interpreter_files_to_run().executable().short_path_equals(
232+
"{package}/{test_name}_built_interpreter",
233+
)
230234
py_runtime_info.files().contains_exactly([
231235
"{package}/extra_default_output.txt",
232236
"{package}/runfile.txt",
@@ -272,6 +276,9 @@ def _test_interpreter_binary_with_single_output_and_runfiles_impl(env, target):
272276
factory = py_runtime_info_subject,
273277
)
274278
py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter")
279+
py_runtime_info.interpreter_files_to_run().executable().short_path_equals(
280+
"{package}/{test_name}_built_interpreter",
281+
)
275282
py_runtime_info.files().contains_exactly([
276283
"{package}/runfile.txt",
277284
"{package}/{test_name}_built_interpreter",
@@ -327,10 +334,12 @@ def _test_system_interpreter(name):
327334
)
328335

329336
def _test_system_interpreter_impl(env, target):
330-
env.expect.that_target(target).provider(
337+
info = env.expect.that_target(target).provider(
331338
PyRuntimeInfo,
332339
factory = py_runtime_info_subject,
333-
).interpreter_path().equals("/system/python")
340+
)
341+
info.interpreter_path().equals("/system/python")
342+
env.expect.that_bool(info.actual.interpreter_files_to_run == None).equals(True)
334343

335344
_tests.append(_test_system_interpreter)
336345

tests/py_runtime_info/py_runtime_info_tests.bzl

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515

1616
load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
1717
load("@rules_testing//lib:test_suite.bzl", "test_suite")
18+
load("@rules_testing//lib:truth.bzl", "matching")
1819
load("//python:py_runtime_info.bzl", "PyRuntimeInfo")
20+
load("//tests/support:py_runtime_info_subject.bzl", "py_runtime_info_subject")
1921

2022
def _create_py_runtime_info_without_interpreter_version_info_impl(ctx):
2123
return [PyRuntimeInfo(
@@ -35,6 +37,57 @@ _create_py_runtime_info_without_interpreter_version_info = rule(
3537
},
3638
)
3739

40+
def _simple_binary_impl(ctx):
41+
executable = ctx.actions.declare_file(ctx.label.name)
42+
ctx.actions.write(executable, "", is_executable = True)
43+
return [DefaultInfo(
44+
executable = executable,
45+
files = depset([executable]),
46+
)]
47+
48+
_simple_binary = rule(
49+
implementation = _simple_binary_impl,
50+
executable = True,
51+
)
52+
53+
def _file_target_impl(ctx):
54+
output = ctx.actions.declare_file(ctx.label.name + ".txt")
55+
ctx.actions.write(output, "")
56+
return [DefaultInfo(files = depset([output]))]
57+
58+
_file_target = rule(
59+
implementation = _file_target_impl,
60+
)
61+
62+
def _create_py_runtime_info_with_interpreter_files_to_run_impl(ctx):
63+
files_to_run = ctx.attr.files_to_run[DefaultInfo].files_to_run
64+
kwargs = dict(
65+
bootstrap_template = ctx.file.bootstrap_template,
66+
interpreter_files_to_run = files_to_run,
67+
python_version = "PY3",
68+
)
69+
if ctx.attr.use_interpreter_path:
70+
kwargs["interpreter_path"] = "/python"
71+
else:
72+
kwargs["files"] = depset()
73+
kwargs["interpreter"] = ctx.executable.interpreter
74+
75+
return [PyRuntimeInfo(**kwargs)]
76+
77+
_create_py_runtime_info_with_interpreter_files_to_run = rule(
78+
implementation = _create_py_runtime_info_with_interpreter_files_to_run_impl,
79+
attrs = {
80+
"bootstrap_template": attr.label(allow_single_file = True, default = "bootstrap.txt"),
81+
"files_to_run": attr.label(mandatory = True),
82+
"interpreter": attr.label(
83+
cfg = "target",
84+
executable = True,
85+
mandatory = True,
86+
),
87+
"use_interpreter_path": attr.bool(),
88+
},
89+
)
90+
3891
_tests = []
3992

4093
def _test_can_create_py_runtime_info_without_interpreter_version_info(name):
@@ -53,6 +106,112 @@ def _test_can_create_py_runtime_info_without_interpreter_version_info_impl(env,
53106

54107
_tests.append(_test_can_create_py_runtime_info_without_interpreter_version_info)
55108

109+
def _test_interpreter_files_to_run_with_interpreter(name):
110+
_simple_binary(
111+
name = name + "_interpreter",
112+
)
113+
_create_py_runtime_info_with_interpreter_files_to_run(
114+
name = name + "_subject",
115+
files_to_run = name + "_interpreter",
116+
interpreter = name + "_interpreter",
117+
)
118+
analysis_test(
119+
name = name,
120+
target = name + "_subject",
121+
impl = _test_interpreter_files_to_run_with_interpreter_impl,
122+
)
123+
124+
def _test_interpreter_files_to_run_with_interpreter_impl(env, target):
125+
info = env.expect.that_target(target).provider(
126+
PyRuntimeInfo,
127+
factory = py_runtime_info_subject,
128+
)
129+
info.interpreter().short_path_equals("{package}/{test_name}_interpreter")
130+
info.interpreter_files_to_run().executable().short_path_equals(
131+
"{package}/{test_name}_interpreter",
132+
)
133+
134+
_tests.append(_test_interpreter_files_to_run_with_interpreter)
135+
136+
def _test_interpreter_files_to_run_disallows_interpreter_path(name):
137+
_simple_binary(
138+
name = name + "_interpreter",
139+
)
140+
_create_py_runtime_info_with_interpreter_files_to_run(
141+
name = name + "_subject",
142+
files_to_run = name + "_interpreter",
143+
interpreter = name + "_interpreter",
144+
tags = ["manual"],
145+
use_interpreter_path = True,
146+
)
147+
analysis_test(
148+
name = name,
149+
target = name + "_subject",
150+
impl = _test_interpreter_files_to_run_disallows_interpreter_path_impl,
151+
expect_failure = True,
152+
)
153+
154+
def _test_interpreter_files_to_run_disallows_interpreter_path_impl(env, target):
155+
env.expect.that_target(target).failures().contains_predicate(
156+
matching.str_matches("*interpreter_files_to_run*interpreter_path*"),
157+
)
158+
159+
_tests.append(_test_interpreter_files_to_run_disallows_interpreter_path)
160+
161+
def _test_interpreter_files_to_run_requires_executable(name):
162+
_simple_binary(
163+
name = name + "_interpreter",
164+
)
165+
_file_target(
166+
name = name + "_files_to_run",
167+
)
168+
_create_py_runtime_info_with_interpreter_files_to_run(
169+
name = name + "_subject",
170+
files_to_run = name + "_files_to_run",
171+
interpreter = name + "_interpreter",
172+
tags = ["manual"],
173+
)
174+
analysis_test(
175+
name = name,
176+
target = name + "_subject",
177+
impl = _test_interpreter_files_to_run_requires_executable_impl,
178+
expect_failure = True,
179+
)
180+
181+
def _test_interpreter_files_to_run_requires_executable_impl(env, target):
182+
env.expect.that_target(target).failures().contains_predicate(
183+
matching.str_matches("*interpreter_files_to_run*executable*"),
184+
)
185+
186+
_tests.append(_test_interpreter_files_to_run_requires_executable)
187+
188+
def _test_interpreter_files_to_run_requires_matching_interpreter(name):
189+
_simple_binary(
190+
name = name + "_interpreter",
191+
)
192+
_simple_binary(
193+
name = name + "_other_interpreter",
194+
)
195+
_create_py_runtime_info_with_interpreter_files_to_run(
196+
name = name + "_subject",
197+
files_to_run = name + "_other_interpreter",
198+
interpreter = name + "_interpreter",
199+
tags = ["manual"],
200+
)
201+
analysis_test(
202+
name = name,
203+
target = name + "_subject",
204+
impl = _test_interpreter_files_to_run_requires_matching_interpreter_impl,
205+
expect_failure = True,
206+
)
207+
208+
def _test_interpreter_files_to_run_requires_matching_interpreter_impl(env, target):
209+
env.expect.that_target(target).failures().contains_predicate(
210+
matching.str_matches("*interpreter_files_to_run.executable*interpreter*"),
211+
)
212+
213+
_tests.append(_test_interpreter_files_to_run_requires_matching_interpreter)
214+
56215
def py_runtime_info_test_suite(name):
57216
test_suite(
58217
name = name,

tests/support/py_runtime_info_subject.bzl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ def py_runtime_info_subject(info, *, meta):
3737
coverage_tool = lambda *a, **k: _py_runtime_info_subject_coverage_tool(self, *a, **k),
3838
files = lambda *a, **k: _py_runtime_info_subject_files(self, *a, **k),
3939
interpreter = lambda *a, **k: _py_runtime_info_subject_interpreter(self, *a, **k),
40+
interpreter_files_to_run = lambda *a, **k: (
41+
_py_runtime_info_subject_interpreter_files_to_run(self, *a, **k)
42+
),
4043
interpreter_path = lambda *a, **k: _py_runtime_info_subject_interpreter_path(self, *a, **k),
4144
interpreter_version_info = lambda *a, **k: _py_runtime_info_subject_interpreter_version_info(self, *a, **k),
4245
python_version = lambda *a, **k: _py_runtime_info_subject_python_version(self, *a, **k),
@@ -84,6 +87,15 @@ def _py_runtime_info_subject_interpreter(self):
8487
meta = self.meta.derive("interpreter()"),
8588
)
8689

90+
def _py_runtime_info_subject_interpreter_files_to_run(self):
91+
return subjects.struct(
92+
self.actual.interpreter_files_to_run,
93+
attrs = dict(
94+
executable = subjects.file,
95+
),
96+
meta = self.meta.derive("interpreter_files_to_run()"),
97+
)
98+
8799
def _py_runtime_info_subject_interpreter_path(self):
88100
return subjects.str(
89101
self.actual.interpreter_path,

0 commit comments

Comments
 (0)