As of CMake 3.21 the generator expression $<TARGET_RUNTIME_DLLS:...>
is helpful. It expands to a list of paths (in an unspecified order) to the locations of all the SHARED
libraries in its transitive dependencies.
add_library(lib1 ...)
add_library(lib2 ...)
find_package(lib3 REQUIRED)
add_executable(exe ...)
target_link_libraries(exe PRIVATE lib1 lib2 imported::lib3)
# The following variable is defined only on DLL systems
if (CMAKE_IMPORT_LIBRARY_SUFFIX)
add_custom_command(
TARGET exe POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_RUNTIME_DLLS:exe> $<TARGET_FILE_DIR:exe>
COMMAND_EXPAND_LISTS
)
endif ()
The above code assumes that $<TARGET_RUNTIME_DLLS:...>
will not be empty, hence the check that we are on a DLL system (CMAKE_IMPORT_LIBRARY_SUFFIX
is only defined when this is the case). If it is empty, the command will fail, as not enough arguments will be supplied to cmake -E copy
. I have personally opened an issue about the bad ergonomics of this. https://gitlab.kitware.com/cmake/cmake/-/issues/23543
Note that $<TARGET_RUNTIME_DLLS:...>
only captures CMake targets. This is a good example of why it is a bad idea in general to link only to CMake targets, even if that means creating an imported target yourself.
If you find you're writing this a lot, you can wrap it up into a function:
function(copy_runtime_dlls TARGET)
get_property(already_applied TARGET "${TARGET}" PROPERTY _copy_runtime_dlls_applied)
if (CMAKE_IMPORT_LIBRARY_SUFFIX AND NOT already_applied)
add_custom_command(
TARGET "${TARGET}" POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy
"$<TARGET_RUNTIME_DLLS:${TARGET}>" "$<TARGET_FILE_DIR:${TARGET}>"
COMMAND_EXPAND_LISTS
)
set_property(TARGET "${TARGET}" PROPERTY _copy_runtime_dlls_applied 1)
endif ()
endfunction()
Then just call copy_runtime_dlls(exe)
above.