Vendored dependencies
One file per dependency. Each file exports a Dependency object that tells
the build system where to fetch the source, how to build it, and what
libraries/headers it provides.
Adding a dependency
- Copy
hdrhistogram.ts(the simplest direct dep) to<name>.ts - Fill in
name,repo,commit,sources,includes,provides.includes - Add
import { <name> } from "./<name>.ts"+ entry inallDepsarray inindex.ts bun run scripts/build/phase3-test.tsto verify it builds
That's it. For most deps you're done. If the dep's build is too entangled
to list sources by hand (zlib-ng's per-file SIMD flags are about the
limit), use kind: "nested-cmake" instead — see NestedCmakeBuild in
../source.ts for the fields.
name must match the directory on disk (vendor/<name>/). If your repo
is oven-sh/WebKit, name it "WebKit" — that's what git clone creates.
Case-sensitive filesystems enforce this.
Ordering in allDeps matters:
- Put deps with
fetchDeps: ["X"]AFTER X in the list - Link order: deps that PROVIDE symbols go after deps that USE them
Removing a dependency
- Delete
<name>.ts - Remove from
allDepsinindex.ts - If any other dep has
fetchDeps: ["<name>"], remove that reference
Updating a commit
Change the commit field. That's it. The build system computes a source
identity hash from sha256(commit + patch_contents) — changing the commit
invalidates .ref, triggers re-fetch, and everything downstream rebuilds.
The .github/workflows/update-<name>.yml jobs do this automatically by
sed'ing the const <NAME>_COMMIT = "..." line. If you rename that
constant, update the workflow too.
For direct deps: the source list is hardcoded, so a bump that adds
or removes a .c/.cpp upstream needs a matching list edit here. CI
catches a missed addition (link error on the unresolved symbol); a
removed file fails at compile. Either way the auto-bump PR goes red,
which is the cue to diff the upstream CMakeLists.txt.
Iterating on a dep from a local checkout
To hack on a vendored dep (e.g. chase a bug in the mimalloc or libuv fork) without cutting a commit and bumping the pin each round, point the build at a git clone:
git clone https://github.com/oven-sh/mimalloc ~/code/mimalloc
bun bd --local-deps=mimalloc=~/code/mimalloc test foo.test.ts
Any github-archive dep the graph compiles or includes can be redirected
(so not lolhtml, which cargo reads from vendor/lolhtml via the workspace
Cargo.toml — point that path at your checkout instead); several at once
with name=path,name=path. Cross-dep references (depSourceDir(), e.g.
lsquic's -I into boringssl) follow the redirect. The checkout is compiled
as-is — no fetch, no .ref stamp, and the dep's patches are not
applied (they target the pinned tarball), so start the clone from the pinned
commit if you want an identical baseline. Switching a dep between pinned and
local moves its -I path, so the first build after the switch recompiles
every TU that sees the dep's headers; after that, edits are picked up
incrementally: direct deps through the compiler depfiles,
nested-cmake/cargo deps by re-invoking their inner build every run. The
build banner shows local:<name> while this is on. Don't edit
vendor/<name>/ in place instead — it is wiped whenever the pin or patches
change. WebKit has its own switch (--webkit=local).
Common fields
export const mydep: Dependency = {
name: "mydep",
// Source tarball. Fetched from GitHub's archive endpoint (no git history,
// just the files at `commit`). Most deps use this.
//
// Other kinds: `prebuilt` (download pre-compiled .a, e.g. WebKit default),
// `local` (user-managed checkout — WebKit declares it because its clone is
// too slow to automate; any github-archive dep becomes one via
// `--local-deps`, see below), `in-tree` (source in src/).
source: () => ({ kind: "github-archive", repo: "owner/repo", commit: "..." }),
// Optional: macro name for bun_dependency_versions.h (process.versions).
// Omit if this dep shouldn't appear there.
versionMacro: "MYDEP",
// Optional: .patch files applied after extraction, or overlay files
// copied into source root (e.g. inject a CMakeLists.txt).
patches: ["patches/mydep/fix-something.patch"],
// Optional: deps whose SOURCE must be ready before this one builds
// (for -I cross-dep headers). See libarchive for an example.
fetchDeps: ["zlib"],
// How to build. `direct` lists sources explicitly; emitDirect compiles
// each as a first-class cc/cxx edge and the resulting .o's go straight
// into bun's link line. See `DirectBuild` in ../source.ts for all
// optional fields (lang/pic/defines/headers/codegen).
build: cfg => ({
kind: "direct",
sources: ["src/foo.c", "src/bar.c"],
includes: [".", "include"],
// defines: { MYDEP_STATIC: 1 },
// cflags: ["-std=c11"],
// headers: { "config.h": "..." }, // Hand-written config.h.
}),
// What this dep exposes to bun's own compile. `libs` is ignored for
// direct builds (the .o's are already on the link line); `includes`
// are relative to the SOURCE dir.
provides: cfg => ({
libs: [],
includes: ["include"],
// defines: ["MY_DEP_STATIC=1"], // Preprocessor defines for bun's compile.
}),
// Optional: skip this dep on some platforms.
enabled: cfg => !cfg.windows,
};
Build types
direct: Sources compiled as first-classccedges in our ninja graph — no sub-process. Best for deps with a stable, small file list and no configure-time codegen we can't replicate. SeeDirectBuildin../source.ts. Prefer this overnested-cmakewhen feasible: it skips a cmake configure (often 5–20s of try_compile probes) and lets LTO see across the dep boundary into bun's call sites.nested-cmake: Runscmake --fresh -B ...thencmake --build. SeeNestedCmakeBuildin../source.tsfor all fields.cargo: Rust deps (currently just lolhtml). SeeCargoBuildin../source.ts.none: Header-only or prebuilt. No build step;.refstamp is the output.
Worked examples
- hdrhistogram.ts / libdeflate.ts — simplest direct deps
- mimalloc.ts — direct build, single unity TU compiled as C++
- tinycc.ts — direct build with a build-time codegen tool
- zlib.ts — direct build with per-source SIMD
-mflags +.h.insubstitution - libarchive.ts / cares.ts — direct build with hand-written per-target config.h
- boringssl.ts — direct build with NASM assembly (win-x64) and a large gen/ manifest
- sqlite.ts — direct build, in-tree source (lives in
src/, notvendor/) - libuv.ts —
enabled: cfg => cfg.windowsfor a platform-only dep - lolhtml.ts — cargo build with rustflags
- webkit.ts —
nested-cmake(sourceSubdir,preBuild) andprebuilt
How the three-step build works
Each dep becomes three ninja build statements, each with restat = 1:
- fetch →
vendor/<name>/.refstamp- Downloads tarball, extracts, applies patches
.refcontainssha256(commit + patches)[:16]- restat: if identity unchanged, no write, downstream pruned
- configure →
buildDir/deps/<name>/CMakeCache.txtcmake --fresh -B <dir> -D...--freshdrops the cache so stale -D values don't persist- restat: inner cmake might not touch cache
- build →
.afilescmake --build <dir> --target ...- restat: inner ninja no-ops if nothing changed
restat is what makes incremental builds fast — if step N was a no-op,
ninja prunes everything after it.