Skip to content

Commit 69edec8

Browse files
feat(py_runtime): Allow py_runtime to take an executable target as the interpreter (bazel-contrib#1621)
This PR allows `py_runtime` to accept an executable (e.g. `sh_binary`). This makes it easier to customize the interpreter binary used, as it allows intercepting invocation of the interpreter. For example, it can be used to change how the interpreter searches for dynamic libraries. Related to bazel-contrib#1612 --------- Co-authored-by: Richard Levasseur <rlevasseur@google.com>
1 parent ebd779e commit 69edec8

File tree

4 files changed

+142
-9
lines changed

4 files changed

+142
-9
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ A brief description of the categories of changes:
3737
* (pip_install) the deprecated `pip_install` macro and related items have been
3838
removed.
3939

40+
* (toolchains) `py_runtime` can now take an executable target. Note: runfiles
41+
from the target are not supported yet.
42+
4043
### Fixed
4144

4245
* (gazelle) The gazelle plugin helper was not working with Python toolchains 3.11

python/private/common/py_runtime_rule.bzl

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ _py_builtins = py_internal
2525

2626
def _py_runtime_impl(ctx):
2727
interpreter_path = ctx.attr.interpreter_path or None # Convert empty string to None
28-
interpreter = ctx.file.interpreter
28+
interpreter = ctx.attr.interpreter
2929
if (interpreter_path and interpreter) or (not interpreter_path and not interpreter):
3030
fail("exactly one of the 'interpreter' or 'interpreter_path' attributes must be specified")
3131

@@ -34,12 +34,30 @@ def _py_runtime_impl(ctx):
3434
for t in ctx.attr.files
3535
])
3636

37+
runfiles = ctx.runfiles()
38+
3739
hermetic = bool(interpreter)
3840
if not hermetic:
3941
if runtime_files:
4042
fail("if 'interpreter_path' is given then 'files' must be empty")
4143
if not paths.is_absolute(interpreter_path):
4244
fail("interpreter_path must be an absolute path")
45+
else:
46+
interpreter_di = interpreter[DefaultInfo]
47+
48+
if interpreter_di.files_to_run and interpreter_di.files_to_run.executable:
49+
interpreter = interpreter_di.files_to_run.executable
50+
runfiles = runfiles.merge(interpreter_di.default_runfiles)
51+
52+
runtime_files = depset(transitive = [
53+
interpreter_di.files,
54+
interpreter_di.default_runfiles.files,
55+
runtime_files,
56+
])
57+
elif _is_singleton_depset(interpreter_di.files):
58+
interpreter = interpreter_di.files.to_list()[0]
59+
else:
60+
fail("interpreter must be an executable target or must produce exactly one file.")
4361

4462
if ctx.attr.coverage_tool:
4563
coverage_di = ctx.attr.coverage_tool[DefaultInfo]
@@ -88,7 +106,7 @@ def _py_runtime_impl(ctx):
88106
BuiltinPyRuntimeInfo(**builtin_py_runtime_info_kwargs),
89107
DefaultInfo(
90108
files = runtime_files,
91-
runfiles = ctx.runfiles(),
109+
runfiles = runfiles,
92110
),
93111
]
94112

@@ -186,10 +204,28 @@ runtime. For a platform runtime this attribute must not be set.
186204
""",
187205
),
188206
"interpreter": attr.label(
189-
allow_single_file = True,
207+
# We set `allow_files = True` to allow specifying executable
208+
# targets from rules that have more than one default output,
209+
# e.g. sh_binary.
210+
allow_files = True,
190211
doc = """
191-
For an in-build runtime, this is the target to invoke as the interpreter. For a
192-
platform runtime this attribute must not be set.
212+
For an in-build runtime, this is the target to invoke as the interpreter. It
213+
can be either of:
214+
215+
* A single file, which will be the interpreter binary. It's assumed such
216+
interpreters are either self-contained single-file executables or any
217+
supporting files are specified in `files`.
218+
* An executable target. The target's executable will be the interpreter binary.
219+
Any other default outputs (`target.files`) and plain files runfiles
220+
(`runfiles.files`) will be automatically included as if specified in the
221+
`files` attribute.
222+
223+
NOTE: the runfiles of the target may not yet be properly respected/propagated
224+
to consumers of the toolchain/interpreter, see
225+
bazelbuild/rules_python/issues/1612
226+
227+
For a platform runtime (i.e. `interpreter_path` being set) this attribute must
228+
not be set.
193229
""",
194230
),
195231
"interpreter_path": attr.string(doc = """

tests/py_runtime/py_runtime_tests.bzl

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,20 @@ _SKIP_TEST = {
3030
}
3131

3232
def _simple_binary_impl(ctx):
33-
output = ctx.actions.declare_file(ctx.label.name)
34-
ctx.actions.write(output, "", is_executable = True)
33+
executable = ctx.actions.declare_file(ctx.label.name)
34+
ctx.actions.write(executable, "", is_executable = True)
3535
return [DefaultInfo(
36-
executable = output,
36+
executable = executable,
37+
files = depset([executable] + ctx.files.extra_default_outputs),
3738
runfiles = ctx.runfiles(ctx.files.data),
3839
)]
3940

4041
_simple_binary = rule(
4142
implementation = _simple_binary_impl,
42-
attrs = {"data": attr.label_list(allow_files = True)},
43+
attrs = {
44+
"data": attr.label_list(allow_files = True),
45+
"extra_default_outputs": attr.label_list(allow_files = True),
46+
},
4347
executable = True,
4448
)
4549

@@ -239,6 +243,95 @@ def _test_in_build_interpreter_impl(env, target):
239243

240244
_tests.append(_test_in_build_interpreter)
241245

246+
def _test_interpreter_binary_with_multiple_outputs(name):
247+
rt_util.helper_target(
248+
_simple_binary,
249+
name = name + "_built_interpreter",
250+
extra_default_outputs = ["extra_default_output.txt"],
251+
data = ["runfile.txt"],
252+
)
253+
254+
rt_util.helper_target(
255+
py_runtime,
256+
name = name + "_subject",
257+
interpreter = name + "_built_interpreter",
258+
python_version = "PY3",
259+
)
260+
analysis_test(
261+
name = name,
262+
target = name + "_subject",
263+
impl = _test_interpreter_binary_with_multiple_outputs_impl,
264+
)
265+
266+
def _test_interpreter_binary_with_multiple_outputs_impl(env, target):
267+
target = env.expect.that_target(target)
268+
py_runtime_info = target.provider(
269+
PyRuntimeInfo,
270+
factory = py_runtime_info_subject,
271+
)
272+
py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter")
273+
py_runtime_info.files().contains_exactly([
274+
"{package}/extra_default_output.txt",
275+
"{package}/runfile.txt",
276+
"{package}/{test_name}_built_interpreter",
277+
])
278+
279+
target.default_outputs().contains_exactly([
280+
"{package}/extra_default_output.txt",
281+
"{package}/runfile.txt",
282+
"{package}/{test_name}_built_interpreter",
283+
])
284+
285+
target.runfiles().contains_exactly([
286+
"{workspace}/{package}/runfile.txt",
287+
"{workspace}/{package}/{test_name}_built_interpreter",
288+
])
289+
290+
_tests.append(_test_interpreter_binary_with_multiple_outputs)
291+
292+
def _test_interpreter_binary_with_single_output_and_runfiles(name):
293+
rt_util.helper_target(
294+
_simple_binary,
295+
name = name + "_built_interpreter",
296+
data = ["runfile.txt"],
297+
)
298+
299+
rt_util.helper_target(
300+
py_runtime,
301+
name = name + "_subject",
302+
interpreter = name + "_built_interpreter",
303+
python_version = "PY3",
304+
)
305+
analysis_test(
306+
name = name,
307+
target = name + "_subject",
308+
impl = _test_interpreter_binary_with_single_output_and_runfiles_impl,
309+
)
310+
311+
def _test_interpreter_binary_with_single_output_and_runfiles_impl(env, target):
312+
target = env.expect.that_target(target)
313+
py_runtime_info = target.provider(
314+
PyRuntimeInfo,
315+
factory = py_runtime_info_subject,
316+
)
317+
py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter")
318+
py_runtime_info.files().contains_exactly([
319+
"{package}/runfile.txt",
320+
"{package}/{test_name}_built_interpreter",
321+
])
322+
323+
target.default_outputs().contains_exactly([
324+
"{package}/runfile.txt",
325+
"{package}/{test_name}_built_interpreter",
326+
])
327+
328+
target.runfiles().contains_exactly([
329+
"{workspace}/{package}/runfile.txt",
330+
"{workspace}/{package}/{test_name}_built_interpreter",
331+
])
332+
333+
_tests.append(_test_interpreter_binary_with_single_output_and_runfiles)
334+
242335
def _test_must_have_either_inbuild_or_system_interpreter(name):
243336
if br_util.is_bazel_6_or_higher():
244337
py_runtime_kwargs = {}

tests/py_runtime_info_subject.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def py_runtime_info_subject(info, *, meta):
3131
# buildifier: disable=uninitialized
3232
public = struct(
3333
# go/keep-sorted start
34+
actual = info,
3435
bootstrap_template = lambda *a, **k: _py_runtime_info_subject_bootstrap_template(self, *a, **k),
3536
coverage_files = lambda *a, **k: _py_runtime_info_subject_coverage_files(self, *a, **k),
3637
coverage_tool = lambda *a, **k: _py_runtime_info_subject_coverage_tool(self, *a, **k),

0 commit comments

Comments
 (0)