chromium
的编译过程中用到了GYP,GN和Ninja这三个构建工具,GYP
是一个在不同平台构建项目的工具,GN
是GYP
的升级版,Ninja
是一个小型追求速度的构建系统。
GYP
是Generate Your Projects
的缩写,GYP
的目的是为了支持更大的项目编译在不同的平台,比如Mac
,Windows
,Linux
,它可以生成Xcode工程,Visual Studio工程,Ninja编译文件和Makefiles。
GYP
的输入是.gyp
和.gypi
文件,.gypi
文件是用于.gyp
文件include使用的。.gyp
文件就是符合特定格式的json
文件。
先来看一个chromium
中缩减的.gyp
文件:
{
'variables': {
.
.
.
},
'includes': [
'../build/common.gypi',
],
'target_defaults': {
.
.
.
},
'targets': [
{
'target_name': 'target_1',
.
.
.
},
{
'target_name': 'target_2',
.
.
.
},
],
'conditions': [
['OS=="linux"', {
'targets': [
{
'target_name': 'linux_target_3',
.
.
.
},
],
}],
['OS=="win"', {
'targets': [
{
'target_name': 'windows_target_4',
.
.
.
},
],
}, { # OS != "win"
'targets': [
{
'target_name': 'non_windows_target_5',
.
.
.
},
}],
],
}
上面指定下面几个属性的值:
variables
: 定义可能被修改或者用于文件其它地方的变量。
includes
: 需要包含进来的有.gypi
后缀的文件。target_defaults
: 默认设置,应用于文件中的所有target。targets
: 指定该文件生成的target列表。conditions
: 指定不同的条件,修改文件中的变量。
下面来看构建一个简单的可执行文件的target:
{
'targets': [
{
'target_name': 'foo',
'type': 'executable',
'msvs_guid': '5ECEC9E5-8F23-47B6-93E0-C3B328B3BE65',
'dependencies': [
'xyzzy',
'../bar/bar.gyp:bar',
],
'defines': [
'DEFINE_FOO',
'DEFINE_A_VALUE=value',
],
'include_dirs': [
'..',
],
'sources': [
'file1.cc',
'file2.cc',
],
'conditions': [
['OS=="linux"', {
'defines': [
'LINUX_DEFINE',
],
'include_dirs': [
'include/linux',
],
}],
['OS=="win"', {
'defines': [
'WINDOWS_SPECIFIC_DEFINE',
],
}, { # OS != "win",
'defines': [
'NON_WINDOWS_DEFINE',
],
}]
],
},
],
}
target_name
: 唯一的来表示工程名称。
type
: 文件类型,这里是executable
。msvs_guid
: 用于生成Visual Studio solution
文件的GUID
值。dependencies
: 该target所依赖的其它target。defines
: 宏定义,用于-D
or/D
。include_dirs
: 包含头文件的文件夹,用于-I
or/I
。sources
: 该target的源文件列表。conditions
: 一些条件设置。
再来看一个简单的库的target:
{
'targets': [
{
'target_name': 'foo',
'type': '<(library)'
'msvs_guid': '5ECEC9E5-8F23-47B6-93E0-C3B328B3BE65',
'dependencies': [
'xyzzy',
'../bar/bar.gyp:bar',
],
'defines': [
'DEFINE_FOO',
'DEFINE_A_VALUE=value',
],
'include_dirs': [
'..',
],
'direct_dependent_settings': {
'defines': [
'DEFINE_FOO',
'DEFINE_ADDITIONAL',
],
'linkflags': [
],
},
'export_dependent_settings': [
'../bar/bar.gyp:bar',
],
'sources': [
'file1.cc',
'file2.cc',
],
'conditions': [
['OS=="linux"', {
'defines': [
'LINUX_DEFINE',
],
'include_dirs': [
'include/linux',
],
],
['OS=="win"', {
'defines': [
'WINDOWS_SPECIFIC_DEFINE',
],
}, { # OS != "win",
'defines': [
'NON_WINDOWS_DEFINE',
],
}]
],
],
}
大部分和可执行文件的target是一样的,有些不一样的:
type
: 类型要设置为<(library)
direct_dependent_settings
: 这些设置会应用到直接依赖于这个target的target,也就是在dependencies
中指定了该target的。
export_dependent_settings
: 导出列target的direct_dependent_settings
设置到目标target。
下面来看几个简单的例子:
{
'targets': [
{
'target_name': 'foo',
'type': 'executable',
'sources': [
'independent.cc',
'specific_win.cc',
],
},
],
},
生成一个可执行文件foo
,参与编译的源文件有independent.cc
,specific_win.cc
,可以通过指定后缀_linux
,_mac
,_posix
,_win
来指定某个文件在指定的平台才参与编译,比如上面specific_win.cc
只在Windows平台参与编译。
也可以指定conditions
,在不同的平台,加上不同的条件:
{
'targets': [
{
'target_name': 'foo',
'type': 'executable',
'sources': [
'linux_specific.cc',
],
'conditions': [
['OS != "linux"', {
'sources!': [
# Linux-only; exclude on other platforms.
'linux_specific.cc',
]
}[,
],
},
],
},
上面表示:如果不是linux平台,就不包含源文件linux_specific.cc
。
再来看下依赖的使用:
{
'targets': [
{
'target_name': 'new_unit_tests',
'type': 'executable',
'defines': [
'FOO',
],
'include_dirs': [
'..',
'include',
],
'dependencies': [
'other_target_in_this_file',
'other_gyp2:target_in_other_gyp2',
],
'sources': [
'new_additional_source.cc',
'new_unit_tests.cc',
],
},
],
}
dependencies
可以指定依赖,该文件的其它target,或者其它文件的某个target。
或者指定编译参数:
{
'targets': [
{
'target_name': 'existing_target',
'conditions': [
['OS=="win"', {
'cflags': [
'/WX',
],
}, { # OS != "win"
'cflags': [
'-Werror',
],
}],
],
},
],
},
target之间的相互依赖:
{
'targets': [
{
'target_name': 'foo',
'type': 'executable',
'dependencies': [
'libbar',
],
},
{
'target_name': 'libbar',
'type': '<(library)',
'defines': [
'LOCAL_DEFINE_FOR_LIBBAR',
'DEFINE_TO_USE_LIBBAR',
],
'include_dirs': [
'..',
'include/libbar',
],
'direct_dependent_settings': {
'defines': [
'DEFINE_TO_USE_LIBBAR',
],
'include_dirs': [
'include/libbar',
],
},
},
],
}
foo
依赖libbar
。也可以是其它文件的libbar
,那就要写成'../bar/bar.gyp:libbar',
。
而且foo
会加上编译选项-DDEFINE_TO_USE_LIBBAR -Iinclude/libbar
。
还支持Mac OS X bundles
{
'target_name': 'test_app',
'product_name': 'Test App Gyp',
'type': 'executable',
'mac_bundle': 1,
'sources': [
'main.m',
'TestAppAppDelegate.h',
'TestAppAppDelegate.m',
],
'mac_bundle_resources': [
'TestApp/English.lproj/InfoPlist.strings',
'TestApp/English.lproj/MainMenu.xib',
],
'link_settings': {
'libraries': [
'$(SDKROOT)/System/Library/Frameworks/Cocoa.framework',
],
},
'xcode_settings': {
'INFOPLIST_FILE': 'TestApp/TestApp-Info.plist',
},
},
GN
GN(Generate Ninja)是chromium project用来取代GYP的新工具,由于GN是用C++
编写,比起用 python
写的GYP快了很多,GN新的DSL的语法也被认为是比较好写以及维护的。
在source project的根目录新增一个.gn
,内容如下:
buildconfig = "//build/BUILDCONFIG.gn"
.gn
所在的目录会被GN工具认定是project的source root,.gn
的内容最基本就是用buildconfig
来指定build config
的位置,其中//build/BUILDCONFIG.gn
用来指定相对于source root的路径。
建立build/NUILDCONFIG.gn
根据前面的设定,需要在build/
下再新增一个BUILDCONFIG.gn
,内容如下:
set_default_toolchain("//build/toolchains:gcc")
cflags_cc = [ "-std=c++11" ]
第一行指定要使用的toolchain
,参数给的是一个label,//build/toolchains:gcc
指的是build/toolchains/BUILD.gn
里面定义的gcc
toolchain。第三行则是设定编译C++时会用到的命令行参数。
建立build/toolchains/BUILD.gn
因为GN没有内建的toolchain
规则,toolchain里的各种tool例如 cc
,cxx
,link
等必须自己指定:
toolchain("gcc") {
tool("cc") {
depfile = "{{output}}.d"
command = "gcc -MMD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_c}} -c {{source}} -o {{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}} -o {{output}}"
depsformat = "gcc"
description = "CXX {{output}}"
outputs = [
"{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o",
]
}
tool("alink") {
rspfile = "{{output}}.rsp"
command = "rm -f {{output}} && ar rcs {{output}} @$rspfile"
description = "AR {{target_output_name}}{{output_extension}}"
rspfile_content = "{{inputs}}"
outputs = [
"{{target_out_dir}}/{{target_output_name}}{{output_extension}}",
]
default_output_extension = ".a"
output_prefix = "lib"
}
tool("solink") {
soname = "{{target_output_name}}{{output_extension}}" # e.g. "libfoo.so".
rspfile = soname + ".rsp"
command = "g++ -shared {{ldflags}} -o $soname -Wl,-soname=$soname @$rspfile"
rspfile_content = "-Wl,--whole-archive {{inputs}} {{solibs}} -Wl,--no-whole-archive {{libs}}"
description = "SOLINK $soname"
# Use this for {{output_extension}} expansions unless a target manually
# overrides it (in which case {{output_extension}} will be what the target
# specifies).
default_output_extension = ".so"
outputs = [
soname,
]
link_output = soname
depend_output = soname
output_prefix = "lib"
}
tool("link") {
outfile = "{{target_output_name}}{{output_extension}}"
rspfile = "$outfile.rsp"
command = "g++ {{ldflags}} -o $outfile -Wl,--start-group @$rspfile {{solibs}} -Wl,--end-group {{libs}}"
description = "LINK $outfile"
rspfile_content = "{{inputs}}"
outputs = [
outfile,
]
}
tool("stamp") {
command = "touch {{output}}"
description = "STAMP {{output}}"
}
tool("copy") {
command = "cp -af {{source}} {{output}}"
description = "COPY {{source}} {{output}}"
}
}
注意下之前提到的cflags_cc
是怎么被tool cxx使用的。
建立BUILD.gn
最后在source root新增一个BUILD.gn
,内容如下:
executable("hello") {
sources = [
"main.cpp",
]
}
指定hello执行程序由main.cpp编译。
编译
先用gn gen
指定在out/
目录里面生成ninja。
gn gen out
再执行ninja来build code
ninja -C out
以后所有修改gn设定的话,也不用重新执行gn,ninja会自动更新设置:
Ninja是一个追求速度的构建系统,相比别的构建系统,Ninja的特点是快和简洁,仅保留最少的特性来提高编译速度。Ninja使用build.ninja文件来定义构建规则,和Makefile里的元编程不同,build.ninja几乎是完全静态的,动态生成依赖其他工具,如gyp或者CMake。
build.niinja
build.niinja相当于ninja的makefile,一个简单的build.ninja
文件如下,分为rule
和dependency
两部分。
# part rull
cc=gcc
cflags= -g -c
rule cc
command = $cc $cflags $in -o $out
rule link
command = $cc $in -o $out
rule cleanup
command = rm -rf *.exe *.o
#part dependency
build func.o : cc func.c
build main.o : cc main.c
build app.exe : link main.o func.o
build all: phony || app.exe
build clean: cleanup
ninja命令的使用如下:
# compile
ninja
# help
ninja -h
其它细节
phony
: 可以创建其他target的别名。如上面的build all: phony || app.exe
。
default
: 如果没有在命令行中指定target,可以使用default来指定默认的target。
pools
: 为了支持并发作业,Ninja还支持pool的机制,和用-j
并行模式一样。
Ninja构建日志保存在构建过程的根目录或.ninja文件中 builddir
变量对应的目录的.ninja_log文件中。
Ninja的定位非常清晰,就是达到更快的构建速度。
ninja的设计是对于make的缺陷的考虑,认为make有下面几点造成编译速度过慢:
ninja认为描述文件应该是这样的:
针对这点所以基本上可以认为ninja就是make的最最精简版。
ninja相对于make增加了下面这些功能:
构建工具太多了,我个人觉得make主要偏大众化一点,可以进行各种隐式推导,比较灵活,每一条命令执行都有输出。
而Ninja主要的设计目的是为了像chromium
这种大型项目,能够显著的提高编译速度,一方面它去掉了各种计算和推导,把一些耗时的需要计算的东西去掉了,只留下简单重要的部分,所以如果自己去写build.ninja
文件的话比较繁琐,所以都是依赖于其它构建工具生成的,另一方面它每次输出只输出一个描述,而不是真正的命令执行输出,真正的命令执行再后台运行,只有警告和报错信息才会显示出来,这也提高了它的速度。
Make vs Ninja Performance Comparison这篇文章对Make接Ninja进行测试对比。