欢迎光临
我们一直在努力

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

在这里插入图片描述

🔥草莓熊Lotso:个人主页

❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》

✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:

在这里插入图片描述


文章目录

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

前言:

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

本篇飞书笔记


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

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

1.1 set_target_properties 与 get_target_properties

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

基本语法

# 设置一个或多个目标的多个属性
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_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

# 创建动态库目标
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是下游使用者必须包含的头文件路径,会自动传递
  • VERSION和SOVERSION用于生成版本化的库文件,最终会生成libadd.so.1.2.3和libadd.so.20两个符号链接

1.2 add_subdirectory:构建层次化工程

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

基本语法

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

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

作用域与变量可见性

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

  • 子目录可以读取父目录已经定义的变量
  • 子目录中定义的普通变量默认不会影响父目录
  • 子目录中定义的目标会被注册到全局目标注册表,在任何目录都可以链接
  • 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容器中注册一个名为MyMath的cmTarget对象。

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

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

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

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

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

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

    g++ main.o -o myexe build/lib/libMyMath.a

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

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

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

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

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

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

    [ 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文件中。我们可以直接查看这个文件来验证上面的结论:

    cat app/CMakeFiles/main.dir/link.txt

    输出内容大致如下:

    /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_library和find_path来查找库文件和头文件,然后手动设置INCLUDE_DIRS和LIBRARIES变量。这种方式有很多缺点:

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

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

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

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

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

    # 收集源代码
    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:安装库文件与头文件

    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 MyMathTargets将MyMath目标添加到名为MyMathTargets的导出集合中
    • GNUInstallDirs模块定义了各个平台下的标准安装目录,遵循 GNU 规范

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

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

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

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

    # 导出目标集合到安装树,供安装后使用
    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:

    @PACKAGE_INIT@
    include(${CMAKE_CURRENT_LIST_DIR}/MyMathTargets.cmake)

    然后在 CMakeLists.txt 中添加:

    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 会执行以下步骤:

  • 查找配置文件:CMake 会在标准路径下查找MyMathConfig.cmake文件,搜索顺序为:
  • CMAKE_PREFIX_PATH指定的路径
  • 系统标准路径:/usr/local/lib/cmake/、/usr/lib/cmake/等
  • 执行配置文件:找到后会执行这个文件,它会包含MyMathTargets.cmake文件
  • 创建导入目标:MyMathTargets.cmake文件会创建一个名为MyMath::MyMath的导入目标,并设置好所有必要的属性
  • 使用导入目标:你可以像使用普通目标一样链接这个导入目标:
  • 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 从目标属性设置到库发布与查找的完整流程,核心要点总结如下:

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

  • 结尾:

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

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

    ✨把这些内容吃透超牛的!放松下吧✨

    ʕ˘ᴥ˘ʔ

    づきらど

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 【CMake】静态库:目标属性、库发布与 find_package 完整指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址