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",
|
||||
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"],
|
||||
)
|
||||
|
||||
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
|
||||
# of the rule and should not be directly used by anything else.
|
||||
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