第27章 导出与导入:export、install(EXPORT ...)、find_package消费自己的库

好,咱们今天聊一个实战性很强的话题——怎么把自己写的库“交出去”,让别人用find_package找到它。

很多新手写完一个库,直接把头文件和.so/.a扔给同事,然后丢一句“你链接一下就行”。结果对方折腾半天,不是找不到头文件,就是链接报错。嗯,这种场景我见过太多次了。

CMake提供了一套完整的“导出与导入”机制。说白了,就是让你的库变得像OpenCV、Boost那样,别人一句find_package就能用。

27.1 为什么需要导出?

先想一个问题:你写了一个数学库叫MathLib,里面有加法、乘法。同事小明想用,他需要知道什么?

  • 头文件在哪?
  • 库文件在哪?
  • 链接哪个库?Debug还是Release?
  • 有没有依赖别的库?

如果你手动告诉他,每次更新都得重新说一遍。累不累?

导出机制就是把这些信息打包成一个“配置文件”。CMake通过这个文件,自动帮你搞定所有路径和链接参数。

核心思想: 导出 = 把你的库的“使用说明书”生成出来。别人拿到说明书,就能自动配置好一切。

27.2 两种导出场景

我个人习惯把导出分成两种情况:

场景 说明 常用命令
构建树内导出 在源码构建目录里直接用,不安装 export(TARGETS ...)
安装后导出 把库安装到系统路径后,供其他项目使用 install(EXPORT ...)

构建树内导出适合开发阶段快速测试。安装后导出才是生产环境的标准做法。

27.3 实战:写一个可导出的库

咱们直接上代码。假设我有一个简单的加法库:

# MathLib/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MathLib VERSION 1.0.0)

add_library(MathLib STATIC src/add.cpp)
target_include_directories(MathLib PUBLIC include)

# 第一步:生成导出头文件
include(GenerateExportHeader)
generate_export_header(MathLib)

# 第二步:安装头文件和库
install(TARGETS MathLib
    EXPORT MathLibTargets
    ARCHIVE DESTINATION lib
    PUBLIC_HEADERS DESTINATION include
)

# 第三步:安装CMake配置文件
install(EXPORT MathLibTargets
    FILE MathLibTargets.cmake
    NAMESPACE MathLib::
    DESTINATION lib/cmake/MathLib
)

# 第四步:生成并安装Config文件
include(CMakePackageConfigHelpers)
configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/MathLibConfig.cmake
    INSTALL_DESTINATION lib/cmake/MathLib
)
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MathLibConfig.cmake
    DESTINATION lib/cmake/MathLib
)

这里有个细节我特别想强调:NAMESPACE。加上MathLib::这个命名空间后,别人用的时候就是MathLib::MathLib,一看就知道是哪个库的。我曾经见过一个项目,三个库都叫core,链接的时候全乱了……

27.4 Config.cmake.in 模板文件

这个文件是“说明书”的模板。CMake会用实际路径替换里面的变量:

# MathLib/Config.cmake.in
@PACKAGE_INIT@

set_and_check(MathLib_INCLUDE_DIR "@PACKAGE_INCLUDE_INSTALL_DIR@")
set_and_check(MathLib_LIBRARY_DIR "@PACKAGE_LIB_INSTALL_DIR@")

include("${CMAKE_CURRENT_LIST_DIR}/MathLibTargets.cmake")

check_required_components(MathLib)

注意那个@PACKAGE_INIT@,它是CMake自动替换的宏,用来处理路径计算。我刚开始写的时候忘了加这行,结果路径全是绝对路径,换个机器就炸了。

27.5 消费者怎么用?

现在小明想用我的库,他只需要:

# 小明的项目/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyApp)

find_package(MathLib REQUIRED)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE MathLib::MathLib)

就这三行。头文件路径、库路径、链接参数,全部自动搞定。

小技巧: 如果库还没安装到系统路径,可以用 -DCMAKE_PREFIX_PATH=/path/to/MathLib/build 指定构建目录。这样不用安装也能测试。

27.6 避坑指南

我这些年踩过的坑,列几个典型的:

  • 忘记PUBLIC头文件target_include_directories一定要用PUBLIC,否则导出后别人找不到头文件。
  • 命名空间不一致:导出时用了MathLib::,消费时也要用MathLib::MathLib。少一个冒号都不行。
  • 路径硬编码:永远不要在Config.cmake.in里写死路径。用@PACKAGE_INIT@set_and_check
  • 忘记安装依赖:如果你的库依赖了别的库,记得在Config.cmake.in里也find_dependency
重要: 我曾经有一个项目,库A依赖库B,但A的Config文件里没写find_dependency(B)。结果用户只装了A,编译时报链接错误,排查了两天才发现是B没装。所以,依赖传递一定要在配置文件里体现

27.7 知识体系图

下面这张图总结了整个导出导入的流程:

CMake 导出与导入流程 库作者(导出方) 1. 编写库代码 + CMakeLists.txt 2. install(EXPORT ...) 生成目标文件 3. configure_package_config_file 生成Config 4. 安装到系统路径或指定目录 安装后 消费者(导入方) 1. find_package(MathLib REQUIRED) 2. 自动加载 MathLibConfig.cmake 3. 包含 MathLibTargets.cmake 4. target_link_libraries(MathLib::MathLib) 关键文件:MathLibConfig.cmake + MathLibTargets.cmake

27.8 总结

导出与导入,说白了就是让你的库拥有“自描述能力”。别人不需要知道你的库是怎么组织的,只需要find_package然后target_link_libraries

我个人建议:从写第一个库开始就加上导出配置。哪怕现在只有你自己用,养成习惯后,以后团队协作、开源发布都会轻松很多。

嗯,这一章的内容就到这里。记住那个流程图,理解清楚“作者导出”和“消费者导入”两个角色,你就掌握了CMake库分发的核心。


公众号:蓝海资料掘金营,微信 deep3321