Files
BreezeSDK/breeze/cmake/BuildHelpers.cmake
T
2026-10-24 08:05:45 -03:00

628 lines
30 KiB
CMake

# =============================================================================
# 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_<CONFIG> /
# CMAKE_LIBRARY_OUTPUT_DIRECTORY_<CONFIG> 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_<CONFIG> /
# LIBRARY_OUTPUT_DIRECTORY_<CONFIG> 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} "$<TARGET_FILE_DIR:${TARGET}>"
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 <dest-only, no sources>` 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=$<TARGET_FILE_DIR:${TARGET}>
"-DFILES=$<TARGET_RUNTIME_DLLS:${TARGET}>"
-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 "<target-name>=<absolute-path-to-binary>"
# lines for every executable module into
# OUTPUT_DIR/module_manifest-<CONFIG>.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.
# $<CONFIG> is part of the filename because file(GENERATE) requires that
# for content (like $<TARGET_FILE:...>) 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}=$<TARGET_FILE:${_t}>\n")
endforeach()
file(GENERATE
OUTPUT "${OUTPUT_DIR}/module_manifest-$<CONFIG>.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
$<$<CONFIG:Debug>: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()