现代 CMake 项目构建完全指南:从基础配置到高级技巧的目标属性管理与智能依赖传递机制解析

文章目录

本篇摘要

本文系统讲解CMake核心命令,涵盖目标创建、属性管理、依赖传递及文件收集。通过add_librarytarget_include_directories等命令实现模块化构建,详解PUBLIC/PRIVATE/INTERFACE关键字的依赖传递机制,并演示file(GLOB/GLOB_RECURSE)自动化文件收集技巧。

cmakeAPI及属性传递常见命令

add_library 命令

  1. 核心功能:创建库目标

    add_library 命令的核心作用是指示 CMake 创建一个库文件构建目标,这是管理库构建的起点。

  2. 关键操作:命名库

    必须通过 <name> 参数为要创建的库指定一个项目内唯一 的名称(如 add_library(my_lib ...))。CMake 会自动根据平台添加标准前缀和后缀(如 libmy_lib.amy_lib.lib)。

  3. 关键操作:选择库类型

    通过指定关键字来选择生成的库类型,这是决定库行为的关键操作:

  • STATIC :创建静态库.a.lib),库代码会直接嵌入到最终的可执行文件中。
  • SHARED :创建动态库/共享库.so.dll),库代码在程序运行时才被加载,可被多个程序共享。
  • MODULE :创建模块库(类似插件),运行时动态加载,但通常不参与链接。
  1. 基础操作:指定源文件

    必须在命令中提供构建该库所需的源文件列表 (如 add_library(my_lib file1.cpp file2.cpp))。这是生成库的原材料。

  2. 辅助操作:控制构建范围

    使用 EXCLUDE_FROM_ALL 选项可以控制该库是否在默认构建(如直接运行 make)时被编译。通常用于那些需要特定条件才编译的辅助库或测试库。

  3. 后续操作:定制输出位置

    库文件默认生成在 CMake 当前所在的构建目录中。之后可以通过设置以下目标属性来灵活地更改输出路径:

  • ARCHIVE_OUTPUT_DIRECTORY :主要控制静态库.a/.lib)的输出目录。
  • LIBRARY_OUTPUT_DIRECTORY :主要控制动态库.so/.dll)的输出目录。
  • RUNTIME_OUTPUT_DIRECTORY:对于某些平台(如 Windows),DLL 有时被视为运行时组件,也可用此属性控制。

一句话: add_library 命令的操作流程是:创建目标 -> 命名 -> 选择类型 -> 指定源码 -> (可选)排除默认构建 ,后续再通过其他命令定制输出位置

下面看下官方定义的库的选项:

对应地址:官网文档

下面简单介绍下常见的选项库:

库类型 核心用途 一句话用法
普通库 (Normal) 构建项目内部的静态库(STATIC)或动态库(SHARED) add_library(my_lib STATIC src.cpp) 把 src.cpp 编译成 my_lib.a;相当于在makefile里加了-I -L 等选项,供用户make
对象库 (Object) 只编译不链接,生成 .o 文件供其他目标使用 add_library(my_obj OBJECT src.cpp) 生成 my_obj.o,给别人链接
接口库 (Interface) 不编译代码,只定义头文件路径等依赖供其他目标使用 add_library(my_interface INTERFACE) 然后设置 INTERFACE 属性给别人用;用来约束消费者的;自己不用
导入库 (Imported) 引用一个外部已存在的预编译库(第三方库) add_library(third_lib IMPORTED) 然后设置 IMPORTED_LOCATION 指向已有的 libthird.so;比如使用内存对象来引用磁盘上存在的库等
别名库 (Alias) 给一个已存在的库目标起一个简短的别名或代号 add_library(alias_name ALIAS original_lib) 之后可以用 alias_name 代替 original_lib

总结:

  • 自己编译代码 生成库,用 STATIC/SHARED (普通库)或 OBJECT(对象库)。
  • 管理依赖和接口 而不编译,用 INTERFACE(接口库)。
  • 使用别人编译好的库 ,用 IMPORTED(导入库)。
  • 想给库换个短名字 方便调用,用 ALIAS(别名库)。

2.target_include_directories 命令

  1. 核心作用

    用于为指定的构建目标(<target>)设置头文件(.h)的搜索路径 。这些路径会在编译时通过 -I 参数传递给编译器(如 gcc)。

  2. 关键特性:依赖传递

    通过 PUBLICPRIVATEINTERFACE 关键字(这里要知道 PUBLIC=PRIVATE+INTERFACE 根据它三设置目标对应的 INCLUDE_DIRECTORIESINTERFACE_INCLUDE_DIRECTORIES的属性变量值),可以将头文件路径自动传递给 链接该目标的其他目标,避免了手动管理全局路径的混乱。

  3. 基本语法形式

    cmake 复制代码
    target_include_directories(<target>
        <INTERFACE|PUBLIC|PRIVATE> <path1> [<path2> ...]
        ...
    )
  4. 重要可选参数

  • SYSTEM :告知编译器该路径是系统头文件路径 ,编译器会使用 -isystem 而非 -I 来抑制第三方库的编译警告。
  • BEFORE :将新路径插入 到现有列表的最前面
  1. 路径解析规则

    如果提供的 path相对路径 ,则其解析基准是当前 CMakeLists.txt 文件所在的目录(CMAKE_CURRENT_SOURCE_DIR)。

  2. 最佳实践

    文档明确推荐使用 target_include_directories 而非旧的 include_directories 命令,因为后者是全局设置,容易导致路径被错误地添加到其他不相关的目标中,造成污染和冲突。

一句话: 此命令是现代 CMake 管理头文件依赖的核心方法,它能精确、可控地为每个目标设置搜索路径,并能自动传递给下游使用者,是替代旧式全局设置的最佳实践。

下面举个例子解释下:

  • 这里我们在构建一个动态库的时候;表明了它是公有的也就是自己可以用;并且下游的使用者也可以用(库以及一些接口文件目录等);当构建库(表明构建树)的时候查找文件去指定当前执行cmake命令的目录对应的去找。
  • 当这个库被其他用户安装使用后,然后去找这个库依赖的文件就去安装路径找(安装树/usr/local/include);然后完成库的正常使用。
  1. 核心作用

    用于为一个构建目标(库或可执行程序)指定它需要链接的依赖库列表。这是管理项目依赖关系的核心命令。

  2. 底层原理

    该命令的底层操作等同于使用 set_target_properties 设置目标的以下两个属性:

  • LINK_LIBRARIES :该目标自身 需要链接的库(相当于PRIVATE)。
  • INTERFACE_LINK_LIBRARIES :需要传递给 其他依赖此目标的目标的库(相当于interface)。
  • 两个都设置上相当于PUBLIC
  1. 最终效果

    这些被指定的库会最终转换为编译器(如 GCC)的 -l 链接器选项,从而在链接阶段被正确找到并链接。

  2. 关键特性:依赖传递 (PUBLIC/PRIVATE/INTERFACE)

    通过三个关键字精确控制依赖的传播范围,这是现代 CMake 的精髓:

  • PRIVATE :依赖库仅用于当前目标自身的链接,不传播给使用者。
  • INTERFACE :依赖库不用于当前目标自身 ,但要求所有使用者必须链接它们。
  • PUBLIC :依赖库既用于当前目标自身 ,也传播给所有使用者
  1. 基本语法形式

    cmake 复制代码
    target_link_libraries(<目标名>
        <PRIVATE|PUBLIC|INTERFACE> <库名>...
        [<PRIVATE|PUBLIC|INTERFACE> <库名>...]...
    )

一句话:

target_link_libraries 是告诉 CMake " 需要链接哪些库 ",并通过 PUBLIC/PRIVATE/INTERFACE 关键字智能地控制这些依赖关系如何传递,最终生成正确的链接器命令。

下面演示下效果:

对应测试目录:

bash 复制代码
├── CMakeLists.txt
├── main.cpp
└── src
    ├── add.cpp
    └── CMakeLists.txt

对应add.cpp这里搞成空文件;可能报错;为了演示这个效果。

src里面的cmakeLists.txt文件:

顶层cmakeLists.txt文件:

下面走到构建目录cmake ..然后 --build -v查看下构建目标详细:

  • 这里可以看到当add静态库制作的时候执行make就根据属性去对应的头文件目录去搜索了(PRIVATE+PUBLIC的),也就是 - I;又因为是静态库故链接库文件搜索路径就没有去。
  • 对于main中加载add静态库成elf的时候;因为add属性传递(PUBLIC+INTERFACE);再次去搜索头文件和库文件(因为这里是属性传递;只要是符合这俩要求都传递;故库文件路径也去搜索了),也就是 -I -L

因此总结下:

对于属性传递:如果是类似于main文件需要加载或者链接库add;此时就相当于加载/链接库本身+add库的一些属性接口传递+add依赖的库被设置进来的一些属性传递的接口等;最后这些关系都转化成makefile里的 gcc -I -L等。

4. set_target_propertiesget_target_property 命令

  1. 核心作用

    这两个命令是目标属性管理 的核心。set_target_properties 用于设置或修改 一个或多个目标的属性,而 get_target_property 用于查询某个目标的特定属性值。

  2. 操作对象

    操作的对象是目标(Target) ,即通过 add_executableadd_library 创建的可执行文件或库。

  3. 基本语法形式

  • 设置属性

    cmake 复制代码
    set_target_properties(target1 target2 ...
        PROPERTIES
            prop1 value1
            prop2 value2
    )
  • 获取属性

cmake 复制代码
 get_target_property(<out_var> <target> <property>)
  1. 属性分类(关键概念)

    属性按其用途主要分为两大类,这是现代 CMake 的精髓:

  • 构建规范 (Build Specification) :目标自身构建 时所需的属性(如 INCLUDE_DIRECTORIES, LINK_LIBRARIES)。
  • 使用要求 (Usage Requirements) :目标被其他目标使用时,传递给使用者 的属性(如 INTERFACE_INCLUDE_DIRECTORIES, INTERFACE_LINK_LIBRARIES)。
  1. 常见属性示例
  • 路径控制LIBRARY_OUTPUT_DIRECTORY (库输出路径)、ARCHIVE_OUTPUT_DIRECTORY (静态库输出路径)。
  • 名称控制OUTPUT_NAME (控制输出文件名)。
  • 依赖管理LINK_LIBRARIES (需要链接的库列表)。
  • 运行时路径BUILD_RPATH (构建目录中的运行时库搜索路径)、INSTALL_RPATH (安装后的运行时库搜索路径)。
  1. 与编译器选项的映射

    许多属性在底层会转换为具体的编译器选项,例如:

  • INCLUDE_DIRECTORIES --> -I
  • LINK_LIBRARIES -->-l
  • BUILD_RPATH -->-Wl,-rpath

总结: 这两个命令提供了直接、底层的方式来精细控制每个目标的构建行为,是实现高级构建配置的基础。通过设置不同的属性,可以控制目标的输出位置、名称、依赖关系以及如何与编译器和链接器交互,也就是我们可以通过设置目标的属性来减少一些命令的书写;与直接给它设在属性里是等同的。

下面演示下:

对应目录:

bash 复制代码
├── CMakeLists.txt
├── main.cpp
└── src
    ├── add.cpp
    └── CMakeLists.txt

这里只改动下底层cmakeLists文件:

cpp 复制代码
add_library(add  SHARED add.cpp)


set_target_properties(add PROPERTIES 

    COMPILE_OPTIONS "-g"
    COMPILE_OPTIONS "-O3"
    COMPILE_OPTIONS "-fPIC"
    # 类似于target_include_directories
    INCLUDE_DIRECTORIES "/public"
    INTERFACE_INCLUDE_DIRECTORIES "/interface"

    # 私有链接+接口路径 类似于target_link_directories  target_link_libraries
     LINK_DIRECTORIES "/public"
    INTERFACE_LINK_DIRECTORIES "/interface"
    LINK_LIBRARIES "curl"
    INTERFACE_LINK_LIBRARIES "jsoncpp"

    # 输出路径设置(相对与顶层cmakeLists.txt)
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
    ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
    LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"

    # 设置参数
    BUILD_RPATH "${CMAKE_BINARY_DIR}/lib"#告诉编译器链接这个动态库的时候去这个路径搜索这个动态库
    INSTALL_RPATH "lib" #//usr/local/lib 没有/就是相对cmake_install_prefix
    OUTPUT_NAME "add"
    VERSION "1.2.3"
    SOVERSION "20"

)

然后进行cmake ..完成makefile制作再cmake+build;构建对应目标:

  • 这里可以看到对应的add是动态库,因此可以看到对应的-L等。
  • 这里关于INTERFACE的设置;因此在main被传递过来的时候就去搜索对应头文件以及去链接目录搜索及链接对应库。
  • 可以看出对应main程序进行链接的add动态库过程就根据设置进add里面的build_rpath属性对应路径去搜索这个库了。
  • 对应的库的版本号以及对应生成库的输出位置都是在cmake中设置进库的属性的输出路径一样的。

注:

  • 对应设置属性的时候不能对别名设置(比如给target起了个名字;用alias);应该对它本身设置。
  • 其次就是如果设置编译时候属性的时候,complies_options需要分开写来防止报错。
  • 属性设置基于cmake;落实于gcc工作阶段。

5. add_subdirectory与include 命令

  1. 核心作用

    用于将子目录 添加到构建系统中。执行该命令后,CMake 会立即暂停 当前 CMakeLists.txt 的处理,转而进入指定的子目录并执行其内的 CMakeLists.txt 文件,待子目录处理完毕后,再返回继续执行后续命令。

  2. 基本语法

    cmake 复制代码
    add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])
  • source_dir必需 。子目录名(相对于当前 CMakeLists.txt 所在目录)。
    binary_dir可选 。指定子目录的构建输出路径(相对于当前构建目录)。如不指定,则默认与 source_dir 同名。
  1. 变量作用域规则
  • 局部性 :在子目录中定义的普通变量默认是局部的 比如set(key "value"),不会影响父目录的变量;但是比如add_librarise add_executable等设置属性时候就是全局设置的。
  • 向上传递 :可通过 set(var PARENT_SCOPE) 显式将变量传递到父目录作用域。
  • 全局性缓存变量(Cache Variables)是全局的,在子目录中设置,父目录也可获取。
  1. 内置变量变化

    当 CMake 进入子目录执行时,以下"CURRENT"系列内置变量的值会自动改变,以反映新的上下文:

  • CMAKE_CURRENT_SOURCE_DIR -> 指向子目录的源路径
  • CMAKE_CURRENT_BINARY_DIR -> 指向子目录的构建输出路径
  • CMAKE_CURRENT_LIST_FILE -> 指向子目录的 CMakeLists.txt 文件
  • CMAKE_CURRENT_LIST_DIR -> 指向子目录的 CMakeLists.txt 所在目录
  1. 路径基准
  • source_dir 的路径解析基准是当前 CMakeLists.txt 文件所在的目录
  • binary_dir 的路径解析基准是当前的构建目录
  1. 最佳实践建议

    为了获得相对于正在执行的 CMake 脚本文件 的路径,建议统一使用 CMAKE_CURRENT_LIST_DIR 作为参考基准,这在 add_subdirectoryinclude 命令中均适用。

add_subdirectory与include区别对比

  1. 核心行为差异
  • add_subdirectory (新建) :进入一个独立的子项目/模块,拥有自己的源目录和构建目录(类似进入一个新的作用域执行)。

  • include (复用) :将另一个 .cmake 脚本文件的内容插入到当前上下文中执行,不改变当前项目的源和构建目录上下文(类似于文件直接展开在当前cmakeLists.txt文件)。

  1. 目录路径变量 (CMAKE_CURRENT_SOURCE_DIR, CMAKE_CURRENT_BINARY_DIR)
  • add_subdirectory会变化 。指向子目录对应的源路径和构建路径。
  • include不变化 。保持指向父目录的源路径和构建路径。
  1. 列表文件变量 (CMAKE_CURRENT_LIST_FILE, CMAKE_CURRENT_LIST_DIR)
  • 两者都会变化 。均指向当前正在被处理的脚本文件 (子目录的 CMakeLists.txt)及其所在目录。
  1. 设计目的与用途
  • add_subdirectory :用于管理大型项目的模块化子组件(如一个独立的库或应用),每个子目录通常是一个相对独立的构建目标。
  • include :用于代码复用和模块化配置 ,通常用于包含函数定义、宏、或通用的配置片段(.cmake 脚本文件)。

一句话:

  • add_subdirectory添加一个需要独立构建的子项目(会改变源和构建目录)。
  • include插入并执行一段通用的配置脚本(不改变源和构建目录,只改变当前执行的文件上下文)。
  • 因此在这俩各种如果想统一;就可以选一个基准点如:CMAKE_BINARY_DIR;这个无论在这俩哪里都是基于顶层cmakeLists.txt为基准构建的。

区别验证

下面验证下:

就分别打印上面说的四个变量:

顶层cmake文件与子目录的底层cmake文件:

底层cmake文件:

顶层cmake文件:

  • 这里设置了对应子目录的cmake文件的目标构建输出目录为sub的build。

下面运行看下结果:


  • 符合上面说的预期。

顶层cmake文件与include的cmake脚本:

顶层cmake文件:

子目录cmake脚本:

效果:

  • 也是符合上面的预期。

6.file 命令

  1. 核心作用

    用于在构建时 (configure-time)匹配指定模式的文件 ,并将匹配到的文件路径列表存入一个变量中。常用于快速收集源文件或资源文件

  2. 两种模式

  • GLOB非递归 。仅匹配当前目录下符合模式的文件。
  • GLOB_RECURSE递归 。匹配当前目录及其所有子目录下符合模式的文件。
  1. 基本语法形式

    cmake 复制代码
    file(GLOB <out_var> [CONFIGURE_DEPENDS] <pattern1> [<pattern2> ...])
    file(GLOB_RECURSE <out_var> [CONFIGURE_DEPENDS] <pattern1> [<pattern2> ...])
  2. 关键参数

  • <out_var>必需 。用于存储匹配结果的文件列表变量(如 MY_FILES)。
  • <pattern>必需 。通配符表达式(如 *.cpp, src/**/*.h)。** 表示递归任何子目录。
  • [CONFIGURE_DEPENDS]可选推荐添加。让 CMake 在每次构建时重新检查文件列表是否有变化(如新增文件),从而自动重新运行配置,避免因文件系统变动导致构建过期。
  1. 注意事项(重要)
  • 慎用 :官方文档通常建议显式列出源文件 ,而非使用 GLOB。因为如果漏掉新文件,CMake 不会自动检测到(除非加 CONFIGURE_DEPENDS),可能导致构建错误。
  • 用途 :更适合用于收集非核心构建依赖的文件,如资源文件、测试文件等。

一句话: file(GLOB) 是一个方便的"文件收集器",但需谨慎使用,添加 CONFIGURE_DEPENDS 选项可提升其可靠性。

下面演示下:

测试目录:

bash 复制代码
├── c.cpp
├── CMakeLists.txt
└── sub
    ├── a.cpp
    └── a.js

下面我们使用递归模式的搜索当前目录的js与cpp文件保存在变量中;然后打印出来看看效果:

  • 这里加上了自动检测标志:CONFIGURE_DEPENDS。

效果:

  • 符合预期。

本篇演示代码

点我查看代码示例

本篇小结

本篇将带你掌握现代CMake构建系统的核心命令与属性管理,能够高效实现项目模块化与自动化依赖管理。通过目标属性精确控制构建行为,利用依赖传递简化多模块协作,使用文件收集优化资源管理,显著提升C++项目的工程化水平与维护性。

相关推荐
Theliars6 天前
Ubuntu 上使用 VSCode 调试 C++ (CMake 项目) 指南
c++·vscode·ubuntu·cmake
Molesidy18 天前
【Embedded System】【CMake】Windows下CMake+VSCode的开发环境搭建以及初步认识
ide·vscode·编辑器·cmake
fedorayang23 天前
precompilation-headers 以及在cmake中的实现
cmake
AAA小肥杨25 天前
cmake使用教程
c语言·c++·cmake
辰尘_星启25 天前
『CMake』关于使用CMake构建项目时的现代/传统指令
c++·架构·系统·cmake·项目·构建
安全二次方security²1 个月前
TF-A CMake构建系统
编译·cmake·atf·tf-a·arm安全架构·构建系统
玩转C语言和数据结构1 个月前
CMake下载和安装图解(附安装包,适合新手)
cmake·cmake下载·cmake安装·cmake 下载·cmake 安装·安装cmake·cmake下载安装
周之鸥1 个月前
Qt 项目国际化从零到一:用 Qt Linguist 实现多语言动态切换(含源码与踩坑指南)
qt·i18n·cmake·qmake·linguist·lupdate·lrelease
老黄编程1 个月前
ros2 中 CMakeLists.txt 的 ament_package 有什么用?有什么使用约束?必须放置尾部吗?
ros·cmake