GN + Ninja (Polyglot (C/C++))
A two-layer build architecture from Google: GN is a meta-build system that turns a tree of BUILD.gn files into one giant declarative dependency graph, and Ninja is the deliberately "dumb", maximally-fast executor that runs that graph — the generate/execute split that powers Chromium, Fuchsia, V8, Dart, and Flutter Engine.
| Field | Value |
|---|---|
| Language | C++ (both GN and Ninja engines); GN's BUILD.gn configuration language is a small dynamically-typed imperative DSL |
| License | GN: BSD 3-Clause ("Copyright 2015 The Chromium Authors"); Ninja: Apache-2.0 |
| Repository | gn.googlesource.com/gn · ninja-build/ninja |
| Documentation | GN reference · GN quick start · Ninja manual |
| Category | Polyglot Build Orchestrator |
| Workspace model | Single source tree rooted at a .gn dotfile; the whole tree is one workspace of BUILD.gn packages, sliced per toolchain |
| First released | Ninja 2012 (Evan Martin, for Chrome); GN 2015 (Brett Wilson, replacing GYP) |
| Latest release | Ninja v1.13.x; GN has no release versions — only the sequence of commits on main |
Latest release (as of June 5, 2026):
Ninja's current line is1.13.x(which added GNU-Make jobserver support).GNis unusual: it "does not guarantee the backwards-compatibility of new versions and has no branches or versioning scheme beyond the sequence of commits to the main git branch (which is expected to be stable)" (README.md) — you pin a commit hash, not a tag. Separately, Google is rolling outSiso, a Go drop-in replacement for theNinjaexecutor with native remote execution, across Chromium/WebRTC during 2025–2026;GNstays unchanged as the generator. See Caching & remote execution.
Overview
What it solves
GN + Ninja solve the giant multi-language, multi-platform C/C++ repository problem: a single tree (Chromium is tens of thousands of source files) that must build for many targets — Linux, Windows, macOS, Android, iOS, Fuchsia, host vs. device — from one coherent dependency graph, fast, and correctly, with header inclusion validated against the declared graph. This is the same "one repo, one graph, build exactly what changed" goal as Bazel, Buck2, and Please, but reached from the opposite direction: rather than a hermetic content-addressed engine with its own package manager, GN is a pure code generator that emits files for a separate, minimal executor.
The architecture is a strict two-layer split:
GNparsesBUILD.gnfiles (an imperative DSL), resolves the target dependency graph, applies per-platform configuration, and writes outbuild.ninjafiles — it never compiles anything itself.Ninjareads those generated files and does nothing but schedule and run commands as fast as possible, withmtime-based incremental rebuilds.
Ninja's own design statement makes the division explicit (Ninja manual):
"Where other build systems are high-level languages, Ninja aims to be an assembler."
So GN is the "compiler" (the policy, the decisions, the human-facing language) and Ninja is the "assembler" (no policy, no decisions, just execute). Within the polyglot survey, GN+Ninja is the "generator + minimal executor" data point — contrast the all-in-one hermetic engines Bazel/Buck2 and the sibling generators Meson and CMake, which also target Ninja as a backend.
Design philosophy
Two complementary philosophies stack here. GN's, from its landing page (gn.googlesource.com/gn):
"It is designed for large projects and large teams. It scales efficiently to many thousands of build files and tens of thousands of source files. … It has a focus on correctness. GN checks for the correct dependencies, inputs, and outputs to the extent possible."
GN's README adds a striking, deliberately self-limiting goal — it wants to be under-powered (README.md):
"GN has the goal of being minimally expressive."
The rationale is team-scale legibility: direct "members of a large team (who may not have much knowledge about the build) down an easy-to-understand, well-lit path." GN deliberately lacks the Turing-complete extensibility of Bazel Starlark rules — there is no user-defined rule type; you compose the built-in executable/static_library/source_set/action/group targets.
Ninja's philosophy is the mirror image — be maximally minimal so it can be maximally fast (Ninja manual):
"Build systems get slow when they need to make decisions. … when convenience and speed are in conflict, prefer speed."
Ninja's explicit non-goals are revealing: "convenient syntax for writing build files by hand," "built-in rules," and "build-time decision-making ability such as conditionals or search paths." All of that is pushed up into the generator. The result: Ninja files "shouldn't be hand-written" — they are an intermediate representation, the way object files are.
How it works
The two-layer pipeline
# 1. GN reads BUILD.gn + the .gn dotfile, resolves the graph, writes Ninja files
gn gen out/Default
# 2. Ninja executes the generated graph (build.ninja under out/Default)
ninja -C out/Default # or: ninja -C out/Default //chrome:chromegn gen out/Default "Generates ninja files from the current tree and puts them in the given output directory" (GN reference). Each output directory (out/Default, out/Release, out/android) is an independent build with its own args.gn — GN "supports multiple parallel output directories, each with their own configuration" (gn.googlesource.com/gn). A key ergonomic detail: the generated graph includes a rule to regenerate itself, so after editing a BUILD.gn you just re-run ninja and it re-invokes gn gen automatically — no manual regeneration step.
Targets, configs, and labels
A BUILD.gn file declares targets. The built-in target types are fixed (you cannot define new ones):
| Target type | Role |
|---|---|
executable | A linked binary |
static_library | A .a/.lib archive |
shared_library | A .so/.dll/.dylib |
source_set | A virtual archive — object files grouped without an actual .a link step |
group | "a collection of dependencies that's not compiled or linked" (a meta-target) |
config | A named bundle of defines, include_dirs, cflags, … applied to targets |
action / action_foreach | An arbitrary script invocation (codegen, the universal escape hatch) |
copy | File copy |
A minimal BUILD.gn (GN quick start):
# //tutorial/BUILD.gn
executable("tutorial") {
sources = [
"tutorial.cc",
]
deps = [
"//base",
]
}Targets are addressed by labels. // is the source root (the directory holding the .gn dotfile):
//tutorial:tutorial # target 'tutorial' in package //tutorial
//base # shorthand for //base:base (target named like its dir)
:other_target # a target in the SAME BUILD.gn
//foo:bar(//build/toolchain:host) # 'bar' built in a SPECIFIC toolchainThat last form is the crucial one: a label carries an optional toolchain suffix. The full label syntax is //path/to/dir:name(//path/to/toolchain:label).
Configs and the propagation model
GN's answer to "how do compile flags flow across the graph" is the config target plus three flavors of dependency edge:
config("my_lib_config") {
defines = [ "ENABLE_DOOM_MELON" ]
include_dirs = [ "//third_party/doom_melon" ]
}
static_library("hello") {
sources = [ "hello.cc" ]
configs += [ ":my_lib_config" ] # applies to THIS target
public_configs = [ ":my_lib_config" ] # ALSO applies to anything depending on it
}deps— "Private linked dependencies"; only this target links them.public_deps— "Declare public dependencies"; dependents inherit them transitively (the analogue of CMake'sPUBLIClinkage). This is how a header exposed in your public API forces downstream targets to also see its dependency.data_deps— "Non-linked dependencies"; built but not linked, for runtime data/test fixtures.
(All quoted from the GN reference.) public_configs does the same upward propagation for compile flags. This declarative propagation is what lets GN "cleanly express many complicated build variants" without per-target flag duplication.
Build arguments: declare_args() and args.gn
Build configuration is parameterized with build args, declared with defaults in .gn/.gni files and overridden per-output-dir in args.gn:
# somewhere in the build config
declare_args() {
enable_teleporter = true
is_debug = true
target_cpu = "x64"
}# out/android/args.gn (edited via `gn args out/android`)
target_os = "android"
target_cpu = "arm64"
is_debug = falsegn args out/android --list prints every available arg with its default and docstring. Args are ordinary global variables, so BUILD.gn files branch on them with plain if (is_debug) { … } / if (is_android) { … }.
Ninja's execution model
The generated build.ninja has just two primitives (Ninja manual):
rule cc
command = gcc $cflags -c $in -o $out
depfile = $out.d
deps = gcc
build foo.o: cc foo.c # an edge: output : rule inputsA rule is a command template; a build statement is one edge of the DAG (outputs : rule explicit_inputs | implicit_inputs || order_only_inputs). Ninja does mtime-based change detection: an edge re-runs when an input is newer than an output, or when the command line itself changed — "Outputs implicitly depend on the command line that was used to generate them, which means that changing e.g. compilation flags will cause the outputs to rebuild." Two on-disk databases make restart cheap: .ninja_log (command hashes per output) and .ninja_deps (compacted header-dependency info). Header dependencies are discovered from the compiler, not enumerated by hand: deps = gcc reads gcc -MD depfiles, deps = msvc parses /showIncludes. The restat attribute (which GN sets on actions) re-checks output mtime after a command, so a regenerated-but-unchanged output doesn't needlessly cascade rebuilds downstream.
The five dimensions
1. Workspace declaration & topology
- Root marker, not a member list. A
GNworkspace is rooted at the.gndotfile: whengnstarts it searches the current directory and parents for a file named.gn, and that file marks the source root (the//of all labels). There is nomembers = [...]array (contrast Cargo's[workspace]or pnpm's globbedpnpm-workspace.yaml). The workspace is the entire tree under the root, and topology is implicit: every directory with aBUILD.gnis a package; targets and theirdepsform one global graph. - The
.gndotfile is the root config. It names thebuildconfigscript that bootstraps every toolchain, optionally aroottarget/dir,root_patternsto scope which targets get generated,check_targets/no_check_targetsfor header checking,exec_script_allowlist(whichBUILD.gnfiles may shell out at parse time),script_executable(the Python used foractions), anddefault_args. - Topology is sliced by toolchain, not by member. This is
GN's defining structural feature and has no analogue in the package-manager tools. "All the GN files are instantiated separately in each toolchain. Each toolchain can set global variables differently, so GN code can use tests likeif (is_kernel)orif (current_toolchain == some_toolchain)to behave differently in different contexts" (Fuchsia: Introduction to GN). Onegn gencan build the same//basefor the host compiler, the device ARM64 compiler, and a kernel toolchain simultaneously — the toolchain is a second axis on every label. - Code sharing via
import(). Reusable declarations (configs, templates, arg declarations) live in.gnifiles pulled in withimport("//build/foo.gni")— "Import a file into the current scope."template()defines reusable macros that expand to built-in targets (the closestGNgets to user-defined rules, short of a true rule type). - No multi-root / no nested workspaces. Unlike go.work's multi-module roots, a
GNbuild has exactly one.gnroot. Vendored third-party code lives in subdirectories of the same tree with their ownBUILD.gnfiles.
2. Dependency handling & isolation
- No package manager, no resolver, no lockfile. This is the sharpest contrast with every language-package-manager in the survey.
GNhas no concept of fetching, versioning, or resolving external dependencies — there is nodub.selections.json/Cargo.lock/virtual store equivalent. All code that participates in the build must already be present in the source tree (Chromium vendors its dependencies viagclient/DEPS, an entirely separate tool that runs beforeGN).GNonly knows about targets that exist on disk under//. - Cross-references are just labels — no hoisting, no symlinks. A library in one directory is depended on from another purely by its
//path:targetlabel. There is no hoistednode_modules(npm), no isolated symlink tree (pnpm), noworkspace:/path=protocol (yarn-berry) — because there are no packages in the registry sense, only graph nodes. The dependency graph is the cross-reference mechanism, and topological build order falls out of it automatically. - Isolation is by
config/visibility, not by sandbox.GNdoes not sandbox actions the way Bazel/Buck2/Please do — there are no per-action namespaces, and the input hash is not trustworthy against undeclared reads. InsteadGNenforces graph discipline two ways:visibility(a target lists which labels may depend on it;gn generrors on violations) andgn check, the header-inclusion checker — "GN's include header checker validates that the includes for C-like source files match the build dependency graph" (GNreference). Plusassert_no_depslets a target forbid a banned dependency anywhere in its transitive closure. These are correctness tools, not hermeticity tools. testonlymarks targets usable only by othertestonlytargets — preventing test code from leaking into shipping binaries, checked atgn gentime.
3. Task orchestration & scheduling
GNbuilds the DAG;Ninjaschedules it.GNresolves the full transitive target graph and lowers it tobuild.ninjaedges.Ninjathen constructs its own file-level DAG and runs ready edges concurrently — "Builds are always run in parallel, based by default on the number of CPUs your system has" (override with-j) (Ninjamanual). SinceNinja1.13 it also speaks the GNU Make jobserver protocol, so nested builds share one global job budget.Change detection:
mtime+ command-line hash (not content hashing). This is a deliberate divergence from the content-addressed engines.Ninjarebuilds an edge when an inputmtimeis newer than the output, or when the recorded command changed (.ninja_log). It does not hash file contents by default, so a touched-but-unchanged file will trigger a rebuild — the speed/correctness tradeNinjachose (restatmitigates the cascade for generators). Header deps come from compiler depfiles into.ninja_deps, so adding/removing an#includecorrectly re-triggers without manual edge edits.Affected-target detection via
gn refs.GN's monorepo-slicing primitive isgn refs, which "Finds reverse dependencies (which targets reference something)" (GNreference). Crucially it accepts file inputs:bash# Which targets list this header as a source? gn refs out/Default //base/macros.h # All targets that depend (directly or transitively) on a changed file: gn refs out/Default //base/macros.h --allFeeding a git diff's changed files into
gn refs … --allyields the set of affected targets — the CI patternninja -C out $(gn refs out <changed-files> --all). This isGN's analogue of Turborepo's--filter=...[ref]or Please'splz query changes, but it is a graph query you run yourself, not a first-class--sinceflag, and it works off the generated graph in an out-dir rather than a git revision diff.Rich graph introspection.
gn desc <out> <target> deps --treeprints a dependency tree;gn path <out> //a //bfinds dependency paths between two targets;gn ls <out> <pattern>lists matching targets;gn outputsmaps a target to its output files;gn desc … defines --blametraces where adefinecame from acrosspublic_configs.
4. Caching & remote execution
GNitself does no caching and no remote execution — by design.GNis a generator; it produces files and exits. There is no build cache, no action cache, no REAPI client inGN. TheGNreference documents none, and that is the whole point of the two-layer split.Ninja's "cache" is the incremental out-dir. NativeNinjahas no remote or shared cache either — its incrementality comes entirely frommtime+ the.ninja_log/.ninja_depsdatabases in the output directory. Reusing an out-dir across git branches gives fast local rebuilds, but there is no content-addressed store and nothing shared between machines out of the box.Remote execution is bolted on at the command level. Large
GN+Ninjashops get caching/RBE by wrapping the compiler command thatGNemits, not by anyGN/Ninjafeature. In Chromium this is agn arg-controlled rewrapper prefix in front ofcc/cxx, plus areproxy/reclientdaemon started around theninjainvocation, talking the Remote Execution API (REAPI) to a backend (e.g. an RBE cluster). Because "GN's language flexibility" lets the toolchain prepend an arbitrary wrapper, RBE integration is purely a configuration choice — neitherGNnorNinjaknows it is happening.Sisois the strategic answer. Google is migrating the executor (notGN) fromNinjatoSiso— a Go reimplementation that is a "drop-in replacement for Ninja" with native remote execution and remote caching built in. As of 2026 Chromium/WebRTC are mid-migration (Android builds began switching in Feb 2026). The division of labor is preserved:GNstill generates the graph;SisoreplacesNinjato make REAPI caching/execution first-class instead of command-wrapped. REAPI servers thatSiso(and reclient) can target includeBuildBuddy,Buildbarn, andNativeLink.
5. CLI / UX ergonomics
- Two binaries, two verbs. The command boundary is
gnfor generate & introspect andninjafor build. There is no single tool: yougn genonce (and re-run only when adding files), thenninja -C <out>to build.ninja //chrome:chromebuilds one target; bareninja -C out/Defaultbuilds the default set. - Out-dir is the unit of selection, label is the target. Unlike the
--filter/-ppackage selectors of pnpm/Turborepo,GNselection is label-based:ninja -C out //path:target. Multiple configurations are multiple out-dirs (out/Debug,out/Release,out/android), each fully independent — the parallel-output-directories model. gnsubcommands are the introspection surface.gn gen,gn args(edit/list build args),gn desc(target details:deps,defines,sources,--tree,--blame),gn refs(reverse deps / affected targets),gn path(path between targets),gn ls(list targets),gn outputs(target→files),gn check(header validation),gn format(canonicalBUILD.gnformatting),gn clean, andgn meta(walk the metadata graph).GN"has comprehensive built-in help available from the command-line" (gn.googlesource.com/gn) —gn help <topic>documents every function and built-in variable, and the entire reference is that help concatenated.- Toolchain on the label, not a flag. Targeting a specific platform is
ninja //foo:bar(//build/toolchain:android)or selecting the right out-dir whoseargs.gnsetstarget_os/target_cpu— the multi-platform story is expressed in configuration and labels rather than CLI filter flags.
Strengths
- Generator/executor separation scales. Pushing all policy into
GNand all speed intoNinjais why Chromium-scale repos (tens of thousands of files) build with near-instant incrementality. The sameNinjabackend is reused byCMakeandMeson, proving the executor's generality. - First-class multi-platform via toolchains. One
gn gencan cross-compile the same sources for host + multiple devices/architectures in a single graph — a capability the language-package-managers simply don't have. - Correctness tooling without a sandbox.
gn check(header/include validation),visibility,testonly, andassert_no_depscatch dependency errors at generate time — strong guard-rails tuned for large teams. - Clean, legible, minimally expressive DSL.
BUILD.gnis easy for non-experts to edit;gn formatenforces one canonical style; built-ingn helpis thorough. - Powerful graph introspection.
gn refs/desc/path/lsmake the dependency graph queryable for affected-target slicing, blame, and auditing. Ninjais tiny, fast, and ubiquitous. A minimal, well-understood executor with reliable depfile-based header tracking and jobserver-aware parallelism.
Weaknesses
- No dependency management whatsoever. No fetch, no version resolution, no lockfile — you must vendor everything (Chromium needs
gclient/DEPSas a wholly separate layer). There is no answer to Cargo/uv-style registry dependencies. - No built-in caching or remote execution. Native
Ninjacaches only via the local out-dir; RBE must be assembled externally (command-wrapping + reclient/Siso), unlike the engine-native REAPI of Bazel/Buck2. - No hermeticity / no content hashing.
mtime-based change detection can both over-build (touched-but-unchanged files) and, without sandboxing, miss undeclared inputs — correctness leans ongn checkand discipline rather than enforcement. - Deliberately limited expressiveness. No user-defined rule types;
template()- built-in targets only. Complex generation patterns that Starlark expresses directly require contortions.
- No versioning / unstable interface.
GNships no releases — you pin a commit; there is no stable API contract beyond "main is expected to be stable." - Two-tool, two-step workflow.
gn genthenninjais more ceremony than a single-binary tool, and the model is squarely C/C++/Rust/ObjC/Swift-centric, not general-purpose.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
Strict generator (GN) / executor (Ninja) split | Policy lives in a legible DSL; execution is a "dumb", maximally-fast assembler | Two binaries, a generate-then-build two-step; the IR (build.ninja) is machine-only |
Ninja "aims to be an assembler"; speed over convenience | Instant incremental builds at Chromium scale; minimal, predictable executor | Ninja files are unwritable by hand; all ergonomics must come from the generator |
GN is "minimally expressive" (no user rule types) | A well-lit path for large teams who don't know the build; legibility and consistency | Cannot express what Starlark/Buck rules can; complex codegen is awkward |
Workspace = tree under a .gn dotfile (no member list) | Zero ceremony — every BUILD.gn dir is a package automatically | No explicit sub-workspace scoping or named member sets; can't partition a repo declaratively |
| Toolchain as a second axis on every label | One gn gen cross-compiles the same sources for many platforms in one graph | A mental-model cost; the same target exists once per toolchain, multiplying graph nodes |
| No package manager / lockfile / resolver | Keeps GN a pure generator; deps are whatever is on disk under // | Needs a wholly separate vendoring tool (gclient/DEPS); no registry-dependency story |
mtime + command-hash change detection (not content hashing) | Fast startup via .ninja_log/.ninja_deps; no hashing every file | Over-builds on touched-but-unchanged files; not hermetic; relies on gn check for input correctness |
| Caching/RBE bolted on by wrapping the compiler command | Org chooses any REAPI backend without engine lock-in; integration is just a gn arg | No engine-native caching; teams must run reclient/reproxy/Siso and a CAS cluster themselves |
Correctness via gn check/visibility/testonly (not a sandbox) | Catches graph errors at generate time without sandbox overhead | Not hermetic — undeclared reads aren't prevented, only header-include mismatches are flagged |
Siso replaces the executor, not GN | Adds native remote execution/caching while preserving the generator and the whole BUILD.gn corpus | A migration in flight (2025–2026); two executors coexist during the transition |
Sources
gn.googlesource.com/gn— landing page (strengths, projects, parallel out-dirs)gn.googlesource.com/gnREADME.md— "minimally expressive", versioning stanceGNreference (all built-in help concatenated): targets,deps/public_deps,gn refs/gn check/gn desc, the.gndotfile, toolchainsGNquick start —BUILD.gnexamples, labels,declare_args,gn gen/gn argsNinjamanual — design goals/non-goals, "aims to be an assembler", rules/build statements,deps/depfile,restat, parallelism, jobserver- Fuchsia — Introduction to GN (per-toolchain instantiation, multi-platform model)
- Chromium build instructions / Reclient + Siso remote execution context
SisoREADME — drop-inNinjareplacement with native remote execution- Sibling deep-dives: Bazel · Buck2 · Please · Pants ·
Meson·CMake·Ninja· Cargo · Turborepo · pnpm · npm · Yarn Berry · Go (go.work) · uv; REAPI backendsBuildBuddy/Buildbarn/NativeLink; the umbrella survey index and the D async/dublandscape