write_file: add rule and tests (#122)
This PR adds two new rules: write_file and write_xfile. Both rules solve a common problem: to write a text file with user-defined contents. The problem is routinely solved using a genrule. That however requires Bash, since genrules execute Bash commands. Requiring Bash is a problem on Windows. The new rules do not require any shell. The only difference between the rules is that write_xfile creates an executable file while write_file doesn't. See https://github.com/bazelbuild/bazel/issues/4319
This commit is contained in:
parent
db27394846
commit
2d1669ed88
|
@ -105,3 +105,10 @@ stardoc(
|
||||||
input = "//rules:maprule.bzl",
|
input = "//rules:maprule.bzl",
|
||||||
deps = ["//rules:maprule"],
|
deps = ["//rules:maprule"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
stardoc(
|
||||||
|
name = "write_file_docs",
|
||||||
|
out = "write_file_doc_gen.md",
|
||||||
|
input = "//rules:write_file.bzl",
|
||||||
|
deps = ["//rules:write_file"],
|
||||||
|
)
|
||||||
|
|
12
rules/BUILD
12
rules/BUILD
|
@ -44,6 +44,18 @@ bzl_library(
|
||||||
srcs = ["maprule_util.bzl"],
|
srcs = ["maprule_util.bzl"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bzl_library(
|
||||||
|
name = "write_file",
|
||||||
|
srcs = ["write_file.bzl"],
|
||||||
|
deps = [":write_file_private"],
|
||||||
|
)
|
||||||
|
|
||||||
|
bzl_library(
|
||||||
|
name = "write_file_private",
|
||||||
|
srcs = ["write_file_private.bzl"],
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
)
|
||||||
|
|
||||||
# Exported for build_test.bzl to make sure of, it is an implementation detail
|
# 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.
|
# of the rule and should not be directly used by anything else.
|
||||||
exports_files(["empty_test.sh"])
|
exports_files(["empty_test.sh"])
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""A rule that writes a UTF-8 encoded text file from user-specified contents.
|
||||||
|
|
||||||
|
native.genrule() is sometimes used to create a text file. The 'write_file' and
|
||||||
|
macro does this with a simpler interface than genrule.
|
||||||
|
|
||||||
|
The rules generated by the macro do not use Bash or any other shell to write the
|
||||||
|
file. Instead they use Starlark's built-in file writing action
|
||||||
|
(ctx.actions.write).
|
||||||
|
"""
|
||||||
|
|
||||||
|
load(
|
||||||
|
":write_file_private.bzl",
|
||||||
|
_write_file = "write_file",
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file = _write_file
|
|
@ -0,0 +1,85 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Implementation of write_file macro and underlying rules.
|
||||||
|
|
||||||
|
These rules write a UTF-8 encoded text file, using Bazel's FileWriteAction.
|
||||||
|
'_write_xfile' marks the resulting file executable, '_write_file' does not.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _common_impl(ctx, is_executable):
|
||||||
|
# ctx.actions.write creates a FileWriteAction which uses UTF-8 encoding.
|
||||||
|
ctx.actions.write(
|
||||||
|
output = ctx.outputs.out,
|
||||||
|
content = "\n".join(ctx.attr.content) if ctx.attr.content else "",
|
||||||
|
is_executable = is_executable,
|
||||||
|
)
|
||||||
|
files = depset(direct = [ctx.outputs.out])
|
||||||
|
runfiles = ctx.runfiles(files = [ctx.outputs.out])
|
||||||
|
if is_executable:
|
||||||
|
return [DefaultInfo(files = files, runfiles = runfiles, executable = ctx.outputs.out)]
|
||||||
|
else:
|
||||||
|
return [DefaultInfo(files = files, runfiles = runfiles)]
|
||||||
|
|
||||||
|
def _impl(ctx):
|
||||||
|
return _common_impl(ctx, False)
|
||||||
|
|
||||||
|
def _ximpl(ctx):
|
||||||
|
return _common_impl(ctx, True)
|
||||||
|
|
||||||
|
_ATTRS = {
|
||||||
|
"content": attr.string_list(mandatory = False, allow_empty = True),
|
||||||
|
"out": attr.output(mandatory = True),
|
||||||
|
}
|
||||||
|
|
||||||
|
_write_file = rule(
|
||||||
|
implementation = _impl,
|
||||||
|
provides = [DefaultInfo],
|
||||||
|
attrs = _ATTRS,
|
||||||
|
)
|
||||||
|
|
||||||
|
_write_xfile = rule(
|
||||||
|
implementation = _ximpl,
|
||||||
|
executable = True,
|
||||||
|
provides = [DefaultInfo],
|
||||||
|
attrs = _ATTRS,
|
||||||
|
)
|
||||||
|
|
||||||
|
def write_file(name, out, content = [], is_executable = False, **kwargs):
|
||||||
|
"""Creates a UTF-8 encoded text file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Name of the rule.
|
||||||
|
out: Path of the output file, relative to this package.
|
||||||
|
content: A list of strings. Lines of text, the contents of the file.
|
||||||
|
Newlines are added automatically after every line except the last one.
|
||||||
|
is_executable: A boolean. Whether to make the output file executable. When
|
||||||
|
True, the rule's output can be executed using `bazel run` and can be
|
||||||
|
in the srcs of binary and test rules that require executable sources.
|
||||||
|
**kwargs: further keyword arguments, e.g. `visibility`
|
||||||
|
"""
|
||||||
|
if is_executable:
|
||||||
|
_write_xfile(
|
||||||
|
name = name,
|
||||||
|
content = content,
|
||||||
|
out = out,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_write_file(
|
||||||
|
name = name,
|
||||||
|
content = content,
|
||||||
|
out = out,
|
||||||
|
**kwargs
|
||||||
|
)
|
|
@ -0,0 +1,117 @@
|
||||||
|
# This package aids testing the 'write_file' rule.
|
||||||
|
#
|
||||||
|
# The package contains 4 write_file rules:
|
||||||
|
# - 'write_empty_text' and 'write_empty_bin' write an empty text and an empty
|
||||||
|
# executable file respectively
|
||||||
|
# - 'write_nonempty_text' and 'write_nonempty_bin' write a non-empty text and
|
||||||
|
# a non-empty executable file (a shell script) respectively
|
||||||
|
#
|
||||||
|
# The 'bin_empty' and 'bin_nonempty' rules are sh_binary rules. They use
|
||||||
|
# the 'write_empty_bin' and 'write_nonempty_bin' rules respectively. The
|
||||||
|
# sh_binary rule requires its source to be executable, so building these two
|
||||||
|
# rules successfully means that 'write_file' managed to make its output
|
||||||
|
# executable.
|
||||||
|
#
|
||||||
|
# The 'run_executables' genrule runs the 'bin_empty' and 'bin_nonempty'
|
||||||
|
# binaries, partly to ensure they can be run, and partly so we can observe their
|
||||||
|
# output and assert the contents in the 'write_file_tests' test.
|
||||||
|
#
|
||||||
|
# The 'file_deps' filegroup depends on 'write_empty_text'. The filegroup rule
|
||||||
|
# uses the DefaultInfo.files field from its dependencies. When we data-depend on
|
||||||
|
# the filegroup from 'write_file_tests', we transitively data-depend on the
|
||||||
|
# DefaultInfo.files of the 'write_empty_text' rule.
|
||||||
|
#
|
||||||
|
# The 'write_file_tests' test is the actual integration test. It data-depends
|
||||||
|
# on:
|
||||||
|
# - the 'run_executables' rule, to get the outputs of 'bin_empty' and
|
||||||
|
# 'bin_nonempty'
|
||||||
|
# - the 'file_deps' rule, and by nature of using a filegroup, we get the files
|
||||||
|
# from the DefaultInfo.files of the 'write_file' rule, and thereby assert that
|
||||||
|
# that field contains the output file of the rule
|
||||||
|
# - the 'write_nonempty_text' rule, and thereby on the DefaultInfo.runfiles
|
||||||
|
# field of it, so we assert that that field contains the output file of the
|
||||||
|
# rule
|
||||||
|
|
||||||
|
load("//rules:write_file.bzl", "write_file")
|
||||||
|
|
||||||
|
package(default_testonly = 1)
|
||||||
|
|
||||||
|
sh_test(
|
||||||
|
name = "write_file_tests",
|
||||||
|
srcs = ["write_file_tests.sh"],
|
||||||
|
data = [
|
||||||
|
":run_executables",
|
||||||
|
# Use DefaultInfo.files from 'write_empty_text' (via 'file_deps').
|
||||||
|
":file_deps",
|
||||||
|
# Use DefaultInfo.runfiles from 'write_nonempty_text'.
|
||||||
|
":write_nonempty_text",
|
||||||
|
"//tests:unittest.bash",
|
||||||
|
],
|
||||||
|
deps = ["@bazel_tools//tools/bash/runfiles"],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "file_deps",
|
||||||
|
# Use DefaultInfo.files from 'write_empty_text'.
|
||||||
|
srcs = [":write_empty_text"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# If 'run_executables' is built, then 'bin_nonempty' and 'bin_empty' are
|
||||||
|
# executable, asserting that write_file makes the output executable.
|
||||||
|
genrule(
|
||||||
|
name = "run_executables",
|
||||||
|
outs = [
|
||||||
|
"empty-bin-out.txt",
|
||||||
|
"nonempty-bin-out.txt",
|
||||||
|
],
|
||||||
|
cmd = ("$(location :bin_empty) > $(location empty-bin-out.txt) && " +
|
||||||
|
"$(location :bin_nonempty) > $(location nonempty-bin-out.txt)"),
|
||||||
|
output_to_bindir = 1,
|
||||||
|
tools = [
|
||||||
|
":bin_empty",
|
||||||
|
":bin_nonempty",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# If 'bin_empty' is built, then 'write_empty_bin' made its output executable.
|
||||||
|
sh_binary(
|
||||||
|
name = "bin_empty",
|
||||||
|
srcs = [":write_empty_bin"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# If 'bin_nonempty' is built, then 'write_nonempty_bin' made its output
|
||||||
|
# executable.
|
||||||
|
sh_binary(
|
||||||
|
name = "bin_nonempty",
|
||||||
|
srcs = [":write_nonempty_bin"],
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(
|
||||||
|
name = "write_empty_text",
|
||||||
|
out = "out/empty.txt",
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(
|
||||||
|
name = "write_nonempty_text",
|
||||||
|
out = "out/nonempty.txt",
|
||||||
|
content = [
|
||||||
|
"aaa",
|
||||||
|
"bbb",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(
|
||||||
|
name = "write_empty_bin",
|
||||||
|
out = "out/empty.sh",
|
||||||
|
is_executable = True,
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(
|
||||||
|
name = "write_nonempty_bin",
|
||||||
|
out = "out/nonempty.sh",
|
||||||
|
content = [
|
||||||
|
"#!/bin/bash",
|
||||||
|
"echo potato",
|
||||||
|
],
|
||||||
|
is_executable = True,
|
||||||
|
)
|
|
@ -0,0 +1,66 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# --- begin runfiles.bash initialization ---
|
||||||
|
# Copy-pasted from Bazel's Bash runfiles library (tools/bash/runfiles/runfiles.bash).
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
|
||||||
|
if [[ -f "$0.runfiles_manifest" ]]; then
|
||||||
|
export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
|
||||||
|
elif [[ -f "$0.runfiles/MANIFEST" ]]; then
|
||||||
|
export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
|
||||||
|
elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
|
||||||
|
export RUNFILES_DIR="$0.runfiles"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
|
||||||
|
source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
|
||||||
|
elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
|
||||||
|
source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
|
||||||
|
"$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
|
||||||
|
else
|
||||||
|
echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# --- end runfiles.bash initialization ---
|
||||||
|
|
||||||
|
source "$(rlocation bazel_skylib/tests/unittest.bash)" \
|
||||||
|
|| { echo "Could not source bazel_skylib/tests/unittest.bash" >&2; exit 1; }
|
||||||
|
|
||||||
|
function assert_empty_file() {
|
||||||
|
local -r path="$1"
|
||||||
|
# Not using 'du' to check the file is empty, because it doesn't work on CI.
|
||||||
|
[[ "$(echo -n "($(cat "$path"))")" = "()" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_write_empty_text() {
|
||||||
|
assert_empty_file "$(rlocation bazel_skylib/tests/write_file/out/empty.txt)"
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_write_nonempty_text() {
|
||||||
|
cat "$(rlocation bazel_skylib/tests/write_file/out/nonempty.txt)" >"$TEST_log"
|
||||||
|
expect_log '^aaa$'
|
||||||
|
expect_log '^bbb$'
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_write_empty_bin() {
|
||||||
|
assert_empty_file "$(rlocation bazel_skylib/tests/write_file/empty-bin-out.txt)"
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_write_nonempty_bin() {
|
||||||
|
cat "$(rlocation bazel_skylib/tests/write_file/nonempty-bin-out.txt)" >"$TEST_log"
|
||||||
|
expect_log '^potato$'
|
||||||
|
}
|
||||||
|
|
||||||
|
run_suite "write_file_tests test suite"
|
Loading…
Reference in New Issue