Android的Kati、Ninja、Android.bp、Blueprint与Soong简介

Android中的Kati

android makefile ninja

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

代码位置

在Android 7.0以上的平台项目中,kati的位置是build/kati/。 另外,平台代码也自带编译好的ckati

$ find prebuilts/ -name ckati
prebuilts/build-tools/linux-x86/asan/bin/ckati
prebuilts/build-tools/linux-x86/bin/ckati
prebuilts/build-tools/darwin-x86/bin/ckati

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

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

编译

在Git库中,可以用make来直接编译。 编译完成后,目录下会出现ckati这个可执行文件。 产生ckati后,通过执行./m2n,可以把Makefile转换成build.ninja文件。 接下来,可以通过执行ninja来再次编译。

在Android项目中,这个Git库自带Android.bp文件,可以作为一个模块自动跟随项目一起编译。 也可以在项目路径中,执行mm单独编译。 编译产物主要是ckati,会被安装到项目的out/host/linux-x86/bin/ckati,作为编译过程中主机环境的一部分。 (这里是使用Linux来编译。如果是Windows或Darwin,产物位置会略有不同。)

使用

在Android项目中,ckati会在编译过程中,自动被使用,无需操心。

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

. ./env.sh
exec ninja -f ./build.ninja "$@"

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

ckati --ninja --ignore_optional_include=out/%.P --ignore_dirty=out/% --use_find_emulator --detect_android_echo --detect_depfiles --gen_all_targets

然而,这些参数的具体含义,只能望文生义。 ckati的文档十分匮乏,不仅没有像样的网站,连帮助命令都没有。 不过,从其源文件find.cc中,还是可以看出一些端倪。

  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, "--gen_all_targets")) {
      gen_all_targets = true;
    } else if (!strcmp(arg, "--regen")) {
      // TODO: Make this default.
      regen = true;
    } else if (!strcmp(arg, "--regen_debug")) {
      regen_debug = true;
    } else if (!strcmp(arg, "--regen_ignoring_kati_binary")) {
      regen_ignoring_kati_binary = true;
    } else if (!strcmp(arg, "--dump_kati_stamp")) {
      dump_kati_stamp = true;
      regen_debug = true;
    } else if (!strcmp(arg, "--detect_android_echo")) {
      detect_android_echo = true;
    } else if (!strcmp(arg, "--detect_depfiles")) {
      detect_depfiles = true;
    } else if (!strcmp(arg, "--color_warnings")) {
      color_warnings = true;
    } else if (!strcmp(arg, "--werror_find_emulator")) {
      werror_find_emulator = true;
    } else if (!strcmp(arg, "--werror_overriding_commands")) {
      werror_overriding_commands = true;
    } else if (ParseCommandLineOptionWithArg(
        "-j", argv, &i, &num_jobs_str)) {
      num_jobs = strtol(num_jobs_str, NULL, 10);
      if (num_jobs <= 0) {
        ERROR("Invalid -j flag: %s", num_jobs_str);
      }
    } else if (ParseCommandLineOptionWithArg(
        "--remote_num_jobs", argv, &i, &num_jobs_str)) {
      remote_num_jobs = strtol(num_jobs_str, NULL, 10);
      if (remote_num_jobs <= 0) {
        ERROR("Invalid -j flag: %s", num_jobs_str);
      }
    } else if (ParseCommandLineOptionWithArg(
        "--ninja_suffix", argv, &i, &ninja_suffix)) {
    } else if (ParseCommandLineOptionWithArg(
        "--ninja_dir", argv, &i, &ninja_dir)) {
    } else if (!strcmp(arg, "--use_find_emulator")) {
      use_find_emulator = true;
    } else if (ParseCommandLineOptionWithArg(
        "--goma_dir", argv, &i, &goma_dir)) {
    } else if (ParseCommandLineOptionWithArg(
        "--ignore_optional_include",
        argv, &i, &ignore_optional_include_pattern)) {
    } else if (ParseCommandLineOptionWithArg(
        "--ignore_dirty",
        argv, &i, &ignore_dirty_pattern)) {
    } else if (ParseCommandLineOptionWithArg(
        "--no_ignore_dirty",
        argv, &i, &no_ignore_dirty_pattern)) {
    } else if (arg[0] == '-') {
      ERROR("Unknown flag: %s", arg);
    } else {
      if (strchr(arg, '=')) {
        cl_vars.push_back(arg);
      } else {
        should_propagate = false;
        targets.push_back(Intern(arg));
      }
    }

总结

kati是一个基于Makefile来生成ninja.build的小项目。 它是Google专为Android而开发,用来修正Android项目创建之初的错误——使用Makefile来做一个超复杂的编译构建系统。

在编译过程中,kati负责把既有的Makefile、Android.mk,转换成Ninja文件。 在Android 7.0中,它独挑大梁。 在Android 8.0以后,它与Soong一起,成为Ninja文件的两大来源。 也许,在几个大版本后,它会与原先的Makefile、Android.mk一起,退出Android平台编译系统的大舞台。

在单独使用时,它对普通的小项目还能勉强生效。 面对复杂的、多嵌套的Makefile时,它往往无法支持,会出现各种各样的问题。 当然,也可以理解为,它只为Android而设计。

总之,不推荐在Android以外的任何项目中使用kati。

Android中的Ninja简介

android makefile ninja

如果说Makefile是一个DSL,那么Ninja就是一种配置文件。 本文简单介绍Android中的Ninja。

Makefile与Ninja的对比

二者最核心的区别,在于设计哲学。 Makefile是设计来给人手写的,而Ninja设计出来是给其它程序生成的。 如果说Makefile是C语言,那么Ninja就是汇编语言。 如果说Makefile是一个DSL,那么Ninja就是一种配置文件。 Makefile支持分支、循环等流程控制,而Ninja只支持一些固定形式的配置。

二者的相同点是,都是为了控制编译流程而设计。 所以,他们的核心功能,都是指定目标,以及目标之间的依赖关系,自动计算执行顺序。

与Makefile相比,由于Ninja仅仅专注于核心的功能,所以有轻巧、速度快的优点。

Makefile默认文件名为Makefilemakefile,也常用.make.mk作为文件后缀。 Ninja的默认文件名是build.ninja,其它文件也以.ninja为后缀。 执行Makefile的程序,默认是GNU make,也有一些其它的实现。 Ninja的执行程序,就是ninja命令。

在Android项目中,make需要编译主机上安装,作为环境的一部分。 而ninja命令则是Android平台代码自带。

$ find prebuilts/ -name ninja
prebuilts/build-tools/linux-x86/asan/bin/ninja
prebuilts/build-tools/linux-x86/bin/ninja
prebuilts/build-tools/darwin-x86/bin/ninja

ninja命令的用法

通过ninja -h,可以看到该命令的帮助文档。

$ ninja -h
usage: ninja [options] [targets...]

if targets are unspecified, builds the 'default' target (see manual).

options:
  --version  print ninja version ("1.7.2")

  -C DIR   change to DIR before doing anything else
  -f FILE  specify input build file [default=build.ninja]

  -j N     run N jobs in parallel [default=6, derived from CPUs available]
  -k N     keep going until N jobs fail [default=1]
  -l N     do not start new jobs if the load average is greater than N
  -n       dry run (don't run commands but act like they succeeded)
  -v       show all command lines while building

  -d MODE  enable debugging (use -d list to list modes)
  -t TOOL  run a subtool (use -t list to list subtools)
    terminates toplevel options; further flags are passed to the tool
  -w FLAG  adjust warnings (use -w list to list warnings)

很多参数,和make是比较类似的,比如-f-j等,不再赘述。 有趣的是-t-d-w这三个参数,最有用的是-t

$ ninja -t list
ninja subtools:
    browse  browse dependency graph in a web browser
     clean  clean built files
  commands  list all commands required to rebuild given targets
      deps  show dependencies stored in the deps log
     graph  output graphviz dot file for targets
     query  show inputs/outputs for a path
   targets  list targets by their rule or depth in the DAG
    compdb  dump JSON compilation database to stdout
 recompact  recompacts ninja-internal data structures

ninja -t clean是清理产物,是自带的,而make clean往往需要自己实现。 其它都是查看编译过程信息的工具,各有作用,可以进行复杂的编译依赖分析。

Ninja的专注,在这里完全超越了Makefile。

Android中的Ninja文件

从Android 7开始,编译时默认使用Ninja。 但是,Android项目里是没有.ninja文件的。 遵循Ninja的设计哲学,编译时,会先把Makefile通过kati转换成.ninja文件,然后使用ninja命令进行编译。 这些.ninja文件,都产生在out/目录下,共有三类。

一类是build-*.ninja文件,通常非常大,几十到几百MB。 对make全编译,命名是build-.ninja。 如果Makefile发生修改,需要重新产生Ninja文件。

这里Android有一个bug,或者说设计失误。 mmmma的Ninja文件,命名是build--.ninja。 而mmmmmma的Ninja文件,命名是build--_.ninja。 显然,不同的单模块编译,产生的也是不同的Ninja文件。

这个设计本身就有一些问题了,为什么不同模块不能共用一个总的Ninja文件? 这大概还是为了兼容旧的Makefile设计。 在某些Android.mk中,单模块编译与全编译时,编译内容截然不同。 如果说这还只能算是设计失误的话,那么mmmmm使用不同的编译文件,就是显然的bug了。 二者相差一个下划线_,通过mvcp,可以通用。

第二类是combined-*.ninja文件。 在使用了Soong后,除了build-*.ninja之外,还会产生对应的combined-*.ninja,二者的*内容相同。 以下以AOSP的aosp_arm64-eng为例,展示out/combined-aosp_arm64.ninja文件的内容。

builddir = out
include out/build-aosp_arm64.ninja
include out/soong/build.ninja
build out/combined-aosp_arm64.ninja: phony out/soong/build.ninja

这类是组合文件,是把build-*.ninjaout/soong/build.ninja组合起来。 所以,使用Soong后,combined-*.ninja是编译执行的真正入口。

第三类是out/soong/build.ninja文件,它是从所有的Android.bp转换过来的。

build-*.ninja是从所有的Makefile,用Kati转换过来的,包括build/core/*.mk和所有的Android.mk。 所以,在不使用Soong时,它是唯一入口。 在使用了Soong以后,会新增源于Android.bpout/soong/build.ninja,所以需要combined-*.ninja来组合一下。

可以通过以下命令,单独产生全编译的Ninja文件。

make nothing

用ninja编译

在产生全编译的Ninja文件后,可以绕过Makefile,单独使用ninja进行编译。

全编译(7.0版本),相当于make

ninja -f out/build-aosp_arm64.ninja

单独编译模块,比如Settings,相当于make Settings

ninja -f out/build-aosp_arm64.ninja Settings

在8.0以上,上面的文件应该替换为out/combined-aosp_arm64.ninja,否则可能找不到某些Target。

另外,还有办法不用输入-f参数。 如前所述,如同Makefile之于makeninja默认的编译文件是build.ninja。 所以,使用软链接,可以避免每次输入繁琐的-f

ln -s out/combined-aosp_arm64.ninja build.ninja
ninja Settings

ninja进行单模块编译的好处,除了更快以外,还不用生成单模块的Ninja文件,省了四五分钟。

总结

在以Ninja在实际编译中替换Makefile以后,Android在编译时更快了一些。 不过,在首次生成、或重新生成Ninja文件时,往往额外耗时数分钟,反而比原先使用Makefile更慢了。

在增量编译方面,原先由于其Makefile编译系统的实现问题,是不完善的。 也就是说,在make编译完一个项目后,如果再执行make,会花费较长时间重新编译部分内容。 而使用Ninja以后,增量编译做得比较完善,第二次make将在一分钟内结束。

除此之外,由于Ninja的把编译流程集中到了一个文件,并且提供了一些工具命令。 所以编译信息的提取、编译依赖的分析,变得更加方便了。

Android编译系统中的Android.bp、Blueprint与Soong

android ninja

本文简单介绍Android Nougat(7.0)中引入的Android.bp,及其相关工具链。

简介

Android.bp,是用来替换Android.mk的配置文件。 它使用Blueprint框架来解析,最终转换成Ninja文件。

与Android.mk不同的是,Android.bp是纯粹的配置文件,不包含分支、循环等流程控制,也不能做算数、逻辑运算。 与此同时,Ninja文件也是如此。 这就产生了一些新的问题与需求——在Android项目上进行选择编译、解析配置、转换成Ninja等——Soong应运而生。 Soong其实就相当于Makefile编译系统的核心,即build/make/core/下面的内容。 它负责提供Android.bp的含义定义与解析,并将之转换为Ninja文件。。

此外,Soong还会编译产生一个androidmk命令,可以手动把Android.mk转换成Android.bp。 这只对无选择、循环等复杂流程控制的Android.mk生效。

Blueprint和Soong都是由Golang写的项目。 从Android Nougat开始,prebuilts/go/目录下新增了Golang所需的运行环境,在编译时使用。

Android.bp以及相关支持,从Android Nougat开始加入,从Android Oreo(8.0)开始默认开启。 如果需要在Android Nougat的版本使用,需要在执行编译时添加变量。

make 'USE_SOONG=true'

单独编译blueprint

启用Soong以后,在Android编译最开始的准备阶段,会执行build/soong/soong.bash进行环境准备。 其中会先编译、安装Blueprint到out目录下。 也就是说,在编译Android项目时,Android.bp相关工具链会自动编译,无需费神。

Soong是与Android强关联的一个项目,而Blueprint则相对比较独立,可以单独编译、使用。

编译Blueprint,首先要具备Golang环境。 然后,按照以下步骤执行命令。

go get github.com/google/blueprint
cd $GOPATH/src/github.com/google/blueprint
./bootstrap.bash
./blueprint.bash
ls bin

在新生成的bin目录中,包含4个可执行文件:

  • bpfmt
  • bpmodify
  • microfactory
  • minibp

由于文档较少,甚至连帮助命令都不包括命令的描述,所以其作用只能望文生义。

工具链关系

Android.mk、Android.bp、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-.ninja文件。 而从Android.bp,则会生成out/soong/build.ninja。 此外,还会生成一个较小的out/combined-.ninja文件,负责把二者组合起来,作为执行入口。

最终,Ninja文件才是真正直接控制源码编译的工具。

Android.bp

样例与基本概念

    // Android.bp sample
    cc_defaults(
        deps = [
            "libc",
        ],
    )

    cc_library(
        name = "cmd",
        srcs = [
            "main.c",
        ],
    )

    subdirs = ["subdir1", "subdir2"]

前面的样例中,cc_library这种()前面的,就是模块(module)。 这里module的概念,直接对应Android.mk中module的概念。 而=前面的namesrcs等,就是该模块的属性(property)。

subdirs是一个文件级的顶层属性,指定后会查找次级目录下的Android.bp。 类似于Android.mk中常用的include $(call all-subdir-makefiles)

模块是可以继承属性的。 cc_defaults就是一个文件中所有模块的父模块,可以指定公用的属性。 在以上代码中,cc_library模块虽然没有指定,但已经包含了deps属性。

语法

Blueprint文件的语法比较简单,毕竟只是配置文件。

变量与属性都是动态强类型的,赋值时确定。 变量类型只有四种。

  1. Bool(truefalse
  2. 字符串Strings("string")
  3. 字符串列表(["string1", "string2"]
  4. 映射关系Map({key1: "value1", key2: ["value2"]}

注释方式,与Golang类似。 支持行注释// line与块注释/* block */

操作符除了赋值的=以外,只有+

常用工具

虽然编译过程中的相关很多,不过在开发过程中可能需要手动执行的命令却不多。

一个是格式化工具bpfmt。 与gofmt类似,可以格式化Blueprint文件。 (其实,代码基本上都是从gofmt复制而来。)

例如,格式化当前目录及其递归子目录下的所有Android.bp:

bpfmt -w .

另一个是androidmk,负责转换Android.mk为Android.bp。 其实,现阶段没有必要学会写Android.bp,通过写Android.mk来转换也行。

androidmk Android.mk > Android.bp

Android.mk转换Android.bp实例

下面,以一个AOSP上的简单模块,system/core/sdcard/Android.mk,来做为案例。

    LOCAL_PATH := $(call my-dir)

    include $(CLEAR_VARS)

    LOCAL_SRC_FILES := sdcard.cpp fuse.cpp
    LOCAL_MODULE := sdcard
    LOCAL_CFLAGS := -Wall -Wno-unused-parameter -Werror
    LOCAL_SHARED_LIBRARIES := libbase libcutils libminijail libpackagelistparser

    LOCAL_SANITIZE := integer

    include $(BUILD_EXECUTABLE)

这是一个编译二进制可执行文件的小模块,内容非常简单。 通过执行androidmk Android.mk > Android.bp,可以转换成Android.bp。

    cc_binary {
        srcs: [
            "sdcard.cpp",
            "fuse.cpp",
        ],
        name: "sdcard",
        cflags: [
            "-Wall",
            "-Wno-unused-parameter",
            "-Werror",
        ],
        shared_libs: [
            "libbase",
            "libcutils",
            "libminijail",
            "libpackagelistparser",
        ],
        sanitize: {
            misc_undefined: ["integer"],
        },
    }

可以看出,虽然行数变多,但其实含义更明确了。 这个名为sdcard的模块,源码有两个cpp文件,依赖库有四个。 cc_binary,就相当于include $(BUILD_EXECUTABLE)。 转换前后,该有的信息都在,只是表达方式变化了而已。

注意:如果Android.mk中包含复杂的逻辑,则转换结果会有问题,详见结果文件中的注释

至于Android.bp支持多少像cc_binarycc_library这样的模块,每个模块又支持多少像namecflags这样的属性, 则只能去查找Soong的文档。

文档

目前(2017年),整个Android.bp工具链,都处于文档极度缺失的阶段。 除了官方那点可怜的README以外,基本只能去看代码与注释,参考其它已经存在的Android.bp。

另外,在已经使用Soong编译的项目中,out/soong/.bootstrap/docs/soong_build.html描述了所有的可用模块及其属性。 这多少缓解了两眼一抹黑症状,不算太过难受。 实际上,整个Soong仍然处于发展期,Google肆无忌惮地修改,完全没考虑兼容。 在8.0.0写的Android.bp,也许在8.0.1就会编译失败。 这或许是文档与编译绑定的真意吧。 等Soong完全成熟了,也许Android开发官网,就会有详尽的信息。

本站提供了从AOSP的android-8.0.0-r9,编译出来的一个soong_build.html,仅供参考。

转载自

  • 《Android中的Kati》
  • 《Android中的Ninja简介》
  • 《Android编译系统中的Android.bp、Blueprint与Soong》

你可能感兴趣的:(Android的Kati、Ninja、Android.bp、Blueprint与Soong简介)