Skip to content

Makefile与Kati

追踪 build/make、Soong UI 和 Kati 如何解析并求值 main.mk、导出 Kati Ninja,再与 Soong 输出汇合。

基于android-17.0.0_r1
AndroidMakefileKatickati

Makefile与Kati ​

本文面向已经读过 Soong构建链、Android.mk迁移链 和 Ninja构建系统 的读者。你需要知道 Make 变量、目标依赖和 Ninja 文件的基本含义,但不必先掌握整个产品配置体系。

本文回答一个边界明确的问题:Android 17 的 soong_ui 如何启动 ckati,ckati 如何从 build/make/core/main.mk 进入 Make parser、Evaluator 和 NinjaGenerator,最后产出哪个 Ninja 文件;这个文件又如何与 Soong Ninja 组成 combined.ninja。本文不把 Android.mk 语法写成字典,不展开 Kati 的所有 GNU Make 兼容函数,也不把 Kati 说成 Go 实现。

Android 17 manifest 不包含独立的 platform/build/kati project;运行时使用 prebuilts/build-tools 中的 ckati。因此正文把 Android 17 的集成源码和 Kati 上游实现分开标记:前者固定在 android-17.0.0_r1,后者固定到 Kati 上游 commit,用于解释二进制内部的 parser/evaluator/generator。

1. 双层边界 ​

Kati 不是另一个构建执行器。它读取 Makefile 并把目标图和命令写成 Ninja;真正执行编译命令的是之后的 Ninja/Siso。Soong UI 负责调用它,并把 Kati Ninja、Soong Ninja 和 packaging Ninja 组织到 combined 文件。

这里的 owner 分工决定故障定位:soong_ui 错误通常是命令、环境或输出路径问题;Kati 错误是 Make 解析/求值或 Ninja 导出问题;combined 文件错误则是两个 producer 的边界问题。

2. UI调用 ​

2.1 命令参数 ​

Android 17 的 runKatiBuild 把 build/make/core/main.mk 作为 -f 入口,传入 --ninja、输出目录、suffix、--regen 和多个 Android 专用错误检查开关。它还把 Soong 生成的 make_vars 和 Android.mk 桥接文件作为 Make 输入。

相关源码:

  • build/soong/ui/build/kati.go
  • build/make/core/main.mk

源码文件:build/soong/ui/build/kati.go

相关函数/类型:runKatiBuild

go
args := []string{
    "--writable", config.OutDir() + "/",
    "--werror_implicit_rules",
    "-f", "build/make/core/main.mk",
}

args = append(args,
    "SOONG_MAKEVARS_MK="+config.SoongMakeVarsMk(),
    "SOONG_ANDROID_MK="+config.SoongAndroidMk(),
    "TARGET_DEVICE_DIR="+config.TargetDeviceDir(),
    "KATI_PACKAGE_MK_DIR="+config.KatiPackageMkDir())

runKati(ctx, config, e, katiBuildSuffix, args, func(env *Environment) {})

SOONG_ANDROID_MK 的作用不是把 Android.bp 交给 Kati 解析,而是让 Soong 模块以 Make 变量/目标的兼容形式参与遗留 Make 图。Soong 和 Kati 仍各自拥有自己的主 Ninja producer。

2.2 运行选项 ​

runKati 统一追加 --ninja_dir、--ninja_suffix、--no_ninja_prelude、--regen、--gen_all_targets、find emulator 和多项 --werror_*。这些开关改变的是 Kati 的解析/导出边界,不是普通用户 Makefile 的默认语义。

源码文件:build/soong/ui/build/kati.go

相关函数/类型:runKati

go
args = append([]string{
    "--ninja",
    "--ninja_dir=" + config.OutDir(),
    "--ninja_suffix=" + config.KatiSuffix() + extraSuffix,
    "--no_ninja_prelude",
    "--use_ninja_phony_output",
    "--regen",
    "--ignore_optional_include=" + filepath.Join(config.OutDir(), "%.P"),
    "--detect_android_echo",
    "--use_find_emulator",
    "--werror_find_emulator",
    "--werror_suffix_rules",
    "--werror_real_to_phony",
    "--kati_stats",
}, args...)

--regen 使 Kati 为 Makefile、环境、wildcard 和 shell 结果建立再生成判断;--no_ninja_prelude 则允许多个 Kati Ninja 通过 subninja 合并,避免每个文件都写一份全局 prelude。

2.3 环境隔离 ​

UI 在启动 Kati 前会清掉 SOONG_USE_PARTIAL_COMPILE、BUILD_HOSTNAME 和 BUILD_NUMBER 等变量,并通过文件或替代变量传递稳定输入。目的不是隐藏变量,而是避免每次构建编号或主机名变化都触发 Kati 重新分析。

cmd.WaitOrFatal() 是进程完成屏障:只有 ckati 正常退出,后续 build/package 输出才可进入 combined 文件。stdout/stderr 通过 status.KatiReader 汇入统一构建 UI,但日志汇总不会改变 Kati 的退出状态。

3. Make入口 ​

3.1 main.mk ​

固定的 build/make/core/main.mk 首先要求 KATI 已定义,然后加载 config.mk、clang 配置、definitions、产品配置和 dex/preopt 等 Make 输入。它不是一个只负责递归 Android.mk 的薄包装,而是同时构造产品安装集合、模块依赖和 packaging 目标。

源码文件:build/make/core/main.mk

makefile
ifndef KATI
$(warning Calling make directly is no longer supported.)
$(warning Either use 'envsetup.sh; m' or 'build/soong/soong_ui.bash --make-mode')
$(error done)
endif

include build/make/core/config.mk
include $(BUILD_SYSTEM)/clang/config.mk
include $(BUILD_SYSTEM)/definitions.mk
include $(BUILD_SYSTEM)/art_config.mk
include $(BUILD_SYSTEM)/dex_preopt.mk

3.2 文件清单 ​

main.mk 在产品配置完成后读取 .module_paths/Android.mk.list,并根据 PRODUCT_ANDROIDMK_ALLOWLIST_FILE 决定是否限制 Android.mk 范围;Linux 构建才走这条 Android.mk 分支,Mac 只支持 Android.bp。它随后通过 foreach 和 eval include 顺序展开每个 Makefile。

源码文件:build/make/core/main.mk

makefile
ifeq ($(filter Linux,$(BUILD_OS)),)
  subdir_makefiles :=
else ifneq ($(PRODUCT_ANDROIDMK_ALLOWLIST_FILE),)
  subdir_makefiles += $(filter $(allowed_androidmk_files),$(file <$(OUT_DIR)/.module_paths/Android.mk.list))
else
  subdir_makefiles += $(file <$(OUT_DIR)/.module_paths/Android.mk.list)
endif

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

这个阶段的生效时机是 Make 求值期间:include 会把子文件的 AST/语句加入当前 Evaluator scope;它不是 Ninja 执行期间的动态 include。顺序错误、allowlist 漏项或生成的 .module_paths 过期,都会改变 Kati 看到的模块集合。

3.3 构建模板 ​

config.mk 把 CLEAR_VARS、BUILD_SHARED_LIBRARY、BUILD_PACKAGE、BUILD_JAVA_LIBRARY 等变量指向 build/make/core/*.mk 模板。Android.mk 的 include $(BUILD_*) 因而是一次 Make include 和变量展开,不是 Kati 内部的特殊“模块注册 API”。

源码文件:build/make/core/config.mk

makefile
CLEAR_VARS :=$= $(BUILD_SYSTEM)/clear_vars.mk
BUILD_STATIC_LIBRARY :=$= $(BUILD_SYSTEM)/static_library.mk
BUILD_SHARED_LIBRARY :=$= $(BUILD_SYSTEM)/shared_library.mk
BUILD_EXECUTABLE :=$= $(BUILD_SYSTEM)/executable.mk
BUILD_PACKAGE :=$= $(BUILD_SYSTEM)/package.mk
BUILD_PREBUILT :=$= $(BUILD_SYSTEM)/prebuilt.mk
BUILD_JAVA_LIBRARY :=$= $(BUILD_SYSTEM)/java_library.mk

因此 include $(CLEAR_VARS) 的 owner 是 Make 模板与变量 scope,include $(BUILD_SHARED_LIBRARY) 的 owner 是对应模板产生的规则和 ALL_MODULES.* 变量。Android.mk迁移链 的 androidmk 转换器只模拟有限模块边界,不等于这里的完整模板求值。

4. Kati内部 ​

4.1 解析节点 ​

Kati 上游 parser.cc 把一行 Make 文本分派为 assignment、rule、include、define、if/else/endif、export 或 command。它先形成 Stmt,并不在 parser 中完成变量求值。

源码文件:src/parser.cc(Kati 上游 project)

相关函数/类型:Parser::ParseRuleOrAssign / directive map

cpp
void ParseRuleOrAssign(std::string_view line) {
  size_t sep = FindThreeOutsideParen(line, ':', '=', ';');
  if (sep == std::string::npos || line[sep] == ';') {
    ParseRule(line, std::string::npos);
  } else if (line[sep] == '=') {
    ParseAssign(line, sep);
  } else if (sep + 1 < line.size() && line[sep + 1] == '=') {
    ParseAssign(line, sep + 1);
  } else {
    ParseRule(line, sep);
  }
}

const Parser::DirectiveMap Parser::make_directives_ = {
  {"include", &Parser::ParseInclude},
  {"ifdef", &Parser::ParseIfdef},
  {"ifeq", &Parser::ParseIfeq},
  {"else", &Parser::ParseElse},
  {"endif", &Parser::ParseEndif},
  {"define", &Parser::ParseDefine},
};

4.2 求值状态 ​

Kati 的 Stmt::Eval 把 AST 节点交给 Evaluator。EvalAssign 处理变量赋值,EvalRule 解析目标和 prerequisites,EvalInclude 读取并递归求值被包含文件,EvalIf 决定 true/false 分支。scope 与 include stack 在 Evaluator 中持有,而不是由 Ninja 接管。

源码文件:src/eval.cc(Kati 上游 project)

相关函数/类型:Evaluator dispatch

cpp
void Evaluator::EvalAssign(const AssignStmt* stmt) {
  loc_ = stmt->loc();
  Symbol lhs = stmt->GetLhsSymbol(this);
  bool needs_assign;
  Var* var = EvalRHS(lhs, stmt->rhs, stmt->orig_rhs, stmt->op,
                     stmt->directive == AssignDirective::OVERRIDE,
                     &needs_assign);
  if (needs_assign) {
    bool readonly;
    lhs.SetGlobalVar(var, stmt->directive == AssignDirective::OVERRIDE,
                     &readonly);
  }
}

void Evaluator::EvalInclude(const IncludeStmt* stmt) {
  const std::string&& pats = stmt->expr->Eval(this);
  for (std::string_view pat : WordScanner(pats)) {
    // ... 对每个 pattern 做 Glob,并对匹配文件调用 DoInclude
  }
}

变量的最终值是在当前 Evaluator scope 中求出的;因此 :=、=、+=、递归变量、include 顺序和条件表达式会改变后续 rule。不能把 Makefile 当作静态 JSON,也不能把 parser 产生的 AST 直接当作构建图。

4.3 规则节点 ​

Kati 的 EvalRule 把目标表达式、prerequisites、recipe 和 rule-specific assignment 交给规则对象。带 recipe 的 rule 会成为潜在 Ninja build edge;只有经过条件判断、变量展开和目标筛选后,才知道它是否出现在最终 Ninja。

5. Ninja导出 ​

5.1 生成器 ​

Kati 上游 NinjaGenerator::Generate 依次生成 Ninja、shell wrapper 和 stamp。GenerateNinja 写 rule、build、pool、变量和输出;GenerateStamp 写入输入文件与 undefined variable 信息,供 --regen 判断下一次是否需要重新生成。

源码文件:src/ninja.cc(Kati 上游 project)

相关函数/类型:NinjaGenerator

cpp
void Generate(const std::vector<NamedDepNode>& nodes,
              const std::string& orig_args) {
  GenerateNinja();
  GenerateShell();
  GenerateStamp(orig_args);
}

void GenerateNinja() {
  out << "# Generated by kati " << kGitVersion << "\n\n";
  // ... 写 pools、rules、build edges 和默认目标
}

5.2 输出命名 ​

Android UI 用 KatiSuffix 区分产品、coverage 和 Kati 参数。KatiBuildNinjaFile() 返回 out/build<suffix>.ninja,package 阶段另有 build<suffix>-packaging.ninja;Soong 输出则位于 out/soong/build*.ninja。不能用一个固定的 out/build.ninja 概括所有 Kati 输出。

输出生产者主要消费者失效输入
build<suffix>.ninjarunKatiBuild/ckaticombined Ninjamain.mk、Android.mk、环境、Kati 参数
build<suffix>-packaging.ninjarunKatiPackage/ckaticombined Ninjapackaging .mk 与产品安装集合
out/soong/build*.ninjasoong_buildcombined NinjaBP、产品变量、used environment
combined<suffix>.ninjaSoong UINinja/Siso三个 subninja 路径与 skip 配置

suffix 由 target product、coverage 和 Kati args 构成;过长时 UI 用 MD5 压缩,并把原 suffix 关系写入辅助文件。恢复旧输出时不能随意复制另一产品的 Kati Ninja,因为文件名本身编码了配置边界。

5.3 再生印章 ​

Kati stamp 保存 Makefile 列表、命令行参数、环境依赖和未定义变量信息。输入发生变化时,Kati 或 UI 会重新运行;只删除最终 .ninja 而保留不一致 stamp,可能让下一次判断失真。恢复时应让同一 suffix 的输出和 stamp 一起重建。

6. 汇合输出 ​

6.1 三个子图 ​

Android 17 的 Soong UI 会生成 Soong Ninja、Kati main build Ninja 和 Kati packaging Ninja。combinedBuildNinjaTemplate 用 subninja 依次包含 Kati build、Kati package 和 Soong Ninja;combined 文件是执行器入口,不是某一个 producer 的输出覆盖另一个。

源码文件:build/soong/ui/build/build.go

相关函数/类型:combinedBuildNinjaTemplate

go
builddir = {{.OutDir}}
{{if and (not .SkipKatiNinja) .HasKatiSuffix}}
subninja {{.KatiBuildNinjaFile}}
subninja {{.KatiPackageNinjaFile}}
{{end}}
subninja {{.SoongNinjaFile}}

6.2 依赖桥 ​

Soong 生成的 SOONG_ANDROID_MK 使 Make 侧能看到 Soong module 的兼容信息;Make 侧的 ALL_MODULES、安装集合和 packaging 规则又会影响 Kati Ninja。桥接文件不把两个图合成一个语言,而是给另一侧提供它需要的变量和目标。

6.3 执行时机 ​

combined Ninja 写完后,Soong UI 再选择 Ninja、N2、Siso 或 ninjago 执行 -f combined<suffix>.ninja。Kati 只负责生成 edge;当某个 edge 真正执行失败,日志来自 Ninja action,不应回头把所有失败都归因于 Kati。

7. 失败路径 ​

7.1 解析失败 ​

Make 语法错误、未闭合 define、非法 rule 或 include 文件不可读,会在 parser/读文件阶段失败。此时没有可靠的 Evaluator 状态,不能使用旧 Ninja 推断本次输入有效。

7.2 求值失败 ​

条件分支、递归变量、$(shell ...)、wildcard、只读变量和 Android 专用 .KATI_* 约束可能在 Evaluator 阶段失败或产生 warning。main.mk 中的 .KATI_READONLY、obsolete/deprecated variable 检查是产品构建策略的一部分,不是普通 GNU Make 的通用语义。

7.3 导出失败 ​

--werror_implicit_rules、--werror_suffix_rules、--werror_real_to_phony 和 --werror_writable 会把 Kati 能生成但 Android 不接受的图提升为错误。修复时先看 Kati 的错误类别和 include stack,再决定改 Make 输入、模板或产品配置。

7.4 执行失败 ​

Kati 成功写出 Ninja 后,编译器、脚本或 copy action 仍可能失败。此时应检查 combined Ninja 中的具体 edge、输入和 command;重新运行 Kati 只有在 Make 输入、stamp 或环境依赖变化时才必要。

8. 验证实验 ​

8.1 源码定位 ​

bash
rg -n "func runKati|func runKatiBuild|KatiBuildNinjaFile|KatiPackageNinjaFile" \
  build/soong/ui/build/kati.go build/soong/ui/build/config.go
rg -n "subninja|createCombinedBuildNinjaFile|RunKatiNinja" \
  build/soong/ui/build/build.go
rg -n "ifndef KATI|subdir_makefiles|eval include|SOONG_ANDROID_MK" \
  build/make/core/main.mk

第一组追踪 UI 到 ckati,第二组追踪三个 Ninja 的汇合,第三组追踪 main.mk 的入口、Android.mk 清单和 Soong 桥接。

8.2 Kati测试 ​

Kati 上游的 testcase/ 回归样例和 run_test.go 驱动覆盖 Make 输入到输出的兼容行为,src/ninja_test.cc 覆盖 Ninja 生成器的局部逻辑;Android 17 的 Soong UI 测试则验证命令参数、suffix、skip Kati 和 combined 文件选择。两类测试职责不同:前者针对 Kati 机制,后者针对 Android 如何调用它。

TestKati 会枚举 testcase/,按文件类型选择 GNU Make 对照或脚本执行;代表性输入包括 assign_types.mk、basic_dep.mk、circular_dep.mk、命令行变量和 include 用例。测试比较 Kati 与预期输出/退出行为,证明具体 Make 兼容案例,而不是证明 Android 的 main.mk 全树求值。

src/ninja_test.cc 更靠近 generator:它构造 dependency node 或命令输入,断言 Ninja 转义、输出和规则文本。即使 generator 测试通过,Android UI 仍可能因 suffix、环境或 combined 文件配置失败,所以要分别检查 Kati 本身和 Android 调用层。

8.3 读者复述 ​

给定一个 Android.mk 模块,先从 main.mk 的清单确认文件是否被 include,再找到 BUILD_* 模板,沿 Kati Evaluator 找变量和 rule 的生效顺序,最后在 build<suffix>.ninja 或 combined 文件中定位输出。若失败,先判断发生在 parser、Evaluator、Ninja export 还是 edge execution 阶段。

9. 边界收束 ​

Kati 的核心价值不是“让 Make 变成 Go”,而是把 Make 的动态求值结果稳定导出为 Ninja,并通过 stamp/regen 机制减少不必要的重生成。Android 17 的 Soong UI 再将 Kati 产物、Soong 产物和 packaging 产物以 subninja 组合,交给统一执行器。

本文没有声称 Android 17 manifest 提供 Kati 源码 project;Kati parser/evaluator/generator 的代码引用来自固定的 Kati 上游 commit。也没有把旧 GNU Make 行为、Kati warning 或 Ninja action failure 混成同一层结论。