现代编译构建工具-Ninja语言
在传统的C/C++项目中,通常采用make系统,使用Makefile约束进行整个项目的编译构建。Makefile指定的编译依赖规则会使编译流程简单,但是make的依赖大而且复杂,在大型项目编译时,使用的模块越来越多,Makefile组织的项目编译时间越来越长,这个对于编译效率来说是一个极大的浪费。在执行增量或无操作时Make相较于Ninja构建时很慢,特别诸如Google Chrome这种将40,000个输入文件编译为单个可执行文件的工程,这可能会大大降低开发人员在大型项目上的工作速度。
根据Chromium的实际测试:在超过30,000个源文件的情况下,Ninja也能够在1秒钟内开始进行真正的构建。与之相比,通过资深工程师进行编写的Makefiles文件也需要10-20秒才能开始构建。
Ninja是由Google员工Evan Martin开发的小型构建系统。Ninja注重速度,Ninja被设计为使其输入文件由更高级别的构建系统生成,并且其被设计为尽可能快地运行构建。同时与Make相比,Ninja缺少诸如字符串操作比较、隐式规则、函数和遍历搜索等的功能,因为Ninja生成文件不需要手工编写。相反,应使用"生成器"生成Ninja生成文件。CMake,Meson,Gyp(Google早期用来维护chromium项目的构建系统,GN则是用来替代GYP的工具)和Gn是流行的构建管理软件工具,这些更高层次的工具,支持生成Ninja文件构建工程项目。
Ninja特有功能:
- Ninja特别支持在构建时发现额外的依赖项,从而可以轻松地为C/C++代码获取正确的头文件
- 更改编译标志将导致输出重新生成,因为输出隐式依赖于用于生成它们的命令行(等等,啥意思?)
- 规则可以提供正在运行的命令的简短描述,因此您可以在构建时打印例如CC foo.o而不是长命令行
- 构建工作始终并行运行。默认情况下,根据系统具有的CPU核数决定并行执行的线程数
- 命令输出始终是缓冲的。这意味着并行运行的命令不会交错其输出,当命令失败时可以将其失败输出打印在产生故障的完整命令行旁边
疑问: 特有功能1中,gcc -M 输出用于make系统的规则,该规则描述了源文件的依赖关系,makefile也可以轻松地为C/C++代码获取正确的头文件(见文章Gcc部分参数说明)
接下来将通过具体示例,探索GN、Meson和CMake这些现代构建工具如何生成Ninja文件,并分析它们在实际项目中的应用优势。
0.GN
GN是专为生成Ninja文件而设计的,运行速度极快,尤其适合大型项目,已在Chromium项目验证。其语法简单直接,避免了复杂的逻辑处理,专注于配置构建规则并且内置对依赖项的高效处理和验证机制,减少了构建错误,能够很好地应对超大规模代码库。但是其由Google内部主导开发,社区生态和扩展能力并没有Cmake和Meson好,并且缺少某些高级特性,如内置的包管理和复杂的自定义逻辑支持。
gn把.gn文件转换成.ninja文件,然后Ninja根据.ninja文件将源码生成目标程序
GN工具安装
apt-get install -y ninja-build
git clone https://gn.googlesource.com/gn
cd gn && python3 build/gen.py --allow-warning
out/gn_unittests
GN构建样例
gn-sample.tar.gz是关于gn构建的入门样例
tree -a
.
├── build
│ ├── BUILDCONFIG.gn
│ └── config
│ └── toolchains
│ └── BUILD.gn
├── BUILD.gn
├── .gn
└── src
├── BUILD.gn
├── main.c
├── tools.c
└── tools.h
- .gn文件: gn命令执行时最先加载的配置文件,.gn置于根目录,buildconfig参数指定构建配置文件位置. 例子中指定的是//build/BUILDCONFIG.gn
- build/BUILDCONFIG.gn: 针对项目的一些设置包括不同操作系统的设置,默认编译工具的设置,例子中通过set_default_toolchain,指定//build/config/toolchains:gcc
- build/config/toolchains/BUILD.gn: 这个是针对工具链的一些设置
- BUILD.gn: 一般进行配置的文件,主要是配置需要编译的文件,库,最终的可执行文件,本例中依赖子目录src中的a.out目标
buildconfig = "//build/BUILDCONFIG.gn"
group("default") {
deps = [ "//src:a.out" ]
}
set_default_toolchain("//build/config/toolchains:gcc")
import("//build/config/component.gni") // 允许使用component模板
declare_args() {
is_debug = false
is_component_build = false // 配置[静态编译,动态编译];如果为true的话,就全部是动态库,否则就是静态库
is_mac = false
}
executable("a.out") {
sources = [
"main.c",
]
include_dirs = [
".",
]
deps = [
":tools",
]
libs = [ "json-c" ]
if (is_debug) {
cflags = [ "-g", "-O0" ]
}
shared_library("tools") {
sources = [
"tools.c",
]
if (is_debug) {
cflags = [ "-g", "-O0" ]
}
component("test") {
sources = [ "tools.c" ]
if (is_debug) {
cflags = [ "-g", "-O0" ]
}
}
toolchain("gcc") {
tool("cc") {
depfile = "{{output}}.d"
command = "gcc -MMD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_c}} -c {{source}} -{{output}}"
depsformat = "gcc"
description = "CC {{output}}"
outputs =
[ "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o" ]
}
tool("cxx") {
depfile = "{{output}}.d"
command = "g++ -MMD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_cc}} -c {{source}} -{{output}}"
depsformat = "gcc"
description = "CXX {{output}}"
outputs =
[ "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o" ]
}
tool("alink") {
command = "ar rcs {{output}} {{inputs}}"
description = "AR {{target_output_name}}{{output_extension}}"
outputs =
[ "{{target_out_dir}}/{{target_output_name}}{{output_extension}}" ]
default_output_extension = ".a"
output_prefix = "lib"
}
tool("solink") {
soname = "{{target_output_name}}{{output_extension}}"
sofile = "{{output_dir}}/$soname"
rspfile = soname + ".rsp"
if (is_mac) {
os_specific_option = "-install_name @executable_path/$sofile"
rspfile_content = "{{inputs}} {{solibs}} {{libs}}"
} else {
os_specific_option = "-Wl,-soname=$soname"
rspfile_content = "-Wl,--whole-archive {{inputs}} {{solibs}} -Wl,--no-whole-archive {{libs}}"
}
command = "g++ -shared {{ldflags}} -o $sofile $os_specific_option @$rspfile"
description = "SOLINK $soname"
default_output_extension = ".so"
default_output_dir = "{{root_out_dir}}"
outputs = [ sofile ]
link_output = sofile
depend_output = sofile
output_prefix = "lib"
}
tool("link") {
outfile = "{{target_output_name}}{{output_extension}}"
rspfile = "$outfile.rsp"
if (is_mac) {
command = "g++ {{ldflags}} -o $outfile @$rspfile {{solibs}} {{libs}}"
} else {
command = "g++ {{ldflags}} -o $outfile -Wl,--start-group @$rspfile {{solibs}} -Wl,--end-group {{libs}}"
}
description = "LINK $outfile"
default_output_dir = "{{root_out_dir}}"
rspfile_content = "{{inputs}}"
outputs = [ outfile ]
}
tool("stamp") {
command = "touch {{output}}"
description = "STAMP {{output}}"
}
tool("copy") {
command = "cp -af {{source}} {{output}}"
description = "COPY {{source}} {{output}}"
}
}
template("component") {
if (is_component_build) {
_component_mode = "shared_library
if (!defined(invoker.output_name)) {
_output_name = get_label_info(":$target_name", "label_no_toolchain")
_output_name = string_replace(_output_name, "$target_name:$target_name", target_name)
_output_name = string_replace(_output_name, "//", "")
_output_name = string_replace(_output_name, "/", "_")
_output_name = string_replace(_output_name, ":", "_")
}
} else if (defined(invoker.static_component_type)) {
assert(invoker.static_component_type == "static_library" || invoker.static_component_type == "source_set")
_component_mode = invoker.static_component_type
} else if (!defined(invoker.sources) || invoker.sources == []) {
_component_mode = "source_set"
} else {
_component_mode = "static_library"
}
target(_component_mode, target_name) {
if (defined(_output_name)) {
output_name = _output_name
}
if (is_component_build) {
output_extension = "so"
}
forward_variables_from(invoker, ["testonly", "visibility",])
forward_variables_from(invoker, "*", ["testonly", "visibility",])
}
}
GN相关概念
1.GN命令
Read more...
