返回博客

记录一次 Windows 下 CMake DLL 文件拷贝问题

cmakecppwindowsdllbuild

一句话结论:CMake 不会把依赖 DLL 拷到可执行目标目录,导致单元测试报“找不到 xxx.dll”。用 add_custom_command + 生成器表达式,按 Ninja(单配置)和 Visual Studio(多配置)区分目标目录即可。

我遇到的问题

最近做 sese-core,运行单元测试时又遇到“找不到 xxx.dll”的报错。原因是 CMake 不会直接把依赖项拷贝到对应的可执行目标文件目录下。

复现结构(示例项目)

MyProject
│  CMakeLists.txt

├─MyLib
│      CMakeLists.txt
│      MyLib.cpp

└─MyTest
        CMakeLists.txt
        MyTest.cpp

MyTest 依赖 MyLib。生成单元测试 MyTest 时,MyLib.dll 并不会拷贝到 MyTest.exe 的同级目录,运行即报错。

原因:不同生成器的目录结构不同

网上大多数答案只提到用 add_custom_command 拷贝文件(我先前也是这么做),但没考虑到不同生成器构建产生的二进制文件目录结构是有些许不同的

  • Ninja / Makefiles:单配置,直接拷到目标目录即可。
  • Visual Studio:一次生成 Debug、Release 等多套配置,二进制目录会多一层同名目录;此时 CMAKE_BUILD_TYPE 是空值,无法用来判断构建所用的配置。

解决方案:add_custom_command + 生成器表达式

判断是否使用了 Ninja;未使用则按 VS 的多配置生成器处理,用生成器表达式决定目标目录:

if (${CMAKE_GENERATOR} MATCHES "Ninja")
    # 单配置
    add_custom_command(
            TARGET
            MyLib
            POST_BUILD
            COMMAND
            ${CMAKE_COMMAND} -E copy_if_different
            $<TARGET_FILE:MyLib> ${PROJECT_BINARY_DIR}/MyTest
    )
else ()
    # 多配置
    add_custom_command(
            TARGET
            MyLib
            POST_BUILD
            COMMAND
            ${CMAKE_COMMAND} -E copy_if_different
            $<TARGET_FILE:MyLib> ${PROJECT_BINARY_DIR}/MyTest/$<CONFIG>
    )
endif ()

后来的改进(2024.06.24)

用最新的 msbuild 时发现存在平台行为不一致:生成的文件会被直接命名为 Debug / Release 等构建类型名称。所以改成封装成宏,支持多层目录之间的拷贝,并针对 Visual Studio 生成器处理:

macro(sese_copy_dll_to_binary_path target dest)
    if (${CMAKE_GENERATOR} MATCHES "Ninja")
        # 单配置
        add_custom_command(
                TARGET
                ${target}
                POST_BUILD
                COMMAND
                ${CMAKE_COMMAND} -E copy_if_different
                $<TARGET_FILE:${target}> "${PROJECT_BINARY_DIR}/${dest}"
        )
    elseif (${CMAKE_GENERATOR} MATCHES "Visual Studio")
        # 多配置
        add_custom_command(
                TARGET
                ${target}
                POST_BUILD
                COMMAND
                ${CMAKE_COMMAND} -E copy_if_different
                $<TARGET_FILE:${target}> "${PROJECT_BINARY_DIR}/${dest}/$<CONFIG>/$<TARGET_FILE_NAME:${target}>"
        )
    endif ()
endmacro()

说明 / 适用范围

  • 指定两种个人常用 case:VS Code 默认用 VS 生成器,CLion 默认用 Ninja 生成器。
  • 宏支持多层目录:用 add_subdirectory 会产生一级新目录,具体情况具体分析。
  • 只适配并测试了 Windows 平台,工具链版本可能与各位不同,更多是提供一种思路,而不是完美的解决方案。

相关阅读

常见问题

为什么 CMake 生成的测试会报找不到 xxx.dll?

CMake 不会自动把依赖的 DLL 拷贝到可执行目标文件所在目录。MyTest 依赖 MyLib 时,MyLib.dll 不会自动出现在 MyTest.exe 旁边,运行即报错。

为什么只加 add_custom_command 还不够?

因为不同生成器的二进制目录结构不同:Ninja / Makefiles 是单配置,Visual Studio 是多配置且多一层 <CONFIG> 目录(此时 CMAKE_BUILD_TYPE 为空)。需要用生成器表达式判断目标目录。

这个方案支持哪些生成器?

通过判断 CMAKE_GENERATOR:Ninja 走单配置路径;Visual Studio 走多配置路径,用 $<CONFIG> 区分。

这个宏在其它平台能用吗?

作者只适配并测试了 Windows 平台,工具链版本也可能不一致,更多是提供一种思路而不是完美方案。