AOSP Foundations
6 min read

Advanced Soong Build Architecture

Explore advanced Soong features, including static dependencies, reusable defaults modules, and visibility rules.

Building a custom Android image requires coordinating thousands of interconnected C++ libraries, daemons, and applications. Managing that complexity manually with traditional Makefiles and compiler flags quickly becomes impossible at an operating system scale. The modern AOSP build system solves this by treating configuration as a rigid, interconnected graph.

Why You Never Write Linker Flags in AOSP

Linking C/C++ code normally involves a mess of -L library paths and -I header includes. Imagine doing that across tens of thousands of modules in AOSP. Keeping track of those paths manually would stall development entirely.

Soong handles dependencies semantically using module names. The build system acts like a package manager for C/C++ code. You state the dependency name, and Soong resolves the exact file paths, fetches the headers, and links the binaries.

You declare dependencies in Android.bp arrays like shared_libs, static_libs, and header_libs. Shared libraries link dynamically against your component. Static libraries embed their binary code directly into the final output. Header modules provide compile-time interface paths without contributing compiled code.

Before looking at code, let us visualize how these different library declarations affect the final compiled artifact. This clarifies why distinguishing between static and header libraries matters.

The camera daemon embeds the static library code entirely. Shared libraries provide dynamic linking, while the header library only provides include paths during compilation.

cc_binary {
    name: "my_camera_daemon",
    srcs: ["daemon.cpp"],
    shared_libs: ["libcamera_metadata"],
    static_libs: ["libmath_custom"],
}

Common Mistake: Beginners often try to list .so or .a file paths directly inside the srcs array. Always use module names in the library arrays instead.

Transitive dependency resolution means you never worry about the absolute path of a header file again. Now that we have multiple modules linked semantically, we need a way to manage identical compiler flags across dozens of them without copying and pasting.

Eliminating Copy Paste with Defaults Modules

Imagine your team needs to enforce strict C++ warnings across a hundred different microservices. Manually copying -Wall and -Werror into every single Android.bp file invites inconsistency. A single missed file breaks your build compliance.

Blueprint provides the cc_defaults module type to solve this exact configuration nightmare. Think of a defaults module as a base class in object-oriented programming. Real modules inherit from it and can append their own specific properties.

You define a cc_defaults block with your shared flags. Other modules then reference it using the defaults property.

This diagram shows how multiple independent applications inherit configuration from a single centralized template. Structural inheritance prevents configuration drift across a massive codebase.

The standard flags define the base configuration once. Both applications pull those exact properties in automatically during the build phase.

cc_defaults {
    name: "my_company_standard_flags",
    cflags: [
        "-Wall",
        "-Werror",
        "-O3",
    ],
    shared_libs: ["liblog"],
}

cc_binary {
    name: "app_one",
    defaults: ["my_company_standard_flags"],
    srcs: ["app1.cpp"],
}

Tip: Defaults modules are strictly templates. They produce no build artifacts on their own.

Centralized configuration makes large-scale refactoring trivial. Sharing code and configuration globally is powerful, but sometimes in a massive OS, you must explicitly prevent modules from sharing internal code.

Locking Down Access with Visibility Rules

AOSP builds everything in a single massive tree. By default, any module can link against any other module. If you write a sensitive cryptographic component, you do not want an untrusted vendor app linking it dynamically.

Soong implements access control directly in the build graph through the visibility property. This feature acts exactly like Java access modifiers applied at the module level. Such enforcement establishes architectural security boundaries at compile time.

You add the visibility array to a module definition and list the exact paths permitted to link against it. The Soong parser halts the build immediately if an unauthorized module declares a dependency.

We can map out authorized versus unauthorized linking attempts to visualize architectural enforcement. This shows how Soong protects sensitive components before the compiler even runs.

The authentication daemon successfully links to the crypto library because the visibility rules authorize its path. Unauthorized components like the Settings app receive a fatal parser error because they fall outside the visibility rules.

cc_library {
    name: "libsecret_crypto",
    srcs: ["crypto.cpp"],
    visibility: [
        "//system/security/auth_daemon",
    ],
}

Warning: Setting visibility to //visibility:private only applies to the exact directory where the Android.bp resides. The restriction does not cascade down to subdirectories.

Secure design requires strict boundaries, and Soong ensures those boundaries exist in the build graph itself. These modern Blueprint features are incredibly powerful, but to build AOSP today, they still must coexist with ten years of legacy Makefiles.

Bridging the Gap Between Soong and Make

Google introduced Soong years ago, but thousands of legacy hardware drivers still use Android.mk files. You cannot rewrite every vendor module overnight. Modern Blueprint modules must find a way to interact with legacy Make modules.

Kati is the architectural bridge that enables interoperability between Soong and Make. This tool translates Makefiles into Ninja build rules so the two systems merge. A hybrid setup lets an Android.bp module depend on an Android.mk library safely.

You reference legacy module names in Blueprint arrays just as you would modern modules. If you want to migrate a legacy file permanently, the build system includes a conversion tool called androidmk.

The following diagram illustrates how the build system merges modern and legacy configurations. This architecture reveals Kati's role in creating a unified execution graph.

Soong parses Blueprint files while Kati translates Makefiles. Both tools feed their output directly into a single Ninja graph for the final build phase.

androidmk Android.mk > Android.bp

Tip: The androidmk tool provides a best-effort conversion. Complex Make logic might require manual tuning after generation.

This hybrid build graph lets Android push forward cleanly without breaking legacy vendor code.

Semantic dependency management handles linking automatically, replacing manual linker flags. Defaults modules eliminate configuration boilerplate entirely. Visibility rules lock down secure boundaries within the build graph itself. Kati ensures everything remains backward compatible during the long migration away from Makefiles.

Now that you can manage a massive C++ codebase efficiently, how do we plug our own custom hardware abstraction layer into this build system without breaking the rules?