好的,各位观众老爷们,今天咱们聊聊C++项目的大规模杀器:Bazel和Ninja!
开场白:C++ 项目的痛点
各位都是C++老司机,肯定遇到过这种场景:辛辛苦苦写了几个月的代码,信心满满地按下编译按钮,结果…CPU风扇狂转,电脑卡成PPT,半个小时过去了,屏幕上还是那一串串编译信息。更可怕的是,改了一行代码,又要重新编译整个项目!这感觉,就像好不容易拼好的乐高模型,你动了一下,结果全散架了。
C++项目大了,编译速度慢是常态。传统的Makefile、CMake在大型项目面前,往往显得力不从心。这时候,就需要更强大的构建系统来拯救我们于水火之中。
主角登场:Bazel和Ninja
今天的主角就是Bazel和Ninja。先说说Bazel,这玩意儿是Google出品的,专门用来构建大型项目的,特点是:
- 可重复构建: 保证每次构建的结果都一样,排除环境因素的干扰。
- 增量构建: 只编译修改过的部分,大大缩短编译时间。
- 可扩展: 支持多种语言和平台,不仅仅是C++。
- 依赖管理: 自动处理依赖关系,避免版本冲突。
再说Ninja,它是一个小型、快速的构建系统,主要目标是提高构建速度。它通常作为其他构建工具的后端,比如CMake和Meson。
简单来说,Bazel就像一个经验丰富的项目经理,负责整个项目的组织和调度;Ninja就像一个高效的工人,负责快速地执行编译任务。它们俩可以配合使用,发挥出更大的威力。
Bazel:构建规则的艺术
Bazel的核心在于构建规则。你需要告诉Bazel,你的项目由哪些文件组成,它们之间有什么依赖关系,以及如何编译它们。这些信息都写在BUILD
文件中。
咱们来举个例子:
# BUILD 文件
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
cc_library(
name = "my_library",
srcs = ["my_library.cpp"],
hdrs = ["my_library.h"],
visibility = ["//visibility:public"],
)
cc_binary(
name = "my_program",
srcs = ["my_program.cpp"],
deps = [":my_library"],
)
这段代码定义了一个库my_library
和一个可执行文件my_program
。
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
:这行代码导入了Bazel提供的C++构建规则。cc_library(...)
:定义了一个C++库,srcs
指定源文件,hdrs
指定头文件,visibility
指定可见性。cc_binary(...)
:定义了一个C++可执行文件,srcs
指定源文件,deps
指定依赖的库。
语法解释:
name
:构建目标的名称,在Bazel中唯一标识。srcs
:源文件列表。hdrs
:头文件列表。deps
:依赖项列表,可以是其他库或目标。visibility
:可见性设置,//visibility:public
表示公开可见。
要构建这个项目,只需要在命令行输入:
bazel build :my_program
Bazel会自动分析依赖关系,编译my_library
,然后链接成my_program
。
更复杂的例子:
# BUILD 文件
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
cc_library(
name = "string_utils",
srcs = ["string_utils.cpp"],
hdrs = ["string_utils.h"],
visibility = ["//visibility:public"],
copts = ["-std=c++17"], # 设置编译选项
)
cc_library(
name = "file_utils",
srcs = ["file_utils.cpp"],
hdrs = ["file_utils.h"],
visibility = ["//visibility:public"],
deps = [":string_utils"], # 依赖 string_utils 库
)
cc_binary(
name = "my_app",
srcs = ["my_app.cpp"],
deps = [":string_utils", ":file_utils"], # 依赖 string_utils 和 file_utils 库
linkopts = ["-lboost_system"], # 设置链接选项
)
这个例子展示了如何设置编译选项(copts
)和链接选项(linkopts
),以及如何定义库之间的依赖关系。
一些重要的Bazel概念:
- Workspace: 你的项目根目录,包含所有源文件和
BUILD
文件。 - Target:
BUILD
文件中定义的构建目标,比如库、可执行文件等。 - Label: 唯一标识一个Target的字符串,格式为
//path/to/package:target_name
。 - Package: 包含
BUILD
文件的目录。
Bazel的优势:
- 增量构建: Bazel会缓存构建结果,只重新编译修改过的部分。
- 远程缓存: 可以将构建结果缓存到远程服务器上,供团队成员共享。
- 沙箱构建: Bazel会在沙箱环境中构建,避免环境污染。
- 可扩展性: Bazel支持多种语言和平台,可以通过自定义规则来扩展功能。
Ninja:速度的化身
Ninja是一个小型、快速的构建系统。它不负责分析依赖关系,而是直接执行构建命令。因此,Ninja通常需要与其他构建工具配合使用,比如CMake和Meson。
CMake可以生成Ninja的构建文件,然后Ninja负责快速地执行这些文件。
CMake + Ninja:黄金搭档
CMake是一个跨平台的构建工具,它可以生成各种构建系统的构建文件,包括Makefile、Ninja等。
使用CMake + Ninja的流程如下:
- 编写
CMakeLists.txt
文件,描述项目的结构和依赖关系。 - 使用CMake生成Ninja的构建文件。
- 使用Ninja执行构建。
CMakeLists.txt 示例:
cmake_minimum_required(VERSION 3.15)
project(MyApp)
set(CMAKE_CXX_STANDARD 17)
add_library(string_utils string_utils.cpp string_utils.h)
add_library(file_utils file_utils.cpp file_utils.h)
target_link_libraries(file_utils string_utils)
add_executable(my_app my_app.cpp)
target_link_libraries(my_app string_utils file_utils)
这段代码定义了两个库string_utils
和file_utils
,以及一个可执行文件my_app
。
构建步骤:
- 创建build目录:
mkdir build && cd build
- 使用CMake生成Ninja构建文件:
cmake -G Ninja ..
- 使用Ninja构建:
ninja
CMake + Ninja 的优势:
- 跨平台: CMake支持多种平台,可以生成各种构建系统的构建文件。
- 速度快: Ninja执行构建速度非常快。
- 易于使用: CMake的语法相对简单,容易上手。
Bazel vs CMake + Ninja:选哪个?
Bazel和CMake + Ninja都是优秀的构建系统,但它们的应用场景略有不同。
特性 | Bazel | CMake + Ninja |
---|---|---|
适用场景 | 大型项目,需要高度可重复性和增量构建 | 中小型项目,需要跨平台支持和快速构建 |
复杂性 | 较高,需要学习Bazel的构建规则 | 较低,CMake语法相对简单 |
增量构建 | 优秀,支持细粒度的增量构建 | 良好,但不如Bazel |
可重复性 | 优秀,保证每次构建的结果都一样 | 依赖环境,可能存在差异 |
远程缓存 | 支持,可以共享构建结果 | 需要额外配置,不如Bazel方便 |
社区支持 | 活跃,但不如CMake广泛 | 非常活跃,CMake是业界标准 |
总的来说,如果你的项目非常大,对构建速度和可重复性要求很高,那么Bazel是更好的选择。如果你的项目规模适中,需要跨平台支持,并且希望快速构建,那么CMake + Ninja更适合你。
实际项目案例:
假设我们有一个大型C++项目,包含以下目录:
my_project/
├── src/
│ ├── core/
│ │ ├── string_utils.h
│ │ └── string_utils.cpp
│ ├── ui/
│ │ ├── button.h
│ │ └── button.cpp
│ ├── main.cpp
├── include/
│ ├── core/
│ │ └── string_utils.h
│ ├── ui/
│ │ └── button.h
├── BUILD
Bazel 构建文件 (BUILD):
# BUILD
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
cc_library(
name = "core",
srcs = glob(["src/core/*.cpp"]),
hdrs = glob(["include/core/*.h"]),
visibility = ["//visibility:public"],
copts = ["-std=c++17"],
includes = ["include"], # 告诉 Bazel 头文件搜索路径
)
cc_library(
name = "ui",
srcs = glob(["src/ui/*.cpp"]),
hdrs = glob(["include/ui/*.h"]),
visibility = ["//visibility:public"],
copts = ["-std=c++17"],
deps = [":core"],
includes = ["include"],
)
cc_binary(
name = "my_app",
srcs = ["src/main.cpp"],
deps = [":core", ":ui"],
copts = ["-std=c++17"],
includes = ["include"],
)
CMakeLists.txt (CMake + Ninja):
cmake_minimum_required(VERSION 3.15)
project(MyApp)
set(CMAKE_CXX_STANDARD 17)
include_directories(include) # 告诉 CMake 头文件搜索路径
add_library(core
src/core/string_utils.cpp
include/core/string_utils.h
)
add_library(ui
src/ui/button.cpp
include/ui/button.h
)
target_link_libraries(ui core)
add_executable(my_app src/main.cpp)
target_link_libraries(my_app core ui)
代码解释:
glob(["src/core/*.cpp"])
: Bazel的glob
函数用于匹配多个文件,简化了文件列表的编写。CMake中需要手动列出所有文件。includes = ["include"]
: Bazel和CMake都需要指定头文件搜索路径,以便编译器能够找到头文件。target_link_libraries(ui core)
: CMake中显式指定库之间的依赖关系。Bazel通过deps
属性指定依赖关系。
高级技巧:
- 使用
select
语句: Bazel的select
语句可以根据不同的平台或配置选择不同的构建规则。 - 自定义构建规则: 如果你需要构建一些特殊的资源或执行一些自定义的操作,可以编写自己的构建规则。
- 使用远程执行: 可以将构建任务分发到多台机器上并行执行,大大缩短构建时间。
- 使用CI/CD集成: 可以将Bazel或CMake + Ninja集成到CI/CD系统中,实现自动化构建和测试。
总结:
Bazel和Ninja都是强大的构建系统,可以帮助你更快、更可靠地构建C++项目。选择哪个取决于你的项目规模、需求和个人偏好。掌握这些工具,你的C++项目开发效率将会大大提升!
最后,温馨提示:
- 学习Bazel需要花费一些时间,但它是值得的。
- CMake的文档非常丰富,可以帮助你解决各种构建问题。
- 多实践,多尝试,才能真正掌握这些工具。
希望今天的分享对大家有所帮助!谢谢大家!