写在开头:本文为Android10.0编译系统这个系列的笔记。
要站在巨人的肩膀上去学习,知识就在那里,一个好的老师会让你学得更快,更好。我刚入门那会儿,上司教导我,不会没有关系,要多借鉴他人的经验,见得多了,自己多做总结就好了,记下来的就是自己的了。大部分人的知识是都不是凭空而来的,创新的毕竟是少数。
笔记的作用就是回忆的时候,找重点。每个人掌握的知识不一样,关注点也不同。记录,可以让下次快速找到某个知识点。毕竟搜索有时候并不可靠,由于信息有丢失,所以我都会注明来源。
本篇目录结构如下:
- 1.概述
- 2.编译构成
- 3.编译步骤
- 4.main.mk
- 5.工具链的关系
- 6.system.img的打包
- 7.Kati
- 8.Blueprint
- 9.Ninja
- 10.android.mk的总结
1.概述
Ninja
Ninja是一个致力于速度的小型编译系统(类似于Make),如果把其他编译系统比做高级语言的话,Ninja就是汇编语言。通常使用Kati或soong把makefile转换成Ninja files,然后用Ninja编译。
Ninja的诞生,主要是为了提升编译速度,Ninja中去除了变量的计算,没有默认规则, 依赖必须显式声明,从而提升编译速度。基本上可以认为ninja就是make的最最精简版。
Android.bp
Android.bp只是一个纯粹的配置文件,不包括分支、循环语句等控制流程,本质上就是一个json配置文件。Android.bp 通过Blueprint+soong转换成ninja的构建规则文件build.ninja,再使用ninja来进行构建工作。
Android10.0上,mk和bp编译的列表可以从 \out.module_paths中的Android.bp.list、Android.mk.list中看到,Android10.0还有400多个mk文件没有被替换完,Google任重道远。
Android编译演进过程:
Android7.0之前 使用GNU Make
Android7.0 引入ninja、kati、Android.bp和soong构建系统
Android8.0 默认打开Android.bp
Android9.0 强制使用Android.bp
https://blog.csdn.net/yiranfeng/article/details/109082489
2.编译构成
Android的编译目录在/build 中,看一下Android 10源码中的build目录,现在是这个样子:
这个目录中可以看到core文件夹被link到了make/core,envsetup.sh被link到make/envsetup.sh,这主要是为了对使用者屏蔽切换编译系统的差异。
这里重点看四个文件夹:blueprint、kati、make、soong
在编译过程中,Android.bp会被收集到out/soong/build.ninja.d,blueprint以此为基础,生成out/soong/build.ninja
Android.mk会由kati/ckati生成为out/build-aosp_arm.ninja
两个ninja文件会被整合进入out/combined-aosp_arm.ninja
3.编译步骤
编译会用到3条命令。
source build/envsetup.sh
lunch aosp_arm-eng // 或者 m PRODUCT-aosp_x86_64-eng ,Android10.0不一定需要lunch命令
make -j8 //编译模块也可以直接用 m libart
-j8代表用系统的8个线程去编译,因为一般不是单人使用所以不能太高,会影响其他人的使用。
加载envsetup.sh,初始化一下环境变量。
envsetup.sh 主要做了下面几个事情:
环境变量初始化完成后,我们需要选择一个编译目标。lunch 主要作用是根据用户输入或者选择的产品名来设置与具体产品相关的环境变量。eng表示工程版本,一般调试编译用userdebug,正式版本用user。
区别可看这篇: Android编译选项eng、user、userdebug的区别
执行完lunch命令后,就可以使用make命令来执行编译Build。
main.mk文件把一些环境变量和目标都配置好后,会执行envsetup.sh中的make()进行编译
4.main.mk
执行runKatiBuild时,有个重要的步骤,就是加载build/make/core/main.mk,main.mk文件是Android Build系统的主控文件。从main.mk开始,将通过include命令将其所有需要的.mk文件包含进来,最终在内存中形成一个包括所有编译脚本的集合,这个相当于一个巨大Makefile文件。Makefile文件看上去很庞大,其实主要由三种内容构成: 变量定义、函数定义和目标依赖规则,此外mk文件之间的包含也很重要。
main.mk主要做了以下几件事情:
1.定义编译目标product
2.加载config.mk来初始化相关变量,检测编译环境和目标环境
3.清除规则,清除out目录中的dex文件
4.加载build/croe/definitions.mk,定义了很多通用函数,供编译过程调用
5.加载平台开发工具包 build/make/core/pdk_config.mk
6.加载系统中所有的Android.mk,最终会被存放到out/.module_paths/Android.mk.list
7.Link 类型检查,校验Link
8.要为此产品生成的模块的基本列表由相应的产品定义文件指定,这些定义在"product_config.mk"中
9.运行时APEX库,并进行检查校验
10.将所有要安装的模块都保存在变量ALL_DEFAULT_INSTALLED_MODULES中,并且将build/core/Makefie文件加载进来。build/core/Makefie文件会根据要安装的模块生成system.img、super.img、boot.img和recovery.img等镜像文件的生成规则
11.定义编译的image目标
12.构建文件,然后将其打包成rom格式
5.工具链的关系
Android10.0的编译系统中,涉及以下一些工具链,由这些工具链相辅相成,才最终编译出了我们所需要的镜像版本。
Android10.0编译工具链:
soong kati blueprint ninja
Soong
Soong 构建系统是在 Android 7.0 (Nougat) 中引入的,旨在取代 Make。它利用 Kati GNU Make 克隆工具和 Ninja 构建系统组件来加速 Android 的构建。
Soong是由Go语言写的一个项目,从Android 7.0开始,在prebuilts/go/目录下新增了Go语言所需的运行环境,Soong在编译时使用,解析Android.bp,将之转化为Ninja文件,完成Android的选择编译,解析配置工作等。故Soong相当于Makefile编译系统的核心,即build/make/core下面的内容。
另外Soong还会编译产生一个androidmk命令,可以用来手动将Android.mk转换成Android.bp文件。不过这只对无选择、循环等复杂流程控制的Android.mk生效。
soong脚本和代码目录:/build/soong
kati
kati是一个基于Makefile来生成ninja.build的小项目。主要用于把Makefiel转成成ninja file,自身没有编译能力,转换后使用Ninja编译。
在编译过程中,kati负责把既有的Makefile、Android.mk文件,转换成Ninja文件。在Android 8.0以后,它与Soong一起,成为Ninja文件的两大来源。Kati更像是Google过渡使用的一个工具,等所有Android.mk都被替换成Android.bp之后,Kati有可能退出Android编译过程.
在单独使用时,它对普通的小项目还能勉强生效。面对复杂的、多嵌套的Makefile时,它往往无法支持,会出现各种各样的问题。当然,也可以理解为,它只为Android而设计。
kati脚本和代码目录:/build/kati
Blueprint
Blueprint是生成、解析Android.bp的工具,是Soong的一部分。Soong则是专为Android编译而设计的工具,Blueprint只是解析文件的形式,而Soong则解释内容的含义。
ninja
最开始,Ninja 是用于Chromium 浏览器中,Android 在SDK 7.0 中也引入了Ninja。
Ninja是一个致力于速度的小型编译系统(类似于Make),如果把其他编译系统比做高级语言的话,Ninja就是汇编语言。通常使用Kati或soong把makefile转换成Ninja files,然后用Ninja编译。
主要两个特点:
1)可以通过其他高级的编译系统生成其输入文件;
2)它的设计就是为了更快的编译;
ninja核心是由C/C++编写的,同时有一部分辅助功能由python和shell实现。由于其开源性,所以可以利用ninja的开源代码进行各种个性化的编译定制。
从Android 7开始,编译时默认使用Ninja。但是,Android项目里是没有.ninja文件的。遵循Ninja的设计哲学,编译时,会先把Makefile通过kati转换成.ninja文件,然后使用ninja命令进行编译。
Android.mk文件、Android.bp、kati、Soong、Blueprint、Ninja之间的关系如下:
Android.bp --> Blueprint --> Soong --> Ninja
Makefile or Android.mk --> kati --> Ninja
(Android.mk --> Soong --> Blueprint --> Android.bp)
Blueprint是生成、解析Android.bp的工具,是Soong的一部分。Soong则是专为Android编译而设计的工具,Blueprint只是解析文件的形式,而Soong则解释内容的含义。
Android.mk可以通过Soong提供的androidmk转换成Android.bp,但仅限简单配置。目前Oreo的编译流程中,仍然是使用kati来做的转换。
现存的Android.mk文件、既有的Android.bp,都会分别被转换成Ninja。
从Android.mk与其它Makefile,会生成out/build-<product_name>.ninja文件。而从Android.bp,则会生成out/soong/build.ninja。此外,还会生成一个较小的out/combined-<product_name>.ninja文件,负责把二者组合起来,作为执行入口。
最终,Ninja文件才是真正直接控制源码编译的工具。
6.system.img的打包
main.mk中,最后两步定义了需要编译的image和构建一个rom的过程。
main.mk中只是做了一些定义和启动编译流程,正在的image打包在build/core/Makefile中完成。
在Makefile中,.PHONY后面的target表示的也是一个伪造的target, 而不是真实存在的文件target,注意Makefile的target默认是文件。如果有人依赖它,就无条件执行。
[build/core/Makefile]
...
.PHONY: systemimage
...
.PHONY: systemimage-nodeps snod
...
# Rules that need to be present for the all targets, even
# if they don't do anything.
.PHONY: systemimage
systemimage:
...
INSTALLED_SYSTEMIMAGE_TARGET := $(PRODUCT_OUT)/system.img
SYSTEMIMAGE_SOURCE_DIR := $(TARGET_OUT)
$(INSTALLED_SYSTEMIMAGE_TARGET): $(BUILT_SYSTEMIMAGE) $(RECOVERY_FROM_BOOT_PATCH)
@echo "Install system fs image: $@"
$(copy-file-to-target)
$(hide) $(call assert-max-image-size,$@ $(RECOVERY_FROM_BOOT_PATCH),$(BOARD_SYSTEMIMAGE_PARTITION_SIZE))
systemimage: $(INSTALLED_SYSTEMIMAGE_TARGET)
.PHONY: systemimage-nodeps snod
systemimage-nodeps snod: $(filter-out systemimage-nodeps snod,$(MAKECMDGOALS)) \
| $(INTERNAL_USERIMAGES_DEPS)
@echo "make $@: ignoring dependencies"
$(call build-systemimage-target,$(INSTALLED_SYSTEMIMAGE_TARGET))
$(hide) $(call assert-max-image-size,$(INSTALLED_SYSTEMIMAGE_TARGET),$(BOARD_SYSTEMIMAGE_PARTITION_SIZE))
...
关于system.img,这里定义了两个伪目标systemimage 和 systemimage-nodeps。
systemimage 依赖于INSTALLED_SYSTEMIMAGE_TARGET,最终生成目标文件
$(PRODUCT_OUT)/system.img
$(INSTALLED_SYSTEMIMAGE_TARGET): $(BUILT_SYSTEMIMAGE) $(RECOVERY_FROM_BOOT_PATCH)
@echo "Install system fs image: $@"
$(copy-file-to-target)
$(hide) $(call assert-max-image-size,$@ $(RECOVERY_FROM_BOOT_PATCH),$(BOARD_SYSTEMIMAGE_PARTITION_SIZE))
INSTALLED_SYSTEMIMAGE_TARGET依赖于BUILT_SYSTEMIMAGE和RECOVERY_FROM_BOOT_PATCH,再调用了函数copy-file-to-target进行文件拷贝
最后调用build/make/tools/releasetools/build_image.py来生成system.img。
img格式是一种文件压缩格式。img就是把一些文件压缩打包。
INTERNAL_USERIMAGES_DEPS:列出了制作system.img所需要的工具,例如out/host/linux-x86/bin/simg2img、out/host/linux-x86/bin/mkuserimg_mke2fs 等,如果支持f2fs的文件系统,会加载out/host/linux-x86/bin/make_f2fs
INTERNAL_SYSTEMIMAGE_FILES:列出了制作system.img所需要的文件,释义如下:
ALL_GENERATED_SOURCES:描述的是要拷贝到目标设备上去的由工具自动生成的源代码文件。
ALL_DEFAULT_INSTALLED_MODULES:描述的是所有需要安装的module
PDK_FUSION_SYSIMG_FILES:是从PDK(Platform Development Kit)提取出来的相关文件
RECOVERY_RESOURCE_ZIP:描述的是Android的recovery系统要使用的资源文件,对应于/system/etc目录下的recovery-resource.dat文件。
PDK_FUSION_SYMLINK_STAMP:PDK的符号链接文件
7.Kati
Kati详解-Android10.0编译系统(五)
这篇文字不多,而且都是理论性的,因此不做提取。
8.Blueprint
Blueprint简介-Android10.0编译系统(六)
Blueprint
是一个meta-build系统,它读取描述需要构建的模块的bp文件,并生成Ninja描述需要运行的命令及其依赖项的清单。
bluepring--soong的编译过程经历下面四个阶段:
1.运行microfactory.bash以建立minibp -
2.运行.minibootstrap / build.ninja来构建.bootstrap / build.ninja -
3.运行.bootstrap / build.ninja来构建和运行主构建器 -
4.运行build.ninja来构建您的代码
Blueprint文件是一个伪python数据格式的模块列表,其中模块类型看起来像函数调用,模块的属性看起来像可选参数。例如,一个简单的模块可能看起来像:
cc_library {
name: "cmd",
srcs: [
"main.c",
],
deps: [
"libc",
],
}
subdirs = ["subdir1", "subdir2"]
格式说明:
[module type] {
name: "[name value]",
[property1 name]:"[property1 value]",
[property2 name]:"[property2 value]",
}
bp文件中的模块(module) 以模块类型(module type)开头,后面跟着一系列的属性(property)。每个模块都必须具有一个属性名为name的属性,并且name的属性值在所有Android.bp文件中必须是唯一的。
更多编写规则,可参考 :
Android 编译之android.bp
Android.bp 语法浅析-Android10.0编译系统(八)
基本上写的时候,就是参照着源码去写。
Android系统中现在基本是bp和mk文件混着用。bp一般要搭配go脚本来达到宏控制的目的。Android.bp 文件很简单。它们不包含任何条件语句,也不包含控制流语句;所有复杂问题都由用 Go 编写的构建逻辑处理。
具体可参考这篇:
Android.bp正确姿势添加宏控制编译指南
这个博主写的系列文章也都很不错,细节性很强。
//xxxparser.go
package xxxparser
import (
"android/soong/android"
"android/soong/cc"
)
func init() {
// resister a module "xxxparser_defaults"
android.RegisterModuleType("xxxparser_defaults", xxxdroidDefaultsFactory)
}
func xxxdroidDefaultsFactory() (android.Module) {
module := cc.DefaultsFactory()
android.AddLoadHook(module, xxxdroidDefaults)
return module
}
func xxxdroidDefaults(ctx android.LoadHookContext) {
type props struct {
Cflags []string
}
p := &props{}
p.Cflags = globalDefaults(ctx)
ctx.AppendProperties(p)
}
func globalDefaults(ctx android.BaseContext) ([]string) {
var cppflags []string
if ctx.AConfig().Getenv("ANDROIDBP_FUN") == "YES" {
cppflags = append(cppflags,"-DXXX")
}
return cppflags
}
在Android.bp开头位置引入go脚本文件xxxparser.go,如下:
//引入go脚本
bootstrap_go_package {
name: "soong-xxxparser",
pkgPath: "android/soong/xxxparser",
deps: [
"blueprint",
"blueprint-pathtools",
"soong",
"soong-android",
"soong-cc",
"soong-genrule",
],
srcs: [
"xxxparser.go",
],
pluginFor: ["soong_build"],
}
xxxparser_defaults {
name: "xxxparser_defaults",
}
ANDROIDBP_FUN = ["YES"]
cc_binary {
defaults: ["xxxparser_defaults"],
name: "AndroidBp",
srcs: ["main.c"],
cflags: ["-Wno-error=implicit-function-declaration"],
shared_libs: [
"libcutils",
"liblog",
"libutils",
],
}
以上语句可以保证运行Android.bp时,先编译对应的xxxparser.go运行go脚本时,会首先运行init函数,将 xxxdroidDefaultsFactory函数注册到module中,之后调用xxxdroidDefaultsFactory函数时,会将回调函数 xxxdroidDefaults注册进去之后调用 privateParserDefaults 时,我们可以从 ctx.AConfig() 中获取好多属性
(参考 build/soong/android/config.go 中对 build/soong/android/module.go中的 androidBaseContext interface的各种函数实现),其中有一项是获取宏值的,之后回调xxxdroidDefaults添加宏信息。
9.Ninja
Ninja简介-Android10.0编译系统(九)
Ninja提升编译速度的方法-Android10.0编译系统(十)
Ninja
是一个编译框架,会根据相应的ninja格式的配置文件进行编译,但是ninja文件一般不会手动修改,而是通过将Android.bp文件转换成ninja格文件来编译。
Ninja是一个注重速度的小型构建系统。它与其他构建系统在两个主要方面不同:它被设计为使其输入文件由更高级别的构建系统生成,并且被设计为尽可能快地运行构建。
编译分析
从Android O开始,soong已经是google的入口。从soong入口后,会经soong_ui,soong,kati,blueprint几个阶段,把mk,bp转换成ninja文件后,然后执行ninja命令解析ninja文件进行编译。
如下图所示整个编译过程,准备过程非常冗长。
每次编译都要重新收集所有的文件、.mk、.bp的修改,然后重新生成build.ninja,在合并成combined-aosp_arm.ninja。
大部分情况下,研发的工作是不断的修改.c .h .cpp .java 然后增量,此时真正的编译工作是非常少的,这样相对而言,准备工作往往是占大头的,所以我们可以考虑舍弃combined-aosp_arm.ninja之前的准备过程。
这里以增量编译init_system为例,之前我们已经编好了init_system,然后如果我们继续用m命令单编init_system,需要2分钟。
ingresge:~/AP/AOSP_Q$ time m init_system
[100% 6336/6336] Install: out/target/product/generic/fake_packages/init_system-timestamp
#### build completed successfully (02:37 (mm:ss)) ####
real 2m36.672s
user 43m51.510s
sys 2m51.991s
为了对比编译时间,我们直接抛弃了编译的环境和ninja文件生成的逐步过程,我们使用下面的命令直接跑ninja,结果只花了5秒。
命令:
time prebuilts/build-tools/linux-x86/bin/ninja -v -d keepdepfile init_system -f out/combined-aosp_arm.ninja -w dupbuild=err
编译结果:
ingresge:~/AP/AOSP_Q$ time prebuilts/build-tools/linux-x86/bin/ninja -v -d keepdepfile init_system -f out/combined-aosp_arm.ninja -w dupbuild=err
[7/7] /bin/bash -c "(rm -f out/target/product/generic/system/bin/init ) && (cp out/target/product/generic/obj/EXECUTABLES/init_second_stage_intermediates/init out/target/product/generic/system/bin/init )"
real 0m5.351s
user 0m14.752s
sys 0m3.201s
这种命令可以写到sh脚本中去,方便快速执行。
在修改build/make/envsetup.sh,新增一个qninja函数。
function qninja()
{
local cmdline="time prebuilts/build-tools/linux-x86/bin/ninja -v -d keepdepfile $@ -f out/combined-aosp_arm.ninja -w dupbuild=warn"
echo $cmdline
$cmdline
}
只是修改了某个模块中的.c .h .cpp .java后,进行增量,编译命令如下:
source build/envsetup.sh
qninja init_system
加载配置文件,执行qninja函数。
早期可以是mm单编某个应用提升编译效率,现在就可以使用Ninja了。
编译Settings
./prebuilts/build-tools/linux-x86/bin/ninja -f out/combined-xxx.ninja Settings -j32
编译selinux
./prebuilts/build-tools/linux-x86/bin/ninja -f out/combined-xxx.ninja selinux_policy -j32
编译Framework
./prebuilts/build-tools/linux-x86/bin/ninja -f out/combined-xxx.ninja framework -j32
全编译
./prebuilts/build-tools/linux-x86/bin/ninja -f out/combined-xxx.ninja -j32 2>&1 |tee ninja_build.log
10.android.mk的总结
android.mk的使用实例介绍看这篇。
Android 编译之android.mk
文章中介绍了以下几种mk的编写。
参考链接:
编译系统入门篇-Android10.0编译系统(一)
Android编译系统简要介绍和学习计划
从CM刷机过程和原理分析Android系统结构
Android编译系统分析五:system.img的生成过程
Makefile中.PHONY的作用
Android系统快速编译方式ninja
Makefile概念入门
Android 编译之android.mk