bazelizing, bazelization, to bazelize, bazelized: The act of converting some existing software artifact to be easily consumed by the Bazel build tool

This blog post is about bazelizing {fmt}. The public GitHub repository of {fmt} can be found here. In this blog post, I want to describe different approaches on how to use {fmt} with Bazel.

First approach: Inject a BUILD file

In your WORKSPACE.bazel file you can do something like this:

maybe(
    new_git_repository,
    name = "fmt",
    branch = "master",
    remote = "https://github.com/fmtlib/fmt",
    build_file = "//third_party:fmt.BUILD",
)

The corresponding fmt.BUILD file can look like this:

cc_library(
    name = "fmt",
    srcs = [
        #"src/fmt.cc", # No C++ module support
        "src/format.cc",
        "src/os.cc",
    ],
    hdrs = [
        "include/fmt/args.h",
        "include/fmt/chrono.h",
        "include/fmt/color.h",
        "include/fmt/compile.h",
        "include/fmt/core.h",
        "include/fmt/format.h",
        "include/fmt/format-inl.h",
        "include/fmt/locale.h",
        "include/fmt/os.h",
        "include/fmt/ostream.h",
        "include/fmt/printf.h",
        "include/fmt/ranges.h",
        "include/fmt/xchar.h",
    ],
    includes = [
        "include",
        "src",
    ],
    strip_include_prefix = "include",
    visibility = ["//visibility:public"],
)

Advantages:

  • fmt-8.01 does not have out-of-the-box support for Bazel. This way Bazel can make use of {fmt} without the need that {fmt} knows anything about Bazel
  • The original source code of fmt-8.0.1 needs not to be modified

Disadvantages:

  • Reinvent the wheel: Every Bazel project that wants to use {fmt} has to reinvent this fmt.BUILD file.
  • Missing knowledge: Maybe for some reason, it makes sense to define some special defines upfront, etc. It also takes some time and knowledge of {fmt} to set up such a BUILD file. What is the best practice to build this lib?
  • Maintenance costs: If different Bazel projects want to adapt to future versions of {fmt} every single project has to do this maintenance on its own. Maybe new files will be introduced.

Second approach: Bazelize {fmt}

Add a WORKSPACE.bazel and BUILD.bazel file to the {fmt} repository.

This way {fmt} gets bazelized and can be used in your Bazel builds.

Example

Create a WORKSPACE.bazel file with the following content:

load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")

# Fetch bazelized fmt
git_repository(
    name = "fmt",
    branch = "bazel-support", # A copy of master where BUILD.bazel, WORKSPACE.bazel, .bazelrc and .bazelversion are moved to root
    remote = "https://github.com/<user_or_organisation>/fmt", # replace <user_or_organisation> by a valid account
)

Create a BUILD.bazel file and add a dependency to {fmt} (with the content of fmt.BUILD as shown before).

This is my favorite approach. Therefore, I created a pull request to add a WORKSPACE.bazel and BUILD.bazel file to the official {fmt} repository. In favor of keeping the {fmt} project directory clean the {fmt} maintainers decided that those files should not be added to the project root directory. Nevertheless, the allowed me to add the Bazel build files to the support/bazel directory of the {fmt} repository.

Third approach: Using the {fmt} repository with Bazel

Even though the {fmt} repository does not contain a WORKSPACE.bazel file in its root directory, there is an easy approach to use the {fmt} repository with Bazel out of the box. This is demonstrated in the following example.

Add to your WORKSPACE.bazel file:

load("@bazel_tools//tools/build_defs/repo:git.bzl", "new_git_repository")

# Fetch all files from fmt including the BUILD file `support/bazel/BUILD.bazel`
new_git_repository(
    name = "fmt_workaround",
    branch = "master",
    remote = "https://github.com/fmtlib/fmt/",
    build_file_content = "# Empty build file on purpose"
)

# Now the BUILD file `support/bazel/BUILD.bazel` can be used:
new_git_repository(
    name = "fmt",
    branch = "master",
    remote = "https://github.com/fmtlib/fmt/",
    build_file = "@fmt_workaround//:support/bazel/BUILD.bazel"
)

Create a BUILD.bazel file and add a dependency to {fmt}:

cc_binary( # Build a binary
    name = "Demo", # Name of the binary
    srcs = ["main.cpp"], # List of files - we only have main.cpp
    deps = ["@fmt//:fmt"], # Depend on fmt
)

Make use of {fmt} in main.cpp:

#include "fmt/core.h"

int main() {
  fmt::print("The answer is {}.\n", 42);
}

The expected output of this example is The answer is 42.

Forth approach: Make use of patch_cmd

The attribute patch_cmds of the git_repository rule can be used to execute some random shell commands. This can be used to move files from a subdirectory to the root directory of the repository:

load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")

git_repository(
    name = "fmt",
    branch = "master",
    patch_cmds = [
        "mv support/bazel/.bazelrc .bazelrc",
        "mv support/bazel/.bazelversion .bazelversion",
        "mv support/bazel/BUILD.bazel BUILD.bazel",
        "mv support/bazel/WORKSPACE.bazel WORKSPACE.bazel",
    ],
    # Windows-related patch commands are only needed in the case MSYS2 is not installed
    patch_cmds_win = [
        "Move-Item -Path support/bazel/.bazelrc -Destination .bazelrc",
        "Move-Item -Path support/bazel/.bazelversion -Destination .bazelversion",
        "Move-Item -Path support/bazel/BUILD.bazel -Destination BUILD.bazel",
        "Move-Item -Path support/bazel/WORKSPACE.bazel -Destination WORKSPACE.bazel",
    ],
    remote = "https://github.com/fmtlib/fmt",
)

More details here.

Advantages:

  • {fmt} maintainers are happy with the given approach

Disadvantages:

  • local_repository repository rule can not be used to embed {fmt} in your project, since {fmt} needs to be patched first, which is not supported by this rule.

Fifth approach: Bzlmod

Bzlmod, is the new external dependency subsystem of Bazel.

You can create a MODULE.bazel file in your workspace directory with the following content:

bazel_dep(name = "fmt", version = "9.1.0")

Furthermore, you can create an empty WORKSPACE.bazel and an empty WORKSPACE.bzlmod file in your workspace directory.

Add also a main.cpp file

#include "fmt/core.h"

int main() {
  fmt::print("The answer is {}.\n", 42);
}

and a BUILD.bazel file like this:

cc_binary( # Build a binary
    name = "Demo", # Name of the binary
    srcs = ["main.cpp"], # List of files - we only have main.cpp
    deps = ["@fmt//:fmt"], # Depend on fmt
)

Overview of file tree:

.
├── .bazelversion (content "6.1.0")
├── BUILD.bazel
├── main.cpp
├── MODULE.bazel
├── WORKSPACE.bazel (empty)
└── WORKSPACE.bzlmod (empty)

Now you can build the project with Bazel:

bazel run --enable_bzlmod //:Demo

This will fetch {fmt} version 9.1.0 from the central Bazel registry and build the project.

Outlook

For a “single-header” library such as {fmt} this may look like shooting sparrows with cannons. Anyways, the patterns, tricks, and ideas shown here can be reused also for bazelizing other libraries.

Other libraries

I have written a few blog posts about Bazelizing different libs: