Bazel Build System
Lumio uses a dual build system: Cargo for local development, Bazel for CI and container images. Developers write and test code with standard Cargo commands locally. Bazel takes over in CI where its remote caching and incremental target detection provide significant speed improvements.
Why Bazel
- Remote caching via BuildBuddy -- unchanged targets are not rebuilt across CI runs
- Incremental target detection via bazel-diff -- only affected targets are tested on PRs
- Hermetic builds -- reproducible container images from pinned toolchains and vendored dependencies
- Multi-app image builds -- a single workflow builds and pushes images for all 10 deployable apps
Architecture
lumio/
├── MODULE.bazel # Root module definition
├── .bazelrc # Local Bazel settings
├── .bazelrc.ci # CI-specific settings (BuildBuddy remote cache)
├── misc/toolchains/
│ ├── rust.MODULE.bazel # Rust toolchain (edition 2024, pinned version)
│ ├── proto.MODULE.bazel # Protobuf toolchain
│ ├── v8.MODULE.bazel # V8 prebuilt static library
│ ├── docker.MODULE.bazel # Base image (Alpine, pinned by digest)
│ └── BUILD.bazel # cc_library for V8 native linking
├── vendor/cargo/ # 816 vendored crate BUILD files
│ ├── BUILD.actix-web-4.13.0.bazel
│ ├── BUILD.tokio-1.*.bazel
│ └── ...
├── crates/*/BUILD.bazel # One BUILD per internal crate
└── apps/*/BUILD.bazel # One BUILD per app (binary + image targets)
The root MODULE.bazel includes four toolchain modules and pins the rules_rust fork via git_override.
zaflun/rules_rust Fork
Lumio uses a fork of rules_rust at github.com/zaflun/rules_rust. The fork fixes two issues in the upstream repository:
- Sandbox path rotation for
cargo_build_script-- the upstreamOUT_DIRhandling breaks when Bazel rotates sandbox paths between actions. The fork stabilizesOUT_DIRso build scripts (e.g.,utoipa-swagger-uiasset embedding) produce deterministic outputs. cargo_runfilesdata file mapping -- the upstream mapping does not resolve data dependencies (e.g., V8 binding files) correctly in sandboxed builds. The fork patches the runfiles lookup.
The fork publishes prebuilt cargo-bazel binaries via GitHub Releases for Linux (x86_64, aarch64), macOS (aarch64), and Windows (x86_64, aarch64). These are fetched as http_archive dependencies in MODULE.bazel so that the vendor step does not require building cargo-bazel from source.
Vendored Crates
All external Rust dependencies are vendored into vendor/cargo/ as auto-generated BUILD files (816 files). This avoids network fetches during builds and ensures reproducibility.
Update workflow after changing Cargo.lock:
bazel run //vendor:cargo_vendor
This regenerates all BUILD.*.bazel files in vendor/cargo/ using crate_universe. The vendored files are committed to the repository.
BUILD.bazel Patterns
Crate (library + tests)
Every internal crate follows this pattern:
exports_files(["Cargo.toml"])
load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
rust_library(
name = "lo-common",
srcs = glob(["src/**/*.rs"]),
edition = "2024",
deps = [
"@crate_index//:actix-web",
"@crate_index//:serde",
"@crate_index//:serde_json",
],
visibility = ["//visibility:public"],
)
rust_test(
name = "lo-common_test",
crate = ":lo-common",
)
The exports_files(["Cargo.toml"]) line is required -- the vendor toolchain reads Cargo.toml metadata from every workspace member. Omitting it causes vendor failures.
App (binary + container image)
App BUILD files define a rust_binary, then layer it into a container image:
load("@rules_img//img:image.bzl", "image_manifest")
load("@rules_img//img:layer.bzl", "image_layer")
load("@rules_img//img:push.bzl", "image_push")
load("@rules_rust//rust:defs.bzl", "rust_binary")
rust_binary(
name = "api",
srcs = glob(["src/**/*.rs"]),
edition = "2024",
deps = ["//crates/lo-common", ...],
)
image_layer(
name = "api_layer",
srcs = {
"/usr/local/bin/lumio-api": ":api",
"/etc/lumio/config/default.toml": "config/default.toml",
"/etc/lumio/config/production.toml": "config/production.toml",
},
)
image_manifest(
name = "api_image",
base = "@alpine",
layers = [":api_layer"],
entrypoint = ["/usr/local/bin/lumio-api"],
env = {
"LUMIO__SERVER__HOST": "0.0.0.0",
"LUMIO__SERVER__PORT": "8080",
},
)
image_push(
name = "api_push",
image = ":api_image",
registry = "ghcr.io",
repository = "zaflun/lumio/api",
)
The base image (@alpine) is pinned by digest in misc/toolchains/docker.MODULE.bazel for reproducibility.
Adding a New Crate
- Create the crate directory under
crates/withCargo.tomlandsrc/. - Add it to the workspace
[members]in the rootCargo.toml. - Create
crates/{name}/BUILD.bazelfollowing the library + test pattern above. Includeexports_files(["Cargo.toml"])at the top. - Run
bazel run //vendor:cargo_vendorto regenerate vendored BUILD files if the crate introduces new external dependencies. - Verify the build:
bazel build //crates/{name}:... - If the crate is a dependency of a deployable app, add it to the app's
depsin the app'sBUILD.bazel.
V8 Integration
Crates and apps that depend on deno_core (the V8 JavaScript engine) require special linking. The V8 engine is distributed as a prebuilt static library for x86_64-unknown-linux-gnu, fetched from the denoland/rusty_v8 GitHub Releases.
Toolchain setup (misc/toolchains/v8.MODULE.bazel):
http_archive(
name = "v8_prebuilt",
build_file_content = 'exports_files(["librusty_v8.a"], ...)',
urls = ["https://github.com/denoland/rusty_v8/releases/download/v147.4.0/..."],
)
Linking in app BUILD files -- apps that transitively depend on V8 define a local cc_library and add it to the binary's deps:
load("@rules_cc//cc:cc_library.bzl", "cc_library")
cc_library(
name = "v8_native",
srcs = ["@v8_prebuilt//:librusty_v8.a"],
linkstatic = True,
linkopts = ["-lstdc++", "-ldl", "-lpthread"],
)
rust_binary(
name = "bot-module-worker",
deps = [
"//crates/lo-bot-module-worker",
":v8_native",
...
],
)
Vendor BUILD for the v8 crate -- the vendored BUILD.v8-*.bazel uses $(execpath) to resolve the binding file path at build time:
rustc_env = {
"RUSTY_V8_SRC_BINDING_PATH": "$(execpath gen/src_binding_simdutf_release_x86_64-unknown-linux-gnu.rs)",
},
Key Commands
| Command | Description |
|---|---|
just bazel-build | Build all crates and apps |
just bazel-test | Run all crate tests |
just bazel-images | Build container images for all Rust apps |
just bazel-clippy | Run clippy via Bazel aspects |
just bazel-vendor | Regenerate vendored crate BUILD files |
Direct Bazel invocations:
# Build a single crate
bazel build //crates/lo-common
# Test a single crate
bazel test //crates/lo-common:lo-common_test
# Build a specific app image
bazel build //apps/api:api_image
# Push an image (requires GHCR authentication)
bazel run //apps/api:api_push --@rules_img//img/settings:push_tag="2026.5.3"
BuildBuddy
Remote caching is provided by BuildBuddy. Configuration lives in .bazelrc.ci, which is imported by CI workflows:
build --remote_cache=grpcs://remote.buildbuddy.io
build --remote_timeout=3600
build --remote_cache_compression
Cache write policy:
| Context | Writes |
|---|---|
| PR builds | Read-only (--noremote_upload_local_results) |
Protected branches (next, main) | Read-write (--remote_upload_local_results=true) |
This prevents cache poisoning from untrusted PR builds while keeping the cache warm from protected-branch builds. Authentication uses the BUILDBUDDY_API_KEY secret passed via --remote_header.
Troubleshooting
OpenSSL in sandbox
Some crates (e.g., openssl-sys) need environment variables to find system libraries inside the Bazel sandbox. If builds fail with OpenSSL linker errors, check that action_env settings in .bazelrc expose the necessary paths.
Missing exports_files for Cargo.toml
Every crate's BUILD.bazel must include exports_files(["Cargo.toml"]). Without it, the vendor toolchain cannot read crate metadata and bazel run //vendor:cargo_vendor fails with a missing-file error.
V8 linking failures
V8 linking requires:
- The prebuilt
librusty_v8.aarchive matching thedeno_coreversion inCargo.lock. - A
cc_librarytarget in the app'sBUILD.bazelthat links the archive with-lstdc++ -ldl -lpthread. - The
v8vendor BUILD must have the correct$(execpath)for the binding source file.
If the deno_core or v8 crate version changes in Cargo.lock, update the SHA and URL in misc/toolchains/v8.MODULE.bazel to match the corresponding rusty_v8 release.