Bazel Modules, rules_xcodeproj, and Fastbuild Rule for iOS Monorepo at Bilibili
Bilibili’s iOS team migrated from the cumbersome WORKSPACE model to Bazel Modules, adopted the community‑maintained rules_xcodeproj to replace Tulsi, and built a custom Fastbuild rule that hashes source files to reuse binaries, cutting incremental build times from dozens of minutes to under two.
Bilibili's iOS engineering team adopts a Bazel‑based monorepo architecture. The article reviews the evolution from the traditional WORKSPACE dependency management to the newer Bazel Modules (Bzlmod) approach, and shares practical experiences with custom rules such as rules_xcodeproj and a Fastbuild rule designed to accelerate large‑scale builds.
Problems with the WORKSPACE model
The legacy WORKSPACE file suffers from three major drawbacks:
Uncertain dependency versions – diamond‑dependency scenarios make it hard to know which version of a library is finally used.
Excessively long files – every transitive external library must be listed, leading to maintenance headaches.
Difficulty configuring a unified proxy – external GitHub/Google dependencies often require manual URL rewrites.
An excerpt of a typical WORKSPACE for a custom container‑orchestrated rules repository (≈50 lines) is shown below:
workspace(name = "build_bazel_rules_gripper")
load("@build_bazel_rules_gripper//:repositories.bzl", "gripper_rules_dependencies")
gripper_rules_dependencies()
load("@com_github_buildbuddy_io_rules_xcodeproj//xcodeproj:repositories.bzl", "xcodeproj_rules_dependencies")
xcodeproj_rules_dependencies()
load("@build_bazel_rules_swift//swift:repositories.bzl", "swift_rules_dependencies")
swift_rules_dependencies()
load("@build_bazel_rules_swift//swift:extras.bzl", "swift_rules_extra_dependencies")
swift_rules_extra_dependencies()
load("@cgrindel_rules_spm//spm:deps.bzl", "spm_rules_dependencies")
spm_rules_dependencies()
load("@cgrindel_rules_spm//spm:defs.bzl", "spm_pkg", "spm_repositories")
spm_repositories(
name = "swift_pkgs",
dependencies = [
spm_pkg("https://github.com/apple/swift-syntax", name = "SwiftSyntax", revision = "swift-5.7-DEVELOPMENT-SNAPSHOT-2022-08-02-a", products = ["SwiftSyntaxParser", "SwiftSyntaxBuilder"]),
spm_pkg("https://github.com/apple/swift-argument-parser", name = "swift-argument-parser", from_version = "1.1.3", products = ["ArgumentParser"]),
],
)Bazel Modules (Bzlmod)
Starting with Bazel 5.0.0, Bzlmod replaces WORKSPACE . By enabling the flag --enable_bzlmod , external dependencies are fetched from the Bazel Central Registry (BCR) using module names and versions, eliminating manual URL handling. Bzlmod applies the Minimum Version Selection (MVS) algorithm to resolve diamond dependencies deterministically, guaranteeing a single version (e.g., the higher 1.2 version in the illustrated case).
The new MODULE.bazel file is dramatically shorter. An example derived from the same repository is:
module(name = "rules_gripper", version = "0.2.4")
bazel_dep(name = "rules_swift", version = "1.6.0")
bazel_dep(name = "rules_apple", version = "2.1.0")
bazel_dep(name = "swift_argument_parser", version = "1.2.0")
bazel_dep(name = "swift_syntax", version = "0.50700.1")
bazel_dep(name = "swiftgraph", version = "3.1.0")
bazel_dep(name = "swiftast", version = "0.0.4")
bazel_dep(name = "rules_xcodeproj", version = "1.2.0")Beyond version determinism, Bzlmod allows a unified proxy configuration at the BCR level, removing the need to rewrite URLs in the source.
Replacing Tulsi with rules_xcodeproj
Historically, Bazel users relied on Tulsi to generate Xcode projects, but Tulsi required complex .tulsiproj files and suffered from indexing slowness, IDE crashes, and unreliable autocomplete. The community‑maintained rules_xcodeproj rule simplifies this workflow: a single xcodeproj rule in a BUILD file wraps an ios_application (or ios_unit_test ) target.
# srcs/binary/BUILD
load("@rules_xcodeproj//xcodeproj:defs.bzl", "top_level_target", "xcodeproj")
ios_application(
name = "bili-universal",
...
)
xcodeproj(
name = "my-xcodeproj",
project_name = "bili-universal",
tags = ["manual"],
top_level_targets = [top_level_target(":bili-universal", target_environments = ["device", "simulator"])],
build_mode = "bazel",
ios_simulator_cpus = "x86_64",
pre_build = "${SRCROOT}/tools/pre_build.sh",
post_build = "${SRCROOT}/tools/post_build.sh",
)Generating the Xcode project is as simple as:
bazel run //srcs/binary:my-xcodeprojThe rule supports two modes: BwB (build with Bazel) and BwX (build with Xcode). BwB leverages the index‑import tool for fast, accurate indexing, effectively eliminating the performance problems observed with Tulsi.
Fastbuild Rule
Even with Bzlmod, large iOS monorepos still suffer from costly recompilations when low‑level header files change. To mitigate this, the team introduced a Fastbuild mode that trades strict correctness for speed. The idea is to compute a lightweight "Fastbuild Hash" based only on a target's own source files and compilation options, ignoring transitive header changes.
Implementation uses Bazel Aspects to attach a .fb.json artifact to every target in the dependency graph:
def _fastbuild_aspect_impl(target, ctx):
dump_tool = ctx.executable._dump_tool
fastbuild_hash = ctx.actions.declare_file(ctx.label.name + ".fb.json")
# ... compute hash ...
ctx.actions.run(
executable = dump_tool,
arguments = [fastbuild_hash.path],
inputs = inputs,
tools = [dump_tool],
outputs = [fastbuild_hash],
)
return [OutputGroupInfo(fastbuild_info_json = [fastbuild_hash])]
fastbuild_aspect = aspect(
implementation = _fastbuild_aspect_impl,
attr_aspects = ["deps"],
attrs = {
"_dump_tool": attr.label(
default = Label("@//rules/fastbuild:fastbuild_hash"),
allow_files = True,
executable = True,
cfg = "exec",
),
},
)During a build, the aspect computes the Fastbuild hash for every target, and the custom rules replace the original objc_library and cc_library with variants that download pre‑built binaries when the hash matches, skipping compilation.
Typical struct definition used to illustrate correctness trade‑offs:
struct Person {
char name[20];
int age;
float height;
};Adding a new field changes the memory layout, which can cause runtime errors if an old binary is reused. The team accepts this risk for the massive speed gains in their development workflow.
Performance impact
Initial build
Subsequent build
Without Fastbuild
120 mins
60 mins
With Fastbuild
15 mins
1 min
In practice, enabling Fastbuild reduced a typical incremental compile from dozens of minutes to under two minutes, improving developer productivity by over 90%.
Conclusion
Since its inception five years ago, Bazel has matured into a robust, cross‑platform build system. Bilibili's iOS team has transitioned from early‑stage challenges to a streamlined workflow using Bzlmod, rules_xcodeproj , and a custom Fastbuild rule. The shared experience aims to help other mobile teams adopt Bazel more confidently.
Bilibili Tech
Provides introductions and tutorials on Bilibili-related technologies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.