前言
在 第一个 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
| #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
#include <stdio.h> #include "operate.h"
int add(int a, int b) { return a + b; }
#include <stdio.h> #include "operate.h"
int subtraction(int a, int b) { return a - b; }
#include <stdio.h> #include "operate.h"
int multiply(int a, int b) { return a * b; }
#include <stdio.h> #include "operate.h"
int divide(int a, int b) { return a / b; }
#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(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>:通配符表达式(如 *.cpp、src/*.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
| file(GLOB SOURCES "*.cpp")
file(GLOB_RECURSE ALL_SOURCES "src/*.cpp" "src/*.h" "include/*.h" )
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
| 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 )
target_compile_features(myapp PRIVATE cxx_std_17)
|
app/main.cpp:
1 2 3 4 5 6
| #include "json.h"
int main() { return 0; }
|
test/CMakeLists.txt:
1 2 3 4 5
| add_executable(mytest main.cpp)
target_compile_features(mytest PRIVATE cxx_std_17)
|
使用场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| add_subdirectory(src) add_subdirectory(test)
add_subdirectory(test ${CMAKE_BINARY_DIR}/test/build)
add_subdirectory(test EXCLUDE_FROM_ALL)
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
| set(PARENT_VAR "parent_value") add_subdirectory(subdir) message("父目录:${PARENT_VAR}")
message("子目录继承:${PARENT_VAR}") set(PARENT_VAR "modified") set(CHILD_VAR "child_value")
message("父目录:${PARENT_VAR}") message("父目录:${CHILD_VAR}")
|
3. 缓存作用域(Cache Scope)
1 2 3 4 5
| set(CACHE_VAR "value" CACHE STRING "Description")
message("缓存变量:${CACHE_VAR}")
|
变量传递方法
方法 1:PARENT_SCOPE(向直接父作用域传递)
1 2 3 4 5 6 7 8 9 10
| set(RESULT "computed_value" 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
| set(GLOBAL_CONFIG "value" CACHE STRING "全局配置")
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}")
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
| option(ENABLE_TEST "Enable test project" ON)
if(ENABLE_TEST) set(TEST_ENABLED TRUE CACHE BOOL "Test is enabled") add_subdirectory(test) endif()
if(TEST_ENABLED) add_executable(mytest main.cpp) target_compile_definitions(mytest PUBLIC TEST_ENABLED) endif()
|
1 2 3 4 5
|
#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
| function(set_value) set(VAR "value" PARENT_SCOPE) message("函数内:${VAR}") endfunction()
function(set_value) set(VAR "value" PARENT_SCOPE) set(VAR "value") message("函数内:${VAR}") endfunction()
set(MY_LIST a b c) add_subdirectory(sub) message("${MY_LIST}")
list(APPEND MY_LIST d)
list(APPEND MY_LIST d) set(MY_LIST ${MY_LIST} PARENT_SCOPE)
set(VAR "normal") set(VAR "cached" CACHE STRING "") message("${VAR}")
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)
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
| set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
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 )
|
参考资源