Skip to content

Commit 64c4e73

Browse files
authored
Add extensive runfiles examples (bazelbuild#129)
1 parent f5dfabb commit 64c4e73

File tree

9 files changed

+298
-5
lines changed

9 files changed

+298
-5
lines changed

.bazelci/presubmit.yml

+2
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ tasks:
156156
working_directory: rules
157157
build_targets:
158158
- "..."
159+
# TODO(bazel-team): Make runfiles examples work on Windows.
160+
- "-//runfiles/..."
159161
# TODO(bazel-team): Make //test_rule:80columns work on Windows
160162
# test_targets:
161163
# - "--"

rules/WORKSPACE

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
workspace(name = "examples")
2+
13
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
24

35
http_archive(

rules/runfiles/BUILD

+53-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
load(":complex_tool.bzl", "complex_tool", "sub_tool")
12
load(":execute.bzl", "execute")
3+
load(":library.bzl", "runfiles_library", "runfiles_binary")
4+
load(":tool.bzl", "tool", "tool_user")
25

3-
# `bazel build //runfiles:all` will create an executable.
4-
# `bazel run //runfiles:all` will run it.
6+
# Tests in this package do not currently work on Windows (as they create
7+
# shell-script binaries not compatible with Windows).
8+
9+
# 1. Basic example
10+
# `bazel build //runfiles:e` will create an executable.
11+
# `bazel run //runfiles:e` will run it.
512
execute(
613
name = "e",
714
# The location will be expanded to "pkg/data.txt", and it will reference
@@ -10,3 +17,47 @@ execute(
1017
command = "cat $(location :data.txt)",
1118
data = [":data.txt"],
1219
)
20+
21+
# 2. Library runfiles
22+
# A library which declares a file which is needed at runtime, and a binary
23+
# which invokes this library without direct knowledge of its runtime requirement.
24+
# `bazel run //runfiles:bin` to run this example.
25+
runfiles_library(
26+
name = "lib",
27+
command = "cat $(location :data.txt)",
28+
data = [":data.txt"],
29+
)
30+
runfiles_binary(
31+
name = "bin",
32+
lib = ":lib",
33+
)
34+
35+
# 3. Tool with runfiles
36+
# An action may require use of a tool that needs files at runtime. This demonstrates
37+
# the definition of such a tool and a rule which uses that tool.
38+
# `bazel build //runfiles:tool_user` to build this example.
39+
tool(
40+
name = "tool",
41+
)
42+
tool_user(
43+
name = "tool_user",
44+
tool = ":tool",
45+
)
46+
47+
# 4. Tool with runfiles depending on another tool
48+
# Same as (3), except adds the complexity of a tool which depends on another tool
49+
# with its own runfiles. This adds complexity, as the runfiles of the "inner tool"
50+
# is in a different location than if the inner tool were run itself.
51+
# `bazel build //runfiles:complex_tool_user` to build this example.
52+
sub_tool(
53+
name = "subtool",
54+
)
55+
complex_tool(
56+
name = "complex_tool",
57+
)
58+
tool_user(
59+
name = "complex_tool_user",
60+
tool = ":complex_tool",
61+
)
62+
63+

rules/runfiles/complex_tool.bzl

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Create a complex tool with runfiles and a rule which uses it.
2+
3+
A tool (executable used for action registration) may depend on another
4+
tool with its own runfiles. This example demonstrates this scenario."""
5+
6+
def _sub_tool_impl(ctx):
7+
# Since this tool may be used by another tool, it must support accepting
8+
# a different runfiles directory root. The runfiles directory is always
9+
# adjacent to the *root* tool being run, which may not be this tool.
10+
# (In this case, this is done by environment variable RUNFILES_DIR.)
11+
command = """
12+
if [[ -z "${RUNFILES_DIR}" ]]; then
13+
RUNFILES_DIR=${0}.runfiles
14+
fi
15+
16+
cat ${RUNFILES_DIR}/examples/runfiles/data.txt > $1"""
17+
18+
# Using root_symlinks or symlinks for a tool is very brittle if the
19+
# tool may be used by another tool; there will be a collision when merging
20+
# runfiles if the other tool defines a symlink of the same name as one
21+
# defined by this rule.
22+
ctx.actions.write(
23+
output = ctx.outputs.executable,
24+
content = command,
25+
is_executable = True,
26+
)
27+
28+
# Subtool depends on RUNFILES_DIR/<workspace_name>/runfiles/data.txt.
29+
return [DefaultInfo(
30+
runfiles = ctx.runfiles(files = [ctx.files._data[0]]),
31+
)]
32+
33+
sub_tool = rule(
34+
implementation = _sub_tool_impl,
35+
executable = True,
36+
attrs = {
37+
"command": attr.string(),
38+
"_data": attr.label(
39+
allow_files = True,
40+
default = "//runfiles:data.txt"),
41+
},
42+
)
43+
44+
def _complex_tool_impl(ctx):
45+
my_runfiles = ctx.runfiles(files = [ctx.files._data[0]])
46+
# Use runfiles.merge to merge the runfiles of both tools. All runfiles will
47+
# be rooted under the runfiles directory owned by this rule, however.
48+
my_runfiles = my_runfiles.merge(ctx.attr._subtool[DefaultInfo].default_runfiles)
49+
50+
# Thus the example directory structure is:
51+
# runfiles/complex_tool (executable)
52+
# runfiles/complex_tool.runfiles/
53+
# <workspace_name>/
54+
# runfiles/
55+
# complex_tool_data.txt
56+
# data.txt
57+
# subtool
58+
59+
runfiles_relative_tool_path = ctx.workspace_name + "/" + ctx.attr._subtool[DefaultInfo].files_to_run.executable.short_path
60+
61+
# This tool forwards its runfiles directory via the RUNFILES_DIR to the
62+
# subtool, otherwise the subtool would be looking to $0.runfiles, which does
63+
# not exist.
64+
command = ("#!/bin/bash\nexport RUNFILES_DIR=\"$0.runfiles\" && "
65+
+ "${RUNFILES_DIR}/%s $1 && cat ${RUNFILES_DIR}/examples/%s >> $1") % (
66+
runfiles_relative_tool_path, ctx.files._data[0].short_path)
67+
68+
ctx.actions.write(
69+
output = ctx.outputs.executable,
70+
content = command,
71+
is_executable = True,
72+
)
73+
74+
return [DefaultInfo(
75+
runfiles = my_runfiles,
76+
)]
77+
78+
complex_tool = rule(
79+
implementation = _complex_tool_impl,
80+
executable = True,
81+
attrs = {
82+
"command": attr.string(),
83+
"_subtool": attr.label(
84+
allow_files = True,
85+
default = "//runfiles:subtool"),
86+
"_data": attr.label(
87+
allow_files = True,
88+
default = "//runfiles:complex_tool_data.txt"),
89+
},
90+
)

rules/runfiles/complex_tool_data.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I like turtles

rules/runfiles/execute.bzl

+1-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ def _impl(ctx):
1818

1919
# Create runfiles from the files specified in the data attribute.
2020
# The shell executable - the output of this rule - can use them at
21-
# runtime. It is also possible to define data_runfiles and
22-
# default_runfiles. However if runfiles is specified it's not possible to
23-
# define the above ones since runfiles sets them both.
21+
# runtime.
2422
return [DefaultInfo(
2523
runfiles = ctx.runfiles(files = ctx.files.data),
2624
)]

rules/runfiles/lib.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cat rules/runfiles/data.txt

rules/runfiles/library.bzl

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Create a library with runfiles and a rule which uses it.
2+
3+
A library might need some external files during runtime, and every dependent
4+
binary should know about them. This demonstrates best practices for handling
5+
such a scenario.
6+
"""
7+
8+
# When possible, use custom providers to manage propagating information
9+
# between dependencies and their dependers.
10+
RuntimeRequiredFiles = provider(fields = ["file", "data_files"])
11+
12+
def _library_impl(ctx):
13+
# Expand the label in the command string to a runfiles-relative path.
14+
# The second arg is the list of labels that may be expanded.
15+
command = ctx.expand_location(ctx.attr.command, ctx.attr.data)
16+
17+
my_out = ctx.actions.declare_file(ctx.attr.name + "_out")
18+
ctx.actions.write(
19+
output = my_out,
20+
content = command,
21+
is_executable = True,
22+
)
23+
24+
return [
25+
RuntimeRequiredFiles(file = my_out, data_files = depset(ctx.files.data)),
26+
]
27+
28+
runfiles_library = rule(
29+
implementation = _library_impl,
30+
attrs = {
31+
"command": attr.string(),
32+
"data": attr.label_list(allow_files = True),
33+
},
34+
provides = [RuntimeRequiredFiles],
35+
)
36+
37+
def _binary_impl(ctx):
38+
# Create the output executable file, which simply runs the library's
39+
# primary output file (obtained from RuntimeRequiredFiles.file).
40+
ctx.actions.write(
41+
output = ctx.outputs.executable,
42+
content = "$(cat " + ctx.attr.lib[RuntimeRequiredFiles].file.short_path + ")",
43+
is_executable = True,
44+
)
45+
46+
my_runfiles = ctx.runfiles(
47+
files = [ctx.attr.lib[RuntimeRequiredFiles].file],
48+
transitive_files = ctx.attr.lib[RuntimeRequiredFiles].data_files)
49+
50+
return [DefaultInfo(
51+
runfiles = my_runfiles
52+
)]
53+
54+
runfiles_binary = rule(
55+
implementation = _binary_impl,
56+
executable = True,
57+
attrs = {
58+
"lib": attr.label(mandatory = True, providers = [RuntimeRequiredFiles]),
59+
},
60+
)

rules/runfiles/tool.bzl

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Create a tool with runfiles and a rule which uses it.
2+
3+
A rule may register an action that uses a tool which requires external files
4+
during runtime. This demonstrates best practices for handling such a scenario.
5+
"""
6+
7+
def _tool_impl(ctx):
8+
# Runfiles expansion for tools (executables that are to be run
9+
# as part of other build actions) is tricky and error-prone.
10+
11+
# There is a {rulename}.runfiles directory adjacent to the tool's
12+
# executable file which contains all runfiles. This is not guaranteed
13+
# to be relative to the directory in which the executable file is run.
14+
runfiles_path = "$0.runfiles/"
15+
16+
# Each runfile under the runfiles path resides under a directory with
17+
# with the same name as its workspace.
18+
data_file_root = runfiles_path + ctx.workspace_name + "/"
19+
20+
data_file_path = data_file_root + ctx.files._data[0].path
21+
22+
# Alternatively, one can use the root_symlinks parameter of `runfiles`
23+
# to create a symlink rooted directly under the {rulename}.runfiles
24+
# directory.
25+
my_runfiles = ctx.runfiles(files = [ctx.files._data[0]],
26+
root_symlinks = {"data_dep" : ctx.files._data[0]})
27+
28+
# Even root symlinks are under the runfiles path.
29+
data_dep_path = runfiles_path + "data_dep"
30+
31+
# Thus the example directory structure is:
32+
# runfiles/tool (executable)
33+
# runfiles/tool.runfiles/
34+
# data_dep (symlink to data.txt)
35+
# <workspace_name>/
36+
# runfiles/
37+
# udata.txt
38+
39+
# Create the output executable file with command as its content.
40+
ctx.actions.write(
41+
output = ctx.outputs.executable,
42+
# Simple example, effectively puts the contents of data.txt into
43+
# the output twice (read once via symlink, once via normal file).
44+
content = "#!/bin/bash\ncat %s %s > $1" % (data_file_path, data_dep_path),
45+
is_executable = True,
46+
)
47+
48+
return [DefaultInfo(
49+
# The tool itself should just declare `runfiles`. The build
50+
# system will automatically create a `files_to_run` object
51+
# from the result of this declaration (used later).
52+
runfiles = my_runfiles,
53+
)]
54+
55+
tool = rule(
56+
implementation = _tool_impl,
57+
executable = True,
58+
attrs = {
59+
"_data": attr.label(
60+
allow_files = True,
61+
default = "//runfiles:data.txt"),
62+
},
63+
)
64+
65+
def _tool_user_impl(ctx):
66+
my_out = ctx.actions.declare_file(ctx.attr.name + "_out")
67+
68+
# If the tool dependency attribute was declared with `executable = True`,
69+
# then the tool's file can be found under `ctx.executable.<attr_name>`.
70+
# If this file is passed to `ctx.actions.run()`, the runfiles for this file
71+
# are automatically added to the action.
72+
tool = ctx.executable.tool
73+
74+
ctx.actions.run(
75+
outputs = [my_out],
76+
executable = tool,
77+
arguments = [my_out.path]
78+
)
79+
80+
return [DefaultInfo(files = depset([my_out]))]
81+
82+
tool_user = rule(
83+
implementation = _tool_user_impl,
84+
attrs = {
85+
"tool": attr.label(mandatory = True, executable = True, cfg = "host"),
86+
},
87+
88+
)

0 commit comments

Comments
 (0)