美文网首页
Android 编译系统--05:Kati详解

Android 编译系统--05:Kati详解

作者: DarcyZhou | 来源:发表于2023-12-06 08:35 被阅读0次

    本文转载自:Kati详解-Android10.0编译系统(五)

    本文基于Android 10.0源码分析

    1.概述

      kati是Google专门为了Android而开发的一个小项目,基于Golang和C++。目的是为了把Android中的Makefile,转换成ninja文件。

      在最新的Android R(11)中,Google已经移除了/build/kati目录,只保留了一个预先编译出来的可执行文件:prebuilts/build-tools/linux-x86/bin/ckati,这意味着Google在逐渐从编译系统中移除kati,预计1-2个Android大版本,.mk文件全部都切换成.bp文件后,kati将会正式退出Android历史舞台。

    2.kati和ckati区别

      kati是go语言写的,而ckati是c++写的。kati官方文档对它的描述是:kati is an experimental GNU make clone。也就是说,kati是对等make命令的。只不过kati并不执行具体的编译工作,而是生成ninja文件。

      这里有个疑惑?为什么有两个版本的kati:kati/ckati?kati刚开始是使用Golang编写的,但是后来验证下来发现编译速度不行,于是改成C++编写,所以现在存在两个版本:kati、ckati。我们在Android 10.0编译过程中,是通过ckati来把makefile文件转换成ninja文件的。

      关于Go版本kati编译速度问题,可以通过kati自带文档:build/kati/INTERNALS.md来查看:

    编译系统5-1.PNG

      Go版本比C++版本有更多的不必要的字符串分配。至于Go本身,似乎GC是主要的展示器。例如,Android的构建系统定义了大约一百万个变量,缓冲区将永远不会被释放。,这种分配格局对于非代际GC(non-generational)是不利的。因此采用C++编译会减少缓冲区分配问题,提高编译速度,因此我们现在主要还是使用ckati进行mk文件的转换。

    3.Kati整体架构

      Kati由以下组件组成:

    • 解析器(Parser)

    • 评估器(Evaluator)

    • 依赖构建器(Dependency builder)

    • 执行器(Executor)

    • Ninja生成器(Ninja generator)

    Makefile有一些由零个或多个表达式组成的语句。有两个解析器和两个评估器, 一个用于Makefile的语句,另一个用于Makefile的表达式。

      GNU make的大部分用户可能不太关心评估器。但是,GNU make的评估器非常强大,并且是图灵完整的。对于Android的空构建,大部分时间都花在这个阶段。其他任务,例如构建依赖关系图和调用构建目标的stat函数,并不是瓶颈。这将是一个非常具体的Android特性。Android的构建系统使用了大量的GNU make黑魔法。

      评估器输出构建规则(build rules)和变量表(variable table)的列表。依赖构建器从构建规则列表中创建依赖图(dependency graph)。注意这一步不使用变量表。然后将使用执行器或Ninja生成器。无论哪种方式,Kati再次为命令行运行其评估器。该变量表再次用于此步骤。

    4.kati是如何生成的

    4.1 代码位置

      Android 10.0中kati的代码位置:build/kati,AOSP中自带编译好的ckati。

    prebuilts/build-tools/linux-x86/asan/bin/ckati
    prebuilts/build-tools/linux-x86/bin/ckati
    prebuilts/build-tools/darwin-x86/bin/ckati
    

    kati也是它也是一个独立发布的项目,在GitHub上的位置是google/kati。

    git clone https://github.com/google/kati.git
    

    4.2 kati的使用方法

      在Android的编译过程中,ckati会自动被使用,无须开发者担心。

      单独使用时,在包含Makefile的目录下,执行ckati,效果与make基本相同。执行ckati --ninja,可以根据Makefile生成build.ninja文件,并且附带env-aosp_arm.sh和ninja-aosp_arm.sh 。通过env-aosp_arm.sh来配置环境,通过执行./ninja-aosp_arm.sh来启动Ninja、使用build.ninja编译。

      除了--ninja以外,ckati支持很多其它参数。比如,和make一样,可以通过-f指定Makefile位置,通过-j指定线程数。另外,在kati项目的m2n脚本中,就可以看到以下的复杂用法:

    ${kati} --ninja ${ninja_suffix_flag} --ignore_optional_include=out/%.P --ignore_dirty=out/% --use_find_emulator --detect_android_echo --detect_depfiles --gen_all_targets ${goma_flag} ${extra_flags} ${targets}
    

    4.3 生成kati

      Android 10.0编译时都是使用编译好的ckati(prebuilts/build-tools/linux-x86/bin/ckati)进行makefile的转换,不会再编译一下ckati,但是我们可以看看ckati是如何被编译出来的。ckati的编译方法:

    cd  .../build/kati
    make ckati
    

    会在build/kati的目录中生成一个二进制文件ckati。

    4.4 kati生成过程

      在build/kati中有个Makefile,执行make时,会编译其中的内容。

    ## build/kati/Makefile
    all: ckati ckati_tests
    
    include Makefile.kati
    include Makefile.ckati
    
    test: all ckati_tests
      go test --ckati --ninja
    
    clean: ckati_clean
    
    .PHONY: test clean ckati_tests
    

    Makefile中有两个目标:ckat和ckati_tests,其中ckati就是我们要编译出来的内容,它对应的Makefile为 Makefile.ckati。

    4.4.1 Makefile.ckati

      从Makefile.ckati中可以看出,ckati通过C++进行编译,而且依赖于KATI_CXX_OBJS和KATI_CXX_GENERATED_OBJS。

    ## Makefile.ckati
    # Rule to build ckati into KATI_BIN_PATH
    $(KATI_BIN_PATH)/ckati: $(KATI_CXX_OBJS) $(KATI_CXX_GENERATED_OBJS)
           @mkdir -p $(dir $@)
           $(KATI_LD) -std=c++11 $(KATI_CXXFLAGS) -o $@ $^ $(KATI_LIBS)
    
    # Rule to build normal source files into object files in KATI_INTERMEDIATES_PATH
    $(KATI_CXX_OBJS) $(KATI_CXX_TEST_OBJS): $(KATI_INTERMEDIATES_PATH)/%.o: $(KATI_SRC_PATH)/%.cc
           @mkdir -p $(dir $@)
           $(KATI_CXX) -c -std=c++11 $(KATI_CXXFLAGS) -o $@ $<
    
    # Rule to build generated source files into object files in KATI_INTERMEDIATES_PATH
    $(KATI_CXX_GENERATED_OBJS): $(KATI_INTERMEDIATES_PATH)/%.o: $(KATI_INTERMEDIATES_PATH)/%.cc
           @mkdir -p $(dir $@)
           $(KATI_CXX) -c -std=c++11 $(KATI_CXXFLAGS) -o $@ $<
    

    ckati的编译log:

    g++ -c -std=c++11 -g -W -Wall -MMD -MP -O -DNOLOG -march=native -o main.o main.cc
    g++ -c -std=c++11 -g -W -Wall -MMD -MP -O -DNOLOG -march=native -o ninja.o ninja.cc
    g++ -c -std=c++11 -g -W -Wall -MMD -MP -O -DNOLOG -march=native -o parser.o parser.cc
    g++ -c -std=c++11 -g -W -Wall -MMD -MP -O -DNOLOG -march=native -o regen.o regen.cc
    g++ -c -std=c++11 -g -W -Wall -MMD -MP -O -DNOLOG -march=native -o rule.o rule.cc
    

    4.4.2 /build/kati/main.cc

      ckati的入口在main.cc中,调用栈如下:

    编译系统5-2.png

    4.4.2.1 main()

      main()主要步骤:

    • 进行环境的初始化,初始化makefile解析器,包括include、define、ifndef等语法规则;

    • 解析ckati传入的参数内容,例如:"--ninja""--regen"等;

    • 执行编译,最终生成build-xxxx.ninja文件;

    • 退出ckati。

      接下来针对相关的函数,进行分析。

    ## /build/kati/main.cc
    int main(int argc, char* argv[]) {
      if (argc >= 2 && !strcmp(argv[1], "--realpath")) {
        HandleRealpath(argc - 2, argv + 2);
        return 0;
      }
      Init();
      string orig_args;
      for (int i = 0; i < argc; i++) {
        if (i)
          orig_args += ' ';
        orig_args += argv[i];
      }
      g_flags.Parse(argc, argv);
      FindFirstMakefie();
      if (g_flags.makefile == NULL)
        ERROR("*** No targets specified and no makefile found.");
      // This depends on command line flags.
      if (g_flags.use_find_emulator)
        InitFindEmulator();
      int r = Run(g_flags.targets, g_flags.cl_vars, orig_args);
      Quit();
      return r;
    }
    

    4.4.2.2 Flags::Parse()

      解析ckati传入的参数内容,例如:"--ninja""--regen"等

    void Flags::Parse(int argc, char** argv) {
    ...
      for (int i = 1; i < argc; i++) {
        const char* arg = argv[i];
        bool should_propagate = true;
        int pi = i;
        if (!strcmp(arg, "-f")) {
          makefile = argv[++i];
          should_propagate = false;
        } else if (!strcmp(arg, "-c")) {
          is_syntax_check_only = true;
        } else if (!strcmp(arg, "-i")) {
          is_dry_run = true;
        } else if (!strcmp(arg, "-s")) {
          is_silent_mode = true;
        } else if (!strcmp(arg, "-d")) {
          enable_debug = true;
        } else if (!strcmp(arg, "--kati_stats")) {
          enable_stat_logs = true;
        } else if (!strcmp(arg, "--warn")) {
          enable_kati_warnings = true;
        } else if (!strcmp(arg, "--ninja")) {
          generate_ninja = true;
        } else if (!strcmp(arg, "--empty_ninja_file")) {
          generate_empty_ninja = true;
        } else if (!strcmp(arg, "--gen_all_targets")) {
          gen_all_targets = true;
        } else if (!strcmp(arg, "--regen")) {
          // TODO: Make this default.
          regen = true;
        }
    ...
      }
    }
    

    4.4.2.3 Run()

      根据传入的参数包含--ninja时,需要执行GenerateNinja(),Kati如果指定了--regen标志,则Kati会检查你的环境中的任何内容是否在上次运行后发生更改。如果Kati认为它不需要重新生成Ninja文件,它会很快完成。对于Android,第一次运行Kati需要接近30秒,但第二次运行只需要1秒。

    static int Run(const vector<Symbol>& targets,
                   const vector<StringPiece>& cl_vars,
                   const string& orig_args) {
      double start_time = GetTime();
    
    //传入参数包含--ninja 和 (--regen 或者--dump_kati_stamp)时,进入该流程
      if (g_flags.generate_ninja && (g_flags.regen || g_flags.dump_kati_stamp)) {
        ScopedTimeReporter tr("regen check time");
        if (!NeedsRegen(start_time, orig_args)) {
          fprintf(stderr, "No need to regenerate ninja file\n");
          return 0;
        }
        if (g_flags.dump_kati_stamp) {
          printf("Need to regenerate ninja file\n");
          return 0;
        }
        ClearGlobCache();
      }
      ...
    //传入参数包含--ninja时,需要执行GenerateNinja()
      if (g_flags.generate_ninja) {
        ScopedTimeReporter tr("generate ninja time");
        GenerateNinja(nodes, ev.get(), orig_args, start_time);
        ev->DumpStackStats();
        return 0;
      }
      ...
      return 0;
    }
    

    4.4.2.4 GenerateNinja()

      GenerateNinja()会先初始化一个 NinjaGenerator的结构,然后解析之前的makefile,并且将node进行整理,会将所依赖的.o;.a; .so进行归类,在整理好了依赖之后,会将所的步骤写入文件build-xxxx.ninja中。

    void GenerateNinja(const vector<NamedDepNode>& nodes,
                       Evaluator* ev,
                       const string& orig_args,
                       double start_time) {
      NinjaGenerator ng(ev, start_time);        //初始化了一个 NinjaGenerator的结构
      ng.Generate(nodes, orig_args);
    }
    
      void Generate(const vector<NamedDepNode>& nodes, const string& orig_args) {
        unlink(GetNinjaStampFilename().c_str());
        PopulateNinjaNodes(nodes); //对前面include的makefile进行解析,并且将node进行整理,会将所依赖的.o;.a; .so进行归类
        GenerateNinja();  //在整理好了依赖之后,会将所的步骤写入文件build-xxxx.ninja中
        GenerateShell();
        GenerateStamp(orig_args);
      }
    

    GenerateNinja() 会产生build-aosp_arm.ninja,GenerateShell()会产生env-aosp_arm.sh、ninja-aosp_arm.sh,这里我们主要关注build-aosp_arm.ninja的生成过程。

    void GenerateNinja() {
        ScopedTimeReporter tr("ninja gen (emit)");
        fp_ = fopen(GetNinjaFilename().c_str(), "wb");
        if (fp_ == NULL)
          PERROR("fopen(build.ninja) failed");
    
        fprintf(fp_, "# Generated by kati %s\n", kGitVersion);
        fprintf(fp_, "\n");
    
        if (!used_envs_.empty()) {
          fprintf(fp_, "# Environment variables used:\n");
          for (const auto& p : used_envs_) {
            fprintf(fp_, "# %s=%s\n", p.first.c_str(), p.second.c_str());
          }
          fprintf(fp_, "\n");
        }
    
        if (!g_flags.no_ninja_prelude) {
          if (g_flags.ninja_dir) {
            fprintf(fp_, "builddir = %s\n\n", g_flags.ninja_dir);
          }
    
          fprintf(fp_, "pool local_pool\n");
          fprintf(fp_, " depth = %d\n\n", g_flags.num_jobs);
    
          fprintf(fp_, "build _kati_always_build_: phony\n\n");
        }
    
        unique_ptr<ThreadPool> tp(NewThreadPool(g_flags.num_jobs));
        CHECK(g_flags.num_jobs);
        int num_nodes_per_task = nodes_.size() / (g_flags.num_jobs * 10) + 1;
        int num_tasks = nodes_.size() / num_nodes_per_task + 1;
        vector<ostringstream> bufs(num_tasks);
        for (int i = 0; i < num_tasks; i++) {
          tp->Submit([this, i, num_nodes_per_task, &bufs]() {
            int l =
                min(num_nodes_per_task * (i + 1), static_cast<int>(nodes_.size()));
            for (int j = num_nodes_per_task * i; j < l; j++) {
              EmitNode(nodes_[j], &bufs[i]);
            }
          });
        }
        tp->Wait();
    
        if (!g_flags.generate_empty_ninja) {
          for (const ostringstream& buf : bufs) {
            fprintf(fp_, "%s", buf.str().c_str());
          }
        }
    
        SymbolSet used_env_vars(Vars::used_env_vars());
        // PATH changes $(shell).
        used_env_vars.insert(Intern("PATH"));
        for (Symbol e : used_env_vars) {
          StringPiece val(getenv(e.c_str()));
          used_envs_.emplace(e.str(), val.as_string());
        }
    
        string default_targets;
        if (g_flags.targets.empty() || g_flags.gen_all_targets) {
          CHECK(default_target_);
          default_targets = EscapeBuildTarget(default_target_->output);
        } else {
          for (Symbol s : g_flags.targets) {
            if (!default_targets.empty())
              default_targets += ' ';
            default_targets += EscapeBuildTarget(s);
          }
        }
        if (!g_flags.generate_empty_ninja) {
          fprintf(fp_, "\n");
          fprintf(fp_, "default %s\n", default_targets.c_str());
        }
    
        fclose(fp_);
      }
    

    Kati认为,当更改以下任一项时,需要重新生成Ninja文件:

    • 传递给Kati的命令行标志;

    • 用于生成上一个ninja文件的Makefile的时间戳;

    • 评估Makefile时使用的环境变量;

    • $(wildcard ...)的结果;

    • $(shell ...)的结果。

    5.kati执行过程

      在第三节的make编译过程中,我们知道soong_ui执行编译时,会调用ckati把makefile编译成*.ninja文件,这里我们就看看具体的流程是如何执行的。

    5.1 soong_ui build调用栈

    编译系统5-3.png

      在之前的编译过程中,其中第三步和第四步,运行runKatiBuild()和runKatiPackage(),加载core/main.mk和packaging/main.mk,搜集所有的Android.mk文件,分别生成out/build-aosp_arm.ninja 和out/build-aosp_arm-package.ninja,这就是kati/ckati的编译过程。

      下面我们来一起看看具体的执行过程。

    5.2 runKatiBuild()

    // /build/soong/ui/build/kati.go
    func runKatiBuild(ctx Context, config Config) {
           ctx.BeginTrace(metrics.RunKati, "kati build")
           defer ctx.EndTrace()
    
           args := []string{
                   "--writable", config.OutDir() + "/",
                   "-f", "build/make/core/main.mk",
           }
    
           // PDK builds still uses a few implicit rules
           if !config.IsPdkBuild() {
                   args = append(args, "--werror_implicit_rules")
           }
    
           if !config.BuildBrokenDupRules() {
                   args = append(args, "--werror_overriding_commands")
           }
    
           if !config.BuildBrokenPhonyTargets() {
                   args = append(args,
                           "--werror_real_to_phony",
                           "--werror_phony_looks_real",
                           "--werror_writable")
           }
    
           args = append(args, config.KatiArgs()...)
    
           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, katiBuildSuffix, args, func(env *Environment) {})
    }
    

    这里的参数args,通过fmt打印后,内容为:

    [--writable out/ -f build/make/core/main.mk --werror_implicit_rules --werror_overriding_commands --werror_real_to_phony --werror_phony_looks_real --werror_writable SOONG_MAKEVARS_MK=out/soong/make_vars-aosp_arm.mk SOONG_ANDROID_MK=out/soong/Android-aosp_arm.mk TARGET_DEVICE_DIR=build/target/board/generic KATI_PACKAGE_MK_DIR=out/target/product/generic/obj/CONFIG/kati_packaging]
    

    这里指定了makefile的入口为build/make/core/main.mk,编译target的目录为build/target/board/generic。

    func runKati(ctx Context, config Config, extraSuffix string, args []string, envFunc func(*Environment)) {
           executable := config.PrebuiltBuildTool("ckati")
           args = append([]string{
                   "--ninja",
                   "--ninja_dir=" + config.OutDir(),
                   "--ninja_suffix=" + config.KatiSuffix() + extraSuffix,
                   "--no_ninja_prelude",
                   "--regen",
                   "--ignore_optional_include=" + filepath.Join(config.OutDir(), "%.P"),
                   "--detect_android_echo",
                   "--color_warnings",
                   "--gen_all_targets",
                   "--use_find_emulator",
                   "--werror_find_emulator",
                   "--no_builtin_rules",
                   "--werror_suffix_rules",
                   "--warn_real_to_phony",
                   "--warn_phony_looks_real",
                   "--top_level_phony",
                   "--kati_stats",
           }, args...)
    
           if config.Environment().IsEnvTrue("EMPTY_NINJA_FILE") {
                   args = append(args, "--empty_ninja_file")
           }
           cmd := Command(ctx, config, "ckati", executable, args...)
           cmd.Sandbox = katiSandbox
           pipe, err := cmd.StdoutPipe()
           if err != nil {
                   ctx.Fatalln("Error getting output pipe for ckati:", err)
           }
           cmd.Stderr = cmd.Stdout
    
           envFunc(cmd.Environment)
    
           if _, ok := cmd.Environment.Get("BUILD_USERNAME"); !ok {
                   u, err := user.Current()
                   if err != nil {
                           ctx.Println("Failed to get current user")
                   }
                   cmd.Environment.Set("BUILD_USERNAME", u.Username)
           }
    
           if _, ok := cmd.Environment.Get("BUILD_HOSTNAME"); !ok {
                   hostname, err := os.Hostname()
                   if err != nil {
                           ctx.Println("Failed to read hostname")
                   }
                   cmd.Environment.Set("BUILD_HOSTNAME", hostname)
           }
    
           cmd.StartOrFatal()
           status.KatiReader(ctx.Status.StartTool(), pipe)
           cmd.WaitOrFatal()
    }
    

    调用Command(),根据传入的参数,生成一个cmd的结构,其中相关参数如下: (1)args

    [--ninja --ninja_dir=out --ninja_suffix=-aosp_arm --no_ninja_prelude --regen --ignore_optional_include=out/%.P --detect_android_echo --color_warnings --gen_all_targets --use_find_emulator --werror_find_emulator --no_builtin_rules --werror_suffix_rules --warn_real_to_phony --warn_phony_looks_real --top_level_phony --kati_stats --writable out/ -f build/make/core/main.mk --werror_implicit_rules --werror_overriding_commands --werror_real_to_phony --werror_phony_looks_real --werror_writable SOONG_MAKEVARS_MK=out/soong/make_vars-aosp_arm.mk SOONG_ANDROID_MK=out/soong/Android-aosp_arm.mk TARGET_DEVICE_DIR=build/target/board/generic KATI_PACKAGE_MK_DIR=out/target/product/generic/obj/CONFIG/kati_packaging]
    

    (2)config

    {%!s(*build.configImpl=&{[] false 0xc00000ecc0 out/dist 16 1 false false false false [] [droid] -aosp_arm generic build/target/board/generic false false false false true})}
    

    (3)executable

    prebuilts/build-tools/linux-x86/bin/ckati
    

    Command封装方法如下:

    // /build/soong/ui/build/exec.go
    func Command(ctx Context, config Config, name string, executable string, args ...string) *Cmd {
           ret := &Cmd{
                   Cmd:         exec.CommandContext(ctx.Context, executable, args...),
                   Environment: config.Environment().Copy(),
                   Sandbox:     noSandbox,
                   ctx:    ctx,
                   config: config,
                   name:   name,
           }
           return ret
    }
    

    根据上述的相关参数可知,最终是调用系统准备好的prebuilts/build-tools/linux-x86/bin/ckati参与编译,其中传入的参数有--ninja\ --regen--detect_android_echo等,最终编译出build-aosp_arm.ninja。具体的kati实现过程比较复杂,这里就不详细展开,有兴趣的朋友,可以把/build/kati中的C++源码详细的分析一下。

    6.总结

      Kati的主要功能就是把Makefile转换成build-xxx.ninja文件从而来参与系统编译,随着Android逐步消除版本中的Makefile文件,Kati最终也会退出Android的历史舞台。

    相关文章

      网友评论

          本文标题:Android 编译系统--05:Kati详解

          本文链接:https://www.haomeiwen.com/subject/dbyxidtx.html