1. gn 构建系统简介
1.1 构建系统发展历史
我最早开发 iOS app 时,只知道使用 Xcode 一键运行,并不知道从代码到构建,Xcode 在中间帮我们做了什么。后来知道了 Xcode 使用 clang + llvm 编译,再后来了解到 Xcode 在中间其实还生成了 makefile.
不只是 Xcode,基本上常用的 IDE 都会把编译相关的细节隐藏起来,来提高工程开发效率。但是在计算机早期,工程师需要处理很多编译相关的工作,尤其是需要跨平台开发。
石器时代
在计算机早期编程中,工程结构简单,工程师也往往只在一个计算平台开发,此时一个强大的编译器就可以完成工作。主流编译器有 msvc、gcc、clang + llvm.
农业时代
随着不同的系统发展起来,工程师需要在各个操作系统上实现同一个应用,基本的方式都是将大部分能复用的代码和逻辑封装到底层,减少冗余的开发和维护工作。但是各平台底层编译链接都有所不同,任何一个源码文件的增加/删除、模块调整都会涉及到多个集成开发环境的工程调整。
此时我们可以编写 makefile 文件,使用各平台上的 make 来编译 makefile 文件。如微软的 MS nmake、GNU 的 make.
但各平台的 make 实现不同,需要针对各平台单独编写差异巨大的 makefile 文件。
工业时代
使用 cmake 元构建系统,编写统一的构建文件,最后由 cmake 自动生成各平台相关的 makefile 文件执行编译。把大部分构建工作自动化,cmake 已经是比较好的跨平台工具了,一般的跨平台工程基本已经满足需求了。
信息时代
当工程规模增大到难以想象的量级时,编译速度和工程模块的划分变得尤为重要。Google 在管理 Chromium 工程时,开发了 gyp,后面升级为 gn,gn 再调用 ninja. 这套组合拳,使 Chromium 庞大的工程拥有更加清晰的模块,和更快的编译速度。
1.2 gn 的优势
速度快
gyp(python 编写)比 cmake 快,而 gn(C++ 编写)比前辈 gpy 还快 20 倍。
我们 Android 曾经尝试用 cmake 改造来构建 Chromium,结果卡死了,但是用 gn 就很顺利。
易编写
gn 构建脚本的语言风格类似于常规代码。
这里看个简单的例子。
- 要构建一个可执行文件,只需要在
BUILD.gn文件中,声明一个executable(),gn 里这是一个target. - 然后在里面使用
sources声明包含的源文件,使用deps声明依赖的其他target.
工具强大
gn 有很多命令行工具来辅助工程构建,比如查询目标依赖等。这个会在后面进行介绍。
调试支持
我们在 gn 构建脚本中,可以使用 print 调试 gn 文件。当构建开始时,将会在终端中打印配置、输出参数等。也有 assert 断言来中断异常。
我们熟知的 Chromium、鸿蒙系统等大型工程都使用 gn 来管理构建。
2. gn 构建流程和示例
2.1 项目结构
我们通过一个示例项目来看一下 gn 项目的工程结构。
文件
说明
.gn
根文件,所在目录为构建根目录(路径以 // 开头)
build/BUILDCONFIG.gn
配置文件,定义默认的配置、使用的工具链
build/toolchain/BUILD.gn
构建工具链,定义构建的编译、链接规则
BUILD.gn
构建目标入口,从这里开始执行代码的构建
out/
构建结果目录,可以生成多个构建
2.2 gn 构建示例
安装 gn
首先我们需要安装 gn. gn 的官网地址是 gn.googlesource.com/gn,也是 gn 源码仓库地址。
我们可以直接下载 gn 二进制文件:
也可以通过源码来构建:
git clone https://gn.googlesource.com/gn
cd gn
python build/gen.py
ninja -C out
# To run tests:
out/gn_unittests
构建项目
如果是通过源码构建的 gn,在源码仓库中,有个 Google 提供的示例项目 /examples/simple_build,这也是上一节介绍 项目结构 时所用的例子。
构建该项目使用以下命令:
cd examples/simple_build
../../out/gn gen -C out/a_build
ninja -C out/a_build
# Execute binary
./out/a_build/hello
我们再来看下构建的主要成果物。
成果物
类型
源文件
构建 target
hello
可执行二进制文件
hello.cc
executable("hello")
libhello_shared.so
动态库
hello_shared.h/.cc
shared_library("hello_shared")
libhello_static.a
静态库
hello_static.h/.cc
static_library("hello_static")
2.3 构建流程
上面介绍了构建 gn 项目的主要命令。那执行这些命令时,gn 做了哪些工作呢?
- 执行
gn gen时,在当前目录中查找.gn文件,然后沿着目录树向上走,直到找到.gn文件所在目录为止。将此目录设置为“source root”并解析此文件以查找构建配置文件的名称。 - 执行构建
.gn中声明的配置文件BUILDCONFIG.gn,获取默认的工具链文件的位置。 - 执行
toolchain,确定编译、链接规则。 - 找到根目录下的
BUILD.gn文件。 - 递归加载其他目录下的
BUILD.gn以解析所有当前依赖项。 - 解决目标的依赖关系后,将
xx.ninja文件写入构建结果目录。 - 解决所有目标后,写出根
build.ninja文件。 - 再执行
ninja -C命令进行编译。
3. 重要语法
3.1 数据类型
字符串
字符串的定义方式和其他语言大同小异,也可以使用 $/${} 来引用前面的变量。
数组
数组可以使用 += 运算符添加新的数组元素,相应的 -= 可以删除数组元素。
3.2 控制语句
条件判断
条件判断也是常规的使用方式。
循环
循环 foreach 有2个参数,参数2是待遍历的数组。需要注意的是,参数1不是遍历时的索引,而是元素的拷贝。
3.3 函数调用
普通函数
gn 提供了一些内置函数供我们调用,print、assert 等用于调试支持。
调用目标
上面构建动态库 shared_library()、静态库 static_library() 等,调用的是 gn 的内置函数,或者称为目标(target)。
3.4 文件路径
相对路径
根目录绝对路径
.gn 文件所在的目录是构建根目录。
系统目录绝对路径
3.5 目标
gn 提供的目标(target)用于不同的构建类型、执行脚本等。以下是部分常用目标:
目标类型
说明
action、action_foreach
运行脚本,仅支持 python
bundle_data、create_bundle
创建 Mac/iOS 包
executable
生成一个可执行文件
group
将一组目标组织起来
shared_library
动态库,.dll、.so
loadable_module
只能在运行时加载的 .dll、.so
source_set
轻量级虚拟静态库,构建速度比静态库快
static_library
静态库,.lib、.a
目标(target)是函数类型,可以通过模板自定义目标。
3.6 模板
通过模板,我们可以自定义目标,来抽象一些重复使用的构建任务。
定义模板
模板定义在 xx.gni 文件中。和 BUILD.gn 不同,xx.gni 的文件名可以自定义。
模板有2个内置参数:
- target_name: 调用模板时声明的目标名称。
- invoker: 调用者的对象。可以使用
invoker.sources、invoker.deps等属性获取到调用者的构建内容。
使用模板
本例子中,模板定义中内置参数的值为:
- target_name = my_interfaces
- invoker.sources = [ "a.idl", "b.idl" ]
3.7 配置
配置通常用于设置目标的编译选项、头文件目录、宏定义等。
定义配置
使用配置
公共配置
使用 public_configs 声明的公共配置,可以被当前目标的上一级调用者使用。类似的还有 public_deps 公共依赖。
4. 实用命令
4.1 help
gn help: 查看 gn 全部命令的帮助信息gn help <command>: 针对某个命令查看详细帮助信息
4.2 gen
gn 命令中最常用的应该是 gn gen -C <out_dir> 了,这个命令在上面 构建项目 小节中已经有过接触。
该命令用于在指定的构建目录(例子中是 out/a_build)中生成 ninja 文件,随后再使用 ninja 命令来执行编译。
4.3 args
gn args 可以查看当前构建的配置参数,也可以生成构建配置文件。
在学习该命令前,我们先看下如果声明构建参数。
构建参数
定义构建参数:
使用构建参数:
查看构建参数
gn args <out_dir> --list
可以查看指定构建的参数、参数的默认值,还能看到声明自定义参数所在的文件,如 is_debug 参数位于根目录的 BUILD.gn 文件中。
指定构建参数
gn args <out_dir>
执行该命令时,会在终端打开一个文本编辑器,我们在编辑器中指定构建的参数。保存退出后,gn 会创建 <out_dir>/args.gn 文件;同时,会自动执行 gn gen,根据刚刚的构建配置,生成 xx.ninja 文件。
4.4 desc
查看完整构建信息
gn desc <out_dir> <target_name>
使用该命令我们可以查看某个 target 或 config 的构建信息,包含源文件、公开的头文件、依赖的库、编译参数等。
该命令非常强大,借助于它,我们可以编写脚本,把构建出的二进制、公开的头文件、相关的依赖库等提取出来,作为一个独立的完整库提供给其他项目。比如我们曾剥离 Chromium 中的 base 库、net 库:
查看依赖树
gn desc <out_dir> <target_name> deps --tree
查看某个 target 的依赖树。
4.5 其他常用命令
命令
作用
gn path <out_dir> <target_name> <target_name>
查看目标之间的依赖路径
gn refs <out_dir> <target_name> --tree
查看有哪些其他目标依赖了该目标
gn clean <out_dir>
清空某个构建目录,保留 args.gn、build.ninja
gn format <list of build_files...>
格式化 BUILD.gn
gn ls <out_dir>
列出某个构建的所有目标
gn ls <out_dir> “<directory>”
列出某个构建在某个目录下的目标
5. vcpkg 包依赖平台
如果我们的 C++ 工程是一个基础库,需要提供给其他项目使用。解决了 C++ 工程的构建问题,还需要解决包依赖的问题。
不像 iOS 有 CocoaPods,基本已成为工程标准。目前市面上 C++ 包依赖工具还处于发展阶段,比较主流的 C++ 包依赖工具有 Conan、vcpkg、xrepo 等。他们都支持 cmake 构建系统,而原生支持本文 gn 构建系统的暂时只有 vcpkg.
5.1 简介
vcpkg 是微软 C++ 团队开发的适用于 C 和 C++ 库的跨平台开源软件包管理器,它大大简化了 Windows、Linux 和 macOS 上第三方库相关的下载和配置操作,目前已有超过1600个第三方库可以通过 vcpkg 来安装。
vcpkg 的优势:
- 自动下载开源库源代码。
- 一键安装第三方库。
- 源码包的缓存管理和版本管理,可以依需求安装指定的版本。
- 自动检查库的依赖关系并安装其依赖项。
- 无缝集成 Visual Studio,不用手动设置任何的库相关的路径。
- 主流系统多平台支持,包括 Windows、Linux、macOS.
vcpkg 的官方源码地址:github.com/microsoft/v…
本文我简单介绍下 vcpkg 针对 gn 的应用,后续将写文章更详细地介绍 vcpkg.
5.2 打包 gn 项目
通过 vcpkg,可以直接使用它官方提供的第三方库。但我们组内开发时,会有一些组件和模块沉淀下来,并需要打包提供给其他项目。通过 vcpkg,我们能够将使用 gn 构建的组件进行打包。
创建注册表 git 仓库
vcpkg 管理私有库,提供的解决方案叫
registries 注册表。目前主要有两种方式可以实现:git registries与filesystem registries.Git registries
简单来说,git 注册表就是一个简单的 git 仓库,包含所需要的文件,可以通过 git 仓库的正常方式来实现私有分享或者公共分享,例如:vcpkg git 库。
Filesystem registries
文件系统注册表就是所创建的注册表只在文件系统中,唯一办法就是通过文件共享来共享给其他人,对于非 git 的版本控制项目来说,这种方式还是很实用的。
本文主要介绍通过
git registries管理私有库。
从 iOS CocoaPods 的角度来看,注册表可以理解为 Specs 仓库:github.com/CocoaPods/S…
我们创建一个 git 仓库,并创建注册表所需的目录结构。
注册表目录结构说明如下:
-- ports/ // 该目录下存放私有库的构建脚步、元数据,每个子目录是一个库
---- beicode/ // 该级目录命名为私有库的名称
------ portfile.cmake // 如何拉取代码、构建私有库的脚本
------ vcpkg.json // 私有库的元数据
-- versions/ // 该目录下存放私有库的版本信息
---- baseline.json // 所有私有库的默认版本信息
---- b-/ // 该级目录命名格式为私有库的"首字母-",相同首字母的私有库的版本文件放在同一个目录下
------ beicode.json // 私有库的版本信息
编写私有库的注册表
编写私有库的注册表,主要是编写 portfile.cmake 构建脚本。打包 gn 构建的脚本的主要函数如下:
# 从 git 仓库拉取代码
vcpkg_from_git(
OUT_SOURCE_PATH <SOURCE_PATH>
URL <https://android.googlesource.com/platform/external/fdlibm>
REF <59f7335e4d...> # commit id
[HEAD_REF <ref>] # branch
[PATCHES <patch1.patch> <patch2.patch>...]
)
# gn 配置,debug/release 等
vcpkg_configure_gn(
SOURCE_PATH <SOURCE_PATH>
[OPTIONS <OPTIONS>]
[OPTIONS_DEBUG <OPTIONS_DEBUG>]
[OPTIONS_RELEASE <OPTIONS_RELEASE>]
)
# 构建 gn 仓库
vcpkg_install_gn(
SOURCE_PATH <SOURCE_PATH>
[TARGETS <target>...] # 需要构建的 gn target
)
使用私有库
在实际工程的根目录下,创建 vcpkg-configuration.json、vcpkg.json 两个文件。这两个文件组合,可以理解为 CocoaPods 在工程中编写的 Podfile.
vcpkg-configuration.json 该文件中声明需要被 vcpkg 查找、安装到的私有库。文件格式如下:
{
"registries": [ # 使用的注册表,可以添加多个
{
"kind": "git",
"baseline": "a1031ac1a7cc7a69a1bc994874808e6179b5eda0", # 注册表仓库指定的 commit id
"repository": "https://g.hz.netease.com/zhuguang/vcpkg-registry", # 注册表仓库地址
"packages": ["beicode"] # 需要使用的包,可以添加多个
}
]
}
vcpkg.json 则是编写工程实际需要依赖的库,包括 vcpkg 官方库、vcpkg-configuration.json 中声明的私有库。文件格式如下:
{
"$note": "json 不能添加注释。vcpkg 定义以 $ 开头的字段为注释",
"name": "demo",
"version": "0.0.1",
"dependencies": [
"sqlite3",
{
"name": "beicode",
"version>=": "0.0.1"
}
],
"builtin-baseline": "99dc49dae7e170c3be63dd097230007f3bb73c4f",
"overrides": [
{
"name": "beicode",
"version": "0.0.1"
}
]
}
然后在工程 build/ 目录下,执行 [path to vcpkg]/vcpkg install,就会将 vcpkg.json 中定义的依赖库,安装到 build/vcpkg_installed/ 目录下。
在工程的 CMakeLists.txt 中,将可执行文件和打包的库链接起来。
# CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(test)
add_executable(main main.cpp)
find_package(unofficial-sqlite3 CONFIG REQUIRED)
target_link_libraries(main PRIVATE unofficial::sqlite3::sqlite3)
此时便可以启动执行完整的工程。
工程执行 cmake 需要指定 vcpkg 的 toolchain:
$ cmake -B [build directory] -S . "-DCMAKE_TOOLCHAIN_FILE=[path to vcpkg]/scripts/buildsystems/vcpkg.cmake"
vcpkg 目前支持打包 gn 构建的组件,但是实际工程仍然需要使用 cmake 作为构建系统。针对实际工程也使用 gn 的项目,我们尝试通过打包库路径依赖、gn 调用 cmake 等方式解决,后续文章也将介绍。
6. 最后
gn 作为由 Google 创造,又被华为青睐的构建系统,其编译速度快、脚本语法简单、目录结构清晰。我们内部多个 C++ 组件库,都使用 gn 来执行构建。
现在又有了微软的 vcpkg 包依赖平台,也能解决组件库需要手动打包、或者源码依赖提供给其他实际工程的问题。