From Make Rules to Declarative Intent
How did Google orchestrate compiling millions of lines of code without the system collapsing? In the old Android.mk days, developers wrote procedural recipes for the build system. Makefiles read like shell scripts, executing commands sequentially to figure out how to compile files. This imperative approach created tangled, unmaintainable logic loops.
Soong replaces those scripts with strict declarative module types in Android.bp files. You must declare exactly what you are building from a fixed menu of module definitions. Rather than telling the build system how to compile a target, you simply state what the target is.
Instead of typing include $(BUILD_SHARED_LIBRARY) at the bottom of a file, you open a cc_library_shared block. The build system parses these typed blocks to construct a massive dependency graph in memory. This graph guarantees that all components compile in the correct order every time.
Tip: Do not expect
Android.bpfiles to run top-to-bottom like a script. Soong parses all configuration files globally to map out the dependency graph before any actual build steps begin.
This enforced discipline makes AOSP builds predictable and easily parallelizable. But enforcing discipline is only half the battle when dealing with the sheer volume of native C/C++ code required to boot a device.
Compiling Native Code: The cc_ Family
Android requires immense amounts of native code for performance and hardware interaction. Developers need a way to tell the build system whether their C++ code should run as a standalone binary or act as a reusable library. Without a clean way to define these boundaries, linking errors would plague the platform.
The cc_ family of module types handles all native compilation. You use cc_binary for executable daemons that run in the shell. The cc_library_shared module creates dynamically linked libraries (.so), while cc_library_static produces statically linked archives (.a).
A major challenge arises when multiple downstream modules need different versions of the same library. Defining separate static and shared targets manually duplicates code and risks mismatches. To solve this, Soong provides the hybrid cc_library type.
The following diagram illustrates this workflow in three steps. First, the source files feed into a single declarative node. Second, the build system processes that unified definition. Third, two distinct library formats emerge for downstream components to consume.
The diagram illustrates how one module definition shoulders the burden of dual compilation. Downstream targets simply request the library by name, and the build system routes them to the right output file.
We configure this module to expose its header files so other targets can link to it successfully.
cc_library {
name: "libcustommath",
srcs: ["math.cpp"],
export_include_dirs: ["include"],
}
By defining export_include_dirs, consumers immediately know where to find the headers without hardcoding paths. A common mistake is hardcoding include paths in every dependent module instead of letting the provider export them.
Warning: Avoid using
cc_library_sharedorcc_library_staticexclusively when creating reusable components. The hybridcc_libraryis significantly safer and provides maximum flexibility for downstream dependencies.
Native modules form the foundation of the OS architecture. However, the system cannot function on C++ alone, especially when constructing the background services and tools that keep the framework running.
Pure Java and Host Tools: The java_ Family
Not everything in Android revolves around screens and UI events. The build system must compile vast amounts of pure background logic and command-line tools. These utilities often need to execute on the developer's computer rather than the phone itself.
The java_ module family specifically handles pure Java and Kotlin compilation. The java_library type compiles source files into a standard .jar file intended for execution on the device. For build tools and code generators, java_library_host produces a .jar file that runs directly on your Ubuntu build machine.
You write a java_library_host definition when building tools that process data during the compilation phase. Think of this process like building a traditional Maven or Gradle artifact. The build system sandboxes this execution to the host machine architecture.
Common Mistake: Do not try to include Android resources (
res/) inside ajava_library. This module strictly produces standard.jarfiles and cannot process Android manifests.
Isolating host tools from device targets prevents architectural pollution. Yet pure Java stops being enough the moment your application needs to display a screen to the user or request permissions from the OS.
Apps and Resources: The android_ Family
System developers frequently need to build privileged applications that integrate deeply with the OS. Standard Java libraries cannot handle visual layouts, string translations, or OS-level permissions. You need a way to package resources and access internal framework capabilities.
The android_ module family constructs Android Packages (.apk) and Android Archives (.aar). You use android_library for reusable components that contain resources. The android_app type builds the final installable application.
Imagine an application fails to compile because it cannot find hidden framework components like ActivityManagerNative. The public SDK actively hides these internal classes. You solve this by adding the platform_apis: true flag to your module configuration.
android_app {
name: "MyCustomSettings",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
certificate: "platform",
platform_apis: true,
}
The platform_apis flag tells the build system to bypass the public SDK and link against the internal framework. We also assign the "platform" certificate to grant the application system-level privileges. Omitting this certificate often leads to frustrating permission denials at runtime.
Tip: Always include
resource_dirsand explicitly set your certificate when building system applications. These properties ensure the app receives correct visual assets and elevated permissions.
Mastering these application modules lets you build features that feel native to the OS. Occasionally, however, you will encounter edge cases where none of these standard modules quite fit the problem you are trying to solve.
Escape Hatches: Filegroups and Genrules
What happens when you need to share a hundred source files across three different modules? Suppose you must execute a custom Python script during the build. The strict declarative nature of Soong prevents you from embedding raw shell scripts directly in your configuration files.
Soong provides filegroup and genrule to solve these edge cases cleanly. A filegroup acts like an array variable, holding a list of file paths under a single recognizable name. This genrule module creates a controlled sandbox where you can safely execute custom commands without breaking the build graph.
You reference a filegroup anywhere a source list is required. This prevents you from re-typing massive arrays of file paths across multiple targets. For command execution, you map your script and inputs into a genrule definition.
The following diagram illustrates the rule sandbox in three steps. First, the build system gathers the declared input files. Second, the custom command executes against those specific inputs. Third, the script emits the requested output file for other modules.
The diagram shows how files flow cleanly through the isolated execution environment. You never run scripts blindly in the root directory.
We pass inputs to the script using built-in variables like $(in) and $(out).
genrule {
name: "generate_headers",
srcs: ["input.txt"],
out: ["output.h"],
cmd: "python $(location script.py) $(in) > $(out)",
}
The $(location) variable resolves the exact path to the script file automatically. If you hardcode paths in the command string, the build will fail when directories change.
Warning: Do not attempt to write complex bash loops directly inside the
Android.bpfile. Soong completely forbids arbitrary shell scripts unless explicitly encapsulated within agenrulesandbox.
These utility modules give developers necessary flexibility without compromising the integrity of the build. They form the final vocabulary you need to wire an entire AOSP subsystem together.
We have seen how Soong enforces a declarative configuration model to keep the build graph clean and predictable. The cc_, java_, and android_ prefixes dictate the core compilation logic for every component. When standard rules fall short, the genrule escape hatch provides a safe environment for custom scripting.
Our building blocks are clearly defined, but a real system has hundreds of modules depending on one another. How does Soong handle complex dependency chains and linking between different folders without a global Makefile?