How AOSP builds
The platform build is a different universe from your app build: Make gave way to Soong, Soong feeds Ninja, and Android.bp replaced Android.mk. It is the largest source tree most people will ever compile.
./gradlew assembleDebug builds one app. m builds an operating system: hundreds of gigabytes of
source, thousands of modules, and a graph that has been re-architected three times to keep it
buildable.
TL;DR#
- AOSP historically used Make (
Android.mk). It became unmaintainably slow on a tree this large. - Kati is a Make clone that translates Makefiles into Ninja files; Soong parses
Android.bpand also emits Ninja. - Ninja does the actual execution: it is a fast, dumb executor of a graph someone else computed.
lunchselects a product, a device and a build variant, producing the environment thatmuses.Android.bpis deliberately not a programming language: no conditionals, no shell, no arbitrary logic. That restriction is the feature.- aconfig adds feature-flag-style build-time configuration for platform code.
Mental model#
Think of building a city rather than a house.
Soong and Kati are the city planners. They read the zoning rules — every Android.bp and
Android.mk — and produce a blueprint: this building needs these materials, which come from that
factory, which needs power from that substation.
Ninja is the construction crew. It does not plan. It receives a complete blueprint and executes it as fast as possible, in parallel, skipping anything already built.
lunch is choosing which city to build: which product (a phone, a TV, a car head unit), which
device tree, and which variant (user, userdebug, eng).
The build target decides what gets included at all: a product's PRODUCT_PACKAGES list is the
manifest of what the city contains.
Where the analogy breaks#
A city has one plan. AOSP has two planners that must agree, because the tree contains both
Android.mk and Android.bp modules and they can depend on each other. Soong can call back into
Kati for unconverted modules, and the resulting Ninja graph is a single merged plan produced by two
different programs. That hybrid is the actual state of the tree, not a transitional detail.
The big picture#
Text description of this diagram
A comparison of Make and Soong across eight aspects. File: Android.mk versus Android.bp. Syntax: Make variables, functions and conditionals versus JSON-like name-equals-value declarations. Conditionals: allowed anywhere in Make, not allowed in Soong, which uses select or variants instead. Evaluation: Kati, a Make clone that emits Ninja, versus soong_ui with a Go parser that emits Ninja directly. Error timing: often only when the recipe runs, versus at parse time before Ninja starts. Readability: order-dependent shell recipes versus declarative independent modules. Legacy integration: Make holds every legacy module, while Soong can call back into Make. Status: Make remains for legacy and some vendor code, Soong is the default for platform code.
Toggle between the two columns. The row that matters most is error timing: Soong catches most mistakes before Ninja runs a single command, while a Make-based build could fail twenty minutes in because a variable was empty.
Deep dive#
Why Make stopped working#
Make is a fine build system for a few hundred files. AOSP is a few hundred thousand. Three problems compound:
- Make evaluates everything. Every
Android.mkis parsed and its variables expanded, for every build, whether or not anything in it is needed. - Make is untyped. A typo in a variable name expands to the empty string, and the failure surfaces much later as a missing file.
- Make has no module model. Android needed "a shared library with these sources, these headers, these dependencies, installed to this partition" as a first-class concept.
Soong was the answer: a Go program with a declarative input format and a real module model.11
Android.bp: a data file, not a script#
Here is a real-shaped module definition:
cc_library_shared {
name: "libexample",
srcs: [
"example.cpp",
"helper.cpp",
],
shared_libs: [
"libbase",
"liblog",
],
cflags: [
"-Wall",
"-Werror",
],
vendor_available: true,
}There is no if. There is no shell. That is not a limitation someone forgot to fix; it is the
design. A declarative file can be parsed, validated, and reasoned about without executing anything,
which is what makes parse-time errors possible.
When you genuinely need conditional logic, Soong gives you select() over a named configuration, or
you define two modules and choose between them in the product. Both are explicit, and both are
visible to the build system.
Kati and Ninja#
Kati is a Make implementation that reads Android.mk files and emits Ninja. It exists so the
hundreds of unconverted modules keep building while the tree migrates. It is faster than GNU Make
for this workload, and, crucially, it produces the same kind of output Soong does.
Ninja executes the combined graph. It is intentionally minimal: no conditionals, no variables,
no dependency discovery.1 It reads build.ninja, checks timestamps, and runs commands in parallel.
All the intelligence is upstream.
# The generated graph Ninja executes
out/soong/build.ninja
# Ask Ninja what it would do for a target
ninja -f out/soong/build.ninja -n droidlunch: choosing what to build#
lunch selects a build target — a string of the form product-device-variant:
source build/envsetup.sh
lunch aosp_arm64-userdebug- product chooses
PRODUCT_PACKAGES, the list of what is installed. - device chooses the board configuration: kernel, partitions, HALs.
- variant chooses
user(production, no root),userdebug(debuggable, root available), oreng(engineering, extra tools).
The variant changes the build in ways that matter: userdebug enables adb root and more logging;
user strips them. It is not a compiler flag, it is a different product configuration.
aconfig: build-time feature flags#
Modern AOSP uses aconfig to declare feature flags for platform code.2 A flag is declared in a
.aconfig file with a namespace, a container and a default, and the build generates code that reads
it. The point is to ship one binary that can have features enabled by configuration rather than by
compiling a different variant.2
The Bazel question#
Google has invested in Bazel as a successor to Soong for parts of the platform, and the migration status has changed more than once. The honest summary: Soong remains the build system the tree actually uses, Bazel adoption is partial and evolving, and any specific claim about which modules have moved needs to be checked against the current tree.
Bazel has replaced Soong for a subset of AOSP modules, with the migration proceeding incrementally.
unverified(Bazel adoption in AOSP has shifted repeatedly; verify against the current source tree rather than any fixed description)Why a build system is an architecture decision#
It is tempting to treat the platform build as plumbing. It is not. The build system decides what can be a module, how a module declares its dependencies, and therefore what can be replaced independently.
Soong's module model is what made APEX possible: an APEX is a module with a declared boundary, and the build system has to know that boundary to package it. The same is true of the vendor interface: the split between system and vendor code is expressed as a difference in module declarations, not as a convention in a Makefile.
That is why a build-system migration is never just a build-system migration. Moving from Make to Soong changed how the tree describes itself, and every subsequent modularity project built on the description Soong produced.
The mental cost of a large build#
A culture point worth stating: on a tree this size, a clean build is measured in hours, and no engineer does one casually. The workflow is built around incrementality — build one module, flash one partition, iterate.
That is why m is a shell function rather than a binary, why mm builds only the current
directory, and why out/ is enormous. The build system is not optimised for the clean case; it is
optimised for the case where one file changed in a tree that has been built before. A change that
invalidates the world — a header included by thousands of files, a changed global flag — is a
noticeable event, and teams talk about it the way other teams talk about an outage.
What to read first#
The tree is intimidating, and the useful entry point is not the top-level directory listing. It is the Ninja graph and one module.
Pick a module you already understand — a small native library, or a Java library with no
dependencies — find its Android.bp, and follow it to its output in out/. Then look at what Ninja
says about that target with ninja -f out/soong/build.ninja -n <target>. The graph will name
hundreds of inputs, and every one of them is a real question about how the platform is assembled.
That is a far better introduction than reading the build system's documentation top to bottom.
How it evolved#
The pattern is the same one the app build followed: replace a general-purpose tool with a restricted, analysable one, and keep the old format working while the tree migrates.
Under the hood#
- the Soong build system itself, written in Go
- the legacy Make infrastructure, including Kati integration
- envsetup.shdefines lunch, m, mm and the other shell functions
- microfactory.bashbootstraps the Go tools that build Soong
- build_flags.bzlan example of the release configuration surface
- build.ninjathe merged Ninja graph every module ends up in
Curiosity corner#
mis a shell function, not a binary. It is defined byenvsetup.shand wrapssoong_uiwith the right arguments. Ifmis "not found", you forgot to source the environment.mmbuilds only the current directory's modules, which is the closest thing AOSP has to an incremental single-module build.- The tree is bigger than a phone's system image.
out/routinely outweighs the source checkout, because every intermediate object is kept for incremental builds.
Try it#
source build/envsetup.sh
Example output
including device/generic/goldfish/vendorsetup.sh
including ...lunch aosp_arm64-userdebug
Example output
============================================
PLATFORM_VERSION_CODENAME=REL
TARGET_PRODUCT=aosp_arm64
TARGET_BUILD_VARIANT=userdebug
============================================ninja -f out/soong/build.ninja -n droid | head -20
Example output
[1/2847] ...
[2/2847] ...Build it#
You do not need to build all of AOSP to learn this. Build one module:
source build/envsetup.sh
lunch aosp_arm64-userdebug
m libexampleWatching a single cc_library_shared go from Android.bp to a .so in out/ teaches the Soong →
Ninja relationship faster than any diagram.
Check yourself#
Check yourself
Put these build stages in order, from first to last.
Drag, or use the arrows to reorder.
- 1AAPT2 link produces resources.arsc and the R class
- 2kotlinc compiles Kotlin to JVM class files
- 3D8 converts class files to DEX
- 4apksigner writes the v2/v3 signature blocks
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsBuild Androidsource.android.comaccessed 2026-10-27↩
- docsConfiguration (aconfig) overviewsource.android.comaccessed 2026-10-27↩
- docsModular system componentssource.android.comaccessed 2026-10-27↩
- docsGradle user manualdocs.gradle.orgaccessed 2026-10-27↩