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: