| """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, | |
| ) |