Android.mk的深入介绍

Android.mk作为Android单模块编译的Makefile,有其独特的写法。 本文从原理、源码的角度触发,介绍Android.mk怎么写,以及如何查找更多信息。

Android的编译系统,总体来说是由Makefile写的一个项目。 而Android.mk,只是其中的一个子集。 详见此前的一篇《Android 6.0中的Makefile》。

新增Android.mk

在Android平台项目任意一个合适的目录下,创建一个Android.mk文件,都可以新增一个模块(Module)。 所谓『合适的』,是一个比较含糊的要求。

build/core/main.mk中:

subdir_makefiles := \
	$(shell build/tools/findleaves.py $(FIND_LEAVES_EXCLUDES) $(subdirs) Android.mk)

$(foreach mk, $(subdir_makefiles), $(info including $(mk) ...)$(eval include $(mk)))

其中的FIND_LEAVES_EXCLUDES,定义在build/core/config.mk中:

FIND_LEAVES_EXCLUDES := $(addprefix --prune=, $(OUT_DIR) $(SCAN_EXCLUDE_DIRS) .repo .git)

build/tools/findleaves.py中的主要逻辑如下:

def perform_find(mindepth, prune, dirlist, filename):
    result = []
    pruneleaves = set(map(lambda x: os.path.split(x)[1], prune))
    for rootdir in dirlist:
        rootdepth = rootdir.count("/")
        for root, dirs, files in os.walk(rootdir, followlinks=True):
            # prune
            check_prune = False
            for d in dirs:
                if d in pruneleaves:
                    check_prune = True
                break
            if check_prune:
                i = 0
                while i < len(dirs):
                    if dirs[i] in prune:
                        del dirs[i]
                else:
                    i += 1
            # mindepth
            if mindepth > 0:
                depth = 1 + root.count("/") - rootdepth
                if depth < mindepth:
                    continue
            # match
            if filename in files:
                result.append(os.path.join(root, filename))
                del dirs[:]
    return result

其中,prune就是FIND_LEAVES_EXCLUDES传入的--prune=列表, 而mindepth,则是默认值-1

三段代码组合起来,可以看出查找Android.mk的逻辑:

  1. 排除.repo.git和产物输出目录(默认为out)。 SCAN_EXCLUDE_DIRS默认为空,但可以通过参数传入、修改产品配置等方式设置。
  2. 查找目录深度不限。
  3. 如果父目录已经有Android.mk,不再查找子目录。

(不得不说一句题外话,代码写得真是不咋滴。)

使新增模块参与编译

满足上述逻辑的Android.mk,都会被include到Android的Makefile中。 但是,其中定义的模块是否参与编译,则未必。

相关代码,都在build/core/main.mk中。

modules_to_install := $(sort \
    $(ALL_DEFAULT_INSTALLED_MODULES) \
    $(product_FILES) \
    $(foreach tag,$(tags_to_install),$($(tag)_MODULES)) \
    $(CUSTOM_MODULES) \
  )

可以看到,可以被安装到手机ROM的模块,有四种来源。

  1. ALL_DEFAULT_INSTALLED_MODULES,从字面意思理解,系统默认的那些模块。
  2. product_FILES,产品指定。 通过在产品配置中,指定PRODUCT_PACKAGES,可以进入这个列表。
  3. tags_to_install,指定Tag。 比如,Tag为debug的模块,只在eng或userdebug的编译模式下才编译,在user模式下不编译。
  4. CUSTOM_MODULES,自定义模块。 从实现上来讲,其实也是通过Tag来添加,但主要是支持mmmmm这种单模块编译。 在模块未被添加到完整编译中时,在该模块的目录下执行mm,仍然可以强制编译,就是通过这里来指定的。

实际上,代码中的情况相当复杂,不像这里看上去这么简单。 不过,复杂的代码就不再进一步剖析了,上述理解与真实情况差别并不大。 四种来源的模块,是有重复的。 而Makefile的$(sort list),不仅可以排序,还可以去重。

修改ALL_DEFAULT_INSTALLED_MODULES需要改核心Makefile,通常不考虑。 而CUSTOM_MODULES是为单模块编译提供支持,也不考虑。 第三种方法是指定LOCAL_MODULE_TAGS,利用Tag来参与编译。 比如,只在eng下编译,可以指定LOCAL_MODULE_TAGS := eng。 原本可以通过指定Tag为user,参与全部类型的编译,但是已经被禁用了。

所以,平台开发者要想把已经准备好Android.mk的模块,添加到编译中,现在只剩指定PRODUCT_PACKAGES一种办法。 每个产品都改一次,虽然略显麻烦,但与做产品的逻辑,恰好吻合。

如何写Android.mk

Android.mk本质上就是用Makefile写的配置文件。 与普通Makefile不同的是,它相当于是Android编译系统的一个子系统。 配置简单,可读性高,但若要凭空手写,基本没可能。

以下以packages/apps/Settings/Android.mk的内容为例,说明Android.mk的配置方式。 这是系统设置的源码,在系统应用中具有一定的代表性。

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_JAVA_LIBRARIES := bouncycastle conscrypt telephony-common ims-common
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 android-support-v13 jsr305

LOCAL_MODULE_TAGS := optional

LOCAL_SRC_FILES := \
        $(call all-java-files-under, src) \
        src/com/android/settings/EventLogTags.logtags

LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res

LOCAL_PACKAGE_NAME := Settings
LOCAL_CERTIFICATE := platform
LOCAL_PRIVILEGED_MODULE := true

LOCAL_PROGUARD_FLAG_FILES := proguard.flags

ifneq ($(INCREMENTAL_BUILDS),)
    LOCAL_PROGUARD_ENABLED := disabled
    LOCAL_JACK_ENABLED := incremental
endif

include frameworks/opt/setupwizard/navigationbar/common.mk
include frameworks/opt/setupwizard/library/common.mk
include frameworks/base/packages/SettingsLib/common.mk

include $(BUILD_PACKAGE)

# Use the following include to make our test apk.
ifeq (,$(ONE_SHOT_MAKEFILE))
include $(call all-makefiles-under,$(LOCAL_PATH))
endif

Android.mk中,可以放置任意多个模块,这里只有一个模块。 对一个模块来说,大致可以分为开头清理、指定模块变量与调用编译文件三部分。

开头清理

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

include $(CLEAR_VARS),其实就是调用build/core/clear_vars.mk文件。 这个文件中,把单模块编译中,系统可能用到变量全部清理一遍。 如果想知道单模块编译时,可以指定哪些模块变量LOCAL_*,可以查看该文件。

LOCAL_PATH不是系统支持的模块变量,而是一个约定俗成的自定义变量,所以可以在调用CLEAR_VARS前指定。 其它自定义变量,不推荐使用LOCAL_作为前缀,防止和模块变量雷同。 my-dir是在build/core/definitions.mk定义的函数,通过Android.mk的位置来计算模块的路径。 如果想知道其它的常用函数,可以查看该文件。

由于Makefile天生的问题,不支持命名空间。 在需要反复使用一组变量时,要么给每一组变量不同的命名,要么必须反复初始化。 这里,Android使用的方法是后者。 所以在开始编译一个新模块前,必须清理模块变量LOCAL_*,以免受上一模块的干扰。

接下来,就可以开始指定这些变量。

指定模块变量

对于一个Android模块来说,最重要的是名称、AndroidManifest.xml、源码、资源文件、编译依赖。

LOCAL_MODULE就是模块名,不能和既有模块相同。 如果该变量未设置,则使用LOCAL_PACKAGE_NAME。 如果再没有,就会编译失败。

LOCAL_MANIFEST_FILE可以指定AndroidManifest.xml的位置。 如果未设置,则默认使用Android.mk的相同路径下的AndroidManifest.xml文件。

LOCAL_SRC_FILES是指定源文件列表。 这里用$(call all-java-files-under, src)来指定大部分文件。 all-java-files-under也是定义在definitions.mk中的函数。

LOCAL_RESOURCE_DIR是指定资源文件的目录。 LOCAL_JAVA_LIBRARIESLOCAL_STATIC_JAVA_LIBRARIES,则指定了所依赖的共享与静态Java模块。

除了这些比较核心的变量以外,还有几十个其它变量可以按需指定。

调用编译文件

include $(BUILD_PACKAGE)

调用build/core/package.mk文件,执行APK的编译。 前面指定的模块变量LOCAL_*,都是为这一句而准备。

这些用来给Android.mk调用的文件及其变量,定义在build/core/config.mk中。

BUILD_COMBOS:= $(BUILD_SYSTEM)/combo

CLEAR_VARS:= $(BUILD_SYSTEM)/clear_vars.mk
BUILD_HOST_STATIC_LIBRARY:= $(BUILD_SYSTEM)/host_static_library.mk
BUILD_HOST_SHARED_LIBRARY:= $(BUILD_SYSTEM)/host_shared_library.mk
BUILD_STATIC_LIBRARY:= $(BUILD_SYSTEM)/static_library.mk
BUILD_SHARED_LIBRARY:= $(BUILD_SYSTEM)/shared_library.mk
BUILD_EXECUTABLE:= $(BUILD_SYSTEM)/executable.mk
BUILD_HOST_EXECUTABLE:= $(BUILD_SYSTEM)/host_executable.mk
BUILD_PACKAGE:= $(BUILD_SYSTEM)/package.mk
BUILD_PHONY_PACKAGE:= $(BUILD_SYSTEM)/phony_package.mk
BUILD_HOST_PREBUILT:= $(BUILD_SYSTEM)/host_prebuilt.mk
BUILD_PREBUILT:= $(BUILD_SYSTEM)/prebuilt.mk
BUILD_MULTI_PREBUILT:= $(BUILD_SYSTEM)/multi_prebuilt.mk
BUILD_JAVA_LIBRARY:= $(BUILD_SYSTEM)/java_library.mk
BUILD_STATIC_JAVA_LIBRARY:= $(BUILD_SYSTEM)/static_java_library.mk
BUILD_HOST_JAVA_LIBRARY:= $(BUILD_SYSTEM)/host_java_library.mk
BUILD_DROIDDOC:= $(BUILD_SYSTEM)/droiddoc.mk
BUILD_COPY_HEADERS := $(BUILD_SYSTEM)/copy_headers.mk
BUILD_NATIVE_TEST := $(BUILD_SYSTEM)/native_test.mk
BUILD_NATIVE_BENCHMARK := $(BUILD_SYSTEM)/native_benchmark.mk
BUILD_HOST_NATIVE_TEST := $(BUILD_SYSTEM)/host_native_test.mk

BUILD_SHARED_TEST_LIBRARY := $(BUILD_SYSTEM)/shared_test_lib.mk
BUILD_HOST_SHARED_TEST_LIBRARY := $(BUILD_SYSTEM)/host_shared_test_lib.mk
BUILD_STATIC_TEST_LIBRARY := $(BUILD_SYSTEM)/static_test_lib.mk
BUILD_HOST_STATIC_TEST_LIBRARY := $(BUILD_SYSTEM)/host_static_test_lib.mk

BUILD_NOTICE_FILE := $(BUILD_SYSTEM)/notice_files.mk
BUILD_HOST_DALVIK_JAVA_LIBRARY := $(BUILD_SYSTEM)/host_dalvik_java_library.mk
BUILD_HOST_DALVIK_STATIC_JAVA_LIBRARY := $(BUILD_SYSTEM)/host_dalvik_static_java_library.mk

根据以上代码,整理相关信息为以下表格。 其中,加粗的几个是自定义模块里比较常用的变量。 说明中的主机是指编译Android的机器,而设备是指安装Android的机器。

include 变量 Makefile 说明
BUILD_HOST_STATIC_LIBRARY host_static_library.mk 编译主机上的静态库。
BUILD_HOST_SHARED_LIBRARY host_shared_library.mk 编译主机上的共享库。
BUILD_STATIC_LIBRARY static_library.mk 编译设备上的静态库。
BUILD_SHARED_LIBRARY shared_library.mk 编译设备上的共享库。
BUILD_EXECUTABLE executable.mk 编译设备上的可执行文件。
BUILD_HOST_EXECUTABLE host_executable.mk 编译主机上的可执行文件。
BUILD_PACKAGE package.mk 编译APK文件。
BUILD_PHONY_PACKAGE phony_package.mk 定义一个不能实际编译的虚假模块。可以指定依赖。
BUILD_HOST_PREBUILT host_prebuilt.mk 处理一个或多个主机上使用的已编译文件,该文件的实现依赖 multi_prebuilt.mk。
BUILD_PREBUILT prebuilt.mk 处理一个已经编译好的文件( 例如Jar包)。
BUILD_MULTI_PREBUILT multi_prebuilt.mk 处理一个或多个已编译文件,该文件的实现依赖prebuilt.mk。
BUILD_JAVA_LIBRARY java_library.mk 编译设备上的共享Java库。
BUILD_STATIC_JAVA_LIBRARY static_java_library.mk 编译设备上的静态Java库。
BUILD_HOST_JAVA_LIBRARY host_java_library.mk 编译主机上的共享Java库。
BUILD_DROIDDOC droiddoc.mk 编译生成droiddoc或javadoc文件。
BUILD_COPY_HEADERS copy_headers.mk 复制相关的头文件,被static_library.mk等文件使用,通常不直接include。
BUILD_NATIVE_TEST native_test.mk 编译设备上的可执行文件测试。
BUILD_NATIVE_BENCHMARK native_benchmark.mk 添加libbenchmark,编译设备上的可执行文件。
BUILD_HOST_NATIVE_TEST host_native_test.mk 主机上的可执行文件测试。
BUILD_SHARED_TEST_LIBRARY shared_test_lib.mk 设备上共享库的测试。
BUILD_HOST_SHARED_TEST_LIBRARY host_shared_test_lib.mk 主机上共享库的测试。
BUILD_STATIC_TEST_LIBRARY static_test_lib.mk 设备上静态库的测试。
BUILD_HOST_STATIC_TEST_LIBRARY host_static_test_lib.mk 主机上静态库的测试。
BUILD_NOTICE_FILE notice_files.mk 追踪、生成版权相关的NOTICE文件。
BUILD_HOST_DALVIK_JAVA_LIBRARY host_dalvik_java_library.mk 编译主机上的Dalvik共享Java库。
BUILD_HOST_DALVIK_STATIC_JAVA_LIBRARY host_dalvik_static_java_library.mk 编译主机上的Dalvik静态Java库。

其它

在Settings这个模块的Android.mk中,还有几个有趣的地方。

编译额外模块

# Use the following include to make our test apk.
ifeq (,$(ONE_SHOT_MAKEFILE))
include $(call all-makefiles-under,$(LOCAL_PATH))
endif

mm这类单模块编译的情况下,再额外include当前目录下的所有其它Android.mk,比如测试APK。

增量编译

ifneq ($(INCREMENTAL_BUILDS),)
    LOCAL_PROGUARD_ENABLED := disabled
    LOCAL_JACK_ENABLED := incremental
endif

在增量编译的情况下,禁用混淆、启用Jack工具链的增量编译功能。

引用其它mk文件

include frameworks/opt/setupwizard/navigationbar/common.mk
include frameworks/opt/setupwizard/library/common.mk
include frameworks/base/packages/SettingsLib/common.mk

引用这三个文件,目的是添加额外的资源文件、编译依赖与aapt参数。 这是一种比较高明的方式来使用自定义的模块。

总结

Android.mk是Android的编译系统中,独立模块的编译配置文件。 优点突出,兼容Makefile语法、可读性也还不错,代码量也不太高。 缺点也很明显,好读不好写,不过好在有很多既有文件可以借鉴。

在7.0以后的项目,开始逐渐使用更简洁的Android.bp,利用blueprint转换成Ninja,参加编译。

参考


相关笔记