Bazel Rules: Packaging

2022-01-24

Intro #

Packaging Bazel rules for re-use is not very straightforward, and I hope the addition of Bzlmod in Bazel 5 will help to improve the situation. For now, we typically package rules as a Git repo or an HTTP archive and then use workspace rules to load them into our current workspace. We will create a couple of repositories demonstrating this scenario in this example.

Packaging a repository #

The demo_repo folder provides an example of a packaged Bazel repository. It contains a simple rule that produces a text file:

demo_repo/
├── BUILD.bazel
├── rules.bzl
└── WORKSPACE.bazel

When dealing with multiple repositories, naming becomes essential, so the WORKSPACE.bazel file contains the name of the repo:

workspace(name = "demo_repo")

The rules.bzl file provides the demo_rule implementation:

def _demo_rule_impl(ctx):
    ctx.actions.write(
        output = ctx.outputs.out,
        content = "Hello, World!\n",
    )

demo_rule = rule(
    implementation = _demo_rule_impl,
    attrs = {
        "out": attr.output(mandatory = True),
    },
)

The BUILD.bazel file is empty as I don’t intend to build any targets within this repository. Still, Bazel requires this file to be present to indicate that the folder constitutes a package.

Using the packaged repository #

We have a second repository called user_repo which we will use to call the demo_rule:

user_repo/
├── BUILD.bazel
└── WORKSPACE.bazel

Let’s have a look at the WORKSPACE.bazel:

workspace(name = "user_repo")

local_repository(
    name = "demo_repo",
    path = "../demo_repo",
)

The important part here is the call to local_repository function that allows us to import an external repository from the local filesystem into our workspace. While local_repository is very convenient for testing and debugging rules, the actual users would expect to be able to use either the http_archive or the git_repository rule:

  load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
  load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")

  http_archive(
      name = "foo_rules",
      urls = ["http://example.com/foo_rules.zip"],
      sha256 = "...",
  )

  git_repository(
    name = "bar_rules",
    remote = "http://example.com/git/bar_rules.git",
    tag = "...",
)

We can use non-Bazel sources with new_local_repository, new_http_archive, or new_git_repository rules, and we can also implement a custom [repository rule], but that is out of scope for now.

The only interesting part about the BUILD.bazel file is that we refer to the repository we imported:

load("@demo_repo//:rules.bzl", "demo_rule")

demo_rule(
    name = "file",
    out = "hello.txt",
)

Building the example within the user_repo should now produce the file:

$ cd user_repo
$ build //:file
...
Target //:file up-to-date:
  bazel-bin/hello.txt
...
$ cat bazel-bin/hello.txt
Hello, World!

The complete code for this example is here.

bazel

How to Merge Git Repositories Bazel Rules: Decoupling Rules Interface