GN 和 ninja 构建系统

介绍

Ninja 是什么?

Ninja是一个专注于 速度 的小型构建系统,由Evan Martin于2010年在Chrome团队工作时开发。它与其他构建系统在两个主要方面有所不同:它的设计目的是让更高级别的构建系统生成其输入文件,并且它的设计目的是尽可能快地运行构建。

Ninja 用于构建过 Google Chrome、Android 的部分内容、LLVM,并且由于 CMake 的 Ninja 后端,它可以用于许多其他项目。

Ninja github: https://github.com/ninja-build/ninja

Ninja 主页:https://ninja-build.org/

Ninja 手册:https://ninja-build.org/manual.html

Ninja 支持在 类Unix系统和Windows系统上运行。它在Linux上经过最多的测试,所以在Linux具备最佳性能,当然也能运行在 Mac OS X 和 FreeBSD。

Ninja环境

在Ubuntu 上可以执行命令下载:

1
2
3
sudo apt update
sudo apt install build-essential clang cmake curl git python3 python-is-python3
sudo apt install ninja-build

编译GN时,需要使用 python3,另外 python-is-python3 是用于将系统的python默认设置为 python3,pip默认设置为 pip3

另外,编译GN使用的编译器是 clang++

当然,后续项目使用GNU的编译进行编译,所以安装 build-essential 包,包含了:gcc/g++、gdb、ld、make以及GNU 标准库文件和头文件。

构建GN

1
2
3
4
git clone https://gn.googlesource.com/gn
cd gn
python build/gen.py
ninja -C out

编译报错:

  • 执行ninja -C out 出错,找不到C++标准库 iostream 的头文件。

解决方法:设置C++标准库头文件目录环境变量

1
export CPLUS_INCLUDE_PATH=/usr/include/c++/11:/usr/include/x86_64-linux-gnu/c++/11
  • 执行ninja -C out 出错,找不到C++标准库的 stdc++ 库文件。

解决方法:设置C++标准库库文件目录环境变量

1
2
export LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/11:$LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/11:$LD_LIBRARY_PATH

构建成功后,在 out 文件下生成 gn 可执行文件。将 gn 的目录添加到环境变量,如下:

1
export PATH="~/C/005-ninja-hello-world/gn/out:$PATH"

当然,也可以将 gn 可执行文件拷贝到 /usr/bin ,这样全局都可以使用。

编译hello_world:

创建项目和源文件:

1
2
3
4
5
6
mkdir -p ninja-hello-world
cd ninja-hello-world
mkdir -p src && cd src
touch main.cpp
cd ..
touch BUILD.gn .gn

其中 main.cpp 内容如下:

1
2
3
4
5
6
7
8
9
10
#include <iostream>

using namespace std;

int main(int argc, char *argv[])
{
cout << "hello world" << endl;

return 0;
}

BUILD.gn 内容如下:

1
2
3
4
5
executable("hello_world") {
sources = [
"src/main.cpp"
]
}

.gn 内容如下:

1
2
# The location of the build configuration file.
buildconfig = "//build/BUILDCONFIG.gn"

注意:这里需要使用配置文件指定编译工具链。可以从 gn/example/simple_build/build 拷贝到当前项目。

接下来就可以编译项目了

1
2
3
4
5
6
cd ninja-hello-world
gn gen out
ninja -C out

# 运行二进制文件
./out/hello_world

Ninja&Make

Ninja 在精神和功能上与 Make 最为接近,依赖于文件时间戳之间的简单依赖关系。

但从根本上讲,make 有很多功能:后缀规则、函数、内置规则(例如,在构建源代码时搜索 RCS 文件)。Make 的语言是为人类设计的。许多项目发现,仅使用 make 就足以解决他们的构建问题。

相比之下,Ninja 几乎没有任何功能;只有那些使构建正确所必需的功能,同时将大部分复杂性转移到生成 Ninja 输入文件上。Ninja 本身不太可能对大多数项目有用。

Ninja 为 Make 添加的一些功能:

  • Ninja 特别支持在构建时发现额外的依赖项,从而可以轻松获取C/C++ 代码的正确头依赖项 。
  • 构建边缘可能有多个输出。
  • 输出隐式地依赖于用于生成它们的命令行,这意味着更改编译标志等将导致输出重建。
  • 在运行依赖于输出目录的命令之前,总是会隐式创建输出目录。
  • 规则可以提供正在运行的命令的更简短的描述,因此您可以CC foo.o在构建时打印例如而不是长命令行。
  • 构建始终并行运行,默认情况下取决于系统的 CPU 数量。构建依赖项指定不足将导致构建不正确。
  • 命令输出始终是缓冲的。这意味着并行运行的命令不会交错其输出,并且当命令失败时,我们可以将其失败输出打印在导致失败的完整命令行旁边。

GN&Ninja

GN 构建配置流程:

  1. gn 通过BUILD.gn文件生成 build.ninja
  2. ninja 通过 build.ninja 生成可执行文件

如果知道cmake 和 make,可以进行类比成 gn 和 ninja。ninja 和 make 一样,并不是编译器,仅仅调用了编译器去编译项目源文件。

GN

GN Command Usage

GN语法

GN Language and Operation

GN 使用动态类型语言。类型包括:

  • Boolean
  • int64
  • string
  • list
  • scope

string

字符串用双引号括起来,并使用反斜杠作为转义字符。仅支持的转义序列有:

  • \"
  • \$
  • \\

通过 $ 支持简单的变量替换,其中美元符号($)后面的单词将替换为变量的值。如果没有非变量名称字符来终止变量名称,可以使用 {} 将变量命令括起来。例如:

1
2
3
a = "mypath"
b = "$a/foo.cc" # b -> "mypath/foo.cc"
c = "foo${a}bar.cc" # c -> "foomypathbar.cc"

list

对于 list ,不能获取其长度。但支持在 list 中,追加元素,元素可以有相同项。也可以将一个list追加到第二个list中的项,而不是将list追加为嵌套成员。例如

1
2
3
4
a = [ "first" ]
a += [ "second" ] # a -> [ "first", "second" ]
a += [ "third", "fourth" ] # a -> [ "first", "second", "third", "fourth" ]
b = a + [ "fifth" ] # b -> [ "first", "second", "third", "fourth", "fifth" ]

当然,也可以从 list 中删除元素。如果在删除元素时,没有找到匹配的元素,则会引发错误,因此在删除元素前,需要提前知道元素是否存在。例如:

1
2
3
a = ["first", "second", "third", "first"]
b = a - ["first"] # b -> [second", "third"]
a -= ["second"] # a -> ["third"]

list 也支持通过下标索引读取元素,从0开始,但是不能通过下表索引值修改元素值。例如:

1
2
a = ["first", "second", "third"]
b = a[1] # b -> "second"

变量

在GN中预定了一些变量:

1
2
3
4
5
6
7
8
9
10
11
Flags: asmflags, cflags, cflags_c, cflags_cc, cflags_objc,
cflags_objcc, defines, include_dirs, inputs, ldflags,
lib_dirs, libs, precompiled_header, precompiled_source,
rustenv, rustflags, swiftflags, testonly
Deps: assert_no_deps, data_deps, deps, public_deps, runtime_deps,
write_runtime_deps
Dependent configs: all_dependent_configs, public_configs
General: check_includes, configs, data, friend, inputs, metadata,
output_extension, output_name, public, sources, testonly,
visibility
Rust variables: aliased_deps, crate_root, crate_name

对于:cflagsinclude_dirlibs 就很熟悉了。

条件判断

可以使用类似 C语言中的 if 语句。例如:

1
2
3
4
5
6
7
if (is_linux || (is_win && target_cpu == "x86")) {
sources -= ["something.cc"]
} else if (...) {
...
} else {
...
}

函数

函数和C语言中的类似。例如:

1
2
3
4
5
6
print("hello world")
assert(is_win, "This should only be executed on Wondows")

static_library("mylibrary") {
sources = ["a.cc"]
}

构建参数

参数可以从命令行传入。可以声明接受哪些参数并通过 declare_args 指定默认值。

1
2
3
4
declare_args() {
enable_teleporter = true
enable_doom_melon = false
}

还可以这样使用:

1
gn --args="enable_doom_melon=true enable_teleporter=true"

可以查看一些帮助文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
pi@pi-NMH-WCX9:~/workspace/connectedhomeip$ gn help declare_args
declare_args: Declare build arguments.

Introduces the given arguments into the current scope. If they are not
specified on the command line or in a toolchain's arguments, the default
values given in the declare_args block will be used. However, these defaults
will not override command-line values.
将给定的参数引入当前范围。如果未在命令行或工具链的参数中指定它们,则将使用 declare_args 块中给出的默认值。但是,这些默认值不会覆盖命令行值。


See also "gn help buildargs" for an overview.

The precise behavior of declare args is:

1. The declare_args() block executes. Any variable defined in the enclosing
scope is available for reading, but any variable defined earlier in
the current scope is not (since the overrides haven't been applied yet).

2. At the end of executing the block, any variables set within that scope
are saved, with the values specified in the block used as the "default value"
for that argument. Once saved, these variables are available for override
via args.gn.

3. User-defined overrides are applied. Anything set in "gn args" now
overrides any default values. The resulting set of variables is promoted
to be readable from the following code in the file.

This has some ramifications that may not be obvious:

- You should not perform difficult work inside a declare_args block since
this only sets a default value that may be discarded. In particular,
don't use the result of exec_script() to set the default value. If you
want to have a script-defined default, set some default "undefined" value
like [], "", or -1, and after the declare_args block, call exec_script if
the value is unset by the user.

- Because you cannot read the value of a variable defined in the same
block, if you need to make the default value of one arg depend
on the possibly-overridden value of another, write two separate
declare_args() blocks:

declare_args() {
enable_foo = true
}
declare_args() {
# Bar defaults to same user-overridden state as foo.
enable_bar = enable_foo
}

Example

declare_args() {
enable_teleporter = true
enable_doom_melon = false
}

If you want to override the (default disabled) Doom Melon:
gn --args="enable_doom_melon=true enable_teleporter=true"
This also sets the teleporter, but it's already defaulted to on so it will
have no effect.

预处理器宏

defines 是一个用于定义预处理器的list,可以定义预处理器宏传入。通过构建参数变量 target_product 决定定义哪个宏。

1
2
3
4
5
6
7
8
defines = []
if (target_product == "distribution") {
defines += [ "DISTRIBUTION_TARGET" ]
} else if (target_product == "ihost") {
defines += [ "IHOST_TARGET" ]
} else {
defines += [ "LOCAL_TARGET" ]
}

构建目标

executable 是一种目标类型,用于生成一个可执行文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
executable("target_name") {
# 源文件列表
sources = [
"main.cc",
"other_source_file.cc",
# 添加更多源文件...
]

# 依赖的目标
deps = [
"//path/to/dependency:target",
# 添加更多依赖...
]

# 定义预处理器宏
defines = [
"DEBUG",
"VERSION=1.0",
# 添加更多宏定义...
]

# 包含路径
include_dirs = [
".",
"//path/to/include",
# 添加更多包含路径...
]

# 链接器标志
ldflags = [
"-lmylib",
# 添加更多链接器标志...
]

# 编译器标志
cflags = [
"-Wall",
"-ggdb3",
"-Wconversion",
"-w",
# 添加更多编译器标志...
]
}

输出目录

gn执行后,执行一个输出目录

1
2
3
4
shared_library("doom_melon") {
output_dir = "$root_out_dir/plugin_libs"
...
}

导入文件

先看一下帮助文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
pi@pi-NMH-WCX9:~/workspace/connectedhomeip$ gn help import
import: Import a file into the current scope.

The import command loads the rules and variables resulting from executing the
given file into the current scope.
import命令会加载给定的文件的规则和变量到当前范围

By convention, imported files are named with a .gni extension.
根据习惯,import文件的有 .gni 后缀

An import is different than a C++ "include". The imported file is executed in
a standalone environment from the caller of the import command. The results
of this execution are cached for other files that import the same .gni file.

Note that you can not import a BUILD.gn file that's otherwise used in the
build. Files must either be imported or implicitly loaded as a result of deps
rules, but not both.

The imported file's scope will be merged with the scope at the point import
was called. If there is a conflict (both the current scope and the imported
file define some variable or rule with the same name but different value), a
runtime error will be thrown. Therefore, it's good practice to minimize the
stuff that an imported file defines.

Variables and templates beginning with an underscore '_' are considered
private and will not be imported. Imported files can use such variables for
internal computation without affecting other files.

Examples

import("//build/rules/idl_compilation_rule.gni")

# Looks in the current directory.
import("my_vars.gni")

目标组

帮助文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pi@pi-NMH-WCX9:~/workspace/connectedhomeip$ gn help group
group: Declare a named group of targets.

This target type allows you to create meta-targets that just collect a set of
dependencies into one named target. Groups can additionally specify configs
that apply to their dependents.
此目标类型允许您创建元目标,这些元目标仅将一组依赖项收集到一个命名目标中。组还可以指定适用于其依赖项的配置。

Variables

Deps: assert_no_deps, data_deps, deps, public_deps, runtime_deps,
write_runtime_deps
Dependent configs: all_dependent_configs, public_configs
General: check_includes, configs, data, friend, inputs, metadata,
output_extension, output_name, public, sources, testonly,
visibility

Example

group("all") {
deps = [
"//project:runner",
"//project:unit_tests",
]
}

GN交叉编译

在 linux-x86 平台上编译出 arm-32 平台的应用。那么如何配置 gn –args 呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gn gen \
--args="\
library_dir=\"$SCRIPT_DIR/libs/arm\" \
target_product=\"ihost\" \
sysroot=\"$sdk_target_sysroot\" \
target_os=\"linux\" \
target_cpu=\"arm\" \
arm_arch=\"armv7\" \
treat_warnings_as_errors=false \
import(\"//build_overrides/build.gni\") \
custom_toolchain=\"\${build_root}/toolchain/custom\" \
target_cc=\"$toolchain/bin/arm-none-linux-gnueabihf-gcc\" \
target_cxx=\"$toolchain/bin/arm-none-linux-gnueabihf-g++\" \
target_ar=\"$toolchain/bin/arm-none-linux-gnueabihf-ar\"" \
--check --fail-on-unused-args \
--root="examples/$example/" \
examples/$example/out/ihost/
  • sysroot:指定交叉编译工具链路径
  • target_cpu:指定目标CPU架构。支持 armarm64x86x64
  • target_os:指定目标操作系统。支持 androidioslinuxmacwin
  • target_ar:指定静态库管理工具
  • target_cc:指定C编译器
  • target_cxx:指定C++编译器
  • custom_toolchain:指定用户自定义工具链