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 @@ +![](./diagrams/android.png) + **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 @@ +![](./diagrams/android.png) + +**代码走读:对 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")
+}
+
+