Building with Bazel¶
Bazel is the build system for SIPI. It owns the entire build graph — every C/C++ target (sipi binary, foreign_cc ext libraries, unit + approval tests), the OCI Docker image, and the Rust e2e + smoke test binaries.
Nix is no longer the build orchestrator — it provisions the
dev shell (bazelisk, host tools needed by rules_foreign_cc, gh,
crane, just, etc.) and nothing else.
Mental model¶
A few concepts that make the rest of this page click:
MODULE.bazel is the project's manifest. It declares the Bazel
modules sipi depends on (rules_foreign_cc, rules_oci,
toolchains_llvm, rules_rust, …) and pins every third-party C/C++
source archive via http_archive. Version bumps live here.
BUILD.bazel files describe the target graph. Each first-party
package — //src, //shttps, //test/unit/<mod>, //ext/<lib>,
//fuzz/handlers, //tools/fuzz, //bazel/... — has its own
BUILD.bazel that declares its cc_library/cc_binary/cc_test/
oci_image/rust_test targets and visibility rules.
Hermetic toolchain. toolchains_llvm registers a pinned LLVM 19
toolchain that every cc action runs under. The host compiler version
is irrelevant — bazel build //src/cli:sipi produces the same binary on
macOS, linux-x86_64, and linux-aarch64 (modulo platform-specific
codegen).
Stamping. tools/workspace_status.sh reads version.txt and
emits STABLE_* keys (STABLE_SIPI_VERSION, STABLE_GIT_COMMIT,
…). expand_template substitutes them into include/SipiVersion.h.in
so sipi --version reports the right string. Keys are also consumed
by the oci_image rule to stamp Docker labels and tags.
Caching. Bazel keeps three caches:
| Cache | What it holds | Where |
|---|---|---|
| Action cache | Hashed action outputs (compile, link, test) | bazel info output_base |
| Repository cache | Downloaded http_archive source tarballs |
~/.cache/bazel/_bazel_<user>/cache/repos/v1 |
| Disk cache | Action cache mirrored to a stable path (CI) | ~/.cache/bazel-disk (CI only) |
bazel build //src/cli:sipi after a single-file edit re-runs only the
affected compile + link via the action cache — typically sub-second
through link.
Quick start¶
nix develop # bazelisk + host tools on PATH
just bazel-build # bazel build --stamp //src/cli:sipi
./bazel-bin/src/cli/sipi server --config config/sipi.localdev-config.lua
# Subsequent edits:
just bazel-build # incremental, sub-second through link
just run chains the two: it depends on bazel-build and starts
sipi with the localdev config in one step.
Common commands¶
Every CI step invokes one of these recipes — there are no inline
bazel ... invocations in workflows.
# Build sipi (fastbuild — fast incremental for inner-loop edits)
just bazel-build # bazel build --stamp //src/cli:sipi
just bazel-build -c opt # production-shape build (matches Docker image)
just bazel-build --config=asan # ASan+UBSan; same flag form for ad-hoc variants
# Tests
just bazel-test-unit # bazel test //test/unit/... (12 components)
just bazel-test-approval # bazel test //test/approval:approvaltests
just bazel-test-e2e # Rust e2e tests via rules_rust
just bazel-test-smoke # Docker smoke test (OCI tarball loaded by the test)
# Coverage (canonical CI build — what ci.yml invokes on every PR)
just bazel-coverage # unit + approval + e2e under instrumentation;
# lcov at bazel-out/_coverage/_coverage_report.dat
# Sanitizer + fuzz
just bazel-build-sanitized # bazel build --config=asan --config=ubsan //src/cli:sipi
just bazel-build-fuzz # libFuzzer harness (linux-x86_64 in CI, darwin-aarch64 local)
just bazel-run-fuzz <corpus> <duration> [seed]
# Docker (rules_oci)
just bazel-docker-build-amd64 # build + load amd64 image as daschswiss/sipi:latest
just bazel-docker-build-arm64 # arm64 equivalent
just bazel-docker-publish-manifest # crane index append → daschswiss/sipi:v<version>
just bazel-docker-extract-debug <arch> # produce sipi-<arch>.debug for sentry-cli
--config= flags¶
Defined in .bazelrc. Each flag composes with bazel build /
bazel test to switch the build configuration.
| Flag | Effect |
|---|---|
-c opt |
-O3 -DNDEBUG. Production shape; matches the Docker image. |
-c dbg |
-O0 -g. Full debug symbols; what bazel-build-sanitized consumes. |
--config=asan |
AddressSanitizer + DWARF inline; consults .lsan_suppressions.txt at e2e time. |
--config=ubsan |
UndefinedBehaviorSanitizer. Composes with --config=asan. |
--config=fuzz |
Selects the libstdc++ LLVM toolchain for libFuzzer ABI parity (linux-x86_64) or the default libc++ toolchain (darwin-aarch64). |
Querying the build graph¶
bazel query answers structural questions about the target graph
without running an action:
# What does //src/cli:sipi depend on?
bazel query 'deps(//src/cli:sipi)' --output=label_kind | head -20
# Which targets transitively depend on //shttps:shttps?
bazel query 'rdeps(//..., //shttps:shttps)' --output=label
# Which BUILD files declare cc_test targets?
bazel query 'kind("cc_test", //test/unit/...)' --output=label
# What sources does //test/unit/cache:cache_test compile?
bazel query 'attr("srcs", "", //test/unit/cache:cache_test)' --output=build
bazel cquery is the configured-graph variant: it accounts for
select() and platform-specific deps, useful when querying targets
that vary by config (//src:image is target_compatible_with-gated
to Linux, etc.).
Cache hygiene¶
CI cache strategy is documented in .github/workflows/ci.yml (see
the long "CACHE STRATEGY" comment block):
- Disk cache managed by
actions/cache@v5(notsetup-bazel's built-in disk-cache wiring) so an analysis-phase failure cannot 0-byte-poison the cache. - Targeted key formula on inputs that actually affect foreign_cc
action keys:
MODULE.bazel{,.lock},ext/**/BUILD.bazel,bazel/**,patches/**,.bazelrc,.bazelversion,flake.lock. App/test sources are deliberately excluded. - Repository cache off in CI (would persist ~4 GB per arch and evict the real disk-cache speedup).
Locally, bazel clean flushes per-action outputs while preserving
downloaded archives; bazel clean --expunge flushes everything
including the repository cache.
Kakadu¶
The proprietary Kakadu SDK is fetched at build time by a custom
kakadu_archive repository_rule (bazel/kakadu.bzl) that shells out
to gh release download against dasch-swiss/dsp-ci-assets. Auth
flows through GH_TOKEN; locally gh auth login once is enough, in
CI the workflow injects GH_TOKEN: ${{ secrets.DASCHBOT_PAT }} on
every Bazel-invoking step.
See Kakadu setup for the full version-bump procedure.
Cross-platform¶
Linux-only targets (//src:image, //src:image_load,
//src:image_push_*, //src:sipi_debug_layout) are gated by
target_compatible_with = ["@platforms//os:linux"] and skipped on
macOS hosts. The fuzz harness is supported on linux-x86_64 (CI) and
darwin-aarch64 (local dev) — just bazel-build-fuzz selects the
host's matching //tools/fuzz:<host>_fuzz platform automatically.
linux-aarch64 is out of scope for the fuzz harness.
For Linux-target builds from a macOS host, see Building from source.
Reference¶
MODULE.bazel— module manifest + every third-partyhttp_archivepin.bazelrc— flag defaults, sanitizer/fuzz configs, production hardening (stack-protector-strong, _FORTIFY_SOURCE=2, stack-clash-protection, BindNow) with per-config exemptions for asan/ubsan/fuzztools/workspace_status.sh— emitsSTABLE_*keys consumed byexpand_templateandoci_image's stamping- Bazel: BUILD files
- Bazelmod (
MODULE.bazel) rules_foreign_ccrules_ocitoolchains_llvm