Ubuntu 18.04下GN与Ninja编译工具链的完整配置与实践指南
1. 项目概述:为什么要在Ubuntu 18.04上折腾GN和Ninja?
如果你正在接触像Chromium、Fuchsia、或者一些基于GN构建系统的大型C++项目,那么“gn”和“ninja”这两个名字对你来说肯定不陌生。它们不是某种神秘的忍者工具,而是现代C++生态里高效构建的黄金搭档。我最近在为一台老旧的Ubuntu 18.04服务器配置PX4无人机固件或类似嵌入式项目的编译环境时,就重新踩了一遍坑。网上教程很多,但要么版本过时,要么步骤零散,对于新手或者需要稳定复现的环境来说,总差那么点意思。
简单来说,GN (Generate Ninja)是一个元构建系统,它不直接编译代码,而是读取项目中的BUILD.gn文件,然后生成一个供Ninja读取的build.ninja文件。Ninja则是一个专注于速度的小型构建工具,它只做一件事:以最快的速度执行GN生成的构建指令。这种“生成-执行”的分离架构,让大型项目的配置和编译过程变得极其高效和清晰。在Ubuntu 18.04上,系统仓库自带的Ninja版本通常较老,而GN则根本不存在,这就需要我们手动安装或编译。这个过程本身不复杂,但其中的版本匹配、环境配置和后续验证,却藏着不少新手容易忽略的细节。这篇文章,我就结合最近的实际操作,把从零开始、到能成功运行gn gen和ninja命令的完整路径,以及我踩过的那些坑,给你彻底讲明白。
2. 环境准备与核心工具解析
在开始动手之前,我们得先搞清楚要安装的是什么,以及为什么选择特定的安装方式。Ubuntu 18.04的官方源在某些方面已经略显陈旧,对于开发工具链,追求稳定和可控的版本比一味求新更重要。
2.1 系统基础环境确认
首先,确保你的Ubuntu 18.04系统是最新状态。打开终端,执行以下命令:
sudo apt update sudo apt upgrade -y这个操作会更新软件包列表并升级所有可升级的包。对于一台干净的、刚安装的系统,这一步至关重要,它能避免很多因基础库版本过低导致的诡异问题。更新完成后,建议重启一次系统,确保所有更新生效。
接下来,安装一些编译和工具依赖。GN本身是用C++写的,我们需要编译它;而Ninja虽然提供预编译包,但为了确保环境一致性,我们也准备好编译环境:
sudo apt install -y git cmake python3 build-essentialgit:用于从代码仓库克隆GN的源代码。cmake:GN项目可以使用CMake来构建(虽然它主要用自带的build/gen.py)。python3:GN的构建脚本和许多现代项目构建流程都依赖Python 3。build-essential:提供了GCC、G++、make等基础编译工具链。
注意:Ubuntu 18.04默认可能仍然安装了Python 2.7。请务必使用
python3命令。你可以通过python3 --version确认版本(应该是3.6.x)。如果系统没有python3命令,上述安装命令会自动处理。
2.2 Ninja:极速构建执行器
Ninja的设计哲学是“快”。它没有make那样复杂的规则和函数,它的输入文件(build.ninja)更像一个直接的依赖关系执行清单。因此,它的启动和运行开销极小。
在Ubuntu 18.04上安装Ninja,我推荐两种方式,各有优劣:
方案一:通过APT安装(最简单,但版本可能较旧)
sudo apt install -y ninja-build安装后,运行ninja --version查看。在Ubuntu 18.04上,这样安装的版本通常是1.8.x。这个版本对于大多数项目来说已经够用,尤其是如果你不需要用到Ninja最新的某些特性。优点是简单、稳定,与系统集成好。
方案二:从源码编译安装(获取最新版本)如果你想使用最新的Ninja(比如1.11.x),或者需要绝对控制安装路径,可以从源码编译。
- 首先,克隆Ninja的仓库:
git clone https://github.com/ninja-build/ninja.git cd ninja - Ninja的编译非常“元”——它用一个Python脚本引导编译自己:
这个命令会生成一个可执行的python3 configure.py --bootstrapninja二进制文件。 - 将编译好的二进制文件复制到系统路径(例如
/usr/local/bin):sudo cp ninja /usr/local/bin/ - 验证安装:
ninja --version
实操心得:对于生产环境或需要长期稳定的开发环境,我建议使用方案一(APT安装)。版本旧一点反而避免了因Ninja本身更新而引入的不确定性。只有当你的项目明确要求高版本Ninja(例如某些Chromium构建需求),或者你在探索新特性时,才选择方案二。我曾经因为盲目追求新版Ninja,导致与一个老项目的GN模板不兼容,排查了半天才发现是Ninja版本问题。
2.3 GN:元构建系统的生成器
GN是Google开发的工具,用于生成Ninja构建文件。它不是一个通用的构建系统,而是针对像Chromium这样规模巨大、平台众多的项目设计的。它的语法相对简洁,但学习曲线比CMake要陡峭一些。
关键点在于,GN没有现成的二进制包(官方不提供适用于所有Linux发行版的预编译包),我们必须从源码编译。好消息是,GN的源码就托管在Chromium的仓库里,并且编译过程已经脚本化了。
3. GN的编译与安装全流程
这是整个过程中的核心部分,步骤稍多,但跟着走一遍就能成功。我们将在用户目录下操作,避免污染系统空间。
3.1 获取GN源代码
GN的源代码作为Chromium项目的一部分存在。我们不需要克隆整个巨大的Chromium仓库,官方提供了一个包含GN及其直接依赖的独立仓库镜像。
- 创建一个工作目录并进入:
mkdir -p ~/gn_build cd ~/gn_build - 克隆
depot_tools仓库。这是Google用于管理Chromium等大型代码库的工具集合,里面包含了gclient等工具,我们需要用它来同步GN的代码。git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git - 将
depot_tools添加到你的PATH环境变量中,这样你才能直接使用gclient等命令。最好将这一行添加到你的shell配置文件(如~/.bashrc或~/.zshrc)中,使其永久生效。
执行echo 'export PATH="$PATH:$HOME/gn_build/depot_tools"' >> ~/.bashrc source ~/.bashrcsource ~/.bashrc让配置在当前终端立即生效。你可以通过which gclient命令来验证是否添加成功。
3.2 同步GN代码并编译
有了depot_tools,获取和构建GN就变得流程化了。
- 创建一个专门存放GN源码的目录并进入:
mkdir -p ~/gn_source cd ~/gn_source - 使用
fetch命令(来自depot_tools)来获取GN的代码。这个命令会初始化仓库并同步代码。
这里的fetch --nohooks gn--nohooks参数表示跳过运行钩子脚本。对于只构建GN来说,我们不需要运行Chromium的那些预编译钩子,可以节省大量时间和流量。命令执行后,你会看到一个名为gn的目录被创建出来。 - 进入GN源码目录,并切换到稳定分支。直接使用
main分支的代码有时可能不稳定,我们可以切换到一个已知的稳定标签(tag)。
这条命令有点复杂,它做了:列出所有类似版本号的标签 -> 按版本号排序 -> 取最新的一个 -> 创建并切换到一个名为cd gn git checkout -b stable $(git tag -l | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1)stable的新分支。你也可以直接使用一个已知的版本,例如git checkout 0.2123.0(版本号请以仓库实际标签为准)。 - 运行
gclient sync来同步所有依赖。这一步是必须的,它会根据当前GN代码的配置,拉取所需的第三方库。gclient sync - 开始编译GN。GN项目自带了一个构建脚本:
这个python3 build/gen.pygen.py脚本会生成Ninja构建文件。接着,使用Ninja进行编译:
这里的ninja -C out-C out参数告诉Ninja在out目录下寻找build.ninja文件并执行编译。编译过程很快,通常一两分钟就完成了。
3.3 安装与验证
编译完成后,在out目录下会生成一个名为gn的可执行文件。我们需要把它放到系统路径下。
- 将GN二进制文件复制到系统目录,比如
/usr/local/bin,这是存放本地用户安装软件的标准位置:sudo cp out/gn /usr/local/bin/ - 验证GN是否安装成功:
如果成功,你会看到类似“GN version xxxx”的输出。同时,也再次确认Ninja可用:gn --versionninja --version
至此,GN和Ninja都已经在你的Ubuntu 18.04系统上安装完毕。但安装成功只是第一步,更重要的是让它们能为你手上的项目工作。
4. 实战:配置与编译一个示例项目
理论说得再多,不如实际动手试一下。我们用一个最简单的示例来验证GN和Ninja的协作流程,并理解其中的关键文件。
4.1 创建示例项目结构
假设我们要构建一个名为hello_gn的简单C++项目,它包含一个库和一个可执行文件。
- 创建项目根目录和必要的文件:
mkdir -p ~/hello_gn cd ~/hello_gn - 创建源代码文件。首先是一个简单的库(
hello.cc和hello.h):hello.h:#ifndef HELLO_H_ #define HELLO_H_ #include <string> std::string get_greeting(const std::string& name); #endif // HELLO_H_hello.cc:#include "hello.h" std::string get_greeting(const std::string& name) { return "Hello, " + name + " from GN!"; } - 创建主程序(
main.cc):#include <iostream> #include "hello.h" int main() { std::cout << get_greeting("Developer") << std::endl; return 0; }
4.2 编写GN构建定义文件
GN的构建逻辑写在BUILD.gn文件中。我们需要在项目根目录和子目录中创建它。
- 在项目根目录(
~/hello_gn)创建顶层的BUILD.gn文件。这个文件通常用于声明整个项目的构建配置和目标集合。这里我们简单地定义一个executable目标。BUILD.gn(在项目根目录):
同时,在根目录再创建一个# 声明一个可执行文件目标 executable("hello_world") { # 指定源文件列表 sources = [ "main.cc", ] # 指定依赖的库目标 deps = [ "//:hello_lib", # “//”代表项目根目录,“:hello_lib”是根目录下名为hello_lib的目标 ] # 设置输出文件名(可选,默认与目标名相同) output_name = "hello_gn_app" }BUILD.gn来定义我们的库(当然,也可以把库和可执行文件定义在同一个BUILD.gn里,这里分开是为了演示结构):BUILD.gn(同样在项目根目录,追加或新建):
这个结构里,# 声明一个静态库目标 static_library("hello_lib") { sources = [ "hello.cc", "hello.h", ] # 定义公开的头文件目录,这样依赖它的目标才能找到hello.h public_configs = [ ":hello_headers" ] } # 定义一个配置(config),用于处理头文件包含路径 config("hello_headers") { include_dirs = [ "." ] # 将当前目录(项目根目录)添加到头文件搜索路径 }hello_lib是一个静态库,它暴露了一个hello_headers配置,告诉依赖者在哪里可以找到hello.h。然后hello_world可执行文件依赖hello_lib。
4.3 生成Ninja构建文件并编译
现在,我们有了源代码和GN构建定义,接下来就是让GN生成Ninja文件,并用Ninja编译。
在项目根目录,首先需要创建一个构建目录(通常叫
out或build),并进入。然后运行gn gen命令来生成Ninja文件。mkdir -p out/Default cd out/Defaultgn gen命令需要一个参数来指定构建参数文件(args.gn)的位置。我们可以在生成时指定一个基本参数。gn gen . --args='is_debug=true target_cpu="x64"'is_debug=true:生成带调试信息的构建。target_cpu="x64":目标CPU架构是x86_64。 执行成功后,你会在当前目录(out/Default)下看到一个build.ninja文件和一个args.gn文件。args.gn里保存了你刚才传入的参数。
使用Ninja进行编译。现在只需要一条简单的命令:
ninjaNinja会读取
build.ninja文件,分析依赖,然后并行编译所有目标。由于我们的项目很小,瞬间就能完成。如果一切顺利,你会在out/Default目录下看到编译产物:hello_gn_app(可执行文件)和libhello_lib.a(静态库)。运行测试:
./hello_gn_app终端应该会输出:
Hello, Developer from GN!
这个过程清晰地展示了GN/Ninja的工作流:你编写声明式的BUILD.gn文件描述“要构建什么”,GN将其转换为指令式的build.ninja文件描述“如何一步步构建”,最后由Ninja高效地执行这些指令。
5. 深度配置解析与高级用法
掌握了基础编译后,我们来看看如何更精细地控制构建过程,这在实际项目中至关重要。
5.1 理解与配置args.gn
args.gn文件是GN构建的“控制中心”。你可以在gn gen时通过--args设置,也可以事后编辑这个文件。常见的配置项包括:
is_debug:布尔值。true表示调试版本(默认优化级别低,带调试符号);false表示发布版本(高优化级别,无调试符号)。target_cpu:字符串。设置目标CPU架构,如"x64","arm64","x86"。target_os:字符串。设置目标操作系统,如"linux","win","mac"。is_component_build:布尔值。true时会将库构建为动态库(共享库),便于快速增量链接,适合开发;false时构建为静态库,适合发布。clang_use_default_sample_profile:布尔值。与Clang编译器优化相关。- 自定义变量:你还可以在
BUILD.gn中通过declare_args()定义自己的参数,并在args.gn中覆盖。
例如,一个典型的开发配置args.gn可能如下:
# 设置构建类型为调试 is_debug = true # 启用组件化构建(动态库),加速开发时的链接速度 is_component_build = true # 目标平台为64位Linux target_os = "linux" target_cpu = "x64" # 额外的编译器警告 treat_warnings_as_errors = false # 根据团队规范调整 # 自定义我们项目的安装前缀路径(假设) my_project_install_prefix = "/usr/local"重要提示:修改args.gn后,必须重新运行gn gen来重新生成build.ninja文件,因为Ninja文件依赖于这些参数。直接运行ninja不会自动感知args.gn的变化。
5.2 依赖管理与作用域
GN的依赖管理非常清晰。deps字段指定目标所依赖的其他目标。依赖路径使用标签(label)来标识。
- 绝对标签:以
//开头,从项目根目录算起。例如//base:strings表示根目录下base目录中的BUILD.gn文件里定义的strings目标。 - 相对标签:不以
//开头,相对于当前BUILD.gn文件所在目录。例如:helper_lib表示当前目录下的helper_lib目标。 - 外部依赖:可以使用
//third_party/depot_tools/...这样的路径来引用项目外部的代码(前提是这些代码的路径通过某种方式已知,比如通过gclient管理)。
依赖具有传递性吗?默认情况下,配置(configs)和库依赖不具有完全的传递性。一个目标(比如我们的hello_lib)通过public_configs导出的头文件路径,会被其依赖者(如hello_world)继承。但是,如果hello_lib自己依赖了另一个库lib_a,那么hello_world并不会自动链接lib_a,除非它显式地也依赖lib_a,或者hello_lib通过public_deps来声明传递依赖。这是GN为了保持依赖图清晰和避免隐式依赖而做的设计。
5.3 工具链(Toolchain)概念初探
工具链定义了构建时使用的编译器、链接器、编译标志等。GN支持为不同的目标(如主机工具、目标设备)配置不同的工具链。在简单的项目中,你通常使用的是默认工具链。查看默认工具链定义的方式是:
gn args out/Default --list --short在输出中,你会看到很多以*开头的变量,这些就是当前工具链定义的。在复杂的跨平台项目中(例如,在x64主机上编译ARM64的固件),你就需要配置并使用不同的工具链。这通常涉及编写或导入.gni(GN Import)文件来定义新的工具链,并在BUILD.gn中通过toolchain参数指定。对于入门来说,理解默认工具链的存在即可。
6. 常见问题排查与性能优化
即使按照步骤操作,你也可能会遇到一些问题。下面是我在多次配置中总结的常见坑点及其解决方案。
6.1 安装与编译阶段问题
问题1:执行fetch --nohooks gn时网络超时或速度极慢。这是由于仓库源在国外。可以尝试配置Git的代理,或者使用国内镜像源(如果存在)。更务实的办法是耐心等待,或者选择在网络状况好的时候操作。--nohooks已经节省了大量下载钩子脚本的时间。
问题2:gclient sync失败,提示缺少.gclient文件或同步错误。确保你在执行gclient sync之前,已经正确进入了通过fetch命令创建的gn目录。fetch命令会生成一个隐藏的.gclient配置文件,gclient sync需要读取它。如果文件丢失,你可以尝试退回到gn_source目录,重新执行fetch --nohooks gn。
问题3:编译GN时出现Python或编译器错误。首先确认Python 3已正确安装(python3 --version)。其次,确保build-essential已安装,提供了完整的GCC套件。错误信息通常会明确指出缺失的头文件或库,根据提示安装对应的-dev包即可,例如sudo apt install libstdc++-dev。
6.2 项目构建与使用阶段问题
问题4:运行gn gen时提示“ERROR Can’t find source root.”这通常是因为你没有在包含.gn文件的项目根目录下执行命令。GN需要一个项目根目录标识,通常是一个名为.gn的文件。对于我们从零创建的项目,需要手动创建一个空的.gn文件在项目根目录:
cd ~/hello_gn touch .gn然后在.gn里可以指定buildconfig文件的位置,但对于简单项目,一个空文件就足以让GN识别这里是项目根。
问题5:ninja编译失败,提示“未知的目标‘xxx’”或“未定义的引用”。
- “未知的目标”:检查你的
BUILD.gn文件中的deps字段。标签是否拼写正确?路径是否正确?确保你依赖的目标确实在指定的BUILD.gn文件中定义了。 - “未定义的引用”:这是链接错误。说明你的可执行文件或库依赖了某个函数或类,但提供它的库没有被正确链接。请检查:
- 库目标(
static_library或shared_library)是否被正确构建? - 可执行目标的
deps里是否包含了那个库? - 如果是系统库(如
pthread,dl),需要在目标的libs或ldflags中添加。例如:executable("my_app") { ... libs = [ "pthread", "dl" ] # 或者使用ldflags # ldflags = [ "-lpthread", "-ldl" ] }
- 库目标(
问题6:如何清理构建产物?Ninja本身没有make clean这样的命令。你有几种选择:
- 删除整个构建目录:最彻底。
rm -rf out/Default,然后重新gn gen。 - 使用Ninja的
-t clean工具:ninja -t clean。这会删除Ninja记录的所有输出文件,但保留build.ninja和args.gn。 - 在GN中定义
clean步骤:有些项目会在根BUILD.gn里定义一个group目标来包装清理逻辑,但这并不常见。
6.3 性能调优与最佳实践
1. 利用Ninja的并行编译:Ninja默认会使用所有可用的CPU核心进行并行编译。你可以通过-j参数手动指定并行任务数,例如ninja -j 8。对于内存较小的机器,适当降低-j值可以避免内存溢出(OOM)。
2. 为开发启用组件构建(is_component_build=true):这将把大多数库编译成动态库(.so文件)。当你只修改一个源文件时,Ninja只需要重新编译对应的动态库并重新链接可执行文件,速度远快于重新链接所有静态库。这在开发迭代时能节省大量时间。
3. 使用CCache加速重复编译:CCache是一个编译器缓存工具。安装后,Ninja会自动检测并使用它。
sudo apt install ccache安装后无需额外配置,Ninja在后续编译中会自动缓存编译结果,当相同代码再次编译时,直接使用缓存,速度极快。
4. 保持构建目录独立:为不同的构建配置(如Debug、Release、ARM64交叉编译)创建不同的构建目录(如out/Debug,out/Release,out/ARM64_Release)。这样可以在不同配置间快速切换,而不会相互干扰。args.gn文件是每个构建目录独立的。
5. 学习使用gn args命令:gn args out/Default会打开一个编辑器(默认是vim),让你直接编辑args.gn文件,非常方便。gn args out/Default --list可以列出所有可用的参数及其当前值,是探索和调试配置的好帮手。
从在Ubuntu 18.04上安装这两个工具,到理解它们如何协同工作,再到配置一个实际项目并优化构建流程,整个过程的核心在于理解“GN定义,Ninja执行”的分层思想。对于从Makefile或CMake迁移过来的开发者,可能需要一点时间来适应GN的声明式语法和明确的作用域规则,但一旦熟悉,其构建速度和组织大型项目的能力会带来显著的效率提升。尤其是在为像PX4这类复杂的嵌入式项目搭建编译环境时,一套正确配置的GN+Ninja工具链是保证后续开发顺畅的基础。如果在为特定项目配置时遇到问题,第一件事就是检查该项目提供的构建说明,并确保你的GN和Ninja版本符合要求,这能帮你避开大部分初始的坑。
