Project layout and files
Here's my MRE project. For convenience, I've published this at https://github.com/alexreinking/so69978314 but everything that's needed is included in the present answer.
This is the directory structure:
$ tree
.
├── CMakeLists.txt
├── include
│ └── myLib.h
├── src
│ └── myLib.cpp
├── test
│ └── test.cpp
3 directories, 4 files
Here are the file contents:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(so69978314)
if (PROJECT_IS_TOP_LEVEL)
include(CTest)
endif ()
option(BUILD_SHARED_LIBS "Build shared libraries instead of static libraries" ON)
find_package(tinyxml2 REQUIRED)
add_library(myLib src/myLib.cpp include/myLib.h)
add_library(myLib::myLib ALIAS myLib)
target_link_libraries(myLib PRIVATE tinyxml2::tinyxml2)
target_include_directories(
myLib PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>")
set_target_properties(myLib PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES)
if (BUILD_TESTING)
find_package(GTest REQUIRED)
include(GoogleTest)
add_executable(unittest test/test.cpp)
target_link_libraries(unittest PRIVATE GTest::gtest GTest::gtest_main myLib::myLib)
if (WIN32)
add_custom_command(
TARGET unittest POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_RUNTIME_DLLS:unittest> $<TARGET_FILE_DIR:unittest>
COMMAND_EXPAND_LISTS
)
endif ()
gtest_discover_tests(unittest)
endif ()
// include/myLib.h
#ifndef MYLIB_H
#define MYLIB_H
bool validate_xml(const char *xml);
#endif
// src/myLib.cpp
#include "myLib.h"
#include <tinyxml2.h>
bool validate_xml(const char *xml) {
tinyxml2::XMLDocument doc;
doc.Parse(xml);
return doc.ErrorID() == 0;
}
// test/test.cpp
#include <myLib.h>
#include <gtest/gtest.h>
namespace {
TEST(MyLibTest, Success) { ASSERT_TRUE(validate_xml("<element/>")); }
TEST(MyLibTest, Fail) { ASSERT_FALSE(validate_xml("<foo></bar>")); }
} // namespace
Explanation
There are two Windows-specific CMake settings here. The first one may be applied unconditionally:
set_target_properties(myLib PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES)
This is for convenience... Linux and macOS shared libraries export all their symbols by default, while Windows does the Right Thingtm and only exposes those that are explicitly exported. I thought that was too much for a StackOverflow answer, so this is a shortcut.
A more principled approach would set:
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN YES)
and use the GenerateExportHeader module to control symbol visibility.
Now, the real meat that makes testing work for the various shared libraries is this:
if (WIN32)
add_custom_command(
TARGET unittest POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_RUNTIME_DLLS:unittest> $<TARGET_FILE_DIR:unittest>
COMMAND_EXPAND_LISTS
)
endif ()
This uses a new feature in CMake 3.21: the $<TARGET_RUNTIME_DLLS:> generator expression. On DLL platforms, it expands to the transitive list of DLLs on which the given target depends. In this case, it will be Google Test's two libraries (gtest.dll, gtest_main.dll), TinyXml2's library (tinyxml2.dll), and the internal myLib.dll.
This is passed to cmake -E copy_if_different to place these DLLs next to the unittest binary. The destination directory is given by $<TARGET_FILE_DIR:unittest>.
The custom command is guarded by if (WIN32) because $<TARGET_RUNTIME_DLLS:unittest> will be empty otherwise. In that case, the cmake -E copy_if_different command won't get enough arguments and will fail at build time.
Finally COMMAND_EXPAND_LISTS makes sure that the semicolon-delimited list returned by $<TARGET_RUNTIME_DLLS:unittest> is split into multiple arguments, rather than being passed as a single argument with escaped semicolons.
To see that this works on Windows, check the output logs of the relevant GitHub Actions workflow: https://github.com/alexreinking/so69978314/runs/6546304512?check_suite_focus=true