C++ Bazel/Ninja:大规模 C++ 项目的快速构建系统

好的,各位观众老爷们,今天咱们聊聊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的流程如下:

  1. 编写CMakeLists.txt文件,描述项目的结构和依赖关系。
  2. 使用CMake生成Ninja的构建文件。
  3. 使用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_utilsfile_utils,以及一个可执行文件my_app

构建步骤:

  1. 创建build目录:mkdir build && cd build
  2. 使用CMake生成Ninja构建文件:cmake -G Ninja ..
  3. 使用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的文档非常丰富,可以帮助你解决各种构建问题。
  • 多实践,多尝试,才能真正掌握这些工具。

希望今天的分享对大家有所帮助!谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注