blob: 64a4b18efa32000131bdde11cb745e2556c53e9f [file]
"""Rules that execute `ctx.actions.run_shell` and expose the produced output.
The single output file of each target is compared against a golden file with
`diff_test`, which exercises the runtime behavior of `run_shell` (command
execution, positional argument substitution, environment, helper scripts for
long commands) without a Bazel-in-Bazel integration test.
"""
load("@with_cfg.bzl", "with_cfg")
def _run_shell_output_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".out")
ctx.actions.run_shell(
outputs = [out],
command = ctx.attr.command,
arguments = [out.path] + ctx.attr.extra_arguments,
env = ctx.attr.env,
use_default_shell_env = ctx.attr.use_default_shell_env,
mnemonic = "RunShellOutput",
)
return [DefaultInfo(files = depset([out]))]
run_shell_output = rule(
implementation = _run_shell_output_impl,
attrs = {
# The shell command. `$1` is the output path; `$2`, `$3`, ... are
# `extra_arguments`.
"command": attr.string(mandatory = True),
"extra_arguments": attr.string_list(),
"env": attr.string_dict(),
"use_default_shell_env": attr.bool(),
},
)
# Same as run_shell_output, but transitions --action_env so that targets can
# observe an action env entry when use_default_shell_env = True.
run_shell_output_with_action_env, _run_shell_output_with_action_env = (
with_cfg(run_shell_output)
.extend("action_env", ["FROM_ACTION_ENV=action_env_value"])
.build()
)
def _run_shell_script_args_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".out")
# A script that records its own positional arguments.
script = ctx.actions.declare_file(ctx.label.name + "_echo_args.sh")
ctx.actions.write(
output = script,
content = """#!/bin/bash
set -euo pipefail
echo "args=($*)" > "$OUT"
""",
is_executable = True,
)
# `arguments` are passed as the positional parameters ($1, $2, ...) of the
# command string (with an empty $0), not to a script named within it.
command = script.path
if ctx.attr.forward_arguments:
command += " \"$@\""
ctx.actions.run_shell(
outputs = [out],
tools = [script],
command = command,
arguments = ["a", "b", "c"],
env = {"OUT": out.path},
mnemonic = "RunShellScriptArgs",
)
return [DefaultInfo(files = depset([out]))]
run_shell_script_args = rule(
implementation = _run_shell_script_args_impl,
attrs = {
"forward_arguments": attr.bool(),
},
)
def _run_shell_helper_script_args_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".out")
command = 'echo "arg=$1" > "$OUT"'
if ctx.attr.long:
# Pad past the spill threshold with a trailing comment so the command is
# written to a helper script without otherwise changing its behavior.
command += " #" + ("x" * 70000)
ctx.actions.run_shell(
outputs = [out],
command = command,
arguments = ["the_argument"],
env = {"OUT": out.path},
mnemonic = "RunShellHelperScriptArgs",
)
return [DefaultInfo(files = depset([out]))]
run_shell_helper_script_args = rule(
implementation = _run_shell_helper_script_args_impl,
attrs = {
"long": attr.bool(),
},
)
def _run_shell_long_output_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".out")
# A command well above the 64,000 char (8,000 on Windows) threshold at which
# run_shell spills the command into a helper script. The length comes from a
# trailing comment rather than many statements: a deeply nested command list
# (e.g. tens of thousands of `;`-separated commands) overflows the stack of
# the smaller-stacked bash on Windows. The output path is embedded directly
# because positional arguments are not forwarded into the helper script.
command = "echo done > %s #%s" % (out.path, "x" * 70000)
ctx.actions.run_shell(
outputs = [out],
command = command,
mnemonic = "RunShellLongOutput",
)
return [DefaultInfo(files = depset([out]))]
run_shell_long_output = rule(
implementation = _run_shell_long_output_impl,
)