Bazel Rules: Output

2021-12-27 • edited 2021-12-30

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.

bazel

Bazel Rules: Attributes Bazel Rules: Source