文章目录
- 1. 目的
- 2. 设计
- 整体思路
- 多层依赖的处理
- 获取 DLL 所在目录
- 探测剩余的 DLL 文件
- 3. 代码实现
- 判断 stack 是否为空
- 判断 stack 是否为空
- 获取所有 target
- 检测并拷贝 DLL
- 4. 使用
1. 目的
在基于 CMake 构建的 C/C++ 工程中,拷贝当前工程需要的每个DLL文件到 Visual Studio 工程的启动路径下, 让可执行目标在运行阶段正常运行,解决“DLL找不到”导致的程序终止、异常退出等问题,解决“每次都要手动拷贝,有时候忘记拷贝”的问题。
举例:
- OpenCV: 官方预编译版本,包含的 opencv_world.dll, 以及读取某些视频时需要的 opencv_ffmpeg dll 文件
- windows-pthreads 的 DLL 文件
- 其他依赖库当中,提供的 DLL 文件
实际上不仅限于 Windows 平台的 DLL, 在 Linux / MacOSX 上也同样有这样的问题,此时只需要增加 .so
和 .dylib
文件后缀的动态库支持即可。
本文给出基于 CMake 语言的解决方案。
2. 设计
整体思路
枚举所有 target, 筛选出 SHARED_LIBRARRY
类型的 target, 获取它们的动态库的路径 shared_library_path
, 然后拷贝到用户指定的目录 dstDir
.
此外有些 dll 文件并没有被 xxx-config.cmake 等配置文件所配置, 需要额外扫描和拷贝,例如 opencv 预编译库中的 ffmpeg 的 dll 文件。
多层依赖的处理
有时候工程比较复杂, target 至少包括三层, 最后一层是可执行文件, 第二层可能没有DLL,但第二层依赖的第一层则可能存在DLL文件,这就导致枚举 target 时不能只枚举一层。换言之,枚举 target 的过程是一个递归过程, 需要使用深度优先搜索 DFS 算法。
获取 DLL 所在目录
这个问题比较简单, 用 cmake 的 get_target_property
函数获取。
探测剩余的 DLL 文件
包括两种情况:
- target 本身是动态库类型, 那么它的 DLL 文件所在的目录应该被扫描,扫描出的新的 DLL 文件也应该被拷贝
- target 本身是静态库类型, 但它所在目录的上一级目录中, 有一个
bin
目录,bin
目录里存放有 DLL 文件
3. 代码实现
代码实现过程中遇到一些“难点”,主要是对 cmake 不够足够熟悉, 简单列举:
判断 stack 是否为空
- DFS 算法的实现过程中, 怎样判断 stack 为空?获取 stack 首部元素?依赖于对
list
的操作, 包括将“列表是否为空”封装为函数
#======================================================================
# Determine if a list is empty
#======================================================================
# Example:
# cvpkg_is_list_empty(testbed_requires testbed_requires_empty)
# message(STATUS "testbed_requires_empty: ${testbed_requires_empty}")
#----------------------------------------------------------------------
function(cvpkg_is_list_empty the_list ret)
list(LENGTH ${the_list} the_list_length)
if(${the_list_length} EQUAL 0)
set(${ret} TRUE PARENT_SCOPE)
else()
set(${ret} FALSE PARENT_SCOPE)
endif()
endfunction()
判断 stack 是否为空
通过判断元素是否在列表中来实现。封装为了函数
#======================================================================
# Determine if item is in the list
#======================================================================
# Example:
# cvpkg_is_item_in_list(testbed_requires "protobuf" protobuf_in_the_lst)
# message(STATUS "protobuf_in_the_lst: ${protobuf_in_the_lst}")
#
# cvpkg_is_item_in_list(testbed_requires "opencv" opencv_in_the_lst)
# message(STATUS "opencv_in_the_lst: ${opencv_in_the_lst}")
#----------------------------------------------------------------------
function(cvpkg_is_item_in_list the_list the_item ret)
list(FIND ${the_list} ${the_item} index)
if(index EQUAL -1)
set(${ret} FALSE PARENT_SCOPE)
else()
set(${ret} TRUE PARENT_SCOPE)
endif()
endfunction()
获取所有 target
原本的依赖关系是 hierarchical 的, 怎样拍平,得到一维的依赖列表?并且不能有重复元素?答案是用 DFS。
#======================================================================
# 4. Recursively get required packages for a package. No duplicated.
#======================================================================
# Example:
# cvpkg_get_flatten_requires(testbed flatten_pkgs)
# message(STATUS "flatten_pkgs: ${flatten_pkgs}")
#----------------------------------------------------------------------
function(cvpkg_get_flatten_requires input_pkg the_result)
list(LENGTH input_pkg input_pkg_length)
if(NOT (${input_pkg_length} EQUAL 1))
cvpkg_error("input_pkg should be single element list")
endif()
set(visited_pkgs "")
set(pkg_stack ${input_pkg})
while(TRUE)
cvpkg_is_list_empty(pkg_stack pkg_stack_empty)
if(${pkg_stack_empty})
break()
endif()
cvpkg_debug("pkg_stack: ${pkg_stack}")
# pop the last element
list(POP_BACK pkg_stack pkg)
cvpkg_debug("pkg: ${pkg}")
# mark the element as visited
cvpkg_is_item_in_list(visited_pkgs "${pkg}" pkg_visited)
if(NOT ${pkg_visited})
cvpkg_debug(" visiting ${pkg}")
list(APPEND visited_pkgs ${pkg})
# traverse it's required dependencies and put into pkg_stack
get_target_property(subpkgs ${pkg} LINK_LIBRARIES)
cvpkg_debug("LINK_LIBRARIES: ${subpkgs}")
if(subpkgs)
foreach(subpkg ${subpkgs})
if(TARGET ${subpkg}) # if called target_link_libraries() more than once, subpkgs contains stuffs like `::@(000001FAFA8C75C0)`
cvpkg_debug(" subpkg: ${subpkg}")
list(APPEND pkg_stack ${subpkg})
endif()
endforeach()
endif()
get_target_property(subpkgs ${pkg} INTERFACE_LINK_LIBRARIES)
cvpkg_debug("INTERFACE_LINK_LIBRARIES: ${subpkgs}")
if(subpkgs)
foreach(subpkg ${subpkgs})
if(TARGET ${subpkg}) # if called target_link_libraries() more than once, subpkgs contains stuffs like `::@(000001FAFA8C75C0)`
cvpkg_debug(" subpkg: ${subpkg}")
list(APPEND pkg_stack ${subpkg})
endif()
endforeach()
endif()
endif()
endwhile()
list(POP_FRONT visited_pkgs visited_pkgs)
set(${the_result} ${visited_pkgs} PARENT_SCOPE)
endfunction()
检测并拷贝 DLL
这是代码最多的函数, 不过思路上前面已经提到过, 并不复杂。
代码多的几个原因:
- 支持 .dll 的同时, 要支持 .so 和 .dylib
- windows 上的 target, 可能 debug 和 release 库的文件不是同一个,都需要拷贝,因此需要枚举5个属性
set(prop_lst "IMPORTED_LOCATION;IMPORTED_LOCATION_DEBUG;IMPORTED_LOCATION_RELEASE")
- 去重: 拷贝过的文件要忽略, 重复的目录要合并
Talk is cheap, show me the code:
#======================================================================
# Copy imported lib for all build types
# Should only be used for shared libs, e.g. .dll, .so, .dylib
#======================================================================
# Example:
# cvpkg_copy_imported_lib(testbed ${CMAKE_BINARY_DIR}/${testbed_output_dir})
#----------------------------------------------------------------------
function(cvpkg_copy_imported_lib targetName dstDir)
set(prop_lst "IMPORTED_LOCATION;IMPORTED_LOCATION_DEBUG;IMPORTED_LOCATION_RELEASE")
if(NOT (TARGET ${targetName}))
return()
endif()
if(CMAKE_SYSTEM_NAME MATCHES "Windows")
set(shared_library_filename_ext ".dll")
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
set(shared_library_filename_ext ".so")
elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin")
set(shared_library_filename_ext ".dylib")
endif()
get_target_property(pkg_type ${targetName} TYPE)
if(NOT (${pkg_type} STREQUAL "SHARED_LIBRARY"))
if(${pkg_type} STREQUAL "STATIC_LIBRARY")
if(CMAKE_SYSTEM_NAME MATCHES "Windows")
set(static_library_filename_ext ".lib")
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
set(static_library_filename_ext ".a")
elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin")
set(static_library_filename_ext ".a")
endif()
### for static library targets, there might be `bin` directory, parallel to `lib` directory.
# 先获取静态库文件路径
foreach(prop ${prop_lst})
get_target_property(static_library_path ${pkg} ${prop})
if(static_library_path)
# 获取静态库所在目录
get_filename_component(static_library_live_directory ${static_library_path} DIRECTORY)
# 获取静态库目录的上层目录
get_filename_component(static_library_parent_directory ${static_library_live_directory} DIRECTORY)
set(candidate_bin_dir "${static_library_parent_directory}/bin")
# 判断上层目录是否存在 bin 目录, 如果存在 bin 目录, 执行扫描和拷贝
if(EXISTS "${candidate_bin_dir}")
set(glob_pattern "${candidate_bin_dir}/*${shared_library_filename_ext}")
file(GLOB shared_library_path_lst "${glob_pattern}")
foreach(shared_library_path ${shared_library_path_lst})
list(APPEND copied_shared_library_path_lst "${shared_library_path}")
cvpkg_info("Copy ${shared_library_filename_ext} file (for static library, we detect and copy them!)")
cvpkg_info(" - shared library file: ${prop}=${static_library_path}")
cvpkg_info(" - dstDir: ${dstDir}")
execute_process(COMMAND ${CMAKE_COMMAND} -E copy ${shared_library_path} ${dstDir})
endforeach()
endif()
endif()
endforeach()
endif()
return()
endif()
### copy as the package description file (xxx-config.cmake or xxx.cmake) decribed
set(pkg ${targetName})
set(copied_shared_library_path_lst "")
foreach(prop ${prop_lst})
cvpkg_debug("!! prop: ${prop}")
get_target_property(shared_library_path ${pkg} ${prop})
if(shared_library_path)
list(APPEND copied_shared_library_path_lst "${shared_library_path}")
cvpkg_info("Copy ${shared_library_filename_ext} file")
cvpkg_info(" - package(target): ${pkg}")
cvpkg_info(" - prop: ${prop}=${shared_library_path}")
cvpkg_info(" - dstDir: ${dstDir}")
execute_process(COMMAND ${CMAKE_COMMAND} -E copy ${shared_library_path} ${dstDir})
endif()
endforeach()
### copy un-tracked shared library files that under same directory of each tracked shared library files
cvpkg_is_list_empty(copied_shared_library_path_lst copied_shared_library_path_lst_empty)
if(${copied_shared_library_path_lst_empty})
return()
endif()
# get directories of each copied shared library files
set(shared_library_live_directory_lst "")
foreach(copied_shared_library_path ${copied_shared_library_path_lst})
get_filename_component(shared_library_live_directory ${copied_shared_library_path} DIRECTORY)
list(APPEND shared_library_live_directory_lst "${shared_library_live_directory}")
endforeach()
# remove duplicated directories
list(REMOVE_DUPLICATES "${shared_library_live_directory_lst}")
# for each candidate directory, scan shared library files
foreach(shared_library_live_directory ${shared_library_live_directory_lst})
set(glob_pattern "${shared_library_live_directory}/*${shared_library_filename_ext}")
file(GLOB shared_library_path_lst "${glob_pattern}")
foreach(shared_library_path ${shared_library_path_lst})
# if the scanned shared library file is not copied, do a copy
cvpkg_is_item_in_list(copied_shared_library_path_lst "${shared_library_path}" shared_library_already_copied)
if(NOT shared_library_already_copied)
list(APPEND copied_shared_library_path_lst "${shared_library_path}")
cvpkg_info("Copy ${shared_library_filename_ext} file (xxx-config.cmake forget this file, but we copy them!)")
cvpkg_info(" - package(target): ${pkg}")
cvpkg_info(" - prop: ${prop}=${shared_library_path}")
cvpkg_info(" - dstDir: ${dstDir}")
execute_process(COMMAND ${CMAKE_COMMAND} -E copy ${shared_library_path} ${dstDir})
endif()
endforeach()
endforeach()
endfunction()
4. 使用
从使用的角度非常简单:调用 cvpkg_copy_required_dlls()
函数即可,它的实现代码为:
#======================================================================
# Recursively copy required DLL files into destination directory
#======================================================================
# Example:
# cvpkg_copy_required_dlls(testbed ${CMAKE_BINARY_DIR})
# cvpkg_copy_required_dlls(testbed ${CMAKE_BINARY_DIR}/${testbed_output_dir})
#----------------------------------------------------------------------
function(cvpkg_copy_required_dlls targetName dstDir)
cvpkg_get_flatten_requires(testbed flatten_pkgs)
#cvpkg_debug("flatten_pkgs: ${flatten_pkgs}")
message(STATUS "flatten_pkgs: ${flatten_pkgs}")
foreach(pkg ${flatten_pkgs})
cvpkg_copy_imported_lib(${pkg} ${dstDir})
endforeach()
endfunction()
调用代码为:
cvpkg_copy_required_dlls(testbed ${CMAKE_BINARY_DIR})