CMake 进阶教程(一):多文件项目管理

PointY Lv1

前言

第一个 CMake 项目 中,我们构建的CMake项目仅包含单个源文件。然而,在实际开发中,项目通常由多个文件和目录组成,并且常常需要集成第三方库或系统库。那么,如何有效地组织和管理这些文件结构呢?


多文件项目

1
2
3
4
5
6
7
8
9
10
11
project/
├── CMakeLists.txt # 主 CMake 配置
├── include/ # 公共头文件
│ └── operate.h
├── src/ # 源文件
│ ├── add.cpp
│ ├── div.cpp
│ ├── mult.cpp
│ ├── sub.cpp
│ └── main.cpp
└── build/ # 构建目录

SRC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// operate.h
#ifndef OPERATE_H
#define OPERATE_H

int add(int a, int b);
int divide(int a, int b);
int multiply(int a, int b);
int subtraction(int a, int b);

#endif

// add.cpp
#include <stdio.h>
#include "operate.h"

int add(int a, int b) { return a + b; }

// sub.cpp
#include <stdio.h>
#include "operate.h"

int subtraction(int a, int b) { return a - b; }

// mult.cpp
#include <stdio.h>
#include "operate.h"

int multiply(int a, int b) { return a * b; }

// div.cpp
#include <stdio.h>
#include "operate.h"

int divide(int a, int b) { return a / b; }

// main.cpp
#include <stdio.h>
#include "operate.h"

auto main() -> int
{
int a = 10;
int b = 2;

printf("add: %d\n", add(a, b));
printf("subtraction: %d\n", subtraction(a, b));
printf("multiply: %d\n", multiply(a, b));
printf("divide: %d\n", divide(a, b));

return 0;
}

基本 CMakeLists.txt 配置

CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.15)
project(Calculator VERSION 1.0.0 LANGUAGES CXX)

set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/build)

add_executable(Calculator
src/main.cpp
src/add.cpp
src/sub.cpp
src/mult.cpp
src/div.cpp
include/operate.h
)

target_include_directories(Calculator PRIVATE ${PROJECT_SOURCE_DIR}/include)

使用变量组织源文件

当文件较多时,我们还可以使用变量组织源文件使逻辑更清晰,例如:

1
2
3
4
5
6
7
8
9
10
set(SOURCES
src/main.cpp
src/add.cpp
src/sub.cpp
src/mult.cpp
src/div.cpp
include/operate.h
)

add_executable(Calculator ${SOURCES})

set 完整语法:

1
set(<variable> <value>... [PARENT_SCOPE])
  • <variable>:变量名
  • <value>...:一个或多个值,多个值会组成列表
  • PARENT_SCOPE:可选,将变量设置到父作用域

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 设置单个值
set(MY_VAR "hello")

# 设置多个值(列表)
set(MY_LIST item1 item2 item3)

# 设置路径
set(SRC_DIR ${PROJECT_SOURCE_DIR}/src)

# 追加到列表
set(MY_LIST ${MY_LIST} item4)
# 或使用 list 命令
list(APPEND MY_LIST item5)

自动查找源文件

aux_source_directory

手动管理每个源文件会比较麻烦,使用aux_source_directory命令可以自动查找目录下的所有源文件,例如:

1
2
3
4
5
6
7
include_directories(${PROJECT_SOURCE_DIR}/include)

aux_source_directory(src SOURCES)

add_executable(Calculator
${SOURCES}
)

aux_source_directory 完整语法:

1
aux_source_directory(<dir> <variable>)
  • <dir>:要搜索的目录路径
  • <variable>:存储找到的源文件列表的变量名

特点:

  • 自动查找 .c.cpp.cc.cxx 等源文件
  • 不会递归搜索子目录,只搜索指定目录
  • 不包含头文件(.h.hpp 等)
  • 如果在CMakeLists.txt中使用了该命令,CMake将不能生成一个可以感知新的源文件何时被加入的构建系统。这意味着,当新文件被添加到目录中但没有修改CMakeLists.txt文件时,用户必须手动重新运行CMake来生成包含新文件的构建系统

注意aux_source_directory 命令只能查找源文件,且不会递归查找子目录下的源文件,只搜索指定的单个目录。如需递归搜索,需要使用 file(GLOB_RECURSE)

file(GLOB) & file(GLOB_RECURSE)

1
2
file(GLOB_RECURSE APP_SRC_LIST main.cpp *.cpp *.h)
add_executable(Calculator ${APP_SRC_LIST})

file(GLOB)/file(GLOB_RECURSE) 基本语法:

1
2
3
4
5
6
7
8
file(GLOB <variable> [LIST_DIRECTORIES true|false]
[RELATIVE <path>] [CONFIGURE_DEPENDS]
[<globbing-expressions>...])

file(GLOB_RECURSE <variable> [FOLLOW_SYMLINKS]
[LIST_DIRECTORIES true|false] [RELATIVE <path>]
[CONFIGURE_DEPENDS]
[<globbing-expressions>...])
  • <variable>:存储找到的文件列表的变量名
  • GLOB:在指定目录中查找匹配的文件 (不递归)
  • GLOB_RECURSE递归查找整个目录树中匹配的文件
  • LIST_DIRECTORIES:是否包含目录(默认 true)
  • RELATIVE <path>:返回相对于指定路径的相对路径
  • CONFIGURE_DEPENDS:文件变化时自动重新运行 CMake(推荐)
  • <globbing-expressions>:通配符表达式(如 *.cppsrc/*.h

通配符规则:

  • *:匹配任意字符(不包括 /
  • ?:匹配单个字符
  • [abc]:匹配 a、b 或 c
  • **:在 GLOB_RECURSE 中匹配任意层级目录

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 查找当前目录下的所有 .cpp 文件(不递归)
file(GLOB SOURCES "*.cpp")

# 递归查找所有 .cpp 和 .h 文件
file(GLOB_RECURSE ALL_SOURCES
"src/*.cpp"
"src/*.h"
"include/*.h"
)

# 使用 CONFIGURE_DEPENDS 自动检测文件变化
file(GLOB_RECURSE SOURCES
CONFIGURE_DEPENDS
"src/*.cpp"
)

# 获取相对路径
file(GLOB_RECURSE SOURCES
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
"src/*.cpp"
)

# 排除特定目录(需要手动过滤)
file(GLOB_RECURSE ALL_SOURCES "src/*.cpp")
list(FILTER ALL_SOURCES EXCLUDE REGEX ".*test.*")

注意事项:

  • 添加/删除文件后需要手动重新运行 CMake
  • 可能导致构建不一致,需要清理构建缓存
  • IDE 可能无法正确识别新文件

解决方案:

1
2
# 使用 CONFIGURE_DEPENDS(CMake 3.12+)
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")

子项目管理

对于更复杂的项目,我们可以将其拆分成几个子项目,每个子项目负责特定功能,通过add_subdirectory 组织起来。

add_subdirectory

add_subdirectory 命令用于将子目录添加到构建系统中,子目录必须包含自己的 CMakeLists.txt 文件。

基本语法:

1
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])

参数说明:

  • source_dir:子目录的路径(相对或绝对路径)
    • 相对路径:相对于当前源目录(CMAKE_CURRENT_SOURCE_DIR
    • 绝对路径:可以指向源码树外的目录
  • binary_dir:可选,指定子目录的构建输出目录
    • 如果省略,默认使用与 source_dir 相同的相对路径
    • 对于源码树外的目录,必须指定此参数
  • EXCLUDE_FROM_ALL:可选,排除子目录的目标
    • 子目录中的目标不会被包含在父目录的 ALL 目标中
    • 不会在默认构建时自动构建,除非被显式依赖
    • 适用于可选组件、示例代码或测试
  • SYSTEM:可选(CMake 3.25+),将子目录的包含目录标记为系统目录
    • 可以抑制来自子目录头文件的编译器警告

示例项目:

1
2
3
4
5
6
7
8
9
10
11
project/
├── CMakeLists.txt # 根 CMakeLists
├── app/
│ ├── 3rd/
│ | └── nlohmanjson/
│ | └── json.h
│ ├── CMakeLists.txt # 应用 的 CMakeLists
│ └── main.cpp
└── test/
├── CMakeLists.txt # 测试工程 的 CMakeLists
└── main.cpp

根 CMakeLists.txt:

1
2
3
4
5
6
cmake_minimum_required(VERSION 3.15)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

# 添加子项目
add_subdirectory(app)
add_subdirectory(test)

app/CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
# 创建可执行文件
add_executable(myapp main.cpp)

# 设置包含目录(包含第三方库)
target_include_directories(myapp PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/3rd/nlohmanjson
)

# 设置 C++ 标准
target_compile_features(myapp PRIVATE cxx_std_17)

app/main.cpp:

1
2
3
4
5
6
#include "json.h"  // 使用第三方 JSON 库

int main() {
// 使用 nlohmann::json
return 0;
}

test/CMakeLists.txt:

1
2
3
4
5
# 创建测试可执行文件
add_executable(mytest main.cpp)

# 设置 C++ 标准
target_compile_features(mytest PRIVATE cxx_std_17)

使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. 基本用法:添加同级子目录
add_subdirectory(src)
add_subdirectory(test)

# 2. 指定构建目录
add_subdirectory(test ${CMAKE_BINARY_DIR}/test/build)

# 3. 可选组件
add_subdirectory(test EXCLUDE_FROM_ALL)

# 4. 条件添加子目录
option(BUILD_TEST "Build test" ON)
if(BUILD_TEST)
add_subdirectory(test)
endif()

变量作用域与传递

CMake 变量有作用域限制,理解作用域对于管理复杂项目至关重要。

作用域类型

1. 函数作用域(Function Scope)

1
2
3
4
5
6
7
function(my_function)
set(LOCAL_VAR "value") # 仅在函数内可见
message("函数内:${LOCAL_VAR}")
endfunction()

my_function()
message("函数外:${LOCAL_VAR}") # 空值,函数外不可见

2. 目录作用域(Directory Scope)

1
2
3
4
5
6
7
8
9
10
11
12
13
# 父目录 CMakeLists.txt
set(PARENT_VAR "parent_value")
add_subdirectory(subdir) # 添加子目录
message("父目录:${PARENT_VAR}") # 输出 "parent_value"

# subdir/CMakeLists.txt
message("子目录继承:${PARENT_VAR}") # 输出 "parent_value"(继承)
set(PARENT_VAR "modified") # 修改仅在子目录有效
set(CHILD_VAR "child_value") # 子目录变量

# 返回父目录后
message("父目录:${PARENT_VAR}") # 仍然是 "parent_value"(未被修改)
message("父目录:${CHILD_VAR}") # 空值(子目录变量不可见)

3. 缓存作用域(Cache Scope)

1
2
3
4
5
# 缓存变量在整个项目中可见
set(CACHE_VAR "value" CACHE STRING "Description")

# 在任何 CMakeLists.txt 中都可以访问
message("缓存变量:${CACHE_VAR}")

变量传递方法

方法 1:PARENT_SCOPE(向直接父作用域传递)

1
2
3
4
5
6
7
8
9
10
# 子目录 CMakeLists.txt
set(RESULT "computed_value" PARENT_SCOPE) # 传递给 直接父目录

# 注意:PARENT_SCOPE 不会在当前作用域设置变量
set(VAR "value" PARENT_SCOPE)
message("当前作用域:${VAR}") # 空值!

# 如果需要在当前和父作用域都设置,需要调用两次
set(VAR "value") # 当前作用域
set(VAR "value" PARENT_SCOPE) # 父作用域

方法 2:CACHE 变量(全局共享)

1
2
3
4
5
6
7
8
9
# 根 CMakeLists.txt
set(GLOBAL_CONFIG "value" CACHE STRING "全局配置")

# 任何子目录都可以访问和修改
# 子目录 CMakeLists.txt
message("全局配置:${GLOBAL_CONFIG}")

# 强制覆盖缓存变量
set(GLOBAL_CONFIG "new_value" CACHE STRING "全局配置" FORCE)

方法 3:使用属性(推荐用于目标间通信)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 设置全局属性
set_property(GLOBAL PROPERTY MY_GLOBAL_PROP "value")

# 在任何地方读取
get_property(result GLOBAL PROPERTY MY_GLOBAL_PROP)
message("全局属性:${result}")

# 设置mylib目标库属性:版本1.0.0 , API版本 1
set_target_properties(mylib PROPERTIES
VERSION 1.0.0
SOVERSION 1
)

# 读取目标属性
get_target_property(version mylib VERSION)

实用示例

条件配置传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 根 CMakeLists.txt
option(ENABLE_TEST "Enable test project" ON)

if(ENABLE_TEST)
set(TEST_ENABLED TRUE CACHE BOOL "Test is enabled")
add_subdirectory(test)
endif()

# test/CMakeLists.txt
if(TEST_ENABLED)
add_executable(mytest main.cpp)
target_compile_definitions(mytest PUBLIC TEST_ENABLED)
endif()

1
2
3
4
5
// test/main.cpp

#ifdef TEST_ENABLED
std::cout << "Test mode enabled" << std::endl;
#endif

常见陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# ❌ 陷阱 1:PARENT_SCOPE 不影响当前作用域
function(set_value)
set(VAR "value" PARENT_SCOPE)
message("函数内:${VAR}") # 空值!
endfunction()

# ✅ 正确:需要设置两次
function(set_value)
set(VAR "value" PARENT_SCOPE) # 父作用域
set(VAR "value") # 当前作用域
message("函数内:${VAR}") # 输出 "value"
endfunction()

# ❌ 陷阱 2:子目录修改不影响父目录
# 父 CMakeLists.txt
set(MY_LIST a b c)
add_subdirectory(sub)
message("${MY_LIST}") # 仍然是 "a;b;c"

# sub/CMakeLists.txt
list(APPEND MY_LIST d) # 只在子目录有效

# ✅ 正确:使用 PARENT_SCOPE
# sub/CMakeLists.txt
list(APPEND MY_LIST d)
set(MY_LIST ${MY_LIST} PARENT_SCOPE)

# ❌ 陷阱 3:缓存变量的优先级
set(VAR "normal")
set(VAR "cached" CACHE STRING "")
message("${VAR}") # 输出 "normal"(普通变量优先)

# ✅ 使用 FORCE 强制覆盖
set(VAR "cached" CACHE STRING "" FORCE)

💡 实用技巧

子项目组织原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 按依赖顺序添加
add_subdirectory(app) # 应用程序
add_subdirectory(test) # 测试程序

# 可选组件使用 EXCLUDE_FROM_ALL
add_subdirectory(examples EXCLUDE_FROM_ALL)
add_subdirectory(benchmarks EXCLUDE_FROM_ALL)

# 条件构建
option(BUILD_TEST "Build test" ON)
option(BUILD_DOCS "Build documentation" OFF)

if(BUILD_TEST)
enable_testing()
add_subdirectory(test)
endif()

if(BUILD_DOCS)
add_subdirectory(docs)
endif()

输出目录统一管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 设置所有可执行文件输出到 bin 目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

# 设置所有库文件输出到 lib 目录
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)

# 针对不同构建类型设置不同输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/bin/debug)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/bin/release)

# 或者针对单个目标设置
set_target_properties(myapp PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
)

参考资源

  • 标题: CMake 进阶教程(一):多文件项目管理
  • 作者: PointY
  • 创建于 : 2025-12-31 14:51:35
  • 更新于 : 2026-01-09 22:50:16
  • 链接: https://siyuhong.github.io/2025/12/31/cmake-tutorial02/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论