diff --git a/articles/20211026-lunch.md b/articles/20211026-lunch.md index 9eafcf038b1c65c9931491b73df0a68dea1e0844..71cff6b1d689d96cb78cfc8ef0b597d4a8dd272c 100644 --- a/articles/20211026-lunch.md +++ b/articles/20211026-lunch.md @@ -1,3 +1,5 @@ + + **envsetup.sh 中的一些重要函数分析** diff --git a/articles/20211102-codeanalysis-soong_ui.md b/articles/20211102-codeanalysis-soong_ui.md new file mode 100644 index 0000000000000000000000000000000000000000..629c48ec8acb67483b8b9758d24767f305f4a411 --- /dev/null +++ b/articles/20211102-codeanalysis-soong_ui.md @@ -0,0 +1,435 @@ + + +**代码走读:对 soong_ui 的深入理解** + + + +- [1. 构建系统对 soong_ui 的封装](#1-构建系统对-soong_ui-的封装) + - [1.1. 第一步:准备环境](#11-第一步准备环境) + - [1.2. 第二步:构建](#12-第二步构建) + - [1.3. 第三步:执行](#13-第三步执行) +- [2. `soong_ui` 程序分析](#2-soong_ui-程序分析) + - [2.1. `soong_ui` 的 main 函数。](#21-soong_ui-的-main-函数) + - [2.2. `soong_ui` 的参数和使用](#22-soong_ui-的参数和使用) + - [2.2.1. `soong_ui` 的 "--dumpvar-mode" 和 "--dumpvars-mode" 参数](#221-soong_ui-的---dumpvar-mode-和---dumpvars-mode-参数) + - [2.2.2. `soong_ui` 的 "--make-mode" 参数](#222-soong_ui-的---make-mode-参数) + - [2.3. `build.runSoong()`](#23-buildrunsoong) + + + +# 1. 构建系统对 soong_ui 的封装 + +`soong_ui` 这个程序可以认为是 Google 在替代原先基于 make 的构建系统而引入的一个非常重要的程序,整个构建可以说就是由这个程序驱动完成的。但代码中我们很难看到直接调用 `soong_ui` 这个程序的地方,更多的我们看到的是形如在 `envsetup.sh` 脚本文件中诸如 `$T/build/soong/soong_ui.bash ...` 这样的调用,这个脚本正是对 `soong_ui` 程序的封装调用,以这个脚本函数为入口, Google 将原来的以 make 为核心的框架改造为以 Soong 为核心的构建框架。 + +我们可以认为 Soong 的入口封装在 `build/soong/soong_ui.bash` 这个脚本中,下面我们来看看这个脚本的核心处理逻辑,主要包括以下三步: + +
+// 第一步
+source ${TOP}/build/soong/scripts/microfactory.bash
+
+// 第二步
+soong_build_go soong_ui android/soong/cmd/soong_ui
+
+// 第三步
+cd ${TOP}
+exec "$(getoutdir)/soong_ui" "$@"
+
+
+
+## 1.1. 第一步:准备环境
+
+```
+source ${TOP}/build/soong/scripts/microfactory.bash
+```
+这个被导入的脚本主要做了以下几件事情:
+
+- 设置 GOROOT 环境变量,指向 prebuild 的 go 编译工具链
+- 定义 `getoutdir()` 和 `soong_build_go()` 这两个函数。`getoutdir()` 的作用很简单,就是用于 `Find the output directory`;`soong_build_go()` 实际上是一个对 `build_go()` 函数的调用封装。`soong_build_go()` 在第二步会用到。
+- 导入 `${TOP}/build/blueprint/microfactory/microfactory.bash` 这个脚本,这个脚本中定义了 `build_go()` 这个函数,这个函数的中会调用 go 的命令,根据调用的参数生成相应的程序,其中第一个参数用于指定生成的程序的名字,第二个参数用于指定源码的路径,还有第三个参数可以用于指定额外的编译参数。举个例子:`build_go soong_ui android/soong/cmd/soong_ui` 就是根据 AOSP 源码树目录 `soong/cmd/soong_ui` 的 package 生成一个可执行程序叫 `soong_ui`。
+
+## 1.2. 第二步:构建
+
+```
+soong_build_go soong_ui android/soong/cmd/soong_ui
+```
+
+其作用是调用 `soong_build_go` 函数。这个函数有两个参数,从第一步的分析可以知道,`soong_build_go` 实际上是一个对 `build_go()` 函数的调用封装,所以以上语句等价于 `build_go soong_ui android/soong/cmd/soong_ui`。第一参数 `soong_ui` 是指定了编译生成的可执行程序的名字, `soong_ui` 是一个用 go 语言写的程序,也是 Soong 的实际执行程序。在第二个参数告诉 `soong_build_go` 函数,`soong_ui` 程序的源码在哪里,这里制定了其源码路径 `android/soong/cmd/soong_ui`(实际对应的位置是 `build/soong/cmd/soong_ui`)
+
+综上所述,`build/soong/soong_ui.bash` 的第二步的效果就是帮助我们把 `soong_ui` 制作出来,制作好的 `soong_ui` 路径在 `out/soong_ui` 下。
+
+p.s.: `soong_ui` 是 “soong native UI” 的缩写,这是一个运行在 host 上的可执行程序,即 Soong 的总入口。
+
+## 1.3. 第三步:执行
+
+```
+cd ${TOP}
+exec "$(getoutdir)/soong_ui" "$@"
+```
+就是在前述步骤的基础上调用生成的· `soong_ui`, 并接受所有参数并执行,等价替换了原来的 `make $@`
+
+
+# 2. `soong_ui` 程序分析
+
+`soong_ui` 的主文件是 `build/soong/cmd/soong_ui/main.go` 这个文件可以认为只是 `soong_ui` 作为一个命令行程序的入口,但这个程序的内容绝对不止这一个文件。从其 `soong/cmd/soong_ui/Android.bp` 文件来看:
+
+```
+blueprint_go_binary {
+ name: "soong_ui",
+ deps: [
+ "soong-ui-build",
+ "soong-ui-logger",
+ "soong-ui-terminal",
+ "soong-ui-tracer",
+ ],
+ srcs: [
+ "main.go",
+ ],
+}
+```
+
+编译这个 soong_ui 会涉及到以下几个依赖的 module:
+
+- `soong/ui/build`:soong_ui 的主逻辑
+- `soong/ui/logger`:Package logger implements a logging package designed for command line utilities. It uses the standard 'log' package and function, but splits output between stderr and a rotating log file.
+- `soong/ui/terminal`:Package terminal provides a set of interfaces that can be used to interact with the terminal
+- `soong/ui/tracer`:This package implements a trace file writer, whose files can be opened in chrome://tracing.
+
+
+## 2.1. `soong_ui` 的 main 函数。
+
+main 函数定义在 `build/soong/cmd/soong_ui/main.go`
+
+
+func main() {
+ // 前面都是在做一些准备工作,譬如准备控制台和 log 等
+ ......
+
+ // 定义一个 build.Context 的对象, build.Context 定义参考
+ // `soong\ui\build\context.go`
+ // 我理解 Context 对象是一个容器,包含了在 build 过程中可能会涉及的 log,trace
+ // 等等辅助对象, 会传给其他函数,譬如在执行过程中打印日志
+ buildCtx := build.Context{ContextImpl: &build.ContextImpl{
+ Context: ctx,
+ Logger: log,
+ Metrics: met,
+ Tracer: trace,
+ Writer: writer,
+ Status: stat,
+ }}
+
+ // 下面的代码都是在为进一步解析 `--make-mode` 后面的参数做处理
+ // 定义了一个 build.Config 类型的 config 对象
+ // 这个 Config 对象的类定义参考 `soong/ui/build/config.go` 中的 configImpl,
+ // 命令行中带入的各种选项参数会影响这个结构体中的成员的取值,并进而影响后面 make
+ // 的行为。
+ // 创建 config 时传入了 buildCtx
+ // build.NewConfig() 这个函数的作用有一部分是会解析更多的参数
+ // `build.NewConfig() -> build.parseArgs()`
+ // 在 parse 的过程中一部分参数会导致设置 configImpl 中的一些成员,
+ // 譬如 “showcommands” 会设置 c.verbose;更多的则直接加入到一个 c.arguments
+ // 中,在处理中会直接分析,通过调用 Arguments() 函数来获得,
+ // 例子:`if inList("help", config.Arguments()) {...}`
+ var config build.Config
+ if os.Args[1] == "--dumpvars-mode" || os.Args[1] == "--dumpvar-mode" {
+ config = build.NewConfig(buildCtx)
+ } else {
+ // 如果不是,我猜测就是对应的 --make-mode,则初始化时传入更多的命令行参数
+ config = build.NewConfig(buildCtx, os.Args[1:]...)
+ }
+
+ // 这部分略去,都是在设置一些 build output 路径等等比较次要的环境设置
+ // 需要注意的是:
+ // log 对应的是诸如 ./out/soong.log 这个 log 是 soong_ui 直接产生的
+ // trace 对应的是 ./out/build.trace 因为比较大,实际都是被压缩了
+ // status, 包含了 ./out/verbose.log 和 ./out/error.log 这些文件
+ // 需要注意的是,根据代码注释 verbos.log 是以gz 形式保存,如果运行多次,
+ // 则以前运行的 verbose.log 会保存为 verbose.log.#.gz
+ ......
+
+ // 这里会产生一些东西
+ // build.FindSources 会创建 `out/.module_paths` 这个目录
+ // 并会递归地搜索所有子目录下的 Android.bp 文件,并将这些文件的路径记录到
+ // `out/.module_paths/Android.bp.list` 下去
+ // 这个文件会在 runSoong() 时传递
+ f := build.NewSourceFinder(buildCtx, config)
+ defer f.Shutdown()
+ build.FindSources(buildCtx, config, f)
+
+ if os.Args[1] == "--dumpvar-mode" {
+ dumpVar(buildCtx, config, os.Args[2:])
+ } else if os.Args[1] == "--dumpvars-mode" {
+ dumpVars(buildCtx, config, os.Args[2:])
+ } else {
+ // 这里对应的命令选项 "--make-mode"
+ ......
+ // BuildAll 是一个枚举值,参考 `soong/ui/build/build.go`
+ // 中类似如下语句:
+ // BuildAll = BuildProductConfig | BuildSoong | BuildKati | BuildNinja
+ toBuild := build.BuildAll
+ if config.Checkbuild() {
+ // 所谓 checkout 是指用户希望在 build 过程中增加额外的测试检查,
+ // 这会导致 build 时间变长。
+ toBuild |= build.RunBuildTests
+ }
+ // 譬如 m 命令时,调用 build 的 Build 函数,传进入三个主要参数:
+ // buildCtx(上下文辅助信息),
+ // config(配置信息,重要),
+ // toBuild(控制整个 Build 流程关键步骤的控制参数)
+ build.Build(buildCtx, config, toBuild)
+ }
+}
+
+
+综上所述,我们知道 `soong_ui` 会接受三个参数
+
+- "--dumpvar-mode": 对应调用 `dumpVar()`
+- "--dumpvars-mode": 对应调用 `dumpVars()`
+- "--make-mode": 对应调用 `build.Build()`
+
+## 2.2. `soong_ui` 的参数和使用
+
+`--dumpvars-mode` 和 `--dumpvar-mode` 用于 `dump the values of one or more legacy make variables`
+
+譬如例子:
+```
+./out/soong_ui --dumpvar-mode TARGET_PRODUCT
+```
+
+`--make-mode` 参数告诉 soong_ui,是正儿八经要开始编译。也就是说 `soong_ui --make-mode` 可以替代原来的 make, 所以后面还可以带一些参数选项。这些参数可能都是为了兼容 make 的习惯。
+
+### 2.2.1. `soong_ui` 的 "--dumpvar-mode" 和 "--dumpvars-mode" 参数
+
+"--dumpvar-mode" 对应 soong_ui 的 `dumpVar()` 函数, 从代码中的 help 信息我们可以了解
+
+```
+usage: soong_ui --dumpvar-mode [--abs]
+
+In dumpvar mode, print the value of the legacy make variable VAR to stdout
+
+'report_config' is a special case that prints the human-readable config banner
+from the beginning of the build.
+```
+
+"--dumpvars-mode" 对应 soong_ui 的 dumpVars 函数
+
+```
+usage: soong_ui --dumpvars-mode [--vars=\"VAR VAR ...\
+In dumpvars mode, dump the values of one or more legacy make variables, in
+shell syntax. The resulting output may be sourced directly into a shell to
+set corresponding shell variables.
+
+'report_config' is a special case that dumps a variable containing the
+human-readable config banner from the beginning of the build.
+
+```
+
+这两个函数差不多,区别仅在于 dump 的 var 的个数多少。内部核心都是调用的 `build.DumpMakeVars()`, 具体的代码实现在 `./build/soong/ui/build/dumpvars.go`
+
+而 `build.DumpMakeVars()` 内部最终封装的 `build.dumpMakeVars()`, 注意对于 "--make-mode" 内部如果要 BuildProductConfig 也会调用 `build.dumpMakeVars()` 这个函数。
+
+`build.dumpMakeVars()` 这个函数就非常有趣了,看它的代码实际上是用命令行的方式去执行一个叫做 `build/make/core/config.mk` 的脚本。
+
+这个脚本是从 Android 原先的 make 系统里遗留下来的,从该文件的最前面注释上来看,原先的 Android 的 build 系统中,top-level Makefile 会包含这个 config.mk 文件,这个文件根据 platform 的不同以及一些 configration 的不同设置了一些 standard variables,这些变量 `are not specific to what is being built`。
+
+这个 config.mk 会 include 大量的其他 mk 文件,这些文件存放在 BUILD_SYSTEM(`./build/make/common`) 和 BUILD_SYSTEM(`./build/make/core`) 下
+
+注意在这个 config.mk 的最后 include 了这么两个文件
+```
+ifeq ($(CALLED_FROM_SETUP),true)
+include $(BUILD_SYSTEM)/ninja_config.mk
+include $(BUILD_SYSTEM)/soong_config.mk
+endif
+```
+
+其中 `soong_config.mk` 里将大量 Soong 需要的,但原先定义在 mk 文件中的变量打印输出到 `out/soong/soong.variables` 这个文件中,这是一个 json 格式的文件,这也是我们所谓的 dump Make Vars 的含义。dump 出来后我们就可以随时使用了。生成的 jason 语法格式为:
+
+```
+$(call add_json_str, BuildId, $(BUILD_ID))
+$(call add_json_val, Platform_sdk_version, $(PLATFORM_SDK_VERSION))
+$(call add_json_csv, Platform_version_active_codenames, $(PLATFORM_VERSION_ALL_CODENAMES))
+$(call add_json_bool, Allow_missing_dependencies, $(ALLOW_MISSING_DEPENDENCIES))
+$(call add_json_list, ProductResourceOverlays, $(PRODUCT_PACKAGE_OVERLAYS))
+```
+
+对应打印生成的例子为:
+```
+"BuildId": "QP1A.191105.004",
+"Platform_sdk_version": 29,
+"Platform_version_active_codenames": ["REL"],
+"Allow_missing_dependencies": false,
+"ProductResourceOverlays": ["device/generic/goldfish/overlay"],
+```
+
+这个地方对于理解 Android 中从 make 到 Soong 的转换非常重要,看上去 Android 的思路还是先保留了原先 Make 的一套核心的 setup 逻辑,然后导出为 soong variables 供新的 Soong 使用,完成了转换。
+
+### 2.2.2. `soong_ui` 的 "--make-mode" 参数
+
+现在来看 `build.Build()` 这个核心函数, 源码在 `./soong/ui/build/build.go`, 略去所有辅助的步骤,只保留核心的步骤
+
+
+func Build(ctx Context, config Config, what int) {
+ ......
+
+ // what 传入的是 BuildAll = BuildProductConfig | BuildSoong | BuildKati | BuildNinja
+ // runMakeProductConfig 这个函数定义在 `./soong/ui/build/dumpvars.go`
+ // 对 config 设置环境变量,为下面 runKati/runNinja 做准备
+ // 具体的 runMakeProductConfig() 逻辑看上一节有关 dumpMakeVars 的分析
+ if what&BuildProductConfig != 0 {
+ // Run make for product config
+ runMakeProductConfig(ctx, config)
+ }
+
+ ......
+
+ // runSoong() 这个函数定义在 `./soong/ui/build/soong.go` 中,
+ // 是 Soong 系统的重点函数!!!
+ if what&BuildSoong != 0 {
+ // Run Soong
+ runSoong(ctx, config)
+ }
+ if what&BuildKati != 0 {
+ // Run ckati
+ // ......
+ }
+ // Write combined ninja file
+ createCombinedBuildNinjaFile(ctx, config)
+ if what&RunBuildTests != 0 {
+ testForDanglingRules(ctx, config)
+ }
+ if what&BuildNinja != 0 {
+ if !config.SkipMake() {
+ installCleanIfNecessary(ctx, config)
+ }
+ // Run ninja
+ runNinja(ctx, config)
+ }
+}
+
+
+对 Build 这个核心函数的分析来看,其实最重要的是 `runSoong()`, 这个函数最终生成了 `./out/soong/build.ninja`, `runNinja()` 啥的都是以这个最终的 ninja 文件作为输入,在这个基础上执行编译构建的工作。
+
+## 2.3. `build.runSoong()`
+
+参考 `./soong/ui/build/soong.go`
+
+这个函数中的每一步,都要搞清楚,否则没法彻底搞清楚基于 Soong 的 Android 构建过程。
+
+
+
+func runSoong(ctx Context, config Config) {
+ ctx.BeginTrace(metrics.RunSoong, "soong")
+ defer ctx.EndTrace()
+
+ // 本质上就是执行 `build/blueprint/bootstrap.bash -t` 这个脚本, “-t“ 表示执行测试
+ // BOOTSTRAP/BLUEPRINTDIR,我们获得了 blueprint 的源码位置
+ // BUILDDIR 确定了生成输出的二进制程序的位置在 out/soong 目录下
+ // NINJA_BUILDDIR 存放的是生成 .ninja_log/.ninja_deps 的位置
+ // GOROOT 指向 go 编译器的位置
+ // 查看 `build/blueprint/bootstrap.bash` 其主要工作流程:
+ // - 创建目录 $BUILDDIR/.minibootstrap,
+ // - 在上面创建的目录下创建一系列文件,最主要的包括
+ // - $BUILDDIR/.minibootstrap/build.ninja:这个文件的内容很关键,
+ // 是生成下一个阶段 bootstrap 的 目标的 ninja build 文件
+ // - $BUILDDIR/.minibootstrap/build-globs.ninja: 内容为空
+ // - $BUILDDIR/.blueprint.bootstrap:
+ // - ${BUILDDIR}/.out-dir
+ // - 拷贝了一个 $WRAPPER 到 $BUILDDIR 下, 具体的这个变量为空,所以没有什么动作
+ func() {
+ ctx.BeginTrace(metrics.RunSoong, "blueprint bootstrap")
+ defer ctx.EndTrace()
+ cmd := Command(ctx, config, "blueprint bootstrap", "build/blueprint/bootstrap.bash", "-t")
+ cmd.Environment.Set("BLUEPRINTDIR", "./build/blueprint")
+ cmd.Environment.Set("BOOTSTRAP", "./build/blueprint/bootstrap.bash")
+ cmd.Environment.Set("BUILDDIR", config.SoongOutDir())
+ cmd.Environment.Set("GOROOT", "./"+filepath.Join("prebuilts/go", config.HostPrebuiltTag()))
+ cmd.Environment.Set("BLUEPRINT_LIST_FILE", filepath.Join(config.FileListDir(), "Android.bp list"))
+ cmd.Environment.Set("NINJA_BUILDDIR", config.OutDir())
+ cmd.Environment.Set("SRCDIR", ".")
+ cmd.Environment.Set("TOPNAME", "Android.bp")
+ cmd.Sandbox = soongSandbox
+ cmd.RunAndPrintOrFatal()
+ }()
+
+ // 环境检查, 运行了一个 soong_env 的 程序, 实际测试好像也没有生成,也没有运行
+ func() {
+ ctx.BeginTrace(metrics.RunSoong, "environment check")
+ defer ctx.EndTrace()
+ envFile := filepath.Join(config.SoongOutDir(), ".soong.environment")
+ envTool := filepath.Join(config.SoongOutDir(), ".bootstrap/bin/soong_env")
+ if _, err := os.Stat(envFile); err == nil {
+ if _, err := os.Stat(envTool); err == nil {
+ cmd := Command(ctx, config, "soong_env", envTool, envFile)
+ cmd.Sandbox = soongSandbox
+ var buf strings.Builder
+ cmd.Stdout = &buf
+ cmd.Stderr = &buf
+ if err := cmd.Run(); err != nil {
+ ctx.Verboseln("soong_env failed, forcing manifest regeneration")
+ os.Remove(envFile)
+ }
+ if buf.Len() > 0 {
+ ctx.Verboseln(buf.String())
+ }
+ } else {
+ ctx.Verboseln("Missing soong_env tool, forcing manifest regeneration")
+ os.Remove(envFile)
+ }
+ } else if !os.IsNotExist(err) {
+ ctx.Fatalf("Failed to stat %f: %v", envFile, err)
+ }
+ }()
+
+ var cfg microfactory.Config
+ cfg.Map("github.com/google/blueprint", "build/blueprint")
+
+ cfg.TrimPath = absPath(ctx, ".")
+
+ // 利用 blueprint 的 microfactory 创建 minibp 这个可执行程序
+ // minibp 的源码在 `build/blueprint/bootstrap/minibp`
+ func() {
+ ctx.BeginTrace(metrics.RunSoong, "minibp")
+ defer ctx.EndTrace()
+ minibp := filepath.Join(config.SoongOutDir(), ".minibootstrap/minibp")
+ if _, err := microfactory.Build(&cfg, minibp, "github.com/google/blueprint/bootstrap/minibp") err != nil {
+ ctx.Fatalln("Failed to build minibp:", err)
+ }
+ }()
+
+ // 利用 blueprint 的 microfactory 创建 bpglob 这个可执行程序
+ // bpglob 的源码在 `build/blueprint/bootstrap/bpglob`
+ func() {
+ ctx.BeginTrace(metrics.RunSoong, "bpglob")
+ defer ctx.EndTrace()
+ bpglob := filepath.Join(config.SoongOutDir(), ".minibootstrap/bpglob")
+ if _, err := microfactory.Build(&cfg, bpglob, "github.com/google/blueprint/bootstrap/ pglob") err != nil {
+ ctx.Fatalln("Failed to build bpglob:", err)
+ }
+ }()
+
+ // 这里是定义一个匿名函数 ninja
+ ninja := func(name, file string) {
+ ctx.BeginTrace(metrics.RunSoong, name)
+ defer ctx.EndTrace()
+ fifo := filepath.Join(config.OutDir(), ".ninja_fifo")
+ nr := status.NewNinjaReader(ctx, ctx.Status.StartTool(), fifo)
+ defer nr.Close()
+ cmd := Command(ctx, config, "soong "+name,
+ config.PrebuiltBuildTool("ninja"),
+ "-d", "keepdepfile",
+ "-w", "dupbuild=err",
+ "-j", strconv.Itoa(config.Parallel()),
+ "--frontend_file", fifo,
+ "-f", filepath.Join(config.SoongOutDir(), file))
+ cmd.Sandbox = soongSandbox
+ cmd.RunAndPrintOrFatal()
+ }
+
+ // 利用 ninja,输入 `.minibootstrap/build.ninja`, 输出 `out/soong/.bootstrap/build.ninja`
+ // 至此可以认为 minibootstrap 阶段结束
+ ninja("minibootstrap", ".minibootstrap/build.ninja")
+
+ // 利用 ninja,输入 `out/soong/.bootstrap/build.ninja`, 输出 `out/soong/build.ninja`
+ ninja("bootstrap", ".bootstrap/build.ninja")
+}
+
+