I have a Bazel rule that produces an output file, and can optionally take a dependency to a similar target, like so:
load(":node.bzl", "node")
node(
name = "one",
src = "one.txt",
)
node(
name = "two",
src = "two.txt",
dep = "one",
)
Now, if I call bazel build :two
, I want it to build :one first, and then :two, each producing an output file. One would think that'd be as simple as writing a node.bzl file like
def _node_impl(ctx):
out_file = ctx.actions.declare_file(ctx.attr.name)
print("ok running: out_file={} src={}".format(out_file.path, ctx.file.src.path))
ctx.actions.run_shell(
outputs = [out_file],
inputs = [ctx.file.src],
command = ("echo {} >{}".format(ctx.file.src.path, out_file.path)),
)
print("done running")
return [
DefaultInfo(
files = depset([out_file]),
),
]
node = rule(
implementation = _node_impl,
attrs = {
"src": attr.label(
mandatory = True,
allow_single_file = True,
),
"dep": attr.label(
),
},
)
but, alas, this will only create the "two" file, not the dependent "one":
INFO: Starting clean (this may take a while). Consider using --async if the clean takes more than several minutes.
bazel build :two
DEBUG: nok/node.bzl:3:12: ok running: out_file=bazel-out/k8-fastbuild/bin/one src=one.txt
DEBUG: nok/node.bzl:9:12: done running
DEBUG: nok/node.bzl:3:12: ok running: out_file=bazel-out/k8-fastbuild/bin/two src=two.txt
DEBUG: nok/node.bzl:9:12: done running
INFO: Analyzed target //:two (4 packages loaded, 9 targets configured).
INFO: Found 1 target...
Target //:two up-to-date:
bazel-bin/two
INFO: Elapsed time: 0.491s, Critical Path: 0.05s
INFO: 1 process: 1 linux-sandbox.
INFO: Build completed successfully, 2 total actions
Note that the debug output confirms that it sees the dependency when it builds the dependency graph, just when it comes to executing the dependent target, it decides to skip it.
Now, I've gotten this to work by adding a provider that adds a transitive dependency to the depset of output files, but shouldn't bazel do that automatically? Here's my solution:
NodeProv = provider(fields = ["out_file"])
def _node_impl(ctx):
out_file = ctx.actions.declare_file(ctx.attr.name)
print("ok running: out_file={} src={}".format(out_file.path, ctx.file.src.path))
ctx.actions.run_shell(
outputs = [out_file],
inputs = [ctx.file.src],
command = ("echo {} >{}".format(ctx.file.src.path, out_file.path)),
)
print("done running")
dep_file = [ctx.attr.dep[NodeProv].out_file] if ctx.attr.dep else []
return [
DefaultInfo(
files = depset([out_file], transitive = [depset(dep_file)]),
),
NodeProv(
out_file = out_file
),
]
node = rule(
implementation = _node_impl,
attrs = {
"src": attr.label(
mandatory = True,
allow_single_file = True,
),
"dep": attr.label(
providers = [(NodeProv)],
),
},
)
This does indeed build both targets:
INFO: Analyzed target //:two (4 packages loaded, 9 targets configured).
INFO: Found 1 target...
Target //:two up-to-date:
bazel-bin/one
bazel-bin/two
INFO: Elapsed time: 0.532s, Critical Path: 0.07s
INFO: 2 processes: 2 linux-sandbox.
INFO: Build completed successfully, 3 total actions
What does the collective hive mind think, is this the recommended way of solving this or is there better approach?