22、导出与包注册:export()与install(EXPORT)、生成并安装<Package>Config.cmake、包版本文件、find_package消费

好,咱们今天聊一个让很多CMake新手头疼、但又是现代CMake构建体系中最核心的一环——导出与包注册

说白了,就是你的库编译完了,怎么让别人(或者别的项目)方便地找到它、用它。你想想看,如果你辛辛苦苦写了一个牛逼的数学库,结果别人想用还得手动去翻你的头文件路径、库文件路径,那多掉价?

嗯,这里要说的就是一套标准化的流程:export()install(EXPORT)、生成<Package>Config.cmake、包版本文件,最后让find_package来消费。

一句话总结:导出是让构建树内部能用,安装是让系统全局能用。Config.cmake 就是你的库的“身份证”和“使用说明书”。

22.1 为什么需要导出与包注册?

我记得刚入行那会儿,项目里依赖第三方库,全是手动设置include_directorieslink_directories。那叫一个痛苦——版本一换,路径一变,整个项目编译就炸了。

后来CMake引入了find_package机制,才算是有了标准。但find_package有两种模式:

  • Module模式:找FindXXX.cmake模块文件。这种文件你得自己写,或者别人写好给你。
  • Config模式:找XXXConfig.cmake文件。这个文件由库的作者在构建时生成并安装。这才是现代CMake推荐的方式。

我个人习惯用Config模式。为什么?因为Config.cmake 文件里包含了目标的所有属性——头文件路径、库文件路径、编译定义、依赖关系等等。你只要find_package一下,所有东西都自动配好了,干净利落。

22.2 export():构建树内的导出

export()命令的作用是:把当前构建树中的目标导出,让同一个构建树下的其他子项目能直接使用。它不会安装到系统里,只是临时生成一个XXXConfig.cmake文件放在构建目录中。

语法很简单:

export(TARGETS mylib
       FILE ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
       NAMESPACE MyLib::)

这里要注意:

  • TARGETS后面跟你要导出的目标名
  • FILE指定生成的Config文件路径
  • NAMESPACE给目标加个命名空间,比如MyLib::mylib,这样用起来更清晰

我在项目中遇到过一种情况:一个大项目拆成多个子模块,每个子模块独立编译,但又有依赖关系。用export()就能让子模块之间互相引用,而不需要先安装。这在开发阶段特别方便。

小提示:export()生成的Config文件只在当前构建树有效。一旦你清理了构建目录,它就没了。所以它只适合开发阶段的内部使用。

22.3 install(EXPORT):安装到系统

如果你想让你的库被系统上其他项目使用,那就得用install(EXPORT)。它会生成一个XXXConfig.cmake文件,并把它安装到指定目录(通常是lib/cmake/XXX)。

基本用法:

install(TARGETS mylib
        EXPORT MyLibTargets
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib
        RUNTIME DESTINATION bin
        INCLUDES DESTINATION include)

install(EXPORT MyLibTargets
        FILE MyLibConfig.cmake
        NAMESPACE MyLib::
        DESTINATION lib/cmake/MyLib)

这里的关键是:

  • 第一个install(TARGETS ...)里指定了EXPORT MyLibTargets,意思是把目标导出到名为MyLibTargets
  • 第二个install(EXPORT ...)把这个导出集生成一个MyLibConfig.cmake文件,并安装到lib/cmake/MyLib目录下

嗯,这里要注意:两个install命令必须配对使用。第一个负责安装库文件本身,第二个负责安装Config文件。缺一不可。

警告:我曾经犯过一个错误——只安装了库文件,忘了安装Config文件。结果别人用find_package死活找不到我的库。排查了半天才发现是Config文件没装。所以,一定要检查安装目录下有没有XXXConfig.cmake

22.4 生成并安装<Package>Config.cmake

刚才我们看到的install(EXPORT)自动生成了Config文件。但有时候,你需要手动编写Config文件,或者添加一些额外的逻辑。这时候可以用configure_package_config_file()

这个函数是CMake的CMakePackageConfigHelpers模块提供的。它让你写一个模板文件(通常叫XXXConfig.cmake.in),然后生成最终的Config文件。

模板文件示例:

@PACKAGE_INIT@

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

check_required_components(MyLib)

然后在CMakeLists.txt中:

include(CMakePackageConfigHelpers)

configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/MyLibConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
    INSTALL_DESTINATION lib/cmake/MyLib
    PATH_VARS INCLUDE_INSTALL_DIR)

install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
    DESTINATION lib/cmake/MyLib)

这样做的好处是:你可以控制Config文件的内容,比如添加版本检查、处理依赖关系、设置变量等。

我个人习惯用configure_package_config_file(),因为它更灵活。特别是当你的库有多个组件或者复杂的依赖时,手动写模板比自动生成更可控。

22.5 包版本文件

光有Config文件还不够。如果你想让find_package支持版本检查(比如find_package(MyLib 2.0 EXACT)),你还需要一个包版本文件,通常叫XXXConfigVersion.cmake

这个文件可以用write_basic_package_version_file()来生成:

include(CMakePackageConfigHelpers)

write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY AnyNewerVersion)

install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake
    DESTINATION lib/cmake/MyLib)

COMPATIBILITY参数有几种选项:

选项 含义
AnyNewerVersion 只要请求的版本 ≤ 安装的版本,就兼容
SameMajorVersion 主版本号必须相同
SameMinorVersion 主版本号和次版本号都必须相同
ExactVersion 版本必须完全匹配

嗯,这里有个坑:版本文件必须和Config文件放在同一个目录下,否则find_package找不到版本信息。我曾经把版本文件放错了目录,结果find_package一直报版本不匹配,折腾了半天才发现是路径问题。

22.6 find_package消费

好了,现在你的库已经安装好了,Config文件和版本文件也都到位了。别人怎么用呢?很简单:

find_package(MyLib REQUIRED)

target_link_libraries(myapp PRIVATE MyLib::mylib)

就这么两行。CMake会去CMAKE_PREFIX_PATH或系统默认路径下找MyLibConfig.cmake。找到后,它会加载这个文件,文件中定义了MyLib::mylib这个目标,然后target_link_libraries就能自动继承所有属性。

你想想看,这比手动设置include_directorieslink_directories省了多少事?而且不会出错。

核心要点:Config模式下的find_package,本质上是导入目标。你链接的不是库文件路径,而是一个CMake目标。这个目标携带了所有你需要的信息。

22.7 完整流程总结

来,我把整个流程串一遍:

  1. 构建阶段:用export()在构建树内导出目标,方便子项目使用
  2. 安装阶段:用install(TARGETS ... EXPORT ...)安装库文件,用install(EXPORT ...)安装Config文件
  3. 版本管理:用write_basic_package_version_file()生成版本文件并安装
  4. 消费阶段:其他项目用find_package()找到你的库,然后target_link_libraries()链接目标

下面这张图可以帮你理清关系:

导出与包注册完整流程 构建阶段 export() 导出目标 安装阶段 install(EXPORT) 安装Config 版本管理 write_basic_package_version_file() 安装目录结构 (lib/cmake/MyLib/) ├── MyLibConfig.cmake ├── MyLibConfigVersion.cmake └── MyLibTargets.cmake (由install(EXPORT)生成) 消费阶段:find_package(MyLib) → target_link_libraries()

22.8 避坑指南

最后,分享几个我踩过的坑:

  • 命名空间不一致:导出时用了MyLib::,但消费时写成了MyLib::(大小写不同),结果链接失败。记住,命名空间是大小写敏感的
  • 忘记安装头文件:Config文件里引用了头文件路径,但你没把头文件安装到include目录。别人一编译就报头文件找不到。所以install(DIRECTORY ...)别忘了。
  • 依赖传递丢失:如果你的库依赖了其他库(比如OpenSSL),Config文件里必须用find_dependency来声明依赖。否则别人链接你的库时,会报链接错误。

我的习惯:每次写完导出和安装逻辑后,我会在干净的虚拟机里测试一遍——从零开始编译、安装、然后写一个简单的测试项目用find_package来消费。这样能确保流程万无一失。

好了,关于导出与包注册,就聊这么多。记住,Config.cmake 是你的库的名片,花点心思把它做好,能让你的用户(包括未来的自己)省下大把时间。


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

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