Building mbun With Bazel
Author’s note: This article was created with substantial AI assistance.
mbun (an experimental project that rewrites the Bun runtime in C++26 and C++ modules) was originally built using the custom mcpp toolchain.
This migration introduces a Bazel 9 build on top of baseline 5a901d7 to test Bazel’s support for C++20 modules. Previously, when testing modules, the module version of async_simple was the only available open-source project suitable for testing. mbun itself is a native modules project (with 98 modules), making it an ideal test case for Bazel and Clang’s module support. The mcpp build system has not been removed; both systems coexist during the migration. The BUILD.bazel files retain comments mapping them to the mcpp manifests (for example, src/BUILD.bazel maps each item to [targets.mbun] / [dependencies] in mcpp.toml).
The complete code is available at https://github.com/PikachuHyA/mbun/tree/bazel_build_mbun_opensource_20260805.
Commit History
There are three commits on top of baseline 5a901d7. GCC could build the project after the second commit, while both GCC and Clang could build it after the third:
| Order | Commit | Subject | Scale |
|---|---|---|---|
| 1 | b171984 | Baseline: Bazel project skeleton + BUILD files for all modules | 106 files, +1,100 lines, including 100 BUILD.bazel files |
| 2 | c6dc219 | Fix GCC build: GCC 16 module semantic fixes | 4 files, +19/-13 |
| 3 | 6f72ee9 | Fix Clang build: Clang modules/linking fixes | 7 files, +95/-9 |
Commit 1: Bazel Project Skeleton
Module Count
There are 98 modules under modules/. Each module has its own BUILD.bazel, while src/ contains another one defining the cc_binary(mbun) target, for a total of 100 BUILD files.
Dependency Declarations
MODULE.bazel declares mbun_cpp 0.1.0 and uses the BCR to declare dependencies on rules_cc 0.2.22, googletest, boringssl, zlib, nlohmann_json, mimalloc, zstd, brotli, libwebp, sqlite3, libarchive, and openssl 3.5.5.
The std Module
Upstream rules_cc (0.2.22) does not provide automatic detection of the std module. A public fork (https://github.com/PikachuHyA/rules_cc.git, pinned to commit 11baaed) replaces rules_cc through git_override. The fork’s unix_cc_configure.bzl detects libstdc++’s bits/std.cc (or libc++’s std.cppm) and exposes it as @local_config_cc//:std. Anyone interested in native std module support in Bazel can participate in the upstream discussion at https://github.com/bazelbuild/rules_cc/pull/522.
bazel/defs.bzl defines the bun_library() macro, which automatically injects the @local_config_cc//:std dependency into every module. Consequently, each module’s BUILD definition has the following form:
bun_library(name, module_interfaces = glob(["src/**/*.cppm"]), deps=[...])
jsc-prebuilt
The jsc-prebuilt tarball (a bun-webkit static library pinned by SHA-256) contains neither BUILD nor MODULE files. Furthermore, archive_override supports neither build_file nor overlays, and its patch mechanism cannot create new files. In practice, patch_cmds uses a heredoc to generate BUILD.bazel (alwayslink = True + --start-group/--end-group + -latomic -lpthread -ldl) and MODULE.bazel.
JSC Include Semantics and N-API Symbol Exports
- JSC module include semantics:
src/runtime/*.incand*.hppare textually included by.cppmfiles, so they are declared ashdrswithincludes = ["src"].prelude.hppforcesNDEBUGto match the release build of JSC. Because fastbuild does not use-O, it triggers an#errorinwtf/Compiler.h, requiring-DRELEASE_WITHOUT_OPTIMIZATIONS. An explicit dependency on@openssl//:sslis also required: the system headers are present, but the library is not linked, which otherwise leavesEVP_*symbols undefined. - N-API symbol exports:
cc_binary(mbun)adds-Wl,--export-dynamic. In the global module fragment,prelude.hppdefines 105extern "C" napi_*symbols that dynamically loaded.nodeaddons must resolve from the host executable. Without this flag, the linker discardsnapi_*symbols that have no static references, causing Node-API addons to fail duringdlopenwithundefined symbol: napi_create_function.
Commit 2: GCC 16 Compilation Issues
The following two issues occurred with Ubuntu’s system GCC 16. The g++ compiler provided by mbun’s bundled toolchain (mcpp/xlings) did not exhibit them, possibly because of version differences in Ubuntu’s GCC package.
Affected files: modules/core/src/strings.cppm (issue one), and modules/jsc/src/runtime/prelude.hpp, modules/jsc/src/runtime/process_base.inc, and modules/jsc/src/runtime/process_extended.inc (issue two).
Issue One: TU-Local Entity Exposure
In modules/core/src/strings.cppm, an anonymous namespace inside an exported module interface unit enclosed Unicode tables and helper functions. The inline body of the exported string_width function referenced these internal-linkage entities. GCC 16 reported exposes TU-local entity 'enum GBC'. The fix was to remove the three anonymous namespaces from the interface unit. Non-export declarations in a module interface unit are already invisible to importers.
Issue Two: environ Linker Error
The symbol _ZN12_GLOBAL__N_17environE was undefined. Inside the anonymous namespace in runtime.cppm, process_base.inc and process_extended.inc redundantly declared extern "C" char** environ;. GCC 16 treated this as a new internal-linkage entity with a declaration but no definition, whereas Clang merged it with the global ::environ from <unistd.h>. The fix was to remove the declarations from the anonymous namespace. POSIX environ now comes from <unistd.h> in the global module fragment of prelude.hpp, while the Windows _environ declaration was moved into prelude.hpp.
Commit 3: Clang Compilation Issues
Affected files: .bazelrc, modules/ffi/src/native.cppm, modules/install/src/install.cppm, modules/install/src/migration.cppm, modules/js/src/neutral_literal_seam.cppm, modules/jsc/src/runtime.cppm, and modules/resolver/src/resolver.cppm. The changes address the following three compilation issues.
Issue One: Assembly in the Global Module Fragment Is Discarded
mbun_ffi_call_sysv (hand-written SysV AMD64 assembly) was placed in a top-level __asm__ block in the global module fragment (GMF) of native.cppm. Clang’s modules frontend did not emit object code for it and produced no diagnostic, leading to an undefined symbol at link time. Bisection revealed that -Wunsafe-buffer-usage causes Clang to emit the GMF assembly—the same source and compiler behave differently depending on this warning flag. The fix was to enable the flag specifically for this file in .bazelrc:
build:clang --per_file_copt=modules/ffi/src/native\.cppm@-Wunsafe-buffer-usage
The assembly remains in the .cppm file rather than being split out. This issue has been reported upstream: llvm/llvm-project#213822.
Issue Two: Conflict Between import std and GMF Headers
The GMF of runtime.cppm includes JSC/libstdc++ headers, while the same unit also used import std (where GCC’s std.cc exports using std::*). Clang reported cannot befriend target of using declaration. The fix was to replace import std in this unit with #include <bits/stdc++.h>. Because bits/stdc++.h is specific to libstdc++, porting to libc++ will require a redesigned solution. This behavior conforms to the C++ standard, but no more elegant solution has yet been found, so the code temporarily falls back to the header-based approach.
Issue Three: Passing Incomplete Types to Traits Causes Hard Errors
Self-referential structures such as Json, JsonValue, and Literal (whose members include vector<pair<string, Self>>) caused the Clang hard error incomplete type used in type trait expression. The instantiation chain was: the explicit(...) constraint on pair’s default constructor checks __is_implicitly_default_constructible<Self> → requires a Self{} temporary to be destructible → implicit ~Self → vector::~vector → is_trivially_destructible<pair<...,Self>>—forming a cycle while Self is still incomplete.
Starting with PR #121333 (corresponding to LWG3929), Clang changed “incomplete type passed to a trait” from a soft SFINAE failure to a hard error; GCC 16 still treats it as a soft failure. The fix was to declare Self() = default; inside the class, preventing the implicit destructor from being instantiated while the class is incomplete. This change had a secondary effect: declaring a constructor meant the class was no longer an aggregate, so several call sites (six in total, across resolver.cppm and neutral_literal_seam.cppm) were changed to use explicit construction.
Build Verification
bazelisk build //src:mbun --config=clang # Clang
bazelisk build //src:mbun --config=gcc # GCC 16 (requires Bazel PR #30024)
./bazel-bin/src/mbun --version # Build artifact
//src:mbuncompiles and links successfully in full with both Clang and GCC 16.- The GCC build depends on https://github.com/bazelbuild/bazel/pull/30024; that PR must be merged before use.
- A full build executes more than 2,400 actions.