Skip to content

Commit 55bb581

Browse files
authored
[bazel] add more documentation around our Bazel build (#32597)
This PR adds more documentation for how Bazel is setup at Materialize. It's goal is to cover both building Rust crates with Bazel in general, and any parts of our setup specific to Materialize. [Rendered](https://github.com/ParkMyCar/materialize/blob/bazel/docs-improvement/doc/developer/bazel.md) ### Motivation Knowledge sharing and redundancy ### Checklist - [ ] This PR has adequate test coverage / QA involvement has been duly considered. ([trigger-ci for additional test/nightly runs](https://trigger-ci.dev.materialize.com/)) - [ ] This PR has an associated up-to-date [design doc](https://github.com/MaterializeInc/materialize/blob/main/doc/developer/design/README.md), is a design doc ([template](https://github.com/MaterializeInc/materialize/blob/main/doc/developer/design/00000000_template.md)), or is sufficiently small to not require a design. <!-- Reference the design in the description. --> - [ ] If this PR evolves [an existing `$T ⇔ Proto$T` mapping](https://github.com/MaterializeInc/materialize/blob/main/doc/developer/command-and-response-binary-encoding.md) (possibly in a backwards-incompatible way), then it is tagged with a `T-proto` label. - [ ] If this PR will require changes to cloud orchestration or tests, there is a companion cloud PR to account for those changes that is tagged with the release-blocker label ([example](https://github.com/MaterializeInc/cloud/pull/5021)). <!-- Ask in #team-cloud on Slack if you need help preparing the cloud PR. --> - [ ] If this PR includes major [user-facing behavior changes](https://github.com/MaterializeInc/materialize/blob/main/doc/developer/guide-changes.md#what-changes-require-a-release-note), I have pinged the relevant PM to schedule a changelog post.
1 parent 8240642 commit 55bb581

File tree

2 files changed

+156
-38
lines changed

2 files changed

+156
-38
lines changed

.bazelrc

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ build --nobuild_runfile_links
6565
# common --verbose_explanations
6666

6767
# Compress any artifacts larger than 2MiB with zstd.
68+
#
69+
# Note(parkmycar): These thresholds were chosen arbitrarily. You should feel
70+
# free to change them if you encounter issues.
6871
common --remote_cache_compression
6972
common --experimental_remote_cache_compression_threshold=2097152
7073
# Memoizes merkle tree calculations to improve the remote cache hit checking speed.
@@ -105,7 +108,9 @@ build:linux --linkopt="-Wl,-O2"
105108
build:linux --@rules_rust//:extra_rustc_flag="-Clink-arg=-Wl,-O2"
106109
build:linux --copt="-gz=zlib"
107110

108-
# Match the DWARF version used by Rust
111+
# Match the DWARF version used by Rust.
112+
#
113+
# Note(parkmycar): This might not be necessary but seemed nice to do.
109114
#
110115
# See: <https://doc.rust-lang.org/stable/unstable-book/compiler-flags/dwarf-version.html>
111116
build:linux --copt="-gdwarf-4"
@@ -141,7 +146,8 @@ build --@rules_rust//:extra_rustc_flag="-Csymbol-mangling-version=v0"
141146
# Cargo when incremental compilation is enabled.
142147
build --@rules_rust//:extra_rustc_flag="-Ccodegen-units=64"
143148
# Enabling pipelined builds allows dependent libraries to begin compiling with
144-
# just `.rmeta` instead of the full `.rlib`.
149+
# just `.rmeta` instead of the full `.rlib`. This is what Cargo does and offers
150+
# a significant speedup in end-to-end build times!
145151
build --@rules_rust//rust/settings:pipelined_compilation=True
146152

147153
# `cargo check` like config, still experimental!
@@ -248,19 +254,3 @@ build:hwasan --action_env=ASAN_OPTIONS=verbosity=1
248254
# HACK(parkmycar): We want to tell Rust to use the Bazel provided `clang++` but there isn't
249255
# a great way to do that. We know this is where `clang++` lives though, so it works.
250256
build:hwasan --@rules_rust//:extra_rustc_flag=-Clinker=external/llvm_toolchain_llvm/bin/clang++
251-
252-
# Cross Language LTO
253-
#
254-
# <https://blog.llvm.org/2019/09/closing-gap-cross-language-lto-between.html>
255-
#
256-
# TODO(parkmycar): Re-enable these. They eiter cause a compilation failure because of
257-
# missing object files, or (seemingly) makes the number of codegen units 1 which compiles
258-
# way too slow.
259-
#build:release --@rules_rust//:extra_rustc_flag="-Clinker-plugin-lto"
260-
#build:linux --@rules_rust//rust/settings:experimental_use_cc_common_link=True
261-
262-
# This 'features' option comes from the `unix_cc_toolchain_config` in Bazel core and sets all the
263-
# right flags.
264-
#
265-
# See: <https://github.com/bazelbuild/bazel/blob/master/tools/cpp/unix_cc_toolchain_config.bzl>
266-
#build:release --features=thin_lto

doc/developer/bazel.md

Lines changed: 148 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,18 @@ allows Bazel to aggressively cache artifacts, which reduces build times.
4242
* [How Bazel Works](#how-bazel-works)
4343
* [`WORKSPACE`, `BUILD.bazel`, `*.bzl` files](#workspace-buildbazel-bzl-files)
4444
* [Generating `BUILD.bazel` files](#generating-buildbazel-files)
45+
* [`cargo-gazelle`](#cargo-gazelle)
4546
* [Supported Configurations](#supported-configurations)
4647
* [Platforms](#platforms)
48+
* [Custom Build Flags](#custom-build-flags)
4749
* [Toolchains](#toolchains)
48-
* [`rules_rust`](#rules_rust)
50+
* [System Roots](#system-roots)
51+
* [Building Rust Code](#building-rust-code)
4952
* [`crates_repository`](#crates_repository)
53+
* [Rust `-sys` crates](#rust--sys-crates)
54+
* [Other C dependencies](#other-c-dependencies)
55+
* [Protobuf Generation](#protobuf-generation)
56+
* [Git Hash Versioning](#git-hash-versioning)
5057

5158
# Getting Started
5259

@@ -96,7 +103,8 @@ common --experimental_disk_cache_gc_max_age=7d
96103
## Remote caching
97104

98105
Bazel supports reading and writing artifacts to a [remote cache](https://bazel.build/remote/caching).
99-
We currently have two setup that are backed by S3 and running [`bazel-remote`](https://github.com/buchgr/bazel-remote).
106+
We currently have two setup in [`i2`](https://github.com/MaterializeInc/i2/blob/daacaca7e04e9c30e9914b5efa084047b3e58994/i2/apps/__init__.py#L126-L141)
107+
that are backed by S3 and running [`bazel-remote`](https://github.com/buchgr/bazel-remote).
100108
One is accessible by developers and used by PR builds in CI, we treat this as semi-poisoned. The
101109
other is only accessible by CI and used for builds from `main` and tagged builds.
102110

@@ -135,15 +143,16 @@ it for other tools as well like `mzimage` and `mzcompose`! To enable Bazel speci
135143
flag like you would specify the `--dev` flag, e.g. `bin/mzcompose --bazel ...`.
136144

137145
Otherwise Bazel can be used just like `cargo`, to build individual targets and run tests. We
138-
provide a thin wrapper around the `bazel` command in the form of `bin/bazel`. This sets up remote
139-
caching, and provides the `fmt` and `gen` subcommands. Otherwise it forwards all commands onto
140-
`bazel` itself.
146+
provide a thin wrapper around the `bazel` command in the form of [`bin/bazel`](../../misc/python/materialize/cli/bazel.py).
147+
This sets up remote caching, and provides the `fmt` and `gen` subcommands.
148+
Otherwise it forwards all commands onto `bazel` itself.
141149

142150
## Building a crate
143151

144-
All Rust crates have a `BUILD.bazel` file that define different build targets for the crate. You
145-
don't have to write these files, they are automatically generated from the crate's `Cargo.toml`.
146-
For more details see the [Generating `BUILD.bazel` files](#generating-buildbazel-files) section.
152+
All Rust crates in our Cargo Workspace have a `BUILD.bazel` file that define different build
153+
targets for the crate. You don't have to write these files, they are automatically generated from
154+
the crate's `Cargo.toml`. For more details see the [Generating `BUILD.bazel` files](#generating-buildbazel-files)
155+
section.
147156

148157
> **tl;dr** to build a crate run `bin/bazel build //src/<crate-name>` from the root of the repo.
149158
@@ -264,7 +273,7 @@ There are three kinds of files in our Bazel setup:
264273
> **tl;dr** run `bin/bazel gen` from the root of the repository.
265274
266275
Just like `Cargo.toml`, associated with every crate is a `BUILD.bazel` file that provides targets that
267-
Bazel can build. We auto-generate these files with a [`cargo-gazelle`](../bazel/cargo-gazelle/) which
276+
Bazel can build. We auto-generate these files with [`cargo-gazelle`](../../misc/bazel/cargo-gazelle) which
268277
developers can easily run via `bin/bazel gen`.
269278

270279
There are times though when `Cargo.toml` doesn't provide all of the information required to build a
@@ -281,8 +290,22 @@ compile_data = ["path/to/my/file.txt"]
281290
This will add `"path/to/my/file.txt"` to the `compile_data` attribute on the
282291
resulting [`rust_library`](http://bazelbuild.github.io/rules_rust/defs.html#rust_library) Bazel target.
283292

293+
### `cargo-gazelle`
294+
295+
[`gazelle`](https://github.com/bazel-contrib/bazel-gazelle) is a semi-official `BUILD.bazel` file
296+
generator that supports Golang and protobuf. There exists a [`gazelle_rust`](https://github.com/Calsign/gazelle_rust)
297+
plugin, but it's not yet mature enough to fit our needs. Still, it's important for producivity that
298+
developers who don't want to interact with Bazel shouldn't have to, so generating a `BUILD.bazel`
299+
file from a `Cargo.toml` is quite important.
300+
301+
Thus we decided to write our own generator, `cargo-gazelle`! It's not a plugin for the existing
302+
`gazelle` tool but theoretically could be. It's designed to be fully generic with very few (if any)
303+
Materialize specific configurations built in.
304+
284305
### Supported Configurations
285306

307+
`cargo-gazelle` supports the following configuration in a `Cargo.toml` file.
308+
286309
```toml
287310
# Configuration for the crate as a whole.
288311
[package.metadata.cargo-gazelle]
@@ -403,12 +426,29 @@ serve:
403426
this is always the same as the "Host platform" since we don't utilize distributed builds.
404427
3. Target: the platform we are building for.
405428

406-
The platforms that we build for are defined in [/platforms/BUILD.bazel](../bazel/platforms/BUILD.bazel).
429+
The platforms that we build for are defined in [`/platforms/BUILD.bazel`](../../misc/bazel/platforms/BUILD.bazel).
407430

408431
A common way to configure a build based on platform is to use the
409432
[`select`](https://bazel.build/reference/be/functions#select) function. This allows you to return
410433
different values depending on the platform we're targetting.
411434

435+
### Custom Build Flags
436+
437+
Not necessarily related to platforms, but still defined in
438+
[`/platforms/BUILD.bazel`](../../misc/bazel/platforms/BUILD.bazel) are our custom build flags.
439+
Currently we have custom build settings for the following features:
440+
441+
1. Sanitizers like AddressSanitizer (ASan).
442+
2. Cross language LTO
443+
444+
While most build settings can get defined in the `.bazelrc` these features require slightly more
445+
complex configuration. For example, if we're building with a sanitizer we need to disable
446+
`jemalloc`, this is because sanitizers commonly have their own allocator. To do this we create a
447+
new build flag with the [`string_flag`](https://github.com/bazelbuild/bazel-skylib/blob/454b25912a8ddf3d90eb47f25260befd5ee274a8/docs/common_settings_doc.md)
448+
rule from the Bazel Skylib rule set and match on this using the [`config_setting`](https://bazel.build/docs/configurable-attributes)
449+
rule that is built in to Bazel. The [`config_setting`] is then what we can match on in our
450+
`BUILD.bazel` files with a `select({ ... })` function.
451+
412452
## Toolchains
413453

414454
[Official Documentation](https://bazel.build/extending/toolchains)
@@ -418,32 +458,120 @@ specify a Rust toolchain every time you use the `rust_library` rule, you instead
418458
Rust toolchain that rules resolve during analysis.
419459

420460
Toolchains are defined and registered in the [`WORKSPACE`](/WORKSPACE) file. We currently use
421-
Clang/LLVM to build C/C++ code, the version is defined by `LLVM_VERSION`, and we support both
422-
stable and nightly Rust, the versions defined by `RUST_VERSION` and `RUST_NIGHTLY_VERSION`
461+
Clang/LLVM to build C/C++ code (via the [`toolchains_llvm`](https://github.com/bazel-contrib/toolchains_llvm) ruleset)
462+
where the version is defined by the `LLVM_VERSION` constant. For Rust we support both stable and
463+
nightly, where the versions defined by the `RUST_VERSION` and `RUST_NIGHTLY_VERSION` constants
423464
respectively.
424465

466+
Both [`toolchains_llvm`](https://github.com/bazel-contrib/toolchains_llvm/blob/0d3594c3edbe216e4734d52aa1e305049f877ea5/toolchain/osx_cc_wrapper.sh.tpl)
467+
and [`rules_rust`](https://github.com/bazelbuild/rules_rust/tree/e38fa8c2bc0990debceaf28daa4fcb2c57dcdc1c/util/process_wrapper) have "process wrappers".
468+
These are small wrappers around `clang` and `rustc` that are able to inspect the absolute path they
469+
are being invoked from. Bazel does not expose absolute paths _at all_ so these wrappers are how
470+
arguments like [`--remap-path-prefix`](https://doc.rust-lang.org/stable/rustc/command-line-arguments.html#--remap-path-prefix-remap-source-names-in-output)
471+
get set. These wrappers are helpful but can also cause issues like [`toolchains_llvm#421`](https://github.com/bazel-contrib/toolchains_llvm/issues/421).
472+
425473
The upstream [LLVM toolchains](https://github.com/llvm/llvm-project/releases) are very large and
426-
built for bespoke CPU architectures. As such we build our own, see the
427-
[MaterializeInc/toolchains](https://github.com/MaterializeInc/toolchains) repo for more details.
474+
built for bespoke CPU architectures. While maybe not ideal, we build our own LLVM toolchains which
475+
live in the [MaterializeInc/toolchains](https://github.com/MaterializeInc/toolchains) repo. This
476+
ensures we're using the same version of `clang` across all architectures we support and greatly
477+
improves the speed of cold builds.
478+
479+
> Note: The upstream LLVM toolchains are ~1 GiB and compressed with gzip, end-to-end they took
480+
about 3 minutes to download and setup. Our toolchains are ~80MiB and compressed with zstd which
481+
end-to-end take less than 30 seconds to download and setup.
482+
483+
### System Roots
484+
485+
Along with a C-toolchain we also provide a system root for our builds. A system root contains
486+
things like `libc`, `libm`, and `libpthread`, as well as their associated header files. Our system
487+
roots also live in the [MaterializeInc/toolchains](https://github.com/MaterializeInc/toolchains/blob/22e21deac9cec196c6adfcca811722882f92f941/sysroot/Dockerfile) repo.
428488

429-
# [`rules_rust`](https://github.com/bazelbuild/rules_rust)
489+
# Building Rust Code
430490

431-
For building Rust code we use `rules_rust`. It's primary component is the
432-
[`crates_repository`](http://bazelbuild.github.io/rules_rust/crate_universe.html#crates_repository) rule.
491+
For building Rust code we use [`rules_rust`](https://github.com/bazelbuild/rules_rust). It's
492+
primary component is the [`crates_repository`](http://bazelbuild.github.io/rules_rust/crate_universe.html#crates_repository)
493+
rule.
433494

434495
## `crates_repository`
435496

436497
Normally when building a Rust library you define external dependencies in a `Cargo.toml`, and
437-
`cargo` handles fetching the relevant crates, generally from `crates.io`. The [`crates_repository`]
498+
`cargo` handles fetching the relevant crates, generally from `crates.io`. The [`crates_repository`](https://bazelbuild.github.io/rules_rust/crate_universe_workspace.html#crates_repository)
438499
rule does the same thing, we define a set of manifests (`Cargo.toml` files), it will analyze them
439500
and create a Bazel [repository](https://bazel.build/external/overview#repository) containing all of
440501
the necessary external dependencies.
441502

442-
Then to build our crates, e.g. [`mz-adapter`](/src/adapter/), we use the handy
503+
Then to build our crates, e.g. [`mz-adapter`](../../src/adapter/), we use the handy
443504
[`all_crate_deps`](http://bazelbuild.github.io/rules_rust/crate_universe.html#all_crate_deps)
444505
macro. When using this macro in a `BUILD.bazel` file, it determines which package we're in (e.g.
445506
`mz-adapter`) and expands to all of the necessary external dependencies. Unfortunately it does not
446507
include dependencies from within our own workspace, so we still need to do a bit of manual work
447508
of specifying dependencies when writing our `BUILD.bazel` files.
448509

449-
In the [`WORKSPACE`](/WORKSPACE) file we define a "root" `crates_repository` named `crates_io`.
510+
In the [`WORKSPACE`](../../WORKSPACE) file we define a "root" `crates_repository` named `crates_io`.
511+
512+
## Rust `-sys` crates
513+
514+
There are some Rust crates that are wrappers around C libraries, like
515+
[`decnumber-sys`](https://crates.io/crates/decnumber-sys) is a wrapper around
516+
[`libdecnumber`](https://speleotrove.com/decimal/). `cargo-gazelle` will generate a Bazel target
517+
for the crate's build script, but it's likely this build script will fail because it can't find
518+
tools like `cmake`, our system root, or implicitly depends on some other C library.
519+
520+
The general approach we've used to get these crates to build is to duplicate the logic from the
521+
`-sys` crate's `build.rs` script into a Bazel target. See
522+
[bazel/c_deps/rust-sys](../../misc/bazel/c_deps/rust-sys) for some examples. Once you write a
523+
`BUILD.bazel` file for the C dependency we add a `crate.annotation` in our [`WORKSPACE`](https://github.com/MaterializeInc/materialize/blob/5f2f45a162c44e4c6a03ba017f8b7d1d00c3775b/WORKSPACE#L464-L469)
524+
file that appends your newly written `BUILD.bazel` file to the one generated for the Rust crate.
525+
526+
Duplicating logic is never great, but having Bazel explicitly build these C dependencies provides
527+
better caching and more control over the process which unlocks features like cross language LTO.
528+
529+
### Other C dependencies
530+
531+
There are a few C dependencies which are used both by a Rust `-sys` crate and another C dependency.
532+
For example `zstd` is used by both the `zstd-sys` Rust crate and the `rocksdb` C library. For these
533+
cases instead of depending on the version included via the Rust `-sys` crate, we "manually" include
534+
them by downloading the source files as an [`http_archive`](https://bazel.build/rules/lib/repo/http).
535+
All cases of external C dependencies live in [`bazel/c_deps/repositories.bzl`](../../misc/bazel/c_deps/repositories.bzl).
536+
537+
## Protobuf Generation
538+
539+
Nearly all of our Rust build scripts do a single thing, and that's generate Rust bindings to
540+
protobuf definitions. `rules_rust` includes [rules for generating protobuf bindings](https://bazelbuild.github.io/rules_rust/rust_prost.html)
541+
when using Prost and Tonic, but they don't interact with Cargo Build Scripts very well. Instead we
542+
added a new crate called [`build-tools`](../../src/build-tools) whose purpose is to abstract over
543+
whatever build system you're using and provide the tool a build script might need, like `protoc`.
544+
545+
For Bazel we provide the necessary tools via ["runfiles"](https://bazel.build/rules/lib/builtins/runfiles), which are defined in the [`data` field](https://github.com/MaterializeInc/materialize/blob/5f2f45a162c44e4c6a03ba017f8b7d1d00c3775b/src/build-tools/BUILD.bazel#L30-L33)
546+
of the `rust_library` target. Bazel "runfiles" are a set of files that are provided at runtime
547+
execution. So in your build script to get the current path of the `protoc` executable you would
548+
call `mz_build_tools::protoc` ([example](https://github.com/MaterializeInc/materialize/blob/5f2f45a162c44e4c6a03ba017f8b7d1d00c3775b/src/persist/build.rs#L14))
549+
which returns a [different path depending on the build system](https://github.com/MaterializeInc/materialize/blob/5f2f45a162c44e4c6a03ba017f8b7d1d00c3775b/src/build-tools/src/lib.rs#L38-L64)
550+
currently being used.
551+
552+
## Git Hash Versioning
553+
554+
Development builds of Materialize include the current git hash in their version number. The sandbox
555+
that Bazel creates when building a Rust library does not include any git info, so attempts to get
556+
the current hash will fail.
557+
558+
But! Bazel has a concept of "stamping" builds which allows you to provide local system information
559+
as part of the build process, this information is known as the [workspace status](https://bazel.build/docs/user-manual#workspace-status).
560+
Generating the workspace status and providing it to Rust libraries requires a few steps, all of
561+
which are described in the [`bazel/build-info/BUILD.bazel`](https://github.com/MaterializeInc/materialize/blob/5f2f45a162c44e4c6a03ba017f8b7d1d00c3775b/misc/bazel/build-info/BUILD.bazel#L12-L29) file.
562+
563+
Unfortunately this isn't the whole story though. It turns out workspace status and stamping builds
564+
causes poor remote cache performance. On a new build Bazel will regenerate the `volatile-status.txt`
565+
file used in workspace stamping which causes any stamped libraries to not be fetched from the
566+
remote cache, see [`bazelbuild#10075`](https://github.com/bazelbuild/bazel/issues/10075). For us
567+
this caused a pretty serious regression in build times so we came up with a workaround:
568+
569+
1. When [building in release mode but not a tagged build](https://github.com/MaterializeInc/materialize/blob/e427108049dd97f55b8b6d634ae01b025b8520b5/misc/python/materialize/mzbuild.py#L156-L163),
570+
(e.g. a PR) `mzbuild.py` will [write out the current git hash to a temporary file](https://github.com/MaterializeInc/materialize/blob/e427108049dd97f55b8b6d634ae01b025b8520b5/misc/python/materialize/bazel.py#L39-L64).
571+
2. Our `build-info` Rust crate knows to [read from this temporary file](https://github.com/MaterializeInc/materialize/blob/main/src/build-info/src/lib.rs#L123-L141)
572+
in a non-hermetic/side-channel way to get the git hash into the current build without
573+
invalidating the remote cache.
574+
575+
While definitely hacky, our side-channel for the git hash does provide a substantial improvement in
576+
build times, while providing similar guarantees to the Cargo build with respect to when the hash
577+
gets re-computed.

0 commit comments

Comments
 (0)