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?

Kevin
  • 16,549
  • 8
  • 60
  • 74
Joachim W
  • 7,290
  • 5
  • 31
  • 59
  • Does this answer your question? [How to copy DLL files into the same folder as the executable using CMake?](https://stackoverflow.com/questions/10671916/how-to-copy-dll-files-into-the-same-folder-as-the-executable-using-cmake) – fdk1342 Dec 17 '19 at 17:02
  • 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. – Joachim W Dec 17 '19 at 17:08
  • 1
    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. – fdk1342 Dec 17 '19 at 17:13

2 Answers2

4

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)
Joachim W
  • 7,290
  • 5
  • 31
  • 59
Kevin
  • 16,549
  • 8
  • 60
  • 74
  • 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()`. – fdk1342 Dec 17 '19 at 17:01
  • @fdk1342 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`](https://cmake.org/cmake/help/latest/prop_tgt/RUNTIME_OUTPUT_DIRECTORY.html#prop_tgt:RUNTIME_OUTPUT_DIRECTORY). – Kevin Dec 17 '19 at 17:12
  • 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? – Joachim W Dec 17 '19 at 17:17
  • 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. – Joachim W Dec 17 '19 at 17:31
  • @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`](https://cmake.org/cmake/help/latest/manual/ctest.1.html#environment-variables), using one of work-arounds suggested in my answer, or those already documented [here](https://stackoverflow.com/a/39807774/3987854). – Kevin Dec 17 '19 at 17:33
  • @JoachimW Yes, if it best solves your issue feel free to modify, or post your own answer! – Kevin Dec 17 '19 at 17:35
2

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.

Roger Leigh
  • 479
  • 4
  • 10