Zygote
The build systemnerd

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.

11 minAPI 1–37 · 17interactivequizverified 2026-10-27

./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.bp and also emits Ninja.
  • Ninja does the actual execution: it is a fast, dumb executor of a graph someone else computed.
  • lunch selects a product, a device and a build variant, producing the environment that m uses.
  • Android.bp is 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#

Loading diagram…
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.mk is 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 droid

lunch: 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), or eng (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#

2008–2015MakeAndroid.mk everywhere; a single GNU Make graph
2015–nowSoong + KatiAndroid.bp for new code, Kati for the legacy tree
2021–nowaconfig + APEXfeature flags and independently updatable modules

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.

Mainline modules3 releases
APIVersionWhat changed
289 Pie
  • Mainline groundwork
2910
  • Project Mainline: APEX and updatable system modules
  • Project Mainline starts shipping through Play
3011
  • GKI: the Generic Kernel Image splits the kernel into core + vendor modules

Under the hood#

Curiosity corner#

  • m is a shell function, not a binary. It is defined by envsetup.sh and wraps soong_ui with the right arguments. If m is "not found", you forgot to source the environment.
  • mm builds 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#

Set up an AOSP shell

source build/envsetup.sh

Example output
including device/generic/goldfish/vendorsetup.sh
including ...
Pick a target

lunch aosp_arm64-userdebug

Example output
============================================
PLATFORM_VERSION_CODENAME=REL
TARGET_PRODUCT=aosp_arm64
TARGET_BUILD_VARIANT=userdebug
============================================
Ask Ninja what building droid would do

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 libexample

Watching 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

1 / 12

Put these build stages in order, from first to last.

Drag, or use the arrows to reorder.

  1. 1AAPT2 link produces resources.arsc and the R class
  2. 2kotlinc compiles Kotlin to JVM class files
  3. 3D8 converts class files to DEX
  4. 4apksigner writes the v2/v3 signature blocks
0 answered · 0 correct

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.

  1. docsBuild Androidsource.android.comaccessed 2026-10-27↩
  2. docsConfiguration (aconfig) overviewsource.android.comaccessed 2026-10-27↩
  3. docsModular system componentssource.android.comaccessed 2026-10-27↩
  4. docsGradle user manualdocs.gradle.orgaccessed 2026-10-27↩