构建系统(讲义版)
引子
Q:同学们是怎么编译和运行自己的代码的呢?
- A: 诶🤓☝,点“运行”就可以了啊!
- B:俺用的是VSCode咋点击运行按钮一直用不了啊啊啊啊啊啊啊...😭
- A:我不到啊,我用的VS2022 :(🤪
- C:Linux服务器怎么没有开图形化界面啊啊啊啊啊怎么点运行啊🤡
- D:上课学了通过gcc在命令行执行编译命令,但是每次都要重复写命令。🧐
- E:太好了是 自动补全 和 命令历史记录 ,我们有救了... Bro,我的项目结构怎么这么复杂,只靠命令语句完全不够用啊!💀
- F:......
是的,同学们在大学课程中接触到的,通常就是图形化操作和使用编译器命令来构建自己的应用程序
图形化操作的缺陷
如果IDE没有提供一键运行的按钮,或者对应的平台没有提供图形化界面,就难受了
直接使用编译器命令
gcc -o hello hell.c
缺陷
如果你的项目具有复杂结构,如何优雅的构建项目这件事仍然是一件令人抓狂的事情
使用命令脚本(例如Shell)
优势:显示描述依赖关系,可持久复用。
缺点:编写脚本的灵活度较高的同时,难度也较高,没有特定的语法模式,新手不容易理解,并且在跨平台上存在一定的困难。
构建系统
现代的项目工具不仅仅是单纯的着眼于如何构建,还糅合了很多其他的任务
- __
- __
<?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>
而makefile
,ninja
就属于比较纯粹的构建工具
OBJS = main.o utils.o
program: $(OBJS)
$(CC) $^ -o $@
而Python
的pip
,就是比较纯粹的包管理
构建工具预览
现在我们的以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则会更加深入,掌握比较现代化的构建系统的使用
三种构建工具的关系预览
Make
构建规则文件
默认为当前工作目录下的makefile
或者通过make -f <file_name>
指定的 .mk后缀文件
基础知识
makefile文件结构
# 定义变量 // 使用变量时$(variable)
CC = gcc
CFLAGS = -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 $@
# 包含进另一个 Makefile
include debug.mk
make hello
规则
目标1 ... : 依赖1 ...
命令1
命令2
. . .
- 对一个规则,Makefile文件默认只生成第一个目标就结束构建
- make会通过文件最后一次修改的时间戳来检查所依赖的文件是否发生变动,之后才重新生成目标文件。
- 规则里的命令在执行时会先在终端输出正在执行的命令语句,然后输出命令的执行结果。如果在命令语句前加上
@
符号,就只会输出命令结果。
目标
伪目标
INFO
如果目录下已存在与目标重名的文件,会导致目标构建失效(make认为这是一个生成目标),可以通过在makefile中加入以下配置来显示声名哪些是伪目标以避免检测。
.PHONY clean all
常用的伪目标命名
- all
- clean
变量
自定义变量
// 定义
CC = gcc // 展开时赋值
CFLAGS := -Wall -g // 立即赋值
CC ?= gcc // 未定义时赋值
CFLAGS += -I/inc // 添加值到末尾
//使用
hello.o: hello.c
$(CC) -c $(CFLAGS) hello.c
特殊变量/自动变量
$^
表示所有的依赖文件
$@
表示生成的目标文件
$<
代表第一个依赖文件
$+
与$^
类似,但是不会包含重复项。
这些变量可以用在规则里,用于表示目标和依赖,例如
print_msg = @echo Building $@
all: my_target
$(print_msg)
my_target: main.cpp
g++ main.cpp -o $@
# 其他命令来生成 my_target
print_msg展开后,$@
就成为了命令语句的一部分,然后$@
再展开为具体的目标名字。当然你也可以直接用在命令语句里面。
工具函数
$(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 解析时就会生成文件列表。
# 获取当前目录下所有的 .c 文件
C_FILES = $(wildcard *.c)
# 获取某个子目录下所有的 .h 文件
HEADERS = $(wildcard include/*.h)
# 获取当前目录以及子目录下所有的 .txt 文件
TXT_FILES = $(wildcard *.txt) $(wildcard */*.txt)
文件目录相关
$(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): 获取文件名列表的目录部分。
其他
$(foreach item, list, body): 对列表中的每个元素执行 body命令 并扩展 item。
$(shell command): 执行 shell 命令并返回输出。
模式匹配
在 Makefile 中,通配符 *
和 %
用于文件名模式匹配,它们在规则中非常有用,尤其是在你需要指定一组文件而不是单个文件时。
*
(星号):
*
匹配任意数量的字符(包括零个字符)。- 例如,
*.c
会匹配当前目录下所有以.c
结尾的文件。
%
(百分号):
%
在模式匹配中用作通配符,它可以匹配任意单个非斜线(/
)字符的序列。- 例如,
%.c
会匹配任何单个字符后面跟着.c
的文件名。 %
特别有用,因为它允许你在替换操作中保留文件名的一部分,例如
$(patsubst %.c,%.o, /src/file.c)
只会将 file.c
转换为 file.o
。
?
(英文问号):
?
用于匹配文件名中的单个字符。
# 为当前目录的所有 .c 文件创建 一一对应的 .o 文件的规则
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 清理所有 .o 文件
clean:
rm -f *.o
# 匹配当前目录下所有文件名中包含三个字符,且以 o 结尾的 .h 文件
HEADER_FILES = $(wildcard *???o.h)
注意事项
# 以下规则是错误的:
# 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后缀文件
ninja -f custom.ninja
基础知识
示例
# 使用变量: $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 statement
default TARGET1 TARGET2 # 可以指定多个目标(包括伪目标)
default TARGET3 # 最后编写的default行才会生效(这里只会生成TARGET3)
build TARGET1 TARGET2 TARGET3
变量
定义
var = src/source
使用
var2 = $var/test # 结果为var2 = src/source/test
# 也可以这样写
var2 = ${var}/test
# 变量之间相互调用时,需要按定义顺序写下来,在定义之前使用变量会为空值
规则 (rule)
规则描述了一组数据的处理方式,可接受两个固定的参数:in
和 out
(类似于一个函数的参数变量),其实你可以将规则从表面上理解为一个函数...
rule 规则名
command = 命令行指令
description = 执行规则时打印在终端的语句
pool = <pool_name>,在池可以指定其 depth(最大并发数)
构建 (build)
build 输出文件 : 使用的规则 输入
输入文件(依赖文件)的类型及格式:
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
<font style="color:rgb(6, 6, 7);">hello.c</font>
header.h
hello.o
hello.o
仅顺序依赖(Order-Only Dependencies)
用||
标记仅顺序依赖的开始
校验脚本(选讲)
用|@
标记校验脚本的开始
关于目标(输出文件)
build语句中的输出文件,也可视为目标
例如刚才的 hello.o
,你只需要ninja hello.o
就可以构建指定的目标
可以定义default
:默认的构建目标
# DEFAULT target statement
default TARGET1 TARGET2 # 可以指定多个目标(包括伪目标,例如all)
default TARGET3 # 最后编写的default行才会生效(这里只会生成TARGET3)
定义后,你只需要输入ninja
并运行,就可以使用default
目标
构建系统中关于通过时间戳来减少非必要的重构的构建机制
别名
例如下面这个例子,all
就依赖于两个构建目标,通过phony
伪规则,对他们这个组合起了别名
build obj/main.o: compile src/main.cpp
build obj/utils.o: compile src/utils.cpp
build 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
命令 - 效果图示例:
- 用例:
targets
:通过构建关系的有向图,列出最终的一些targetbrowse
:在浏览器中浏览依赖关系图。(默认会启动一个基于python的http服务)- 不能在Windows平台上运行,且需要python环境
池(选讲)
在 Ninja 构建系统中,pool 用于控制并行任务的资源分配,限制某些规则(rules)的并发执行数量。默认情况下,Ninja 会根据系统的 CPU 核心数并行执行任务,但某些任务(如内存密集型操作或需要独占资源的任务)可能需要通过 pool 限制并发量,避免资源争用或资源占用过高导致构建失败。
Ninja 默认使用ninja -j N
(N 为并行任务数)控制全局并发量,例如-j 4
表示最多同时执行 4 个任务。
pool 可以覆盖这一全局设置,针对特定规则(rule)限制其并发量。
定义
pool heavy
depth = 2
使用示例
# 定义两个池:编译池(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.cpp
build utils.o: compile utils.cpp
build app: link main.o utils.o
CMake
构建规则文件
配置文件名为CMakeLists.txt
,放在 项目 以及 子项目 的 根目录
同时也有.cmake
文件,但是与CMakeLists.txt
不同的是,.cmake
文件是用于复用CMake代码,例如函数、宏等,而CMakeLists.txt
才是可执行的配置文件
后文会详细介绍其间的区别...
CMakeLists.txt文件基础结构
- 指定 CMake 的最低版本要求
- 设置项目名与源文件语言
- 设置目标文件名及其依赖文件
以下是一个最简单的示例结构
cmake_minimum_required(VERSION <version>)
project(<project_name> [<language>...])
add_executable(<target> <source_files>...)
示例
cmake_minimum_required (VERSION 3.5)
project (test)
add_executable(hello hello.c)
基础知识
CMake使用机制
使用CMake完整的构建流程为:
其中,构建项目的主要部分可以被概括为两步:
- 预构建:根据指定的生成器类型(makefile, Ninja, msvc ...),将CMake配置转化为对应的生成器配置文件
- **构建:**使用对应的生成器的命令(make, ninja,msbuild ...)生成目标文件
预构建
cmake需要选择generator
(生成器)
我们在执行cmake命令时,可以指定:
- 使用哪个生成器,通过
-G <generator>
指定 - 项目位置,默认为当前目录,可通过
-S <path>
指定 - 生成器配置文件输出目录,默认为当前目录,可通过
-B <path>
指定。因为生成的生成器配置文件较多,一般都建议设置一个输出目录
cmake -B build -G "Unix Makefiles" # 生成对应的makefile配置
# 或者
cmake -S . -B build -G Ninja # 生成对应的ninja配置
构建
- 接着运行
cmake --build <dir>
,由于例子中-B
的值为build,所以直接cmake --build build
,CMake就可以自动执行你刚才指定的生成器类型的构建命令 - 进入
-B
指定的目录(例子中dir的值为build),这个目录下就有你的生成器入口配置文件,手动执行ninja
或者make
即可
变量
set(a dist) # a = dist
set(b ${a}/bin) # b = dist/bin
unset(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++)
变量操作
覆盖、追加、移除、※ 【 获取长度、索引访问、元素拼接、元素查找、查找、翻转、排序....
这里只介绍一些常用的重要内容
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.cpp
list(REMOVE_ITEM VAR ${PROJECT_SOURCE_DIR}/src/main.cpp)
file
file
是一个比较实用的函数,可以执行诸多文件和目录操作,包括对文件和目录的CRUD
,还有对目录下的文件进行通配符匹配
获取,以及路径计算和转化,远程文件下载等...
这里比较常用的就是通配符获取文件了,其他操作:CMake中的file操作
通配符匹配文件
file(GLOB <variable> # 表明使用通配符模式,并将匹配文件存储到变量
[LIST_DIRECTORIES true|false] # 列出(可选)
[RELATIVE <path>] # 指定匹配路径(可选)
[<globbing-expressions>...]) # 匹配表达式
file(GLOB cpp_sources "src/*.cpp")
日志消息
在构建过程中输出一些信息
message([message_type] "message to display" ...)
关于message_type
的取值:
- (无) :重要消息
- STATUS :非重要消息
- WARNING:警告, 会继续执行
- AUTHOR_WARNING:警告, 会继续执行
- SEND_ERROR:错误, 继续执行,但是会跳过生成的步骤
- FATAL_ERROR:致命错误, 终止所有处理过程
流程控制
分支语句
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
循环遍历
foreach(item IN ITEMS apple banana orange)
message("Fruit: ${item}")
endforeach()
foreach(i RANGE 1 5)
message("Count: ${i}") # 输出 1,2,3,4,5
endforeach()
set(counter 3)
while(counter GREATER 0)
message("Counter: ${counter}")
math(EXPR counter "${counter} - 1")
endwhile()
函数 & 宏
函数和宏都可以用于定义一段可复用的代码,不过使用时稍有区别
<font style="color:rgb(64, 64, 64);">PARENT_SCOPE</font>
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
<font style="color:rgb(64, 64, 64);">set</font>
<font style="color:rgb(64, 64, 64);">PARENT_SCOPE</font>
函数(Function)
function(print_sum a b)
math(EXPR sum "${a} + ${b}")
message("Sum: ${sum}")
endfunction()
print_sum(3 5) # 输出 Sum: 8
宏(Macro)
macro(print_sum_macro a b)
math(EXPR sum "${a} + ${b}")
message("Sum: ${sum}")
endmacro()
print_sum_macro(3 5) # 输出 Sum: 8
文件生成与链接
add_executable
<font style="color:rgb(64, 64, 64);">add_executable</font>
add_executable(<target_name> # 目标
<source1 source2 ...> # 依赖源文件
)
使用示例
add_executable(hello hello_world.cpp)
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()
set(APP_SOURCES
src/main.cpp
src/utils.cpp
include/utils.h
)
add_executable(myapp ${APP_SOURCES})
set_target_properties
使用示例
add_executable(app
main.cpp
)
set_target_properties(app PROPERTIES
COMPILE_OPTIONS "-Wall" # 设置编译选项
OUTPUT_NAME "main" # 控制输出文件名
... # 其他属性
)
add_library
基本语法
add_library(<target>
[STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL] # 不作为默认构建的目标
<source1 source2 ...> # 依赖源文件
)
aux_source_directory
找到指定目录下的所有符合源文件形式的文件(非递归搜索),并储存为列表变量
aux_source_directory(source_dir var_name)
include_directories
将这个目录作为所有目标的include目录
target_include_directories
对特定目标设置include目录
link_libraries
link_libraries(library1 library2 ...)
target_link_libraries
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中,只会导出符号。
link_directories
link_directories(directory1 directory2 ...)
<font style="color:rgb(64, 64, 64);">target</font>
<font style="color:rgb(64, 64, 64);">add_executable</font>
<font style="color:rgb(64, 64, 64);">add_library</font>
作用类似于link_directories
,不过只作用在指定的target
上
target_link_directories(taregt dir1 dir2...)
嵌套项目结构
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
# source_dir:指定了子CMakeLists.txt的目录
# binary_dir:指定了输出文件的目录
# EXCLUDE_FROM_ALL:在子路径下的目标默认不会被包含到父路径的ALL目标里,
# 因此用户必须显式构建在子路径下的目标。
在CMake中,include
和 add_subdirectory
是两个常用的命令,用于组织和管理项目代码。它们的功能和使用场景有所不同,下面分别进行解释:
include
命令
include
命令用于包含并执行指定的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
文件,从而将子目录中的代码纳入当前项目的构建。
语法:
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
source_dir
:指定包含CMakeLists.txt
文件的子目录路径。binary_dir
:指定子目录的构建输出路径(可选,默认为source_dir
)。EXCLUDE_FROM_ALL
:如果指定,子目录中的目标不会包含在默认的构建目标中。
使用场景:
- 将项目拆分为多个子目录,每个子目录有自己的
CMakeLists.txt
文件。 - 管理模块化或分层的项目结构。
区别与联系
特性 | include | add_subdirectory |
---|---|---|
作用 | 包含并执行CMake脚本文件 | 添加子目录并处理其 CMakeLists.txt |
适用场景 | 复用CMake代码(函数、宏、变量等) | 管理模块化项目结构 |
文件类型 | 通常用于 .cmake 文件 | 用于包含 CMakeLists.txt 的目录 |
作用域 | 共享当前作用域 | 子目录有独立的作用域 |
构建目标 | 不直接创建构建目标 | 可以创建新的构建目标 |
示例项目结构
假设项目结构如下:
project/
├── CMakeLists.txt
├── src/
│ ├── CMakeLists.txt
│ └── main.cpp
├── cmake/
│ └── MyFunctions.cmake
- 在
project/CMakeLists.txt
中:
cmake_minimum_required(VERSION 3.10)
project(MyProject)
include(cmake/MyFunctions.cmake) # 包含自定义函数
add_subdirectory(src) # 添加 src 子目录
- 在
project/src/CMakeLists.txt
中:
add_executable(MyApp main.cpp)
通过这种方式,include
用于复用CMake代码,而 add_subdirectory
用于管理子目录的构建。
课后实操(选做)
https://github.com/JieAlpha/Building-System-Tests-for-Linux
https://github.com/JieAlpha/Building-System-Tests-for-Windows
Tips
构建系统的实际内容非常丰富,但是限于篇幅,本次授课仅仅是带大家以C/C++语言为例,入门了构建系统。想要更进一步
不要让你的项目路径包含中文路径!
尽管你有时候感觉运行起来没问题。。。
规则命令
因为构建工具一般是根据规则名去匹配目标文件名的,而可执行文件在windows下带有.exe
后缀。所以在windows平台下,对于如果最终生成目标为可执行文件,最好指定规则名为后缀为.exe
,而在Linux下则不用。
时间戳的比较
以make为例,正常工作时,只需要比较目标文件和输入文件的时间戳,如果目标文件比输入文件新,则可以断定输入文件在目标文件首次构建后没发生变化。
Ninja会在构建完成后生成一个记录(.ninja_log),记录任务的输入/输出文件的时间戳,用于下次构建时快速比较。