From bf8a55b6687fc93c003d07505d886bbd1a7ff39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20Csomor?= Date: Tue, 21 May 2019 14:46:09 +0200 Subject: [PATCH] run_binary: runs an executable as an action (#153) This rule is an alternative for genrule(): it can run a binary with the desired arguments, environment, inputs, and outputs, as a single build action, without shelling out to Bash. Fixes https://github.com/bazelbuild/bazel-skylib/issues/149 --- docs/BUILD | 7 ++ docs/run_binary_doc.md | 75 +++++++++++++++ rules/BUILD | 6 ++ rules/run_binary.bzl | 97 ++++++++++++++++++++ tests/run_binary/BUILD | 166 ++++++++++++++++++++++++++++++++++ tests/run_binary/printargs.cc | 36 ++++++++ 6 files changed, 387 insertions(+) create mode 100755 docs/run_binary_doc.md create mode 100644 rules/run_binary.bzl create mode 100644 tests/run_binary/BUILD create mode 100644 tests/run_binary/printargs.cc diff --git a/docs/BUILD b/docs/BUILD index b959e15..9894eb5 100644 --- a/docs/BUILD +++ b/docs/BUILD @@ -121,3 +121,10 @@ stardoc( input = "//rules:native_binary.bzl", deps = ["//rules:native_binary"], ) + +stardoc( + name = "run_binary_docs", + out = "run_binary_doc_gen.md", + input = "//rules:run_binary.bzl", + deps = ["//rules:run_binary"], +) diff --git a/docs/run_binary_doc.md b/docs/run_binary_doc.md new file mode 100755 index 0000000..188e277 --- /dev/null +++ b/docs/run_binary_doc.md @@ -0,0 +1,75 @@ + +## run_binary + +
+run_binary(name, args, env, outs, srcs, tool)
+
+ +Runs a binary as a build action.

This rule does not require Bash (unlike native.genrule). + +### Attributes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
name + Name; required +

+ A unique name for this target. +

+
args + List of strings; optional +

+ Command line arguments of the binary.

Subject to$(location) expansion. +

+
env + Dictionary: String -> String; optional +

+ Environment variables of the action.

Subject to $(location) expansion. +

+
outs + List of labels; required +

+ Output files generated by the action.

These labels are available for $(location) expansion in args and env. +

+
srcs + List of labels; optional +

+ Additional inputs of the action.

These labels are available for $(location) expansion in args and env. +

+
tool + Label; required +

+ The tool to run in the action.

Must be the label of a *_binary rule, of a rule that generates an executable file, or of a file that can be executed as a subprocess (e.g. an .exe or .bat file on Windows or a binary with executable permission on Linux). This label is available for $(location) expansion in args and env. +

+
+ + diff --git a/rules/BUILD b/rules/BUILD index 5a976aa..d61e755 100644 --- a/rules/BUILD +++ b/rules/BUILD @@ -33,6 +33,12 @@ bzl_library( deps = ["//rules/private:copy_file_private"], ) +bzl_library( + name = "run_binary", + srcs = ["run_binary.bzl"], + deps = ["//lib:dicts"], +) + # Exported for build_test.bzl to make sure of, it is an implementation detail # of the rule and should not be directly used by anything else. exports_files(["empty_test.sh"]) diff --git a/rules/run_binary.bzl b/rules/run_binary.bzl new file mode 100644 index 0000000..76a5393 --- /dev/null +++ b/rules/run_binary.bzl @@ -0,0 +1,97 @@ +# Copyright 2019 The Bazel Authors. All rights reserved. +# +# 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. + +""" +run_binary() build rule implementation. + +Runs a binary as a build action. This rule does not require Bash (unlike native.genrule()). +""" + +load("//lib:dicts.bzl", "dicts") + +def _impl(ctx): + tool_as_list = [ctx.attr.tool] + tool_inputs, tool_input_mfs = ctx.resolve_tools(tools = tool_as_list) + args = [ + # Expand $(location) / $(locations) in args. + # + # To keep the rule simple, do not expand Make Variables (like *_binary.args usually would). + # (We can add this feature later if users ask for it.) + # + # Also for simple implementation and usage, do not Bash-tokenize the arguments. Without + # tokenization the user can write args=["a b"] to pass (a b) as one argument, but with + # tokenization they would have to write args=["'a b'"] or args=["a\\ b"]. There's no + # documented tokenization function anyway (as of 2019-05-21 ctx.tokenize exists but is + # undocumented, see https://github.com/bazelbuild/bazel/issues/8389). + ctx.expand_location(a, tool_as_list) if "$(location" in a else a + for a in ctx.attr.args + ] + envs = { + # Expand $(location) / $(locations) in the values. + k: ctx.expand_location(v, tool_as_list) if "$(location" in v else v + for k, v in ctx.attr.env.items() + } + ctx.actions.run( + outputs = ctx.outputs.outs, + inputs = depset(direct = ctx.files.srcs, transitive = [tool_inputs]), + executable = ctx.executable.tool, + arguments = args, + mnemonic = "RunBinary", + use_default_shell_env = False, + env = dicts.add(ctx.configuration.default_shell_env, envs), + input_manifests = tool_input_mfs, + ) + return DefaultInfo( + files = depset(items = ctx.outputs.outs), + runfiles = ctx.runfiles(files = ctx.outputs.outs), + ) + +run_binary = rule( + implementation = _impl, + doc = "Runs a binary as a build action.

This rule does not require Bash (unlike" + + " native.genrule).", + attrs = { + "tool": attr.label( + doc = "The tool to run in the action.

Must be the label of a *_binary rule," + + " of a rule that generates an executable file, or of a file that can be" + + " executed as a subprocess (e.g. an .exe or .bat file on Windows or a binary" + + " with executable permission on Linux). This label is available for" + + " $(location) expansion in args and env.", + executable = True, + allow_files = True, + mandatory = True, + cfg = "host", + ), + "env": attr.string_dict( + doc = "Environment variables of the action.

Subject to " + + " $(location)" + + " expansion.", + ), + "srcs": attr.label_list( + allow_files = True, + doc = "Additional inputs of the action.

These labels are available for" + + " $(location) expansion in args and env.", + ), + "outs": attr.output_list( + mandatory = True, + doc = "Output files generated by the action.

These labels are available for" + + " $(location) expansion in args and env.", + ), + "args": attr.string_list( + doc = "Command line arguments of the binary.

Subject to" + + "$(location)" + + " expansion.", + ), + }, +) diff --git a/tests/run_binary/BUILD b/tests/run_binary/BUILD new file mode 100644 index 0000000..d780f60 --- /dev/null +++ b/tests/run_binary/BUILD @@ -0,0 +1,166 @@ +load("//rules:diff_test.bzl", "diff_test") +load("//rules:run_binary.bzl", "run_binary") +load("//rules:write_file.bzl", "write_file") + +package( + default_testonly = 1, + default_visibility = ["//visibility:private"], +) + +diff_test( + name = "run_script_test", + file1 = ":run_script.out", + file2 = ":run_script_expected", +) + +# Generate this file with write_file instead of checking it in to the source +# tree. This ensures line endings are consistent across "run_script.expected" +# and "run_script.out". +write_file( + name = "run_script_expected", + out = "run_script.expected", + content = [ + "arg1=(foo)", + "arg2=(bar)", + "ENV_LOCATION=(a tests/run_binary/BUILD)", + "ENV_LOCATIONS=(b\\ tests/run_binary/BUILD tests/run_binary/printargs.cc)", + "ENV_COMPLEX=(xx/yy \\\"zz)", + "ENV_PATH_BASH=($PATH)", + "ENV_PATH_CMD=(%PATH%)", + # Can't prevent "echo" from adding a newline on Windows, so let's add + # one to the expected output too. + "", + ], +) + +run_binary( + name = "run_script", + srcs = [ + "BUILD", + ":dummy_srcs", + ], + outs = ["run_script.out"], + # Not testing any complex arguments here, because Windows .bat file argument + # escaping is different from most MSVC-built Windows binaries. We test + # argument escaping in "run_bin". + args = [ + "foo", + "bar", + ], + # Test complex environment variables. They are location-expanded but not + # Bash-tokenized, and should appear the same for Windows .bat files and Bash + # .sh scripts. + env = { + # Testing $(location) expansion only on source files so the result is + # predictable. The path of generated files depends on the target + # platform. + "ENV_LOCATION": "a $(location BUILD)", + "ENV_LOCATIONS": "b\\ $(locations :dummy_srcs)", + "ENV_COMPLEX": "xx/yy \\\"zz", + "ENV_PATH_BASH": "$PATH", + "ENV_PATH_CMD": "%PATH%", + "OUT": "$(location run_script.out)", + }, + tool = ":script", +) + +write_file( + name = "script", + # On Windows we need the ".bat" extension. + # On other platforms the extension doesn't matter. + # Therefore we can use ".bat" on every platform. + out = "script.bat", + content = select({ + "@bazel_tools//src/conditions:host_windows": [ + "@echo>%OUT% arg1=(%1)", + "@echo>>%OUT% arg2=(%2)", + "@echo>>%OUT% ENV_LOCATION=(%ENV_LOCATION%)", + "@echo>>%OUT% ENV_LOCATIONS=(%ENV_LOCATIONS%)", + "@echo>>%OUT% ENV_COMPLEX=(%ENV_COMPLEX%)", + "@echo>>%OUT% ENV_PATH_BASH=(%ENV_PATH_BASH%)", + "@echo>>%OUT% ENV_PATH_CMD=(%ENV_PATH_CMD%)", + ], + "//conditions:default": [ + "#!/bin/bash", + "echo > \"$OUT\" \"arg1=($1)\"", + "echo >> \"$OUT\" \"arg2=($2)\"", + "echo >> \"$OUT\" \"ENV_LOCATION=($ENV_LOCATION)\"", + "echo >> \"$OUT\" \"ENV_LOCATIONS=($ENV_LOCATIONS)\"", + "echo >> \"$OUT\" \"ENV_COMPLEX=($ENV_COMPLEX)\"", + "echo >> \"$OUT\" \"ENV_PATH_BASH=($ENV_PATH_BASH)\"", + "echo >> \"$OUT\" \"ENV_PATH_CMD=($ENV_PATH_CMD)\"", + ], + }), + is_executable = True, +) + +diff_test( + name = "run_bin_test", + file1 = ":run_bin.out", + file2 = ":run_bin_expected", +) + +# Generate this file with write_file instead of checking it in to the source +# tree. This ensures line endings are consistent across "run_bin.expected" +# and "run_bin.out". +write_file( + name = "run_bin_expected", + out = "run_bin.expected", + content = [ + "arg1=(a b)", + "arg2=(\"c d\")", + "arg3=(e\\ f)", + "arg4=(xx/yy\\ \\\"zz)", + "arg5=(tests/run_binary/BUILD)", + "arg6=(tests/run_binary/BUILD tests/run_binary/printargs.cc)", + "arg7=('tests/run_binary/BUILD $tests/run_binary/BUILD')", + "arg8=($PATH)", + "arg9=($$PATH)", + "arg10=(${PATH})", + # Add trailing newline, as printed by printargs. + "", + ], +) + +run_binary( + name = "run_bin", + srcs = [ + "BUILD", + ":dummy_srcs", + ], + outs = ["run_bin.out"], + # Test complex arguments here. They are location-expanded but not + # Bash-tokenized, and should appear the same on every platform. + args = [ + "a b", + "\"c d\"", + "e\\ f", + "xx/yy\\ \\\"zz", + # Testing $(location) expansion only on source files so the result is + # predictable. The path of generated files depends on the target + # platform. + "$(location BUILD)", + "$(locations :dummy_srcs)", + "'$(location BUILD) $$(location BUILD)'", + "$PATH", + "$$PATH", + "${PATH}", + ], + # Not testing any complex envvars here, because we already did in + # "run_script". + env = {"OUT": "$(location run_bin.out)"}, + tool = ":printargs", +) + +filegroup( + name = "dummy_srcs", + srcs = [ + "BUILD", + "printargs.cc", + ], +) + +cc_binary( + name = "printargs", + srcs = ["printargs.cc"], +) diff --git a/tests/run_binary/printargs.cc b/tests/run_binary/printargs.cc new file mode 100644 index 0000000..64eedf2 --- /dev/null +++ b/tests/run_binary/printargs.cc @@ -0,0 +1,36 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// 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. + +#include +#include + +int main(int argc, char** argv) { + char* out_path = getenv("OUT"); + if (!out_path || !*out_path) { + fprintf(stderr, "ERROR(" __FILE__ ":%d): envvar OUT is undefined\n", + __LINE__); + return 1; + } + FILE* f = fopen(out_path, "wt"); + if (!f) { + fprintf(stderr, "ERROR(" __FILE__ ":%d): could not open output file '%s'\n", + __LINE__, out_path); + return 1; + } + for (int i = 1; i < argc; ++i) { + fprintf(f, "arg%d=(%s)\n", i, argv[i]); + } + fclose(f); + return 0; +}