现代编译构建工具-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命令
gn支持的命令
analyze: Analyze which targets are affected by a list of files.
args: Display or configure arguments declared by the build.
check: Check header dependencies.
clean: Cleans the output directory.
clean_stale: Cleans the stale output files from the output directory.
desc: Show lots of insightful information about a target or config.
format: Format .gn files.
gen: Generate ninja files.
help: Does what you think.
ls: List matching targets.
meta: List target metadata collection results.
outputs: Which files a source/target make.
path: Find paths between two targets.
refs: Find stuff referencing a target or file.
2.GN目标
申明的构建目标
action: Declare a target that runs a script a single time.
action_foreach: Declare a target that runs a script over a set of files.
bundle_data: [iOS/macOS] Declare a target without output.
copy: Declare a target that copies files.
create_bundle: [iOS/macOS] Build an iOS or macOS bundle.
executable: Declare an executable target.
generated_file: Declare a generated_file target.
group: Declare a named group of targets.
loadable_module: Declare a loadable module target.
rust_library: Declare a Rust library target.
rust_proc_macro: Declare a Rust procedural macro target.
shared_library: Declare a shared library target.
source_set: Declare a source set target.
static_library: Declare a static library target.
target: Declare a target with the given programmatic type.
3.函数
可以在.gn文件中使用这些函数
assert: Assert an expression is true at generation time.
config: Defines a configuration object.
declare_args: Declare build arguments.
defined: Returns whether an identifier is defined.
exec_script: Synchronously run a script and return the output.
filter_exclude: Remove values that match a set of patterns.
filter_include: Remove values that do not match a set of patterns.
filter_labels_exclude: Remove labels that match a set of patterns.
filter_labels_include: Remove labels that do not match a set of patterns.
foreach: Iterate over a list.
forward_variables_from: Copies variables from a different scope.
get_label_info: Get an attribute from a target's label.
get_path_info: Extract parts of a file or directory name.
get_target_outputs: [file list] Get the list of outputs from a target.
getenv: Get an environment variable.
import: Import a file into the current scope.
label_matches: Returns whether a label matches any of a list of patterns.
not_needed: Mark variables from scope as not needed.
pool: Defines a pool object.
print: Prints to the console.
print_stack_trace: Prints a stack trace.
process_file_template: Do template expansion over a list of files.
read_file: Read a file into a variable.
rebase_path: Rebase a file or directory to another location.
set_default_toolchain: Sets the default toolchain name.
set_defaults: Set default values for a target type.
split_list: Splits a list into N different sub-lists.
string_join: Concatenates a list of strings with a separator.
string_replace: Replaces substring in the given string.
string_split: Split string into a list of strings.
template: Define a template rule.
tool: Specify arguments to a toolchain tool.
toolchain: Defines a toolchain.
write_file: Write a file to disk.
4.预定义变量
.gn文件中预定义变量
current_cpu: [string] The processor architecture of the current toolchain.
current_os: [string] The operating system of the current toolchain.
current_toolchain: [string] Label of the current toolchain.
default_toolchain: [string] Label of the default toolchain.
gn_version: [number] The version of gn.
host_cpu: [string] The processor architecture that GN is running on.
host_os: [string] The operating system that GN is running on.
invoker: [string] The invoking scope inside a template.
python_path: [string] Absolute path of Python.
root_build_dir: [string] Directory where build commands are run.
root_gen_dir: [string] Directory for the toolchain's generated files.
root_out_dir: [string] Root directory for toolchain output files.
target_cpu: [string] The desired cpu architecture for the build.
target_gen_dir: [string] Directory for a target's generated files.
target_name: [string] The name of the current target.
target_os: [string] The desired operating system for the build.
target_out_dir: [string] Directory for target output files.
5.目标变量
定义在目标中的相关变量
aliased_deps: [scope] Set of crate-dependency pairs.
all_dependent_configs: [label list] Configs to be forced on dependents.
allow_circular_includes_from: [label list] Permit includes from deps.
arflags: [string list] Arguments passed to static_library archiver.
args: [string list] Arguments passed to an action.
asmflags: [string list] Flags passed to the assembler.
assert_no_deps: [label pattern list] Ensure no deps on these targets.
bridge_header: [string] Path to C/Objective-C compatibility header.
bundle_contents_dir: Expansion of {{bundle_contents_dir}} in create_bundle.
bundle_deps_filter: [label list] A list of labels that are filtered out.
bundle_executable_dir: Expansion of {{bundle_executable_dir}} in create_bundle
bundle_resources_dir: Expansion of {{bundle_resources_dir}} in create_bundle.
bundle_root_dir: Expansion of {{bundle_root_dir}} in create_bundle.
cflags: [string list] Flags passed to all C compiler variants.
cflags_c: [string list] Flags passed to the C compiler.
cflags_cc: [string list] Flags passed to the C++ compiler.
cflags_objc: [string list] Flags passed to the Objective C compiler.
cflags_objcc: [string list] Flags passed to the Objective C++ compiler.
check_includes: [boolean] Controls whether a target's files are checked.
code_signing_args: [string list] [deprecated] Args for the post-processing script.
code_signing_outputs: [file list] [deprecated] Outputs of the post-processing step.
code_signing_script: [file name] [deprecated] Script for the post-processing step.
code_signing_sources: [file list] [deprecated] Sources for the post-processing step.
complete_static_lib: [boolean] Links all deps into a static library.
configs: [label list] Configs applying to this target or config.
contents: Contents to write to file.
crate_name: [string] The name for the compiled crate.
crate_root: [string] The root source file for a binary or library.
crate_type: [string] The type of linkage to use on a shared_library.
data: [file list] Runtime data file dependencies.
data_deps: [label list] Non-linked dependencies.
data_keys: [string list] Keys from which to collect metadata.
defines: [string list] C preprocessor defines.
depfile: [string] File name for input dependencies for actions.
deps: [label list] Private linked dependencies.
externs: [scope] Set of Rust crate-dependency pairs.
framework_dirs: [directory list] Additional framework search directories.
frameworks: [name list] Name of frameworks that must be linked.
friend: [label pattern list] Allow targets to include private headers.
gen_deps: [label list] Declares targets that should generate when this one does.
include_dirs: [directory list] Additional include directories.
inputs: [file list] Additional compile-time dependencies.
ldflags: [string list] Flags passed to the linker.
lib_dirs: [directory list] Additional library directories.
libs: [string list] Additional libraries to link.
metadata: [scope] Metadata of this target.
mnemonic: [string] Prefix displayed when ninja runs this action.
module_name: [string] The name for the compiled module.
output_conversion: Data format for generated_file targets.
output_dir: [directory] Directory to put output file in.
output_extension: [string] Value to use for the output's file extension.
output_name: [string] Name for the output file other than the default.
output_prefix_override: [boolean] Don't use prefix for output name.
outputs: [file list] Output files for actions and copy targets.
partial_info_plist: [filename] Path plist from asset catalog compiler.
pool: [string] Label of the pool used by binary targets and actions.
post_processing_args: [string list] Args for the post-processing script.
post_processing_outputs: [file list] Outputs of the post-processing step.
post_processing_script: [file name] Script for the post-processing step.
post_processing_sources: [file list] Sources for the post-processing step.
precompiled_header: [string] Header file to precompile.
precompiled_header_type: [string] “gcc” or “msvc”.
precompiled_source: [file name] Source file to precompile.
product_type: [string] Product type for the bundle.
public: [file list] Declare public header files for a target.
public_configs: [label list] Configs applied to dependents.
public_deps: [label list] Declare public dependencies.
rebase: [boolean] Rebase collected metadata as files.
response_file_contents: [string list] Contents of .rsp file for actions.
rustflags: [string list] Flags passed to the Rust compiler.
script: [file name] Script file for actions.
sources: [file list] Source files for a target.
swiftflags: [string list] Flags passed to the swift compiler.
testonly: [boolean] Declares a target must only be used for testing.
transparent: [bool] True if the bundle is transparent.
visibility: [label list] A list of labels that can depend on a target.
walk_keys: [string list] Key(s) for managing the metadata collection walk.
weak_frameworks: [name list] Name of frameworks that must be weak linked.
write_runtime_deps: Writes the target's runtime_deps to the given path.
xcasset_compiler_flags: [string list] Flags passed to xcassets compiler
xcode_extra_attributes: [scope] Extra attributes for Xcode projects.
xcode_test_application_name: [string] Name for Xcode test target.
6.其他
其他相关主题
all: Print all the help at once
buildargs: How build arguments work.
dotfile: Info about the toplevel .gn file.
execution: Build graph and execution overview.
grammar: Language and grammar for GN build files.
input_conversion: Processing input from exec_script and read_file.
file_pattern: Matching more than one file.
label_pattern: Matching more than one label.
labels: About labels.
metadata_collection: About metadata and its collection.
ninja_rules: How Ninja build rules are named.
nogncheck: Annotating includes for checking.
output_conversion: Specifies how to transform a value to output.
runtime_deps: How runtime dependency computation works.
source_expansion: Map sources to outputs for scripts.
switches: Show available command-line switches.
1.Meson
Meson是一个开源, 用户友好, 编译速度快的编译系统,很多开源软件都在拥抱Meson,比如Linux系统和服务管理器Systemd就是Meson系统构建,再比如QEMU,GNOME,PostgreSQL部分组件,都使用Meson构建系统。
- multiplatform support for Linux, macOS, Windows, GCC, Clang, Visual Studio and others
- supported languages include C, C++, D, Fortran, Java, Rust
- build definitions in a very readable and user friendly non-Turing complete DSL
- cross compilation for many operating systems as well as bare metal
- optimized for extremely fast full and incremental builds without sacrificing correctness
- built-in multiplatform dependency provider that works together with distro packages
- fun!
虽然Meson具备以上有优点,但是相较于Cmake其生态还是较弱,不如CMake那样灵活,某些场景下需要额外工具辅助处理特殊需求。
Meson安装
sudo apt install ninja-build
sudo pip install meson
Meson构建样例
meson-sample.tar.gz是关于meson构建的入门样例. meson项目通过meson.build定义构建流程,meson_options.txt添加构建选项.
project('meson-sample', 'c', version: '0.0.1')
subdir('tools')
if get_option('debug')
add_project_arguments('-DDEBUG', language: ['c'])
endif
if get_option('code_format')
clang_format = find_program('clang-format', '/usr/sbin/clang-format', required : true)
files = run_command(get_clang_format_files, 'src/**/*.c,src/**/*.h', check: false).stdout().split()
foreach item:files
run_command(clang_format, '--style=@0@'.format(get_option('code_style')) , '-i', item, check: false)
endforeach
endif
tools = shared_library('libtools.so', './src/tools.c')
executable('a.out', './src/main.c', dependencies: dependency('json-c'), link_with: tools)
option('code_format', type : 'boolean', value : false, description : 'format code')
option('code_style', type : 'combo', choices : ['llvm', 'google', 'chromium', 'mozilla', 'webkit'], value 'google', description : 'code style')
python3 = ['meson', 'runpython']
get_clang_format_files = python3 + files('get-clang-format-files.py')
import sys
import os
from glob import iglob
if len(sys.argv) != 2:
print("Usage: {0} <path-glob>[,<path-glob>[,...]]".format(sys.argv[0]))
sys.exit(1)
for path in sys.argv[1].split(','):
for p in iglob(path, recursive=True):
print(p)
2.CMake
CMake自然无须多言,其成为业界事实标准,几乎所有主流项目都支持。其丰富的文档、教程和插件,生态成熟的同时,对复杂的项目配置的支持、跨平台构建、第三方包集成以及自定义脚本都很优秀。但是与GN和Meson相比,生成Ninja文件的速度稍逊一筹,并且对新手不友好,需要较长时间熟悉其语法和功能,在大规模项目中,易导致配置膨胀,CMakeLists文件难以维护。
CMake安装
sudo apt install cmake
cmake .. -G Ninja
CMake构建样例
cmake-sample.tar.gz是关于cmake构建的入门样例.
cmake_minimum_required(VERSION 3.16.0)
project(cmake-sample)
set(CMAKE_VERBOSE_MAKEFILE on)
set(CMAKE_EXPORT_COMPILE_COMMANDS on)
set(deplib json-c tools)
set(srcs ./src/main.c)
add_library(tools SHARED ./src/tools.c)
target_compile_options(tools PRIVATE -g -O0)
foreach(item ${srcs})
get_filename_component(exec ${item} NAME_WE)
add_executable(${exec} ${item})
target_compile_options(${exec} PRIVATE -g -O0)
target_link_libraries(${exec} PRIVATE ${deplib})
endforeach()
3.Ninja常用命令
依赖关系图
在Ninja源代码树中,为gn目标生成构建图表。如果没有给出目标,则为所有根目标生成一个图表
ninja -C out/Debug -t graph default | dot -Tpng -o default.png
#在Web浏览器中浏览依赖关系图。单击文件会将视图集中在该文件上,显示输入和输出
#该功能在某些版本存在bug
ninja -C out/Debug -t browse --port=8000 --no-browser gn

清除构建产物
ninja -C out/Debug -t clean
显示构建命令
ninja -C out/Debug -t commands
gcc -MMD -MF obj/src/a.out.main.o.d -I../../src -c ../../src/main.c -o obj/src/a.out.main.o
gcc -MMD -MF obj/src/libtools.tools.o.d -c ../../src/tools.c -o obj/src/libtools.tools.o
g++ -shared -o ./libtools.so -Wl,-soname=libtools.so @libtools.so.rsp
g++ -o a.out -Wl,--start-group @a.out.rsp -Wl,--end-group -ljson-c
touch obj/default.stamp
编译数据库
ninja -C out/Debug -t compdb
调试编译过程
ninja -C out/Debug -v -d stats -d explain -d keepdepfile -d keeprsp