# ============================================================================= # BuildHelpers.cmake # # Reusable, project-agnostic build machinery: module discovery, per-target # compiler/RPATH settings, and the module-registration macros (add_lib_module, # add_objects_module, add_exe_module, add_test_module, add_example_module) that # a module's own CMakeLists.txt calls. # # This file is meant to change rarely and hold nothing project-specific. # New targets should not need edits here, or in Breeze.cmake -- # see the "Module discovery" section of Breeze.cmake and the per-module # CMakeLists.txt convention described there. # # Expects the including file (Breeze.cmake) to have already set (from project() # and project/Configuration.cmake): PROJECT_NAME, PROJECT_VERSION, # PROJECT_CODE_NAME. Multiplatform: MSVC/WIN32 branches guard the # Windows-only paths; everything else is generic. # ============================================================================= include_guard(GLOBAL) # ----------------------------------------------------------------------- # all_tests / all_examples -- aggregate targets so groups can be built # without CMake target-name collisions: # cmake --build . --target all_tests # cmake --build . --target all_examples # # enable_testing() is required for add_test_module()'s add_test() calls to # actually be discoverable by ctest -- without it, tests build fine but # `ctest` reports "No tests were found" with no error anywhere else. # ----------------------------------------------------------------------- enable_testing() add_custom_target(all_tests COMMENT "Build all test targets") add_custom_target(all_examples COMMENT "Build all example targets") # ----------------------------------------------------------------------- # _discover_module_subdirectories(ROOT_DIR): add_subdirectory() every # immediate child of ROOT_DIR that contains its own CMakeLists.txt. # # This is the whole mechanism behind "no root edits to add a module": drop # a new directory with a one-line CMakeLists.txt under one of the known # module roots (source/ for libraries and apps, source/tests/ or # source/examples/ for tests and examples) and it is picked up automatically. # CONFIGURE_DEPENDS means the *next build* reconfigures on its own after a # module directory is added or removed -- no manual `cmake` re-run needed. # # Order between add_subdirectory() calls does not matter for correctness: # CMake resolves target_link_libraries() names against the whole project # at generate time, not sequentially, so a module may DEPENDS on a module # discovered later (verified empirically -- this isn't a guess). # # A macro (not a function) so add_subdirectory() behaves exactly as if it # were called directly from the caller -- no extra scope layer involved. # ----------------------------------------------------------------------- macro(_discover_module_subdirectories ROOT_DIR) if(IS_DIRECTORY "${ROOT_DIR}") file(GLOB _pmn_module_candidates CONFIGURE_DEPENDS LIST_DIRECTORIES true "${ROOT_DIR}/*") foreach(_pmn_candidate ${_pmn_module_candidates}) if(IS_DIRECTORY "${_pmn_candidate}" AND EXISTS "${_pmn_candidate}/CMakeLists.txt") add_subdirectory("${_pmn_candidate}") endif() endforeach() endif() endmacro() # ----------------------------------------------------------------------- # _apply_compile_options: per-target warning flags. # MSVC path : /W4 (high warnings) + /permissive- (strict conformance). # GCC/Clang : -Wall -Wextra -Wpedantic. # ----------------------------------------------------------------------- function(_apply_compile_options TARGET) if(MSVC) target_compile_options(${TARGET} PRIVATE /W4 /permissive-) else() target_compile_options(${TARGET} PRIVATE -Wall -Wextra -Wpedantic) endif() endfunction() # ----------------------------------------------------------------------- # _apply_rpath: RPATH is an ELF (Linux) and Mach-O (macOS) concept; skip # entirely on Windows. Callers spell "relative to this binary" the ELF way, # $ORIGIN[/..]; Mach-O's loader has never heard of that token -- its # equivalent is @loader_path -- so it is translated here, once, instead of # every call site having to know which platform it's on. (For an executable # @loader_path is the executable's own directory; for a library, the # library's. That is exactly the layout this project uses: everything # lands flat in bin/, tests and examples one level down in bin/tests and # bin/examples.) # ----------------------------------------------------------------------- function(_apply_rpath TARGET RPATH_VAL) if(NOT WIN32) if(APPLE) string(REPLACE "$ORIGIN" "@loader_path" RPATH_VAL "${RPATH_VAL}") endif() set_target_properties(${TARGET} PROPERTIES BUILD_RPATH "${RPATH_VAL}" INSTALL_RPATH "${RPATH_VAL}" ) endif() endfunction() # ----------------------------------------------------------------------- # _set_output_dirs(TARGET RUNTIME_DIR [LIBRARY_DIR]): # # Breeze.cmake sets CMAKE_RUNTIME_OUTPUT_DIRECTORY_ / # CMAKE_LIBRARY_OUTPUT_DIRECTORY_ globally (once, near the top) so # every target defaults to the flat bin/ layout. That also means every # new target's per-config RUNTIME_OUTPUT_DIRECTORY_ / # LIBRARY_OUTPUT_DIRECTORY_ property is pre-populated at creation # time -- and a per-config property always wins over the plain # (config-independent) one. So a module that needs a *different* output # directory (tests -> bin/tests, examples -> bin/examples) must override # the per-config properties too, or the override silently has no effect. # This helper does both in one place. # ----------------------------------------------------------------------- function(_set_output_dirs TARGET RUNTIME_DIR) set(_library_dir "${RUNTIME_DIR}") if(ARGC GREATER 2) set(_library_dir "${ARGV2}") endif() set_target_properties(${TARGET} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${RUNTIME_DIR}" LIBRARY_OUTPUT_DIRECTORY "${_library_dir}" ) foreach(_cfg Debug Release RelWithDebInfo MinSizeRel) string(TOUPPER "${_cfg}" _CFG) set_target_properties(${TARGET} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_${_CFG} "${RUNTIME_DIR}" LIBRARY_OUTPUT_DIRECTORY_${_CFG} "${_library_dir}" ) endforeach() endfunction() # ----------------------------------------------------------------------- # _copy_mingw_runtime_dlls(TARGET): copies PMN_MINGW_RUNTIME_DLLS (computed # once in Breeze.cmake's "MinGW runtime linking" section) next to # TARGET's own built binary, in the build tree, so it runs without a # manual step. install()-time placement is the matching # install(FILES ...) in Breeze.cmake's "install" section. A no-op # whenever PMN_MINGW_RUNTIME_DLLS is empty (every non-MinGW platform, or # a MinGW toolchain where none of the candidate DLLs were found). # # Called for every SHARED module unconditionally (source/Core's # core_lib/demo_core.dll always uses the dynamic runtime -- see the # "MinGW runtime linking" comment for why making it static too isn't an # option), and for EXECUTABLE/test/example targets only when # PMN_MINGW_STATIC_RUNTIME is OFF (the default): with it ON those are # statically linked and don't need these DLLs, so the caller skips this # call entirely rather than copying files nothing imports. # ----------------------------------------------------------------------- function(_copy_mingw_runtime_dlls TARGET) if(MINGW AND PMN_MINGW_RUNTIME_DLLS) add_custom_command(TARGET ${TARGET} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PMN_MINGW_RUNTIME_DLLS} "$" COMMENT "Copying MinGW runtime DLLs next to ${TARGET}" VERBATIM ) endif() endfunction() # ----------------------------------------------------------------------- # _copy_target_runtime_dlls(TARGET): on Windows (any compiler -- MSVC, # clang-cl, or MinGW), copies every SHARED-library dependency's own .dll # next to TARGET's binary, via CMake's TARGET_RUNTIME_DLLS generator # expression. This is the OTHER half of running on Windows that # _copy_mingw_runtime_dlls doesn't cover: that one only bundles the # compiler's OWN runtime (libgcc/libstdc++/winpthread); this one bundles # this project's SHARED modules (core_lib/demo_core.dll, and anything # else a module's own CMakeLists.txt declares SHARED). # # Needed specifically for add_test_module/add_example_module: they land # one directory below the primary modules (bin/tests, bin/examples), # with an RPATH of "$ORIGIN/.." for the ELF/Mach-O builds where that # works (see _apply_rpath) -- but Windows has no equivalent of a # *relative*, directory-based DLL search path, so a test or example that # depends on a SHARED module built by this project needs that DLL # copied into ITS OWN directory to run at all, not just the primary # one. (The primary modules -- add_exe_module -- don't need this: they # already land in the SAME flat bin/ directory as every SHARED module, # so they already find it there.) Confirmed directly by cross-building # for windows-x64-mingw64 and running the result under Wine: a test # depending on core_lib fails to start (STATUS_DLL_NOT_FOUND, # "libdemo_core.dll ... not found") without this. # # Delegates the actual copy to CopyRuntimeDlls.cmake rather than calling # `cmake -E copy_if_different` directly, because that fails the whole # build the moment TARGET_RUNTIME_DLLS is empty -- the common case for # any test/example that doesn't depend on a SHARED module -- and # `cmake -E copy_if_different ` is exactly that: # zero sources. See that script for the confirmed failure and the # one-line `if(FILES)` fix. # ----------------------------------------------------------------------- function(_copy_target_runtime_dlls TARGET) if(WIN32) add_custom_command(TARGET ${TARGET} POST_BUILD COMMAND ${CMAKE_COMMAND} -DDEST=$ "-DFILES=$" -P "${PMN_BREEZE_CMAKE_DIR}/CopyRuntimeDlls.cmake" COMMENT "Copying ${TARGET}'s own SHARED-library dependencies next to it" VERBATIM ) endif() endfunction() # ----------------------------------------------------------------------- # _register_executable_target(TARGET): tracks every executable module # (exe/test/example) in a global property so _write_module_manifest() # can find them all without anything needing to know their names ahead # of time. # ----------------------------------------------------------------------- function(_register_executable_target TARGET) set_property(GLOBAL APPEND PROPERTY PMN_EXECUTABLE_TARGETS ${TARGET}) endfunction() # ----------------------------------------------------------------------- # _register_module_target(CATEGORY TARGET): tracks a target under # CATEGORY (PRIMARY, TEST, or EXAMPLE) in a global property so # Breeze.cmake's install() rules can be generated from whatever modules # actually got discovered, instead of a hardcoded target list that would # need editing every time a module is added or removed. # ----------------------------------------------------------------------- function(_register_module_target CATEGORY TARGET) set_property(GLOBAL APPEND PROPERTY PMN_MODULE_TARGETS_${CATEGORY} ${TARGET}) endfunction() # ----------------------------------------------------------------------- # _write_module_manifest(OUTPUT_DIR): call once, after every module has # been discovered, to emit "=" # lines for every executable module into # OUTPUT_DIR/module_manifest-.txt. # # breeze/scripts/run.sh / run.bat read this file to resolve a CMake target name to its # actual executable path -- so they never hardcode per-module paths and # never need editing when a module is added, removed, or renamed. # $ is part of the filename because file(GENERATE) requires that # for content (like $) that can differ per build # configuration under a multi-config generator (e.g. Ninja Multi-Config). # ----------------------------------------------------------------------- function(_write_module_manifest OUTPUT_DIR) get_property(_pmn_targets GLOBAL PROPERTY PMN_EXECUTABLE_TARGETS) set(_manifest_content "") foreach(_t ${_pmn_targets}) string(APPEND _manifest_content "${_t}=$\n") endforeach() file(GENERATE OUTPUT "${OUTPUT_DIR}/module_manifest-$.txt" CONTENT "${_manifest_content}" ) endfunction() # ----------------------------------------------------------------------- # _finalize_module_target(TARGET MODULE_NAME [OUTPUT_NAME name] [DEPENDS ...]) # # The tail every module macro below shares: identity compile definitions, # warning flags, OUTPUT_NAME, and DEPENDS linking. Centralized so all four # macros stay in lockstep -- edit once here instead of four times. # Re-parses the caller's raw ARGN, so it's safe to call as # _finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN}) even though # ARGN also contains keywords (TYPE, EXTRA_SOURCES, ...) this function # doesn't know about -- those are simply ignored. # ----------------------------------------------------------------------- # ----------------------------------------------------------------------- # _finalize_module_target(TARGET MODULE_NAME [OUTPUT_NAME name] [DEPENDS ...] # [NO_DOCS]) # # The tail every module macro below shares: identity compile definitions, # warning flags, OUTPUT_NAME, DEPENDS linking, and an automatic per-module # Doxygen target (see breeze/cmake/Documentation.cmake) unless NO_DOCS is given. # Centralized so all four macros stay in lockstep -- edit once here # instead of four times. Re-parses the caller's raw ARGN, so it's safe to # call as _finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN}) even # though ARGN also contains keywords (TYPE, EXTRA_SOURCES, ...) this # function doesn't know about -- those are simply ignored. # ----------------------------------------------------------------------- function(_finalize_module_target TARGET MODULE_NAME) cmake_parse_arguments(FIN "NO_DOCS" "OUTPUT_NAME" "DEPENDS" ${ARGN}) # Every module and what it DEPENDS on, for _resolve_object_modules(): object # modules can't be told apart from libraries here -- the module they name # may not have been discovered yet -- so that is decided once all exist. set_property(GLOBAL APPEND PROPERTY PMN_ALL_MODULES ${TARGET}) set_property(GLOBAL PROPERTY PMN_DEPENDS_${TARGET} "${FIN_DEPENDS}") target_compile_definitions(${TARGET} PRIVATE $<$:DEBUG> MODULE_NAME="${MODULE_NAME}" PROJECT_NAME="${PROJECT_NAME}" PROJECT_NAME_CODE="${PROJECT_CODE_NAME}" PROJECT_VER="${PROJECT_VERSION}" ) _apply_compile_options(${TARGET}) # An objects module has no file to name: for it OUTPUT_NAME is only the name # MODULE_NAME carries (the library it is a piece of, usually). get_target_property(_pmn_target_type ${TARGET} TYPE) if(FIN_OUTPUT_NAME AND NOT _pmn_target_type STREQUAL "OBJECT_LIBRARY") set_target_properties(${TARGET} PROPERTIES OUTPUT_NAME ${FIN_OUTPUT_NAME}) endif() if(FIN_DEPENDS) target_link_libraries(${TARGET} PRIVATE ${FIN_DEPENDS}) endif() if(NOT FIN_NO_DOCS) _add_module_docs(${TARGET}) endif() endfunction() # ============================================================================= # add_lib_module(NAME [TYPE STATIC|SHARED] [OUTPUT_NAME name] [DEPENDS ...] # [EXTRA_SOURCES ...] [NO_DOCS]) # # Called from a library module's own CMakeLists.txt (e.g. source/Core/CMakeLists.txt). # Sources/headers are globbed relative to that module's own directory: # src/*.cpp and include/*.h(pp) underneath it. TYPE picks STATIC or SHARED # for this module only; omit it to use the project-wide default # (BUILD_SHARED_LIBS -- set in Breeze.cmake's OPTIONS section, or pass # -DBUILD_SHARED_LIBS=ON at configure time). # # Headers are globbed in purely so they show up in IDE project trees -- # CMake never compiles headers, so this is inert for the actual build. # ============================================================================= macro(add_lib_module NAME) cmake_parse_arguments(ARG "" "OUTPUT_NAME;TYPE" "DEPENDS;EXTRA_SOURCES" ${ARGN}) if(ARG_OUTPUT_NAME) set(_MODULE_NAME ${ARG_OUTPUT_NAME}) else() set(_MODULE_NAME ${NAME}) endif() if(ARG_TYPE) set(_LIB_TYPE ${ARG_TYPE}) elseif(BUILD_SHARED_LIBS) set(_LIB_TYPE SHARED) else() set(_LIB_TYPE STATIC) endif() if(NOT _LIB_TYPE MATCHES "^(STATIC|SHARED)$") message(FATAL_ERROR "add_lib_module(${NAME}): TYPE must be STATIC or SHARED, got '${_LIB_TYPE}'") endif() file(GLOB_RECURSE ${NAME}_SOURCES CONFIGURE_DEPENDS src/*.cpp) file(GLOB_RECURSE ${NAME}_HEADERS CONFIGURE_DEPENDS include/*.h include/*.hpp) add_library(${NAME} ${_LIB_TYPE} ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES}) target_include_directories(${NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) if(_LIB_TYPE STREQUAL "STATIC") # Keep static libs relocatable in case they end up linked into a # shared library or PIE executable later. set_target_properties(${NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) else() # LIBRARY_OUTPUT_DIRECTORY controls .so placement on Linux. # RUNTIME_OUTPUT_DIRECTORY controls .dll placement on Windows. # WINDOWS_EXPORT_ALL_SYMBOLS auto-generates a .def file so all # public symbols are exported without __declspec(dllexport). # Remove if you prefer explicit visibility macros instead. _set_output_dirs(${NAME} "${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}" "${CMAKE_BINARY_DIR}/${LIBRARY_OUTPUT_SUBDIR}") set_target_properties(${NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) # A SHARED module always keeps the dynamic MinGW runtime -- see # Breeze.cmake's "MinGW runtime linking" comment for why it's # never a candidate for PMN_MINGW_STATIC_RUNTIME -- so this call # is unconditional, unlike the EXECUTABLE-producing macros below. _copy_mingw_runtime_dlls(${NAME}) endif() _register_module_target(PRIMARY ${NAME}) _finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN}) endmacro() # Back-compat wrappers for the old two-macro spelling. # Prefer add_lib_module(... TYPE STATIC|SHARED) directly in new code. macro(add_static_lib_module NAME) add_lib_module(${NAME} TYPE STATIC ${ARGN}) endmacro() macro(add_shared_lib_module NAME) add_lib_module(${NAME} TYPE SHARED ${ARGN}) endmacro() # ============================================================================= # add_objects_module(NAME [OUTPUT_NAME name] [DEPENDS ...] [EXTRA_SOURCES ...] [NO_DOCS]) # # Called from an objects module's own CMakeLists.txt (e.g. source/CoreMath/CMakeLists.txt). # An OBJECT library: its sources are compiled, and that is all -- no archive, no # DLL, nothing in bin/ or lib/. Name it in the DEPENDS of a library, an # executable, a test, an example, or another objects module, and its object # files become part of that target. That is what makes it the way to split # one big library into pieces: each piece is a module of its own (own # directory, own include/, built and tested on its own), and the library # that is left is one line naming them. # # source/CoreMath/CMakeLists.txt add_objects_module(core_math) # source/CoreIo/CMakeLists.txt add_objects_module(core_io DEPENDS core_math) # source/Core/CMakeLists.txt add_lib_module(core DEPENDS core_io) # # Sources/headers are globbed like a library's: src/*.cpp and include/*.h(pp) # under the module's own directory. include/ is public: whoever takes the # objects sees the headers -- and for a library, so do ITS consumers (see # _resolve_object_modules below), because the headers of the pieces are the # library's headers. Keep a piece's private headers in src/. # # OUTPUT_NAME names nothing here -- there is no file -- but is what the # MODULE_NAME definition carries: pass the library's name so the pieces # of one library all identify as it. # # Not installed, not run, not in the module manifest: the objects live on # inside whatever took them. Always compiled position-independent, since # they may land in a shared library. # ============================================================================= macro(add_objects_module NAME) cmake_parse_arguments(ARG "" "OUTPUT_NAME" "DEPENDS;EXTRA_SOURCES" ${ARGN}) if(ARG_OUTPUT_NAME) set(_MODULE_NAME ${ARG_OUTPUT_NAME}) else() set(_MODULE_NAME ${NAME}) endif() file(GLOB_RECURSE ${NAME}_SOURCES CONFIGURE_DEPENDS src/*.cpp) file(GLOB_RECURSE ${NAME}_HEADERS CONFIGURE_DEPENDS include/*.h include/*.hpp) add_library(${NAME} OBJECT ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES}) target_include_directories(${NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) set_target_properties(${NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) # Its own category: install() (Breeze.cmake) only takes PRIMARY, and an # OBJECT library has nothing of its own to install. _register_module_target(OBJECTS ${NAME}) _finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN}) endmacro() # ----------------------------------------------------------------------- # _resolve_object_modules(): call once, after every module has been # discovered (Breeze.cmake does). # # CMake links an OBJECT library's objects into the target that names it, # exactly once -- and no further. A piece that DEPENDS on another piece does # not carry that piece's objects along, so a library naming only the first # one fails at link time with undefined references. And a library that has # taken a piece's objects does not pass the piece's headers on to its own # consumers, unless it links the piece PUBLIC. # # So, for every module target, the objects modules it names -- directly, or # through other objects modules -- are collected, and each is linked: # # * library (static/shared) or objects module: PUBLIC. Objects go into it; # its consumers see the pieces' include/ and compile definitions, and # get no second copy of the objects -- they are inside it already. # * executable (app, test, example): PRIVATE. Nothing consumes it. # # Only objects modules are followed. A module that is an ordinary library # keeps whatever it has inside it; what a piece links (a static library, say) # is reached by CMake on its own at the final link. # # Done here and not in the macros because the macros can't know what a # name in DEPENDS is: modules are discovered in directory order, and the # one named may not exist yet. # ----------------------------------------------------------------------- function(_resolve_object_modules) get_property(_pmn_all_modules GLOBAL PROPERTY PMN_ALL_MODULES) foreach(_pmn_module IN LISTS _pmn_all_modules) get_target_property(_pmn_module_type ${_pmn_module} TYPE) get_property(_pmn_direct GLOBAL PROPERTY PMN_DEPENDS_${_pmn_module}) set(_pmn_queue "${_pmn_direct}") set(_pmn_seen "") set(_pmn_pieces "") while(NOT "${_pmn_queue}" STREQUAL "") list(POP_FRONT _pmn_queue _pmn_name) if(_pmn_name IN_LIST _pmn_seen) continue() endif() list(APPEND _pmn_seen ${_pmn_name}) if(NOT TARGET ${_pmn_name}) continue() endif() get_target_property(_pmn_name_type ${_pmn_name} TYPE) if(NOT _pmn_name_type STREQUAL "OBJECT_LIBRARY") continue() endif() if(_pmn_name STREQUAL _pmn_module) message(FATAL_ERROR "The objects module '${_pmn_module}' depends on itself, through the " "objects modules it names (directly or not) in DEPENDS.") endif() list(APPEND _pmn_pieces ${_pmn_name}) get_property(_pmn_more GLOBAL PROPERTY PMN_DEPENDS_${_pmn_name}) list(APPEND _pmn_queue ${_pmn_more}) endwhile() foreach(_pmn_piece IN LISTS _pmn_pieces) if(_pmn_module_type STREQUAL "EXECUTABLE") # Already linked PRIVATE when named directly (_finalize_module_target). if(NOT _pmn_piece IN_LIST _pmn_direct) target_link_libraries(${_pmn_module} PRIVATE ${_pmn_piece}) endif() else() target_link_libraries(${_pmn_module} PUBLIC ${_pmn_piece}) endif() endforeach() endforeach() endfunction() # ============================================================================= # add_exe_module(NAME [OUTPUT_NAME name] [DEPENDS ...] [EXTRA_SOURCES ...] [NO_DOCS]) # # Called from an app module's own CMakeLists.txt (e.g. HelloApp/CMakeLists.txt). # Sources/headers are globbed relative to that module's own directory: # *.cpp and include/*.h(pp) underneath it. # ============================================================================= macro(add_exe_module NAME) cmake_parse_arguments(ARG "" "OUTPUT_NAME" "DEPENDS;EXTRA_SOURCES" ${ARGN}) if(ARG_OUTPUT_NAME) set(_MODULE_NAME ${ARG_OUTPUT_NAME}) else() set(_MODULE_NAME ${NAME}) endif() file(GLOB_RECURSE ${NAME}_SOURCES CONFIGURE_DEPENDS *.cpp) file(GLOB_RECURSE ${NAME}_HEADERS CONFIGURE_DEPENDS include/*.h include/*.hpp) add_executable(${NAME} ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES}) target_include_directories(${NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) _set_output_dirs(${NAME} "${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}") _apply_rpath(${NAME} "$ORIGIN") _register_module_target(PRIMARY ${NAME}) _register_executable_target(${NAME}) # Skipped when PMN_MINGW_STATIC_RUNTIME is ON: that link option (set in # Breeze.cmake) makes this target self-contained instead, and these # DLLs would be unused files sitting next to it. See that option's own # comment for why OFF (dynamic, copied here) is the safe default. if(NOT PMN_MINGW_STATIC_RUNTIME) _copy_mingw_runtime_dlls(${NAME}) endif() _finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN}) endmacro() # ============================================================================= # add_test_module(NAME [OUTPUT_NAME name] [DEPENDS ...] [EXTRA_SOURCES ...] [NO_DOCS]) # # Called from a test module's own CMakeLists.txt (e.g. source/tests/mytest/CMakeLists.txt). # Lands in bin/tests/, so shared-lib deps one level up need $ORIGIN/... # EXCLUDE_FROM_ALL keeps it out of the default build; BUILD_TESTS opts it # back in. # ============================================================================= macro(add_test_module NAME) cmake_parse_arguments(ARG "" "OUTPUT_NAME" "DEPENDS;EXTRA_SOURCES" ${ARGN}) if(ARG_OUTPUT_NAME) set(_MODULE_NAME ${ARG_OUTPUT_NAME}) else() set(_MODULE_NAME ${NAME}) endif() file(GLOB_RECURSE ${NAME}_SOURCES CONFIGURE_DEPENDS *.cpp) file(GLOB_RECURSE ${NAME}_HEADERS CONFIGURE_DEPENDS include/*.h include/*.hpp) if(BUILD_TESTS) add_executable(${NAME} ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES}) else() add_executable(${NAME} EXCLUDE_FROM_ALL ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES}) endif() target_include_directories(${NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) _set_output_dirs(${NAME} "${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}/tests") _apply_rpath(${NAME} "$ORIGIN/..") _register_module_target(TEST ${NAME}) _register_executable_target(${NAME}) # See add_exe_module's identical guard on the first call: skipped # under PMN_MINGW_STATIC_RUNTIME, which makes this target # self-contained instead. The second call is unconditional and # WIN32-wide (not just MinGW): bin/tests/ is a different directory # from bin/, so any SHARED module this test depends on (this # project's core_lib included) needs its own .dll copied here too -- # see _copy_target_runtime_dlls's own comment for why a *different* # mechanism than the one above is needed for that. if(NOT PMN_MINGW_STATIC_RUNTIME) _copy_mingw_runtime_dlls(${NAME}) endif() _copy_target_runtime_dlls(${NAME}) _finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN}) add_test(NAME ${NAME} COMMAND ${NAME}) add_dependencies(all_tests ${NAME}) endmacro() # ============================================================================= # add_example_module(NAME [OUTPUT_NAME name] [DEPENDS ...] [EXTRA_SOURCES ...] [NO_DOCS]) # # Called from an example module's own CMakeLists.txt (e.g. source/examples/myexample/CMakeLists.txt). # Same bin/examples/ + $ORIGIN/.. layout as tests. # ============================================================================= macro(add_example_module NAME) cmake_parse_arguments(ARG "" "OUTPUT_NAME" "DEPENDS;EXTRA_SOURCES" ${ARGN}) if(ARG_OUTPUT_NAME) set(_MODULE_NAME ${ARG_OUTPUT_NAME}) else() set(_MODULE_NAME ${NAME}) endif() file(GLOB_RECURSE ${NAME}_SOURCES CONFIGURE_DEPENDS *.cpp) file(GLOB_RECURSE ${NAME}_HEADERS CONFIGURE_DEPENDS include/*.h include/*.hpp) if(BUILD_EXAMPLES) add_executable(${NAME} ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES}) else() add_executable(${NAME} EXCLUDE_FROM_ALL ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES}) endif() target_include_directories(${NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) _set_output_dirs(${NAME} "${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}/examples") _apply_rpath(${NAME} "$ORIGIN/..") _register_module_target(EXAMPLE ${NAME}) _register_executable_target(${NAME}) # See add_test_module's identical pair of calls and comment just above. if(NOT PMN_MINGW_STATIC_RUNTIME) _copy_mingw_runtime_dlls(${NAME}) endif() _copy_target_runtime_dlls(${NAME}) _finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN}) add_dependencies(all_examples ${NAME}) endmacro()