Mobile Development 22 min read

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 Tech
Bilibili Tech
Bilibili Tech
Bazel Modules, rules_xcodeproj, and Fastbuild Rule for iOS Monorepo at Bilibili

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-xcodeproj

The 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.

iOSMonorepoBuild SystemBazelFastbuildModulesrules_xcodeproj
Bilibili Tech
Written by

Bilibili Tech

Provides introductions and tutorials on Bilibili-related technologies.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.