Posted on August 11, 2018

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:

  1. Go through every source file in the target’s srcs attribute
  2. For each source file, declare a output source file with the formatted_ prefix
  3. 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