Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ END_UNRELEASED_TEMPLATE

{#v0-0-0-added}
### Added
* (toolchain) Added {obj}`PyRuntimeInfo.interpreter_files_to_run` so action
consumers can execute an in-build runtime interpreter with its runfiles.
* (toolchain) Added {obj}`python.override.toolchain_target_settings` to allow
adding `config_setting` labels to all registered toolchains.
* (windows) Full venv support for Windows is available. Set
Expand Down
24 changes: 24 additions & 0 deletions python/private/py_runtime_info.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def _PyRuntimeInfo_init(
interpreter_path = None,
interpreter = None,
files = None,
interpreter_files_to_run = None,
coverage_tool = None,
coverage_files = None,
pyc_tag = None,
Expand All @@ -74,6 +75,15 @@ def _PyRuntimeInfo_init(
if interpreter_path and files != None:
fail("cannot specify 'files' if 'interpreter_path' is given")

if interpreter_path and interpreter_files_to_run:
fail("cannot specify 'interpreter_files_to_run' if 'interpreter_path' is given")

if interpreter_files_to_run:
if not interpreter_files_to_run.executable:
fail("'interpreter_files_to_run' must have an executable")
if interpreter_files_to_run.executable != interpreter:
fail("'interpreter_files_to_run.executable' must match 'interpreter'")

if (coverage_tool and not coverage_files) or (not coverage_tool and coverage_files):
fail(
"coverage_tool and coverage_files must both be set or neither must be set, " +
Expand Down Expand Up @@ -112,6 +122,7 @@ def _PyRuntimeInfo_init(
"files": files,
"implementation_name": implementation_name,
"interpreter": interpreter,
"interpreter_files_to_run": interpreter_files_to_run,
"interpreter_path": interpreter_path,
"interpreter_version_info": interpreter_version_info_struct_from_dict(interpreter_version_info),
"pyc_tag": pyc_tag,
Expand Down Expand Up @@ -239,6 +250,19 @@ The Python implementation name (`sys.implementation.name`)
If this is an in-build runtime, this field is a `File` representing the
interpreter. Otherwise, this is `None`. Note that an in-build runtime can use
either a prebuilt, checked-in interpreter or an interpreter built from source.
""",
"interpreter_files_to_run": """
:type: None | FilesToRunProvider

The `FilesToRunProvider` for the interpreter target when this runtime was
created from an executable target. This includes the interpreter executable and
the runfiles metadata needed to use it as an action tool. Rules that execute the
interpreter in an action should use this field so Bazel can stage the
interpreter together with its runfiles. This is `None` for platform runtimes
using `interpreter_path` and for file-only interpreter targets.

:::{versionadded} VERSION_NEXT_FEATURE
:::
""",
"interpreter_path": """
:type: str | None
Expand Down
29 changes: 25 additions & 4 deletions python/private/py_runtime_rule.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,44 @@ def _py_runtime_impl(ctx):
runfiles = ctx.runfiles()

hermetic = bool(interpreter)
interpreter_files_to_run = None
if not hermetic:
if runtime_files:
fail("if 'interpreter_path' is given then 'files' must be empty")
if not paths.is_absolute(interpreter_path):
fail("interpreter_path must be an absolute path")
else:
interpreter_di = interpreter[DefaultInfo]

if interpreter_di.files_to_run and interpreter_di.files_to_run.executable:
interpreter_file = None

# Direct file targets also expose files_to_run.executable. They should
# keep py_runtime's file-only behavior and not populate
# interpreter_files_to_run. Rule targets have OutputGroupInfo; direct
# file targets do not.
is_file_target = OutputGroupInfo not in interpreter
if _is_singleton_depset(interpreter_di.files):
interpreter_file = interpreter_di.files.to_list()[0]

if is_file_target and interpreter_file:
# Direct file label: use the file as the interpreter, but do not
# treat it as an executable target with runfiles metadata.
interpreter = interpreter_file
elif interpreter_di.files_to_run and interpreter_di.files_to_run.executable:
# Executable rule target: use the executable and preserve the full
# FilesToRunProvider so action consumers can stage its runfiles.
interpreter = interpreter_di.files_to_run.executable
interpreter_files_to_run = interpreter_di.files_to_run
runfiles = runfiles.merge(interpreter_di.default_runfiles)

runtime_files = depset(transitive = [
interpreter_di.files,
interpreter_di.default_runfiles.files,
runtime_files,
])
elif _is_singleton_depset(interpreter_di.files):
interpreter = interpreter_di.files.to_list()[0]
elif interpreter_file:
# Non-executable rule with exactly one output: preserve the
# historical file-only interpreter behavior.
interpreter = interpreter_file
else:
fail("interpreter must be an executable target or must produce exactly one file.")

Expand Down Expand Up @@ -111,6 +130,7 @@ def _py_runtime_impl(ctx):
py_runtime_info_kwargs = dict(
interpreter_path = interpreter_path or None,
interpreter = interpreter,
interpreter_files_to_run = interpreter_files_to_run,
files = runtime_files if hermetic else None,
coverage_tool = coverage_tool,
coverage_files = coverage_files,
Expand All @@ -119,6 +139,7 @@ def _py_runtime_impl(ctx):
bootstrap_template = ctx.file.bootstrap_template,
)
builtin_py_runtime_info_kwargs = dict(py_runtime_info_kwargs)
builtin_py_runtime_info_kwargs.pop("interpreter_files_to_run", None)

# There are all args that BuiltinPyRuntimeInfo doesn't support
py_runtime_info_kwargs.update(dict(
Expand Down
13 changes: 11 additions & 2 deletions tests/py_runtime/py_runtime_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ def _test_in_build_interpreter_impl(env, target):
info.python_version().equals("PY3")
info.files().contains_predicate(matching.file_basename_equals("file1.txt"))
info.interpreter().path().contains("fake_interpreter")
env.expect.that_bool(info.actual.interpreter_files_to_run == None).equals(True)

_tests.append(_test_in_build_interpreter)

Expand Down Expand Up @@ -227,6 +228,9 @@ def _test_interpreter_binary_with_multiple_outputs_impl(env, target):
factory = py_runtime_info_subject,
)
py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter")
py_runtime_info.interpreter_files_to_run().executable().short_path_equals(
"{package}/{test_name}_built_interpreter",
)
py_runtime_info.files().contains_exactly([
"{package}/extra_default_output.txt",
"{package}/runfile.txt",
Expand Down Expand Up @@ -272,6 +276,9 @@ def _test_interpreter_binary_with_single_output_and_runfiles_impl(env, target):
factory = py_runtime_info_subject,
)
py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter")
py_runtime_info.interpreter_files_to_run().executable().short_path_equals(
"{package}/{test_name}_built_interpreter",
)
py_runtime_info.files().contains_exactly([
"{package}/runfile.txt",
"{package}/{test_name}_built_interpreter",
Expand Down Expand Up @@ -327,10 +334,12 @@ def _test_system_interpreter(name):
)

def _test_system_interpreter_impl(env, target):
env.expect.that_target(target).provider(
info = env.expect.that_target(target).provider(
PyRuntimeInfo,
factory = py_runtime_info_subject,
).interpreter_path().equals("/system/python")
)
info.interpreter_path().equals("/system/python")
env.expect.that_bool(info.actual.interpreter_files_to_run == None).equals(True)

_tests.append(_test_system_interpreter)

Expand Down
159 changes: 159 additions & 0 deletions tests/py_runtime_info/py_runtime_info_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
load("@rules_testing//lib:test_suite.bzl", "test_suite")
load("@rules_testing//lib:truth.bzl", "matching")
load("//python:py_runtime_info.bzl", "PyRuntimeInfo")
load("//tests/support:py_runtime_info_subject.bzl", "py_runtime_info_subject")

def _create_py_runtime_info_without_interpreter_version_info_impl(ctx):
return [PyRuntimeInfo(
Expand All @@ -35,6 +37,57 @@ _create_py_runtime_info_without_interpreter_version_info = rule(
},
)

def _simple_binary_impl(ctx):
executable = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(executable, "", is_executable = True)
return [DefaultInfo(
executable = executable,
files = depset([executable]),
)]

_simple_binary = rule(
implementation = _simple_binary_impl,
executable = True,
)

def _file_target_impl(ctx):
output = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(output, "")
return [DefaultInfo(files = depset([output]))]

_file_target = rule(
implementation = _file_target_impl,
)

def _create_py_runtime_info_with_interpreter_files_to_run_impl(ctx):
files_to_run = ctx.attr.files_to_run[DefaultInfo].files_to_run
kwargs = dict(
bootstrap_template = ctx.file.bootstrap_template,
interpreter_files_to_run = files_to_run,
python_version = "PY3",
)
if ctx.attr.use_interpreter_path:
kwargs["interpreter_path"] = "/python"
else:
kwargs["files"] = depset()
kwargs["interpreter"] = ctx.executable.interpreter

return [PyRuntimeInfo(**kwargs)]

_create_py_runtime_info_with_interpreter_files_to_run = rule(
implementation = _create_py_runtime_info_with_interpreter_files_to_run_impl,
attrs = {
"bootstrap_template": attr.label(allow_single_file = True, default = "bootstrap.txt"),
"files_to_run": attr.label(mandatory = True),
"interpreter": attr.label(
cfg = "target",
executable = True,
mandatory = True,
),
"use_interpreter_path": attr.bool(),
},
)

_tests = []

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

_tests.append(_test_can_create_py_runtime_info_without_interpreter_version_info)

def _test_interpreter_files_to_run_with_interpreter(name):
_simple_binary(
name = name + "_interpreter",
)
_create_py_runtime_info_with_interpreter_files_to_run(
name = name + "_subject",
files_to_run = name + "_interpreter",
interpreter = name + "_interpreter",
)
analysis_test(
name = name,
target = name + "_subject",
impl = _test_interpreter_files_to_run_with_interpreter_impl,
)

def _test_interpreter_files_to_run_with_interpreter_impl(env, target):
info = env.expect.that_target(target).provider(
PyRuntimeInfo,
factory = py_runtime_info_subject,
)
info.interpreter().short_path_equals("{package}/{test_name}_interpreter")
info.interpreter_files_to_run().executable().short_path_equals(
"{package}/{test_name}_interpreter",
)

_tests.append(_test_interpreter_files_to_run_with_interpreter)

def _test_interpreter_files_to_run_disallows_interpreter_path(name):
_simple_binary(
name = name + "_interpreter",
)
_create_py_runtime_info_with_interpreter_files_to_run(
name = name + "_subject",
files_to_run = name + "_interpreter",
interpreter = name + "_interpreter",
tags = ["manual"],
use_interpreter_path = True,
)
analysis_test(
name = name,
target = name + "_subject",
impl = _test_interpreter_files_to_run_disallows_interpreter_path_impl,
expect_failure = True,
)

def _test_interpreter_files_to_run_disallows_interpreter_path_impl(env, target):
env.expect.that_target(target).failures().contains_predicate(
matching.str_matches("*interpreter_files_to_run*interpreter_path*"),
)

_tests.append(_test_interpreter_files_to_run_disallows_interpreter_path)

def _test_interpreter_files_to_run_requires_executable(name):
_simple_binary(
name = name + "_interpreter",
)
_file_target(
name = name + "_files_to_run",
)
_create_py_runtime_info_with_interpreter_files_to_run(
name = name + "_subject",
files_to_run = name + "_files_to_run",
interpreter = name + "_interpreter",
tags = ["manual"],
)
analysis_test(
name = name,
target = name + "_subject",
impl = _test_interpreter_files_to_run_requires_executable_impl,
expect_failure = True,
)

def _test_interpreter_files_to_run_requires_executable_impl(env, target):
env.expect.that_target(target).failures().contains_predicate(
matching.str_matches("*interpreter_files_to_run*executable*"),
)

_tests.append(_test_interpreter_files_to_run_requires_executable)

def _test_interpreter_files_to_run_requires_matching_interpreter(name):
_simple_binary(
name = name + "_interpreter",
)
_simple_binary(
name = name + "_other_interpreter",
)
_create_py_runtime_info_with_interpreter_files_to_run(
name = name + "_subject",
files_to_run = name + "_other_interpreter",
interpreter = name + "_interpreter",
tags = ["manual"],
)
analysis_test(
name = name,
target = name + "_subject",
impl = _test_interpreter_files_to_run_requires_matching_interpreter_impl,
expect_failure = True,
)

def _test_interpreter_files_to_run_requires_matching_interpreter_impl(env, target):
env.expect.that_target(target).failures().contains_predicate(
matching.str_matches("*interpreter_files_to_run.executable*interpreter*"),
)

_tests.append(_test_interpreter_files_to_run_requires_matching_interpreter)

def py_runtime_info_test_suite(name):
test_suite(
name = name,
Expand Down
12 changes: 12 additions & 0 deletions tests/support/py_runtime_info_subject.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ def py_runtime_info_subject(info, *, meta):
coverage_tool = lambda *a, **k: _py_runtime_info_subject_coverage_tool(self, *a, **k),
files = lambda *a, **k: _py_runtime_info_subject_files(self, *a, **k),
interpreter = lambda *a, **k: _py_runtime_info_subject_interpreter(self, *a, **k),
interpreter_files_to_run = lambda *a, **k: (
_py_runtime_info_subject_interpreter_files_to_run(self, *a, **k)
),
interpreter_path = lambda *a, **k: _py_runtime_info_subject_interpreter_path(self, *a, **k),
interpreter_version_info = lambda *a, **k: _py_runtime_info_subject_interpreter_version_info(self, *a, **k),
python_version = lambda *a, **k: _py_runtime_info_subject_python_version(self, *a, **k),
Expand Down Expand Up @@ -84,6 +87,15 @@ def _py_runtime_info_subject_interpreter(self):
meta = self.meta.derive("interpreter()"),
)

def _py_runtime_info_subject_interpreter_files_to_run(self):
return subjects.struct(
self.actual.interpreter_files_to_run,
attrs = dict(
executable = subjects.file,
),
meta = self.meta.derive("interpreter_files_to_run()"),
)

def _py_runtime_info_subject_interpreter_path(self):
return subjects.str(
self.actual.interpreter_path,
Expand Down