目录

一.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.示例

4.1.基本用法示例

4.2.1.示例

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>...)

参数详解

参数

含义

说明

GLOB

非递归匹配

仅搜索当前目录下的文件,不包含子目录

GLOB_RECURSE

递归匹配

递归搜索当前目录及其所有子目录下的文件

<out-var>

输出变量

用于存储匹配到的文件列表的变量名

LIST_DIRECTORIES

目录处理

可选参数,设置为 true 时在结果中包含目录,默认为 false(仅包含文件)

RELATIVE <path>

相对路径

可选参数,使结果输出为相对于指定路径的相对路径

CONFIGURE_DEPENDS

配置依赖

可选参数,让CMake在构建时重新运行glob操作(慎用,可能影响性能)

<globbing-expressions>

通配符表达式

一个或多个匹配模式,如 *.cppsrc/**/*.h 等

使用示例

基本用法

# 收集当前目录下所有.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}")

我们仔细观察运行结果

  1. GLOB_RECURSE_CPP源文件: /root/cmake/project/build/CMakeFiles/3.28.3/CompilerIdCXX/CMakeCXXCompilerId.cpp;/root/cmake/project/main.cpp;/root/cmake/project/utils.cpp
  2. GLOB_RECURSE_头文件: /root/cmake/project/helper.h
  3. 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/" 这个路径的元素

让我们把它拆解开来理解:

  1. list(...)

    • 这是 CMake 的列表操作命令,用于对列表形式的变量进行各种操作,如排序、查找、插入、移除等。

  2. FILTER

    • 这是 list 命令的一个子操作,用于根据特定条件过滤列表中的元素。

  3. ALL_FILES

    • 这是要被操作的目标列表变量。在这个例子中,它是通过 file(GLOB_RECURSE ALL_FILES "*") 获得的,包含了递归找到的所有文件的路径。

  4. EXCLUDE

    • 这是与 FILTER 搭配使用的模式,意思是“排除”。它表示移除所有符合后面条件的元素。

    • 与之相对的是 INCLUDE,意思是“保留”所有符合条件的元素,排除其他所有。

  5. 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})

第一次构建(一切正常)

  1. 你运行 cmake -B build 来配置项目。

  2. CMake 执行 file(GLOB SOURCES "src/*.cpp"),发现 src/main.cpp 和 src/old_file.cpp

  3. 变量 SOURCES 的值被设置为 src/main.cpp;src/old_file.cpp

  4. CMake 生成构建系统(如 Makefile),其中包含编译这两个文件的规则。

  5. 你运行 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 中

结果可能是:

  1. 如果 new_file.cpp 中的代码未被调用:构建可能会成功,但你的新功能根本没有被包含进程序中,导致运行时行为不符合预期。这是一个非常隐蔽的 bug。

  2. 如果 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 的一个布尔变量(取值可以是 ONOFF)。

  • 作用:控制 add_library() 在未指定库类型时(没有写 STATICSHAREDOBJECTINTERFACE)的默认行为。

👉 换句话说:

add_library(mymath my_math.cpp)

这里没有写 STATICSHARED,那么生成的是静态库还是动态库,就由 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

  • "描述信息" → 在 ccmakecmake-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,可以独立于默认设置选择是追加还是前置目录。

INTERFACEPUBLIC 和 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: 下面的一系列目录,这些就是它实际使用的搜索路径。

  1. /usr/include/c++/11:C++ 标准库头文件目录

  2. /usr/include/x86_64-linux-gnu/c++/11:针对特定架构(x86_64)的 C++ 标准库头文件

  3. /usr/include/c++/11/backward:兼容旧版本的头文件目录(提供一些已废弃的头文件)

  4. /usr/lib/gcc/x86_64-linux-gnu/11/include:GCC 自带的内部头文件目录(如 xmmintrin.h 等 SIMD intrinsic)

  5. /usr/local/include:系统管理员或用户通过 make install 安装的库的头文件通常会放在这里

  6. /usr/include/x86_64-linux-gnu针对特定架构的系统头文件

  7. /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)

🔍 各部分的含义

  1. target_include_directories

    • 给某个 target(目标,比如可执行程序或库)指定头文件搜索路径

    • 作用就是给编译器加上 -I 选项

  2. app

    • 这是目标的名字

    • 对应你上面定义的:

      add_executable(app main.cpp)
      
    • 所以这里加的路径只对 app 这个可执行程序生效

  3. PRIVATE

    • 可见性修饰符,控制路径的传播范围:

      • PRIVATE → 只在 app 自己编译时使用

      • PUBLIC → 自己用 + 链接到 app 的目标也能继承

      • INTERFACE → 自己不用,只有别人链接时能继承

    这里用 PRIVATE,意思是“这条 include 路径只给 app 自己用,不传递给其他 target”。

  4. ${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 ...)(库和依赖都会有, addPUBLIC 会传递给 main

  • interface 来自 target_include_directories(add INTERFACE ...)(只传递给依赖的 main,因为 addINTERFACE 会传递给 main


✅ 总结

  • 你日志里的 -I... 就是编译器头文件搜索路径

  • add.cpp 的编译命令 → private + public

  • main.cpp 的编译命令 → public + interface

  • PRIVATE只在库自身用(所以只在编译 add.cpp 时出现)

  • PUBLIC自己能用 + 链接到它的 target 也能用(所以 add.cppmain.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 会:

  1. 检查 main 所依赖的所有目标(这里是 MyLib

  2. 收集这些目标的 INTERFACE_INCLUDE_DIRECTORIES 属性中的所有包含目录

  3. 将这些目录自动添加到 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)

传递过程详解:

  1. CMake 发现 Calculator 链接了 MathLib

  2. 它检查 MathLib 的 INTERFACE_INCLUDE_DIRECTORIES 属性

  3. 发现该属性包含 /absolute/path/to/mathlib/include

  4. 将这个路径添加到 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_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)

这样:

  1. add.cpp 里的函数被编译为 libadd.a

  2. main.cpp 会链接上 libadd.a 和系统库 pthread

  3. 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库。

这三个关键字定义了 库链接关系的传播范围,即:

谁需要链接哪些库。

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 阶段看不到)

首先要明确一个关键点:每个属性都属于一个特定的 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  # 链接线程库,并向外暴露此依赖
)

传递过程详解:

  1. 处理 MathLib 依赖

    • 将 MathLib 添加到 Calculator 的 LINK_LIBRARIES 属性

    • 由于是 PRIVATE,不传播 MathLib 的接口依赖,也就是说不往INTERFACE_LINK_LIBRARIES里面写东西

  2. 处理 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 模块 时。


🧠 小结对比

变量

含义

示例(在 sub/CMakeLists.txt

CMAKE_CURRENT_SOURCE_DIR

当前 CMakeLists.txt 所在源代码目录

/root/cmake/project/sub

CMAKE_CURRENT_LIST_FILE

当前正在执行的 CMakeLists.txt 文件的完整路径

/root/cmake/project/sub/CMakeLists.txt

CMAKE_CURRENT_LIST_DIR

当前 CMakeLists.txt 文件所在的目录

/root/cmake/project/sub

注意:

  • CMAKE_CURRENT_SOURCE_DIRCMAKE_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. includeadd_subdirectoryCMAKE_CURRENT_SOURCE_DIR 的影响

include(<file>)

  • 行为:把指定的 CMake 脚本“粘贴”到当前位置执行,就像宏展开。

  • 目录变量:不会改变当前目录。

    • CMAKE_CURRENT_SOURCE_DIRCMAKE_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_DIRCMAKE_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 改变吗

子目录变量影响父目录

include

会影响

add_subdirectory

默认不影响,需要 PARENT_SCOPE

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.txtinclude() 进来。

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 ..

控制台输出

效果总结

  1. include()

    • 子脚本中定义的变量在父目录可见(INCLUDED_VAR)。

  2. add_subdirectory()

    • 子目录变量默认不可见(SUBDIR_VAR)。

    • 可以用 PARENT_SCOPE 传给父目录(SUBDIR_VAR_PARENT)。

7.2.2.示例2——根目录的变量的作用域

我给你整理一个完整示例,包含三个变量:

  • ROOT_VAR1include 修改父目录

  • ROOT_VAR2add_subdirectory 默认修改不会影响父目录

  • ROOT_VAR3add_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()ROOT_VAR1

✅ 可见

✅ 会影响(同一作用域)

add_subdirectory()ROOT_VAR2

✅ 可见

❌ 默认不影响

add_subdirectory()ROOT_VAR3

✅ 可见

✅ 使用 PARENT_SCOPE 影响父目录


这样就同时展示了三种情况:

  1. include 可以修改父目录变量

  2. add_subdirectory 默认修改不影响父目录

  3. 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 版)

变量

含义

sub3/sub3.cmake 的输出

CMAKE_CURRENT_SOURCE_DIR

顶层 CMakeLists.txt 所在目录(不会随 include 变化)

/root/cmake/project

CMAKE_CURRENT_LIST_FILE

当前正在执行的 .cmake 文件完整路径

/root/cmake/project/sub/sub2/sub3/sub3.cmake

CMAKE_CURRENT_LIST_DIR

当前 .cmake 文件所在目录

/root/cmake/project/sub/sub2/sub3


👉 所以 关键区别

  • 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_FILECMAKE_CURRENT_LIST_DIR 会变
    它们总是随着 include 的文件而更新,精确告诉你 当前正在执行的 .cmake 文件路径和所在目录

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/94743.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/94743.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/94743.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【50页PPT】钢铁企业数字化工厂解决方案需求要点(附下载方式)

篇幅所限&#xff0c;本文只提供部分资料内容&#xff0c;完整资料请看下面链接 https://download.csdn.net/download/2501_92796370/91716817 资料解读&#xff1a;钢铁企业数字化工厂解决方案需求要点 详细资料请看本解读文章的最后内容 钢铁行业数字化转型背景与意义 当…

Java深拷贝与浅拷贝核心解析

Java深拷贝与浅拷贝的概念浅拷贝&#xff08;Shallow Copy&#xff09;只复制对象的引用&#xff0c;而不复制对象本身。拷贝后的对象和原对象共享同一块内存地址中的子对象。修改其中一个对象的非基本类型属性时&#xff0c;另一个对象的对应属性也会被修改。深拷贝&#xff0…

DBeaver 的 PostgreSQL 驱动包默认存储位置

在 Windows 系统中&#xff0c;DBeaver 的 PostgreSQL 驱动包&#xff08;JDBC 驱动 JAR 文件&#xff09;默认存储位置如下&#xff1a; ###&#x1f50d; 默认驱动安装路径 C:\Users\你的用户名\AppData\Roaming\DBeaverData\drivers说明&#xff1a;你的用户名&#xff1a;…

大数据毕业设计选题推荐:基于北京市医保药品数据分析系统,Hadoop+Spark技术详解

&#x1f34a;作者&#xff1a;计算机毕设匠心工作室 &#x1f34a;简介&#xff1a;毕业后就一直专业从事计算机软件程序开发&#xff0c;至今也有8年工作经验。擅长Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等。 擅长&#xff1a;按照需求定制化开发项目…

Package.xml的字段说明

package.xml 的版本说明 <package format"2"></package>每一个 package.xml 都以作为 root 标签&#xff0c;其中 format 代表版本,现在主要是版本 2 为主,与版本 1 之间的差别主要是一些子标签, package.xml 的必备标签 name:功能包名 version:版本号。…

JAVA【抽象类】和【接口】

在面向对象编程中&#xff0c;接口&#xff08;Interface&#xff09;和抽象类&#xff08;Abstract Class&#xff09;都是用于实现抽象化的机制&#xff0c;但它们在设计目的、语法规则和使用场景上有显著区别。以下是它们的核心区别&#xff1a; 1. 定义与关键字接口&#x…

Mysql系列--11、使用c/c++访问mysql服务

目录 一、准备 测试 二、创建对象 三、连接Mysql服务 四、下达指令 3.1增删改 增加 编码格式 删除 修改 3.2查询结果 结构体理解 打印属性 打印数据 前面我们已经学习并练习了本地命令行形式的sql语句的使用&#xff0c;可在以后开发中我们一般 不会直接命令行操作数据库&…

CS144 lab3 tcp_sender

0. 前言 这个实验做了挺久的&#xff0c;刚开始做的时候官方的代码库还是开着的。 调着调着代码官方把仓库给删掉了&#xff0c;又去找别人的代码仓库调发现不 对都打算放弃了&#xff0c;过了几天发现了一个start-code的库 再合进去简直完美。这个实验花的时间应该是前四个里面…

华为HCIP数通学习与认证解析!

大家好&#xff0c;这里是G-LAB IT实验室。在信息技术飞速发展的今天&#xff0c;随着华为产品和服务的广泛应用&#xff0c;成为一名华为数通工程师无疑是许多年轻从业者的目标。然而&#xff0c;对于许多人来说&#xff0c;面对令人眼花缭乱的华为认证体系&#xff0c;不禁要…

深度学习入门Day10:深度强化学习原理与实战全解析

一、开篇&#xff1a;智能决策的科学与艺术在前九天的学习中&#xff0c;我们掌握了处理各种数据类型的深度学习方法&#xff0c;但这些都属于"被动学习"——模型从静态数据中学习模式。今天&#xff0c;我们将进入一个全新的领域&#xff1a;强化学习&#xff08;Re…

Jenkins Pipeline(二)-设置Docker Agent

设计流水线的目的是更方便地使用 Docker镜像作为单个 Stage或整个流水线的执行环境。 1.安装必要插件 在Jenkins服务器上已经安装了插件。 Docker PipelinePipeline Maven IntegrationPipeline Maven Plugin API 如果插件缺少什么&#xff0c;再次检查并安装即可。 2. 配…

神经网络|(十六)概率论基础知识-伽马函数·中

【1】引言 前序学习进程中&#xff0c;已经初步了解了伽马函数&#xff0c;认识到nnn的阶乘计算可以转化为&#xff1a; n!n!⋅limk→∞kn⋅k!(nk)!limk→∞kn⋅k!⋅n!(nk)!limk→∞kn⋅k!(n1)(n2)...(nk)n!n! \cdot lim_{k\rightarrow\infty}\frac{k^n\cdot k!}{(nk)!}\\lim_…

设计模式Books Reading

文章目录 设计模式 创建型设计模式 工厂方法 示例说明 工厂方法模式结构 案例伪代码 工厂方法模式适合应用 实现方式 工厂方法模式优缺点 与其他模式的关系 概念示例 抽象工厂 抽象工厂模式结构 抽象工厂模式适合应用场景 实现方式 抽象工厂模式优缺点 与其他模式的关系 代码示…

接吻数问题:从球体堆叠到高维空间的数学奥秘

本文由「大千AI助手」原创发布&#xff0c;专注用真话讲AI&#xff0c;回归技术本质。拒绝神话或妖魔化。搜索「大千AI助手」关注我&#xff0c;一起撕掉过度包装&#xff0c;学习真实的AI技术&#xff01; 1 接吻数问题概述 接吻数问题&#xff08;Kissing Number Problem&am…

深度学习③【卷积神经网络(CNN)详解:从卷积核到特征提取的视觉革命(概念篇)】

文章目录先言1. 卷积核&#xff1a;特征检测的魔法窗口1.1什么是卷积核&#xff1a;可学习的特征检测器1.2可视化理解&#xff1a;边缘检测、纹理提取、特征发现1.3代码实现&#xff1a;使用PyTorch定义和初始化卷积层2. 卷积运算的数学原理2.1.离散卷积计算&#xff1a;滑动窗…

当不想安装telnet或nc时,可使用 Linux 系统默认自带的bash原生网络功能或ping(辅助判断)测试连通性

1. 用bash原生/dev/tcp测试端口&#xff08;无需任何工具&#xff09;bashshell 内置了/dev/tcp虚拟设备&#xff0c;可直接通过脚本测试端口是否能连接&#xff0c;执行以下命令&#xff08;替换数据库 IP 和端口&#xff09;&#xff1a;# 格式&#xff1a;echo > /dev/tc…

【STM32外设】ADC

声明&#xff1a;上图是STM32产品型号各字段含义&#xff0c;本文基于STM32F103 1、ADC的一些概念 常规通道(常规组)和注入通道(注入组)&#xff08;regular channels and injected channels&#xff09;ADC支持的外部通道总共16个&#xff08;且被3个ADC共享&#xff0c;ADC12…

Aha Moment——啊哈时刻!

1. 理解面试官的意图面试官问你“Aha moment”&#xff0c;其实是想知道&#xff1a;你是否真正理解这个概念&#xff1a;不只是背定义&#xff0c;而是理解其为什么重要。你如何发现它&#xff1a;考察你的数据分析方法论和技术能力&#xff08;用了哪些数据、指标、模型&…

RAG教程5:多表示索引和ColBERT

文章目录 导入依赖包 多表示索引 ColBERT 导入依赖包 %pip install youtube-transcript-api pytube多表示索引 from langchain_community.document_loaders import WebBaseLoader from langchain_text_splitters import RecursiveCharacterTextSplitterloader = WebBaseL

来自火山引擎的 MCP 安全授权新范式

资料来源&#xff1a;火山引擎-开发者社区 本文旨在深入剖析火山引擎 Model Context Protocol (MCP) 开放生态下的 OAuth 授权安全挑战&#xff0c;并系统阐述火山引擎为此构建的多层次、纵深防御安全方案。面对由 OAuth 2.0 动态客户端注册带来的灵活性与潜在风险&#xff0c;…