How to Format Bazel Starlark Files Using bzlformat

This article introduces bzlformat, a set of Bazel rules and macros that format Bazel Starlark source files using Buildifier, test that the formatted files exist in the workspace directory, and copy the formatted files to the workspace directory.

Table of Contents

The Problem

I am a big fan of Bazel. I am also a big fan of automatic code formatters and linters. So, whenever I begin a project, I start with a WORKSPACE file and a BUILD.bazel file. I use Buildifier to format my Starlark files keeping everything nice and tidy. While running Buildifier from my favorite text editor (vim using ALE) works great, it is not a bullet-proof solution. Updates from other sources (e.g., automated scripts, other contributors who may not have Buildifier installed) leave open the possibility of poorly formatted code merging to the main branch.

Folks have solved this problem in several ways. The most common is to add steps to their continuous integration (CI) process that run the code formatters and linters across the source tree. Anything that does not pass muster is automatically updated, or the original merge fails. If the merge fails, the author fixes the code and resubmits the changes. Checking for poorly formatted code in one’s CI works just fine. First, anything except the most trivial project should have a build-test PR merge check. Second, the author should ensure that the submitted code passes all required checks.

The biggest fault in this workflow is that the feedback to the author is pretty late in the cycle. Wouldn’t it be great to get this feedback when building and testing their code before submitting it to the CI system? I think it would. That is why I have written several Bazel rulesets (e.g., updatesrc, rules_swiftformat) to shorten this feedback loop by integrating it into the normal build-test development cycle. With this article, I would like to introduce bzlformat, a Bazel ruleset that formats, tests, and updates Starlark files.

Implementing bzlformat for your Bazel project

The following provides a quick introduction on how to configure and use bzlformat. For more information, check out the how-to quickstart, the API documentation, and the examples.

1. Configure your workspace.

Add the following to your WORKSPACE file to download and configure cgrindel/bazel-starlib, the repository that hosts bzlformat.

NOTE: While there are other rulesets in this repository, the focus of this article will be on using bzlformat and updatesrc.

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

http_archive(
    name = "cgrindel_bazel_starlib",
    sha256 = "fc2ee0fce914e3aee1a6af460d4ba1eed9d82e8125294d14e7d3f236d4a10a5d",
    strip_prefix = "bazel-starlib-0.3.2",
    urls = [
        "http://github.com/cgrindel/bazel-starlib/archive/v0.3.2.tar.gz",
    ],
)

load("@cgrindel_bazel_starlib//:deps.bzl", "bazel_starlib_dependencies")

bazel_starlib_dependencies()

load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")

bazel_skylib_workspace()

Next, add the following to download and configure a prebuilt version of Buildifier using keith/buildifier-prebuilt.

load("@buildifier_prebuilt//:deps.bzl", "buildifier_prebuilt_deps")

buildifier_prebuilt_deps()

load("@buildifier_prebuilt//:defs.bzl", "buildifier_prebuilt_register_toolchains", "buildtools_assets")

buildifier_prebuilt_register_toolchains()

2. Update the build file at your workspace root.

At the root of your workspace, create a Bazel build file, if you don’t have one. Add the following:

load(
    "@cgrindel_bazel_starlib//bzlformat:defs.bzl",
    "bzlformat_missing_pkgs",
    "bzlformat_pkg",
)
load(
    "@cgrindel_bazel_starlib//updatesrc:defs.bzl",
    "updatesrc_update_all",
)

# Ensures that the Starlark files in this package are formatted properly.
bzlformat_pkg(
    name = "bzlformat",
)

# Provides targets to find, test, and fix any Bazel packages that are missing bzlformat_pkg
# declarations.
#
# bzlformat_missing_pkgs_find: Find and report any Bazel packages missing the bzlformat_pkg
#                              declaration.
# bzlformat_missing_pkgs_test: Like find except it fails if any missing packages are found. This is
#                              useful to run in CI tests to ensure that all is well.
# bzlformat_missing_pkgs_fix: Adds bzlformat_pkg declarations to any packages that are missing
#                             the declaration.
bzlformat_missing_pkgs(
    name = "bzlformat_missing_pkgs",
)

# Define a runnable target to execute all of the updatesrc_update targets
# that are defined in your workspace.
updatesrc_update_all(
    name = "update_all",
    targets_to_run = [
        # Fix the Bazel packages when we update our source files from build outputs.
        ":bzlformat_missing_pkgs_fix",
    ],
)

The bzlformat_pkg macro defines targets for a Bazel package that will format the Starlark source files, test that the formatted files are in the workspace directory, and copies the formatted files to the workspace directory.

The bzlformat_missing_pkgs macro defines executable targets that find, test, and fix Bazel packages missing a bzlformat_pkg declaration.

The updatesrc_update_all macro defines a runnable target that copies all of the formatted Starlark source files to the workspace directory. We add a reference to the :bzlformat_missing_pkgs_fix target to fix the appropriate Bazel packages when bazel run //:update_all executes.

3. Add bzlformat_pkg to every Bazel package.

Next, we need to add bzlformat_pkg declarations to every Bazel package. The quickest way to do so is to execute bazel run //:bzlformat_missing_pkgs_fix or bazel run //:update_all.

# Update the world, including any bzlformat_pkg fixes
$ bazel run //:update_all

4. Format, Update, and Test

From the command line, you can format the Starlark source files, copy them back to the workspace directory and execute the tests that ensure the formatted sources are in the workspace directory.

# Format the Starlark source files and copy the formatted files back to the workspace directory
$ bazel run //:update_all

# Execute all of your tests, including the formatting checks
$ bazel test //...

5. (Optional) Update Your CI Test Runs

To ensure that all of your Bazel packages are monitored by bzlformat, add a call to bazel run //:bzlformat_missing_pkgs_test to your CI test runs. If any Bazel packages are missing bzlformat_pkg declarations, this executable target will fail (i.e., exit with a non-zero value).

# Add this to your CI test runs.
$ bazel run //:bzlformat_missing_pkgs_test

If it does fail, run bazel run //:bzlformat_missing_pkgs_fix and execute your tests (bazel test //...).

How It Works

The workhorse for the bzlformat ruleset is bzlformat_pkg.

  • The bzlformat_pkg macro defines a bzlformat_format declaration for each Starlark file (i.e., *.bzl, BUILD, BUILD.bazel) in the Bazel package. Each of these targets is a build-time action that formats the source file and outputs the file to the appropriate Bazel output directory.
  • The bzlformat_pkg macro defines a diff_test target for each Starlark file comparing the source file with the formatted file. If the files differ in any way, the test fails.
  • The bzlformat_pkg macro defines a single updatesrc_update executable target. When executed, it copies all of the formatted Starlark files in the Bazel package to the source directory.

The updatesrc_update_all target executes a Bazel query looking for all of the updatesrc_update targets in your workspace, then runs each one. It will also execute any executable targets listed in the targets_to_run attribute.

The bzlformat_missing_pkgs macro defines three targets:

  • bzlformat_missing_pkgs_find: Executable target that reports any Bazel packages missing a bzlformat_pkg declaration.
  • bzlformat_missing_pkgs_test: Executable target that does the same query as bzlformat_missing_pkgs_find failing if any missing packages are detected. This target is typically run as a step in a CI workflow.
  • bzlformat_missing_pkgs_fix: Executable target that adds bzlformat_pkg declarations to any Bazel packages missing one.

Conclusion

If you have gotten this far, I hope I have piqued your interest in bzlformat. If you have any questions or run into any issues, don’t hesitate to submit an issue.