Generating pretty-printed sources with Bazel
Originally written as an answer for StackOverflow.
Introduction
Pretty-printers are excellent for enforcing style standards across the codebase. In this article, we’ll show how to use Bazel to generate pretty-printed sources in your build.
This method uses involves writing a new Bazel macro and rule. There is another method via aspects, but we are not covering that in this article.
For hermeticity reasons, Bazel does not modify your source files in place. If you want formatting-on-save (e.g. with gofmt
or prettier
), please use editor plugins instead.
As an example, let’s use the C++ tutorial from the Bazel C++ examples and clang-format
for pretty-printing.
Setup
Let’s first mess up the formatting of main/hello-world.cc
:
#include <ctime>
#include <string>
#include <iostream>
std::string get_greet(const std::string& who) { return "Hello " + who; }
void print_localtime() {
std::time_t result =
std::time(nullptr);
std::cout << std::asctime(std::localtime(&result));
}
int main(int argc, char** argv) {
std::string who = "world";
if (argc > 1) {who = argv[1];}
std::cout << get_greet(who) << std::endl;
print_localtime();
return 0;
}
And this is the BUILD file to build main/hello-world.cc
:
# In main/BUILD
cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
)
Macro: clang_formatted_cc_binary
Since cc_binary
doesn’t know anything about clang-format
or pretty-printing in general, let’s create a macro called clang_formatted_cc_binary
and replace cc_binary
with it. The BUILD file now looks like this:
# In main/BUILD
load("//:clang_format.bzl", "clang_formatted_cc_binary")
clang_formatted_cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
)
Next, create a file called clang_format.bzl
with a macro named clang_formatted_cc_binary
. The macro is currently just a wrapper around native.cc_binary
:
# In clang_format.bzl
def clang_formatted_cc_binary(**kwargs):
native.cc_binary(**kwargs)
At this point, you can build the cc_binary
target, but it’s not running clang-format
yet. Let’s add an intermediary rule to do that in clang_formatted_cc_binary
which we’ll call clang_format_srcs
:
# In clang_format.bzl
def clang_formatted_cc_binary(name, srcs, **kwargs):
# Using a filegroup for code cleaniness
native.filegroup(
name = name + "_unformatted_srcs",
srcs = srcs,
)
clang_format_srcs(
name = name + "_formatted_srcs",
srcs = [name + "_unformatted_srcs"],
)
native.cc_binary(
name = name,
srcs = [name + "_formatted_srcs"],
**kwargs
)
Note that we are compiling the cc_binary
’s the formatted sources, but retained the original name
attribute to allow for in-place replacements of cc_binary
-> clang_formatted_cc_binary
within BUILD files.
Rule: clang_format_srcs
Finally, we’ll write the implementation of the clang_format_srcs
rule, in the same clang_format.bzl
file:
# In clang_format.bzl
def _clang_format_srcs_impl(ctx):
formatted_files = []
for unformatted_file in ctx.files.srcs:
formatted_file = ctx.actions.declare_file("formatted_" + unformatted_file.basename)
formatted_files += [formatted_file]
ctx.actions.run_shell(
inputs = [unformatted_file],
outputs = [formatted_file],
progress_message = "Running clang-format on %s" % unformatted_file.short_path,
command = "clang-format %s > %s" % (unformatted_file.path, formatted_file.path),
)
return struct(files = depset(formatted_files))
clang_format_srcs = rule(
attrs = {
"srcs": attr.label_list(allow_files = True),
},
implementation = _clang_format_srcs_impl,
)
Here’s what this clang_format_srcs
rule is doing:
- Go through every source file in the target’s
srcs
attribute - For each source file, declare a output source file with the
formatted_
prefix - Run
clang-format
on the unformatted file to produce the formatted output.
Results
Now, by executing bazel build //main:hello-world
, Bazel runs the actions in clang_format_srcs
before running the cc_binary
compilation actions on the formatted files. We can prove this by running bazel build
with the --subcommands
flag:
$ bazel build //main:hello-world --subcommands
..
SUBCOMMAND: # //main:hello-world_formatted_srcs [action 'Running clang-format on main/hello-world.cc']
..
SUBCOMMAND: # //main:hello-world [action 'Compiling main/formatted_hello-world.cc']
..
SUBCOMMAND: # //main:hello-world [action 'Linking main/hello-world']
..
Looking at the contents of formatted_hello-world.cc
, looks like clang-format
did its job:
#include <ctime>
#include <string>
#include <iostream>
std::string get_greet(const std::string& who) { return "Hello " + who; }
void print_localtime() {
std::time_t result = std::time(nullptr);
std::cout << std::asctime(std::localtime(&result));
}
int main(int argc, char** argv) {
std::string who = "world";
if (argc > 1) {
who = argv[1];
}
std::cout << get_greet(who) << std::endl;
print_localtime();
return 0;
}
If all you want are the formatted sources without compiling them, you can run build the target with the _formatted_srcs
suffix from clang_format_srcs
directly:
$ bazel build //main:hello-world_formatted_srcs
INFO: Analysed target //main:hello-world_formatted_srcs (0 packages loaded).
INFO: Found 1 target...
Target //main:hello-world_formatted_srcs up-to-date:
bazel-bin/main/formatted_hello-world.cc
INFO: Elapsed time: 0.247s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action