如何编写适配现有库的CMake Find模块以支持现代导出目标调用
我来给你拆解这两个CMake实操问题,都是实际项目里经常碰到的痛点,咱们一步步来:
如果你现在维护一个用CMake构建的库,想要用上现代CMake的导出目标功能(让用户不用再手动写头文件路径和库路径,直接一行target_link_libraries搞定),可以按下面的步骤来,同时兼顾一定的旧版本兼容性:
先把库的基础属性配置扎实
在你的库的CMakeLists.txt里,先把核心信息定义清楚,尤其是头文件路径的区分(构建时和安装后是不一样的):add_library(MyLib SHARED src/mylib.cpp) # 设置版本号,SOVERSION是动态库的版本后缀,比如libMyLib.so.1 set_target_properties(MyLib PROPERTIES VERSION 1.2.3 SOVERSION 1 EXPORT_NAME MyLib ) # 头文件路径:BUILD_INTERFACE是本地构建时的路径,INSTALL_INTERFACE是安装后的路径 target_include_directories(MyLib PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include> )配置安装与导出规则
接下来告诉CMake,安装库的时候要把目标信息也导出来,方便用户找到:# 安装库文件本身,同时指定导出集 install(TARGETS MyLib EXPORT MyLibTargets LIBRARY DESTINATION lib # 动态库路径 ARCHIVE DESTINATION lib # 静态库路径 RUNTIME DESTINATION bin # Windows下的exe/dll路径 INCLUDES DESTINATION include # 头文件安装路径 ) # 把导出集写入MyLibTargets.cmake,加上命名空间避免重名 install(EXPORT MyLibTargets FILE MyLibTargets.cmake NAMESPACE MyLib:: DESTINATION lib/cmake/MyLib # 配置文件安装路径 )这里的
NAMESPACE一定要加,比如MyLib::MyLib,不然用户项目里如果有同名目标就冲突了。生成Config.cmake文件
用户要能用find_package(MyLib)找到你的库,还需要一个MyLibConfig.cmake。用CMake自带的configure_package_config_file来生成最靠谱,它会自动处理路径问题:
先写一个模板文件MyLibConfig.cmake.in:@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 )可选:添加版本支持
如果想让用户能指定版本查找(比如find_package(MyLib 1.2 REQUIRED)),可以生成版本文件:write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake VERSION 1.2.3 COMPATIBILITY SameMajorVersion # 主版本相同就兼容,按需调整 ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake DESTINATION lib/cmake/MyLib )
这样用户安装你的库后,只需要两行代码就能用,非常清爽:
find_package(MyLib REQUIRED) target_link_libraries(MyApp PRIVATE MyLib::MyLib)
你说要更新某个库的老旧Find模块,这个库本身是用CMake构建的共享库,用户可能装在系统里或者包仓库里,想要用户用两行代码就能调用。这里分两种情况来处理,核心是既要兼容旧CMake,又要提供现代的目标接口:
情况1:库本身已经支持CMake导出(优先写Config文件)
如果这个库已经按上面的步骤配置了导出目标,那你只需要写一个兼容旧CMake的XXXConfig.cmake文件就行。比如有些老版本CMake(比如3.0以下)不支持@PACKAGE_INIT@,那我们就加个 fallback 逻辑:
修改XXXConfig.cmake.in,兼容CMake 2.8.12及以上(这个版本已经覆盖大部分旧环境了):
# 兼容旧CMake版本的路径处理 if(CMAKE_VERSION VERSION_LESS 3.0) # 手动设置头文件和库路径,@CMAKE_INSTALL_FULL_*@会被CMake替换成实际路径 set(XXX_INCLUDE_DIR "@CMAKE_INSTALL_FULL_INCLUDEDIR@") set(XXX_LIBRARY "@CMAKE_INSTALL_FULL_LIBDIR@/libXXX.so") # Windows下换成libXXX.dll或者XXX.lib else() # 新版本用PACKAGE_INIT自动处理路径 @PACKAGE_INIT@ endif() # 导入目标文件 include("${CMAKE_CURRENT_LIST_DIR}/XXXTargets.cmake") # 兜底:如果导入失败,手动创建导入目标(防止极端情况) if(NOT TARGET XXX::XXX) add_library(XXX::XXX SHARED IMPORTED) set_target_properties(XXX::XXX PROPERTIES IMPORTED_LOCATION "@CMAKE_INSTALL_FULL_LIBDIR@/libXXX.so" INTERFACE_INCLUDE_DIRECTORIES "@CMAKE_INSTALL_FULL_INCLUDEDIR@" ) endif() check_required_components(XXX)
这样不管用户用新还是旧CMake,都能正常找到目标,用户依然用两行代码调用:
find_package(XXX REQUIRED) target_link_libraries(MyApp PRIVATE XXX::XXX)
情况2:库没有CMake导出(写FindXXX.cmake模块)
如果这个库是老旧的,根本没提供CMake导出文件,那我们就写一个FindXXX.cmake模块,既要兼容旧CMake,又要提供现代的导入目标,让用户不用改代码就能切换到现代用法。
示例FindMyLib.cmake:
# 声明兼容的最低CMake版本,这里选2.8.12,已经支持导入目标了 cmake_minimum_required(VERSION 2.8.12) # 第一步:查找头文件 find_path(MyLib_INCLUDE_DIR NAMES mylib.h # 库的核心头文件名 PATHS /usr/include /usr/local/include # 常见的系统路径,按需添加 DOC "MyLib header file directory" ) # 第二步:查找库文件 find_library(MyLib_LIBRARY NAMES mylib libmylib # 库的名称,不同平台可能前缀不同 PATHS /usr/lib /usr/local/lib DOC "MyLib library file" ) # 可选:从头文件里提取版本号(如果头文件里有#define MYLIB_VERSION "x.y.z") if(MyLib_INCLUDE_DIR) file(READ "${MyLib_INCLUDE_DIR}/mylib.h" _mylib_header_content) string(REGEX MATCH "#define MYLIB_VERSION \"([0-9]+\\.[0-9]+\\.[0-9]+)\"" _version_match "${_mylib_header_content}") set(MyLib_VERSION "${CMAKE_MATCH_1}") endif() # 用CMake自带的函数处理查找结果,设置MyLib_FOUND变量 include(FindPackageHandleStandardArgs) find_package_handle_standard_args(MyLib REQUIRED_VARS MyLib_INCLUDE_DIR MyLib_LIBRARY VERSION_VAR MyLib_VERSION # 如果提取到版本号,这里就会显示 ) # 第三步:创建现代CMake导入目标,让用户能用target_link_libraries if(MyLib_FOUND AND NOT TARGET MyLib::MyLib) add_library(MyLib::MyLib SHARED IMPORTED) set_target_properties(MyLib::MyLib PROPERTIES IMPORTED_LOCATION "${MyLib_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${MyLib_INCLUDE_DIR}" ) endif() # 兼容旧项目的变量(可选,方便老项目直接迁移,不用改代码) mark_as_advanced(MyLib_INCLUDE_DIR MyLib_LIBRARY) set(MyLib_LIBRARIES ${MyLib_LIBRARY}) set(MyLib_INCLUDE_DIRS ${MyLib_INCLUDE_DIR})
这样用户还是能用两行代码调用,老项目如果之前用${MyLib_LIBRARIES}也能继续工作,平滑过渡。
兼容旧CMake的关键细节
- 尽量选一个合理的最低兼容版本,比如2.8.12,它已经支持
add_library(... IMPORTED)和INTERFACE_INCLUDE_DIRECTORIES这些核心功能,没必要兼容更早的版本(比如2.8.0,那要做很多额外的兼容,性价比太低)。 - 在Config文件里,用
@PACKAGE_INIT@加上 fallback 逻辑,确保旧CMake也能正确解析安装路径。 - 不管是Config还是Find模块,都要提供
XXX::XXX命名空间的目标,这是现代CMake的标准用法,用户用起来更省心。
内容的提问来源于stack exchange,提问作者Flamefire

