如何在CMake中复现$<TARGET_RUNTIME_DLLS>的跨平台依赖解析?
生成器表达式$<TARGET_RUNTIME_DLLS:..>如何在生成时递归遍历所有链接目标?能否在CMake中复现该行为?
基于CMake的中型C++闭源项目,包含核心共享库、单元/集成测试、示例程序及自研授权工具,需跨Linux多发行版和Windows的Release/Debug配置构建。依赖管理采用Conan+CMakeDeps/CMakeToolchain集成,可通过自定义conanfile.py按需关闭模块减少依赖;使用CPack生成可分发安装包。
多数依赖为开源库,由Conan管理或自带CMake包配置,部分需构建为共享库。Conan生成的包会创建含生成器表达式的INTERFACE IMPORTED目标,相关属性(如INTERFACE_LINK_LIBRARIES、IMPORTED_LOCATION)在配置阶段无法解析。
运行时输出目录的依赖
Windows下可通过$<TARGET_RUNTIME_DLLS>复制所有传递依赖DLL到运行时目录,但Linux无类似$<TARGET_RUNTIME_SO>表达式。
可移植测试包的依赖
Linux下需将目标的所有传递共享库依赖复制到CMAKE_LIBRARY_OUTPUT目录,但自定义递归遍历依赖树的CMake函数因生成器表达式无法解析而失效。
安装阶段的依赖
CPack打包时,Windows可通过$<TARGET_RUNTIME_DLLS>打包CMake已知的依赖,但Linux无对应方式。
file(GET_RUNTIME_DEPENDENCIES ...):返回大量不必要的系统库;install(IMPORTED_RUNTIME_ARTIFACTS ...):需明确指定共享库目标,无法处理传递依赖;install(TARGETS <targets..> RUNTIME_DEPENDENCIES ...):同前,包含过多系统库;- BundleUtilities的
fixup_bundle(...):仅适用于可执行文件,适配Linux困难且包含系统库; include(InstallRequiredSystemLibraries):仅包含通用系统库;$<GENEX_EVAL:...>:无法为配置阶段函数解析生成器表达式。
$<TARGET_RUNTIME_DLLS>的工作原理
该表达式在生成阶段解析目标的完整传递依赖链,自动筛选出符合条件的共享库(Windows DLL):
- 遍历直接/间接链接的所有目标(包括IMPORTED目标);
- 过滤系统库(通过CMake内部标记或
IMPORTED_NO_SYSTEM属性); - 返回所有符合条件的库文件路径,无需手动递归。
Linux下复现该行为的实现
1. 运行时输出目录复制(生成阶段)
利用add_custom_command在生成阶段执行递归复制逻辑,此时生成器表达式已展开:
function(copy_runtime_deps target) add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -P "${CMAKE_CURRENT_SOURCE_DIR}/copy_runtime_deps.cmake" $<TARGET_FILE_DIR:${target}> $<TARGET_FILE:${target}> COMMENT "Copying runtime dependencies for ${target}" ) endfunction()
配套的copy_runtime_deps.cmake脚本(生成阶段执行,可解析所有目标属性):
set(dest_dir ${ARGV0}) set(target_path ${ARGV1}) function(install_deps target dest) get_target_property(linked ${target} LINK_LIBRARIES) if(linked) foreach(lib IN LISTS linked) if(TARGET ${lib}) get_target_property(type ${lib} TYPE) get_target_property(imported ${lib} IMPORTED) get_target_property(no_system ${lib} IMPORTED_NO_SYSTEM) # 筛选非系统的共享库依赖 if((type STREQUAL "SHARED_LIBRARY" OR type STREQUAL "MODULE_LIBRARY") AND imported AND no_system) set(lib_path $<TARGET_FILE:${lib}>) execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different ${lib_path} ${dest}) # 递归处理依赖的依赖 install_deps(${lib} ${dest}) endif() endif() endforeach() endif() endfunction() # 获取目标对应的CMake目标名称(从路径反推) get_filename_component(target_name ${target_path} NAME_WE) install_deps(${target_name} ${dest_dir})
使用方式:
copy_runtime_deps(your_executable)
2. 安装与CPack打包(安装阶段)
使用install(CODE)在安装阶段执行递归安装逻辑,避免配置阶段解析限制:
install(CODE " function(install_runtime_deps target dest) get_target_property(linked_targets \${target} LINK_LIBRARIES) if(linked_targets) foreach(linked IN LISTS linked_targets) if(TARGET \${linked}) get_target_property(type \${linked} TYPE) get_target_property(imported \${linked} IMPORTED) get_target_property(no_system \${linked} IMPORTED_NO_SYSTEM) if((type STREQUAL \"SHARED_LIBRARY\" OR type STREQUAL \"MODULE_LIBRARY\") AND imported AND no_system) install(FILES \$<TARGET_FILE:\${linked}> DESTINATION \${dest}) install_runtime_deps(\${linked} \${dest}) endif() endif() endforeach() endif() endfunction() install_runtime_deps(your_executable \${CMAKE_INSTALL_BINDIR}) ")
关键注意事项
- 必须在生成/安装阶段执行递归逻辑,此时生成器表达式已完全展开,目标属性可正常访问;
- 通过
IMPORTED_NO_SYSTEM属性过滤系统库,确保只复制项目引入的第三方依赖; - 若依赖目标未设置
IMPORTED_NO_SYSTEM,可手动添加:set_target_properties(dep_target PROPERTIES IMPORTED_NO_SYSTEM ON)。
内容的提问来源于stack exchange,提问作者GulaschLulatsch

