blob: b9cf163cc9d0b315717a2cdab59257dbce06c4de [file] [edit]
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Defines a rule for running hermetic bazel builds as executable invocations."""
load(
"//tools/bazel/sh_wrapper:sh_wrapper.bzl",
"DataDepsInfo",
"ShellWrapperInfo",
)
def _bazel_invocation_impl(ctx):
"""Implementation of the bazel_invocation rule.
Returns:
DefaultInfo for bazel run.
ShellWrapperInfo so that it can be the innermost action
in a chain of wrapped commands (Workload Dimension).
DataDepsInfo for location expansion after composition.
"""
runner_script = ctx.executable._runner_script
relevant_targets = ctx.attr.data + ctx.attr.bazel_bin + ctx.attr.srcs
# Assemble the arguments that will be passed to the runner script.
runner_args = []
for target in ctx.attr.bazel_bin:
if target[DefaultInfo].files_to_run.executable:
runner_args.extend(["--bazel_bin", target[DefaultInfo].files_to_run.executable.short_path])
else:
for f in target[DefaultInfo].files.to_list():
runner_args.extend(["--bazel_bin", f.short_path])
if ctx.file.workspace_file:
runner_args.extend(["--workspace_file", ctx.file.workspace_file.short_path])
if ctx.file.build_file:
runner_args.extend(["--build_file", ctx.file.build_file.short_path])
for src in ctx.files.srcs:
runner_args.extend(["--src", src.short_path])
# These are all the files the test needs at runtime.
runfiles = ctx.runfiles(
files = [runner_script] + ctx.files.data + ctx.files.bazel_bin + ctx.files.srcs +
([ctx.file.workspace_file] if ctx.file.workspace_file else []) +
([ctx.file.build_file] if ctx.file.build_file else []),
transitive_files = depset([ctx.file._runfiles]),
)
for target in relevant_targets:
runfiles = runfiles.merge(target[DefaultInfo].default_runfiles)
# --- Provider 1: DefaultInfo (for standalone `bazel run/test`) ---
launcher = ctx.actions.declare_file(ctx.label.name + "_launcher.sh")
ctx.actions.write(
output = launcher,
content = """#!/bin/bash
exec "{runner}" {runner_args} -- {user_args} "$@"
""".format(
runner = runner_script.short_path,
runner_args = " ".join(["'{}'".format(arg) for arg in runner_args]),
user_args = " ".join(["'{}'".format(arg) for arg in ctx.attr.args]),
),
is_executable = True,
)
default_info = DefaultInfo(
runfiles = runfiles,
executable = launcher,
)
# --- Provider 2: ShellWrapperInfo (for composition) ---
shell_wrapper_info = ShellWrapperInfo(
tokens = [runner_script.short_path] + runner_args + ["--"] + ctx.attr.args,
runfiles = runfiles,
position = "terminal",
)
data_deps_info = DataDepsInfo(
deps = depset(relevant_targets),
)
return [default_info, shell_wrapper_info, data_deps_info]
bazel_invocation = rule(
doc = """This rule defines an executable invocation of a Bazel build.
It sets up a temporary workspace and runs a Bazel build command.
""",
implementation = _bazel_invocation_impl,
executable = True,
attrs = {
"workspace_file": attr.label(allow_single_file = True),
"build_file": attr.label(allow_single_file = True),
"srcs": attr.label_list(allow_files = True),
"data": attr.label_list(allow_files = True),
"bazel_bin": attr.label_list(
default = [Label("//prebuilt:bazel_bin")],
allow_files = True,
),
"_runner_script": attr.label(
default = Label("//scripts/bazel_tests:run_bazel_test"),
executable = True,
cfg = "exec",
),
"_runfiles": attr.label(
default = Label("@bazel_tools//tools/bash/runfiles"),
allow_single_file = True,
),
},
)