如何使用CMake将DLL文件复制到可执行文件所在的同一文件夹中?

130

我们使用CMake为SVN中的源代码生成Visual Studio文件。 现在我的工具需要一些DLL文件与可执行文件在同一个文件夹中。 DLL文件在源代码旁边的一个文件夹中。

我应该如何更改我的CMakeLists.txt,以便生成的Visual Studio项目将在发布/调试文件夹中拥有特定的DLL文件,或者在编译时复制它们?


4
CMake 3.21有一个新的方便生成器表达式来处理这种情况。 - Gnimuc
12个回答

155

我会使用add_custom_commandcmake -E copy_if_different...来实现这个目标。了解更多信息,请运行

cmake --help-command add_custom_command
cmake -E
所以在你的情况下,如果你有以下目录结构:
/CMakeLists.txt
/src
/libs/test.dll

如果你的CMake目标是MyTest,并且想要应用该命令,那么可以在CMakeLists.txt中添加以下内容:

add_custom_command(TARGET MyTest POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/res ${CMAKE_BINARY_DIR}/res)

add_custom_command(TARGET MyTest POST_BUILD        # Adds a post-build event to MyTest
    COMMAND ${CMAKE_COMMAND} -E copy_if_different  # which executes "cmake - E copy_if_different..."
        "${PROJECT_SOURCE_DIR}/libs/test.dll"      # <--this is in-file
        $<TARGET_FILE_DIR:MyTest>)                 # <--this is out-file path

如果您只想复制整个/libs/目录的内容,请使用cmake -E copy_directory命令:

add_custom_command(TARGET MyTest POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${PROJECT_SOURCE_DIR}/libs"
        $<TARGET_FILE_DIR:MyTest>)

如果你需要根据不同的配置(例如Release、Debug)复制不同的dll文件,那么你可以在名为相应配置的子目录下放置这些dll文件:/libs/Release/libs/Debug。然后你需要将配置类型注入到add_custom_command调用中的dll路径中,像这样:

add_custom_command(TARGET MyTest POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${PROJECT_SOURCE_DIR}/libs/$<CONFIGURATION>"
        $<TARGET_FILE_DIR:MyTest>)

5
快速记录一下我的情况,以防将来有人需要帮助:我有一个静态库项目,主可执行文件可以选择性地链接它,而该库需要复制一个 DLL 文件。因此,在该库的 CMakeLists.txt 文件中,我使用了 ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$<CONFIG> 作为目标路径。否则,它会将其复制到库构建路径中,这是无用的。 - AberrantWolf
1
cmake的install()指令的目标不是将二进制文件组装起来吗?或者是cmake的LIBRARY相关内容?我并不真正了解这个工具链。 - Sandburg
4
$<TARGET_FILE_DIR:MyTest> - 这是什么?如何打印信息,它确切的意思是什么。 - ilw
除非我在集成命令时漏掉了什么,否则这种方法对于使用IMPORTED_LIBRARIES添加的库不起作用。当没有构建任何内容时,它会抱怨无法运行后构建命令。 - Tzalumen
2
在测试和探测我的cmake时:如果您正在使用IMPORTED库,并且需要重新定位DLL,则需要使用变体命令。 如果您已将IMPORTED库添加为MyImportedLib,则应使用 COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:MyImportedLib> $<TARGET_FILE_DIR:MyTest> 请注意,要运行多个后构建命令,您需要将它们全部绑定到一个自定义命令中,例如 add_custom_command(TARGET MyTest POST_BUILD COMMAND #your first command# COMMAND #Your second command#) - Tzalumen
显示剩余2条评论

33

我把这些行代码放在我的顶层CMakeLists.txt文件中。由CMake编译的所有库和可执行文件都将放置在构建目录的顶层,以便可执行文件可以找到库并且易于运行。

set (CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
请注意,这并不能解决原帖作者从项目源目录复制预编译二进制文件的问题。

26

对于Windows用户,在CMake 3.21+中有一个新的生成器表达式$<TARGET_RUNTIME_DLLS:tgt>,您可以使用这个官方代码片段来复制目标依赖的所有DLL文件。

find_package(foo REQUIRED)

add_executable(exe main.c)
target_link_libraries(exe PRIVATE foo::foo foo::bar)
add_custom_command(TARGET exe POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_RUNTIME_DLLS:exe> $<TARGET_FILE_DIR:exe>
  COMMAND_EXPAND_LISTS
  )

这个问题的一个问题是,如果exe不需要任何DLL,则会失败。您知道是否有一种方法可以解决这个问题,考虑到生成器表达式是在后期评估的吗? - Luke Zhou
不确定我是否理解了你的问题。你是在寻找一种复制那些可执行文件不依赖的dll的方法吗? - Gnimuc
@LukeZhou 这个答案非常接近,但不是你应该做的方式。请看下面我的答案,它可以处理你没有 DLL 的情况。 - midrare
在cmake 3.26中,新的copy -t似乎可以更好地处理这个问题。 - Alec Jacobson
1
同时,这是我的解决方法:COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:exe> $<TARGET_RUNTIME_DLLS:exe> $<TARGET_FILE_DIR:exe>。这也将目标复制到它自己的目录中。这很浪费和愚蠢,但确保如果 *DLLS 是一个空字符串,它仍然是一个有效的命令。 - Alec Jacobson

26

今天我尝试制作程序的Windows版本时遇到了问题。所有这些答案都不能令我满意,所以我不得不自己进行一些研究。主要有三个问题:

  • 我希望将调试版本的库与调试版本的程序进行链接, 并将发布版本的库与发布版本的程序进行链接。

  • 此外,我希望正确版本的DLL文件(Debug/Release)被复制到输出目录中。

  • 同时,我不想编写复杂而容易出错的脚本来实现这一切。

在浏览了一些CMake手册和github上的多平台项目后,我找到了解决方案:

将库声明为“IMPORTED”属性的目标,引用其调试和发布版本的.lib和.dll文件。

add_library(sdl2 SHARED IMPORTED GLOBAL)
set_property(TARGET sdl2 PROPERTY IMPORTED_IMPLIB_RELEASE "${SDL_ROOT_PATH}/lib/SDL2.lib")
set_property(TARGET sdl2 PROPERTY IMPORTED_LOCATION_RELEASE "${SDL_ROOT_PATH}/bin/SDL2.dll")
set_property(TARGET sdl2 PROPERTY IMPORTED_IMPLIB_DEBUG "${SDL_ROOT_PATH}/lib/SDL2d.lib")
set_property(TARGET sdl2 PROPERTY IMPORTED_LOCATION_DEBUG "${SDL_ROOT_PATH}/bin/SDL2d.dll")

像往常一样将此目标与您的项目链接

target_link_libraries(YourProg sdl2 ...)

如果自上次构建以来已更改dll文件,则制作自定义构建步骤将其复制到其目标位置

add_custom_command ( TARGET YourProg POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
    $<TARGET_FILE:sdl2> $<TARGET_FILE_DIR:YourProg>
)

我长期以来一直在努力理解生成器表达式的用处,同时也试图找出一种可靠的方法来实现这一点。这个答案一下子解决了我的两个问题,应该被接受。 - Jesse Wyatt
如果我所有的构建都有相同的库和dll,那么我能否执行相同的命令,例如 set_property(TARGET sdl2 PROPERTY IMPORTED_IMPLIB "${SDL_ROOT_PATH}/lib/SDL2.lib") - KulaGGin
我试过了,但它只复制.lib文件,而不复制.dll文件... - undefined

17
1. 最正确的方法:`install(TARGET_RUNTIME_DLLS)`(CMake >= 3.21)
install(FILES $<TARGET_RUNTIME_DLLS:your_exe_here> TYPE BIN)

为了使其正常工作,您的依赖项的CMake模块必须编写得很好。换句话说,它们使用CMake 3目标,并正确设置了所有目标属性。如果它们正确设置了一切,所有的DLL将会自动收集并与您的可执行文件一起安装。CMake将自动匹配DLL的类型(例如,发布版与调试版)以匹配您的目标可执行文件。
这就是CMake未来的发展方向,也是您应该优先选择的方式。
你应该优先选择使用install()而不是直接将文件复制到$CMAKE_RUNTIME_OUTPUT_DIRECTORY,因为install()是官方认可的CMake方式,用于将文件放置在$CMAKE_RUNTIME_OUTPUT_DIRECTORY中。install()的命名方式很奇怪,因为CMake不仅仅是一个构建工具,它还是一个生成安装程序的工具。这个生成安装程序的功能被称为CPack。在CMake的眼中,$CMAKE_RUNTIME_OUTPUT_DIRECTORY只是CPack的临时存储区。当你使用install()安装一个文件时,你告诉CMake它应该被视为一个程序文件,将随着可执行文件一起复制到其他位置。如果你不通过install()进行安装,CMake将把它视为一个未识别的随机文件。

2. 第二种正确的方式:install(RUNTIME_DEPENDENCIES)(CMake >= 3.21)

install(TARGETS your_exe_here
    RUNTIME ARCHIVE LIBRARY RUNTIME FRAMEWORK BUNDLE PUBLIC_HEADER RESOURCE)
install(TARGETS your_exe_here
    COMPONENT your_exe_here
    RUNTIME_DEPENDENCIES
    PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-"
    POST_EXCLUDE_REGEXES ".*system32/.*\\.dll"
    DIRECTORIES $<TARGET_FILE_DIR:your_exe_here>)

关键在于 RUNTIME_DEPENDENCIES
在内部,RUNTIME_DEPENDENCIES 调用 file(GET_RUNTIME_DEPENDENCIES),它会扫描您的可执行二进制文件,尽最大努力精确地复制实际的依赖解析过程,并记录下沿途提到的所有 DLL。这些 DLL 将传递回到 install()
这意味着它不依赖于您的依赖项的 CMake 模块是否正确设置了目标属性。它会扫描您的实际可执行二进制文件。所有内容都会被捕捉到。
第三种最正确的方法是:file(GET_RUNTIME_DEPENDENCIES)(CMake >= 3.16)。 file(GET_RUNTIME_DEPENDENCIES)install(RUNTIME_DEPENDENCIES) 在幕后调用的函数,但是 file(GET_RUNTIME_DEPENDENCIES) 在较早的 CMake 版本中就可用,而 install(RUNTIME_DEPENDENCIES) 则不是。在旧版本的 CMake 中,我们仍然可以使用相同的方法,只是需要更多的样板代码。
麻煩的部分是,file(GET_RUNTIME_DEPENDENCIES)只能在安裝時調用。這意味著我們需要使用install(CODE)運行一個腳本,該腳本再調用file(GET_RUNTIME_DEPENDENCIES)。有關實現,請參見這裡

4. 最後的手段:install(DIRECTORY)

install(
  DIRECTORY "${DIR_CONTAINING_YOUR_DLLS}"
  TYPE BIN
  FILES_MATCHING REGEX "[^\\\\/.]\\.[dD][lL][lL]$"
)

要使用,请将适用于您构建的DLL放入$DIR_CONTAINING_YOUR_DLLS中。
这里的诀窍是,与install(FILES)不同,install(DIRECTORY)在安装时不关心目录中具体的文件。这意味着我们现在有了配置时间和编译时间来获取您的DLL列表,并将它们放入$DIR_CONTAINING_YOUR_DLLS中。只要在安装时DLL文件在$DIR_CONTAINING_YOUR_DLLS中,install(DIRECTORY)就会将它们拾取起来。 如果您选择这种方法,您有责任将DLL与构建配置匹配。(考虑:静态 vs 动态,调试 vs 发布,导入库版本 vs DLL版本,带有可选多线程的库,忘记删除不再需要的DLL等。)
如果您选择这种方法,您可能希望使用类似于vcpkg的applocal.ps1的东西来自动化DLL的查找和匹配。(假设情况下,可以使用install(CODE)在纯CMake中重新实现vcpkg的applocal.ps1所做的操作,但我没有准备好发布的实现。)

关于 vcpkg 的提示

如果你启用了 VCPKG_APPLOCAL_DEPS 并使用 vcpkg,它会将你的 DLL 文件定位并复制到你的 $CMAKE_RUNTIME_OUTPUT_DIRECTORY 中,但不会通过 install() 进行操作。你需要使用 install(DIRECTORY) 的技巧来让 CMake 加载它们。

(在内部,vcpkg 使用 dumpbinllvm-objdumpobjdump 来扫描你的可执行二进制文件以获取这些文件名。)


1
感谢提供以目标为中心的解决方案,正是我所需要的!我认为这个答案应该比被接受的解决方案获得更多的投票,因为这是CMake现在正在走的方向。虽然我不太满意必须从外部安装中拉取文件到我的安装中,因为像vcpkg这样的包管理器会想要自己管理依赖项(这不是它们存在的原因吗?),但我目前真的看不到其他的选择。除此之外,这就是完美的。 - adentinger
1
@AnthonyD973 这样的评论让我感到开心。 - midrare
不要忘记 .lib 文件 ($<TARGET_LINKER_FILE:tgt>) - starball
@Cobalt 这很奇怪。我不确定为什么会出现这种情况,但我很高兴你找到了一个可行的解决方案。 - undefined
1
@midrare 又有一个更新,我想我找到解决方法了。看起来在从Visual Studio运行/调试时,它使用out/build/...目录下的exe文件。安装命令是有效的,但是将文件放在out/install/目录下,所以当你从VS运行时,它找不到dll文件,一开始似乎什么都没发生。感谢你帮助解决这个问题! - undefined
显示剩余5条评论

7

使用install在构建过程中移动文件

我在尝试按照CMake官方教程第9步操作时遇到了此问题。这是我想要移动的文件位置:

src
 |_build
    |_Debug
       - `MathFunctions.dll`

这是我想要文件所在的位置:

src
 |_build
    |_install
         |_bin
             - `MathFunctions.dll`

由于此DLL是作为共享库生成的,我所做的就是在包含库源代码的子目录 src/Mathfunctions/CMakeLists.txtCMakeLists.txt中添加了这一行。
install(FILES ${PROJECT_BINARY_DIR}/$<CONFIG>/MathFunctions.dll
DESTINATION bin)

感谢您的回答,我能够思考这个问题。只有一行代码,所以我认为没问题。 $<CONFIG> 可以有两个值 DebugRelease ,具体取决于项目如何构建,就像原始问题所要求的那样。

1
老实说,这是最简单有效的答案。 - n0ne

6
您可以使用命令find_library。
find_library(<some_var> NAMES <name_of_lib> PATHS "<path/to/lib>")

如果有定义了EXECUTABLE_PATH,例如:

set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

你可以使用以下方法移动你的可执行文件需要的.dll文件:

file(COPY ${<some_var>}
    DESTINATION ${EXECUTABLE_OUTPUT_PATH})

5

对于已接受的答案,我添加了一个补充,作为单独的答案,以便我获得代码格式:

如果您正在同一项目中构建dll,则通常会在Release、Debug等目录中。您将需要使用Visual Studio环境变量来正确复制它们。例如:

"${PROJECT_BINARY_DIR}/your_library/\$\(Configuration\)/your_library.dll"

源代码和

"${CMAKE_CURRENT_BINARY_DIR}/\$\(Configuration\)/your_library.dll"

针对目的地,注意转义!

你不能使用CMake CMAKE_BUILD_TYPE变量作为配置,因为它在VS项目生成时会被解析,并且始终是默认值。


8
接受答案的最后一部分使用了更清晰的CMake方式来处理这个问题,使用了$<CONFIGURATION> - Chuck Claunch

2

这对它们中的一个很有用

SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
    ${PROJECT_SOURCE_DIR}/lib CACHE
    PATH "Directory where all the .lib files are dumped." FORCE)
SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY
    ${PROJECT_SOURCE_DIR}/bin CACHE
    PATH "Directory where .exe and .dll files are dumped." FORCE)

2

您可能需要添加自定义目标,并使其依赖于其中一个可执行目标。

使用上述函数复制文件:

COMMAND ${CMAKE_PROGRAM} -E copy_if_different ${CMAKE_BINARY_DIR}/path/to/file.dll ${CMAKE_BINARY_DIR}/where/to/put/file.dll`

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接