22、导出与包注册:export()与install(EXPORT)、生成并安装<Package>Config.cmake、包版本文件、find_package消费
好,咱们今天聊一个让很多CMake新手头疼、但又是现代CMake构建体系中最核心的一环——导出与包注册。
说白了,就是你的库编译完了,怎么让别人(或者别的项目)方便地找到它、用它。你想想看,如果你辛辛苦苦写了一个牛逼的数学库,结果别人想用还得手动去翻你的头文件路径、库文件路径,那多掉价?
嗯,这里要说的就是一套标准化的流程:export()、install(EXPORT)、生成<Package>Config.cmake、包版本文件,最后让find_package来消费。
一句话总结:导出是让构建树内部能用,安装是让系统全局能用。Config.cmake 就是你的库的“身份证”和“使用说明书”。
22.1 为什么需要导出与包注册?
我记得刚入行那会儿,项目里依赖第三方库,全是手动设置include_directories和link_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_directories和link_directories省了多少事?而且不会出错。
核心要点:Config模式下的find_package,本质上是导入目标。你链接的不是库文件路径,而是一个CMake目标。这个目标携带了所有你需要的信息。
22.7 完整流程总结
来,我把整个流程串一遍:
- 构建阶段:用
export()在构建树内导出目标,方便子项目使用 - 安装阶段:用
install(TARGETS ... EXPORT ...)安装库文件,用install(EXPORT ...)安装Config文件 - 版本管理:用
write_basic_package_version_file()生成版本文件并安装 - 消费阶段:其他项目用
find_package()找到你的库,然后target_link_libraries()链接目标
下面这张图可以帮你理清关系:
22.8 避坑指南
最后,分享几个我踩过的坑:
- 命名空间不一致:导出时用了
MyLib::,但消费时写成了MyLib::(大小写不同),结果链接失败。记住,命名空间是大小写敏感的。 - 忘记安装头文件:Config文件里引用了头文件路径,但你没把头文件安装到
include目录。别人一编译就报头文件找不到。所以install(DIRECTORY ...)别忘了。 - 依赖传递丢失:如果你的库依赖了其他库(比如OpenSSL),Config文件里必须用
find_dependency来声明依赖。否则别人链接你的库时,会报链接错误。
我的习惯:每次写完导出和安装逻辑后,我会在干净的虚拟机里测试一遍——从零开始编译、安装、然后写一个简单的测试项目用find_package来消费。这样能确保流程万无一失。
好了,关于导出与包注册,就聊这么多。记住,Config.cmake 是你的库的名片,花点心思把它做好,能让你的用户(包括未来的自己)省下大把时间。
公众号:蓝海资料掘金营,微信deep3321