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_commandwith 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_TYPEis 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_subdirectoryadds 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.
Related reading
- A basic IOCP server on Windows, another Windows and C++ note
- See all technical notes
- Shiina Labs open source