尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Android构建系统演进:从Android.mk到Android.bp的全面解析与迁移指南

Android构建系统演进:从Android.mk到Android.bp的全面解析与迁移指南 1. 项目概述从Makefile到Blueprint的构建革命如果你在Android源码里泡过一段时间尤其是AOSPAndroid Open Source Project的深度定制开发那你一定对Android.mk文件又爱又恨。爱的是它功能强大恨的是它语法复杂、构建缓慢。大约从Android 7.0Nougat开始一个名为Android.bp的新面孔悄然出现并在后续版本中逐渐成为构建系统的绝对主角。今天我们就来彻底拆解这个Android.bp文件它到底是什么为什么Google要“另起炉灶”以及我们该如何驾驭它。简单来说Android.bp是Android构建系统Soong取代了老的Make所使用的配置文件其全称是“Blueprint”。它的设计目标非常明确更简单、更快、更可扩展。与Android.mk基于Makefile的复杂逻辑和shell命令不同Android.bp采用一种声明式的、类似于JSON的简洁语法。你只需要告诉构建系统“我要构建什么”比如一个二进制文件、一个库以及“构建它需要什么”比如源码、依赖、编译标志系统就会自动推导出如何构建极大地简化了开发者的心智负担并提升了构建速度。这篇文章适合所有需要接触Android系统底层、进行ROM定制、系统应用开发或驱动开发的工程师。无论你是刚刚从应用层转向系统开发还是已经对Android.mk了如指掌的老手理解Android.bp都是深入Android构建体系的必修课。接下来我会从设计理念、语法详解、实战迁移、到高级技巧和避坑指南带你完整走一遍。2. Android.bp核心设计理念与语法全解2.1 为什么是Blueprint与Android.mk的彻底决裂要理解Android.bp必须先明白它要解决Android.mk的哪些痛点。我在早期做系统定制时一个模块的Android.mk动辄上百行是常事里面充满了条件判断、函数调用、路径拼接和shell命令。这带来了几个严重问题构建速度慢Make需要递归地解析整个Android.mk树执行大量的shell命令进程fork开销巨大。可读性差复杂的逻辑和宏定义让配置文件本身难以理解和维护。并行构建困难Make的隐式依赖关系难以精确分析影响了构建的并行度。测试困难构建逻辑与shell/环境变量深度耦合难以进行单元测试。Soong和Android.bp的诞生就是为了用一套全新的、专门为Android大规模源码构建设计的系统来根治这些问题。它的核心思想是“声明式配置”和“构建图分析”。声明式配置你在Android.bp里写的是纯粹的“数据”描述模块的属性而不是“脚本”或“命令”。比如你写srcs: [foo.cpp, bar.cpp]而不是写一段循环去找.cpp文件。构建系统Soong负责读取所有这些声明在内存中构建一个完整的模块依赖关系图。构建图分析Soong分析这个图计算出最优的、可并行化的构建动作序列然后调用底层的编译工具链如Ninja去高效执行。Ninja是一个专注于速度的小型构建系统它只做一件事以最快的速度执行构建命令。所以Android.bp本身并不执行任何构建操作它只是一个高级的、面向领域的描述语言DSL。这种架构分离了“做什么”Blueprint和“怎么做”SoongNinja使得构建系统更清晰、更健壮、也更快。2.2 语法入门从零开始写一个Android.bpAndroid.bp的语法极其简洁它既不是JSON也不是严格的Go语言而是一种自定义格式。基本规则如下模块定义每个要构建的目标库、二进制、包等都是一个“模块”。模块以模块类型开头后跟模块名和一对花括号{}。cc_binary { // 模块类型C/C可执行文件 name: my_app, // 模块名在整个构建系统中必须唯一 // ... 其他属性 }属性赋值在花括号内使用属性名: 值的格式进行赋值。值可以是字符串、字符串列表、布尔值、整数或映射map。cc_library { name: my_lib, srcs: [src1.cpp, src2.cpp], // 字符串列表 cflags: [-Wall, -O2], // 字符串列表 shared_libs: [liblog], // 依赖其他模块 static_libs: [libutils], header_libs: [libheaders], export_include_dirs: [include], // 向依赖者暴露头文件路径 vendor: true, // 布尔值表示是否是Vendor模块 }注释支持单行注释//和多行注释/* */。变量与操作符支持简单的字符串列表连接和条件赋值但逻辑远比Android.mk简单。srcs [main.cpp], srcs [utils.cpp], // 列表追加 cflags [-DDEBUG] [-O0], // 列表连接一个最基础的、完整的Android.bp示例用于编译一个Hello World的可执行文件// 在 system/core/toybox/ (举例) 目录下 cc_binary { name: my_hello, srcs: [hello.cpp], shared_libs: [liblog], // 链接liblog库以便使用ALOGD等打印 cflags: [-Wall, -Werror], }把它放到源码树的某个目录下然后在该目录执行mm命令或全编mSoong就会找到它并参与构建。2.3 核心模块类型详解你的构建工具箱Android.bp支持丰富的模块类型覆盖了Android构建的方方面面。下面是一些最常用和核心的类型cc_library/cc_library_static/cc_library_sharedC/C库。cc_library默认同时生成静态和动态库。cc_library_static只生成静态库.a。cc_library_shared只生成动态库.so。关键属性srcs,cflags,cppflags,shared_libs链接动态库,static_libs链接静态库,header_libs,export_include_dirs。cc_binaryC/C可执行文件。关键属性与cc_library类似但产出是二进制程序。java_library/java_library_static/java_library_sharedJava库。java_library生成.jar文件可能包含.dex用于APK。static_libs用于依赖其他Java库。关键属性srcs.java文件,libs依赖的jar包,static_libs。android_appAndroid应用程序包APK。这是构建APK的主力。关键属性srcs,manifest,resource_dirs,static_libs依赖的aar或jar,privileged,certificate签名证书。prebuilt_*一系列用于引入预编译文件的模块。prebuilt_etc将文件放到/system/etc。prebuilt_usr_share放到/usr/share。prebuilt_binary预编译的二进制程序。android_app_import导入预编译的APK。关键属性src源文件路径,filename安装后的文件名。filegroup文件组。这是一个非常实用的模块它本身不产生输出只是将一组文件声明为一个逻辑单元供其他模块通过:语法引用。filegroup { name: my_configs, srcs: [config1.xml, config2.json], } prebuilt_etc { name: config1_installed, src: :my_configs, // 引用filegroup实际上需要更精确的路径这里示意概念 sub_dir: myapp, }genrule通用规则。当Soong内置的模块类型无法满足需求时可以用它来执行自定义的shell命令生成文件。这是从Android.mk迁移复杂逻辑的“逃生舱”但应谨慎使用。genrule { name: generate_version_header, tools: [host_bionic], cmd: echo #define VERSION \\\$(VERSION)\\\ $(out), // $(out)是输出文件 srcs: [], // 可以不依赖任何源文件 out: [version.h], }实操心得刚开始写Android.bp时最容易混淆的是依赖关系。记住一个原则static_libs和shared_libs用于链接时的库依赖而header_libs和export_include_dirs用于编译时的头文件查找路径。对于纯C/C项目通常只需要shared_libs/static_libs因为被依赖的库会通过export_include_dirs自动导出头文件路径。但对于那些只提供头文件的库如一些纯头文件的C库就需要用header_libs。3. 从Android.mk到Android.bp的迁移实战将现有的Android.mk迁移到Android.bp是每个系统开发者迟早要面对的任务。Google提供了官方工具androidmk但它只能处理大部分简单情况复杂的逻辑仍需手动调整。下面我结合一个真实案例拆解迁移的全过程。3.1 迁移准备与工具使用首先找到androidmk工具。它通常在out/soong/host/linux-x86/bin/androidmkLinux环境下。确保你已经成功编译过一遍源码或者从预构建的Soong中获取它。假设我们有一个简单的Android.mkLOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : my_legacy_lib LOCAL_SRC_FILES : file1.c file2.c LOCAL_CFLAGS : -DDEBUG -O0 LOCAL_SHARED_LIBRARIES : liblog libcutils LOCAL_EXPORT_C_INCLUDE_DIRS : $(LOCAL_PATH)/include include $(BUILD_SHARED_LIBRARY)使用命令进行转换androidmk Android.mk Android.bp生成的Android.bp可能如下cc_library_shared { name: my_legacy_lib, srcs: [file1.c, file2.c], cflags: [-DDEBUG, -O0], shared_libs: [liblog, libcutils], export_include_dirs: [include], // 注意androidmk可能会生成一些冗余或需要调整的属性 }工具完成了基础转换但我们需要仔细审查。3.2 复杂逻辑的手动重构策略androidmk无法处理Android.mk中的复杂条件判断、循环和shell命令。例如下面这段常见的代码ifeq ($(TARGET_ARCH),arm) LOCAL_SRC_FILES arm_specific.c else LOCAL_SRC_FILES generic.c endif my_sources : $(wildcard $(LOCAL_PATH)/src/*.c) LOCAL_SRC_FILES $(my_sources)在Android.bp中你需要用不同的方式来处理条件编译使用target选择器。cc_library { name: my_lib, srcs: [common.c], target: { android_arm: { srcs: [arm_specific.c], }, android_arm64: { srcs: [arm64_specific.c], }, // 其他架构可以用 android 表示通用 android: { srcs: [generic.c], }, }, }target选择器非常强大可以针对不同的操作系统androidhost、架构arm,arm64,x86,x86_64甚至更细的维度设置属性。文件通配Android.bp原则上不鼓励在srcs中使用通配符如*.c因为这不利于构建系统的精确分析和增量构建。最佳实践是显式列出所有源文件。如果文件实在太多可以考虑使用filegroup辅助管理或者在极少数情况下通过genrule来生成文件列表。但在绝大多数模块中老老实实列出来是更可维护的做法。自定义构建步骤对于Android.mk中通过$(shell ...)或自定义规则执行的操作需要迁移到genrule中。# Android.mk 中生成版本信息 LOCAL_GENERATED_SOURCES : $(intermediates)/version.c $(LOCAL_GENERATED_SOURCES): PRIVATE_VERSION : $(MY_VERSION) $(LOCAL_GENERATED_SOURCES): echo const char* version \\$(PRIVATE_VERSION)\\; $ LOCAL_SRC_FILES $(LOCAL_GENERATED_SOURCES)迁移为genrule { name: generate_version_c, cmd: echo const char* version \\\$(VERSION)\\\; $(out), out: [version.c], // 可以通过 srcs 声明对版本文件的依赖 } cc_library { name: my_lib, srcs: [*.c, :generate_version_c], // 引用genrule的输出 }3.3 属性映射与常见陷阱对照表下表列出了Android.mk中常见变量与Android.bp属性的对应关系并指出了迁移时容易踩的坑Android.mk 变量Android.bp 属性说明与注意事项LOCAL_MODULEname模块名必须全局唯一。LOCAL_SRC_FILESsrcs最大的不同srcs需要列出所有源文件包括相对路径。不支持*.c通配极不推荐。对于生成的文件通过:引用genrule的name。LOCAL_CFLAGS/LOCAL_CPPFLAGScflags/cppflags基本对应。注意Android.bp中列表项是字符串不需要加-但通常都加。LOCAL_SHARED_LIBRARIESshared_libs依赖的动态库模块名。LOCAL_STATIC_LIBRARIESstatic_libs依赖的静态库模块名。LOCAL_HEADER_LIBRARIESheader_libs依赖的纯头文件库。LOCAL_EXPORT_C_INCLUDE_DIRSexport_include_dirs向依赖此模块的其他模块暴露头文件搜索路径。路径是相对于当前Android.bp的。LOCAL_C_INCLUDESlocal_include_dirs仅在本模块编译时使用的头文件路径。不会被依赖者继承。LOCAL_MODULE_CLASS(模块类型隐含)在Android.bp中模块类型如cc_binary,prebuilt_etc已经决定了安装路径类别。LOCAL_MODULE_PATH(由模块类型决定)通常不需要指定。特殊安装需求可通过relative_install_path等属性微调或使用特定的prebuilt_*模块。LOCAL_INIT_RCinit_rc对于cc_binary可以指定一个init.rc文件在模块安装时一同处理。LOCAL_VENDOR_MODULEvendor: true标记为Vendor模块会影响安装路径和编译宏如__ANDROID_VNDK__。LOCAL_PROPRIETARY_MODULEproprietary: true标记为专有模块通常用于ODM分区。include $(BUILD_*)(模块类型)BUILD_SHARED_LIBRARY-cc_library_sharedBUILD_EXECUTABLE-cc_binaryBUILD_PACKAGE-android_app。避坑指南迁移后务必用mm或mma命令单独编译你的模块进行测试。一个非常常见的错误是头文件找不到。在Android.mk时代通过LOCAL_C_INCLUDES添加的路径有时会被间接依赖者用到因为Make的全局变量污染。在Android.bp严格的依赖图下你必须确保每个模块都通过export_include_dirs正确导出其头文件或者依赖方通过header_libs明确声明对头文件库的依赖。使用ninja -v your_module_name命令可以查看详细的编译命令行是排查头文件、库路径问题的利器。4. 高级特性与Soong构建系统深度集成当你掌握了基础语法和迁移技巧后Android.bp更强大的能力在于它与Soong构建系统的深度集成包括模块变量、插件系统、产品配置变量等。4.1 模块变量Module Variables与全局标志Global FlagsSoong允许你定义和使用变量使配置更灵活。顶级变量Top-level Variables在Android.bp文件最外层模块定义之外可以定义变量供本文件内所有模块使用。common_srcs [utils.cpp, helper.cpp] common_cflags [-DPLATFORM_SDK_VERSION String(28)] cc_library { name: lib_a, srcs: common_srcs [a_specific.cpp], cflags: common_cflags, } cc_library { name: lib_b, srcs: common_srcs [b_specific.cpp], cflags: common_cflags [-DFEATURE_B], }全局标志如cflagsldflags在Soong中很多编译标志是通过cc_defaults模块或产品级配置BoardConfig.mk,soong_config.mk全局设置的。你的模块会自动继承这些标志。如果你想移除某个全局标志可以使用cflags: [-Wno-error],来覆盖或者更精细地使用conlyflags,cppflags,ldflags。4.2 产品定制与条件编译实战系统定制中经常需要根据产品型号、特性开关来编译不同的代码或模块。Soong提供了强大的机制。通过soong_config_module_type定义特性变量 这是Google官方推荐的、用于在Android.bp中访问产品Makefile中定义变量的方式。假设你在产品ProductConfig.mk中定义MY_FEATURE_ENABLE : true首先需要在一个.bp文件通常放在build/soong目录中声明这个变量// build/soong/my_variables.bp soong_config_module_type { name: my_company_cc_defaults, module_type: cc_defaults, config_namespace: my_company, variables: [feature_enable], properties: [cflags, srcs], }然后在你的模块中可以这样使用cc_defaults { name: my_feature_defaults, soong_config_variables: { feature_enable: { cflags: [-DMY_FEATURE1], conditions_default: { cflags: [-DMY_FEATURE0], }, }, }, } cc_library { name: my_product_lib, defaults: [my_feature_defaults], srcs: [common.cpp], soong_config_variables: { feature_enable: { srcs: [feature.cpp], }, }, }最后在产品配置中激活它# BoardConfig.mk or ProductConfig.mk $(call soong_config_set, my_company, feature_enable, true)当feature_enable为true时my_product_lib会添加-DMY_FEATURE1编译标志并包含feature.cpp源文件。使用product_variables对于更简单的、与产品分区如eng,userdebug或设备特性treble相关的条件可以使用内置的product_variables。cc_binary { name: my_tool, srcs: [tool.cpp], product_variables: { eng: { cflags: [-DDEBUG_TOOL], }, }, }当编译eng版本时会定义DEBUG_TOOL宏。经验之谈产品条件编译是一把双刃剑。过度使用会导致构建配置复杂化模块行为难以预测。我的建议是尽量将条件逻辑收敛到少数几个cc_defaults模块中让具体的功能模块保持简洁。同时优先考虑在运行时通过配置文件或属性系统来决定行为而不是在编译时写死这样系统的灵活性和可维护性会好得多。4.3 自定义Soong模块插件Bp扩展对于平台厂商或深度定制者Soong允许你使用Go语言编写插件来定义全新的模块类型或扩展现有模块类型的行为。这属于高级话题但了解其存在很有必要。例如你可以创建一个用于处理某种自定义资源文件的模块类型在build/soong目录下创建Go插件例如my_custom_module.go实现Module接口和相关的GenerateBuildActions方法。在Android.bp中就可以使用你定义的my_custom_module类型了。这赋予了构建系统极大的扩展能力但同时也增加了维护成本。除非有非常普遍和稳定的自定义构建需求否则应优先考虑使用genrule或现有模块类型组合来实现功能。5. 调试、排查与性能优化指南即使理解了所有语法在实际操作中依然会遇到各种构建失败、依赖错误问题。掌握调试方法至关重要。5.1 核心调试命令与日志分析m和mm/mmam编译整个系统。mm编译当前目录下的模块不编译依赖。mma编译当前目录下的模块及其依赖。在修改Android.bp后使用mm快速验证语法和基本逻辑是否正确。ninja命令ninja -f out/combined-*.ninja your_module_name这是Soong底层调用的命令。直接使用它可以绕过一些高级逻辑有时能更直接地看到错误。ninja -v ...-v参数会打印出实际执行的每一条命令对于查看编译标志、头文件路径、链接库顺序等细节有奇效。ninja -t deps out/.../your_module查看模块的依赖图分析依赖关系是否正确。Soong调试soong_ui --dumpvars可以打印出Soong解析后的众多变量状态。查看out/soong/.bootstrap/build.ninja和out/soong/build.ninja这是Soong生成给Ninja的最终构建计划非常庞大但包含所有细节。日志文件out/error.log或out/soong.log构建失败时首先查看的地方通常会有具体的错误信息。out/soong/soong.variables包含了Soong加载的所有变量和配置。5.2 常见构建问题与解决方案速查表问题现象可能原因排查步骤与解决方案FAILED: ninja: unknown target xxx1. 模块名拼写错误。2.Android.bp文件语法错误导致模块未被正确解析。3. 模块的visibility属性限制了访问。1. 检查Android.bp文件是否有语法错误如缺少逗号、括号。2. 运行m nothing或soong_build查看是否有解析错误。3. 检查模块的visibility属性确保你的构建目标如make命令在允许的列表内。头文件找不到fatal error: xxx.h file not found1. 依赖的库未正确导出头文件路径缺少export_include_dirs。2. 本模块未声明对头文件库的依赖缺少header_libs。3. 路径拼写错误。1. 使用ninja -v your_module查看编译命令确认-I包含的路径是否正确。2. 检查依赖库的Android.bp确认有export_include_dirs。3. 如果依赖的是纯头文件库确保在本模块中添加了header_libs: [lib_name]。库链接失败undefined reference to1. 依赖的库未在shared_libs或static_libs中声明。2. 库的链接顺序有问题静态库顺序敏感。3. 依赖的库本身编译失败。1. 确认所有需要的库都已添加到shared_libs/static_libs。2. 对于静态库链接顺序很重要。确保依赖关系是单向的或者将一组有循环依赖的静态库合并或改为动态库。3. 单独编译依赖的库确保其本身没问题。Android.bp修改后构建系统似乎没反应Soong的增量解析可能未触发。1. 删除out/.soong.in_memory文件强制Soong重新解析所有Android.bp。2. 使用m nothing命令它会运行Soong的解析阶段。3. 最彻底的方法是rm -rf out然后重新编译但耗时很长。genrule命令执行失败1. 命令语法错误。2. 使用的工具tools属性未定义或不可用。3. 工作目录或环境变量问题。1. 仔细检查cmd中的命令特别是引号和变量引用$(out),$(in)。2. 确保tools中列出的模块是存在的cc_binary主机工具。3.genrule在沙箱中执行访问文件系统受限。确保所有输入文件都在srcs中声明输出文件在out中声明。5.3 构建性能优化实践避免在Android.bp中使用通配符和find命令这破坏了Soong精确分析依赖的能力可能导致不必要的重编。合理使用cc_defaults将通用的属性如cflags,include_dirs提取到cc_defaults模块中可以减少重复配置也让修改更集中。最小化模块依赖只声明真正需要的依赖。过多的shared_libs会增加链接时间并可能扩大最终镜像的体积。关注visibility属性明确设置模块的可见性如visibility: [:my_subdir]可以限制依赖范围加速依赖解析过程。利用增量构建在开发时尽量使用mm或mma来编译你正在修改的模块而不是每次都m全编。SoongNinja的增量构建效率很高。调试时关闭优化在eng或userdebug版本中默认的编译优化可能使调试困难。可以在模块的cflags中临时添加-O0 -g来关闭优化并加入调试信息。我个人在从Android.mk全面转向Android.bp的过程中最大的体会是前期严格的模块化设计和清晰的依赖声明虽然增加了些许配置工作量但换来了后期构建的稳定性和速度的极大提升。最初可能会觉得束手束脚但一旦适应了这种声明式的思维你会发现构建脚本的维护成本显著降低团队协作也更顺畅。遇到复杂构建逻辑时不妨先思考是否能用多个简单的Android.bp模块组合实现或者是否真的需要那么复杂的逻辑很多时候在架构层面进行简化才是根本的解决之道。
返回列表