Add project files.

This commit is contained in:
alejo committed 2026-09-12 00:08:03 -03:00
commit 3087fcea8a
71 files changed
+8325

No files matched your search

+381
View File
@@ -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()
+125
View File
@@ -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()
+221
View File
@@ -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()
+53
View File
@@ -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()
+97
View File
@@ -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()
+95
View File
@@ -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()
+54
View File
@@ -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()
+58
View File
@@ -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()