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 @@ -114,6 +114,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.
* (toolchains) Support dynamically fetching and registering Python runtimes
from a python-build-standalone manifest file using
`python.override(add_runtime_manifest_urls = ..., runtime_manifest_sha = ...)`.
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
37 changes: 34 additions & 3 deletions python/private/py_runtime_rule.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,54 @@ 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]
interpreter_file = None

if interpreter_di.files_to_run and interpreter_di.files_to_run.executable:
if _is_singleton_depset(interpreter_di.files):
interpreter_file = interpreter_di.files.to_list()[0]

is_executable_source_file = (
interpreter_file and
interpreter_file.is_source and
interpreter_di.files_to_run and
interpreter_di.files_to_run.executable == interpreter_file
)

if is_executable_source_file:
# Source files Bazel treats as executable, e.g. direct file labels
# and filegroups: preserve historical runtime-file expansion, but
# do not expose a FilesToRunProvider.
interpreter = interpreter_file
runfiles = runfiles.merge(interpreter_di.default_runfiles)

runtime_files = depset(transitive = [
interpreter_di.files,
interpreter_di.default_runfiles.files,
runtime_files,
])
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 +140,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 +149,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
71 changes: 68 additions & 3 deletions tests/py_runtime/py_runtime_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ _simple_binary = rule(
executable = True,
)

def _source_file_wrapper_impl(ctx):
return [DefaultInfo(
files = depset([ctx.file.src]),
runfiles = ctx.runfiles(files = ctx.files.data),
)]

_source_file_wrapper = rule(
implementation = _source_file_wrapper_impl,
attrs = {
"data": attr.label_list(allow_files = True),
"src": attr.label(allow_single_file = True, mandatory = True),
},
)

def _test_bootstrap_template(name):
rt_util.helper_target(
py_runtime,
Expand Down Expand Up @@ -195,11 +209,54 @@ def _test_in_build_interpreter(name):
def _test_in_build_interpreter_impl(env, target):
info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject)
info.python_version().equals("PY3")
info.files().contains_predicate(matching.file_basename_equals("file1.txt"))
info.files().contains_exactly([
"{package}/fake_interpreter",
"{package}/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)

def _test_non_executable_source_file_interpreter_keeps_file_only_behavior(name):
rt_util.helper_target(
_source_file_wrapper,
name = name + "_wrapped_interpreter",
src = "fake_interpreter",
data = ["runfile.txt"],
)

rt_util.helper_target(
py_runtime,
name = name + "_subject",
interpreter = name + "_wrapped_interpreter",
python_version = "PY3",
files = ["file1.txt"],
)
analysis_test(
name = name,
target = name + "_subject",
impl = _test_non_executable_source_file_interpreter_keeps_file_only_behavior_impl,
)

def _test_non_executable_source_file_interpreter_keeps_file_only_behavior_impl(env, target):
target = env.expect.that_target(target)
py_runtime_info = target.provider(
PyRuntimeInfo,
factory = py_runtime_info_subject,
)
py_runtime_info.interpreter().short_path_equals("{package}/fake_interpreter")
env.expect.that_bool(py_runtime_info.actual.interpreter_files_to_run == None).equals(True)
py_runtime_info.files().contains_exactly([
"{package}/file1.txt",
])

target.default_outputs().contains_exactly([
"{package}/file1.txt",
])

_tests.append(_test_non_executable_source_file_interpreter_keeps_file_only_behavior)

def _test_interpreter_binary_with_multiple_outputs(name):
rt_util.helper_target(
_simple_binary,
Expand Down Expand Up @@ -227,6 +284,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 +332,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 +390,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
Loading