A Nondeterministic Build #
Our previous example
is not yet reusable. Let’s try adding another target
to the BUILD
file using the same demo_rule
(the code for this example is
here):
load(":rules.bzl", "demo_binary")
demo_binary(
name = "english",
message = "Hello, World!",
)
demo_binary(
name = "french",
message = "Bonjour monde!",
)
We will naturally get ‘Hello, World!’ when running the first script:
$ bazel run //nondeterministic:english
...
Target //nondeterministic:nondeterministic up-to-date:
bazel-bin/nondeterministic/hello
...
Hello, World!
The second script will work fine as well:
$ bazel run //nondeterministic:french
...
Target //nondeterministic:nondeterministic up-to-date:
bazel-bin/nondeterministic/hello
...
Bonjour monde!
Unfortunately, when running bazel-bin/nondeterministic/hello
the message will be different depending on the last build command.
Also, if we try to build both targets, Bazel will produce an error:
$ bazel build //nondeterministic:all
ERROR: file 'nondeterministic/hello' is generated by these conflicting actions:
Label: //nondeterministic:french, //nondeterministic:english
...
Using the same name for both targets makes our build nondeterministic, which means we cannot predict the outcome of the build based on its source alone. We need a way to differentiate the name.
Labeled Output #
The simplest fix is to include the target label into the output name:
...
def _demo_binary_impl(ctx):
out = ctx.actions.declare_file("{}/hello".format(ctx.label.name))
...
Let’s copy the code to a different folder (the code for this example is here) and build all targets now:
$ bazel build //labeled_output:all
...
INFO: Build completed successfully, 7 total actions
We can see the directory tree with each executable in a different folder:
$ exa -TF bazel-bin/labeled_output
bazel-bin/labeled_output/
├── english/
│ ├── hello*
...
└── french/
├── hello*
...
Most importantly, we can run either file now:
$ bazel-bin/labeled_output/english/hello
Hello, World!
$ bazel-bin/labeled_output/french/hello
Bonjour monde!
An important advantage of this approach is that it is easy to ensure that user will not get conflicting names.
Predeclared Output #
Another option (see the complete code
here)
is to pass the output name as a rule attribute from the BUILD
file:
demo_binary(
name = "english",
out = "hello",
message = "Hello, World!",
)
demo_binary(
name = "french",
out = "bonjour",
message = "Bonjour monde!",
)
Our users can now define the executable name, but will have to make sure that
the names are different. It is customary to use out
or outs
as the name
of the attribute and it is supported by a special attr
type:
demo_binary = rule(
implementation = _demo_binary_impl,
executable = True,
attrs = {
"message": attr.string(mandatory = True),
"out": attr.output(mandatory = True),
},
)
The value is passed via ctx.outputs
and as Bazel considers the output attributes
predeclared,
we don’t have to call declare_file
anymore:
def _demo_binary_impl(ctx):
out = ctx.outputs.out
ctx.actions.write(
output = out,
content = _TEMPLATE.format(ctx.attr.message),
)
return [DefaultInfo(
files = depset([out]),
executable = out,
)]
We can see that the resulting file structure is different for this approach:
$ exa -TF bazel-bin/predeclared_output
bazel-bin/predeclared_output/
├── bonjour*
...
├── hello*
...
Finally, it is possible and sometimes helpful to combine label names with predeclared outputs.