当前位置: 首页 > 工具软件 > GYP > 使用案例 >

GYP,GN和Ninja

葛鸿轩
2023-12-01

chromium的编译过程中用到了GYPGNNinja这三个构建工具,GYP是一个在不同平台构建项目的工具GNGYP的升级版Ninja是一个小型追求速度的构建系统

GYP

GYPGenerate Your Projects的缩写,GYP的目的是为了支持更大的项目编译在不同的平台,比如MacWindowsLinux,它可以生成Xcode工程,Visual Studio工程,Ninja编译文件和Makefiles。

GYP结构

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: 宏定义,用于-Dor/D
include_dirs: 包含头文件的文件夹,用于-Ior/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。

GYP 实例

下面来看几个简单的例子:

{
'targets': [
{
'target_name': 'foo',
'type': 'executable',
'sources': [
'independent.cc',
'specific_win.cc',
],
},
],
},

生成一个可执行文件foo,参与编译的源文件有independent.ccspecific_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的语法也被认为是比较好写以及维护的。

GN的使用

在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里面定义的gcctoolchain。第三行则是设定编译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的特点是快和简洁,仅保留最少的特性来提高编译速度。Ninja使用build.ninja文件来定义构建规则,和Makefile里的元编程不同,build.ninja几乎是完全静态的,动态生成依赖其他工具,如gyp或者CMake。

build.niinja

build.niinja相当于ninja的makefile,一个简单的build.ninja文件如下,分为ruledependency两部分。

# 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文件中。

Make与Ninja对比

Ninja的定位非常清晰,就是达到更快的构建速度。

ninja的设计是对于make的缺陷的考虑,认为make有下面几点造成编译速度过慢:

  • 隐式规则,make包含很多默认
  • 变量计算,比如编译参与应该如何计算出来
  • 依赖对象计算

ninja认为描述文件应该是这样的:

  • 依赖必须显式写明(为了方便可以产生依赖描述文件)
  • 没有任何变量计算
  • 没有默认规则,没有任何默认值

针对这点所以基本上可以认为ninja就是make的最最精简版。

ninja相对于make增加了下面这些功能:

  • 如果构建命令发生变化,那么这个构建也会重新执行。
  • 所依赖的目录在构建之前都已经创建了,如果不是这样的话,我们执行命令之前都要去生成目录。
  • 每条构建规则,除了执行命令之外,还允许有一个描述,真正执行打印这个描述而不是实际执行命令。
  • 每条规则的输出都是buffered的,也就是说并行编译,输入内容不会被搅和在一起。

构建工具太多了,我个人觉得make主要偏大众化一点,可以进行各种隐式推导,比较灵活,每一条命令执行都有输出。

而Ninja主要的设计目的是为了像chromium这种大型项目,能够显著的提高编译速度,一方面它去掉了各种计算和推导,把一些耗时的需要计算的东西去掉了,只留下简单重要的部分,所以如果自己去写build.ninja文件的话比较繁琐,所以都是依赖于其它构建工具生成的,另一方面它每次输出只输出一个描述,而不是真正的命令执行输出,真正的命令执行再后台运行,只有警告和报错信息才会显示出来,这也提高了它的速度。

Make vs Ninja Performance Comparison这篇文章对Make接Ninja进行测试对比。

参考文档

Ninja - chromium核心构建工具

GN (Generate Ninja) 使用入門

Ninja - a small build system

GYP User Documentation

The Ninja build system

GYP使用笔记

GN Quick Start guide

CMake 入门实战

A LIST OF MAKE SYSTEMS

Make vs Ninja Performance Comparison

tup

 类似资料: