CMake:递归检查并拷贝所有需要的DLL文件

news2024/11/24 13:43:13

文章目录

    • 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})

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/530337.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

将nacos从本地切换到远程服务器上时报错:客户端端未连接,Client not connected

报错信息: 09:34:38.438 [com.alibaba.nacos.client.Worker] ERROR com.alibaba.nacos.common.remote.client - Send request fail, request ConfigBatchListenRequest{headers{charsetUTF-8, Client-AppNameunknown, Client-RequestToken65c0fbf47282ae0a7b85178…

android点击事件,跳转界面

Android 事件处理 1&#xff0c;采用在Activity中创建一个内部类定义点击事件 主要xml代码 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:app"http:…

【SAP Abap】X-DOC:SE37 - ABAP 功能模块之更新模块(Function Module 之 Update module)

【SAP Abap】X-DOC&#xff1a;SE37 - ABAP 功能模块之更新模块&#xff08;Function Module 之 Update module&#xff09; 1、简介1.1、什么是更新函数1.2、更新函数的类型1.3、更新函数的参数要求1.4、更新函数的调用方式1.5、更新函数的调试方式1.6、更新任务的执行模式1.7…

C语言——控制语句

目录 1. 分支语句1.1 if语句1.1.1 基本结构1.1.2 分层结构1.1.3 嵌套结构 1.2 switch case 语句 2.循环语句2.1 for循环2.1.1 基本结构2.1.2 嵌套结构2.1.3 变形 2.2 while循环2.3 do while循环2.4 死循环2.5 循环控制语句 控制语句即用来实现对程序流程的选择、循环、转向和返…

Shiro框架漏洞分析与复现

Shiro简介 Apache Shiro是一款开源安全框架&#xff0c;提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用&#xff0c;同时也能提供健壮的安全性&#xff0c;可以快速轻松地保护任何应用程序——从最小的移动应用程序到最大的 Web 和企业应用程序。 1、Shiro反序列…

Linux下的线程(线程的同步与互斥)

目录 Linux下线程创建函数pthread_ create() 线程的等待函数pthread_ join() 线程终止 函数pthread exit() 函数pthread_cancel() 分离线程pthread_detach() 线程间的互斥 线程间同步 死锁 进程和线程 线程和进程是一对有意义的概念&#xff0c;主要区别和联系如下&am…

【Linux】常见指令以及权限理解

&#x1f307;个人主页&#xff1a;平凡的小苏 &#x1f4da;学习格言&#xff1a;别人可以拷贝我的模式&#xff0c;但不能拷贝我不断往前的激情 &#x1f6f8;C专栏&#xff1a;Linux修炼内功基地 家人们更新不易&#xff0c;你们的&#x1f44d;点赞&#x1f44d;和⭐关注⭐…

Vue 中动态引入图片为什么要是 require

在vue中动态的引入图片为什么要使用require&#xff1f; 因为动态添加src被当做静态资源处理了&#xff0c;没有进行编译&#xff0c;所以要加上require&#xff0c; 我倒着都能背出来...... emmm... 乍一看好像说的很有道理啊&#xff0c;但是仔细一看&#xff0c;这句话说…

《设计模式》之单例模式

文章目录 1、定义2、动机2、类结构3、单例的表现形式4、总结4、代码实现(C) 1、定义 保证一个类仅有一个实例&#xff0c;并提供一个该实例的全局访问点。 2、动机 在软件系统中&#xff0c;经常有这样一些特殊的类&#xff0c;必须保证它们在系统中只存在一个实例&#xff…

可道云上传文件后报错: Call to undefined function shell_exec()

宝塔面板中直接一键部署的可道云&#xff0c;使用的是PHP8.0环境&#xff0c;上传文件或者点击我刚上传好的文件夹就会报错以下错误&#xff1a; 出错了! (warning!) Call to undefined function shell_exec() 系统错误 fileThumb/app.php[376] fileThumbPlugin->checkB…

UML时序图详解

上篇文章&#xff0c;介绍了UML状态图&#xff0c;并通过visio绘制一个全自动洗衣机的UML状态图实例进行讲解。 本篇&#xff0c;来继续介绍UML中的另一种图——时序图。 1 时序图简介 时序图(Sequence Diagram)&#xff0c;也叫顺序图&#xff0c;或序列图&#xff0c;是一…

基于SpringBoot的招聘信息管理系统设计与实现

前言 本次设计任务是要设计一个招聘信息管理系统&#xff0c;通过这个系统能够满足管理员&#xff0c;用户和企业的招聘信息管理功能。系统的主要功能包括首页、个人中心、用户管理、企业管理、工作类型管理、企业招聘管理、投简信息管理、面试邀请管理、求职信息管理、社区留…

银行数字化转型导师坚鹏:银行数字化转型的5大发展趋势

银行数字化转型的发展趋势主要包括以下5个方面&#xff1a; 从过去的局部数字化转型向全面数字化转型转变&#xff1a;2022年1月&#xff0c;中国银保监会发布《关于银行业保险业数字化转型的指导意见》&#xff0c;标志着中国银行业的数字化转型已经不是过去银行自己主导的局…

简单理解正向代理和反向代理

上一篇文章说到反向代理是用来做负载均衡的&#xff0c;同时我就想到了那么正向代理是不是也可以说一说&#xff0c;可能还是有很多人是弄不清他俩的区别是什么的吧&#xff1f; 那么本次文章就用借钱的例子来阐述一下什么是正向代理&#xff0c;什么是反向代理 正向代理 正…

Android系统的问题分析笔记(4) - Android设备硬件基础

问题 典型的Android手机/平板硬件架构是怎么样的&#xff1f; 1 典型Android手机/平板硬件架构图 2 基带处理器 (Baseband Processor) 市场上大多数的手机采用了相互独立的处理单元来分别处理用户界面软件和射频功能。即&#xff1a;应用处理器 (Application Processor&#…

5年积淀,Mapmost打造连接无限的数字孪生平台

数字孪生是充分利用物理模型、传感器更新、运行历史等数据&#xff0c;集成多学科、多物理量、多尺度、多概率的仿真过程&#xff0c;在虚拟空间中完成映射&#xff0c;从而反映相对应的实体装备的全生命周期过程。在“数字中国”、“实景中国”战略指导下&#xff0c;数字孪生…

【Redis】IO多路复用机制

IO多路复用的概念 IO多路复用其实一听感觉很高大上&#xff0c;但是如果细细的拆分以下&#xff0c; IO&#xff1a;网络IO&#xff0c;操作系统层面指数据在内核态和用户态之间的读写操作。 多路&#xff1a;多个客户端连接(连接就是套接字描述符&#xff0c;即Socket) 复用&…

什么是零知识证明?

零知识证明&#xff08;Zero Knowledge Proof&#xff0c;以下简称ZKP&#xff09;是一种加密学中的重要技术&#xff0c;它可以让一个人向另一个人证明某个事情是真的&#xff0c;而不需要透露这个事情的具体内容&#xff0c;即不需要泄露任何信息。ZKP 技术可以在不牺牲隐私的…

难见的oracle 9i恢复---2023年----惜分飞

时过境迁,以前恢复大量oracle 8/9版本的库,现在一套oracle 9i的库都比较稀奇了.今天恢复客户一套9.2.0.6的aix环境rac库,通过分析确认主要问题: 1. 重建控制文件&#xff0c;resetlogs库遗漏数据文件 2. 数据库启动主要报错ORA-600 2663和ORA-600 kclchkblk_4 Tue Nov 8 09:…

Python dshelper:动动鼠标,搞定数据探索!

本次分享一个Python数据探索小工具dshelper&#xff0c;适合快速查看数据基本特征、数据可视化等使用场景。 无需代码&#xff0c;自动完成数据集描述统计&#xff1b; 无需代码&#xff0c;界面点鼠标绘制多种统计图&#xff1a; 支持命令行、jupyter notebook、docker三种…