A simple pattern for creating custom AOSP build modules

Here's how you can create custom build modules that play nice with AOSP.

Preetam D'SouzaApr 10, 2021

We recently had an interesting user request for our open-source project Maru that I think will be educational to explain here for anyone interested in customizing the Android Open Source Project (AOSP).

For those of you unfamiliar with Maru, it's software that turns your phone into a PC. Maru is built on top of AOSP, and enables AOSP to run lightweight system containers that can run fully interactive desktop environments like Linux.

As part of Maru's build process, we copy over a prebuilt desktop root filesystem image to the target device image. In Android makefile speak, it looks like this:

PRODUCT_COPY_FILES += \
    $(LOCAL_PATH)/prebuilts/desktop-rootfs.tar.gz:system/maru/containers/default/rootfs.tar.gz

A user who wanted to run a custom build informed us that he would like to specify his own prebuilt desktop image in his custom build directory, rather than using our default image.

Taking inspiration from how Lineage OS handles overriding the TARGET_BOOTANIMATION build variable to set a device's custom boot animation, we decided to introduce a variable called TARGET_DESKTOP_ROOTFS that users can override to specify the path to their own rootfs image.

The problem is, with AOSP's default build script for prebuilt modules, which uses LOCAL_SRC_FILES and $(BUILD_PREBUILT), we can't specify files outside of our current module directory. To get around this, we had to customize our build recipe.

Here is what our final Android.mk ended up looking like:

# This is the current relative path from the AOSP workspace root directory.
LOCAL_PATH := $(call my-dir)

# TARGET_DESKTOP_ROOTFS can be set in vendor makefiles to override the default
# desktop rootfs image for Maru. Note that the path must be relative to the
# AOSP workspace root directory, which can easily be done by prefixing the path
# with $(LOCAL_PATH).
ifeq ($(TARGET_DESKTOP_ROOTFS),)
  TARGET_DESKTOP_ROOTFS := $(LOCAL_PATH)/desktop-rootfs.tar.gz
endif

# Here is our module declaration.
include $(CLEAR_VARS)
LOCAL_MODULE := rootfs.tar.gz
LOCAL_MODULE_TAGS  := optional
LOCAL_MODULE_CLASS := ETC
LOCAL_MODULE_PATH := $(TARGET_OUT)/maru/containers/default

# Instead of using LOCAL_SRC_FILES and $(BUILD_PREBUILT), we explicitly set up
# the rule ourselves so that we can copy a rootfs path from outside of this
# directory if a vendor overrides TARGET_DESKTOP_ROOTFS.
include $(BUILD_SYSTEM)/base_rules.mk
$(LOCAL_BUILT_MODULE): $(TARGET_DESKTOP_ROOTFS)
	@mkdir -p $(dir $@)
	@cp $(TARGET_DESKTOP_ROOTFS) $@

Note how we specify our own custom make rule using $(LOCAL_BUILT_MODULE), rather than using the standard $(BUILD_PREBUILT) script. Now we can specify any custom commands we want to run. For our module, we just had to copy over the prebuilt image specified by $(TARGET_DESKTOP_ROOTFS), but you can run whichever commands you'd like.

This approach plays nice with AOSP and will let you depend on your custom modules just like any other stock AOSP module:

PRODUCT_PACKAGES += rootfs.tar.gz

In summary, if you need to customize your build beyond what AOSP's standard build scripts offer, you can do so by creating a custom rule with the target $(LOCAL_BUILT_MODULE), and specifying your custom build commands in that rule.

I hope you find this pattern useful in your future AOSP hacks!

Further Reading

Like this article?

Subscribe to receive our latest articles in your inbox.