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:
László Csomor 2019-03-19 07:52:56 +01:00 committed by GitHub
parent db27394846
commit 2d1669ed88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 317 additions and 0 deletions

View File

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

View File

@ -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"])

30
rules/write_file.bzl Normal file
View File

@ -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

View 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
)

117
tests/write_file/BUILD Normal file
View File

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

View File

@ -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"