目录
一.file
1.1.示例一
1.2.示例二
1.2.1.GLOB
1.2.2.GLOB_RECURSE
1.3.示例三
1.3.1.GLOB
1.3.2.GLOB_RECURSE
1.4.file(GLOB)的缺点
二.add_library
示例 1:创建一个简单的静态库
示例 2:创建一个简单的共享库(动态库)
示例 3:让 CMake 自动选择库类型(STATIC 或 SHARED)
2.4.生成文件的路径的控制
2.4.1.示例一——ARCHIVE_OUTPUT_DIRECTORY
2.4.2.示例二——LIBRARY_OUTPUT_DIRECTORY
2.4.3.示例三——RUNTIME_OUTPUT_DIRECTORY
三.target_include_directories(GCC 的 -I 标志)
3.1.使用示例
3.2.PRIVATE、PUBLIC、INTERFACE
3.2.1.示例
3.2.2.背后的头文件包含路径传递机制
3.2.2.1.背后的头文件包含路径传递过程
3.2.2.2.INCLUDE_DIRECTORIES和INTERFACE_INCLUDE_DIRECTORIES
3.2.2.3.示例
四.target_link_libraries(GCC 的 -l 标志)
4.1.基本用法示例
4.2.PRIVATE / PUBLIC / INTERFACE
4.2.1.示例
4.3.LINK_LIBRARIES 和 INTERFACE_LINK_LIBRARIES
4.3.1.示例
五.set_target_properties和 get_target_properties
5.1.示例一——基本示例
5.2.示例二:设置输出目录
5.3.示例三:设置 C++ 标准
5.4.示例四:设置库的版本信息
六.add_subdirectory
6.1.简单示例
6.2.CMake内置路径变量
6.2.1.示例1
6.2.2.示例2
七.include
7.1.简单示例
7.2.变量作用域
7.2.1.示例1——子目录的变量的作用域
7.2.2.示例2——根目录的变量的作用域
7.3.CMake内置路径变量
7.3.1.示例1
7.3.2.示例2
一.file
大家可以去官网看看:文件 — CMake 4.1.0 文档
函数作用
file(GLOB)
命令用于在指定目录中搜索匹配特定模式的文件,并将匹配到的文件路径收集到一个变量中。该命令可以选择是否递归搜索子目录,需要显式指定递归选项。
基本语法
file(GLOB <out-var> [<options>...] <globbing-expressions>...)
file(GLOB_RECURSE <out-var> [<options>...] <globbing-expressions>...)
参数详解
参数 | 含义 | 说明 |
---|---|---|
| 非递归匹配 | 仅搜索当前目录下的文件,不包含子目录 |
| 递归匹配 | 递归搜索当前目录及其所有子目录下的文件 |
| 输出变量 | 用于存储匹配到的文件列表的变量名 |
| 目录处理 | 可选参数,设置为 |
| 相对路径 | 可选参数,使结果输出为相对于指定路径的相对路径 |
| 配置依赖 | 可选参数,让CMake在构建时重新运行glob操作(慎用,可能影响性能) |
| 通配符表达式 | 一个或多个匹配模式,如 |
使用示例
基本用法
# 收集当前目录下所有.cpp文件(不递归)
file(GLOB CPP_SOURCES "*.cpp")# 递归收集所有子目录中的.h文件
file(GLOB_RECURSE HEADER_SOURCES "*.h")
高级用法
# 包含目录在结果中
file(GLOB ALL_ITEMS LIST_DIRECTORIES true "*")# 输出相对路径
file(GLOB_RECURSE SOURCES RELATIVE ${CMAKE_SOURCE_DIR} "src/*.cpp")# 使用多个匹配模式
file(GLOB SOURCE_FILES "*.cpp" "*.cc" "*.cxx")# 使用CONFIGURE_DEPENDS(CMake 3.12+)
file(GLOB CPP_SOURCES CONFIGURE_DEPENDS "src/*.cpp")
1.1.示例一
在 CMake 里:
-
file(GLOB ...)
使用的路径,是相对于 当前正在执行的CMakeLists.txt
所在目录(即CMAKE_CURRENT_SOURCE_DIR
)。 -
不是顶层的
CMakeLists.txt
,也不是随便哪个,而是那个写了file(GLOB ...)
的 CMakeLists.txt 文件所在的目录。
话不多说,我们就来看看
📂 目录结构
GlobDemo/
├── CMakeLists.txt # 顶层
├── src/
│ ├── foo.cpp
│ └── bar.cpp
└── app/├── CMakeLists.txt # 子目录└── main.cpp
📄 src/foo.cpp
void foo() {}
📄 src/bar.cpp
void bar() {}
📄 app/main.cpp
int main() { return 0; }
📄 顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(GlobDemo)# 顶层 GLOB
file(GLOB SRC_LISTS "src/*.cpp")
message("顶层 GLOB 找到: ${SRC_LISTS}")# 添加子目录
add_subdirectory(app)
📄 app/CMakeLists.txt
# 子目录 GLOB
file(GLOB APP_LISTS "src/*.cpp")
message("app 子目录 GLOB 找到: ${APP_LISTS}")
我们可以执行下面这个命令来一键构建出这个目录结构和文件内容
mkdir -p GlobDemo/src GlobDemo/app && \
cat > GlobDemo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.10)
project(GlobDemo)# 顶层 GLOB
file(GLOB SRC_LISTS "src/*.cpp")
message("顶层 GLOB 找到: ${SRC_LISTS}")# 添加子目录
add_subdirectory(app)
EOFcat > GlobDemo/src/foo.cpp <<'EOF'
void foo() {}
EOFcat > GlobDemo/src/bar.cpp <<'EOF'
void bar() {}
EOFcat > GlobDemo/app/CMakeLists.txt <<'EOF'
# 子目录 GLOB
file(GLOB APP_LISTS "src/*.cpp")
message("app 子目录 GLOB 找到: ${APP_LISTS}")
EOFcat > GlobDemo/app/main.cpp <<'EOF'
int main() { return 0; }
EOF
接下来我们就能进行构建这个项目了,我们在顶层的CMakeLists.txt所在目录里面执行下面这个命令
cmake -S . -B build
我们看看
✅ 结论
-
顶层的
file(GLOB "src/*.cpp")
找到了GlobDemo/src/
里的文件。 -
子目录的
file(GLOB "src/*.cpp")
默认是相对于app/
目录的,所以它去找GlobDemo/app/src/
,结果为空。
1.2.示例二
mkdir -p project && cd project && touch CMakeLists.txt main.cpp utils.cpp helper.h config.txt && cd .. && tree project
1.2.1.GLOB
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(GlobExample)# 收集所有.cpp文件
file(GLOB GLOB_CPP_SOURCES "*.cpp")
message("GLOB_CPP源文件: ${GLOB_CPP_SOURCES}")# 收集所有.h文件
file(GLOB GLOB_HEADER_FILES "*.h")
message("GLOB_头文件: ${GLOB_HEADER_FILES}")# 收集所有文件
file(GLOB GLOB_ALL_FILES "*")
message("GLOB_所有文件: ${GLOB_ALL_FILES}")
main.cpp
int main()
{return 0;
}
我们运行一下
- GLOB_CPP源文件: /root/cmake/project/main.cpp;/root/cmake/project/utils.cpp
- GLOB_头文件: /root/cmake/project/helper.h
- GLOB_所有文件: /root/cmake/project/CMakeLists.txt;/root/cmake/project/build;/root/cmake/project/config.txt;/root/cmake/project/helper.h;/root/cmake/project/main.cpp;/root/cmake/project/utils.cpp
我们发现GLOB_所有文件怎么包含了一个目录/root/cmake/project/build啊?这个也正常,Linux下一切皆是文件。
至于GLOB_CPP源文件和GLOB_头文件,则是完全符合我们的预期啊。
1.2.2.GLOB_RECURSE
我们把GLOB换成GLOB_RECURSE来看看
cmake_minimum_required(VERSION 3.10)
project(GlobExample)# 递归收集所有目录中的.cpp文件
file(GLOB_RECURSE GLOB_RECURSE_CPP_SOURCES "*.cpp")
message("GLOB_RECURSE_CPP源文件: ${GLOB_RECURSE_CPP_SOURCES}")# 递归收集所有目录中的.h文件
file(GLOB_RECURSE GLOB_RECURSE_HEADER_FILES "*.h")
message("GLOB_RECURSE_头文件: ${GLOB_RECURSE_HEADER_FILES}")# 递归收集所有目录中的所有文件
file(GLOB_RECURSE GLOB_RECURSE_ALL_FILES "*")
message("GLOB_RECURSE_所有文件: ${GLOB_RECURSE_ALL_FILES}")
我们仔细观察运行结果
- GLOB_RECURSE_CPP源文件: /root/cmake/project/build/CMakeFiles/3.28.3/CompilerIdCXX/CMakeCXXCompilerId.cpp;/root/cmake/project/main.cpp;/root/cmake/project/utils.cpp
- GLOB_RECURSE_头文件: /root/cmake/project/helper.h
- GLOB_RECURSE_所有文件: /root/cmake/project/CMakeLists.txt;/root/cmake/project/build/CMakeFiles/3.28.3/CMakeCCompiler.cmake;/root/cmake/project/build/CMakeFiles/3.28.3/CMakeCXXCompiler.cmake;/root/cmake/project/build/CMakeFiles/3.28.3/CMakeDetermineCompilerABI_C.bin;/root/cmake/project/build/CMakeFiles/3.28.3/CMakeDetermineCompilerABI_CXX.bin;/root/cmake/project/build/CMakeFiles/3.28.3/CMakeSystem.cmake;/root/cmake/project/build/CMakeFiles/3.28.3/CompilerIdC/CMakeCCompilerId.c;/root/cmake/project/build/CMakeFiles/3.28.3/CompilerIdC/a.out;/root/cmake/project/build/CMakeFiles/3.28.3/CompilerIdCXX/CMakeCXXCompilerId.cpp;/root/cmake/project/build/CMakeFiles/3.28.3/CompilerIdCXX/a.out;/root/cmake/project/build/CMakeFiles/CMakeConfigureLog.yaml;/root/cmake/project/config.txt;/root/cmake/project/helper.h;/root/cmake/project/main.cpp;/root/cmake/project/utils.cpp
我们发现这个GLOB_RECURSE方式搜索的还遍历了build目录里面的所有文件。太恐怖了!!!!
GLOB_RECURSE
会递归搜索当前目录及其所有子目录,包括您所在的构建目录 (/root/cmake/project/build
)。这意味着它不仅搜索您的项目源文件,还搜索了 CMake 生成的所有构建文件。
有耐心的可以去看看。
那我们怎么处理这个情况呢?其实很简单啊,只需要添加下面这一句即可
# 先收集所有文件
file(GLOB_RECURSE ALL_FILES "*")
# 然后过滤掉构建目录中的文件
list(FILTER ALL_FILES EXCLUDE REGEX "/build/")
message("过滤后的文件: ${ALL_FILES}")
当然还是有一点缺陷的。我们看看下面这个
list(FILTER ALL_FILES EXCLUDE REGEX "/build/")
这行代码的含义是:从一个名为 ALL_FILES
的列表(变量)中,排除所有包含 "/build/" 这个路径的元素。
让我们把它拆解开来理解:
-
list(...)
-
这是 CMake 的列表操作命令,用于对列表形式的变量进行各种操作,如排序、查找、插入、移除等。
-
-
FILTER
-
这是
list
命令的一个子操作,用于根据特定条件过滤列表中的元素。
-
-
ALL_FILES
-
这是要被操作的目标列表变量。在这个例子中,它是通过
file(GLOB_RECURSE ALL_FILES "*")
获得的,包含了递归找到的所有文件的路径。
-
-
EXCLUDE
-
这是与
FILTER
搭配使用的模式,意思是“排除”。它表示移除所有符合后面条件的元素。 -
与之相对的是
INCLUDE
,意思是“保留”所有符合条件的元素,排除其他所有。
-
-
REGEX "/build/"
-
REGEX
表示后面跟着的是一个正则表达式作为过滤条件。 -
"/build/"
就是正则表达式本身。它的作用是匹配任何包含 "/build/" 这个字符串的路径。
-
任何包含 "/build/" 这个字符串的路径这个就有点牵强,有的也不包含啊。而且我们还很难想到别的过滤策略。
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(GlobExample)# 递归收集所有目录中的.cpp文件
file(GLOB_RECURSE GLOB_RECURSE_CPP_SOURCES "*.cpp")
list(FILTER GLOB_RECURSE_CPP_SOURCES EXCLUDE REGEX "${CMAKE_CURRENT_SOURCE_DIR}/build/")
message("GLOB_RECURSE_CPP源文件: ${GLOB_RECURSE_CPP_SOURCES}")# 递归收集所有目录中的.h文件
file(GLOB_RECURSE GLOB_RECURSE_HEADER_FILES "*.h")
list(FILTER GLOB_RECURSE_HEADER_FILES EXCLUDE REGEX "${CMAKE_CURRENT_SOURCE_DIR}/build/")
message("GLOB_RECURSE_头文件: ${GLOB_RECURSE_HEADER_FILES}")# 递归收集所有目录中的所有文件
file(GLOB_RECURSE GLOB_RECURSE_ALL_FILES "*")
list(FILTER GLOB_RECURSE_ALL_FILES EXCLUDE REGEX "${CMAKE_CURRENT_SOURCE_DIR}/build/")
message("GLOB_RECURSE_所有文件: ${GLOB_RECURSE_ALL_FILES}")
- GLOB_RECURSE_CPP源文件: /root/cmake/project/main.cpp;/root/cmake/project/utils.cpp
- GLOB_RECURSE_头文件: /root/cmake/project/helper.h
- GLOB_RECURSE_所有文件: /root/cmake/project/CMakeLists.txt;/root/cmake/project/config.txt;/root/cmake/project/helper.h;/root/cmake/project/main.cpp;/root/cmake/project/utils.cpp
我们仔细观察一番,好像都有了啊,完全排除了build目录。
1.3.示例三
mkdir -p project && cd project && \
touch CMakeLists.txt main.cpp && \
mkdir -p utils && touch utils/math.cpp utils/math.h && \
mkdir -p core && touch core/core.cpp core/core.h && \
mkdir -p core/internal && touch core/internal/details.cpp && \
mkdir -p config && touch config/settings.ini && \
cd .. && tree project
1.3.1.GLOB
CMakeLists.txt 内容
事实上我们可以像下面这样子写
cmake_minimum_required(VERSION 3.12)
project(GlobExample)# 1. 收集根目录下的.cpp文件
file(GLOB ROOT_CPP_SOURCES "*.cpp")
message("根目录CPP源文件: ${ROOT_CPP_SOURCES}")# 2. 收集utils目录下的.cpp文件
file(GLOB UTILS_CPP_SOURCES "utils/*.cpp")
message("utils目录CPP源文件: ${UTILS_CPP_SOURCES}")# 3. 收集core目录下的.cpp文件
file(GLOB CORE_CPP_SOURCES "core/*.cpp")
message("core目录CPP源文件: ${CORE_CPP_SOURCES}")# 4. 收集core/internal目录下的.cpp文件
file(GLOB CORE_INTERNAL_CPP_SOURCES "core/internal/*.cpp")
message("core/internal目录CPP源文件: ${CORE_INTERNAL_CPP_SOURCES}")# 5. 合并所有.cpp文件
set(ALL_CPP_SOURCES ${ROOT_CPP_SOURCES}${UTILS_CPP_SOURCES}${CORE_CPP_SOURCES}${CORE_INTERNAL_CPP_SOURCES}
)
message("所有CPP源文件: ${ALL_CPP_SOURCES}")# 6. 收集根目录下的.h文件
file(GLOB ROOT_HEADER_FILES "*.h")
message("根目录头文件: ${ROOT_HEADER_FILES}")# 7. 收集utils目录下的.h文件
file(GLOB UTILS_HEADER_FILES "utils/*.h")
message("utils目录头文件: ${UTILS_HEADER_FILES}")# 8. 收集core目录下的.h文件
file(GLOB CORE_HEADER_FILES "core/*.h")
message("core目录头文件: ${CORE_HEADER_FILES}")# 9. 收集core/internal目录下的.h文件
file(GLOB CORE_INTERNAL_HEADER_FILES "core/internal/*.h")
message("core/internal目录头文件: ${CORE_INTERNAL_HEADER_FILES}")# 10. 合并所有.h文件
set(ALL_HEADER_FILES${ROOT_HEADER_FILES}${UTILS_HEADER_FILES}${CORE_HEADER_FILES}${CORE_INTERNAL_HEADER_FILES}
)
message("所有头文件: ${ALL_HEADER_FILES}")
我们看看运行结果
我们可以去看看对比看看
- 根目录CPP源文件: /root/cmake/project/main.cpp
- utils目录CPP源文件: /root/cmake/project/utils/math.cpp
- core目录CPP源文件: /root/cmake/project/core/core.cpp
- core/internal目录CPP源文件: /root/cmake/project/core/internal/details.cpp
- 所有CPP源文件: /root/cmake/project/main.cpp;/root/cmake/project/utils/math.cpp;/root/cmake/project/core/core.cpp;/root/cmake/project/core/internal/details.cpp
- 根目录头文件:
- utils目录头文件: /root/cmake/project/utils/math.h
- core目录头文件: /root/cmake/project/core/core.h
- core/internal目录头文件:
- 所有头文件: /root/cmake/project/utils/math.h;/root/cmake/project/core/core.h
但是上面的太过于冗杂了,我们完全可以像下面这样子写
cmake_minimum_required(VERSION 3.12)
project(GlobExample)# 收集所有.cpp文件(包括子目录)
file(GLOB GLOB_CPP_SOURCES "*.cpp" "utils/*.cpp" "core/*.cpp" "core/internal/*.cpp")
message("GLOB_CPP源文件: ${GLOB_CPP_SOURCES}")# 收集所有.h文件(包括子目录)
file(GLOB GLOB_HEADER_FILES "*.h" "utils/*.h" "core/*.h" "core/internal/*.h")
message("GLOB_头文件: ${GLOB_HEADER_FILES}")
我们可以去对比一下即可知道了
1.3.2.GLOB_RECURSE
- GLOB_CPP源文件: /root/cmake/project/core/core.cpp;/root/cmake/project/core/internal/details.cpp;/root/cmake/project/main.cpp;/root/cmake/project/utils/math.cpp
- GLOB_头文件: /root/cmake/project/core/core.h;/root/cmake/project/utils/math.h
CMakeLists.txt 内容
cmake_minimum_required(VERSION 3.10)
project(RecursiveGlobExample)# 递归收集所有.cpp文件
file(GLOB_RECURSE ALL_SOURCES1 "*.cpp")
list(FILTER ALL_SOURCES1 EXCLUDE REGEX "${CMAKE_CURRENT_SOURCE_DIR}/build/")
message("所有源代码文件: ${ALL_SOURCES1}")# 递归收集所有.h文件
file(GLOB_RECURSE ALL_HEADERS2 "*.h")
list(FILTER ALL_SOURCES2 EXCLUDE REGEX "${CMAKE_CURRENT_SOURCE_DIR}/build/")
message("所有头文件: ${ALL_HEADERS2}")
我们看看
- 所有源代码文件: /root/cmake/project/core/core.cpp;/root/cmake/project/core/internal/details.cpp;/root/cmake/project/main.cpp;/root/cmake/project/utils/math.cpp
- 所有头文件: /root/cmake/project/core/core.h;/root/cmake/project/utils/math.h
自己去对比一下即可
1.4.file(GLOB)的缺点
假设我们有一个简单的项目,初始结构如下:
my_project/
├── CMakeLists.txt
└── src/├── main.cpp└── old_file.cpp
我们的 CMakeLists.txt
使用 GLOB
来收集源文件:
cmake_minimum_required(VERSION 3.10)
project(MyProject)# 注意:这里使用了不推荐的 GLOB
file(GLOB SOURCES "src/*.cpp")add_executable(my_app ${SOURCES})
第一次构建(一切正常)
-
你运行
cmake -B build
来配置项目。 -
CMake 执行
file(GLOB SOURCES "src/*.cpp")
,发现src/main.cpp
和src/old_file.cpp
。 -
变量
SOURCES
的值被设置为src/main.cpp;src/old_file.cpp
。 -
CMake 生成构建系统(如 Makefile),其中包含编译这两个文件的规则。
-
你运行
cmake --build build
,成功生成my_app
可执行文件。
到目前为止,一切都很完美。
问题出现:添加新文件
现在,你在 src/
目录下添加了一个新的源文件 new_file.cpp
。
my_project/
├── CMakeLists.txt
└── src/├── main.cpp├── old_file.cpp└── new_file.cpp <-- 新文件!
关键点来了:你没有重新运行 cmake
,而是直接再次运行构建命令:
cmake --build build
会发生什么?
构建系统(例如 make
)会查看它之前生成的规则(这些规则是基于第一次运行 cmake
时 SOURCES
变量的内容生成的)。它的规则里只知道要编译 main.cpp
和 old_file.cpp
,完全不知道 new_file.cpp
的存在。
因此,构建会继续执行,但它不会编译 new_file.cpp
,也不会将它的目标文件链接到最终的可执行程序 my_app
中。
结果可能是:
-
如果
new_file.cpp
中的代码未被调用:构建可能会成功,但你的新功能根本没有被包含进程序中,导致运行时行为不符合预期。这是一个非常隐蔽的 bug。 -
如果
main.cpp
调用了new_file.cpp
中的函数:链接器会报错:undefined reference to 'some_function_in_new_file()'
。你会感到非常困惑,因为明明看到文件就在那里,为什么说找不到呢?
解决方案:显式列出源文件
CMake 官方推荐的方式是显式地在 add_executable
或 add_library
命令中列出源文件。
让我们重写 CMakeLists.txt
:
cmake_minimum_required(VERSION 3.10)
project(MyProject)# 正确做法:显式列出源文件
add_executable(my_appsrc/main.cppsrc/old_file.cpp
)
现在,当你添加 new_file.cpp
后,你必须修改 CMakeLists.txt
:
add_executable(my_appsrc/main.cppsrc/old_file.cppsrc/new_file.cpp # <-- 手动添加新文件
)
当你再次运行 cmake -B build
时:
CMake 会检测到 CMakeLists.txt
这个文件本身的内容被修改了,于是重新配置并生成新的构建系统。新的构建系统会包含编译和链接 new_file.cpp
的规则。之后再运行 cmake --build build
,一切都会正常工作。
二.add_library
官网:add_library — CMake 4.1.0 Documentation
事实上,我们的CMake支持很多库类型的。它们都可以通过这个add_library来进行创建
add_library(<name> [<type>] [EXCLUDE_FROM_ALL] <sources>...)
添加一个名为 <name>
的库目标,该目标由命令调用中列出的源文件构建而成。
可选的 <type>
参数指定要创建的库类型:
STATIC
静态库:一个目标文件的归档文件,用于链接其他目标时使用。
SHARED
共享库:一个动态库,可被其他目标链接并在运行时加载。
MODULE
模块库:一个插件,不能被其他目标链接,但可以在运行时使用类似 dlopen
的功能进行动态加载。
如果未指定 <type>
,则默认类型为 STATIC
或 SHARED
,具体取决于 BUILD_SHARED_LIBS
变量的值。
可用选项包括:
EXCLUDE_FROM_ALL
自动设置 EXCLUDE_FROM_ALL
目标属性。有关详细信息,请参阅该目标属性的文档。
<name>
对应于逻辑目标名称,在项目中必须具有全局唯一性。实际构建的库文件名基于本地平台的约定(例如 lib<name>.a
或 <name>.lib
)。
示例 1:创建一个简单的静态库
目录结构
project/
├── CMakeLists.txt
├── my_math.cpp
└── my_math.h
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyMathExample)# 创建一个名为 "mymath" 的静态库,源文件是 my_math.cpp
add_library(mymath STATIC my_math.cpp)# 让外部项目在使用时能找到头文件
target_include_directories(mymathPUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
)
my_math.h
#pragma onceint add(int a, int b);
int sub(int a, int b);
my_math.cpp
#include "my_math.h"int add(int a, int b) {return a + b;
}int sub(int a, int b) {return a - b;
}
我们直接执行下面这个即可实现一键构造
mkdir -p project && cd project && \
cat > CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.10)
project(MyMathExample)# 创建一个名为 "mymath" 的静态库,源文件是 my_math.cpp
add_library(mymath STATIC my_math.cpp)# 让外部项目在使用时能找到头文件
target_include_directories(mymathPUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
)
EOFcat > my_math.h <<'EOF'
#pragma onceint add(int a, int b);
int sub(int a, int b);
EOFcat > my_math.cpp <<'EOF'
#include "my_math.h"int add(int a, int b) {return a + b;
}int sub(int a, int b) {return a - b;
}
EOF
接着我们进行构建方法
mkdir build && cd build && \
cmake .. && \
cmake --build .
结果:
-
在
build/
下会生成一个 静态库文件-
Linux/macOS:
libmymath.a
-
Windows:
mymath.lib
-
我们去看看
……
默认的话就是生成在build目录。
示例 2:创建一个简单的共享库(动态库)
创建共享库的语法与静态库几乎完全相同,只需将 STATIC
替换为 SHARED
。
目录结构
project_shared/
├── CMakeLists.txt
├── my_math.cpp
└── my_math.h
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyMathSharedExample)# 创建一个名为 "mymath" 的动态库,源文件是 my_math.cpp
add_library(mymath SHARED my_math.cpp)# 让外部项目在使用时能找到头文件
target_include_directories(mymathPUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
)
my_math.h
#pragma onceint add(int a, int b);
int sub(int a, int b);
my_math.cpp
#include "my_math.h"int add(int a, int b) {return a + b;
}int sub(int a, int b) {return a - b;
}
我们直接一键构造上面这个目录结构
mkdir -p project_shared && cd project_shared && \
cat > CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.10)
project(MyMathSharedExample)# 创建一个名为 "mymath" 的动态库,源文件是 my_math.cpp
add_library(mymath SHARED my_math.cpp)# 让外部项目在使用时能找到头文件
target_include_directories(mymathPUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
)
EOFcat > my_math.h <<'EOF'
#pragma onceint add(int a, int b);
int sub(int a, int b);
EOFcat > my_math.cpp <<'EOF'
#include "my_math.h"int add(int a, int b) {return a + b;
}int sub(int a, int b) {return a - b;
}
EOF
接下来我们就来进行构建
mkdir build && cd build && \
cmake .. && \
cmake --build .
结果
在 build/
目录下会生成一个 动态库文件:
-
Linux/macOS →
libmymath.so
-
Windows →
mymath.dll
+mymath.lib
(导入库)
我们看看
……
示例 3:让 CMake 自动选择库类型(STATIC 或 SHARED)
通过 CMake 的开关 BUILD_SHARED_LIBS
来控制生成 静态库还是动态库。
1. 什么是
BUILD_SHARED_LIBS
-
它是 CMake 的一个布尔变量(取值可以是
ON
或OFF
)。 -
作用:控制
add_library()
在未指定库类型时(没有写STATIC
、SHARED
、OBJECT
、INTERFACE
)的默认行为。
👉 换句话说:
add_library(mymath my_math.cpp)
这里没有写 STATIC
或 SHARED
,那么生成的是静态库还是动态库,就由 BUILD_SHARED_LIBS
来决定:
-
BUILD_SHARED_LIBS=OFF
(默认) → 生成 静态库 (.a
或.lib
) -
BUILD_SHARED_LIBS=ON
→ 生成 动态库 (.so
或.dll
)
这样你只需要维护一套 CMake 配置文件,构建时通过开关就能选择生成哪种库。
2.
option
option(BUILD_SHARED_LIBS "Build shared libraries instead of static ones" OFF)
这是 CMake 提供的定义布尔开关的语法,格式是:
option(<变量名> "描述信息" <默认值>)
-
<变量名>
→ 这里是BUILD_SHARED_LIBS
-
"描述信息"
→ 在ccmake
或cmake-gui
里显示的提示文字,这里是 “Build shared libraries instead of static ones”(构建动态库而不是静态库) -
<默认值>
→OFF
,表示默认关闭,也就是默认生成静态库
3. 使用方式
在构建时可以通过命令行参数修改它:
# 默认情况下(OFF),生成静态库
cmake -B build ..# 显式开启(ON),生成动态库
cmake -B build -DBUILD_SHARED_LIBS=ON ..
✨ 总结一句话:
BUILD_SHARED_LIBS
就是一个 全局开关,决定未指定类型的add_library
生成静态库还是动态库。通过option()
把它定义为一个可配置项,用户就可以在配置时自由切换。
我们直接看例子
目录结构
project_unified/
├── CMakeLists.txt
├── my_math.cpp
└── my_math.h
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyMathUnifiedExample)# 如果没有设置,默认是 OFF(即静态库)
option(BUILD_SHARED_LIBS "Build using shared libraries" OFF)# 创建库,类型由 BUILD_SHARED_LIBS 控制
add_library(mymath my_math.cpp)# 让外部项目在使用时能找到头文件
target_include_directories(mymathPUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
)
my_math.h
#pragma onceint add(int a, int b);
int sub(int a, int b);
my_math.cpp
#include "my_math.h"int add(int a, int b) {return a + b;
}int sub(int a, int b) {return a - b;
}
我们直接一句代码就来进行构建出上面的文件及其里面的内容
mkdir -p project_unified && cd project_unified && \
cat > CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.10)
project(MyMathUnifiedExample)# 如果没有设置,默认是 OFF(即静态库)
option(BUILD_SHARED_LIBS "Build using shared libraries" OFF)# 创建库,类型由 BUILD_SHARED_LIBS 控制
add_library(mymath my_math.cpp)# 让外部项目在使用时能找到头文件
target_include_directories(mymathPUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
)
EOFcat > my_math.h <<'EOF'
#pragma onceint add(int a, int b);
int sub(int a, int b);
EOFcat > my_math.cpp <<'EOF'
#include "my_math.h"int add(int a, int b) {return a + b;
}int sub(int a, int b) {return a - b;
}
EOF
接下来我们就来测试一下
默认构建(静态库
.a
/.lib
)
mkdir build && cd build && \
cmake .. && \
cmake --build .
我们去看看
……
结果:生成了一个静态库
-
Linux/macOS →
libmymath.a
-
Windows →
mymath.lib
生成动态库(
.so
/.dll
)
rm -rf build && \
mkdir build && cd build && \
cmake -DBUILD_SHARED_LIBS=ON .. && \
cmake --build .
我们去看看
……
结果:发现生成了一个动态库
-
Linux/macOS →
libmymath.so
-
Windows →
mymath.dll
+mymath.lib
这样你一个 CMakeLists.txt
就能同时支持静态和动态库,非常常见 👍
2.4.生成文件的路径的控制
默认情况下,CMake会在与源文件目录结构对应的构建目录中生成库文件。(就像在上面,我们并没有控制生成文件的路径,结果它们全部生成在build目录里面了)
例如,如果源代码位于 project/src
目录下,那么生成的库文件将位于 build/project/src
目录中。
为了更好地组织构建输出并集中管理生成的目标文件,CMake提供了几个重要的目标属性来控制输出路径:
-
ARCHIVE_OUTPUT_DIRECTORY:用于控制静态库(扩展名为
.a
或.lib
)的输出目录 -
LIBRARY_OUTPUT_DIRECTORY:用于控制共享库(扩展名为
.so
、.dylib
或.dll
)的输出目录 -
RUNTIME_OUTPUT_DIRECTORY:用于控制可执行文件的输出目录
大家感兴趣可以去官网看看
-
ARCHIVE_OUTPUT_DIRECTORY:ARCHIVE_OUTPUT_DIRECTORY — CMake 4.1.0 Documentation
-
LIBRARY_OUTPUT_DIRECTORY:LIBRARY_OUTPUT_DIRECTORY — CMake 4.1.0 Documentation
-
RUNTIME_OUTPUT_DIRECTORY:RUNTIME_OUTPUT_DIRECTORY — CMake 4.1.0 Documentation
2.4.1.示例一——ARCHIVE_OUTPUT_DIRECTORY
我们在上面的示例1中进行修改一下。
我们在这个示例1里面的CMakeLists.txt添加一句语句来控制一下这个静态库的输出路径。
cmake_minimum_required(VERSION 3.10)
project(MyMathExample_ArchivePath)# 静态库
add_library(mymath STATIC my_math.cpp)target_include_directories(mymathPUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
)# 控制静态库输出路径
set_target_properties(mymath PROPERTIESARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
)
我们来仔细看看这个添加的这个语句啥意思
# 控制静态库输出路径
set_target_properties(mymath PROPERTIESARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
)
1. set_target_properties
这是 CMake 的一个命令,用来给某个目标(target)设置属性。
目标可以是 add_library()
或 add_executable()
定义的库 / 可执行文件。
这里的目标就是 mymath
。
2. PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ...
-
PROPERTIES
→ 关键字,表示要设置后面跟的属性和值。 -
ARCHIVE_OUTPUT_DIRECTORY
→ 属性名,表示 控制静态库的输出目录。-
比如
libmymath.a
(Linux/macOS)或mymath.lib
(Windows)就是“archive”。 -
它只对静态库有效。
-
3. ${CMAKE_BINARY_DIR}/lib
-
${CMAKE_BINARY_DIR}
是一个 CMake 内置变量,表示构建目录的顶层路径(通常就是你cmake -B build
里的build/
)。 -
/lib
→ 拼接一个子目录。
所以 ${CMAKE_BINARY_DIR}/lib
就是 build/lib。
我们来重新构建一下整个项目看看
mkdir build && cd build && \
cmake .. && \
cmake --build .
我们去看看我们静态库的存储地点在哪里?
……
很好啊。
我们可以跟没有设置路径前的情况进行对比看看
2.4.2.示例二——LIBRARY_OUTPUT_DIRECTORY
我们拿到示例二的CMakeLists.txt,然后加上控制动态库输出路径的语句
cmake_minimum_required(VERSION 3.10)
project(MyMathSharedExample)# 创建一个名为 "mymath" 的动态库,源文件是 my_math.cpp
add_library(mymath SHARED my_math.cpp)# 让外部项目在使用时能找到头文件
target_include_directories(mymathPUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
)# 控制动态库输出路径
set_target_properties(mymath PROPERTIESLIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
)
我们来仔细讲解一下这个添加的这句啥意思
# 控制动态库输出路径
set_target_properties(mymath PROPERTIESLIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
)
1. set_target_properties
这是 CMake 的一个命令,用来 设置目标(target)的属性。
目标就是你在 add_library(mymath SHARED ...)
里定义的 mymath 动态库。
2. PROPERTIES LIBRARY_OUTPUT_DIRECTORY ...
-
PROPERTIES
:告诉 CMake“我要设置一些属性”。 -
LIBRARY_OUTPUT_DIRECTORY
:这是具体的属性名。-
它的作用是:控制动态库(共享库)的生成路径。
-
动态库指的是:
-
Linux →
.so
-
macOS →
.dylib
-
Windows →
.dll
(注意.dll
同时也跟运行时相关,有时还会受RUNTIME_OUTPUT_DIRECTORY
影响)。
-
-
3. ${CMAKE_BINARY_DIR}/lib
-
${CMAKE_BINARY_DIR}
:一个 CMake 内置变量,表示你构建时指定的 build 目录的顶层。-
例如你执行:
cmake -B build ..
那么
${CMAKE_BINARY_DIR}
就是<项目路径>/build
。
-
-
/lib
:你手动拼接的一个子目录,表示把输出放到 build 目录下的lib
文件夹里。
所以 ${CMAKE_BINARY_DIR}/lib
→ 实际路径就是 build/lib
。
4. 合起来的效果
这一句的意思就是:
告诉 CMake:把
mymath
动态库的输出文件放到build/lib/
目录中。
我们重新构建一下这个项目
mkdir build && cd build && \
cmake .. && \
cmake --build .
我们去看看这个动态库的路径在哪里
……
我们进行对比一下修改路径前后
2.4.3.示例三——RUNTIME_OUTPUT_DIRECTORY
首先我们先来看看一个不控制输出路径的项目看看
📂 目录结构
project/
├── CMakeLists.txt
└── main.cpp
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(RuntimeOutputExample)# 创建一个可执行文件
add_executable(myapp main.cpp)
main.cpp
#include <iostream>
int main() {std::cout << "Hello from myapp!" << std::endl;return 0;
}
我们直接一句话构建出这个项目来
mkdir -p project && cd project && echo 'cmake_minimum_required(VERSION 3.10)
project(RuntimeOutputExample)add_executable(myapp main.cpp)set_target_properties(myapp PROPERTIESRUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)' > CMakeLists.txt && echo '#include <iostream>
int main() {std::cout << "Hello from myapp!" << std::endl;return 0;
}' > main.cpp
接下来我们就来来构建一下项目。
mkdir build && cd build && \
cmake .. && \
cmake --build .
我们看看
……
我们看到了吧!!这个可执行文件就在build目录里面。
现在我们看看修改了控制输出路径的情况
我们现在修改一下CMakeLists.txt,然后就添加一句控制输出路径的语句。
cmake_minimum_required(VERSION 3.10)
project(RuntimeOutputExample)# 创建一个可执行文件
add_executable(myapp main.cpp)# 控制可执行文件的输出路径
set_target_properties(myapp PROPERTIESRUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
然后我们重新
rm -rf build && \
mkdir build && cd build && \
cmake .. && \
cmake --build .
我们去看看:
我们成功控制了这个可执行文件的生成路径。
三.target_include_directories(GCC 的 -I
标志)
大家可以去官网:target_include_directories — CMake 4.1.0 Documentation
设置⽬标在开发和发布阶段的头⽂件搜索⽬录,可以传递性传播给下游的使⽤⽅。
target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]<INTERFACE|PUBLIC|PRIVATE> [items1...][<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
指定编译给定目标时要使用的包含目录。
指定的 <target>
必须由 add_executable()
或 add_library()
等命令创建,且不能是 ALIAS 目标。
通过显式使用 AFTER
或 BEFORE
,可以独立于默认设置选择是追加还是前置目录。
INTERFACE
、PUBLIC
和 PRIVATE
关键字用于指定后续参数的作用域:
-
PRIVATE
和PUBLIC
项将填充<target>
的INCLUDE_DIRECTORIES
属性。 -
PUBLIC
和INTERFACE
项将填充<target>
的INTERFACE_INCLUDE_DIRECTORIES
属性。
后续参数指定包含目录。
可能大家看了上面那段文字有点懵,没事,我们接着看,看完了下面这些,我们就会有一番新天地。
3.1.使用示例
我们来看看什么时候会用到target_include_direcories?
我们来看个例子即可
📂 目录结构
Example1/
├── CMakeLists.txt
├── main.cpp
└── include/└── hello.h
📄 include/hello.h
#pragma once
#include <iostream>inline void say_hello() {std::cout << "Hello from header!" << std::endl;
}
📄 main.cpp
#include "hello.h"int main() {say_hello();return 0;
}
📄 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(Example1)add_executable(app main.cpp)
上面这个结构,其实我们完全可以一键复制下面的代码就能构建成功
mkdir -p Example/include && \
echo '#pragma once
#include <iostream>inline void say_hello() {std::cout << "Hello from header!" << std::endl;
}' > Example/include/hello.h && \
echo '#include "hello.h"int main() {say_hello();return 0;
}' > Example/main.cpp && \
echo 'cmake_minimum_required(VERSION 3.10)
project(Example)add_executable(app main.cpp)' > Example/CMakeLists.txt
接下来我们来构建一下项目
mkdir build && cd build && \
cmake .. && \
cmake --build .
它说找不到头文件!!这其实是正常的。
我们来仔细看看cmake里面的头文件搜索路径机制
在 Linux 下使用 GCC/Clang 时,CMake 并不会主动为你添加额外的头文件搜索路径,它只是负责生成构建规则并将源文件交给编译器去处理。
因此,真正生效的“默认头文件搜索路径”取决于编译器本身(如 g++)。
以 g++ 为例,其常见的默认搜索路径通常包括:
-
/usr/include
-
/usr/local/include
-
/usr/lib/gcc/x86_64-linux-gnu/<版本号>/include
如果你想查看编译器的完整默认搜索路径,可以使用下面的命令:
echo | g++ -E -x c++ - -v
执行后,编译器会在输出中列出 #include <...> search starts here:
下面的一系列目录,这些就是它实际使用的搜索路径。
-
/usr/include/c++/11:
C++ 标准库头文件目录 -
/usr/include/x86_64-linux-gnu/c++/11:
针对特定架构(x86_64)的 C++ 标准库头文件 -
/usr/include/c++/11/backward:
兼容旧版本的头文件目录(提供一些已废弃的头文件) -
/usr/lib/gcc/x86_64-linux-gnu/11/include:
GCC 自带的内部头文件目录(如xmmintrin.h
等 SIMD intrinsic) -
/usr/local/include:
系统管理员或用户通过make install
安装的库的头文件通常会放在这里 -
/usr/include/x86_64-linux-gnu
针对特定架构的系统头文件 -
/usr/include:
系统默认的公共头文件目录
很明显啊我们这个include目录里面的hello.h都不在这些默认的搜索路径里面,所以就会报错啊!!对此,CMake 提供了几种命令来管理头文件路径:target_include_directories()就是其中之一
我们来看看具体怎么使用,我们只需要将CMakeLists.txt修改成下面这样子即可
📄 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(Example1)add_executable(app main.cpp)# 添加头文件搜索路径(仅对 app 有效)
target_include_directories(app PRIVATE ${CMAKE_SOURCE_DIR}/include)
我们也就是添加了一句,我们来看看这个添加的这句啥意思啊
# 添加头文件搜索路径(仅对 app 有效)
target_include_directories(app PRIVATE ${CMAKE_SOURCE_DIR}/include)
🔍 各部分的含义
-
target_include_directories
-
给某个 target(目标,比如可执行程序或库)指定头文件搜索路径
-
作用就是给编译器加上
-I
选项
-
-
app
-
这是目标的名字
-
对应你上面定义的:
add_executable(app main.cpp)
-
所以这里加的路径只对
app
这个可执行程序生效
-
-
PRIVATE
-
可见性修饰符,控制路径的传播范围:
-
PRIVATE
→ 只在app
自己编译时使用 -
PUBLIC
→ 自己用 + 链接到app
的目标也能继承 -
INTERFACE
→ 自己不用,只有别人链接时能继承
-
这里用
PRIVATE
,意思是“这条 include 路径只给 app 自己用,不传递给其他 target”。 -
-
${CMAKE_SOURCE_DIR}/include
-
这是一个 CMake 变量,展开后是你的项目顶层源代码目录
-
${CMAKE_SOURCE_DIR}
→ 例如/root/cmake/Example
-
拼接后就是
/root/cmake/Example/include
-
这样就告诉编译器:在编译
app
时去Example/include
下找头文件
-
✅ 效果
相当于在编译 app
时,CMake 生成的 g++ 命令会多一个选项:
g++ -I/root/cmake/Example/include -c main.cpp -o main.o
这样 #include "hello.h"
就能找到 Example/include/hello.h
了。
修改完之后,我们来重新运行一下构建过程
rm -rf build && \
mkdir build && cd build && \
cmake .. && \
cmake --build .
运行结果:
我们发现能正常编译了。
3.2.PRIVATE、PUBLIC、INTERFACE
这PRIVATE、PUBLIC 和 INTERFACE这三个关键字定义了包含目录的传播范围,即谁需要这些包含目录。
1. PRIVATE - "我自己用"
含义:这些包含目录只在编译当前目标本身时需要,使用当前目标的其他目标不需要知道这些目录。
使用场景:
-
目标内部使用的私有头文件
-
实现细节相关的头文件
-
不希望暴露给使用者的头文件
示例:
add_library(mylib STATIC mylib.cpp)# internal/ 目录中的头文件只在编译 mylib 时使用
target_include_directories(mylib PRIVATE src/internal)
效果:
-
编译
mylib
时:编译器会添加-Isrc/internal
-
其他目标链接
mylib
时:不会获得src/internal
目录
2. INTERFACE - "给别人用"
含义:这些包含目录当前目标自己不需要,但任何使用当前目标的目标都需要。
使用场景:
-
纯头文件库(Header-only libraries)
-
接口定义文件
-
目标提供的API头文件,但目标实现不需要
示例:
# 创建一个纯头文件库(没有.cpp文件)
add_library(myheaders INTERFACE)# 使用者需要包含 include/ 目录才能使用这个库
target_include_directories(myheaders INTERFACE include)
效果:
-
编译
myheaders
时:不需要任何包含目录(因为没有源文件要编译) -
其他目标链接
myheaders
时:会自动获得include
目录
3. PUBLIC - "我自己用,别人也要用"
含义:这些包含目录既在编译当前目标时需要,也要提供给任何使用当前目标的目标。
使用场景:
-
最常见的用法
-
目标自身的实现需要,使用者也需要调用的头文件
-
库的公共API头文件
示例:
add_library(mylib STATIC mylib.cpp)# mylib.cpp 需要包含 include/mylib.h 来实现功能
# 使用 mylib 的代码也需要包含 include/mylib.h 来调用功能
target_include_directories(mylib PUBLIC include)
效果:
-
编译
mylib
时:编译器会添加-Iinclude
-
其他目标链接
mylib
时:也会自动获得-Iinclude
3.2.1.示例
我们看看
CMakeLists.txt
cmake_minimum_required(VERSION 3.18)project(TestLinkLibraryLANGUAGES CXX
)add_subdirectory(src)add_executable(main main.cpp)target_link_libraries(main PRIVATE add)
main.cpp
int main()
{return 0;
}
src/add.cpp
//空文件, 不真的使用add的逻辑,就是生成一个空的elf格式为库文件
src/CMakeLists.txt
# 创建一个静态库
add_library(add STATIC add.cpp)# 头文件包含路径
# 下面这3个路径都是不存在的
target_include_directories(add PRIVATE "/usr/local/include/private")
target_include_directories(add PUBLIC "/usr/local/include/public")
target_include_directories(add INTERFACE "/usr/local/include/interface")
我们可以执行下面这个命令来一键搭建出这个目录结构和文件
mkdir -p Test/src && \
echo 'cmake_minimum_required(VERSION 3.18)project(TestLinkLibraryLANGUAGES CXX
)add_subdirectory(src)add_executable(main main.cpp)target_link_libraries(main PRIVATE add)' > Test/CMakeLists.txt && \
echo 'int main()
{return 0;
}' > Test/main.cpp && \
echo '//空文件, 不真的使用add的逻辑,就是生成一个空的elf格式为库文件' > Test/src/add.cpp && \
echo '# 创建一个静态库
add_library(add STATIC add.cpp)# 头文件包含路径
# 下面这3个路径都是不存在的
target_include_directories(add PRIVATE "/usr/local/include/private")
target_include_directories(add PUBLIC "/usr/local/include/public")
target_include_directories(add INTERFACE "/usr/local/include/interface")' > Test/src/CMakeLists.txt
接下来我们就来构建我们的项目
mkdir build && cd build && cmake ..
接下来我们执行下面这个命令
cmake --build . -v
这个命令会打印出实际的编译和链接过程。
我们来仔细看看
1. 编译 add.cpp
阶段
你日志里这段:
[ 25%] Building CXX object src/CMakeFiles/add.dir/add.cpp.o
cd /root/cmake/Test/build/src && /usr/bin/c++ -I/usr/local/include/private -I/usr/local/include/public -MD -MT src/CMakeFiles/add.dir/add.cpp.o -MF CMakeFiles/add.dir/add.cpp.o.d -o CMakeFiles/add.dir/add.cpp.o -c /root/cmake/Test/src/add.cpp
👉 关键在第二行:
/usr/bin/c++ -I/usr/local/include/private -I/usr/local/include/public ...
这里的 -I/usr/local/include/private
和 -I/usr/local/include/public
就是头文件搜索路径。
-
private
来自target_include_directories(add PRIVATE ...)
-
public
来自target_include_directories(add PUBLIC ...)
2. 编译 main.cpp
阶段
你日志里后面这段:
[ 75%] Building CXX object CMakeFiles/main.dir/main.cpp.o
/usr/bin/c++ -I/usr/local/include/public -I/usr/local/include/interface -MD -MT CMakeFiles/main.dir/main.cpp.o -MF CMakeFiles/main.dir/main.cpp.o.d -o CMakeFiles/main.dir/main.cpp.o -c /root/cmake/Test/main.cpp
👉 关键在第二行:
/usr/bin/c++ -I/usr/local/include/public -I/usr/local/include/interface ...
这里的 -I/usr/local/include/public
和 -I/usr/local/include/interface
就是头文件搜索路径。
-
public
来自target_include_directories(add PUBLIC ...)
(库和依赖都会有,add
的PUBLIC
会传递给main
) -
interface
来自target_include_directories(add INTERFACE ...)
(只传递给依赖的main,
因为add
的INTERFACE
会传递给main
)
✅ 总结
-
你日志里的
-I...
就是编译器头文件搜索路径 -
看
add.cpp
的编译命令 →private
+public
-
看
main.cpp
的编译命令 →public
+interface
-
PRIVATE
→ 只在库自身用(所以只在编译add.cpp
时出现) -
PUBLIC
→ 自己能用 + 链接到它的 target 也能用(所以add.cpp
和main.cpp
都带有) -
INTERFACE
→ 库自己不用,只有链接到它的 target 能用(所以只在编译main.cpp
时出现)
现在你有没有更懂一点PRIVATE、PUBLIC、INTERFACE?
3.2.2.背后的头文件包含路径传递机制
3.2.2.1.背后的头文件包含路径传递过程
我们来详细解析 target_include_directories
背后的头文件包含路径传递机制,这一机制是现代CMake构建系统的核心功能之一。
第一步:属性设置 - 定义"使用要求"
当您在CMake中使用 target_include_directories
命令时,实际上是在为目标设置使用要求(usage requirements)。
这个机制允许您精确地控制头文件包含路径的可见性和传播范围。
首先我们需要知道一些东西
INCLUDE_DIRECTORIES:
这是一个存储目录列表的目标属性,这些目录是编译当前目标自身源文件时所必需的头文件搜索路径。INTERFACE_INCLUDE_DIRECTORIES:
这也是一个存储目录列表的目标属性,但这些目录是其他目标在编译时所需的头文件搜索路径。它定义了当前目标对外提供的“接口”。
具体来说,不同的关键字会影响不同的目标属性:
-
使用
PRIVATE
关键字指定的路径会被添加到目标的INCLUDE_DIRECTORIES
属性中,这些路径仅用于编译目标自身的源文件。 -
使用
PUBLIC
关键字指定的路径具有双重作用:既会被添加到INCLUDE_DIRECTORIES
属性(用于自身编译),也会被添加到INTERFACE_INCLUDE_DIRECTORIES
属性(用于传递给依赖此目标的其他目标)。 -
使用
INTERFACE
关键字指定的路径只会被添加到INTERFACE_INCLUDE_DIRECTORIES
属性中,这些路径不会用于目标自身的编译,但会传递给依赖此目标的其他目标。
target_include_directories(MyLibPRIVATE /path/to/private/include # → INCLUDE_DIRECTORIESPUBLIC /path/to/public/include # → INCLUDE_DIRECTORIES + INTERFACE_INCLUDE_DIRECTORIESINTERFACE /path/to/interface/include # → INTERFACE_INCLUDE_DIRECTORIES
)
通过这种精细的属性设置,每个CMake目标都携带了关于它需要什么包含目录(给自己使用)和提供什么包含目录(给其他目标使用)的完整元信息,为后续的依赖解析和构建命令生成奠定了坚实基础。
第二步:属性传递 - 依赖关系的自动传播
当您使用 target_link_libraries
建立目标之间的依赖关系时,CMake的"属性传播机制"就会自动启动。这一机制的核心是基于目标之间的依赖关系,自动传递必要的构建信息。
在这个过程中,CMake会智能地分析依赖图:检查当前目标所依赖的所有其他目标,收集这些目标的 INTERFACE_INCLUDE_DIRECTORIES
属性中定义的所有包含目录,然后将这些目录自动添加到当前目标的 INCLUDE_DIRECTORIES
属性中。
这种自动传播机制确保了依赖关系的正确性和完整性。例如,当您的应用程序目标链接到某个库时,不需要手动指定该库的头文件路径——CMake会自动处理这些细节,大大简化了构建系统的配置工作。
比如说
target_link_libraries(main PRIVATE MyLib)
在这个过程中,CMake 会:
-
检查
main
所依赖的所有目标(这里是MyLib
) -
收集这些目标的
INTERFACE_INCLUDE_DIRECTORIES
属性中的所有包含目录 -
将这些目录自动添加到
main
目标的INCLUDE_DIRECTORIES
属性中
这就是为什么 main
能够"拿到" /usr/local/include
这样的头文件搜索路径——不是通过魔法,而是通过明确的属性传递规则。
第三步:生成构建命令 - 从CMake属性到编译器参数
在生成构建系统(如Makefile或Ninja文件)的阶段,CMake会将您在配置阶段设置的所有目标属性转换为具体的编译器命令。这个转换过程是自动且高效的。
对于每个需要编译的目标,CMake会执行以下操作:收集该目标的所有包含目录(来自其 INCLUDE_DIRECTORIES
属性),为每个包含目录生成相应的 -I
编译器标志,最后将这些标志插入到具体的编译命令中。
3.2.2.2.INCLUDE_DIRECTORIES和INTERFACE_INCLUDE_DIRECTORIES
1. INCLUDE_DIRECTORIES
是什么?
这是一个存储目录列表的目标属性,这些目录是编译当前目标自身源文件时所必需的头文件搜索路径。
谁填充它?
-
target_include_directories(<target> **PRIVATE** <dirs>)
-
target_include_directories(<target> **PUBLIC** <dirs>)
(PUBLIC
关键字会同时填充INCLUDE_DIRECTORIES
和INTERFACE_INCLUDE_DIRECTORIES
)
谁使用它?
-
只有当前目标自己使用。
-
当编译
add_library(MathLib math.cpp)
中的math.cpp
时,CMake会使用MathLib
的INCLUDE_DIRECTORIES
属性来生成-I
编译器标志。
举个例子:
add_library(MyLib src.cpp)
target_include_directories(MyLibPRIVATE"/path/to/private_headers" # 只有MyLib自己编译时需要PUBLIC"/path/to/public_headers" # MyLib自己需要,别人也需要
)
执行这个target_include_directories会将/path/to/private_headers和/path/to/public_headers都添加进INCLUDE_DIRECTORIES里面。
同时将/path/to/public_headers添加进我们下面所说的INTERFACE_INCLUDE_DIRECTORIES
编译 MyLib
时,命令会是:g++ -I/path/to/private_headers -I/path/to/public_headers -c src.cpp
编译其他引用MyLib库的程序时,编译命令将会是g++ -I
/path/to/public_headers
2. INTERFACE_INCLUDE_DIRECTORIES
是什么?
这也是一个存储目录列表的目标属性,但这些目录是其他目标在编译时所需的头文件搜索路径。它定义了当前目标对外提供的“接口”。
谁填充它?
-
target_include_directories(<target> **PUBLIC** <dirs>)
-
target_include_directories(<target> **INTERFACE** <dirs>)
谁使用它?
-
任何链接了当前目标的其他目标。
-
当某个目标(如
MyApp
)通过target_link_libraries(MyApp PRIVATE MyLib)
链接MyLib
时,MyLib
的INTERFACE_INCLUDE_DIRECTORIES
中的路径会被自动添加到MyApp
的INCLUDE_DIRECTORIES
中。
举个例子:
# 在 MyLib 的CMakeLists中
target_include_directories(MyLibINTERFACE"/path/to/api_headers" # 我自己不用,但用我的人必须要有
)# 在 MyApp 的CMakeLists中
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib) # 链接MyLib
执行这个target_include_directories会将/path/to/api_headers添加进 INTERFACE_INCLUDE_DIRECTORIES里面。
编译 MyApp
时,命令会是:g++ -I/path/to/api_headers -c main.cpp
。
这个 -I
标志就是从 MyLib
的 INTERFACE_INCLUDE_DIRECTORIES
传递过来的。
3.2.2.3.示例
假设我们有一个简单的项目,包含一个数学库 MathLib
和一个使用该库的可执行程序 Calculator
。
项目结构:
.
├── CMakeLists.txt # 根 CMakeLists
├── calculator.cpp # 主程序源文件
└── mathlib/ # 数学库目录├── CMakeLists.txt├── include/ # 对外公开的头文件│ └── mathutils.h # 库的公共接口└── src/├── mathutils.cpp # 库的实现文件└── internal.h # 库内部使用的私有头文件
第一步:属性设置 - 在 MathLib 中定义使用要求
在 mathlib/CMakeLists.txt
中,我们这样配置:
# 创建静态库目标
add_library(MathLib STATIC src/mathutils.cpp)# 设置头文件包含路径
target_include_directories(MathLibPRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src # 仅自己使用:用于查找 internal.hPUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # 自己和他人使用:用于查找 mathutils.h
)
此时发生的属性变化:
-
MathLib
的INCLUDE_DIRECTORIES
属性包含:-
/absolute/path/to/mathlib/src
(来自 PRIVATE) -
/absolute/path/to/mathlib/include
(来自 PUBLIC)
-
-
MathLib
的INTERFACE_INCLUDE_DIRECTORIES
属性包含:-
/absolute/path/to/mathlib/include
(来自 PUBLIC)
-
第二步:属性传递 - Calculator 获取使用要求
在根 CMakeLists.txt
中,我们建立依赖关系:
# 创建可执行文件目标
add_executable(Calculator calculator.cpp)# 建立链接关系,触发属性传递
target_link_libraries(Calculator PRIVATE MathLib)
传递过程详解:
-
CMake 发现
Calculator
链接了MathLib
-
它检查
MathLib
的INTERFACE_INCLUDE_DIRECTORIES
属性 -
发现该属性包含
/absolute/path/to/mathlib/include
-
将这个路径添加到
Calculator
的INCLUDE_DIRECTORIES
属性中
传递后的结果:
-
Calculator
的INCLUDE_DIRECTORIES
属性现在包含:-
/absolute/path/to/mathlib/include
(从 MathLib 传递而来)
-
重要说明:
-
MathLib
的 PRIVATE 路径 (/src
) 没有被传递给Calculator
,这是正确且符合预期的,因为Calculator
不需要知道库的内部实现细节。 -
如果
mathutils.h
内部又包含了其他头文件(如第三方库),而这些头文件的路径在MathLib
中也是用PUBLIC
或INTERFACE
设置的,那么这些路径也会被传递给Calculator
。
第三步:生成构建命令 - 转换为编译器参数
当执行 cmake --build .
时,CMake 生成具体的编译命令:
编译 MathLib 时:
# 编译 mathutils.cpp
g++ -I/absolute/path/to/mathlib/src -I/absolute/path/to/mathlib/include -c mathutils.cpp -o mathutils.o
这里包含了 所有 的包含路径(PRIVATE + PUBLIC),因为库自身编译时需要能看到所有头文件。
编译 Calculator 时:
# 编译 calculator.cpp
g++ -I/absolute/path/to/mathlib/include -c calculator.cpp -o calculator.o# 链接最终可执行文件
g++ calculator.o -L/path/to/lib -lMathLib -o Calculator
这里只包含了从 MathLib
传递过来 的包含路径(即 PUBLIC 路径),因为可执行文件只需要知道库的公共接口。
总结:
target_include_directories
命令最直接、最表象的作用就是告诉 CMake:“在编译这个目标时,请在这些目录里查找头文件”,而 CMake 最终会将这些目录转换为 GCC 的-I
标志(大写i)。
四.target_link_libraries(GCC 的 -l
标志)
有了上面这个target_include_directories的铺垫,我们讲这个target_link_libraries将会非常的轻松。
我在这里就可以先告诉你们:
target_link_libraries
最终导致 GCC 命令中出现 -l
选项(小写L),但这只是它工作的一部分,而且不是最核心的部分。
在最简单的情况下:
# CMakeLists.txt
target_link_libraries(my_app PRIVATE my_library)
这会生成类似这样的 GCC 命令:
g++ -o my_app main.o -lmy_library
这里,CMake 将库名 my_library
转换为了链接器标志 -lmy_library
。
我们可以去看看官网:target_link_libraries — CMake 4.1.0 Documentation
target_link_libraries
命令用于为二进制目标(即可执行文件或库文件)指定其依赖的库列表。
从实现机制上看,该命令本质上是通过设置目标的两个核心属性来工作的:LINK_LIBRARIES
和 INTERFACE_LINK_LIBRARIES
。
在CMake的底层源代码实现中,这两个属性分别对应着不同的字符串向量容器,用于存储不同类型的依赖关系。
- 当使用
PRIVATE
或PUBLIC
关键字时,依赖库会被添加到LINK_LIBRARIES
属性中; - 而当使用
PUBLIC
或INTERFACE
关键字时,依赖库则会被添加到INTERFACE_LINK_LIBRARIES
属性中。
在构建过程中,CMake会解析这些属性并根据依赖关系自动构建完整的链接库列表。
对于每个被链接的库,CMake会生成相应的链接器指令。
在使用GCC编译器时,这些依赖库最终会以 -l
标志的形式出现在编译命令中。
例如,如果链接了一个名为"mylib"的库,生成的gcc命令中就会包含 -lmylib
这样的参数。
4.1.基本用法示例
示例一——链接pthread库
📂 项目结构
project/
├── CMakeLists.txt
└── main.cpp
📄 main.cpp
#include <iostream>
#include <thread>void worker() {std::cout << "Hello from worker thread!" << std::endl;
}int main() {std::thread t(worker);t.join();std::cout << "Main thread finished." << std::endl;return 0;
}
📄 CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(TestPthread LANGUAGES CXX)# 定义一个可执行程序 main
add_executable(main main.cpp)# 链接 pthread 库
target_link_libraries(main PRIVATE pthread)
其实我们完全可以执行下面这个命令来一键搭建出这个目录结构及其文件
mkdir -p project && \
echo '#include <iostream>
#include <thread>void worker() {std::cout << "Hello from worker thread!" << std::endl;
}int main() {std::thread t(worker);t.join();std::cout << "Main thread finished." << std::endl;return 0;
}' > project/main.cpp && \
echo 'cmake_minimum_required(VERSION 3.18)
project(TestPthread LANGUAGES CXX)add_executable(main main.cpp)
target_link_libraries(main PRIVATE pthread)' > project/CMakeLists.txt
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
接下来我们执行下面这个命令
cmake --build . -v
我们发现,编译main程序的时候它去链接了pthread库。这就是target_link_libraries的作用
示例二——链接add库和pthread库
好的 👍 我来把 pthread 的链接也写进同一个 CMakeLists.txt
,保持最小化示例。
📂 项目结构
project/
├── CMakeLists.txt
├── main.cpp
└── add.cpp
📄 main.cpp
#include <iostream>
#include <thread>int add(int a, int b);void worker() {std::cout << "Worker thread says: 2 + 5 = " << add(2, 5) << std::endl;
}int main() {std::thread t(worker);t.join();return 0;
}
📄 add.cpp
int add(int a, int b) {return a + b;
}
📄 CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(TestLinkLib LANGUAGES CXX)# 定义一个动态库 add
add_library(add SHARED add.cpp)# 定义一个可执行程序 main
add_executable(main main.cpp)# main 需要链接 add 库和 pthread 库
target_link_libraries(main PRIVATE add pthread)
这样:
-
add.cpp
里的函数被编译为libadd.a
-
main.cpp
会链接上libadd.a
和系统库pthread
-
main
就能用std::thread
并运行
其实我们可以执行下面这个命令来一键搭建出这个环境
mkdir -p project && \
echo '#include <iostream>
#include <thread>int add(int a, int b);void worker() {std::cout << "Worker thread says: 2 + 5 = " << add(2, 5) << std::endl;
}int main() {std::thread t(worker);t.join();return 0;
}' > project/main.cpp && \
echo 'int add(int a, int b) {return a + b;
}' > project/add.cpp && \
echo 'cmake_minimum_required(VERSION 3.18)
project(TestLinkLib LANGUAGES CXX)# 定义一个静态库 add
add_library(add SHARED add.cpp)# 定义一个可执行程序 main
add_executable(main main.cpp)# main 需要链接 add 库和 pthread 库
target_link_libraries(main PRIVATE add pthread)' > project/CMakeLists.txt
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
接下来我们执行下面这个命令
cmake --build . -v
我们发现,编译main程序的时候它去链接了add库和pthread库。
4.2.PRIVATE / PUBLIC / INTERFACE
这三个关键字定义了 库链接关系的传播范围,即:
谁需要链接哪些库。
1. PRIVATE — "我自己用就行"
含义:链接的库 只在当前目标的构建过程中需要,不会传递给依赖当前目标的其他目标。
使用场景:
-
当前目标的内部实现依赖某个库,但它的对外接口并没有暴露该库的内容
-
使用者不需要关心这个库
示例:
add_library(mylib STATIC mylib.cpp)# mylib 内部用到 pthread,但对外接口不依赖
target_link_libraries(mylib PRIVATE pthread)
效果:
-
编译/链接
mylib
时:会加上-lpthread
-
其他目标链接
mylib
时:不会自动继承pthread
2. INTERFACE — "给别人用"
含义:当前目标自己不需要,但使用当前目标的其他目标需要链接这个库。
使用场景:
-
纯接口库(INTERFACE library)
-
头文件库需要告诉别人:你要用我,就得同时链接某个库
-
适配/封装库(本身不编译代码,只转发依赖)
示例:
# 定义一个纯接口库
add_library(myinterface INTERFACE)# 使用 myinterface 的目标都要链接 pthread
target_link_libraries(myinterface INTERFACE pthread)
效果:
-
myinterface
自己没有要编译的对象,不会用到pthread
-
任何
target_link_libraries(... myinterface)
的目标都会继承-lpthread
3. PUBLIC — "我自己用,别人也要用"
含义:当前目标自己需要链接的库,同时使用当前目标的其他目标也要继承这些库。
使用场景:
-
最常见的情况
-
当前库的实现依赖某个库,并且它的 API 对外也依赖该库
-
使用者必须知道并链接相同的库
示例:
add_library(mylib STATIC mylib.cpp)# mylib 的实现和接口都依赖另一个库,比如 ssl
target_link_libraries(mylib PUBLIC ssl)
效果:
-
编译/链接
mylib
时:会加上-lssl
-
其他目标链接
mylib
时:也会自动继承-lssl
✅ 总结对比
关键字 | 当前目标是否用到 | 依赖当前目标的其他目标是否继承 | 典型场景 |
---|---|---|---|
PRIVATE | ✅ 是 | ❌ 否 | 内部实现依赖库(如日志、线程库) |
INTERFACE | ❌ 否 | ✅ 是 | 纯头文件库、接口库转发依赖 |
PUBLIC | ✅ 是 | ✅ 是 | 常见库依赖,对外 API 需要 |
4.2.1.示例
为了让大家更直观的感受一下,我们来看看下面这个例子
我们看看
CMakeLists.txt
cmake_minimum_required(VERSION 3.18)project(TestLinkLibraryLANGUAGES CXX
)add_subdirectory(src)add_executable(main main.cpp)target_link_libraries(main PRIVATE add)
main.cpp
int main()
{return 0;
}
src/add.cpp
//空文件, 不真的使用add的逻辑,就是生成一个空的elf格式为库文件
src/CMakeLists.txt
#创建一个动态库
add_library(add SHARED add.cpp)# 库文件搜索路径
# 下面这3个路径都是不存在的
target_link_directories(add PRIVATE "/usr/local/lib/private")
target_link_directories(add PUBLIC "/usr/local/lib/public")
target_link_directories(add INTERFACE "/usr/local/lib/interface")# 依赖库列表
target_link_libraries(add INTERFACE "pthread")
我们可以执行下面这个命令来一键搭建出这个目录结构和文件
mkdir -p Test/src && \
echo 'cmake_minimum_required(VERSION 3.18)project(TestLinkLibraryLANGUAGES CXX
)add_subdirectory(src)add_executable(main main.cpp)target_link_libraries(main PRIVATE add)' > Test/CMakeLists.txt && \
echo 'int main()
{return 0;
}' > Test/main.cpp && \
echo '//空文件, 不真的使用add的逻辑,就是生成一个空的elf格式为库文件' > Test/src/add.cpp && \
echo '# 创建一个动态库
add_library(add SHARED add.cpp)# 库文件搜索路径
# 下面这3个路径都是不存在的
target_link_directories(add PRIVATE "/usr/local/lib/private")
target_link_directories(add PUBLIC "/usr/local/lib/public")
target_link_directories(add INTERFACE "/usr/local/lib/interface")# 依赖库列表
target_link_libraries(add INTERFACE "pthread")' > Test/src/CMakeLists.txt
接下来我们就来构建我们的项目
mkdir build && cd build && cmake ..
接下来我们执行下面这个命令
cmake --build . -v
这个命令会打印出实际的编译和链接过程。
我们来看看
我们来仔细看看
1. 链接
libadd.so
阶段
日志里这段:
[ 50%] Linking CXX shared library libadd.so
/usr/bin/c++ -fPIC -shared -Wl,-soname,libadd.so -o libadd.so CMakeFiles/add.dir/add.cpp.o -L/usr/local/lib/private -L/usr/local/lib/public
👉 关键在最后一行:
-L/usr/local/lib/private -L/usr/local/lib/public
这里的 -L/usr/local/lib/private
和 -L/usr/local/lib/public
就是库文件搜索路径。
-
private 来自
target_link_directories(add PRIVATE ...)
(只在编译add
自己时生效,不会传递给依赖main
) -
public 来自
target_link_directories(add PUBLIC ...)
(add
自己要用,同时也会传递给依赖main
)
2. 链接
main
阶段
日志里后面这段:
[100%] Linking CXX executable main
/usr/bin/c++ CMakeFiles/main.dir/main.cpp.o -o main -L/usr/local/lib/public -L/usr/local/lib/interface -Wl,-rpath,/usr/local/lib/public:/usr/local/lib/interface:/root/cmake/Test/build/src src/libadd.so -lpthread
👉 关键在最后一行:
-L/usr/local/lib/public -L/usr/local/lib/interface ... -lpthread
这里:
-
-L/usr/local/lib/public
→ 来自target_link_directories(add PUBLIC ...)
(传递给main
) -
-L/usr/local/lib/interface
→ 来自target_link_directories(add INTERFACE ...)
(只传递给依赖main
,不影响add
本身) -
-lpthread
→ 来自target_link_libraries(add INTERFACE pthread)
(只传递给依赖main
,不影响add
本身)
✅ 总结:
-
PRIVATE → 只在目标自己链接时生效(
libadd.so
阶段能看到,main
阶段看不到) -
PUBLIC → 自己用 + 传递给依赖(
libadd.so
阶段、main
阶段都能看到) -
INTERFACE → 自己不用,只传递给依赖(
main
阶段能看到,libadd.so
阶段看不到)
4.3.LINK_LIBRARIES 和 INTERFACE_LINK_LIBRARIES
首先要明确一个关键点:每个属性都属于一个特定的 CMake 目标。
-
LINK_LIBRARIES
: 这是目标的 “实现依赖” 列表。它回答了这个问题:“为了构建我自己,我需要链接哪些库?”-
受众: 链接器(当构建这个目标本身时)。
-
类比: 建造一栋房子(目标)时,你自己工人需要的材料和工具(如钢筋、水泥、图纸)。这些是构建过程所必需的,但房子建好后,别人看不到你用的是什么牌子的水泥。
-
-
INTERFACE_LINK_LIBRARIES
: 这是目标的 “接口依赖” 或 “传递依赖” 列表。它回答了这个问题:“任何想要使用我的目标,必须额外链接哪些库?”-
受众: 所有链接了当前目标的其他目标(消费者)。
-
类比: 房子建好后,你提供给住户的“使用手册”,上面写着“要使用本房子的水电系统,你必须连接市电网和市政水管”。这些要求是房子(目标)接口的一部分,会传递给它所有的使用者。
-
关键字如何设置这两个属性
target_link_libraries(<target> [PRIVATE|PUBLIC|INTERFACE] <item>...)
命令的本质就是一个高级的、语法糖式的属性设置器。
关键字 | 影响的属性 | 语义 |
---|---|---|
PRIVATE | 仅设置 LINK_LIBRARIES | “我需要 <item> 来构建我自己,但我的使用者不需要知道它。” |
INTERFACE | 仅设置 INTERFACE_LINK_LIBRARIES | “我自己不需要 <item> ,但任何使用我的目标必须链接它。” |
PUBLIC | 同时设置 LINK_LIBRARIES 和 INTERFACE_LINK_LIBRARIES | “我既需要 <item> 来构建我自己,而且我的使用者也必须链接它。” |
事实上呢:这PRIVATE,INTERFACE,PUBLIC这3个关键字在set_target_properties上的用法就揭露了它们对于LINK_LIBRARIES 和 INTERFACE_LINK_LIBRARIES的执行策略
1. PRIVATE 关键字
target_link_libraries(MyTarget PRIVATE SomeLibrary)
这确实等同于:
set_target_properties(MyTarget PROPERTIESLINK_LIBRARIES "SomeLibrary"
)
-
作用范围:仅影响当前目标自身的构建
-
使用场景:当库只在目标实现中使用,而不在公共接口中暴露时
-
示例:一个数学计算库内部使用了加速计算的第三方库,但这个依赖不应该暴露给使用者
2. INTERFACE 关键字
target_link_libraries(MyTarget INTERFACE SomeLibrary)
这确实等同于:
set_target_properties(MyTarget PROPERTIESINTERFACE_LINK_LIBRARIES "SomeLibrary"
)
-
作用范围:不影响当前目标,但影响所有使用此目标的其他目标
-
使用场景:头文件库(header-only libraries)或需要强制使用者链接特定库的情况
-
示例:一个接口库要求使用者必须链接特定的线程库或系统库
3. PUBLIC 关键字
target_link_libraries(MyTarget PUBLIC SomeLibrary)
这确实等同于同时设置两个属性:
set_target_properties(MyTarget PROPERTIESLINK_LIBRARIES "SomeLibrary"INTERFACE_LINK_LIBRARIES "SomeLibrary"
)
-
作用范围:既影响当前目标,也影响所有使用此目标的其他目标
-
使用场景:当库既在目标实现中使用,也在公共接口中暴露时
-
示例:一个库的公共头文件包含了另一个库的头文件,因此使用者也需要能够找到那个库
4.3.1.示例
假设我们有一个简单的项目,包含一个数学库 MathLib
和一个使用该库的可执行程序 Calculator
。
项目结构:
.
├── CMakeLists.txt # 根 CMakeLists
├── calculator.cpp # 主程序源文件
└── mathlib/ # 数学库目录├── CMakeLists.txt├── include/ # 对外公开的头文件│ └── mathutils.h # 库的公共接口└── src/├── mathutils.cpp # 库的实现文件└── internal.h # 库内部使用的私有头文件
假设我们自己创建的MathLib
需要依赖一个外部数学库(如 libm.so,它的库名也就是m
),而 我们最终要生成的可执行程序Calculator
还需要依赖一个线程库。那么我们应该怎么做?
第一步:在 MathLib 中添加依赖
在 mathlib/CMakeLists.txt
中,我们添加库依赖:
# 创建静态库目标
add_library(MathLib STATIC src/mathutils.cpp)# 设置头文件包含路径
target_include_directories(MathLibPRIVATE${CMAKE_CURRENT_SOURCE_DIR}/src # 仅自己使用:用于查找 internal.hPUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # 自己和他人使用:用于查找 mathutils.h
)# 添加库依赖
target_link_libraries(MathLibPRIVATE m # 仅 MathLib 自己需要数学库,不向外暴露
)
此时发生的属性变化:
-
MathLib
的LINK_LIBRARIES
属性包含:"m"
(来自 PRIVATE) -
MathLib
的INTERFACE_LINK_LIBRARIES
属性包含:""
(空,因为target_link_libraries里面没有 PUBLIC/INTERFACE 依赖)
第二步:在根 CMakeLists.txt 中建立完整的依赖关系
# 创建可执行文件目标
add_executable(Calculator calculator.cpp)# 建立链接关系,触发属性传递
target_link_libraries(CalculatorPRIVATE MathLib # 链接数学库PUBLIC pthread # 链接线程库,并向外暴露此依赖
)
传递过程详解:
-
处理 MathLib 依赖:
-
将
MathLib
添加到Calculator
的LINK_LIBRARIES
属性 -
由于是 PRIVATE,不传播
MathLib
的接口依赖,也就是说不往INTERFACE_LINK_LIBRARIES里面写东西
-
-
处理 pthread 依赖:
-
将
pthread
添加到Calculator
的LINK_LIBRARIES
属性 -
由于是 PUBLIC,也将
pthread
添加到Calculator
的INTERFACE_LINK_LIBRARIES
属性
-
传递后的结果:
-
Calculator
的LINK_LIBRARIES
属性包含:"MathLib";"pthread"
-
Calculator
的INTERFACE_LINK_LIBRARIES
属性包含:"pthread"
(来自 PUBLIC)
第三步:生成构建命令 - 转换为链接器参数
当执行 cmake --build .
时,CMake 生成具体的链接命令:
编译 MathLib 时:
# 编译 mathutils.cpp
g++ -I/absolute/path/to/mathlib/src -I/absolute/path/to/mathlib/include -c mathutils.cpp -o mathutils.o# 创建静态库(链接阶段实际上只是打包.o文件)
ar rcs libMathLib.a mathutils.o
编译和链接 Calculator 时:
# 编译 calculator.cpp(只包含传递过来的PUBLIC包含路径)
g++ -I/absolute/path/to/mathlib/include -c calculator.cpp -o calculator.o# 链接最终可执行文件
g++ calculator.o -L. -lMathLib -lpthread -lm -o Calculator
链接器参数说明:
-
-L.
:添加当前目录到库搜索路径(因为 libMathLib.a 在当前构建目录) -
-lMathLib
:链接 MathLib 静态库(来自target_link_libraries(Calculator PRIVATE MathLib)
) -
-lpthread
:链接系统线程库(来自target_link_libraries(Calculator PUBLIC pthread)
) -
-lm
:链接系统数学库(来自 MathLib 的 PRIVATE 依赖,但通过链接 libMathLib.a 间接需要)
五.set_target_properties和 get_target_properties
设置/查询⽬标(如可执⾏⽂件、库)的各种属性,控制编译、链接、安装等⾏为。
set_target_properties(<target1> <target2> ...PROPERTIES <prop1> <value1><prop2> <value2> ...)
get_target_property(<variable> <target> <property>)
属性名称 | 含义说明 | 对应编译器/链接器选项 | 设置方式 |
---|---|---|---|
INCLUDE_DIRECTORIES | 目标自身编译时需要包含的头文件目录列表 | -I<directory> | PRIVATE 或 PUBLIC 关键字 |
INTERFACE_INCLUDE_DIRECTORIES | 使用此目标的下游目标需要包含的头文件目录列表 | -I<directory> | INTERFACE 或 PUBLIC 关键字 |
LINK_LIBRARIES | 目标自身链接时需要链接的库列表 | -l<library> 或完整路径 | PRIVATE 或 PUBLIC 关键字 |
INTERFACE_LINK_LIBRARIES | 使用此目标的下游目标需要链接的库列表 | -l<library> 或完整路径 | INTERFACE 或 PUBLIC 关键字 |
LIBRARY_OUTPUT_DIRECTORY | 库文件的输出目录路径 | - | set_target_properties |
LIBRARY_OUTPUT_NAME | 库文件的输出名称(不包含扩展名和lib前缀) | - | set_target_properties |
BUILD_RPATH | 构建目录中的运行时库文件搜索路径 | -Wl,-rpath,<path> | set_target_properties |
INSTALL_RPATH | 安装目录中的运行时库文件搜索路径 | -Wl,-rpath,<path> | set_target_properties |
如果想要了解更多关于属性的信息,可以去官网看看:cmake-properties(7) — CMake 4.1.0 Documentation
简单示例
1. 设置和获取输出目录和名称
# 创建一个库目标
add_library(mylib STATIC mylib.cpp)# 设置输出属性
set_target_properties(mylib PROPERTIESLIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"LIBRARY_OUTPUT_NAME "mylib_renamed"
)# 获取属性值
get_target_property(MYLIB_OUTPUT_DIR mylib LIBRARY_OUTPUT_DIRECTORY)
get_target_property(MYLIB_OUTPUT_NAME mylib LIBRARY_OUTPUT_NAME)message("Library output directory: ${MYLIB_OUTPUT_DIR}")
message("Library output name: ${MYLIB_OUTPUT_NAME}")
2. 设置和获取包含目录
# 创建一个库目标
add_library(mylib STATIC mylib.cpp)# 设置包含目录属性
set_target_properties(mylib PROPERTIESINCLUDE_DIRECTORIES "/path/to/private/includes"INTERFACE_INCLUDE_DIRECTORIES "/path/to/public/includes"
)# 获取属性值
get_target_property(MYLIB_PRIVATE_INCLUDES mylib INCLUDE_DIRECTORIES)
get_target_property(MYLIB_PUBLIC_INCLUDES mylib INTERFACE_INCLUDE_DIRECTORIES)message("Private includes: ${MYLIB_PRIVATE_INCLUDES}")
message("Public includes: ${MYLIB_PUBLIC_INCLUDES}")
3. 设置和获取链接库
# 创建两个库目标
add_library(mylib STATIC mylib.cpp)
add_library(myotherlib STATIC myotherlib.cpp)# 设置链接库属性
set_target_properties(mylib PROPERTIESLINK_LIBRARIES "pthread;m" # 链接 pthread 和数学库INTERFACE_LINK_LIBRARIES "myotherlib" # 使用者需要链接 myotherlib
)# 获取属性值
get_target_property(MYLIB_LINKS mylib LINK_LIBRARIES)
get_target_property(MYLIB_INTERFACE_LINKS mylib INTERFACE_LINK_LIBRARIES)message("Links: ${MYLIB_LINKS}")
message("Interface links: ${MYLIB_INTERFACE_LINKS}")
4. 设置和获取 RPATH
# 创建一个可执行目标
add_executable(myapp main.cpp)# 设置 RPATH 属性
set_target_properties(myapp PROPERTIESBUILD_RPATH "\$ORIGIN/../lib" # 构建时 RPATH,指向相对路径INSTALL_RPATH "/usr/local/lib" # 安装时 RPATH
)# 获取属性值
get_target_property(MYAPP_BUILD_RPATH myapp BUILD_RPATH)
get_target_property(MYAPP_INSTALL_RPATH myapp INSTALL_RPATH)message("Build RPATH: ${MYAPP_BUILD_RPATH}")
message("Install RPATH: ${MYAPP_INSTALL_RPATH}")
5.1.示例一——基本示例
CMakeLists.txt
cmake_minimum_required(VERSION 3.18)project(TestPropLANGUAGES CXX
)add_subdirectory(src)add_executable(main main.cpp)target_link_libraries(main PRIVATE add)
main.cpp
#include "iostream"int main()
{return 0;
}
add.cpp
//空文件, 不真的使用add的逻辑,就是生成一个空的elf格式为库文件
src/CMakeLists.txt
add_library(mylib SHARED add.cpp)# 一次性设置多个属性
set_target_properties(mylib PROPERTIESLIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"LIBRARY_OUTPUT_NAME "mylib_custom"INCLUDE_DIRECTORIES "/opt/mylib/includes"INTERFACE_INCLUDE_DIRECTORIES "/opt/mylib/public_includes"LINK_LIBRARIES "pthread"INTERFACE_LINK_LIBRARIES "dl"
)# 获取所有属性值
get_target_property(OUTPUT_DIR mylib LIBRARY_OUTPUT_DIRECTORY)
get_target_property(OUTPUT_NAME mylib LIBRARY_OUTPUT_NAME)
get_target_property(INCLUDES mylib INCLUDE_DIRECTORIES)
get_target_property(INTERFACE_INCLUDES mylib INTERFACE_INCLUDE_DIRECTORIES)
get_target_property(LINKS mylib LINK_LIBRARIES)
get_target_property(INTERFACE_LINKS mylib INTERFACE_LINK_LIBRARIES)# 打印所有属性
message("Output directory: ${OUTPUT_DIR}")
message("Output name: ${OUTPUT_NAME}")
message("Includes: ${INCLUDES}")
message("Interface includes: ${INTERFACE_INCLUDES}")
message("Links: ${LINKS}")
message("Interface links: ${INTERFACE_LINKS}")
接下来我们只需要执行下面这些命令即可
怎么样?对上了吧!!!!
5.2.示例二:设置输出目录
其实设置输出目录的例子我们已经在2.4里面详细讲解了一下,我们这里就简单概况一下即可。
📂 目录结构
project1/
├── CMakeLists.txt
├── main.cpp
└── util.cpp
📄 main.cpp
#include <iostream>
void hello();
int main() {hello();return 0;
}
📄 util.cpp
#include <iostream>
void hello() {std::cout << "Hello from util!" << std::endl;
}
📄 CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(OutputDirs LANGUAGES CXX)add_executable(app main.cpp)
add_library(util STATIC util.cpp)# 设置输出路径
set_target_properties(app PROPERTIESRUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)set_target_properties(util PROPERTIESARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
)target_link_libraries(app PRIVATE util)
我们可以一键生成这个目录及其内部文件
mkdir -p project1 && cd project1 && \
echo '#include <iostream>
void hello();
int main() {hello();return 0;
}' > main.cpp && \
echo '#include <iostream>
void hello() {std::cout << "Hello from util!" << std::endl;
}' > util.cpp && \
echo 'cmake_minimum_required(VERSION 3.18)
project(OutputDirs LANGUAGES CXX)add_executable(app main.cpp)
add_library(util STATIC util.cpp)set_target_properties(app PROPERTIESRUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)set_target_properties(util PROPERTIESARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
)target_link_libraries(app PRIVATE util)' > CMakeLists.txt && tree .
现在我们来构建一下项目
mkdir build && cd build && cmake .. && make
我们看看生成的库文件在哪里了
……
可以看到放到build目录里面的lib目录里面了,和我们设置的是一样的
5.3.示例三:设置 C++ 标准
📂 目录结构
project2/
├── CMakeLists.txt
└── main.cpp
📄 main.cpp
#include <iostream>
#include <vector>int main() {std::vector v = {1, 2, 3}; // 只有 C++17 才支持类模板参数推导for (int x : v) {std::cout << x << " ";}std::cout << std::endl;return 0;
}
📄 CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(CxxStandard LANGUAGES CXX)add_executable(app main.cpp)# 设置 C++ 标准为 17,并要求必须支持
set_target_properties(app PROPERTIESCXX_STANDARD 17CXX_STANDARD_REQUIRED YES
)
其实我们可以一键搭建出上面这个目录及其内部文件来
mkdir -p project2 && cd project2 && \
echo '#include <iostream>
#include <vector>int main() {std::vector v = {1, 2, 3};for (int x : v) {std::cout << x << " ";}std::cout << std::endl;return 0;
}' > main.cpp && \
echo 'cmake_minimum_required(VERSION 3.18)
project(CxxStandard LANGUAGES CXX)add_executable(app main.cpp)set_target_properties(app PROPERTIESCXX_STANDARD 17CXX_STANDARD_REQUIRED YES
)' > CMakeLists.txt && tree .
现在我们来构建一下项目
mkdir build && cd build && cmake ..
接下来我们执行下面这个命令来显示详细的构建信息
cmake --build . -v
输出里有这一行:
/usr/bin/c++ -std=gnu++17 -MD -MT CMakeFiles/app.dir/main.cpp.o -MF CMakeFiles/app.dir/main.cpp.o.d -o CMakeFiles/app.dir/main.cpp.o -c /root/cmake/project2/main.cpp
重点在 -std=gnu++17
:
-
这是 CMake 根据设置的
CXX_STANDARD 17
自动加上的编译选项。 -
默认情况下(如果你没设置),它可能是
-std=gnu++11
或者 不带任何-std=
,取决于 CMake 的默认标准。
5.4.示例四:设置库的版本信息
📂 目录结构
project3/
├── CMakeLists.txt
├── main.cpp
└── add.cpp
📄 main.cpp
#include <iostream>
int add(int a, int b);
int main() {std::cout << "3 + 4 = " << add(3, 4) << std::endl;return 0;
}
📄 add.cpp
int add(int a, int b) {return a + b;
}
📄 CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(LibraryVersion LANGUAGES CXX)add_library(add SHARED add.cpp)# 给共享库设置版本号
set_target_properties(add PROPERTIESVERSION 2.4.0 # 完整版本SOVERSION 2 # 兼容版本
)add_executable(main main.cpp)
target_link_libraries(main PRIVATE add)
我们还是一键搭建出这个目录及其内部文件
mkdir -p project3 && cd project3 && \
echo '#include <iostream>
int add(int a, int b);
int main() {std::cout << "3 + 4 = " << add(3, 4) << std::endl;return 0;
}' > main.cpp && \
echo 'int add(int a, int b) {return a + b;
}' > add.cpp && \
echo 'cmake_minimum_required(VERSION 3.18)
project(LibraryVersion LANGUAGES CXX)add_library(add SHARED add.cpp)set_target_properties(add PROPERTIESVERSION 2.4.0SOVERSION 2
)add_executable(main main.cpp)
target_link_libraries(main PRIVATE add)' > CMakeLists.txt && tree .
接下来我们就来构建我们的项目
mkdir build && cd build && cmake ..
make
接下来我们来看看build目录里面的情况
……
和我们设置的完全对上了
六.add_subdirectory
大家可以去官网看看:add_subdirectory — CMake 4.1.0 Documentation
添加⼦⽬录到构建树,cmake会⾃动进⼊到源码树⼦⽬录,执⾏位于⼦⽬录⾥的CMakeLists.txt⽂ 件
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])
其实这个就相当于C语言里面的函数调用,只不过这里调用的不是函数,而是CMakeLists.txt。
执行这个函数时,CMake会跳转着去执行被调用的CMakeLists.txt里面的内容。
具体来说,当 CMake 处理当前目录的 CMakeLists.txt
文件时,遇到 add_subdirectory
命令,它会暂停对当前文件的处理,并跳转到指定的 source_dir
目录中,去执行该子目录下的另一个 CMakeLists.txt
文件。待该子目录的 CMake 逻辑全部执行完毕后,再返回父目录继续执行后续命令。
命令中的可选参数也提供了额外的灵活性:
-
binary_dir
:允许您指定子模块构建结果的存放目录,若不提供,则默认使用source_dir
。 -
EXCLUDE_FROM_ALL
:若设置此选项,则该子目录中的目标默认不会被构建,除非被其他依赖项明确要求。 -
SYSTEM
:将此子目录中的头文件视为系统头文件,编译器在报告警告时会忽略它们。
总而言之,add_subdirectory
是 CMake 实现项目模块化构建的核心命令之一,它通过引入并管理子目录的构建过程,将多个独立的构建单元组织成一个完整、有序的整体。
6.1.简单示例
我给你整一个最常见的 add_subdirectory
使用示例,分两个子目录:
📂 项目结构
project_subdir/
├── CMakeLists.txt # 顶层
├── main.cpp # 主程序
└── mathlib/ # 子目录├── CMakeLists.txt└── add.cpp
📄 main.cpp
#include <iostream>int add(int a, int b);int main() {std::cout << "3 + 4 = " << add(3, 4) << std::endl;return 0;
}
📄 mathlib/add.cpp
int add(int a, int b) {return a + b;
}
📄 顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(UseSubdir LANGUAGES CXX)# 添加子目录
add_subdirectory(mathlib)# 定义主程序
add_executable(main main.cpp)# 链接子目录里的库
target_link_libraries(main PRIVATE mathlib)
📄 mathlib/CMakeLists.txt
# 定义一个库
add_library(mathlib add.cpp)
我们可以执行下面这个命令一键搭建出这个目录结构和文件
# 创建目录结构
mkdir -p project_subdir/mathlib# 创建 main.cpp
cat > project_subdir/main.cpp <<'EOF'
#include <iostream>int add(int a, int b);int main() {std::cout << "3 + 4 = " << add(3, 4) << std::endl;return 0;
}
EOF# 创建 mathlib/add.cpp
cat > project_subdir/mathlib/add.cpp <<'EOF'
int add(int a, int b) {return a + b;
}
EOF# 创建顶层 CMakeLists.txt
cat > project_subdir/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.18)
project(UseSubdir LANGUAGES CXX)# 添加子目录
add_subdirectory(mathlib)# 定义主程序
add_executable(main main.cpp)# 链接子目录里的库
target_link_libraries(main PRIVATE mathlib)
EOF# 创建 mathlib/CMakeLists.txt
cat > project_subdir/mathlib/CMakeLists.txt <<'EOF'
# 定义一个库
add_library(mathlib add.cpp)
EOF
接下来我们就来构建这个项目
mkdir build && cd build && cmake ..
接下来我们执行这个
cmake --build . -v
👉 这样,add_subdirectory(mathlib)
就把子目录的 CMakeLists.txt
拉进来,生成了一个 mathlib
库,顶层的 main
可以直接链接。
6.2.CMake内置路径变量
在学习CMake时,我们肯定会遇到一些路径变量,接下来我们就来讲解一下3个
- 1.
CMAKE_CURRENT_SOURCE_DIR
- 2.
CMAKE_CURRENT_LIST_FILE
- 3.
CMAKE_CURRENT_LIST_DIR
现在我们就接着add_subdirectory来讲解一下
1.
CMAKE_CURRENT_SOURCE_DIR
-
含义:
当前正在处理的CMakeLists.txt
所在的 源代码目录(源目录)。 -
用途:
让你知道这个正在处理的CMakeLists.txt
是在哪个目录下的,从而能用相对路径去找到源码。
✅ 示例:
message(STATUS "CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
如果你在顶层 project/CMakeLists.txt
打印它,输出类似:
CMAKE_CURRENT_SOURCE_DIR = /root/cmake/project
如果你在 sub/CMakeLists.txt
打印它,输出类似:
CMAKE_CURRENT_SOURCE_DIR = /root/cmake/project/sub
👉 它随 add_subdirectory
进入不同子目录而改变。
2.
CMAKE_CURRENT_LIST_FILE
-
含义:
当前正在执行的CMakeLists.txt
文件的完整路径(文件名 + 路径)。 -
用途:
用于调试,知道当前是哪一个 CMakeLists.txt 在执行。
✅ 示例:
message(STATUS "CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
顶层会输出:
CMAKE_CURRENT_LIST_FILE = /root/cmake/project/CMakeLists.txt
sub/CMakeLists.txt
会输出:
CMAKE_CURRENT_LIST_FILE = /root/cmake/project/sub/CMakeLists.txt
👉 这个变量不会只是目录,而是精确到 .txt
文件。
3.
CMAKE_CURRENT_LIST_DIR
-
含义:
当前正在执行的 CMake 脚本文件所在的目录(也就是CMAKE_CURRENT_LIST_FILE
的目录部分)。 -
用途:
常用于引入其他文件,比如include()
或add_subdirectory()
时,能保证你始终在正确目录下。
✅ 示例:
message(STATUS "CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")
顶层会输出:
CMAKE_CURRENT_LIST_DIR = /root/cmake/project
sub2/CMakeLists.txt
会输出:
CMAKE_CURRENT_LIST_DIR = /root/cmake/project/sub/sub2
👉 这是一个非常常用的变量,尤其在写 可复用 CMake 模块 时。
🧠 小结对比
变量 | 含义 | 示例(在 |
---|---|---|
| 当前 CMakeLists.txt 所在源代码目录 |
|
| 当前正在执行的 CMakeLists.txt 文件的完整路径 |
|
| 当前 CMakeLists.txt 文件所在的目录 |
|
注意:
-
CMAKE_CURRENT_SOURCE_DIR
和CMAKE_CURRENT_LIST_DIR
在绝大多数情况下相同(因为CMakeLists.txt
就在源目录里)。 -
区别是:如果你用
include(some_other.cmake)
,那么CMAKE_CURRENT_LIST_DIR
会指向.cmake
文件所在目录,而CMAKE_CURRENT_SOURCE_DIR
还是当前 CMakeLists.txt 的目录。
6.2.1.示例1
CMakeLists.txt
# 1 设置版本要求
cmake_minimum_required(VERSION 3.18)# 2 设置项目名称
project(TestAddSubDir)# 3 打印 内置路径变量message(STATUS "from top-level CMakeLists.txt")
# 打印 当前正在执行的源代码目录--- 也就是CMakeLists.txt所在的目录
message(STATUS "CMAKE_CURRENT_SOURCE_DIR:" ${CMAKE_CURRENT_SOURCE_DIR})
# 打印 当前正在执行的cmake 脚本的 完整名称
message(STATUS "CMAKE_CURRENT_LIST_FILE:" ${CMAKE_CURRENT_LIST_FILE})
# 打印当前正在执行的cmake 脚本的 全目录
message(STATUS "CMAKE_CURRENT_LIST_DIR:" ${CMAKE_CURRENT_LIST_DIR})# 4 包含子目录cmake脚本
add_subdirectory(sub)
sub/CMakeLists.txt
message(STATUS "from sub/CMakeLists.txt")
# 打印 当前正在执行的源代码目录--- 也就是CMakeLists.txt所在的目录
message(STATUS "CMAKE_CURRENT_SOURCE_DIR:" ${CMAKE_CURRENT_SOURCE_DIR})
# 打印 当前正在执行的cmake 脚本的 完整名称
message(STATUS "CMAKE_CURRENT_LIST_FILE:" ${CMAKE_CURRENT_LIST_FILE})
# 打印当前正在执行的cmake 脚本的 全目录
message(STATUS "CMAKE_CURRENT_LIST_DIR:" ${CMAKE_CURRENT_LIST_DIR})
我们执行看看
1. CMAKE_CURRENT_SOURCE_DIR
-
含义:这是当前正在处理的
CMakeLists.txt
文件所在目录的绝对路径。 -
关键点:它的范围是“目录(Directory)”。当 CMake 进入一个由
add_subdirectory()
指定的子目录时,这个变量的值就会改变为那个子目录的路径。 -
类比:可以把它想象成你在终端里执行
pwd
(打印工作目录)命令,它告诉你当前在哪个目录下工作。
2. CMAKE_CURRENT_LIST_FILE
-
含义:这是当前正在被处理的 CMake 脚本文件的完整绝对路径和文件名。
-
关键点:它的范围是“文件(File)”。它不仅对
CMakeLists.txt
有效,对通过include()
命令包含的.cmake
脚本文件也同样有效。每次 CMake 开始处理一个新文件(无论是主列表文件还是被包含的文件),这个变量都会更新。
3. CMAKE_CURRENT_LIST_DIR
-
含义:这是
CMAKE_CURRENT_LIST_FILE
变量所指向文件的所在目录的绝对路径。可以理解为CMAKE_CURRENT_LIST_FILE
的目录部分。 -
关键点:它结合了前两个变量的特点:它像
_LIST_FILE
一样对任何 CMake 脚本文件都有效,但又像_SOURCE_DIR
一样只返回目录路径。这是 CMake 官方推荐用于模块化脚本的变量。
当 CMake 执行 add_subdirectory()
命令跳转到子目录后,其“当前上下文”会发生改变。以下变量会随之更新,以反映新子目录的上下文信息。这对于正确管理多目录项目的源代码路径和构建输出路径至关重要。
变量名 | 进入子目录后是否变化? | 说明 |
---|---|---|
CMAKE_CURRENT_SOURCE_DIR | ✅ 变化 | 变为子目录在源代码树中的路径。 |
CMAKE_CURRENT_BINARY_DIR | ✅ 变化 | 变为与 CMAKE_CURRENT_SOURCE_DIR 对应的构建树(二进制树) 中的路径。 |
CMAKE_CURRENT_LIST_FILE | ✅ 变化 | 变为子目录中 CMakeLists.txt 文件的完整绝对路径。 |
CMAKE_CURRENT_LIST_DIR | ✅ 变化 | 变为 CMAKE_CURRENT_LIST_FILE 变量所指向文件的目录路径。 |
6.2.2.示例2
上面这个示例二的递归层数太低了,我们可以再深入一下来看看
📂 目录结构
project/
├── CMakeLists.txt
├── main.cpp
└── sub/├── CMakeLists.txt└── sub2/├── CMakeLists.txt└── sub3/└── CMakeLists.txt
📄 main.cpp
#include <iostream>
int main() {std::cout << "Hello from main.cpp" << std::endl;return 0;
}
📄 project/CMakeLists.txt (顶层)
cmake_minimum_required(VERSION 3.18)
project(DemoAddSubDir LANGUAGES CXX)message(STATUS ">>> [Top] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Top] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Top] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")add_executable(main main.cpp)# 进入 sub 子目录
add_subdirectory(sub)
📄 project/sub/CMakeLists.txt
message(STATUS ">>> [Sub] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Sub] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Sub] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")# 进入 sub2 子目录
add_subdirectory(sub2)
📄 project/sub/sub2/CMakeLists.txt
message(STATUS ">>> [Sub2] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Sub2] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Sub2] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")# 进入 sub3 子目录
add_subdirectory(sub3)
📄 project/sub/sub2/sub3/CMakeLists.txt
message(STATUS ">>> [Sub3] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Sub3] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Sub3] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")
我们可以复制下面这个目录到命令行里面一键执行,就能搭建出我们这个目录结构和文件
# 创建目录
mkdir -p project/sub/sub2/sub3# 创建 main.cpp
cat > project/main.cpp <<'EOF'
#include <iostream>
int main() {std::cout << "Hello from main.cpp" << std::endl;return 0;
}
EOF# 顶层 CMakeLists.txt
cat > project/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.18)
project(DemoAddSubDir LANGUAGES CXX)message(STATUS ">>> [Top] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Top] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Top] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")add_executable(main main.cpp)# 进入 sub 子目录
add_subdirectory(sub)
EOF# sub/CMakeLists.txt
cat > project/sub/CMakeLists.txt <<'EOF'
message(STATUS ">>> [Sub] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Sub] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Sub] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")# 进入 sub2 子目录
add_subdirectory(sub2)
EOF# sub2/CMakeLists.txt
cat > project/sub/sub2/CMakeLists.txt <<'EOF'
message(STATUS ">>> [Sub2] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Sub2] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Sub2] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")# 进入 sub3 子目录
add_subdirectory(sub3)
EOF# sub3/CMakeLists.txt
cat > project/sub/sub2/sub3/CMakeLists.txt <<'EOF'
message(STATUS ">>> [Sub3] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Sub3] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Sub3] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")
EOF
接下来我们就来运行这个
📌 运行
mkdir build && cd build && cmake ..
这样,你就能很直观地看到:
-
add_subdirectory
会一层层进入子目录,递归执行。 -
每一级的 路径变量 都随着目录切换而变化。
七.include
这个其实和add_subdirectory是差不多的意思,但是也是有一点不同。
在 CMake 中,include
和 add_subdirectory
虽然都用于引入外部 CMake 脚本,但它们的行为,特别是对执行上下文(Context) 的处理,有本质的不同。
关键区别在于:include
保持当前作用域,而 add_subdirectory
创建新的子作用域。
1. include
与 add_subdirectory
对 CMAKE_CURRENT_SOURCE_DIR
的影响
include(<file>)
-
行为:把指定的 CMake 脚本“粘贴”到当前位置执行,就像宏展开。
-
目录变量:不会改变当前目录。
-
CMAKE_CURRENT_SOURCE_DIR
、CMAKE_CURRENT_BINARY_DIR
等变量保持为父目录的值。
-
-
示意:
# 根目录 CMakeLists.txt
message("Root dir: ${CMAKE_CURRENT_SOURCE_DIR}")
include(subdir/subcmake.cmake)
subcmake.cmake
里的 CMAKE_CURRENT_SOURCE_DIR
依然是根目录路径。
add_subdirectory(<dir>)
-
行为:切换到指定子目录执行该目录的 CMakeLists.txt,完成后返回父目录。
-
目录变量:在子目录里,
CMAKE_CURRENT_SOURCE_DIR
、CMAKE_CURRENT_BINARY_DIR
会自动更新为子目录路径。 -
示意:
# 根目录 CMakeLists.txt
message("Root dir: ${CMAKE_CURRENT_SOURCE_DIR}")
add_subdirectory(subdir)
在 subdir/CMakeLists.txt
中:
message("Subdir dir: ${CMAKE_CURRENT_SOURCE_DIR}")
输出会显示子目录路径。
2. 变量作用域
include
-
共享当前作用域。
-
在被 include 的文件中定义的普通变量,会在父作用域中可见。
-
示例:
# root CMakeLists.txt
include(subdir/subcmake.cmake)
message("MY_VAR = ${MY_VAR}") # 可以访问 subcmake.cmake 定义的 MY_VAR
add_subdirectory
-
创建新的子作用域。
-
子目录中定义的普通变量默认局部,只在子目录有效。
-
可以通过
set(VAR value PARENT_SCOPE)
将变量传递给父目录。 -
示例:
# subdir/CMakeLists.txt
set(SUB_VAR 123)
set(SUB_VAR_PARENT 456 PARENT_SCOPE)
-
根目录只能看到
SUB_VAR_PARENT
,看不到SUB_VAR
。
✅ 总结
命令 | 是否创建新作用域 | CMAKE_CURRENT_SOURCE_DIR 改变吗 | 子目录变量影响父目录 |
---|---|---|---|
| 否 | 否 | 会影响 |
| 是 | 是 | 默认不影响,需要 |
7.1.简单示例
在 CMake 里,include()
的作用就是 导入另一个 CMake 脚本文件,这样可以把公共配置拆分出去,避免 CMakeLists.txt
太长。
示例目录结构
project_demo/
├── CMakeLists.txt
└── cmake/└── my_settings.cmake
cmake/my_settings.cmake
# 这是一个自定义的cmake脚本
message(STATUS "正在包含 my_settings.cmake")# 定义一个变量
set(MY_PROJECT_VERSION "1.0.0")
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(DemoProject)# 包含外部的 cmake 文件
include(cmake/my_settings.cmake)# 使用里面的变量
message(STATUS "项目版本号是: ${MY_PROJECT_VERSION}")add_executable(demo main.cpp)
main.cpp
#include <iostream>
int main() {std::cout << "Hello CMake!" << std::endl;return 0;
}
我们可以执行下面这个命令就可以搭建出这么一个目录结构及其文件
mkdir -p project_demo/cmake && \
echo '# 这是一个自定义的cmake脚本
message(STATUS "正在包含 my_settings.cmake")
set(MY_PROJECT_VERSION "1.0.0")' > project_demo/cmake/my_settings.cmake && \
echo 'cmake_minimum_required(VERSION 3.10)
project(DemoProject)# 包含外部的 cmake 文件
include(cmake/my_settings.cmake)# 使用里面的变量
message(STATUS "项目版本号是: ${MY_PROJECT_VERSION}")add_executable(demo main.cpp)' > project_demo/CMakeLists.txt && \
echo '#include <iostream>
int main() {std::cout << "Hello CMake!" << std::endl;return 0;
}' > project_demo/main.cpp
接下来我们来构建一下这个项目
mkdir build && cd build && cmake ..
这样就实现了一个最简单的 include()
使用场景:把配置单独写在 my_settings.cmake
里,然后在 CMakeLists.txt
中 include()
进来。
7.2.变量作用域
7.2.1.示例1——子目录的变量的作用域
我们现在来看看
项目目录结构(只关注变量作用域)
Project/
├── CMakeLists.txt # 根目录
├── include_example.cmake # include 用的子脚本
└── subdir/└── CMakeLists.txt # 子目录 CMake
1️⃣ 根目录 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(VariableScopeExample)# 根目录变量
set(ROOT_VAR "root_value")# 1. include 示例
include(include_example.cmake)message("===== Back in root after include =====")
message("ROOT_VAR = ${ROOT_VAR}")
message("INCLUDED_VAR = ${INCLUDED_VAR}")# 2. add_subdirectory 示例
add_subdirectory(subdir)message("===== Back in root after add_subdirectory =====")
message("ROOT_VAR = ${ROOT_VAR}")
message("SUBDIR_VAR = ${SUBDIR_VAR}") # 默认不可见
message("SUBDIR_VAR_PARENT = ${SUBDIR_VAR_PARENT}") # 使用 PARENT_SCOPE 后可见
2️⃣ include_example.cmake
# 定义一个变量
set(INCLUDED_VAR "included_value")
3️⃣ 子目录 subdir/CMakeLists.txt
# 子目录局部变量
set(SUBDIR_VAR "subdir_value")# 子目录变量传给父目录
set(SUBDIR_VAR_PARENT "subdir_value_parent" PARENT_SCOPE)
我们可以执行下面这个命令来搭建这个环境
# 1. 创建目录结构
mkdir -p Project/subdir# 2. 创建根目录 CMakeLists.txt
cat > Project/CMakeLists.txt << 'EOF'
cmake_minimum_required(VERSION 3.10)
project(VariableScopeExample)# 根目录变量
set(ROOT_VAR "root_value")# 1. include 示例
include(include_example.cmake)message("===== Back in root after include =====")
message("ROOT_VAR = ${ROOT_VAR}")
message("INCLUDED_VAR = ${INCLUDED_VAR}")# 2. add_subdirectory 示例
add_subdirectory(subdir)message("===== Back in root after add_subdirectory =====")
message("ROOT_VAR = ${ROOT_VAR}")
message("SUBDIR_VAR = ${SUBDIR_VAR}") # 默认不可见
message("SUBDIR_VAR_PARENT = ${SUBDIR_VAR_PARENT}") # 使用 PARENT_SCOPE 后可见
EOF# 3. 创建 include_example.cmake
cat > Project/include_example.cmake << 'EOF'
# 定义一个变量
set(INCLUDED_VAR "included_value")
EOF# 4. 创建 subdir/CMakeLists.txt
cat > Project/subdir/CMakeLists.txt << 'EOF'
# 子目录局部变量
set(SUBDIR_VAR "subdir_value")# 子目录变量传给父目录
set(SUBDIR_VAR_PARENT "subdir_value_parent" PARENT_SCOPE)
EOF
接下来我们来构建我们的项目
mkdir build && cd build && cmake ..
控制台输出
✅ 效果总结
-
include()
:-
子脚本中定义的变量在父目录可见(
INCLUDED_VAR
)。
-
-
add_subdirectory()
:-
子目录变量默认不可见(
SUBDIR_VAR
)。 -
可以用
PARENT_SCOPE
传给父目录(SUBDIR_VAR_PARENT
)。
-
7.2.2.示例2——根目录的变量的作用域
我给你整理一个完整示例,包含三个变量:
-
ROOT_VAR1
→include
修改父目录 -
ROOT_VAR2
→add_subdirectory
默认修改不会影响父目录 -
ROOT_VAR3
→add_subdirectory
使用PARENT_SCOPE
修改父目录
我们来看看
目录结构
Project/
├── CMakeLists.txt
├── include_example.cmake
└── subdir/└── CMakeLists.txt
根目录 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(ParentVarExample)# 父目录变量
set(ROOT_VAR1 "root_value1")
set(ROOT_VAR2 "root_value2")
set(ROOT_VAR3 "root_value3")# ===== include 示例 =====
include(include_example.cmake)
message("===== Back in root after include =====")
message("ROOT_VAR1 = ${ROOT_VAR1}") # include 修改父目录变量# ===== add_subdirectory 示例 =====
add_subdirectory(subdir)
message("===== Back in root after add_subdirectory =====")
message("ROOT_VAR2 = ${ROOT_VAR2}") # 子目录默认修改不会影响父目录
message("ROOT_VAR3 = ${ROOT_VAR3}") # 子目录使用 PARENT_SCOPE 修改父目录
include_example.cmake
# 修改父目录变量
set(ROOT_VAR1 "modified_by_include")
subdir/CMakeLists.txt
# 默认修改,不影响父目录
set(ROOT_VAR2 "modified_in_subdir")# 使用 PARENT_SCOPE 修改父目录
set(ROOT_VAR3 "modified_in_subdir_with_parent_scope" PARENT_SCOPE)
我们可以通过下面这个命令来一键搭建出这个环境
# 1. 创建目录结构
mkdir -p Project/subdir# 2. 创建根目录 CMakeLists.txt
cat > Project/CMakeLists.txt << 'EOF'
cmake_minimum_required(VERSION 3.10)
project(ParentVarExample)# 父目录变量
set(ROOT_VAR1 "root_value1")
set(ROOT_VAR2 "root_value2")
set(ROOT_VAR3 "root_value3")# ===== include 示例 =====
include(include_example.cmake)
message("===== Back in root after include =====")
message("ROOT_VAR1 = ${ROOT_VAR1}") # include 修改父目录变量# ===== add_subdirectory 示例 =====
add_subdirectory(subdir)
message("===== Back in root after add_subdirectory =====")
message("ROOT_VAR2 = ${ROOT_VAR2}") # 子目录默认修改不会影响父目录
message("ROOT_VAR3 = ${ROOT_VAR3}") # 子目录使用 PARENT_SCOPE 修改父目录
EOF# 3. 创建 include_example.cmake
cat > Project/include_example.cmake << 'EOF'
# 修改父目录变量
set(ROOT_VAR1 "modified_by_include")
EOF# 4. 创建 subdir/CMakeLists.txt
cat > Project/subdir/CMakeLists.txt << 'EOF'
# 默认修改,不影响父目录
set(ROOT_VAR2 "modified_in_subdir")# 使用 PARENT_SCOPE 修改父目录
set(ROOT_VAR3 "modified_in_subdir_with_parent_scope" PARENT_SCOPE)
EOF
接下来我们来构建这个项目
mkdir build && cd build && cmake ..
运行输出示意
我们发现ROOT_VAR2没有被修改,而另外两个都被修改过!!很容易理解了吧
✅ 总结
命令 / 变量 | 父目录变量对子目录可见吗? | 子目录修改会影响父目录吗? |
---|---|---|
| ✅ 可见 | ✅ 会影响(同一作用域) |
| ✅ 可见 | ❌ 默认不影响 |
| ✅ 可见 | ✅ 使用 |
这样就同时展示了三种情况:
-
include
可以修改父目录变量 -
add_subdirectory
默认修改不影响父目录 -
add_subdirectory
+PARENT_SCOPE
可以修改父目录变量
7.3.CMake内置路径变量
在学习CMake时,我们肯定会遇到一些路径变量,接下来我们就来讲解一下3个
- 1.
CMAKE_CURRENT_SOURCE_DIR
- 2.
CMAKE_CURRENT_LIST_FILE
- 3.
CMAKE_CURRENT_LIST_DIR
现在我们就接着include来讲解一下
假设目录结构如下:
project/
├── CMakeLists.txt
└── sub/├── sub.cmake├── sub2/│ └── sub2.cmake└── sub2/sub3/└── sub3.cmake
顶层 CMakeLists.txt
里写:
include(sub/sub.cmake)
sub.cmake
里又写:
include(sub2/sub2.cmake)
sub2.cmake
里再写:
include(sub3/sub3.cmake)
1. CMAKE_CURRENT_SOURCE_DIR
-
含义:
当前正在处理的 CMakeLists.txt 所在的源目录。
👉 在include
场景下,它不会随着.cmake
文件改变,而是始终保持为顶层CMakeLists.txt
的源目录。 -
用途:
如果你要相对顶层工程源目录定位文件,就用它。
✅ 示例:
在任意 .cmake
文件中打印:
message(STATUS "CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
输出:
CMAKE_CURRENT_SOURCE_DIR = /root/cmake/project
即使进入 sub3/sub3.cmake
,结果还是 /root/cmake/project
。
2. CMAKE_CURRENT_LIST_FILE
-
含义:
当前正在执行的 CMake 脚本的 完整文件路径。
👉 每次进入一个新的.cmake
文件,它都会更新为那个.cmake
文件。 -
用途:
用于调试,知道当前执行到哪一个.cmake
文件。
✅ 示例:
在 sub2/sub2.cmake
打印:
CMAKE_CURRENT_LIST_FILE = /root/cmake/project/sub/sub2/sub2.cmake
在 sub3/sub3.cmake
打印:
CMAKE_CURRENT_LIST_FILE = /root/cmake/project/sub/sub2/sub3/sub3.cmake
3. CMAKE_CURRENT_LIST_DIR
-
含义:
当前正在执行的.cmake
文件所在的目录。
👉 它和CMAKE_CURRENT_LIST_FILE
搭配使用,就是去掉文件名的部分。 -
用途:
在写include(${CMAKE_CURRENT_LIST_DIR}/xxx.cmake)
时非常常用,可以保证路径正确。
✅ 示例:
在 sub3/sub3.cmake
打印:
CMAKE_CURRENT_LIST_DIR = /root/cmake/project/sub/sub2/sub3
📊 小结对比表 (include 版)
变量 | 含义 | 在 |
---|---|---|
| 顶层 CMakeLists.txt 所在目录(不会随 include 变化) |
|
| 当前正在执行的 |
|
| 当前 |
|
👉 所以 关键区别:
-
在
add_subdirectory
里,CMAKE_CURRENT_SOURCE_DIR
会随着子目录进入而改变。 -
在
include
里,CMAKE_CURRENT_SOURCE_DIR
不会变,始终是顶层源目录;而CMAKE_CURRENT_LIST_FILE
/DIR
会随.cmake
文件而改变。
7.3.1.示例1
我们直接看例子
CMakeLists.txt
# 1 设置版本要求
cmake_minimum_required(VERSION 3.18)# 2 设置项目名称
project(TestInclude)# 3 打印 内置路径变量message(STATUS "from top-level CMakeLists.txt")
# 打印 当前正在执行的源代码目录--- 也就是CMakeLists.txt所在的目录
message(STATUS "CMAKE_CURRENT_SOURCE_DIR:" ${CMAKE_CURRENT_SOURCE_DIR})
# 打印 当前正在执行的cmake 脚本的 完整名称
message(STATUS "CMAKE_CURRENT_LIST_FILE:" ${CMAKE_CURRENT_LIST_FILE})
# 打印当前正在执行的cmake 脚本的 全目录
message(STATUS "CMAKE_CURRENT_LIST_DIR:" ${CMAKE_CURRENT_LIST_DIR})# 4 包含子目录cmake脚本
include(sub/sub.cmake)
sub/sub.cmake
message(STATUS "from sub/sub.cmake")
# 打印 当前正在执行的源代码目录--- 也就是CMakeLists.txt所在的目录
message(STATUS "CMAKE_CURRENT_SOURCE_DIR:" ${CMAKE_CURRENT_SOURCE_DIR})
# 打印 当前正在执行的cmake 脚本的 完整名称
message(STATUS "CMAKE_CURRENT_LIST_FILE:" ${CMAKE_CURRENT_LIST_FILE})
# 打印当前正在执行的cmake 脚本的 全目录
message(STATUS "CMAKE_CURRENT_LIST_DIR:" ${CMAKE_CURRENT_LIST_DIR})
我们仔细对比一下:
CMAKE_CURRENT_SOURCE_DIR:
这是当前正在处理的 CMakeLists.txt
文件所在目录的绝对路径。
对于include
无论您在 /root/cmake/test/CMakeLists.txt
中 include
了哪个目录下的文件,CMAKE_CURRENT_SOURCE_DIR
始终是 /root/cmake/test
。
被引入的文件中的代码就像直接写在主 CMakeLists.txt
中一样。
对于add_subdirectory
当您在 /root/cmake/test/CMakeLists.txt
中调用 add_subdirectory(sub)
后,CMake 会进入 sub
目录。
在该子目录的 CMakeLists.txt
中,CMAKE_CURRENT_SOURCE_DIR
的值就从 /root/cmake/test
变成了 /root/cmake/test/sub
。
7.3.2.示例2
📂 目录结构
project/
├── CMakeLists.txt
├── main.cpp
└── sub/├── sub.cmake└── sub2/├── sub2.cmake└── sub3/└── sub3.cmake
📄 project/CMakeLists.txt (顶层)
cmake_minimum_required(VERSION 3.18)
project(DemoInclude LANGUAGES CXX)message(STATUS ">>> [Top] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Top] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Top] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")add_executable(main main.cpp)# include sub.cmake
include(${CMAKE_CURRENT_SOURCE_DIR}/sub/sub.cmake)
📄 project/sub/sub.cmake
message(STATUS ">>> [Sub] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Sub] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Sub] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")# include sub2.cmake
include(${CMAKE_CURRENT_LIST_DIR}/sub2/sub2.cmake)
📄 project/sub/sub2/sub2.cmake
message(STATUS ">>> [Sub2] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Sub2] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Sub2] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")# include sub3.cmake
include(${CMAKE_CURRENT_LIST_DIR}/sub3/sub3.cmake)
📄 project/sub/sub2/sub3/sub3.cmake
message(STATUS ">>> [Sub3] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Sub3] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Sub3] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")
大家可以一键执行一下下面这个命令就能搭建出这么一个目录结构及其文件
# 1. 创建目录
mkdir -p project/sub/sub2/sub3# 2. 写 main.cpp
cat > project/main.cpp << 'EOF'
#include <iostream>
int main() {std::cout << "Hello from main.cpp" << std::endl;return 0;
}
EOF# 3. 写顶层 CMakeLists.txt
cat > project/CMakeLists.txt << 'EOF'
cmake_minimum_required(VERSION 3.18)
project(DemoInclude LANGUAGES CXX)message(STATUS ">>> [Top] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Top] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Top] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")add_executable(main main.cpp)# include sub.cmake
include(${CMAKE_CURRENT_SOURCE_DIR}/sub/sub.cmake)
EOF# 4. 写 sub/sub.cmake
cat > project/sub/sub.cmake << 'EOF'
message(STATUS ">>> [Sub] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Sub] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Sub] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")# include sub2.cmake
include(${CMAKE_CURRENT_LIST_DIR}/sub2/sub2.cmake)
EOF# 5. 写 sub/sub2/sub2.cmake
cat > project/sub/sub2/sub2.cmake << 'EOF'
message(STATUS ">>> [Sub2] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Sub2] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Sub2] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")# include sub3.cmake
include(${CMAKE_CURRENT_LIST_DIR}/sub3/sub3.cmake)
EOF# 6. 写 sub/sub2/sub3/sub3.cmake
cat > project/sub/sub2/sub3/sub3.cmake << 'EOF'
message(STATUS ">>> [Sub3] CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS ">>> [Sub3] CMAKE_CURRENT_LIST_FILE = ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS ">>> [Sub3] CMAKE_CURRENT_LIST_DIR = ${CMAKE_CURRENT_LIST_DIR}")
EOF
现在我们来编译一下
mkdir build && cd build && cmake ..
-
CMAKE_CURRENT_SOURCE_DIR
-
当前
CMakeLists.txt
所在的源目录。 -
它不会因为
include
变化而改变。 -
所以在顶层
CMakeLists.txt
里,它是/root/cmake/project
,到了sub.cmake
/sub2.cmake
/sub3.cmake
,依旧是 顶层的源目录/root/cmake/project
。
-
-
CMAKE_CURRENT_LIST_FILE
-
当前正在 执行的 CMake 脚本的完整路径。
-
每次进入
include(...)
,它会变成被include
的.cmake
文件的完整路径。 -
例如:
-
读到
sub.cmake
→/root/cmake/project/sub/sub.cmake
-
读到
sub2.cmake
→/root/cmake/project/sub/sub2/sub2.cmake
-
读到
sub3.cmake
→/root/cmake/project/sub/sub2/sub3/sub3.cmake
-
-
-
CMAKE_CURRENT_LIST_DIR
-
当前正在执行的 CMake 脚本所在的目录。
-
它和
CMAKE_CURRENT_LIST_FILE
配套:就是文件路径去掉文件名的部分。 -
例如:
-
sub.cmake
→/root/cmake/project/sub
-
sub2.cmake
→/root/cmake/project/sub/sub2
-
sub3.cmake
→/root/cmake/project/sub/sub2/sub3
-
-
🔑 总结 include
的特点
-
CMAKE_CURRENT_SOURCE_DIR
不会变:它始终指向最近的CMakeLists.txt
的目录。
在你这个例子里,只有一个顶层CMakeLists.txt
,所以所有.cmake
文件里看到的都是/root/cmake/project
。 -
CMAKE_CURRENT_LIST_FILE
和CMAKE_CURRENT_LIST_DIR
会变:
它们总是随着include
的文件而更新,精确告诉你 当前正在执行的.cmake
文件路径和所在目录。