# ============================================================================= # 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 : # out/docs//html/index.html -- via `cmake --build --target docs_` # Plus one landing page linking to all of them: # out/docs/index.html -- via `cmake --build --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 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) # ----------------------------------------------------------------------- # Pinned Doxygen -- deliberately NOT found via find_package(Doxygen) # searching the system. Every machine building this project downloads and # uses the exact same Doxygen version, once, cached outside out/ (in # TOOLCACHE_DIR, see Configuration.cmake) so a clean out/ rebuild doesn't # force a re-download. 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 by design; see the integrity check further down). # ----------------------------------------------------------------------- set(DOXYGEN_PINNED_VERSION "1.16.1" CACHE STRING "Exact Doxygen version this project downloads and uses -- never a system-installed doxygen") option(ENABLE_DOXYGEN "Generate per-module Doxygen documentation using a pinned, project-managed Doxygen (downloaded automatically)" ON) if(ENABLE_DOXYGEN) # Loads the FindDoxygen module (defines doxygen_add_docs(), used # below) and, as a side effect, searches the system for `dot` # (Graphviz) for call graphs -- an optional, system-detected # enhancement. This call's own doxygen-executable detection is # discarded and overridden with our pinned copy regardless of what # (if anything) it finds; only `dot` detection from this is used. find_package(Doxygen QUIET OPTIONAL_COMPONENTS dot) string(REPLACE "." "_" _pmn_doxygen_tag_version "${DOXYGEN_PINNED_VERSION}") set(_pmn_doxygen_tag "Release_${_pmn_doxygen_tag_version}") set(_pmn_doxygen_root "${TOOLCACHE_DIR}/doxygen-${DOXYGEN_PINNED_VERSION}") if(CMAKE_HOST_WIN32) set(_pmn_doxygen_url "https://github.com/doxygen/doxygen/releases/download/${_pmn_doxygen_tag}/doxygen-${DOXYGEN_PINNED_VERSION}.windows.x64.bin.zip") set(_pmn_doxygen_expected_hash "ddfa59a4ae9549651330471c2e387ec7a9891080543faed541c859e5ce448653") set(_pmn_doxygen_exe "${_pmn_doxygen_root}/doxygen.exe") elseif(CMAKE_HOST_UNIX AND NOT CMAKE_HOST_APPLE) set(_pmn_doxygen_url "https://github.com/doxygen/doxygen/releases/download/${_pmn_doxygen_tag}/doxygen-${DOXYGEN_PINNED_VERSION}.linux.bin.tar.gz") set(_pmn_doxygen_expected_hash "a56f885d37e3aae08a99f638d17bbb381224c03a878d9e2dda4f9fa4baf1d8bd") set(_pmn_doxygen_exe "${_pmn_doxygen_root}/doxygen-${DOXYGEN_PINNED_VERSION}/bin/doxygen") else() message(WARNING "No pinned Doxygen ${DOXYGEN_PINNED_VERSION} binary is configured for this host " "platform. Install Doxygen ${DOXYGEN_PINNED_VERSION} manually and pass " "-DDOXYGEN_EXECUTABLE=/path/to/doxygen, or extend cmake/Documentation.cmake " "with a download URL + hash for this platform.") set(ENABLE_DOXYGEN OFF) endif() if(ENABLE_DOXYGEN AND NOT EXISTS "${_pmn_doxygen_exe}") message(STATUS "Downloading pinned Doxygen ${DOXYGEN_PINNED_VERSION} (one-time; 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 # defeat the graceful "disable docs, keep building" fallback below. # Downloading first and verifying the hash ourselves afterward # (further down) lets us tell a network hiccup (recoverable, just # disable docs for this configure) apart from a genuine integrity # failure (not recoverable -- see the FATAL_ERROR below). file(DOWNLOAD "${_pmn_doxygen_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(WARNING "Failed to download pinned Doxygen ${DOXYGEN_PINNED_VERSION}: " "${_pmn_doxygen_dl_msg}. Documentation will be disabled for this configure " "-- everything else builds normally. Reconfigure once you have network " "access to retry.") set(ENABLE_DOXYGEN OFF) else() # Verify integrity ourselves, 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 without updating it): a supply-chain- # relevant integrity failure, not something to silently # degrade past the way a plain network failure above is. file(SHA256 "${_pmn_doxygen_archive}" _pmn_doxygen_actual_hash) string(TOLOWER "${_pmn_doxygen_actual_hash}" _pmn_doxygen_actual_hash) string(TOLOWER "${_pmn_doxygen_expected_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 without updating the matching " "hash in cmake/Documentation.cmake, or the download was corrupted " "or tampered with in transit.") endif() file(ARCHIVE_EXTRACT INPUT "${_pmn_doxygen_archive}" DESTINATION "${_pmn_doxygen_root}") file(REMOVE "${_pmn_doxygen_archive}") if(NOT CMAKE_HOST_WIN32) file(CHMOD "${_pmn_doxygen_exe}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) endif() endif() endif() if(ENABLE_DOXYGEN AND NOT EXISTS "${_pmn_doxygen_exe}") message(WARNING "Pinned Doxygen download/extract did not produce the expected executable at " "${_pmn_doxygen_exe}. Documentation will be disabled for this configure.") set(ENABLE_DOXYGEN OFF) endif() if(ENABLE_DOXYGEN) # Override whatever (if anything) find_package(Doxygen) found # above -- this project never uses a system-installed doxygen. set(DOXYGEN_EXECUTABLE "${_pmn_doxygen_exe}" CACHE FILEPATH "Pinned Doxygen executable (never the system's)" FORCE) set(DOXYGEN_VERSION "${DOXYGEN_PINNED_VERSION}") 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_ variable. # Override any of them with -DDOXYGEN_=... 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 is disabled (ENABLE_DOXYGEN=OFF, or Doxygen isn't installed). Install Doxygen and reconfigure to enable it." VERBATIM ) endif() # ----------------------------------------------------------------------- # _add_module_docs(TARGET): called automatically from _finalize_module_target # for every module -- creates docs_ (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 "
  • ${_pmn_mod}
  • \n") endforeach() file(WRITE "${OUTPUT_DIR}/index.html" " ${PROJECT_NAME} -- module documentation

    ${PROJECT_NAME} module documentation

      ${_pmn_doc_links}
    ") endfunction()