Back to Blog

Teaching CMake to Copy Dependent DLLs on Windows

cmakecppwindowsdllbuild

The short version: CMake does not copy dependent DLLs into the executable target directory, so unit tests fail with a missing xxx.dll. Use add_custom_command with a generator expression to pick the target directory for Ninja (single-config) versus Visual Studio (multi-config).

The problem

I was working on sese-core and hit the missing xxx.dll error again while running unit tests. CMake simply does not copy dependencies into the directory of the executable target that needs them.

The reproduction setup

MyProject
│  CMakeLists.txt
│
├─MyLib
│      CMakeLists.txt
│      MyLib.cpp
│
└─MyTest
        CMakeLists.txt
        MyTest.cpp

MyTest depends on MyLib. When the test target builds, MyLib.dll does not land next to MyTest.exe, and running it fails.

The cause: generators lay out binaries differently

Most answers online stop at “copy the file with add_custom_command”, which is what I did at first. They do not account for the fact that different generators produce a slightly different binary directory layout.

  • Ninja and Makefiles are single-config. Copying straight into the target directory works.
  • Visual Studio generates Debug, Release and the rest in one pass, so the binary directory gains a level named after the configuration. CMAKE_BUILD_TYPE is empty under it and cannot tell you which configuration built.

The fix: add_custom_command with a generator expression

Check whether the generator is Ninja. If it is not, treat it as a Visual Studio multi-config generator and let a generator expression decide the target directory:

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

The later revision

A newer msbuild behaved inconsistently across platforms and named the generated files Debug or Release directly, so I wrapped the whole thing in a macro that supports copying between nested directories and handles the Visual Studio generator explicitly:

macro(sese_copy_dll_to_binary_path target dest)
    if (${CMAKE_GENERATOR} MATCHES "Ninja")
        # single-config
        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")
        # multi-config
        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()

Scope

  • It covers the two cases I actually use: VS Code defaults to the Visual Studio generator, CLion defaults to Ninja.
  • The macro supports nested directories, since add_subdirectory adds a level. Work out your own layout from there.
  • I only adapted and tested it on Windows, and your toolchain versions may differ. Treat it as an approach rather than a finished solution.

Frequently asked questions

Why does a CMake test fail with a missing xxx.dll?

CMake does not copy a dependent DLL into the directory holding the executable target. When MyTest depends on MyLib, MyLib.dll does not appear next to MyTest.exe, and running the test fails.

Why is add_custom_command not enough on its own?

Because generators lay out their binary directories differently. Ninja and Makefiles are single-config. Visual Studio is multi-config and adds a <CONFIG> directory level, and CMAKE_BUILD_TYPE is empty under it. You need a generator expression to find the target directory.

Which generators does this support?

It branches on CMAKE_GENERATOR. Ninja takes the single-config path. Visual Studio takes the multi-config path and distinguishes configurations with $<CONFIG>.

Does this macro work on other platforms?

I only adapted and tested it on Windows, and the toolchain versions may differ from yours. It is more of an approach than a finished solution.