Bazel is a powerful tool that becomes a joy to use. Unfortunately this is only after suffering through a painful learning curve. For me I did a lot of hair pulling before beginning to grok how to work with Bazel effectively. The purpose of this post is to share some thoughts on getting started with Bazel as well as some useful introductory concepts. The first thing you will need to do is install Bazel on your machine. To do this you can visit the Bazel download page here: https://bazel.build/start.

To confirm your installation you can run: bazel --version

Next we will create a new directory and chage our current working directory to be the newly created directory.

mkdir hello-bazel && cd hello-bazel

The final step before we can build bazel targets is to define our WORKSPACE and root BUILD.bazel files.

touch WORKSPACE BUILD.bazel

This will create empty configuration files that Bazel will use.

Finally we can build all Bazel targets.

bazel build ...

The ... in this command is bazel shorthand for recursively match all targets. It is common to prefix this with a path to a parent directory to build subtrees of your project. Ex: bazel build a/b/c/...

If everything goes according to plan you should see some output like this:

bazel build ...
INFO: Analyzed 0 targets (1 packages loaded, 0 targets configured).
INFO: Found 0 targets...
INFO: Elapsed time: 0.511s, Critical Path: 0.01s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action

Congratulations! 🎉 you just ran your first successful Bazel build. Now let’s dig in a bit more.

List the files in your current working directory.

ls
ls
BUILD.bazel             bazel-hello-bazel
WORKSPACE               bazel-out
bazel-bin               bazel-testlogs

You will see the original BUILD.bazel and WORKSPACE files that we created above. Additionally there are the bazel- prefixed directories that Bazel will use for various purposes. For now you should just be aware that they exist and are managed by Bazel.

Example: Bazel Executable Rule

Next let’s build something a bit closer to useful. How about a simple bash executable to print “Hello World!”. For this we will write a Bazel executable rule with the target name “hello-world”.

First we will create a new file called index.bzl with the following content:

def _hello(ctx):
  executable = ctx.actions.declare_file("hello-world.sh")
  ctx.actions.write(
    executable,
    """
    #!/bin/bash
    echo Hello World!
    """,
  )
  return [DefaultInfo(executable=executable)]

hello = rule(
  executable=True,
  implementation=_hello,
)

In the above snippet we are defining an executable rule named hello with an implementation _hello. In the implementation we are declaring the output file hello-world.sh with the literal file content as shown.

Next we need to load the hello rule and define a target. Update the BUILD.bazel file with the following content:

load("index.bzl", "hello")
hello(name = "hello-world")

Now we are ready to execute.

bazel run :hello-world

You should see output like:

bazel run :hello-world
INFO: Analyzed target //:hello-world (4 packages loaded, 6 targets configured).
INFO: Found 1 target...
Target //:hello-world up-to-date:
  bazel-bin/hello-world.sh
INFO: Elapsed time: 0.583s, Critical Path: 0.04s
INFO: 4 processes: 4 internal.
INFO: Build completed successfully, 4 total actions
INFO: Build completed successfully, 4 total actions
Hello World!

Fantastic! ⭐️ I think you’re starting to get it. Now let’s take a quick look under the hood.

Your bazel-bin directory should now have a copy of hello-world.sh. As you might have expected you can execute this file directly.

bazel-bin/hello-world.sh
bazel-bin/hello-world.sh
Hello World!

Let’s continue on with a more practical example of building file archives

Example: Archive Bazel Rules

It is very common to deal with archives when working with build systems. The following code snippets will demonstrate how to create zip and tar archives using Bazel rules.

First we will create the archive.bzl rules file with the following content:

def _zip(ctx):
  zip_out = ctx.actions.declare_file("{name}.zip".format(name=ctx.label.name))
  ctx.actions.run_shell(
    outputs=[zip_out],
    inputs=ctx.files.srcs,
    arguments=[f.path for f in ctx.files.srcs],
    command="zip -r {out} $@".format(out=zip_out.path),
  )
  return [DefaultInfo(files = depset([zip_out]))]

zip = rule(
  implementation=_zip,
  attrs={
    "srcs": attr.label_list(
      allow_empty=False,
      allow_files=True,
      mandatory=True,
    )
  },
)

def _tar(ctx):
  tar_out = ctx.actions.declare_file("{name}.tar.xz".format(name=ctx.label.name))
  ctx.actions.run_shell(
    outputs=[tar_out],
    inputs=ctx.files.srcs,
    arguments=[f.path for f in ctx.files.srcs],
    command="tar cfJ {out} $@".format(out=tar_out.path),
  )
  return [DefaultInfo(files = depset([tar_out]))]

tar = rule(
  implementation=_tar,
  attrs={
    "srcs": attr.label_list(
      allow_empty=False,
      allow_files=True,
      mandatory=True,
    )
  },
)

In both implementations we simply execute the system binary for tar or zip passing in some default options and the specified source files as arguments. The important Bazel bits to take away here is that it is crucial to define the inputs and outputs for the run_shell action because only the files defined here will be available in the execution context. Consider that the file “.path” that is used above will not be the path to the file in your working directory but rather a Bazel build directory where the rule will be executed.

Now we can write targets to invoke the tar and zip archive rules. Update your BUILD.bazel with the following content:

load("archive.bzl", "zip", "tar")

zip(
    name = "zip_example",
    srcs = [
        ":BUILD.bazel",
        ":hello-world",
    ],
)

tar(
    name = "tar_example",
    srcs = [
        ":BUILD.bazel",
        ":hello-world",
    ],
)

In the above snippet we defined two Bazel targets zip_example and tar_example which will take as sources the Build.bazel file and the output file from the previously written hello-world rule.

Finally, we can build these targets with:

bazel build :zip_example
bazel build :tar_example

Hooray! 🥳 Everything worked just as expected the first time!

This will produce the output files bazel-bin/zip_example.zip and bazel-bin/tar_example.tar.xz. You can un-archive those files in the typical way to inspect the content.

More to come

I hope this short post is enough fo you to get started working with Bazel and to get the gears in your head turning as to how Bazel may be useful in your future projects. Stay tuned for more to come.