构建系统指北

blog构建系统

2025-04-0211 min

构建系统(讲义版)

引子

Q:同学们是怎么编译和运行自己的代码的呢?

  • A: 诶🤓☝,点“运行”就可以了啊!
  • B:俺用的是VSCode咋点击运行按钮一直用不了啊啊啊啊啊啊啊...😭
  • A:我不到啊,我用的VS2022 :(🤪
  • C:Linux服务器怎么没有开图形化界面啊啊啊啊啊怎么点运行啊🤡
  • D:上课学了通过gcc在命令行执行编译命令,但是每次都要重复写命令。🧐
  • E:太好了是 自动补全 和 命令历史记录 ,我们有救了... Bro,我的项目结构怎么这么复杂,只靠命令语句完全不够用啊!💀
  • F:......

是的,同学们在大学课程中接触到的,通常就是图形化操作使用编译器命令来构建自己的应用程序

图形化操作的缺陷

如果IDE没有提供一键运行的按钮,或者对应的平台没有提供图形化界面,就难受了

直接使用编译器命令

powershell
gcc -o hello hell.c

缺陷

如果你的项目具有复杂结构,如何优雅的构建项目这件事仍然是一件令人抓狂的事情

使用命令脚本(例如Shell)

优势:显示描述依赖关系,可持久复用。

缺点:编写脚本的灵活度较高的同时,难度也较高,没有特定的语法模式,新手不容易理解,并且在跨平台上存在一定的困难。

构建系统

构建系统(Build System)是一套自动化工具和规则的集合,用于将源代码转换为可执行程序、库文件或其他交付物。其核心任务包括:

  • 编译:将源代码(如C++、Java)转换为目标代码(如.o、.class)
  • 链接:将目标代码与依赖库合并为最终可执行文件或动态库
  • 依赖管理:自动处理文件间的依赖关系(如头文件、库文件)
  • 任务编排:按顺序执行编译、测试、打包等任务

为什么需要构建系统?

  • 复杂性管理:现代项目可能包含成千的源文件和依赖项
  • 跨平台支持:不同操作系统(Windows/Linux/macOS)的构建流程差异
  • 团队协作:确保所有开发者使用一致的构建环境
  • 效率提升:增量编译、并行构建加速开发流程

构建系统的特点

  • 依赖解析(Dependency Resolution)
    • 隐式依赖:编译器自动追踪头文件包含关系
    • 显式依赖:开发者手动声明任务间的依赖(如Makefile规则)
  • 任务编排:构建工具将任务组织为有向无环图(DAG),确保执行顺序正确,避免陷入依赖循环
  • 跨平台构建
  • 缓存与增量构建

现代工具

现代的项目工具不仅仅是单纯的着眼于如何构建,还糅合了很多其他的任务

  • 包管理工具:聚焦于依赖的获取与版本控制
    • 核心问题:“项目需要哪些第三方库?如何确保版本一致?”
  • 构建工具:聚焦于代码到产物的转换流程
    • 核心问题:“如何将源代码编译、链接、测试并打包为最终交付物?”

例如Java开发中的mavengradle,它既是包管理工具,又是构建工具

image.png

image.png
xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">  <modelVersion>4.0.0</modelVersion>  <parent>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-parent</artifactId>    <version>3.3.6</version>    <relativePath/> <!-- lookup parent from repository -->  </parent>  <version>1.1.1</version>  <url/>  <licenses>    <license/>  </licenses>  <developers>    <developer/>  </developers>  <scm>    <connection/>    <developerConnection/>    <tag/>    <url/>  </scm>  <properties>    <java.version>17</java.version>    <spring-shell.version>3.3.3</spring-shell.version>  </properties>  <dependencies>    <dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-web</artifactId>    </dependency>
    <dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-aop</artifactId>    </dependency>
    <dependency>      <groupId>org.projectlombok</groupId>      <artifactId>lombok</artifactId>      <optional>true</optional>    </dependency>    <dependency>      <groupId>org.graalvm.js</groupId>      <artifactId>js</artifactId>      <version>23.0.4</version>    </dependency>    <dependency>      <groupId>org.graalvm.sdk</groupId>      <artifactId>graal-sdk</artifactId>      <version>23.0.4</version>    </dependency>    <dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-test</artifactId>      <scope>test</scope>    </dependency>    <dependency>      <groupId>com.google.code.gson</groupId>      <artifactId>gson</artifactId>    </dependency>    <dependency>      <groupId>com.baomidou</groupId>      <artifactId>mybatis-plus-spring-boot3-starter</artifactId>      <version>3.5.7</version>    </dependency>    <dependency>      <groupId>com.mysql</groupId>      <artifactId>mysql-connector-j</artifactId>      <version>8.0.33</version>    </dependency>    <dependency>      <groupId>io.minio</groupId>      <artifactId>minio</artifactId>      <version>8.5.11</version>    </dependency>  </dependencies>  <dependencyManagement>    <dependencies>      <dependency>        <groupId>org.springframework.shell</groupId>        <artifactId>spring-shell-dependencies</artifactId>                <version>${spring-shell.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>        </dependencies>    </dependencyManagement>
    <build>        <plugins>            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-compiler-plugin</artifactId>                <configuration>                    <annotationProcessorPaths>                        <path>                            <groupId>org.projectlombok</groupId>                            <artifactId>lombok</artifactId>                            <version>1.18.30</version>                        </path>                    </annotationProcessorPaths>                </configuration>            </plugin>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>                <configuration>                    <excludes>                        <exclude>                            <groupId>org.projectlombok</groupId>                            <artifactId>lombok</artifactId>                        </exclude>                    </excludes>                </configuration>            </plugin>        </plugins>    </build>
</project>

makefileninja就属于比较纯粹的构建工具

makefile
OBJS = main.o utils.oprogram: $(OBJS)    $(CC) $^ -o $@

Pythonpip,就是比较纯粹的包管理

构建工具预览

现在我们的以C/C++项目为例,来介绍三种经典的构建工具

  • Make
    • Make并没有编译链接的功能,它只是通过批处理的方式来 调用 人类在makefile中编写的指令,达到自动编译和链接。
    • Make的规则很简单,你规定要构建哪个文件、它依赖哪些源文件,当那些文件有变动时,如何重新构建它。
    • 各个平台中的规范,标准,基础命令等并不统一,所以有的平台会有自己的makefile。这样就会导致无法实现跨平台。于是出现了 CMake等其他跨平台的构建工具
  • Ninja
    • Ninja 舍弃了各种高级功能,所以虽然总体功能不如make强大,但是语法和用法非常简单
    • 启动编译的速度非常快。根据实际测试:在超过30,000个源文件的情况下,也能够在1秒钟内开始进行真正的构建
      • 有测试表明,在超过30,000个源文件的情况下,make的构建时间一般在10s-20s
    • Ninja 本身是跨平台的,但是可搭配CMake和ninja构建项目
  • CMake
    • CMake制订了一个统一的标准,编写后能够读取CMakelist.txt根据目标平台生成相适应的生成器规则(例如makefile, ninja),从而迈出跨平台的第一步。再根据对应平台的生成器输入构建命令,来构建项目,最终实现跨平台。

本节课要求了解Make、Ninja,并能通过它们进行基础性的项目构建

对CMake则会更加深入,掌握比较现代化的构建系统的使用

三种构建工具的关系预览

image.png

Make

构建规则文件

默认为当前工作目录下的makefile

或者通过make -f <file_name>指定的 .mk后缀文件

基础知识

makefile文件结构

makefile
# 定义变量 // 使用变量时$(variable)CC = gccCFLAGS = -Wall -g
# 默认目标 (仅输入make并运行时,如果有all,则生成all中所有的目标)all: hello
# 规则:生成 hello 可执行文件hello: hello.o    $(CC) -o hello hello.o
# 规则:从 hello.c 编译 hello.o 对象文件hello.o: hello.c    $(CC) -c $(CFLAGS) hello.c//讲解: 规则的第一行冒号后是依赖文件的名字,第二行是要执行的指令
# 伪目标:清理生成的文件 (如果当前目录存在与伪目标同名文件会失效,需要phony声名才能正常使用)clean:    rm -f hello hello.o
# 模式规则:编译所有 .c 文件为 .o 文件%.o: %.c    $(CC) -c $(CFLAGS) $< -o $@
# 包含进另一个 Makefileinclude debug.mk
powershell
make hello
规则
makefile
目标1 ... : 依赖1 ...  命令1  命令2  . . .
  1. 对一个规则,Makefile文件默认只生成第一个目标就结束构建
  2. make会通过文件最后一次修改的时间戳来检查所依赖的文件是否发生变动,之后才重新生成目标文件。
  3. 规则里的命令在执行时会先在终端输出正在执行的命令语句,然后输出命令的执行结果。如果在命令语句前加上@符号,就只会输出命令结果。
目标
伪目标

:::info 如果目录下已存在与目标重名的文件,会导致目标构建失效(make认为这是一个生成目标),可以通过在makefile中加入以下配置来显示声名哪些是伪目标以避免检测。

:::

makefile
.PHONY clean all

常用的伪目标命名

  • all
  • clean

变量

自定义变量
makefile
// 定义
CC = gcc // 展开时赋值CFLAGS := -Wall -g // 立即赋值CC ?= gcc // 未定义时赋值CFLAGS += -I/inc // 添加值到末尾//使用
hello.o: hello.c    $(CC) -c $(CFLAGS) hello.c
特殊变量/自动变量

$^ 表示所有的依赖文件

$@ 表示生成的目标文件

$< 代表第一个依赖文件

$+$^类似,但是不会包含重复项。

这些变量可以用在规则里,用于表示目标和依赖,例如

makefile
print_msg = @echo Building $@all: my_target    $(print_msg)
my_target: main.cpp    g++ main.cpp -o $@    # 其他命令来生成 my_target

print_msg展开后,$@就成为了命令语句的一部分,然后$@再展开为具体的目标名字。当然你也可以直接用在命令语句里面。

工具函数

makefile
$(text): 将变量值转换为文本字符串。$(subst old_part, new_part, original_text): 替换文本中的所有匹配的字符串。$(patsubst pat,rep,text): 基于模式替换文本中的字符串。$(replace from,to,text): 替换文本中的第一个匹配的字符串。$(suffix file): 获取文件名的后缀。$(basename file): 获取文件名的基本部分(去除后缀)。$(addsuffix suffix,names): 给列表中的每个名字添加后缀。$(addprefix prefix,names): 给列表中的每个名字添加前缀。$(strip string): 去除字符串两端的空白。$(findstring find,in): 在字符串中查找子串。$(wildcard pattern): 匹配所有符合模式的文件名。
wildcard

使用 $(wildcard) 函数时要注意,它会立即扩展,这意味着在 Makefile 解析时就会生成文件列表。

makefile
# 获取当前目录下所有的 .c 文件C_FILES = $(wildcard *.c)
# 获取某个子目录下所有的 .h 文件HEADERS = $(wildcard include/*.h)
# 获取当前目录以及子目录下所有的 .txt 文件TXT_FILES = $(wildcard *.txt) $(wildcard */*.txt)
文件目录相关
makefile
$(realpath file): 返回文件的经过符号链接解析后的绝对路径。# ※ 辨析:# realpath_example := $(realpath ./relative/path/to/file)# 如果 ./relative/path/to/file 是一个符号链接到 /home/user/real/file# realpath_example 将是 /home/user/real/file
$(abspath file): 返回平常意义上的文件的绝对路径。$(dir names): 获取文件名列表的目录部分。
其他
makefile
$(foreach item, list, body): 对列表中的每个元素执行 body命令 并扩展 item。$(shell command): 执行 shell 命令并返回输出。

模式匹配

在 Makefile 中,通配符 *% 用于文件名模式匹配,它们在规则中非常有用,尤其是在你需要指定一组文件而不是单个文件时。

  1. *(星号):
  • * 匹配任意数量的字符(包括零个字符)。
  • 例如,*.c 会匹配当前目录下所有以 .c 结尾的文件。
  1. %(百分号):
  • % 在模式匹配中用作通配符,它可以匹配任意单个非斜线(/)字符的序列。
  • 例如,%.c 会匹配任何单个字符后面跟着 .c 的文件名。
  • % 特别有用,因为它允许你在替换操作中保留文件名的一部分,例如

$(patsubst %.c,%.o, /src/file.c) 只会将 file.c 转换为 file.o

  1. ?(英文问号):
  • ? 用于匹配文件名中的单个字符。
makefile
# 为当前目录的所有 .c 文件创建 一一对应的 .o 文件的规则%.o: %.c    $(CC) $(CFLAGS) -c $< -o $@
# 清理所有 .o 文件clean:    rm -f *.o
# 匹配当前目录下所有文件名中包含三个字符,且以 o 结尾的 .h 文件HEADER_FILES = $(wildcard *???o.h)
注意事项
makefile
# 以下规则是错误的:# make并不能从这个规则里推断.o和.c的对应关系*.o: *.c    $(CC) -c $< -o $@
# 正确的使用方式%.o: %.c    $(CC) -c $< -o $@
# 或者file1.o file2.o file3.o: file1.c file2.c file3.c    $(CC) -c file1.c -o file1.o    $(CC) -c file2.c -o file2.o    $(CC) -c file3.c -o file3.o

Ninja

构建规则文件

默认./build.ninja

也可指定 .ninja后缀文件

powershell
ninja -f custom.ninja

基础知识

示例

makefile
# 使用变量: $name 或者 ${name}cflag = -g -Wall -Werror
# 规则(RULE):rule RULE_NAME    command = gcc $cflags -c $in -o $out    description = ${out} will be treat as "$out"
# BUILD statement:build TARGET_NAME: RULE_NAME INPUTS
# 别名build ALIAS: phony INPUTS ...
# DEFAULT target statementdefault TARGET1 TARGET2 # 可以指定多个目标(包括伪目标)default TARGET3 # 最后编写的default行才会生效(这里只会生成TARGET3)
powershell
build TARGET1 TARGET2 TARGET3

变量

定义
makefile
var = src/source
使用
makefile
var2 = $var/test # 结果为var2 = src/source/test# 也可以这样写var2 = ${var}/test# 变量之间相互调用时,需要按定义顺序写下来,在定义之前使用变量会为空值

规则 (rule)

规则描述了一组数据的处理方式,可接受两个固定的参数:inout(类似于一个函数的参数变量),其实你可以将规则从表面上理解为一个函数...

makefile
rule 规则名    command = 命令行指令    description = 执行规则时打印在终端的语句    pool = <pool_name>,在池可以指定其 depth(最大并发数)

构建 (build)

makefile
build 输出文件 : 使用的规则 输入

输入文件(依赖文件)的类型及格式:

makefile
rule compile  command = gcc -c $in -o $out
rule check  command = ./check_script.sh $in
build hello.o: compile hello.c | header.h || config.txt |@ check_script.sh
显示依赖

指定为某个规则的依赖,就如示例中的hello.c作为compile规则的显示依赖

隐式依赖

|符号标记隐式依赖的开始

例如header.h, 没有直接输入到编译命令中,但是在hello.c中被引入后,就成为隐式依赖文件。如果 header.h 的时间戳比 hello.o 新,hello.o 也会重新构建。

仅顺序依赖(Order-Only Dependencies)

||标记仅顺序依赖的开始

仅顺序依赖是指那些文件的存在性会影响构建顺序,必须在目标文件生成之前存在,但它们的内容变化不会触发重新构建。

假如 hello.c中有读取config.txt的代码,那么config.txt在构建hello.o前需要准备好,但是config.txt内容变化并不会影响程序的读取功能

校验脚本(选讲)

|@标记校验脚本的开始

校验脚本是一种特殊的依赖关系,用于在构建过程中运行额外的校验命令。这些校验命令可以检查某些条件是否满足,如果不满足,则会导致构建失败。

关于目标(输出文件)

build语句中的输出文件,也可视为目标

例如刚才的 hello.o,你只需要ninja hello.o就可以构建指定的目标

可以定义default:默认的构建目标

makefile
# DEFAULT target statementdefault TARGET1 TARGET2 # 可以指定多个目标(包括伪目标,例如all)default TARGET3 # 最后编写的default行才会生效(这里只会生成TARGET3)

定义后,你只需要输入ninja并运行,就可以使用default目标

构建系统中关于通过来减少非必要的重构的构建机制

别名

例如下面这个例子,all就依赖于两个构建目标,通过phony伪规则,对他们这个组合起了别名

makefile
build obj/main.o: compile src/main.cppbuild obj/utils.o: compile src/utils.cppbuild all: phony obj/main.o obj/utils.o

ninja命令工具

ninja自带了一些有用的工具,通过ninja -t <tool_name>的格式来运行,这里仅介绍一些常用的命令工具

  • **clean**: 清理生成的中间文件
  • commands:列出重新构建制定目标所需的所有命令
  • graph:为指定目标生成 graphviz dot 文件,用于展示构建规则的关系图。
    • 用例: ninja -t graph | dot -Tpng -o graph.png,需要安装 graphviz 环境来使用dot命令
    • 效果图示例:
image.png
  • targets:通过构建关系的有向图,列出最终的一些target
  • browse:在浏览器中浏览依赖关系图。(默认会启动一个基于python的http服务)
    • 不能在Windows平台上运行,且需要python环境

池(选讲)

在 Ninja 构建系统中,pool 用于控制并行任务的资源分配,限制某些规则(rules)的并发执行数量。默认情况下,Ninja 会根据系统的 CPU 核心数并行执行任务,但某些任务(如内存密集型操作或需要独占资源的任务)可能需要通过 pool 限制并发量,避免资源争用资源占用过高导致构建失败。

Ninja 默认使用ninja -j N(N 为并行任务数)控制全局并发量,例如-j 4 表示最多同时执行 4 个任务。

pool 可以覆盖这一全局设置,针对特定规则(rule)限制其并发量。

定义
makefile
pool heavy  depth = 2
使用示例
powershell
# 定义两个池:编译池(4并发)、链接池(1并发)pool compile_pool  depth = 4
pool link_pool  depth = 1
rule compile  command = g++ -c $in -o $out  pool = compile_pool  # 编译任务最多并行 4 个
rule link  command = g++ -o $out $in  pool = link_pool     # 链接任务最多并行 1 个
build main.o: compile main.cppbuild utils.o: compile utils.cppbuild app: link main.o utils.o

CMake

构建规则文件

配置文件名为CMakeLists.txt,放在 项目 以及 子项目 的 根目录

同时也有.cmake文件,但是与CMakeLists.txt不同的是,.cmake文件是用于复用CMake代码,例如函数、宏等,而CMakeLists.txt才是可执行的配置文件

后文会详细介绍其间的区别...

CMakeLists.txt文件基础结构

  1. 指定 CMake 的最低版本要求
  2. 设置项目名与源文件语言
  3. 设置目标文件名及其依赖文件

以下是一个最简单的示例结构

cmake
cmake_minimum_required(VERSION <version>)
project(<project_name> [<language>...])
add_executable(<target> <source_files>...)
示例
cmake
cmake_minimum_required (VERSION 3.5)
project (test)
add_executable(hello hello.c)

基础知识

CMake使用机制

使用CMake完整的构建流程为:

  1. 创建构建目录:保持源代码目录整洁。
  2. 使用 CMake 生成构建文件:配置项目并生成适合平台的构建文件。
  3. 编译和构建:使用生成的构建文件执行编译和构建。
  4. 清理构建文件:通过生成的构建配置文件的命令,删除构建的中间文件和目标文件。
  5. 重新配置和构建:处理项目设置的更改。

其中,构建项目的主要部分可以被概括为两步:

  1. 预构建:根据指定的生成器类型(makefile, Ninja, msvc ...),将CMake配置转化为对应的生成器配置文件
  2. 构建:使用对应的生成器的命令(make, ninja,msbuild ...)生成目标文件
预构建

cmake需要选择generator(生成器)

我们在执行cmake命令时,可以指定:

  • 使用哪个生成器,通过-G <generator>指定
  • 项目位置,默认为当前目录,可通过-S <path>指定
  • 生成器配置文件输出目录,默认为当前目录,可通过-B <path>指定。因为生成的生成器配置文件较多,一般都建议设置一个输出目录
powershell
cmake -B build -G "Unix Makefiles" # 生成对应的makefile配置# 或者cmake -S . -B build -G Ninja # 生成对应的ninja配置
构建
  1. 接着运行cmake --build <dir>,由于例子中-B的值为build,所以直接cmake --build build,CMake就可以自动执行你刚才指定的生成器类型的构建命令
  2. 进入-B指定的目录(例子中dir的值为build),这个目录下就有你的生成器入口配置文件,手动执行ninja或者make即可

变量

cmake
set(a dist) # a = distset(b ${a}/bin) # b = dist/binunset(a) # 删除变量a
set(list_a 1;2;3;4;5) # 创建列表变量set(list_b 1 2 3 4 5) # 创建列表变量的第二种方式, list_a 和list_b是相等的
预定义变量
  • CMAKE_SOURCE_DIR 项目根目录(顶层 CMakeLists.txt 所在路径)
  • CMAKE_BINARY_DIR 构建目录(执行 cmake 命令时的工作路径)
  • CMAKE_CURRENT_SOURCE_DIR 当前处理的 CMakeLists.txt 所在目录
  • PROJECT_SOURCE_DIR 当前项目的源代码目录,即 -S指定的目录
  • CMAKE_CXX_COMPILER 指定 C++ 编译器(如 g++/clang++)
变量操作

覆盖、追加、移除、※ 【 获取长度、索引访问、元素拼接、元素查找、查找、翻转、排序....

这里只介绍一些常用的重要内容

cmake
set(VAR 1 2 3 4)set(VAR_2 a;b;c;d)# 使用 set 进行覆盖set(VAR ${VAR_2})# set 也可以完成追加操作set(VAR ${VAR} ${VAR_2})# 使用list 进行一系列操作,其中就有追加list(APPEND VAR ${VAR_2})# 删除指定元素操作list(REMOVE_ITEM <list> <value> [<value> ...])例如:移除VAR的main.cpplist(REMOVE_ITEM VAR ${PROJECT_SOURCE_DIR}/src/main.cpp)
file

file是一个比较实用的函数,可以执行诸多文件和目录操作,包括对文件和目录的CRUD,还有对目录下的文件进行通配符匹配获取,以及路径计算和转化,远程文件下载等...

这里比较常用的就是通配符获取文件了,其他操作:

通配符匹配文件
cmake
file(GLOB <variable> # 表明使用通配符模式,并将匹配文件存储到变量[LIST_DIRECTORIES true|false] # 列出(可选)[RELATIVE <path>] # 指定匹配路径(可选)[<globbing-expressions>...]) # 匹配表达式
cmake
file(GLOB cpp_sources "src/*.cpp")

日志消息

在构建过程中输出一些信息

cmake
message([message_type] "message to display" ...)

关于message_type的取值:

  • (无) :重要消息
  • STATUS :非重要消息
  • WARNING:警告, 会继续执行
  • AUTHOR_WARNING:警告, 会继续执行
  • SEND_ERROR:错误, 继续执行,但是会跳过生成的步骤
  • FATAL_ERROR:致命错误, 终止所有处理过程

流程控制

分支语句
cmake
if(WIN32)  message("Windows")elseif(APPLE)  message("macOS")else()  message("Linux")endif()
常用条件表达式
  • DEFINED var_name(变量是否存在)
  • a STREQUAL b(字符串比较)
  • EXISTS path(文件/目录是否存在)
  • IS_DIRECTORY path 是否是目录/文件夹
常用运算符

逻辑符

  • AND
  • OR
  • NOT

数值比较

  • LESS
  • GREATER
  • EQUAL
  • LESS_EQUAL
  • GREATER_EQUAL

字符串比较

  • STRLESS
  • STRGREATER
  • STREQUAL
  • STRLESS_EQUAL
  • STRGREATER_EQUAL
对于表达式的值判定

如果是1, ON, YES, TRUE, Y, 非零值,非空字符串时,条件判断返回True

如果是 0, OFF, NO, FALSE, N, IGNORE, NOTFOUND,空字符串时,条件判断返回False

循环遍历
cmake
foreach(item IN ITEMS apple banana orange)  message("Fruit: ${item}")endforeach()
foreach(i RANGE 1 5)  message("Count: ${i}")  # 输出 1,2,3,4,5endforeach()
set(counter 3)while(counter GREATER 0)  message("Counter: ${counter}")  math(EXPR counter "${counter} - 1")endwhile()

函数 & 宏

函数和宏都可以用于定义一段可复用的代码,不过使用时稍有区别

  1. 作用域
  • Function: 有独立的作用域。函数内部定义的变量不会影响外部,且外部变量在函数内不可见,除非使用 PARENT_SCOPE 显式传递。
cmake
function(my_function arg)    set(${arg}_internal "Hello")    set(${arg}_internal "Hello" PARENT_SCOPE)endfunction()
set(my_var "World")my_function(my_var)message(${my_var_internal})  # 输出: Hello
  • Macro: 没有独立作用域。宏内部的变量会直接影响外部,且外部变量在宏内可见。
  1. 参数传递
  • Function: 参数按值传递,函数内对参数的修改不会影响外部。
  • Macro: 参数按文本替换传递,宏内对参数的修改会影响外部。
  1. 返回值
  • Function: 通过 setPARENT_SCOPE 返回值。
  • Macro: 没有返回值机制,直接通过修改外部变量传递结果。
  1. 性能
  • Function: 每次调用都会创建新作用域,可能稍慢。
  • Macro: 通过文本替换实现,通常更快。
函数(Function)
cmake
function(print_sum a b)  math(EXPR sum "${a} + ${b}")  message("Sum: ${sum}")endfunction()
print_sum(3 5)  # 输出 Sum: 8
宏(Macro)
cmake
macro(print_sum_macro a b)  math(EXPR sum "${a} + ${b}")  message("Sum: ${sum}")endmacro()
print_sum_macro(3 5)  # 输出 Sum: 8

文件生成与链接

add_executable

add_executable 是CMake构建可执行程序的核心指令,用于定义项目的最终可执行目标。

基本语法
cmake
add_executable(<target_name> # 目标  <source1 source2 ...> # 依赖源文件)
cmake
add_executable(hello hello_world.cpp)
cmake
add_executable(platform_app)if(WIN32)    target_sources(platform_app PRIVATE        win32_main.cpp        platform/win32_impl.cpp    )else()    target_sources(platform_app PRIVATE        unix_main.cpp        platform/posix_impl.cpp    )endif()
cmake
set(APP_SOURCES    src/main.cpp    src/utils.cpp    include/utils.h)add_executable(myapp ${APP_SOURCES})
set_target_properties

设置和修改目标的属性,如编译选项、链接选项等。

cmake
add_executable(app    main.cpp)
set_target_properties(app PROPERTIES    COMPILE_OPTIONS "-Wall" # 设置编译选项    OUTPUT_NAME "main"  # 控制输出文件名    ... # 其他属性)
add_library
cmake
add_library(<target>  [STATIC | SHARED | MODULE]  [EXCLUDE_FROM_ALL] # 不作为默认构建的目标  <source1 source2 ...> # 依赖源文件)
aux_source_directory

找到指定目录下的所有符合源文件形式的文件(非递归搜索),并储存为列表变量

cmake
aux_source_directory(source_dir var_name)
include_directories

将这个目录作为所有目标的include目录

target_include_directories

对特定目标设置include目录

直接指定需要链接的库文件或库名称,影响后续所有目标的链接阶段。

cmake
link_libraries(library1 library2 ...)

直接指定需要链接的库文件或库名称,但是只影响后续指定目标的链接阶段。

cmake
target_link_libraries(<target> ... <item>... ...)target_link_libraries(<target>                      <PRIVATE|PUBLIC|INTERFACE> <item>...                     [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
  • PUBLIC 在public后面的库会被Link到你的target中,并且里面的符号也会被导出,提供给第三方使用。
  • PRIVATE 在private后面的库仅被link到你的target中,并且隐藏掉其中的符号,第三方不能感知你调了啥库
  • INTERFACE 在interface后面引入的库不会被链接到你的target中,只会导出符号

指定链接器查找库文件的目录路径。

cmake
link_directories(directory1 directory2 ...)
  • 参数:一个或多个目录路径,可以是绝对路径或相对路径(相对路径相对于当前 CMakeLists.txt)。
  • 影响范围:全局生效,对所有后续定义的 target(如通过 add_executableadd_library 生成的目标)生效。

作用类似于link_directories,不过只作用在指定的target

cmake
target_link_directories(taregt dir1 dir2...)

嵌套项目结构

cmake
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])# source_dir:指定了子CMakeLists.txt的目录# binary_dir:指定了输出文件的目录# EXCLUDE_FROM_ALL:在子路径下的目标默认不会被包含到父路径的ALL目标里,# 因此用户必须显式构建在子路径下的目标。

在CMake中,includeadd_subdirectory 是两个常用的命令,用于组织和管理项目代码。它们的功能和使用场景有所不同,下面分别进行解释:

include 命令

include 命令用于包含并执行指定的CMake脚本文件(通常是 .cmake 文件)。它的主要作用是复用CMake代码,比如定义函数、宏、变量等。

语法
cmake
include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>])
  • <file|module>:指定要包含的文件或模块。
  • OPTIONAL:如果文件不存在,也不要报错。
  • RESULT_VARIABLE <var>:将包含操作的结果(成功或失败)存储在变量 <var> 中。
使用场景
  • 复用CMake代码(如函数、宏、变量定义)。
  • 加载预定义的CMake模块(如 FindPackage 模块)。
add_subdirectory 命令

add_subdirectory 命令用于将指定的子目录添加到构建系统中。CMake会进入该子目录并处理其中的 CMakeLists.txt 文件,从而将子目录中的代码纳入当前项目的构建。

语法
cmake
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
  • source_dir:指定包含 CMakeLists.txt 文件的子目录路径。
  • binary_dir:指定子目录的构建输出路径(可选,默认为 source_dir)。
  • EXCLUDE_FROM_ALL:如果指定,子目录中的目标不会包含在默认的构建目标中。
使用场景
  • 将项目拆分为多个子目录,每个子目录有自己的 CMakeLists.txt 文件。
  • 管理模块化或分层的项目结构。
区别与联系
特性includeadd_subdirectory
作用包含并执行CMake脚本文件添加子目录并处理其 CMakeLists.txt
适用场景复用CMake代码(函数、宏、变量等)管理模块化项目结构
文件类型通常用于 .cmake 文件用于包含 CMakeLists.txt 的目录
作用域共享当前作用域子目录有独立的作用域
构建目标不直接创建构建目标可以创建新的构建目标
示例项目结构

假设项目结构如下:

plain
project/├── CMakeLists.txt├── src/│   ├── CMakeLists.txt│   └── main.cpp├── cmake/│   └── MyFunctions.cmake
  • project/CMakeLists.txt 中:
cmake
cmake_minimum_required(VERSION 3.10)project(MyProject)
include(cmake/MyFunctions.cmake)  # 包含自定义函数add_subdirectory(src)             # 添加 src 子目录
  • project/src/CMakeLists.txt 中:
cmake
add_executable(MyApp main.cpp)

通过这种方式,include 用于复用CMake代码,而 add_subdirectory 用于管理子目录的构建。

课后实操(选做)

Tips

构建系统的实际内容非常丰富,但是限于篇幅,本次授课仅仅是带大家以C/C++语言为例,入门了构建系统。想要更进一步

不要让你的项目路径包含中文路径

尽管你有时候感觉运行起来没问题。。。

规则命令

因为构建工具一般是根据规则名去匹配目标文件名的,而可执行文件在windows下带有.exe后缀。所以在windows平台下,对于如果最终生成目标为可执行文件,最好指定规则名为后缀为.exe,而在Linux下则不用。

时间戳的比较

以make为例,正常工作时,只需要比较目标文件和输入文件的时间戳,如果目标文件比输入文件新,则可以断定输入文件在目标文件首次构建后没发生变化。

Ninja会在构建完成后生成一个记录(.ninja_log),记录任务的输入/输出文件的时间戳,用于下次构建时快速比较。