Case Study
Migrating BusTub from CMake to zigc
CMU's BusTub educational RDBMS has 11 vendored C/C++ dependencies managed by CMake. This documents how to recreate it as a greenfield zigc project — no CMake anywhere.
Overview
BusTub is CMU's educational relational database, written in C++17 with 11 vendored third-party libraries. The original project uses CMake with CMakeLists.txt files at every level.
Using zigc, we replaced the entire CMake build system with Zig's build system. Each vendored dependency was repackaged as a standalone Zig package, uploaded to S3, and registered in zigc's global registry. The result:zigc init + 11 × zigc add + one build.zig.
Prerequisites
- Zig 0.16.0 — download
- zigc —
curl -fsSL https://raw.githubusercontent.com/nathanjmorton/zigc/main/install.sh | bash - The original BusTub repo —
git clone https://github.com/cmu-db/bustub
Are the dependencies public?
Yes. All 11 dependency packages are hosted on a public S3 bucket at nathanjmorton-zigc-packages.s3.amazonaws.com/bustub-deps/. Anyone can zig fetch them or use zigc add from the registry. No AWS credentials or special access is needed.
They are also pre-registered in zigc's global registry. After running zigc registry update, you can add them by name (e.g. zigc add murmur3).
Step 1 — Scaffold the project
$ zigc init bustub --cpp $ cd bustub $ zigc registry update
This creates build.zig, build.zig.zon, src/main.cpp, and .gitignore. The --cpp flag generates a C++17 template with link_libcpp.
Step 2 — Add all dependencies
Compilable libraries
$ zigc add murmur3 $ zigc add linenoise $ zigc add libfort --lib fort $ zigc add utf8proc $ zigc add libpg_query --lib duckdb_pg_query $ zigc add fmt $ zigc add googletest
Header-only libraries
$ zigc add argparse --header-only $ zigc add backward_cpp --header-only $ zigc add cpp_random_distributions --header-only $ zigc add readerwriterqueue --header-only
Each zigc add writes the dependency to build.zig.zon (URL + content hash) and injects linking boilerplate into build.zig.
--lib overrides the artifact name when it differs from the package name. --header-only inserts mod.addIncludePath instead of mod.linkLibrary.
Step 3 — Copy BusTub source code
From the original BusTub checkout, copy:
src/— all 13 module directories (binder, buffer, catalog, common, concurrency, container, execution, optimizer, planner, primer, recovery, storage, type)src/include/— all BusTub headerstools/shell/shell.cpp— the shell entry point
Remove the scaffolded src/main.cpp and any CMakeLists.txt files that came along.
$ rm src/main.cpp $ cp -r ../bustub/src/{binder,buffer,catalog,common,concurrency,container,execution,optimizer,planner,primer,recovery,storage,type} src/ $ cp -r ../bustub/src/include src/ $ mkdir -p tools/shell && cp ../bustub/tools/shell/shell.cpp tools/shell/ $ find src -name CMakeLists.txt -delete
Step 4 — Write build.zig
Replace the scaffolded build.zig with one that:
- Adds include paths for
src/includeandsrc/ - Links all 7 compilable deps and adds include paths for the 4 header-only deps
- Compiles all 129
.cppfiles fromsrc/ - Compiles
tools/shell/shell.cppas the entry point - Uses
-std=c++17 -wflags
The full build.zig is ~200 lines — mostly just the list of 129 source files. See the complete file on GitHub.
Step 5 — Build
$ zigc build
On first build, Zig fetches all 11 dependencies from S3, compiles the 7 static libraries (murmur3, linenoise, libfort, utf8proc, libpg_query, fmt, googletest), then compiles all 129 BusTub source files and links everything into a single bustub-shell binary.
Result: a 33 MB Mach-O arm64 executable with 11,865 symbols.
Verify
$ zigc check $ zigc verify
Dependency reference
murmur3C++ · static liblinenoiseC · static liblibfortC · static lib--lib fortutf8procC · static liblibpg_queryC++ · static lib--lib duckdb_pg_queryfmtC++ · static libgoogletestC++ · static libargparseC++ · header-only--header-onlybackward_cppC++ · header-only--header-onlycpp_random_distributionsC++ · header-only--header-onlyreaderwriterqueueC++ · header-only--header-onlyArchitecture
The dependency pipeline:
- Package — each library gets a
build.zig+build.zig.zonthat compiles it as a static library (or exposes headers for header-only deps) - Tar + upload — packaged as
.tar.xz(ustar format,application/x-xzcontent-type) and uploaded to S3 - Hash —
zig fetch <url>computes the content hash - Register — URL + hash + lib name added to
registry.jsonin the zigc repo - Consume —
zigc add <name>looks up the registry, writes the dep tobuild.zig.zon, and injects linking code intobuild.zig
Key lesson: S3 must serve .tar.xz files with Content-Type: application/x-xz. Without this, Zig's HTTP tar unpacker misidentifies the compression and fails with TarHeader errors.