AOSP Foundations
7 min read

Dependency Generation

Learn how Soong translates Android.bp modules into Ninja build graphs and how to debug incremental build failures.

Why Android.bp Cannot Compile Your Code

You type m to build your module. A C++ compiler requires absolute paths to function properly. The tool needs explicit include directories passed via flags. It demands a strict linking order for every shared library. Your build file deliberately hides all of these low-level details. This creates a fundamental gap between the declarative code you write and the imperative commands the compiler demands.

A translation engine must exist to bridge this gap. Think of your blueprint file as a restaurant menu. You simply declare that you want a burger. Soong acts as the chef who translates your order into a precise recipe of steps. Ninja then works as the kitchen line cook mindlessly executing that recipe exactly as written.

The build process requires two distinct phases to handle this separation:

  1. The translation engine reads the declarative file.
  2. The engine generates an imperative build script.
  3. The executor runs the generated script to create the binary.

This translation phase ends when Soong writes the build file. The execution phase begins when Ninja reads the file into memory.

Tip: Engineers often assume the m command functions as a compiler wrapper. It actually coordinates Soong to write a giant execution script before telling Ninja to run that script.

Consider a simple three-line module definition. You declare the module name and the source file. Soong translates this into a massive command inside the execution script. The resulting command contains every absolute path and compiler flag required to generate the object file. This established translation step sets up how Soong figures out the implicit instructions your module needs.

Injecting Implicit Dependencies with Soong Mutators

You define a basic application module in your build file. Developers rarely explicitly declare a dependency on the core Java framework. Yet your application compiles perfectly and finds all the standard classes. A mechanism must be silently injecting these required dependencies into your build graph.

Soong accomplishes this using mutators during a strict processing pipeline. The engine first parses your source tree to build a basic in-memory graph. Mutators then modify this graph by automatically injecting implicit dependencies based on your module type.

The engine processes these dependencies through a specific sequence:

  1. The parser builds a basic graph from the source files.
  2. Mutators inject missing requirements into the graph.
  3. The resolution phase verifies every requested module exists.

This pipeline heavily alters the initial graph state. The process finishes by generating the final execution script.

Look at the standard application module type. You write the module name and nothing else. A mutator sees this specific type and silently adds a dependency on the core runtime libraries. If that mutator failed to run, the build would immediately crash because the compiler could not find standard base classes.

Warning: Soong performs magic behind the scenes via mutators. This helpful automation makes debugging confusing when a dependency you did not request fails to resolve.

The engine successfully generates the massive output script containing all explicit and implicit requirements. Our next challenge involves how Ninja uses this file to build only the modules that actually changed.

How Ninja Tracks File Changes Without Hashing

You modify a single file in a massive codebase. The build system must identify exactly what needs recompilation without analyzing millions of files. Evaluating content hashes for every file would consume enormous amounts of processing time. The system needs a faster mechanism to determine the incremental build state.

Ninja solves this by rigorously tracking file modification timestamps. It compares the last modified time of the input source file against the generated output target. The system treats file timestamps like a baker checking ingredient expiration dates. If an open bag of flour is newer than the pre-baked cake sitting on the shelf, the baker knows the cake is stale and must bake a fresh one.

This evaluation process relies on a precise logic flow:

  1. The executor reads the input source timestamp.
  2. It compares the input time against the output target time.
  3. It decides whether to trigger the compiler or skip the step.

The decision tree runs continuously during the execution phase. Ninja reads the rule from the execution script and immediately decides the next action.

Suppose you modify a source file at ten in the morning. Ninja reads the generated script and sees this file listed as an input for a shared library. The shared library was built five minutes earlier. Because the input timestamp is newer than the target, Ninja triggers the compiler to rebuild the library.

Performance Note: Developers often assume AOSP uses content hashing for local incremental builds. Hashing proves computationally expensive for this massive scale. Timestamp comparison is deliberately chosen for raw speed.

This timestamp mechanism works perfectly for standard source files. Custom scripts can break this system completely if they are not tracked correctly.

The Invisible Dependency Bug in Custom Rules

You write a custom Python script to generate a C++ header file. Updating the logic inside the script triggers a new build. Ninja skips the compilation entirely. Your subsequent testing reveals that your C++ code is still running the old logic.

This happens because the execution graph does not automatically know that your script dictates the output. You must explicitly track the script itself as a dependency within your blueprint module. Ninja only watches the files you tell it to watch.

Suppose you use a script to create your header files. Developers often list the raw data files as inputs but forget to add the Python script to the tool files list. You update the Python script logic later that day. Ninja checks the timestamp of the existing header file, sees it has not changed, and skips the build.

Warning: Omitting a generation script from the dependency list causes infuriating invisible logic bugs. The build reports success while leaving your generated files dangerously outdated.

You spend hours debugging C++ code that the compiler never actually touched. The fix requires explicitly declaring the script as an input in the blueprint file. Once declared, Ninja watches the script timestamp and triggers a rebuild whenever you modify the generation logic. When these invisible dependency bugs happen too often, your local build state becomes hopelessly corrupt. This requires knowing how to force a clean slate.

Nuking the Graph: When to Clear the Soong Cache

Your build fails with an error claiming a generated file does not exist. You verify the file is clearly present in the correct output directory. Your code is perfectly fine, but the build system refuses to acknowledge reality. The intermediate dependency graph has become corrupted.

You must surgically reset the build state without destroying hours of compiled binaries. The translation engine stores its intermediate parsing data in a specific cache directory.

The following command solves graph corruption by destroying the intermediate state. It removes the specific cache directory to force a clean parsing phase. Your terminal will return silently on success.

rm -rf out/soong

Engineers often mistakenly run a full clobber command when fixing graph issues. This oversight unnecessarily destroys every compiled binary in the output directory.

Tip: Deleting the intermediate cache is a surgical strike. It saves hours of recompilation while fixing graph corruption compared to a full clobber.

You now understand how the build system generates the dependency graph and resolves implicit requirements. Soong successfully writes a massive execution script containing exact compiler commands. Executing these commands sequentially would take hours. How does Kati and Ninja orchestrate this massive graph across all your CPU cores without deadlocking? That is what the next article covers.