【CMake】静态库:目标属性、库发布与 find_package 完整指南


🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:


文章目录

  • 前言:
  • [一. 操作目标和属性的核心 API](#一. 操作目标和属性的核心 API)
    • [1.1 set_target_properties 与 get_target_properties](#1.1 set_target_properties 与 get_target_properties)
    • [1.2 add_subdirectory:构建层次化工程](#1.2 add_subdirectory:构建层次化工程)
  • [二、CMake 内部静态库的生成与定位流程](#二、CMake 内部静态库的生成与定位流程)
    • [2.1 四步定位法:从目标注册到链接命令生成](#2.1 四步定位法:从目标注册到链接命令生成)
    • [2.2 生成器表达式验证:\ 的妙用](#2.2 生成器表达式验证:<TARGET_FILE> 的妙用)
    • [2.3 实战验证:查看生成的 link.txt 文件](#2.3 实战验证:查看生成的 link.txt 文件)
  • [三. 现代 CMake 库发布与查找机制](#三. 现代 CMake 库发布与查找机制)
    • [3.1 为什么需要导入目标?](#3.1 为什么需要导入目标?)
    • [3.2 库发布完整流程:从编译到安装](#3.2 库发布完整流程:从编译到安装)
    • [3.3 库查找完整流程:find_package 的工作原理](#3.3 库查找完整流程:find_package 的工作原理)
    • [3.4 export 与 install (EXPORT) 的核心区别](#3.4 export 与 install (EXPORT) 的核心区别)
  • 四、总结与核心考点提炼
  • 结尾:

前言:

作为 C/C++ 开发者,你是否遇到过这些问题:写了一个库却不知道怎么让别人用?第三方库的头文件和库文件路径总是配不对?CMake 的 PUBLIC/PRIVATE/INTERFACE 到底有什么区别?很多人学 CMake 只会写简单的add_executableadd_library,但一到实际项目中就会被库的管理、发布和依赖问题卡住。本文将带你深入理解 CMake 最核心的目标 - 属性 - API 模型,从库的属性设置到完整的发布流程,再到find_package的工作原理,一次性解决你 90% 的 CMake 工程问题。

本篇飞书笔记


一. 操作目标和属性的核心 API

现代 CMake 的核心设计思想是基于目标 (Target)。每个可执行文件、静态库、动态库都是一个目标,目标拥有一系列属性 (Property),CMake 通过这些属性来控制编译、链接、安装等整个构建过程。

1.1 set_target_properties 与 get_target_properties

这两个函数是操作目标属性的最底层 API,几乎所有target_*系列函数最终都是通过它们来实现的。

基本语法

bash 复制代码
# 设置一个或多个目标的多个属性
set_target_properties(<target1> <target2> ...
    PROPERTIES
    <prop1> <value1>
    <prop2> <value2>
    ...
)

# 获取单个目标的单个属性值
get_target_property(<variable> <target> <property>)

常用属性对照表

属性名 含义 对应 GCC 选项
COMPILE_OPTIONS 编译选项 -g -O3 -fPIC
INCLUDE_DIRECTORIES 自身编译时的头文件搜索路径 -I
INTERFACE_INCLUDE_DIRECTORIES 下游使用者需要的头文件搜索路径 -I
LINK_LIBRARIES 自身链接时需要的库列表 -l
INTERFACE_LINK_LIBRARIES 下游使用者需要链接的库列表 -l
LIBRARY_OUTPUT_DIRECTORY 库文件输出目录 -
OUTPUT_NAME 输出文件的名称 -
VERSION 库的完整版本号 -
SOVERSION 库的 API 版本号 -
BUILD_RPATH 构建目录中的运行时库搜索路径 -Wl,-rpath
INSTALL_RPATH 安装目录中的运行时库搜索路径 -Wl,-rpath

实战代码演示

我们创建一个test_set_target_properties工程,演示如何设置和获取目标属性:

顶层 CMakeLists.txt

cmake 复制代码
cmake_minimum_required(VERSION 3.18)
project(TestProp LANGUAGES CXX)

# 添加子目录
add_subdirectory(src)

# 添加可执行文件并链接库
add_executable(main main.cpp)
target_link_libraries(main PRIVATE add)

src/CMakeLists.txt

cmake 复制代码
# 创建动态库目标
add_library(add SHARED add.cpp)

# 设置目标属性
set_target_properties(add PROPERTIES
    # 编译类参数
    COMPILE_OPTIONS "-g"
    COMPILE_OPTIONS "-O3"
    COMPILE_OPTIONS "-fPIC"
    INCLUDE_DIRECTORIES "/public"
    INTERFACE_INCLUDE_DIRECTORIES "/interface"
    
    # 链接类参数
    LINK_DIRECTORIES "/public"
    INTERFACE_LINK_DIRECTORIES "/interface"
    LINK_LIBRARIES "curl"
    INTERFACE_LINK_LIBRARIES "jsoncpp"
    
    # 输出类参数
    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"
    OUTPUT_NAME "add"
    VERSION "1.2.3"
    SOVERSION "20"
)

代码解读

  • COMPILE_OPTIONS设置了编译选项-g -O3 -fPIC,分别用于生成调试信息、开启优化和生成位置无关代码
  • INCLUDE_DIRECTORIES是库自身编译时需要的头文件路径,不会传递给下游
  • INTERFACE_INCLUDE_DIRECTORIES是下游使用者必须包含的头文件路径,会自动传递
  • VERSIONSOVERSION用于生成版本化的库文件,最终会生成libadd.so.1.2.3libadd.so.20两个符号链接

1.2 add_subdirectory:构建层次化工程

当项目规模变大时,我们需要将代码组织成多个子目录,每个子目录有自己的CMakeLists.txtadd_subdirectory就是 CMake 构建层次结构的 "递归入口"。

基本语法

cmake 复制代码
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
  • source_dir:子目录的源代码路径,通常是相对路径
  • binary_dir:子目录的构建输出路径,默认与源代码路径同名
  • EXCLUDE_FROM_ALL:该子目录的目标不会被默认构建,只有显式指定才会编译

作用域与变量可见性

这是最容易出错的地方!add_subdirectory会创建一个新的目录作用域

  1. 子目录可以读取父目录已经定义的变量
  2. 子目录中定义的普通变量默认不会影响父目录
  3. 子目录中定义的目标会被注册到全局目标注册表,在任何目录都可以链接

include vs add_subdirectory:核心区别

很多人分不清这两个命令,它们的本质区别在于是否创建新的作用域:

特性 include() add_subdirectory()
作用域 继承当前作用域 创建新的子目录作用域
内置变量变化 CMAKE_CURRENT_SOURCE_DIR不变 CMAKE_CURRENT_SOURCE_DIR变为子目录
变量传递 直接修改当前作用域变量 默认无法修改父目录变量,需用PARENT_SCOPE
典型用途 加载工具函数、配置选项 构建子模块、库或可执行文件

变量变化对比表

使用 add_subdirectory 进入子目录后

变量名 是否变化 说明
CMAKE_CURRENT_SOURCE_DIR ✅ 变化 变为子目录的源代码路径
CMAKE_CURRENT_BINARY_DIR ✅ 变化 变为子目录的构建路径
CMAKE_CURRENT_LIST_FILE ✅ 变化 变为子目录的 CMakeLists.txt 路径
CMAKE_CURRENT_LIST_DIR ✅ 变化 变为子目录的 CMakeLists.txt 所在目录

使用 include 包含子目录文件后

变量名 是否变化 说明
CMAKE_CURRENT_SOURCE_DIR ❌ 不变 还是父目录的源代码路径
CMAKE_CURRENT_BINARY_DIR ❌ 不变 还是父目录的构建路径
CMAKE_CURRENT_LIST_FILE ✅ 变化 变为被包含文件的路径
CMAKE_CURRENT_LIST_DIR ✅ 变化 变为被包含文件所在目录

二、CMake 内部静态库的生成与定位流程

很多人都会好奇:当我在target_link_libraries中写了MyMath,CMake 是怎么知道这个库在build/lib目录下的?

我深入阅读了 CMake 的源代码,现在为你揭秘这个从未在官方文档中详细说明的内部流程。

2.1 四步定位法:从目标注册到链接命令生成

整个过程分为四个清晰的步骤:

第一步:目标生成 当你写下add_library(MyMath STATIC add.cpp sub.cpp)时,CMake 会在全局的Targets容器中注册一个名为MyMathcmTarget对象。

第二步:目标信息存储 每个cmTarget对象内部会存储:

  • 目标名称和类型 (静态库 / 动态库 / 可执行文件)
  • 源文件列表
  • 所有属性 (包括输出路径、编译选项等)
  • 依赖关系

当你使用set_target_properties修改LIBRARY_OUTPUT_DIRECTORY属性时,CMake 会立即更新这个目标的输出路径信息。

第三步:生成器阶段 - 定位静态库路径 配置阶段结束后,CMake 进入生成阶段。生成器会遍历所有目标,根据目标的属性和平台规则,推导出每个目标的实际输出路径。例如:

  • 静态库:build/lib/libMyMath.a
  • 动态库:build/lib/libMyMath.so.1.2.3

第四步:链接命令的生成 生成器会为每个目标生成链接规则,直接将库的完整路径写入链接命令中:

bash 复制代码
g++ main.o -o myexe build/lib/libMyMath.a

这样,链接器就能直接找到并链接对应的库文件,不需要额外的-L参数。

2.2 生成器表达式验证:$<TARGET_FILE> 的妙用

我们可以使用生成器表达式$<TARGET_FILE:目标名>来验证 CMake 计算出的库路径。生成器表达式是 CMake 中一种特殊的语法,在生成阶段(而不是配置阶段) 动态计算值。

修改app/CMakeLists.txt,添加以下代码:

cmake 复制代码
add_custom_command(
    TARGET main POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo "$<TARGET_FILE:MyMath>"
    COMMENT "获取静态库的输出路径"
)

编译后你会看到类似这样的输出:

Plain 复制代码
[ 80%] Linking CXX executable ../bin/main
获取静态库的输出路径
/home/bit/workspace/CMakeClass/my_math/build/lib/libMyMath.a
[100%] Built target main

2.3 实战验证:查看生成的 link.txt 文件

CMake 会将每个目标的链接命令保存在CMakeFiles/目标名.dir/link.txt文件中。我们可以直接查看这个文件来验证上面的结论:

bash 复制代码
cat app/CMakeFiles/main.dir/link.txt

输出内容大致如下:

bash 复制代码
/usr/bin/c++ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk 
-mmaccosx-version-min=15.3 -Wl,-search_paths_first -Wl,-headerpad_max_install_names 
CMakeFiles/main.dir/main.cpp.o -o ../bin/main ../lib/libMyMath.a

可以看到,链接命令中直接使用了静态库的完整路径../lib/libMyMath.a,这就是 CMake 能够自动定位内部库的秘密。


三. 现代 CMake 库发布与查找机制

当你写了一个很棒的库,想要分享给团队其他人使用时,就需要了解现代 CMake 的库发布和查找机制。

3.1 为什么需要导入目标?

在传统的 CMake 中,我们使用find_libraryfind_path来查找库文件和头文件,然后手动设置INCLUDE_DIRSLIBRARIES变量。这种方式有很多缺点:

  • 容易出错,路径配置繁琐
  • 无法传递依赖关系
  • 没有版本检查
  • 不支持编译选项和定义的传递

现代 CMake 引入了 ** 导入目标 (IMPORTED Target)** 的概念,完美解决了这些问题。导入目标就像一个远程的普通目标,拥有所有必要的属性 (头文件路径、依赖库、编译选项等),使用起来和项目内部的目标完全一样。

3.2 库发布完整流程:从编译到安装

我们以一个数学库MyMath为例,演示完整的库发布流程。

步骤 1:编译库并设置使用要求

cmake 复制代码
# 收集源代码
file(GLOB SRC_LISTS "src/*.cpp")

# 添加静态库目标
add_library(MyMath STATIC ${SRC_LISTS})

# 设置库的使用要求(下游消费者必须包含的头文件路径)
target_include_directories(MyMath INTERFACE
    "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
)

# 设置库的输出路径
set_target_properties(MyMath PROPERTIES
    ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
)

关键解读

  • $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>是一个生成器表达式,在安装时会被解析为/usr/local/include(默认安装前缀)
  • 使用INTERFACE关键字表示这个属性只对下游使用者有效,库自身编译时不需要

步骤 2:安装库文件与头文件

cmake 复制代码
include(GNUInstallDirs)

# 安装静态库
install(TARGETS MyMath
    EXPORT MyMathTargets
    DESTINATION ${CMAKE_INSTALL_LIBDIR} # 默认为lib
)

# 安装头文件
install(DIRECTORY include/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/math # 安装到/usr/local/include/math
    FILES_MATCHING PATTERN "*.h"
)

关键解读

  • EXPORT MyMathTargetsMyMath目标添加到名为MyMathTargets的导出集合中
  • GNUInstallDirs模块定义了各个平台下的标准安装目录,遵循 GNU 规范

步骤 3:导出目标到构建树

cmake 复制代码
# 导出目标集合到构建树,供其他项目直接从构建目录使用
export(EXPORT MyMathTargets
    FILE ${CMAKE_CURRENT_BINARY_DIR}/MyMathTargets.cmake
)

这一步会在构建目录生成MyMathTargets.cmake文件,包含了从构建树使用这个库的所有信息。

步骤 4:导出目标到安装树

cmake 复制代码
# 导出目标集合到安装树,供安装后使用
install(EXPORT MyMathTargets
    FILE MyMathTargets.cmake
    NAMESPACE MyMath:: # 添加命名空间,避免命名冲突
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyMath # 安装到/usr/local/lib/cmake/MyMath
)

这一步会在安装时生成MyMathTargets.cmake文件,包含了从安装树使用这个库的所有信息。

步骤 5:生成并安装 Config.cmake

find_package需要找到一个名为MyMathConfig.cmake的文件才能工作。我们使用configure_package_config_file来生成这个文件:

首先创建一个模板文件Config.cmake.in

cmake 复制代码
@PACKAGE_INIT@
include(${CMAKE_CURRENT_LIST_DIR}/MyMathTargets.cmake)

然后在 CMakeLists.txt 中添加:

cmake 复制代码
include(CMakePackageConfigHelpers)

# 生成Config.cmake文件
configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/MyMathConfig.cmake
    INSTALL_DESTINATION "lib/cmake/MyMath"
)

# 安装Config.cmake文件
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MyMathConfig.cmake
    DESTINATION "lib/cmake/MyMath"
)

3.3 库查找完整流程:find_package 的工作原理

当你在另一个项目中使用find_package(MyMath CONFIG REQUIRED)时,CMake 会执行以下步骤:

  1. 查找配置文件 :CMake 会在标准路径下查找MyMathConfig.cmake文件,搜索顺序为:

    1. CMAKE_PREFIX_PATH指定的路径
    2. 系统标准路径:/usr/local/lib/cmake//usr/lib/cmake/
  2. 执行配置文件 :找到后会执行这个文件,它会包含MyMathTargets.cmake文件

  3. 创建导入目标MyMathTargets.cmake文件会创建一个名为MyMath::MyMath的导入目标,并设置好所有必要的属性

  4. 使用导入目标 :你可以像使用普通目标一样链接这个导入目标:
    1.

    cmake 复制代码
    find_package(MyMath CONFIG REQUIRED)
    add_executable(main main.cpp)
    target_link_libraries(main PRIVATE MyMath::MyMath)

3.4 export 与 install (EXPORT) 的核心区别

这两个命令是最容易混淆的,它们的核心区别如下:

维度 export(EXPORT ...) install(EXPORT ...)
触发时机 配置 / 生成阶段立即写文件 只有执行cmake --install时才写文件
写入位置 FILE参数指定,通常在构建目录 安装到系统标准路径,如/usr/local/lib/cmake/
内容差异 针对构建树,路径指向 build 目录 针对安装树,路径指向安装目录
典型用途 开发阶段,其他项目直接从构建目录使用 发布阶段,安装后供其他项目使用

四、总结与核心考点提炼

本文我们深入学习了 CMake 从目标属性设置到库发布与查找的完整流程,核心要点总结如下:

  1. CMake 的核心是目标 (Target):所有构建规则都围绕目标展开,目标拥有一系列属性来控制构建过程
  2. set_target_properties 是底层 API :所有target_*系列函数最终都是通过它来设置目标属性
  3. add_subdirectory 创建新作用域:子目录的变量默认不影响父目录,但目标是全局可见的
  4. CMake 自动定位内部库 :通过全局目标注册表和直接写入完整路径的方式,不需要手动配置-L参数
  5. 现代 CMake 使用导入目标发布库 :通过exportinstall(EXPORT)生成目标描述文件
  6. find_package 是导入目标的入口:Config 模式是现代 CMake 推荐的方式,比 Module 模式更可靠

结尾:

html 复制代码
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:CMake 是 C/C++ 开发者必须掌握的工具,但很多人只停留在 "能用" 的阶段,没有深入理解其背后的设计思想。现代 CMake 基于目标的设计思想,让工程管理变得更加优雅和可维护。学习 CMake 最好的方式是实践,建议你按照本文的步骤,亲手创建一个库并发布,然后在另一个项目中使用它。只有在实际项目中不断踩坑,才能真正掌握 CMake 的精髓。如果你觉得本文对你有帮助,欢迎点赞收藏,也可以在评论区留下你的问题和经验,我们一起交流学习。

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど