CTest, CMake & MinGW: Executables build, but fail to run, because fresh DLL is not found
Asked Answered
R

5

7

The top-level CMakeLists.txt contains:

include(CTest)
add_subdirectory(lib)
add_subdirectory(demo)
add_subdirectory(test)

lib/CMakeLists.txt is essentially:

add_library(MyLib <sources>)

demo/CMakeLists.txt is essentially:

add_executable(Demo demo.c)
target_link_libraries(Demo MyLib)

test/CMakeLists.txt is just:

add_test(NAME Demo COMMAND Demo)

From a gitlab-runner, we execute:

cmake -G "Ninja" -DCMAKE_INSTALL_PREFIX=C:\opt\x64 -B. ..
cmake --build
ctest --output-on-failure

The first two steps succeed; the third one fails with:

Start 1: Demo
1/1 Test #1: Demo .......................Exit code 0xc0000135
***Exception:   0.03 sec

If I retry:

cmake --install
ctest

then the test succeeds. So the sole problem is that build/lib/mylib.dll is not found when running ctest. Whereas C:\opt\x64\lib is in PATH, and therefore the DLL is found after cmake --install. Which, however, is not what we want: ctest shall always use the fresh DLL from the current build, not the installed version.

Under Linux, everything works correctly. Why doesn't it for Windows and MinGW? Is this a bug in CMake? How can we work around this so that ctest executes correctly on all platforms?

Ratiocination answered 17/12, 2019 at 15:1 Comment(3)
Does this answer your question? How to copy DLL files into the same folder as the executable using CMake?Ramey
I edited my question to make clear that it is not only about a workaround but also about understanding why clean standard CMake code does not work.Ratiocination
It does work correctly. Your runtime environment is wrong and you need to fix it. It's no different from having to set RPATH on Linux if you built a shared library that is already installed and you need to use the correct one.Ramey
S
3

Your issue seems to be the Windows DLL search procedure failing to find mylib.dll when your Demo executable is run by ctest. The Windows DLL search order is specified here:

  1. The directory from which the application loaded.
  2. The system directory. Use the GetSystemDirectory function to get the path of this directory.
  3. The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched.
  4. The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
  5. The current directory.
  6. The directories that are listed in the PATH environment variable. Note that this does not include the per-application path specified by the App Paths registry key. The App Paths key is not used when computing the DLL search path.

So, you could modify your PATH environment variable to also include the location of the fresh DLL from the current build.

A better, less error-prone solution might be to place the DLL in the same directory as the Demo executable. You can force CMake to use the same binary directory for both the DLL and executable by modifying your top-level CMake file:

include(CTest)
add_subdirectory(lib ${CMAKE_BINARY_DIR}/demo)
add_subdirectory(demo ${CMAKE_BINARY_DIR}/demo)
add_subdirectory(test)

Alternatively, as a less localized approach, you can place the DLL in the same directory as the executable by setting CMAKE_RUNTIME_OUTPUT_DIRECTORY:

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

Yet another alternative:

add_test(NAME Demo COMMAND Demo WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
Sericeous answered 17/12, 2019 at 16:23 Comment(6)
I prefer using CMAKE_RUNTIME_OUTPUT_DIRECTORY to put the DLL and exe into the same folder instead of forcing the binary folder location via add_subdirectory().Ramey
@Ramey Yes, this is also a valid approach, although seems to be less localized, as I believe it would apply to all targets. You could set the target-specific property, however, using RUNTIME_OUTPUT_DIRECTORY.Sericeous
The library is our product; the demo programs and the tests are auxiliary. Therefore I would dislike to move the library from lib to demo. Is there an idiomatic way in CMake to tell the demo programs where to find the DLL?Ratiocination
If you don't mind, I'll edit your answer to add a third approach: call add_test with the WORKING_DIRECTORY pointing to lib.Ratiocination
@JoachimW Then, you could instead put the demo build artifacts in the lib folder by changing ${CMAKE_BINARY_DIR}/demo to ${CMAKE_BINARY_DIR}/lib. In general, you can either modify your runtime environment, by updating your PATH environment variable, updating it via ctest, using one of work-arounds suggested in my answer, or those already documented here.Sericeous
@JoachimW Yes, if it best solves your issue feel free to modify, or post your own answer!Sericeous
M
5

A more modern approach using generator expressions and appending to the test environment PATH:

add_test(NAME mytest ...)

set_tests_properties(mytest PROPERTIES ENVIRONMENT_MODIFICATION
                     "PATH=path_list_prepend:$<$<BOOL:${WIN32}>:$<TARGET_FILE_DIR:my_library>>")

This will append the directory containing "my_library" to the PATH used by the test runner when building on Windows, otherwise there will be no change. No need to copy libraries or mess around with the locations of build products, or to alter PATH directly or use a specific working directory.

Malvinamalvino answered 16/8, 2023 at 13:38 Comment(1)
This is a good answer, but on CMake 3.27+ one has access to TARGET_RUNTIME_DLL_DIRS, which does the same job even better, as I explain in my answer. Still +1.Yandell
S
3

Your issue seems to be the Windows DLL search procedure failing to find mylib.dll when your Demo executable is run by ctest. The Windows DLL search order is specified here:

  1. The directory from which the application loaded.
  2. The system directory. Use the GetSystemDirectory function to get the path of this directory.
  3. The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched.
  4. The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
  5. The current directory.
  6. The directories that are listed in the PATH environment variable. Note that this does not include the per-application path specified by the App Paths registry key. The App Paths key is not used when computing the DLL search path.

So, you could modify your PATH environment variable to also include the location of the fresh DLL from the current build.

A better, less error-prone solution might be to place the DLL in the same directory as the Demo executable. You can force CMake to use the same binary directory for both the DLL and executable by modifying your top-level CMake file:

include(CTest)
add_subdirectory(lib ${CMAKE_BINARY_DIR}/demo)
add_subdirectory(demo ${CMAKE_BINARY_DIR}/demo)
add_subdirectory(test)

Alternatively, as a less localized approach, you can place the DLL in the same directory as the executable by setting CMAKE_RUNTIME_OUTPUT_DIRECTORY:

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

Yet another alternative:

add_test(NAME Demo COMMAND Demo WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
Sericeous answered 17/12, 2019 at 16:23 Comment(6)
I prefer using CMAKE_RUNTIME_OUTPUT_DIRECTORY to put the DLL and exe into the same folder instead of forcing the binary folder location via add_subdirectory().Ramey
@Ramey Yes, this is also a valid approach, although seems to be less localized, as I believe it would apply to all targets. You could set the target-specific property, however, using RUNTIME_OUTPUT_DIRECTORY.Sericeous
The library is our product; the demo programs and the tests are auxiliary. Therefore I would dislike to move the library from lib to demo. Is there an idiomatic way in CMake to tell the demo programs where to find the DLL?Ratiocination
If you don't mind, I'll edit your answer to add a third approach: call add_test with the WORKING_DIRECTORY pointing to lib.Ratiocination
@JoachimW Then, you could instead put the demo build artifacts in the lib folder by changing ${CMAKE_BINARY_DIR}/demo to ${CMAKE_BINARY_DIR}/lib. In general, you can either modify your runtime environment, by updating your PATH environment variable, updating it via ctest, using one of work-arounds suggested in my answer, or those already documented here.Sericeous
@JoachimW Yes, if it best solves your issue feel free to modify, or post your own answer!Sericeous
S
1

@alex-reinking's answer is an awesome approach, but it fails when multiple folders are used because the list is not escaped. This one works though:

"PATH=path_list_prepend:$<JOIN:$<TARGET_RUNTIME_DLL_DIRS:Demo>,\;>"

It takes into account that this expansion is only used on windows where ; is the path separator.

Sauceda answered 15/2 at 21:17 Comment(0)
Y
0

An even more modern (CMake 3.27+) approach than the one in Roger's answer is to use TARGET_RUNTIME_DLL_DIRS.

add_test(NAME Demo COMMAND Demo)

set_tests_properties(
    Demo
    PROPERTIES
    ENVIRONMENT_MODIFICATION
        "PATH=path_list_prepend:$<TARGET_RUNTIME_DLL_DIRS:Demo>"
)

This has one major advantage: it doesn't require updates when the dependencies of Demo change!

Yandell answered 13/2 at 19:23 Comment(0)
H
0

Not only it is important having the test environment set up, but you should also make sure the dependencies are correctly installed. In my case I also got "Exit code 0xc0000135" because the Qt dependencies were not present, which on Windows can be installed with windeployqt.

Horsley answered 3/6 at 17:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.