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 abzlformat_pkg
declaration.bzlformat_missing_pkgs_test
: Executable target that does the same query asbzlformat_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 addsbzlformat_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.