Android中的Kati
kati是Google專門為了Android而開發(fā)的一個小項目,基于Golang和C++。 目的是為了把Android中的Makefile,轉(zhuǎn)換成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
它也是一個獨立發(fā)布的項目,在GitHub上的位置是google/kati。
git clone https://github.com/google/kati.git
編譯
在Git庫中,可以用make來直接編譯。 編譯完成后,目錄下會出現(xiàn)ckati這個可執(zhí)行文件。 產(chǎn)生ckati后,通過執(zhí)行./m2n,可以把Makefile轉(zhuǎn)換成build.ninja文件。 接下來,可以通過執(zhí)行ninja來再次編譯。
在Android項目中,這個Git庫自帶Android.bp文件,可以作為一個模塊自動跟隨項目一起編譯。 也可以在項目路徑中,執(zhí)行mm單獨編譯。 編譯產(chǎn)物主要是ckati,會被安裝到項目的out/host/linux-x86/bin/ckati,作為編譯過程中主機環(huán)境的一部分。 (這里是使用Linux來編譯。如果是Windows或Darwin,產(chǎn)物位置會略有不同。)
使用
在Android項目中,ckati會在編譯過程中,自動被使用,無需操心。
單獨使用時,在包含Makefile的目錄下,執(zhí)行ckati,效果與make基本相同。 執(zhí)行ckati --ninja,可以根據(jù)Makefile生成build.ninja文件,并且附帶env.sh和ninja.sh。 通過env.sh來配置環(huán)境,通過執(zhí)行./ninja.sh來啟動Ninja、使用build.ninja編譯。 生成的ninja.sh文件,主要內(nèi)容如下。
. ./env.sh
exec ninja -f ./build.ninja "$@"
除了--ninja以外,ckati支持很多其它參數(shù)。 比如,和make一樣,可以通過-f指定Makefile位置,通過-j指定線程數(shù)。 另外,在kati項目的m2n腳本中,就可以看到以下的復(fù)雜用法。
ckati --ninja --ignore_optional_include=out/%.P --ignore_dirty=out/% --use_find_emulator --detect_android_echo --detect_depfiles --gen_all_targets
然而,這些參數(shù)的具體含義,只能望文生義。 ckati的文檔十分匱乏,不僅沒有像樣的網(wǎng)站,連幫助命令都沒有。 不過,從其源文件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));
}
}
總結(jié)
kati是一個基于Makefile來生成ninja.build的小項目。 它是Google專為Android而開發(fā),用來修正Android項目創(chuàng)建之初的錯誤——使用Makefile來做一個超復(fù)雜的編譯構(gòu)建系統(tǒng)。
在編譯過程中,kati負責(zé)把既有的Makefile、Android.mk,轉(zhuǎn)換成Ninja文件。 在Android 7.0中,它獨挑大梁。 在Android 8.0以后,它與Soong一起,成為Ninja文件的兩大來源。 也許,在幾個大版本后,它會與原先的Makefile、Android.mk一起,退出Android平臺編譯系統(tǒng)的大舞臺。
在單獨使用時,它對普通的小項目還能勉強生效。 面對復(fù)雜的、多嵌套的Makefile時,它往往無法支持,會出現(xiàn)各種各樣的問題。 當(dāng)然,也可以理解為,它只為Android而設(shè)計。
總之,不推薦在Android以外的任何項目中使用kati。
Android中的Ninja簡介
如果說Makefile是一個DSL,那么Ninja就是一種配置文件。 本文簡單介紹Android中的Ninja。
Makefile與Ninja的對比
二者最核心的區(qū)別,在于設(shè)計哲學(xué)。 Makefile是設(shè)計來給人手寫的,而Ninja設(shè)計出來是給其它程序生成的。 如果說Makefile是C語言,那么Ninja就是匯編語言。 如果說Makefile是一個DSL,那么Ninja就是一種配置文件。 Makefile支持分支、循環(huán)等流程控制,而Ninja只支持一些固定形式的配置。
二者的相同點是,都是為了控制編譯流程而設(shè)計。 所以,他們的核心功能,都是指定目標,以及目標之間的依賴關(guān)系,自動計算執(zhí)行順序。
與Makefile相比,由于Ninja僅僅專注于核心的功能,所以有輕巧、速度快的優(yōu)點。
Makefile默認文件名為Makefile或makefile,也常用.make或.mk作為文件后綴。 Ninja的默認文件名是build.ninja,其它文件也以.ninja為后綴。 執(zhí)行Makefile的程序,默認是GNU make,也有一些其它的實現(xiàn)。 Ninja的執(zhí)行程序,就是ninja命令。
在Android項目中,make需要編譯主機上安裝,作為環(huán)境的一部分。 而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)
很多參數(shù),和make是比較類似的,比如-f、-j等,不再贅述。 有趣的是-t、-d、-w這三個參數(shù),最有用的是-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是清理產(chǎn)物,是自帶的,而make clean往往需要自己實現(xiàn)。 其它都是查看編譯過程信息的工具,各有作用,可以進行復(fù)雜的編譯依賴分析。
Ninja的專注,在這里完全超越了Makefile。
Android中的Ninja文件
從Android 7開始,編譯時默認使用Ninja。 但是,Android項目里是沒有.ninja文件的。 遵循Ninja的設(shè)計哲學(xué),編譯時,會先把Makefile通過kati轉(zhuǎn)換成.ninja文件,然后使用ninja命令進行編譯。 這些.ninja文件,都產(chǎn)生在out/目錄下,共有三類。
一類是build-*.ninja文件,通常非常大,幾十到幾百MB。 對make全編譯,命名是build-<product_name>.ninja。 如果Makefile發(fā)生修改,需要重新產(chǎn)生Ninja文件。
這里Android有一個bug,或者說設(shè)計失誤。 mm、mma的Ninja文件,命名是build-<product_name>-<path_to_Android.mk>.ninja。 而mmm、mmma的Ninja文件,命名是build-<product_name>-_<path_to_Android.mk>.ninja。 顯然,不同的單模塊編譯,產(chǎn)生的也是不同的Ninja文件。
這個設(shè)計本身就有一些問題了,為什么不同模塊不能共用一個總的Ninja文件? 這大概還是為了兼容舊的Makefile設(shè)計。 在某些Android.mk中,單模塊編譯與全編譯時,編譯內(nèi)容截然不同。 如果說這還只能算是設(shè)計失誤的話,那么mm與mmm使用不同的編譯文件,就是顯然的bug了。 二者相差一個下劃線_,通過mv或cp,可以通用。
第二類是combined-*.ninja文件。 在使用了Soong后,除了build-*.ninja之外,還會產(chǎn)生對應(yīng)的combined-*.ninja,二者的*內(nèi)容相同。 以下以AOSP的aosp_arm64-eng為例,展示out/combined-aosp_arm64.ninja文件的內(nèi)容。
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-*.ninja和out/soong/build.ninja組合起來。 所以,使用Soong后,combined-*.ninja是編譯執(zhí)行的真正入口。
第三類是out/soong/build.ninja文件,它是從所有的Android.bp轉(zhuǎn)換過來的。
build-*.ninja是從所有的Makefile,用Kati轉(zhuǎn)換過來的,包括build/core/*.mk和所有的Android.mk。 所以,在不使用Soong時,它是唯一入口。 在使用了Soong以后,會新增源于Android.bp的out/soong/build.ninja,所以需要combined-*.ninja來組合一下。
可以通過以下命令,單獨產(chǎn)生全編譯的Ninja文件。
make nothing
用ninja編譯
在產(chǎn)生全編譯的Ninja文件后,可以繞過Makefile,單獨使用ninja進行編譯。
全編譯(7.0版本),相當(dāng)于make:
ninja -f out/build-aosp_arm64.ninja
單獨編譯模塊,比如Settings,相當(dāng)于make Settings:
ninja -f out/build-aosp_arm64.ninja Settings
在8.0以上,上面的文件應(yīng)該替換為out/combined-aosp_arm64.ninja,否則可能找不到某些Target。
另外,還有辦法不用輸入-f參數(shù)。 如前所述,如同Makefile之于make,ninja默認的編譯文件是build.ninja。 所以,使用軟鏈接,可以避免每次輸入繁瑣的-f。
ln -s out/combined-aosp_arm64.ninja build.ninja
ninja Settings
用ninja進行單模塊編譯的好處,除了更快以外,還不用生成單模塊的Ninja文件,省了四五分鐘。
總結(jié)
在以Ninja在實際編譯中替換Makefile以后,Android在編譯時更快了一些。 不過,在首次生成、或重新生成Ninja文件時,往往額外耗時數(shù)分鐘,反而比原先使用Makefile更慢了。
在增量編譯方面,原先由于其Makefile編譯系統(tǒng)的實現(xiàn)問題,是不完善的。 也就是說,在make編譯完一個項目后,如果再執(zhí)行make,會花費較長時間重新編譯部分內(nèi)容。 而使用Ninja以后,增量編譯做得比較完善,第二次make將在一分鐘內(nèi)結(jié)束。
除此之外,由于Ninja的把編譯流程集中到了一個文件,并且提供了一些工具命令。 所以編譯信息的提取、編譯依賴的分析,變得更加方便了。
Android編譯系統(tǒng)中的Android.bp、Blueprint與Soong
本文簡單介紹Android Nougat(7.0)中引入的Android.bp,及其相關(guān)工具鏈。
簡介
Android.bp,是用來替換Android.mk的配置文件。 它使用Blueprint框架來解析,最終轉(zhuǎn)換成Ninja文件。
與Android.mk不同的是,Android.bp是純粹的配置文件,不包含分支、循環(huán)等流程控制,也不能做算數(shù)、邏輯運算。 與此同時,Ninja文件也是如此。 這就產(chǎn)生了一些新的問題與需求——在Android項目上進行選擇編譯、解析配置、轉(zhuǎn)換成Ninja等——Soong應(yīng)運而生。 Soong其實就相當(dāng)于Makefile編譯系統(tǒng)的核心,即build/make/core/下面的內(nèi)容。 它負責(zé)提供Android.bp的含義定義與解析,并將之轉(zhuǎn)換為Ninja文件。。
此外,Soong還會編譯產(chǎn)生一個androidmk命令,可以手動把Android.mk轉(zhuǎn)換成Android.bp。 這只對無選擇、循環(huán)等復(fù)雜流程控制的Android.mk生效。
Blueprint和Soong都是由Golang寫的項目。 從Android Nougat開始,prebuilts/go/目錄下新增了Golang所需的運行環(huán)境,在編譯時使用。
Android.bp以及相關(guān)支持,從Android Nougat開始加入,從Android Oreo(8.0)開始默認開啟。 如果需要在Android Nougat的版本使用,需要在執(zhí)行編譯時添加變量。
make 'USE_SOONG=true'
單獨編譯blueprint
啟用Soong以后,在Android編譯最開始的準備階段,會執(zhí)行build/soong/soong.bash進行環(huán)境準備。 其中會先編譯、安裝Blueprint到out目錄下。 也就是說,在編譯Android項目時,Android.bp相關(guān)工具鏈會自動編譯,無需費神。
Soong是與Android強關(guān)聯(lián)的一個項目,而Blueprint則相對比較獨立,可以單獨編譯、使用。
編譯Blueprint,首先要具備Golang環(huán)境。 然后,按照以下步驟執(zhí)行命令。
go get github.com/google/blueprint
cd $GOPATH/src/github.com/google/blueprint
./bootstrap.bash
./blueprint.bash
ls bin
在新生成的bin目錄中,包含4個可執(zhí)行文件:
- bpfmt
- bpmodify
- microfactory
- minibp
由于文檔較少,甚至連幫助命令都不包括命令的描述,所以其作用只能望文生義。
工具鏈關(guān)系
Android.mk、Android.bp、Soong、Blueprint、Ninja,它們之間到底有什么關(guān)系? 以下用簡單的方式表達這幾個概念之間的作用關(guān)系。
Android.bp --> Blueprint --> Soong --> Ninja
Makefile or Android.mk --> kati --> Ninja
(Android.mk --> Soong --> Blueprint --> Android.bp)
Blueprint是生成、解析Android.bp的工具,是Soong的一部分。 Soong則是專為Android編譯而設(shè)計的工具,Blueprint只是解析文件的形式,而Soong則解釋內(nèi)容的含義。
Android.mk可以通過Soong提供的androidmk轉(zhuǎn)換成Android.bp,但僅限簡單配置。 目前Oreo的編譯流程中,仍然是使用kati來做的轉(zhuǎn)換。
現(xiàn)存的Android.mk、既有的Android.bp,都會分別被轉(zhuǎn)換成Ninja。 從Android.mk與其它Makefile,會生成out/build-<product_name>.ninja文件。 而從Android.bp,則會生成out/soong/build.ninja。 此外,還會生成一個較小的out/combined-<product_name>.ninja文件,負責(zé)把二者組合起來,作為執(zhí)行入口。
最終,Ninja文件才是真正直接控制源碼編譯的工具。
Android.bp
樣例與基本概念
// Android.bp sample
cc_defaults(
deps = [
"libc",
],
)
cc_library(
name = "cmd",
srcs = [
"main.c",
],
)
subdirs = ["subdir1", "subdir2"]
前面的樣例中,cc_library這種()前面的,就是模塊(module)。 這里module的概念,直接對應(yīng)Android.mk中module的概念。 而=前面的name、srcs等,就是該模塊的屬性(property)。
subdirs是一個文件級的頂層屬性,指定后會查找次級目錄下的Android.bp。 類似于Android.mk中常用的include $(call all-subdir-makefiles)。
模塊是可以繼承屬性的。 cc_defaults就是一個文件中所有模塊的父模塊,可以指定公用的屬性。 在以上代碼中,cc_library模塊雖然沒有指定,但已經(jīng)包含了deps屬性。
語法
Blueprint文件的語法比較簡單,畢竟只是配置文件。
變量與屬性都是動態(tài)強類型的,賦值時確定。 變量類型只有四種。
- Bool(
true或false) - 字符串Strings("string")
- 字符串列表(
["string1", "string2"]) - 映射關(guān)系Map(
{key1: "value1", key2: ["value2"]})
注釋方式,與Golang類似。 支持行注釋// line與塊注釋/* block */。
操作符除了賦值的=以外,只有+。
常用工具
雖然編譯過程中的相關(guān)很多,不過在開發(fā)過程中可能需要手動執(zhí)行的命令卻不多。
一個是格式化工具bpfmt。 與gofmt類似,可以格式化Blueprint文件。 (其實,代碼基本上都是從gofmt復(fù)制而來。)
例如,格式化當(dāng)前目錄及其遞歸子目錄下的所有Android.bp:
bpfmt -w .
另一個是androidmk,負責(zé)轉(zhuǎn)換Android.mk為Android.bp。 其實,現(xiàn)階段沒有必要學(xué)會寫Android.bp,通過寫Android.mk來轉(zhuǎn)換也行。
androidmk Android.mk > Android.bp
Android.mk轉(zhuǎn)換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)
這是一個編譯二進制可執(zhí)行文件的小模塊,內(nèi)容非常簡單。 通過執(zhí)行androidmk Android.mk > Android.bp,可以轉(zhuǎn)換成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"],
},
}
可以看出,雖然行數(shù)變多,但其實含義更明確了。 這個名為sdcard的模塊,源碼有兩個cpp文件,依賴庫有四個。 cc_binary,就相當(dāng)于include $(BUILD_EXECUTABLE)。 轉(zhuǎn)換前后,該有的信息都在,只是表達方式變化了而已。
注意:如果Android.mk中包含復(fù)雜的邏輯,則轉(zhuǎn)換結(jié)果會有問題,詳見結(jié)果文件中的注釋。
至于Android.bp支持多少像cc_binary、cc_library這樣的模塊,每個模塊又支持多少像name、cflags這樣的屬性, 則只能去查找Soong的文檔。
文檔
目前(2017年),整個Android.bp工具鏈,都處于文檔極度缺失的階段。 除了官方那點可憐的README以外,基本只能去看代碼與注釋,參考其它已經(jīng)存在的Android.bp。
另外,在已經(jīng)使用Soong編譯的項目中,out/soong/.bootstrap/docs/soong_build.html描述了所有的可用模塊及其屬性。 這多少緩解了兩眼一抹黑癥狀,不算太過難受。 實際上,整個Soong仍然處于發(fā)展期,Google肆無忌憚地修改,完全沒考慮兼容。 在8.0.0寫的Android.bp,也許在8.0.1就會編譯失敗。 這或許是文檔與編譯綁定的真意吧。 等Soong完全成熟了,也許Android開發(fā)官網(wǎng),就會有詳盡的信息。
本站提供了從AOSP的android-8.0.0-r9,編譯出來的一個soong_build.html,僅供參考。
轉(zhuǎn)載自