Add project files.
This commit is contained in:
commit
3087fcea8a
71 files changed
+8325
No files matched your search
@@ -0,0 +1,381 @@
|
||||
# =============================================================================
|
||||
# BuildHelpers.cmake
|
||||
#
|
||||
# Reusable, project-agnostic build machinery: module discovery, per-target
|
||||
# compiler/RPATH settings, and the module-registration macros (add_lib_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 the root CMakeLists.txt --
|
||||
# see the "Module discovery" section of CMakeLists.txt and the per-module
|
||||
# CMakeLists.txt convention described there.
|
||||
#
|
||||
# Expects the including CMakeLists.txt to have already set (from project()
|
||||
# and the project-identity block): 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-only concept; skip entirely on Windows.
|
||||
# -----------------------------------------------------------------------
|
||||
function(_apply_rpath TARGET RPATH_VAL)
|
||||
if(NOT WIN32)
|
||||
set_target_properties(${TARGET} PROPERTIES
|
||||
BUILD_RPATH "${RPATH_VAL}"
|
||||
INSTALL_RPATH "${RPATH_VAL}"
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# _set_output_dirs(TARGET RUNTIME_DIR [LIBRARY_DIR]):
|
||||
#
|
||||
# CMakeLists.txt 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()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# _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
|
||||
# CMakeLists.txt'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.
|
||||
#
|
||||
# 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 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})
|
||||
|
||||
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})
|
||||
|
||||
if(FIN_OUTPUT_NAME)
|
||||
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 CMakeLists.txt'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)
|
||||
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_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})
|
||||
|
||||
_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})
|
||||
|
||||
_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})
|
||||
|
||||
_finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN})
|
||||
|
||||
add_dependencies(all_examples ${NAME})
|
||||
endmacro()
|
||||
@@ -0,0 +1,125 @@
|
||||
# =============================================================================
|
||||
# Dependencies.cmake
|
||||
#
|
||||
# Three ways to bring in a third-party dependency, and one helper that ties
|
||||
# two of them together. All three keep the dependency's own warnings out of
|
||||
# your build (SYSTEM) and its sources out of compile_commands.json, so
|
||||
# clangd/clang-tidy stay focused on code this project owns.
|
||||
#
|
||||
# add_vendor_header_only(NAME [INCLUDE_DIR <dir>])
|
||||
# A buildless (header-only) drop checked into vendor/<NAME>/include
|
||||
# (or vendor/<INCLUDE_DIR>). Wraps it as an INTERFACE target NAME.
|
||||
#
|
||||
# add_vendor_subdirectory(SUBDIR)
|
||||
# A vendored dependency that ships its own CMakeLists.txt, checked
|
||||
# into vendor/<SUBDIR> (a plain source drop, git submodule, or git
|
||||
# subtree). Whatever target names that CMakeLists.txt defines are
|
||||
# what you DEPENDS on.
|
||||
#
|
||||
# add_dependency(NAME <FetchContent_Declare args...>)
|
||||
# CMake's native find_package + FetchContent integration: pass
|
||||
# FIND_PACKAGE_ARGS ... to try an already-installed copy (an apt/
|
||||
# vcpkg/conan package) first, and only download + build from source
|
||||
# if nothing is found. Omit FIND_PACKAGE_ARGS to always fetch.
|
||||
# https://cmake.org/cmake/help/latest/module/FetchContent.html#integrating-with-find-package
|
||||
#
|
||||
# All three are safe to call more than once for the same name (e.g. two
|
||||
# modules that both need the same dependency) -- later calls are no-ops.
|
||||
#
|
||||
# A module DEPENDS on whatever target the chosen path defines, exactly
|
||||
# like depending on another module -- add_lib_module/add_exe_module don't
|
||||
# need to know or care which of the three supplied it.
|
||||
#
|
||||
# IMPORTANT -- installing: a vendored/fetched dependency's own
|
||||
# CMakeLists.txt usually has its own install() rules, and there is no
|
||||
# reliable, version-independent way to suppress them from here (EXCLUDE_
|
||||
# FROM_ALL's effect on install() is explicitly documented as undefined,
|
||||
# and CMAKE_SKIP_INSTALL_RULES breaks `cmake --install` outright for
|
||||
# FetchContent -- both were tried and rejected). The actual fix lives in
|
||||
# CMakeLists.txt: our own install(TARGETS ...) calls are tagged
|
||||
# COMPONENT ${PROJECT_CODE_NAME}, and scripts/install.sh / install.bat always pass
|
||||
# --component so a third-party dependency's untagged install rules are
|
||||
# never pulled in. Installing with plain `cmake --install build` (no
|
||||
# --component) will also install whatever the dependency wanted to
|
||||
# install, typically under CMAKE_INSTALL_PREFIX (e.g. /usr/local) --
|
||||
# use the install script, or pass --component yourself.
|
||||
# =============================================================================
|
||||
|
||||
include_guard(GLOBAL)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# _exclude_from_compile_commands(DIR): drop every target defined under
|
||||
# DIR from compile_commands.json. Applied to vendored/fetched third-party
|
||||
# code so the exported compile database stays focused on code this
|
||||
# project actually owns.
|
||||
# -----------------------------------------------------------------------
|
||||
function(_exclude_from_compile_commands DIR)
|
||||
if(NOT IS_DIRECTORY "${DIR}")
|
||||
return()
|
||||
endif()
|
||||
get_property(_pmn_dir_targets DIRECTORY "${DIR}" PROPERTY BUILDSYSTEM_TARGETS)
|
||||
foreach(_t ${_pmn_dir_targets})
|
||||
set_target_properties(${_t} PROPERTIES EXPORT_COMPILE_COMMANDS OFF)
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# add_vendor_header_only(NAME [INCLUDE_DIR <dir>])
|
||||
# -----------------------------------------------------------------------
|
||||
macro(add_vendor_header_only NAME)
|
||||
cmake_parse_arguments(ARG "" "INCLUDE_DIR" "" ${ARGN})
|
||||
if(NOT TARGET ${NAME})
|
||||
add_library(${NAME} INTERFACE)
|
||||
if(ARG_INCLUDE_DIR)
|
||||
target_include_directories(${NAME} SYSTEM INTERFACE "${CMAKE_SOURCE_DIR}/vendor/${ARG_INCLUDE_DIR}")
|
||||
else()
|
||||
target_include_directories(${NAME} SYSTEM INTERFACE "${CMAKE_SOURCE_DIR}/vendor/${NAME}/include")
|
||||
endif()
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# add_vendor_subdirectory(SUBDIR)
|
||||
#
|
||||
# EXCLUDE_FROM_ALL means only targets actually depended on via
|
||||
# target_link_libraries get built. SYSTEM marks its headers so our
|
||||
# per-module warning flags (added only by add_lib_module/add_exe_module/
|
||||
# etc., never globally) don't fire when *our* code includes them.
|
||||
# -----------------------------------------------------------------------
|
||||
macro(add_vendor_subdirectory SUBDIR)
|
||||
get_property(_pmn_vendor_added GLOBAL PROPERTY PMN_VENDOR_ADDED_${SUBDIR} SET)
|
||||
if(NOT _pmn_vendor_added)
|
||||
set_property(GLOBAL PROPERTY PMN_VENDOR_ADDED_${SUBDIR} TRUE)
|
||||
add_subdirectory(
|
||||
"${CMAKE_SOURCE_DIR}/vendor/${SUBDIR}"
|
||||
"${CMAKE_BINARY_DIR}/vendor/${SUBDIR}"
|
||||
EXCLUDE_FROM_ALL SYSTEM
|
||||
)
|
||||
_exclude_from_compile_commands("${CMAKE_SOURCE_DIR}/vendor/${SUBDIR}")
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# add_dependency(NAME <FetchContent_Declare args...>)
|
||||
#
|
||||
# Example -- prefer an installed copy, fetch+build from source otherwise:
|
||||
# add_dependency(fmt
|
||||
# GIT_REPOSITORY https://github.com/fmtlib/fmt.git
|
||||
# GIT_TAG 11.0.2
|
||||
# FIND_PACKAGE_ARGS NAMES fmt
|
||||
# )
|
||||
# add_lib_module(mylib DEPENDS fmt::fmt)
|
||||
#
|
||||
# FetchContent_MakeAvailable() is documented idempotent per name, so
|
||||
# calling this more than once for the same NAME (from multiple modules)
|
||||
# is safe.
|
||||
# -----------------------------------------------------------------------
|
||||
macro(add_dependency NAME)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(${NAME} SYSTEM ${ARGN})
|
||||
FetchContent_MakeAvailable(${NAME})
|
||||
string(TOLOWER "${NAME}" _pmn_lc_name)
|
||||
if(DEFINED ${_pmn_lc_name}_SOURCE_DIR)
|
||||
_exclude_from_compile_commands("${${_pmn_lc_name}_SOURCE_DIR}")
|
||||
endif()
|
||||
endmacro()
|
||||
@@ -0,0 +1,221 @@
|
||||
# =============================================================================
|
||||
# Documentation.cmake
|
||||
#
|
||||
# Automatic, per-module Doxygen documentation, using a pinned Doxygen
|
||||
# version this project downloads and manages itself -- never your
|
||||
# system's doxygen, so every machine gets identical output. Zero setup
|
||||
# required: every module registered via add_lib_module/add_exe_module/
|
||||
# add_test_module/add_example_module gets its own documentation target
|
||||
# for free -- nothing to add to a module's own CMakeLists.txt. Vendored
|
||||
# and fetched dependencies (cmake/Dependencies.cmake) are never
|
||||
# auto-documented, same reasoning as their compile_commands.json
|
||||
# exclusion: it's not your code.
|
||||
#
|
||||
# What gets generated, per module <target>:
|
||||
# out/docs/<target>/html/index.html -- via `cmake --build <build-dir> --target docs_<target>`
|
||||
# Plus one landing page linking to all of them:
|
||||
# out/docs/index.html -- via `cmake --build <build-dir> --target docs`
|
||||
# (or just run scripts/docs.sh / docs.bat, which builds `docs` and prints the path)
|
||||
# Paths above use the defaults from Configuration.cmake (DOCS_OUTPUT_DIR).
|
||||
#
|
||||
# Configuring it:
|
||||
# -DENABLE_DOXYGEN=OFF disable entirely (skips the download too)
|
||||
# -DDOXYGEN_PINNED_VERSION=1.17.0 use a different pinned version (also
|
||||
# update the two SHA256 hashes below to match that release's assets)
|
||||
# -DDOXYGEN_WARN_IF_UNDOCUMENTED=YES / -DDOXYGEN_QUIET=NO stricter/louder output
|
||||
# -DDOXYGEN_<ANY_DOXYFILE_TAG>=... any Doxyfile tag can be set this way --
|
||||
# see https://www.doxygen.nl/manual/config.html
|
||||
# A module can opt out with NO_DOCS, e.g.:
|
||||
# add_lib_module(core_lib TYPE SHARED NO_DOCS ...)
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# Documentation.cmake
|
||||
#
|
||||
# Automatic, per-module Doxygen documentation, using a pinned Doxygen
|
||||
# version -- never your system's doxygen, so every machine gets identical
|
||||
# output. Zero setup required at the CMakeLists.txt level: every module
|
||||
# registered via add_lib_module/add_exe_module/add_test_module/
|
||||
# add_example_module gets its own documentation target for free -- nothing
|
||||
# to add to a module's own CMakeLists.txt. Vendored and fetched
|
||||
# dependencies (cmake/Dependencies.cmake) are never auto-documented, same
|
||||
# reasoning as their compile_commands.json exclusion: it's not your code.
|
||||
#
|
||||
# IMPORTANT: this file only ever LOOKS for an already-downloaded pinned
|
||||
# Doxygen (see cmake/DoxygenPin.cmake for where) -- it never downloads
|
||||
# anything itself, so a plain `scripts/build.sh` never touches Doxygen or
|
||||
# needs network access. Run `scripts/setup.sh` / `setup.bat` once (which
|
||||
# runs cmake/FetchDoxygen.cmake) to actually fetch it; until then,
|
||||
# documentation targets print a message pointing at that script instead
|
||||
# of building, and everything else builds completely normally.
|
||||
#
|
||||
# What gets generated, per module <target>, once set up:
|
||||
# out/docs/<target>/html/index.html -- via `cmake --build <build-dir> --target docs_<target>`
|
||||
# Plus one landing page linking to all of them:
|
||||
# out/docs/index.html -- via `cmake --build <build-dir> --target docs`
|
||||
# (or just run scripts/docs.sh / docs.bat, which builds `docs` and prints the path)
|
||||
# Paths above use the defaults from Configuration.cmake (DOCS_OUTPUT_DIR).
|
||||
#
|
||||
# Configuring it:
|
||||
# -DENABLE_DOXYGEN=OFF disable entirely (skips even the presence check)
|
||||
# -DDOXYGEN_PINNED_VERSION=1.17.0 use a different pinned version (also
|
||||
# update the two SHA256 hashes in cmake/DoxygenPin.cmake to match
|
||||
# that release's assets, and re-run scripts/setup.sh)
|
||||
# -DDOXYGEN_WARN_IF_UNDOCUMENTED=YES / -DDOXYGEN_QUIET=NO stricter/louder output
|
||||
# -DDOXYGEN_<ANY_DOXYFILE_TAG>=... any Doxyfile tag can be set this way --
|
||||
# see https://www.doxygen.nl/manual/config.html
|
||||
# A module can opt out with NO_DOCS, e.g.:
|
||||
# add_lib_module(core_lib TYPE SHARED NO_DOCS ...)
|
||||
# =============================================================================
|
||||
|
||||
include_guard(GLOBAL)
|
||||
|
||||
option(ENABLE_DOXYGEN "Generate per-module Doxygen documentation using a pinned, project-managed Doxygen (run scripts/setup.sh once to fetch it)" ON)
|
||||
|
||||
if(ENABLE_DOXYGEN)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/DoxygenPin.cmake")
|
||||
|
||||
if(NOT DOXYGEN_PIN_PLATFORM_SUPPORTED)
|
||||
message(WARNING "No pinned Doxygen ${DOXYGEN_PINNED_VERSION} binary is configured for this "
|
||||
"host platform. Install Doxygen ${DOXYGEN_PINNED_VERSION} manually, make sure "
|
||||
"it's on PATH or pass -DDOXYGEN_EXECUTABLE=/path/to/doxygen, or extend "
|
||||
"cmake/DoxygenPin.cmake with a download URL + hash for this platform.")
|
||||
set(ENABLE_DOXYGEN OFF)
|
||||
elseif(NOT EXISTS "${DOXYGEN_PIN_EXECUTABLE}")
|
||||
# Not fetched yet -- see the file header. Deliberately not a
|
||||
# message() here: this is the normal, expected state before
|
||||
# scripts/setup.sh has ever run, not something to warn about.
|
||||
set(ENABLE_DOXYGEN OFF)
|
||||
else()
|
||||
# Pre-seed DOXYGEN_EXECUTABLE with our pinned path *before*
|
||||
# find_package(Doxygen) runs, rather than overriding it
|
||||
# afterward. find_program() (which find_package uses internally)
|
||||
# does not re-search when the cache variable already holds a
|
||||
# valid path -- so this makes find_package() treat our pinned
|
||||
# copy as the one it found, and run its own normal success path
|
||||
# using it: creating the Doxygen::doxygen imported target,
|
||||
# determining DOXYGEN_VERSION, and generating the Doxyfile
|
||||
# template doxygen_add_docs() needs. All of that only happens as
|
||||
# a side effect of find_package() succeeding -- overriding
|
||||
# DOXYGEN_EXECUTABLE *after* calling it (the previous approach
|
||||
# here) leaves those steps never having run at all, which
|
||||
# doxygen_add_docs() then fails on. This way we don't have to
|
||||
# reimplement any of find_package's own (undocumented, and
|
||||
# confirmed version-dependent -- CMake 3.28 and 4.3 both hit
|
||||
# this differently) internal machinery ourselves.
|
||||
set(DOXYGEN_EXECUTABLE "${DOXYGEN_PIN_EXECUTABLE}" CACHE FILEPATH "Pinned Doxygen executable (never the system's)" FORCE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(ENABLE_DOXYGEN)
|
||||
# Also searches for `dot` (Graphviz) for call graphs -- an optional,
|
||||
# system-detected enhancement, unrelated to the doxygen executable
|
||||
# itself, which is pre-seeded above to always resolve to our pinned
|
||||
# copy rather than a system one.
|
||||
find_package(Doxygen QUIET OPTIONAL_COMPONENTS dot)
|
||||
|
||||
if(NOT DOXYGEN_FOUND)
|
||||
message(WARNING "Pre-seeding DOXYGEN_EXECUTABLE with the pinned Doxygen at "
|
||||
"${DOXYGEN_PIN_EXECUTABLE} did not result in find_package(Doxygen) "
|
||||
"succeeding. Documentation will be disabled for this configure.")
|
||||
set(ENABLE_DOXYGEN OFF)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(ENABLE_DOXYGEN)
|
||||
message(STATUS "Doxygen ${DOXYGEN_VERSION} ready (pinned, not system-installed: ${DOXYGEN_EXECUTABLE}) -- per-module documentation enabled")
|
||||
|
||||
# Project-wide defaults, one Doxyfile tag per DOXYGEN_<TAG> variable.
|
||||
# Override any of them with -DDOXYGEN_<TAG>=... at configure time.
|
||||
# Kept deliberately quiet and lenient by default (no warnings about
|
||||
# undocumented entities) so a freshly generated module with zero
|
||||
# doxygen comments still produces clean, useful output -- browsable
|
||||
# files/functions/call graphs -- instead of a wall of warnings.
|
||||
set(DOXYGEN_GENERATE_HTML YES)
|
||||
set(DOXYGEN_GENERATE_LATEX NO)
|
||||
set(DOXYGEN_EXTRACT_ALL YES)
|
||||
set(DOXYGEN_EXTRACT_STATIC YES)
|
||||
set(DOXYGEN_EXTRACT_PRIVATE NO)
|
||||
set(DOXYGEN_WARN_IF_UNDOCUMENTED NO)
|
||||
set(DOXYGEN_QUIET YES)
|
||||
set(DOXYGEN_RECURSIVE YES)
|
||||
set(DOXYGEN_SOURCE_BROWSER YES)
|
||||
set(DOXYGEN_GENERATE_TREEVIEW YES)
|
||||
if(DOXYGEN_DOT_FOUND)
|
||||
set(DOXYGEN_HAVE_DOT YES)
|
||||
set(DOXYGEN_CALL_GRAPH YES)
|
||||
set(DOXYGEN_CALLER_GRAPH YES)
|
||||
endif()
|
||||
|
||||
add_custom_target(docs COMMENT "Build documentation for every module")
|
||||
else()
|
||||
add_custom_target(docs
|
||||
COMMAND ${CMAKE_COMMAND} -E echo
|
||||
"Documentation isn't set up yet (or ENABLE_DOXYGEN=OFF). Run scripts/setup.sh (or setup.bat) once to fetch the pinned Doxygen, then reconfigure."
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# _add_module_docs(TARGET): called automatically from _finalize_module_target
|
||||
# for every module -- creates docs_<TARGET> (a Doxygen run scoped to that
|
||||
# module's own directory) and wires it into the aggregate `docs` target.
|
||||
# A no-op if ENABLE_DOXYGEN is off. Excluded from the default build same
|
||||
# as tests/examples -- documentation is opt-in per build, not generated
|
||||
# on every compile.
|
||||
#
|
||||
# If the module's own directory has a README.md, it's used as that
|
||||
# module's Doxygen mainpage automatically.
|
||||
# -----------------------------------------------------------------------
|
||||
function(_add_module_docs TARGET)
|
||||
if(NOT ENABLE_DOXYGEN)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(DOXYGEN_OUTPUT_DIRECTORY "${DOCS_OUTPUT_DIR}/${TARGET}")
|
||||
set(DOXYGEN_PROJECT_NAME "${TARGET}")
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
|
||||
set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
|
||||
else()
|
||||
unset(DOXYGEN_USE_MDFILE_AS_MAINPAGE)
|
||||
endif()
|
||||
|
||||
doxygen_add_docs(docs_${TARGET}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
COMMENT "Generating documentation for module '${TARGET}'"
|
||||
)
|
||||
set_target_properties(docs_${TARGET} PROPERTIES EXCLUDE_FROM_ALL TRUE)
|
||||
add_dependencies(docs docs_${TARGET})
|
||||
|
||||
set_property(GLOBAL APPEND PROPERTY PMN_DOC_MODULES ${TARGET})
|
||||
endfunction()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# _write_docs_index(OUTPUT_DIR): call once, after every module has been
|
||||
# discovered, to write a plain landing page at OUTPUT_DIR/index.html
|
||||
# linking to each module's own generated docs -- so "where do I start
|
||||
# reading" has one obvious answer regardless of how many modules exist.
|
||||
# -----------------------------------------------------------------------
|
||||
function(_write_docs_index OUTPUT_DIR)
|
||||
if(NOT ENABLE_DOXYGEN)
|
||||
return()
|
||||
endif()
|
||||
file(MAKE_DIRECTORY "${OUTPUT_DIR}")
|
||||
get_property(_pmn_doc_modules GLOBAL PROPERTY PMN_DOC_MODULES)
|
||||
list(SORT _pmn_doc_modules)
|
||||
set(_pmn_doc_links "")
|
||||
foreach(_pmn_mod ${_pmn_doc_modules})
|
||||
string(APPEND _pmn_doc_links " <li><a href=\"${_pmn_mod}/html/index.html\">${_pmn_mod}</a></li>\n")
|
||||
endforeach()
|
||||
file(WRITE "${OUTPUT_DIR}/index.html" "<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset=\"utf-8\"><title>${PROJECT_NAME} -- module documentation</title></head>
|
||||
<body>
|
||||
<h1>${PROJECT_NAME} module documentation</h1>
|
||||
<ul>
|
||||
${_pmn_doc_links} </ul>
|
||||
</body>
|
||||
</html>
|
||||
")
|
||||
endfunction()
|
||||
@@ -0,0 +1,53 @@
|
||||
# =============================================================================
|
||||
# DoxygenPin.cmake
|
||||
#
|
||||
# Pure computation of "where does the pinned Doxygen live, and where would
|
||||
# it be downloaded from" -- no network calls, no download, no extraction.
|
||||
# Shared by two very different callers, which is why this is its own file:
|
||||
#
|
||||
# cmake/Documentation.cmake include()s this during the normal CMake
|
||||
# configure to find out where to LOOK for an
|
||||
# already-downloaded pinned Doxygen. Never
|
||||
# downloads anything itself -- see that file.
|
||||
#
|
||||
# cmake/FetchDoxygen.cmake include()s this when run standalone via
|
||||
# `cmake -P cmake/FetchDoxygen.cmake` (what
|
||||
# scripts/setup.sh / setup.bat call) to know
|
||||
# what to actually download.
|
||||
#
|
||||
# Needs TOOLCACHE_DIR to already be set -- Documentation.cmake gets it from
|
||||
# Configuration.cmake (included earlier by CMakeLists.txt); FetchDoxygen.cmake
|
||||
# sets it manually since script mode (`cmake -P`) never runs CMakeLists.txt.
|
||||
#
|
||||
# Bump DOXYGEN_PINNED_VERSION to change the pinned version project-wide --
|
||||
# doing so also requires updating the two SHA256 hashes below to match the
|
||||
# new release's assets (a stale hash fails loud and hard, on purpose, in
|
||||
# FetchDoxygen.cmake rather than silently accepting an unexpected file).
|
||||
# =============================================================================
|
||||
|
||||
include_guard(GLOBAL)
|
||||
|
||||
if(NOT DEFINED DOXYGEN_PINNED_VERSION)
|
||||
set(DOXYGEN_PINNED_VERSION "1.16.1")
|
||||
endif()
|
||||
|
||||
string(REPLACE "." "_" _pmn_doxygen_tag_version "${DOXYGEN_PINNED_VERSION}")
|
||||
set(DOXYGEN_PIN_TAG "Release_${_pmn_doxygen_tag_version}")
|
||||
set(DOXYGEN_PIN_ROOT "${TOOLCACHE_DIR}/doxygen-${DOXYGEN_PINNED_VERSION}")
|
||||
|
||||
if(CMAKE_HOST_WIN32)
|
||||
set(DOXYGEN_PIN_URL "https://github.com/doxygen/doxygen/releases/download/${DOXYGEN_PIN_TAG}/doxygen-${DOXYGEN_PINNED_VERSION}.windows.x64.bin.zip")
|
||||
set(DOXYGEN_PIN_HASH "ddfa59a4ae9549651330471c2e387ec7a9891080543faed541c859e5ce448653")
|
||||
set(DOXYGEN_PIN_EXECUTABLE "${DOXYGEN_PIN_ROOT}/doxygen.exe")
|
||||
set(DOXYGEN_PIN_PLATFORM_SUPPORTED TRUE)
|
||||
elseif(CMAKE_HOST_UNIX AND NOT CMAKE_HOST_APPLE)
|
||||
set(DOXYGEN_PIN_URL "https://github.com/doxygen/doxygen/releases/download/${DOXYGEN_PIN_TAG}/doxygen-${DOXYGEN_PINNED_VERSION}.linux.bin.tar.gz")
|
||||
set(DOXYGEN_PIN_HASH "a56f885d37e3aae08a99f638d17bbb381224c03a878d9e2dda4f9fa4baf1d8bd")
|
||||
set(DOXYGEN_PIN_EXECUTABLE "${DOXYGEN_PIN_ROOT}/doxygen-${DOXYGEN_PINNED_VERSION}/bin/doxygen")
|
||||
set(DOXYGEN_PIN_PLATFORM_SUPPORTED TRUE)
|
||||
else()
|
||||
set(DOXYGEN_PIN_URL "")
|
||||
set(DOXYGEN_PIN_HASH "")
|
||||
set(DOXYGEN_PIN_EXECUTABLE "")
|
||||
set(DOXYGEN_PIN_PLATFORM_SUPPORTED FALSE)
|
||||
endif()
|
||||
@@ -0,0 +1,97 @@
|
||||
# =============================================================================
|
||||
# FetchDoxygen.cmake
|
||||
#
|
||||
# Downloads, verifies, and extracts the pinned Doxygen (see DoxygenPin.cmake
|
||||
# for the version/URL/hash) into TOOLCACHE_DIR. Run standalone, NOT included
|
||||
# by the normal CMake configure:
|
||||
#
|
||||
# cmake -P cmake/FetchDoxygen.cmake
|
||||
#
|
||||
# This is what scripts/setup.sh / setup.bat call. It's deliberately not part
|
||||
# of the main `cmake -S . -B out/build` configure -- see cmake/Documentation.cmake,
|
||||
# which only ever LOOKS for the result of this script and never downloads
|
||||
# anything itself, so a plain build never depends on Doxygen or on network
|
||||
# access. Safe to re-run any time; a no-op if the pinned version is already
|
||||
# present.
|
||||
# =============================================================================
|
||||
|
||||
# TOOLCACHE_DIR normally comes from Configuration.cmake, which isn't loaded
|
||||
# in this standalone script-mode invocation -- match its default here. If
|
||||
# you've overridden TOOLCACHE_DIR via -DTOOLCACHE_DIR=... at configure time,
|
||||
# pass the same value here: cmake -DTOOLCACHE_DIR=... -P cmake/FetchDoxygen.cmake
|
||||
if(NOT DEFINED TOOLCACHE_DIR)
|
||||
get_filename_component(_pmn_project_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
set(TOOLCACHE_DIR "${_pmn_project_root}/.cache/tools")
|
||||
endif()
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/DoxygenPin.cmake")
|
||||
|
||||
if(NOT DOXYGEN_PIN_PLATFORM_SUPPORTED)
|
||||
message(FATAL_ERROR "No pinned Doxygen ${DOXYGEN_PINNED_VERSION} binary is configured for this host "
|
||||
"platform. Install Doxygen ${DOXYGEN_PINNED_VERSION} manually, make sure it's on "
|
||||
"PATH or pass -DDOXYGEN_EXECUTABLE=/path/to/doxygen at project-configure time, or "
|
||||
"extend cmake/DoxygenPin.cmake with a download URL + hash for this platform.")
|
||||
endif()
|
||||
|
||||
if(EXISTS "${DOXYGEN_PIN_EXECUTABLE}")
|
||||
message(STATUS "Doxygen ${DOXYGEN_PINNED_VERSION} already present: ${DOXYGEN_PIN_EXECUTABLE}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
message(STATUS "Downloading pinned Doxygen ${DOXYGEN_PINNED_VERSION} (cached under ${TOOLCACHE_DIR})")
|
||||
set(_pmn_doxygen_archive "${TOOLCACHE_DIR}/_doxygen_download.archive")
|
||||
|
||||
# No EXPECTED_HASH here on purpose: file(DOWNLOAD ... EXPECTED_HASH ...)
|
||||
# raises a hard, unrecoverable CMake Error on ANY failure -- including
|
||||
# plain network/connectivity failures -- which would turn a "you're
|
||||
# offline" hiccup into a worse error than it needs to be. Downloading
|
||||
# first and verifying the hash ourselves afterward lets us give a clearer
|
||||
# message for a network failure, while still treating a genuine hash
|
||||
# mismatch (see below) as fully fatal.
|
||||
file(DOWNLOAD "${DOXYGEN_PIN_URL}" "${_pmn_doxygen_archive}"
|
||||
STATUS _pmn_doxygen_dl_status
|
||||
SHOW_PROGRESS
|
||||
)
|
||||
list(GET _pmn_doxygen_dl_status 0 _pmn_doxygen_dl_code)
|
||||
if(NOT _pmn_doxygen_dl_code EQUAL 0)
|
||||
list(GET _pmn_doxygen_dl_status 1 _pmn_doxygen_dl_msg)
|
||||
file(REMOVE "${_pmn_doxygen_archive}")
|
||||
message(FATAL_ERROR "Failed to download pinned Doxygen ${DOXYGEN_PINNED_VERSION}: ${_pmn_doxygen_dl_msg}. "
|
||||
"Check your network connection and try again -- your build doesn't need this to "
|
||||
"succeed, only `scripts/docs.sh` / `docs.bat` does.")
|
||||
endif()
|
||||
|
||||
# Verify integrity, deliberately as a hard failure -- a mismatch here means
|
||||
# the downloaded artifact doesn't match what this project expects
|
||||
# (corruption or tampering in transit, or a stale pinned hash after
|
||||
# bumping DOXYGEN_PINNED_VERSION in DoxygenPin.cmake without updating the
|
||||
# matching hash there): a supply-chain-relevant integrity failure, not
|
||||
# something to silently continue past.
|
||||
file(SHA256 "${_pmn_doxygen_archive}" _pmn_doxygen_actual_hash)
|
||||
string(TOLOWER "${_pmn_doxygen_actual_hash}" _pmn_doxygen_actual_hash)
|
||||
string(TOLOWER "${DOXYGEN_PIN_HASH}" _pmn_doxygen_expected_hash_lc)
|
||||
if(NOT _pmn_doxygen_actual_hash STREQUAL _pmn_doxygen_expected_hash_lc)
|
||||
file(REMOVE "${_pmn_doxygen_archive}")
|
||||
message(FATAL_ERROR "Pinned Doxygen ${DOXYGEN_PINNED_VERSION} download hash mismatch -- "
|
||||
"expected ${_pmn_doxygen_expected_hash_lc}, got ${_pmn_doxygen_actual_hash}. "
|
||||
"This usually means DOXYGEN_PINNED_VERSION was bumped in cmake/DoxygenPin.cmake "
|
||||
"without updating the matching hash there, or the download was corrupted or "
|
||||
"tampered with in transit.")
|
||||
endif()
|
||||
|
||||
file(ARCHIVE_EXTRACT INPUT "${_pmn_doxygen_archive}" DESTINATION "${DOXYGEN_PIN_ROOT}")
|
||||
file(REMOVE "${_pmn_doxygen_archive}")
|
||||
|
||||
if(NOT CMAKE_HOST_WIN32)
|
||||
file(CHMOD "${DOXYGEN_PIN_EXECUTABLE}" PERMISSIONS
|
||||
OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
||||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
endif()
|
||||
|
||||
if(EXISTS "${DOXYGEN_PIN_EXECUTABLE}")
|
||||
message(STATUS "Doxygen ${DOXYGEN_PINNED_VERSION} ready: ${DOXYGEN_PIN_EXECUTABLE}")
|
||||
else()
|
||||
message(FATAL_ERROR "Download/extract completed but the expected executable is missing: "
|
||||
"${DOXYGEN_PIN_EXECUTABLE}")
|
||||
endif()
|
||||
@@ -0,0 +1,95 @@
|
||||
# =============================================================================
|
||||
# FetchNinja.cmake
|
||||
#
|
||||
# Downloads, verifies, and extracts the pinned ninja (see NinjaPin.cmake for
|
||||
# the version/URL/hash) into TOOLCACHE_DIR. Run standalone, never included by
|
||||
# the normal CMake configure -- ninja is needed as the generator itself
|
||||
# (`cmake -G Ninja`), so it has to be resolved *before* configure runs, unlike
|
||||
# Doxygen (see FetchDoxygen.cmake), which is only needed for a downstream
|
||||
# custom target:
|
||||
#
|
||||
# cmake -P cmake/FetchNinja.cmake
|
||||
#
|
||||
# This is what scripts/setup.sh / setup.bat call, and also what scripts/
|
||||
# build.sh / build.bat call on demand if neither a pinned nor a system ninja
|
||||
# is found (see the comment there). Safe to re-run any time; a no-op if the
|
||||
# pinned version is already present.
|
||||
# =============================================================================
|
||||
|
||||
# TOOLCACHE_DIR normally comes from Configuration.cmake, which isn't loaded
|
||||
# in this standalone script-mode invocation -- match its default here. If
|
||||
# you've overridden TOOLCACHE_DIR via -DTOOLCACHE_DIR=... at configure time,
|
||||
# pass the same value here: cmake -DTOOLCACHE_DIR=... -P cmake/FetchNinja.cmake
|
||||
if(NOT DEFINED TOOLCACHE_DIR)
|
||||
get_filename_component(_pmn_project_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
set(TOOLCACHE_DIR "${_pmn_project_root}/.cache/tools")
|
||||
endif()
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/NinjaPin.cmake")
|
||||
|
||||
if(NOT NINJA_PIN_PLATFORM_SUPPORTED)
|
||||
message(FATAL_ERROR "No pinned ninja ${NINJA_PINNED_VERSION} binary is configured for this host "
|
||||
"platform. Install ninja manually and make sure it's on PATH, or extend "
|
||||
"cmake/NinjaPin.cmake with a download URL + hash for this platform.")
|
||||
endif()
|
||||
|
||||
if(EXISTS "${NINJA_PIN_EXECUTABLE}")
|
||||
message(STATUS "ninja ${NINJA_PINNED_VERSION} already present: ${NINJA_PIN_EXECUTABLE}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
message(STATUS "Downloading pinned ninja ${NINJA_PINNED_VERSION} (cached under ${TOOLCACHE_DIR})")
|
||||
set(_pmn_ninja_archive "${TOOLCACHE_DIR}/_ninja_download.archive")
|
||||
|
||||
# No EXPECTED_HASH here on purpose -- see the identical comment in
|
||||
# FetchDoxygen.cmake: file(DOWNLOAD ... EXPECTED_HASH ...) raises a hard,
|
||||
# unrecoverable CMake Error on ANY failure, including plain network
|
||||
# failures. Downloading first and verifying the hash ourselves afterward
|
||||
# lets a network failure and a genuine integrity failure be told apart and
|
||||
# handled differently (see below).
|
||||
file(DOWNLOAD "${NINJA_PIN_URL}" "${_pmn_ninja_archive}"
|
||||
STATUS _pmn_ninja_dl_status
|
||||
SHOW_PROGRESS
|
||||
)
|
||||
list(GET _pmn_ninja_dl_status 0 _pmn_ninja_dl_code)
|
||||
if(NOT _pmn_ninja_dl_code EQUAL 0)
|
||||
list(GET _pmn_ninja_dl_status 1 _pmn_ninja_dl_msg)
|
||||
file(REMOVE "${_pmn_ninja_archive}")
|
||||
message(FATAL_ERROR "Failed to download pinned ninja ${NINJA_PINNED_VERSION}: ${_pmn_ninja_dl_msg}. "
|
||||
"Check your network connection and try again, or install ninja yourself and "
|
||||
"make sure it's on PATH -- scripts/build.sh / build.bat will use a system ninja "
|
||||
"if no pinned one is found.")
|
||||
endif()
|
||||
|
||||
# Verify integrity, deliberately as a hard failure -- see the identical
|
||||
# comment in FetchDoxygen.cmake: a mismatch here means corruption/tampering
|
||||
# in transit, or a stale pinned hash after bumping NINJA_PINNED_VERSION in
|
||||
# NinjaPin.cmake without updating the matching hash there.
|
||||
file(SHA256 "${_pmn_ninja_archive}" _pmn_ninja_actual_hash)
|
||||
string(TOLOWER "${_pmn_ninja_actual_hash}" _pmn_ninja_actual_hash)
|
||||
string(TOLOWER "${NINJA_PIN_HASH}" _pmn_ninja_expected_hash_lc)
|
||||
if(NOT _pmn_ninja_actual_hash STREQUAL _pmn_ninja_expected_hash_lc)
|
||||
file(REMOVE "${_pmn_ninja_archive}")
|
||||
message(FATAL_ERROR "Pinned ninja ${NINJA_PINNED_VERSION} download hash mismatch -- "
|
||||
"expected ${_pmn_ninja_expected_hash_lc}, got ${_pmn_ninja_actual_hash}. "
|
||||
"This usually means NINJA_PINNED_VERSION was bumped in cmake/NinjaPin.cmake "
|
||||
"without updating the matching hash there, or the download was corrupted or "
|
||||
"tampered with in transit.")
|
||||
endif()
|
||||
|
||||
file(ARCHIVE_EXTRACT INPUT "${_pmn_ninja_archive}" DESTINATION "${NINJA_PIN_ROOT}")
|
||||
file(REMOVE "${_pmn_ninja_archive}")
|
||||
|
||||
if(NOT CMAKE_HOST_WIN32)
|
||||
file(CHMOD "${NINJA_PIN_EXECUTABLE}" PERMISSIONS
|
||||
OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
||||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
endif()
|
||||
|
||||
if(EXISTS "${NINJA_PIN_EXECUTABLE}")
|
||||
message(STATUS "ninja ${NINJA_PINNED_VERSION} ready: ${NINJA_PIN_EXECUTABLE}")
|
||||
else()
|
||||
message(FATAL_ERROR "Download/extract completed but the expected executable is missing: "
|
||||
"${NINJA_PIN_EXECUTABLE}")
|
||||
endif()
|
||||
@@ -0,0 +1,54 @@
|
||||
# HotReload.cmake
|
||||
#
|
||||
# One function: hot_reload_enable_patchable_functions(<target>).
|
||||
# Call it on any SHARED library target whose exported functions you
|
||||
# intend to hot-reload with source/HotReload. It applies whichever
|
||||
# compiler flag guarantees enough safely-overwritable space at each
|
||||
# function's entry point for this library's jump-patch backends
|
||||
# (source/HotReload/src/Architecture.cpp) to write into -- without it,
|
||||
# HotReloadContext::reload() falls back to the compiled function's
|
||||
# symbol size (ELF only -- see SymbolTable.cpp) and silently skips
|
||||
# (HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL) anything too small, rather than
|
||||
# corrupting whatever comes after it in memory, but a target built
|
||||
# with this flag applied should never actually hit that fallback.
|
||||
#
|
||||
# 32 bytes covers every backend with margin: ARM64's worst case is a
|
||||
# 4-byte BTI landing pad (only present if the target enables ARM
|
||||
# branch protection) plus a 16-byte jump sequence = 20 bytes; x86-64's
|
||||
# is a 4-byte ENDBR64 landing pad (on by default on some toolchains --
|
||||
# see Architecture.cpp's file comment) plus its 14-byte jump = 18;
|
||||
# plain x86 needs at most 4 + 5 = 9. 32 leaves headroom over the
|
||||
# largest of those without meaningfully affecting code size -- this is
|
||||
# NOP padding, skipped in a few cycles by the branch predictor on
|
||||
# every call until a function is actually reloaded.
|
||||
function(hot_reload_enable_patchable_functions TARGET_NAME)
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
# M (the "before entry" count) is deliberately 0: all 32 NOPs
|
||||
# land AT and after the function's address, which is exactly
|
||||
# where source/HotReload writes into. Confirmed against
|
||||
# `objdump` output while building this library -- see the
|
||||
# design note this function's file comment links back to.
|
||||
target_compile_options(${TARGET_NAME} PRIVATE -fpatchable-function-entry=32,0)
|
||||
elseif(MSVC)
|
||||
# MSVC's own equivalent: reserves a 2-byte pad at every
|
||||
# function's entry plus additional padding immediately before
|
||||
# it, specifically so an external tool can overwrite the entry
|
||||
# with a short jump. See
|
||||
# https://learn.microsoft.com/en-us/cpp/build/reference/hotpatch
|
||||
# This does not take a byte count the way -fpatchable-function-
|
||||
# entry does; the PE export table also gives this library no
|
||||
# per-symbol size the way ELF's symbol table does (see
|
||||
# SymbolTable.cpp's file comment), so on MSVC this flag is not
|
||||
# just recommended but the primary safety mechanism -- there is
|
||||
# no runtime fallback check backing it up here the way there is
|
||||
# on GCC/Clang/ELF.
|
||||
target_compile_options(${TARGET_NAME} PRIVATE /hotpatch)
|
||||
else()
|
||||
message(WARNING
|
||||
"hot_reload_enable_patchable_functions(${TARGET_NAME}): "
|
||||
"unrecognized compiler '${CMAKE_CXX_COMPILER_ID}', no patchable-entry "
|
||||
"flag applied. Hot-reloading ${TARGET_NAME} will still work for any "
|
||||
"function whose compiled body happens to be large enough on its own; "
|
||||
"see HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL in hot_reload.h.")
|
||||
endif()
|
||||
endfunction()
|
||||
@@ -0,0 +1,58 @@
|
||||
# =============================================================================
|
||||
# NinjaPin.cmake
|
||||
#
|
||||
# Pure computation of "where does the pinned ninja live, and where would it
|
||||
# be downloaded from" -- no network calls, no download, no extraction.
|
||||
# Mirrors DoxygenPin.cmake exactly; see that file for the full rationale.
|
||||
# Shared by two very different callers:
|
||||
#
|
||||
# scripts/build.sh / build.bat include() this (via `cmake -P`, same
|
||||
# mechanism as FetchNinja.cmake below) to
|
||||
# find out where to LOOK for an
|
||||
# already-downloaded pinned ninja, before
|
||||
# CMake configure even runs -- ninja is
|
||||
# needed as the generator itself, unlike
|
||||
# Doxygen, which is only ever needed for a
|
||||
# downstream custom target. Never
|
||||
# downloads anything itself.
|
||||
#
|
||||
# cmake/FetchNinja.cmake include()s this when run standalone via
|
||||
# `cmake -P cmake/FetchNinja.cmake` (what
|
||||
# scripts/setup.sh / setup.bat call) to
|
||||
# know what to actually download.
|
||||
#
|
||||
# Needs TOOLCACHE_DIR to already be set. Both callers above set it manually
|
||||
# (matching Configuration.cmake's default) since neither runs through
|
||||
# CMakeLists.txt.
|
||||
#
|
||||
# Bump NINJA_PINNED_VERSION to change the pinned version project-wide --
|
||||
# doing so also requires updating the two SHA256 hashes below to match the
|
||||
# new release's assets (a stale hash fails loud and hard, on purpose, in
|
||||
# FetchNinja.cmake rather than silently accepting an unexpected file).
|
||||
# =============================================================================
|
||||
|
||||
include_guard(GLOBAL)
|
||||
|
||||
if(NOT DEFINED NINJA_PINNED_VERSION)
|
||||
set(NINJA_PINNED_VERSION "1.13.2")
|
||||
endif()
|
||||
|
||||
set(NINJA_PIN_TAG "v${NINJA_PINNED_VERSION}")
|
||||
set(NINJA_PIN_ROOT "${TOOLCACHE_DIR}/ninja-${NINJA_PINNED_VERSION}")
|
||||
|
||||
if(CMAKE_HOST_WIN32)
|
||||
set(NINJA_PIN_URL "https://github.com/ninja-build/ninja/releases/download/${NINJA_PIN_TAG}/ninja-win.zip")
|
||||
set(NINJA_PIN_HASH "07fc8261b42b20e71d1720b39068c2e14ffcee6396b76fb7a795fb460b78dc65")
|
||||
set(NINJA_PIN_EXECUTABLE "${NINJA_PIN_ROOT}/ninja.exe")
|
||||
set(NINJA_PIN_PLATFORM_SUPPORTED TRUE)
|
||||
elseif(CMAKE_HOST_UNIX AND NOT CMAKE_HOST_APPLE)
|
||||
set(NINJA_PIN_URL "https://github.com/ninja-build/ninja/releases/download/${NINJA_PIN_TAG}/ninja-linux.zip")
|
||||
set(NINJA_PIN_HASH "5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6")
|
||||
set(NINJA_PIN_EXECUTABLE "${NINJA_PIN_ROOT}/ninja")
|
||||
set(NINJA_PIN_PLATFORM_SUPPORTED TRUE)
|
||||
else()
|
||||
set(NINJA_PIN_URL "")
|
||||
set(NINJA_PIN_HASH "")
|
||||
set(NINJA_PIN_EXECUTABLE "")
|
||||
set(NINJA_PIN_PLATFORM_SUPPORTED FALSE)
|
||||
endif()
|
||||
Reference in new issue
Block a user