From c360c678a42ac194e894caecd8566808cc06f662 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 14 Aug 2026 21:09:07 -0500 Subject: [PATCH] feat(cmake): find_package(mrdocs) find_package(mrdocs) was incorrect and untested. The exported configuration was an old placeholder we never updated, so a consumer find_package(mrdocs) failed. This commit implements this feature so projects can consume mrdocs as an external library to build applications, extensions, and plugins. To make the package useful, we had to bundle the private dependencies (LLVM, Clang, JerryScript, Lua) into the mrdocs library. This is so a consumer can link mrdocs without installing the exact toolchain it was built with. Otherwise, extension users would be required to build and install mrdocs and all its dependencies from source. The change required a reorganization in the src/CMakeLists.txt scripts, as many options were hard-coded and non-idiomatic in ways that don't generalize for an installed package. We remove the old data/cmake/MrDocs.cmake / add_mrdocs() helper, as it was contradictory and hasn't been necessary for the project or maintained for years. It provided no benefit over a regular CMake custom target and had to be kept in sync with the CLI. find_package now sets MRDOCS_EXECUTABLE, making it trivial to create custom targets. We also had to add an option to customize the clang-resource-dir so the installation can remain consistent with the canonical installation directory layout. We added tests/cmake, a standalone find_package(mrdocs) project that links mrdocs::mrdocs-core into a small program and runs the installed binary over its own source. It is exercised in the regular CI build and the release build. --- .github/scripts/install-mrdocs-package.sh | 6 + .github/workflows/ci-build.yml | 56 ++- .github/workflows/ci-releases.yml | 20 +- CMakeLists.txt | 60 ++- data/CMakeLists.txt | 6 +- data/README.md | 1 - data/cmake/MrDocs.cmake | 172 -------- data/mrdocs/headers/libc-stubs/stddef.h | 9 + docs/CMakeLists.txt | 32 +- .../schemas/config/mrdocs.schema.json | 6 + .../ROOT/pages/contribute/codebase-tour.adoc | 1 - .../ROOT/pages/extensions/as-library.adoc | 8 +- docs/mrdocs-build.cmake | 79 ++++ docs/mrdocs.yml | 3 +- examples/CMakeLists.txt | 13 +- examples/configuration/CMakeLists.txt | 2 +- examples/dependencies/CMakeLists.txt | 2 +- examples/generators/CMakeLists.txt | 6 +- examples/getting-started/CMakeLists.txt | 2 +- .../library/breaking-changes/CMakeLists.txt | 28 +- .../library/breaking-changes/src/Corpus.cpp | 13 +- .../library/breaking-changes/src/Corpus.hpp | 3 +- .../library/breaking-changes/src/main.cpp | 14 +- .../library/breaking-changes/test/run.cmake | 28 +- .../mrdocs/Config/ReferenceDirectories.hpp | 22 +- .../Metadata/Specifiers/NoexceptInfo.hpp | 2 +- include/mrdocs/Platform.hpp | 5 + .../mrdocs/Support/Reflection/Describe.hpp | 4 +- libs/CMakeLists.txt | 13 +- .../include/mrdocs/polyfill/expected.hpp | 3 + .../include/mrdocs/polyfill/type_traits.hpp | 23 +- mrdocs-config.cmake.in | 77 ++-- src/CMakeLists.txt | 372 +++++++----------- src/mrdocs/AST/ASTVisitor.cpp | 34 +- src/mrdocs/AST/MrDocsFileSystem.hpp | 2 +- src/mrdocs/Config/ReferenceDirectories.cpp | 133 +++++++ src/mrdocs/ConfigOptions.json | 83 ++-- src/mrdocs/Corpus.cpp | 2 +- src/mrdocs/Engines/Lua.cpp | 4 +- src/mrdocs/Engines/Lua/Scope.ipp | 2 +- src/mrdocs/Generators/noop/NoopGenerator.cpp | 2 +- src/mrdocs/MrDocsCompilationDatabase.cpp | 12 + src/setup-llvm.cmake | 103 +++++ tests/CMakeLists.txt | 10 +- tests/cmake/CMakeLists.txt | 37 ++ tests/cmake/docs/compile_commands.json | 7 + tests/cmake/include/example/calculator.hpp | 33 ++ tests/cmake/mrdocs.yml | 11 + tests/cmake/src/calculator.cpp | 27 ++ tests/cmake/src/main.cpp | 51 +++ tests/golden/CMakeLists.txt | 19 +- tools/CMakeLists.txt | 6 - utils/CMakeLists.txt | 1 + utils/cmake/CMakeLists.txt | 13 + utils/cmake/helpers.cmake | 96 ++++- utils/cmake/install.cmake | 71 ++-- utils/codegen/CMakeLists.txt | 130 ++---- utils/codegen/generate-config-info.py | 7 +- utils/codegen/generate-version-header.py | 101 +++++ 59 files changed, 1350 insertions(+), 738 deletions(-) delete mode 100644 data/cmake/MrDocs.cmake create mode 100644 docs/mrdocs-build.cmake create mode 100644 src/mrdocs/Config/ReferenceDirectories.cpp create mode 100644 src/setup-llvm.cmake create mode 100644 tests/cmake/CMakeLists.txt create mode 100644 tests/cmake/docs/compile_commands.json create mode 100644 tests/cmake/include/example/calculator.hpp create mode 100644 tests/cmake/mrdocs.yml create mode 100644 tests/cmake/src/calculator.cpp create mode 100644 tests/cmake/src/main.cpp create mode 100644 utils/cmake/CMakeLists.txt create mode 100644 utils/codegen/generate-version-header.py diff --git a/.github/scripts/install-mrdocs-package.sh b/.github/scripts/install-mrdocs-package.sh index 269df626b13..413c56c8bfd 100755 --- a/.github/scripts/install-mrdocs-package.sh +++ b/.github/scripts/install-mrdocs-package.sh @@ -49,6 +49,12 @@ echo "::endgroup::" echo "::group::Export environment variables" echo "MRDOCS_ROOT=$MRDOCS_INSTALL_DIR" echo "MRDOCS_ROOT=$MRDOCS_INSTALL_DIR" >> "$GITHUB_ENV" +# CMake-valid absolute prefix for find_package(mrdocs). MRDOCS_ROOT above is the +# Git Bash /d/... form on Windows, which CMake rejects; derive this one from +# GITHUB_WORKSPACE and normalize backslashes instead. +MRDOCS_PREFIX="${GITHUB_WORKSPACE//\\//}/.install/mrdocs" +echo "MRDOCS_PREFIX=$MRDOCS_PREFIX" +echo "MRDOCS_PREFIX=$MRDOCS_PREFIX" >> "$GITHUB_ENV" echo "PATH += $MRDOCS_INSTALL_DIR/bin" echo "$MRDOCS_INSTALL_DIR/bin" >> "$GITHUB_PATH" echo "::endgroup::" diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 2d72aa4ab68..98fd4776d9d 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -79,6 +79,8 @@ jobs: ${{ matrix.bootstrap-sanitizer && format('--sanitizer {0}', matrix.bootstrap-sanitizer) || '' }} \ ${{ matrix.common-ccflags && format('--cflags="{0}" --cxxflags="{0}"', matrix.common-ccflags) || '' }} cat "$RUNNER_TEMP/bootstrap-env.txt" >> "$GITHUB_ENV" + # mrdocs install prefix, reused for the install and the consumer test + echo "MRDOCS_PREFIX=${GITHUB_WORKSPACE//\\//}/.local" >> "$GITHUB_ENV" # Save the LLVM cache when bootstrap rebuilt it (cache miss or # stale stamp). BOOTSTRAP_REBUILT lists rebuilt recipes. @@ -129,7 +131,7 @@ jobs: generator: Ninja build-dir: build/mrdocs build-type: ${{ matrix.build-type }} - install-prefix: .local + install-prefix: ${{ env.MRDOCS_PREFIX }} export-compile-commands: true run-tests: true install: true @@ -139,6 +141,58 @@ jobs: package-artifact: false ctest-timeout: 9000 + # Consume the freshly installed mrdocs through its exported CMake package, + # from an out-of-tree project (tests/cmake): find_package(mrdocs), link + # mrdocs::mrdocs-core, and run the installed binary via MRDOCS_EXECUTABLE. + # This is the only coverage of mrdocs-as-a-library, so it runs on every + # normal build, on every platform (the default static library bundles its + # private toolchain, so this is where we prove a consumer links it with no + # LLVM/Clang/JerryScript/Lua present). It goes through the same + # cmake-workflow action as the main build (configure + build + ctest), + # pointed at tests/cmake, discovering the install through mrdocs_ROOT -- the + # package-specific find_package hint rather than CMAKE_PREFIX_PATH, so the + # _ROOT search path is exercised. Only the ABI flags mrdocs was + # built with are reused (the consumer must match the same standard library), + # not mrdocs's own -Werror/-static/-WX matrix flags. Coverage/sanitizer + # builds use instrumented toolchains an out-of-tree consumer would have to + # mirror exactly, so those are skipped. mrdocs_ROOT gets the MRDOCS_PREFIX + # resolved above. + - name: CMake Consumer Test (cmake test) + if: ${{ !matrix.coverage && !matrix.bootstrap-sanitizer }} + uses: alandefreitas/cpp-actions/cmake-workflow@v1.9.5 + with: + cmake-version: '>=3.26' + source-dir: tests/cmake + build-dir: build/cmake-consumer + cxxstd: ${{ matrix.cxxstd }} + cc: ${{ steps.setup-cpp.outputs.cc || matrix.cc }} + cxx: ${{ steps.setup-cpp.outputs.cxx || matrix.cxx }} + cxxflags: ${{ env.BOOTSTRAP_CXXFLAGS }} + ldflags: ${{ env.BOOTSTRAP_LDFLAGS }} + generator: Ninja + build-type: ${{ matrix.build-type }} + extra-args: -D mrdocs_ROOT=${{ env.MRDOCS_PREFIX }} + run-tests: true + install: false + + - name: CMake Consumer Test (library example) + if: ${{ !matrix.coverage && !matrix.bootstrap-sanitizer }} + uses: alandefreitas/cpp-actions/cmake-workflow@v1.9.5 + with: + cmake-version: '>=3.26' + source-dir: examples/library/breaking-changes + build-dir: build/example-breaking-changes + cxxstd: ${{ matrix.cxxstd }} + cc: ${{ steps.setup-cpp.outputs.cc || matrix.cc }} + cxx: ${{ steps.setup-cpp.outputs.cxx || matrix.cxx }} + cxxflags: ${{ env.BOOTSTRAP_CXXFLAGS }} + ldflags: ${{ env.BOOTSTRAP_LDFLAGS }} + generator: Ninja + build-type: ${{ matrix.build-type }} + extra-args: -D mrdocs_ROOT=${{ env.MRDOCS_PREFIX }} + run-tests: true + install: false + # Upload packages for ci-releases.yml to pick up - name: Upload GitHub Release Artifacts if: matrix.is-release-build == 'true' diff --git a/.github/workflows/ci-releases.yml b/.github/workflows/ci-releases.yml index ab349220bb6..001f5daa868 100644 --- a/.github/workflows/ci-releases.yml +++ b/.github/workflows/ci-releases.yml @@ -49,7 +49,7 @@ jobs: - name: Install packages uses: alandefreitas/cpp-actions/package-install@v1.9.5 with: - apt-get: build-essential asciidoctor cmake bzip2 git rsync + apt-get: build-essential asciidoctor cmake ninja-build bzip2 git rsync - name: Clone MrDocs uses: actions/checkout@v6 @@ -101,6 +101,24 @@ jobs: - name: Install MrDocs from Package run: .github/scripts/install-mrdocs-package.sh + # Discovers the package through mrdocs_ROOT (the package-specific + # find_package hint) rather than CMAKE_PREFIX_PATH. install-mrdocs-package.sh + # exports MRDOCS_PREFIX as its CMake-valid form. + - name: CMake Consumer Test (release package) + uses: alandefreitas/cpp-actions/cmake-workflow@v1.9.5 + with: + cmake-version: '>=3.26' + source-dir: tests/cmake + build-dir: build/cmake-consumer + cxxstd: ${{ matrix.cxxstd }} + cc: ${{ steps.setup-cpp.outputs.cc || matrix.cc }} + cxx: ${{ steps.setup-cpp.outputs.cxx || matrix.cxx }} + generator: Ninja + build-type: ${{ matrix.build-type }} + extra-args: -D mrdocs_ROOT=${{ env.MRDOCS_PREFIX }} + run-tests: true + install: false + - name: Clone Boost.URL uses: alandefreitas/cpp-actions/boost-clone@v1.9.5 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d87470fb67..3f323618685 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,34 +32,49 @@ include(utils/cmake/helpers.cmake) # Project options # #------------------------------------------------- -option(MRDOCS_INSTALL "Configure install target" ON) -option(MRDOCS_PACKAGE "Build install package" ON) -option(MRDOCS_BUILD_SHARED "Link shared" ${BUILD_SHARED_LIBS}) +# What to build +option(MRDOCS_MRDOCS_BUILD "Build only public-headers for documentation self-reference" OFF) +if (MRDOCS_MRDOCS_BUILD) + include(docs/mrdocs-build.cmake) + return() +endif() option(MRDOCS_BUILD_TESTS "Build tests" ${BUILD_TESTING}) +option(MRDOCS_BUILD_STRICT_TESTS "Enable costly strict tests" ON) option(MRDOCS_BUILD_EXAMPLES "Build the examples" ${MRDOCS_BUILD_TESTS}) +option(MRDOCS_BUILD_DOCS "Build documentation" OFF) +option(MRDOCS_GENERATE_REFERENCE "Generate MrDocs reference" ${MRDOCS_BUILD_DOCS}) +option(MRDOCS_INSTALL "Configure install target" ON) +option(MRDOCS_PACKAGE "Build install package" ON) if (MRDOCS_BUILD_TESTS OR MRDOCS_BUILD_EXAMPLES) enable_testing() include(CTest) endif() -option(MRDOCS_BUILD_STRICT_TESTS "Enable strict tests" ON) + +# How to build option(MRDOCS_REQUIRE_GIT "Git is required: not being able to extract version build is an error" ON) -option(MRDOCS_BUILD_DOCS "Build documentation" OFF) -option(MRDOCS_BUILD_HEADERS_ONLY "Build only public-headers for self-reference" OFF) -option(MRDOCS_GENERATE_REFERENCE "Generate MrDocs reference" ${MRDOCS_BUILD_DOCS}) +option(MRDOCS_BUNDLE_DEPENDENCIES "Bundle the private dependencies into the library" ON) +option(MRDOCS_BUILD_SHARED "Link shared" ${BUILD_SHARED_LIBS}) option(MRDOCS_GENERATE_ANTORA_REFERENCE "Generate MrDocs reference in Antora module pages" OFF) +# Helper variables based on options set_ternary(MRDOCS_LINK_MODE MRDOCS_BUILD_SHARED SHARED "") set_ternary(MRDOCS_LINK_MODE_DEFINITION MRDOCS_BUILD_SHARED MRDOCS_SHARED_LINK MRDOCS_STATIC_LINK) +set_ternary(MRDOCS_DO_BUNDLE_DEPENDENCIES "MRDOCS_BUNDLE_DEPENDENCIES AND NOT MRDOCS_BUILD_SHARED" ON OFF) +set_ternary(MRDOCS_DO_REEXPORT_DEPENDENCIES "NOT MRDOCS_DO_BUNDLE_DEPENDENCIES AND NOT MRDOCS_BUILD_SHARED" ON OFF) set_ternary(MRDOCS_GCC "CMAKE_CXX_COMPILER_ID STREQUAL \"GNU\"" ON OFF) set_ternary(MRDOCS_CLANG "CMAKE_CXX_COMPILER_ID MATCHES \"Clang$\"" ON OFF) set_property(GLOBAL PROPERTY USE_FOLDERS ON) - +# MSVC flags shared by every mrdocs target below (not installed consumers): +# - /EHs enables C++ exception handling. The mrdocs tool, the unit tests, and the +# examples are separate targets whose TUs include exception-using STL headers, so +# without it MSVC raises C4530 and /WX makes it fatal; the EH model must also be +# uniform across all TUs, so it belongs here rather than on mrdocs-core alone. +# - /wd4100 accepts unused parameters, an accepted style across mrdocs (visitor / +# tag_invoke / finalizer signatures) that the clang build already allows via +# -Wno-unused-parameter; mirror it so /W4 /WX does not fail on C4100. if (MSVC) - # Suppressions we use to get from LLVM's HandleLLVMOptions.cmake - add_compile_options(/wd4100 /wd4127 /wd4141 /wd4146 /wd4204 /wd4244 /wd4245 /wd4267 /wd4291 /wd4310 /wd4319 /wd4324 /wd4351 /wd4389 /wd4456 /wd4457 /wd4458 /wd4459 /wd4503 /wd4505 /wd4510 /wd4512 /wd4577 /wd4592 /wd4610 /wd4611 /wd4624 /wd4701 /wd4702 /wd4703 /wd4706 /wd4722 /wd4805 /wd5105) - # Conformance flags we used to get from LLVM's HandleLLVMOptions.cmake - add_compile_options(/Zc:__cplusplus /Zc:preprocessor) + add_compile_options(/EHs /wd4100) endif() #------------------------------------------------- @@ -70,14 +85,17 @@ endif() add_subdirectory(utils) add_subdirectory(libs) add_subdirectory(src) -if (MRDOCS_BUILD_HEADERS_ONLY) - return() -else() add_subdirectory(tools) -add_subdirectory(tests) -add_subdirectory(examples) -add_subdirectory(docs) -add_subdirectory(data) -include(utils/cmake/install.cmake) -mrdocs_install() +if (MRDOCS_BUILD_TESTS) + add_subdirectory(tests) +endif() +if (MRDOCS_BUILD_EXAMPLES) + add_subdirectory(examples) +endif() +if (MRDOCS_BUILD_DOCS) + add_subdirectory(docs) +endif() +if (MRDOCS_INSTALL) + add_subdirectory(data) + mrdocs_install() endif() diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index c6078185a5e..0d57d4ad67d 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -9,10 +9,6 @@ # # This directory only installs assets, so it does nothing unless installing. -if (MRDOCS_BUILD_HEADERS_ONLY OR NOT MRDOCS_INSTALL) - return() -endif() - # Install the shared assets this directory owns into the FHS share/ location. # (The LLVM-derived assets, bundled libc++ headers and the clang resource dir, # are installed by src/, next to the dependency that provides them.) @@ -25,7 +21,7 @@ foreach (mrdocs_dir addons) DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/mrdocs FILES_MATCHING PATTERN "*") endforeach () -foreach (data_dir cmake gdb) +foreach (data_dir gdb) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${data_dir} DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/mrdocs FILES_MATCHING PATTERN "*") diff --git a/data/README.md b/data/README.md index f8ce0ad4768..5c2c9d9109b 100644 --- a/data/README.md +++ b/data/README.md @@ -8,6 +8,5 @@ mirrored into the `share/` subtree; this README is not installed. ## Contents - `mrdocs/` — runtime assets installed with MrDocs: `addons/` (Handlebars templates and generator helpers) and `headers/` (bundled libc stubs). -- `cmake/` — `MrDocs.cmake`, the consumer helper for downstream CMake projects. - `gdb/` — GDB pretty-printers for MrDocs types. - `lldb/` — LLDB data formatters for MrDocs types. diff --git a/data/cmake/MrDocs.cmake b/data/cmake/MrDocs.cmake deleted file mode 100644 index fe7a9dda0b8..00000000000 --- a/data/cmake/MrDocs.cmake +++ /dev/null @@ -1,172 +0,0 @@ -# -# Licensed under the Apache License v2.0 with LLVM Exceptions. -# See https://llvm.org/LICENSE.txt for license information. -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -# -# Copyright (c) 2023 Klemens Morgenstern (klemens.morgenstern@gmx.net) -# Copyright (c) 2023 Alan de Freitas (alandefreitas@gmail.com) -# -# Official repository: https://github.com/cppalliance/mrdocs -# - -#[=======================================================================[.rst: -MrDocs ------ - -This module provides a function to generate documentation from C++ source -files with mrdocs. - -It identifies the necessary MrDocs configuration options for the current -project and generates a documentation target. - -See the MrDocs usage documentation for complete instructions. - -#]=======================================================================] - -function(add_mrdocs MRDOCS_TARGET_NAME) - #------------------------------------------------- - # Parse arguments - #------------------------------------------------- - set(options_prefix MRDOCS_TARGET) - set(options EXCLUDE_FROM_ALL) - set(oneValueArgs CONFIG OUTPUT ADDONS) - set(multiValueArgs COMMENT) - cmake_parse_arguments(${options_prefix} "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - set(MRDOCS_TARGET_SOURCES ${MRDOCS_TARGET_UNPARSED_ARGUMENTS}) - - #------------------------------------------------- - # Executable - #------------------------------------------------- - if (NOT DEFINED MRDOCS_EXECUTABLE AND TARGET mrdocs) - set(MRDOCS_EXECUTABLE $) - set(MRDOCS_EXECUTABLE_DEPENDENCY mrdocs) - endif() - if (NOT DEFINED MRDOCS_EXECUTABLE) - find_program(MRDOCS_EXECUTABLE mrdocs) - if (NOT MRDOCS_EXECUTABLE) - message(FATAL_ERROR "MrDocs build script requires mrdocs to be installed") - endif() - endif() - - #------------------------------------------------- - # CMake compile commands - #------------------------------------------------- - if (NOT DEFINED MRDOCS_COMPILE_COMMANDS) - if (NOT CMAKE_EXPORT_COMPILE_COMMANDS) - message(FATAL_ERROR "MrDocs requires either CMAKE_EXPORT_COMPILE_COMMANDS=ON or MRDOCS_COMPILE_COMMANDS to be set") - endif() - set(MRDOCS_COMPILE_COMMANDS ${CMAKE_BINARY_DIR}/compile_commands.json) - endif() - set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES}) - - #------------------------------------------------- - # EXCLUDE_FROM_ALL - #------------------------------------------------- - if (MRDOCS_TARGET_EXCLUDE_FROM_ALL) - set(MRDOCS_TARGET_ALL_STR "") - else() - set(MRDOCS_TARGET_ALL_STR "ALL") - endif() - - #------------------------------------------------- - # Configuration - #------------------------------------------------- - if (NOT MRDOCS_TARGET_CONFIG) - set(MRDOCS_TARGET_CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/mrdocs.yml) - if (NOT EXISTS ${MRDOCS_TARGET_CONFIG}) - foreach (dir doc docs antora) - if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${dir}/mrdocs.yml) - set(MRDOCS_TARGET_CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/${dir}/mrdocs.yml) - break() - endif() - endforeach() - endif() - if (NOT EXISTS ${MRDOCS_TARGET_CONFIG}) - foreach (dir ${CMAKE_CURRENT_SOURCE_DIR}) - if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${dir}/mrdocs.yml) - set(MRDOCS_TARGET_CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/${dir}/mrdocs.yml) - break() - endif() - endforeach() - endif() - if (NOT EXISTS ${MRDOCS_TARGET_CONFIG}) - message(FATAL_ERROR "MrDocs: CONFIG option not set and no mrdocs.yml found in ${CMAKE_CURRENT_SOURCE_DIR}") - endif() - endif() - get_filename_component(MRDOCS_TARGET_CONFIG ${MRDOCS_TARGET_CONFIG} ABSOLUTE) - - #------------------------------------------------- - # Format - #------------------------------------------------- - if (MRDOCS_TARGET_ADDONS) - if (NOT EXISTS ${MRDOCS_TARGET_ADDONS}) - message(FATAL_ERROR "MrDocs: ADDONS directory ${MRDOCS_TARGET_ADDONS} does not exist") - endif() - endif() - - if (NOT MRDOCS_TARGET_ADDONS) - get_filename_component(MRDOCS_EXECUTABLE_DIR ${MRDOCS_EXECUTABLE} DIRECTORY) - set(${PROJECT_NAME}_DIR ${CMAKE_CURRENT_SOURCE_DIR}) - set(DEFAULT_ADDONS_PATHS - # Project has its own addons - ${${PROJECT_NAME}_DIR}/share/mrdocs/addons - ${${PROJECT_NAME}_DIR}/docs/mrdocs/addons - ${${PROJECT_NAME}_DIR}/docs/addons - ${${PROJECT_NAME}_DIR}/antora/addons - # Relative to mrdocs executable - ${MRDOCS_EXECUTABLE_DIR}/../share/mrdocs/addons # FHS - ${MRDOCS_EXECUTABLE_DIR}/share/mrdocs/addons # FHS with no `bin` - ${MRDOCS_EXECUTABLE_DIR}/../addons # Non-FHS - ${MRDOCS_EXECUTABLE_DIR}/addons # Non-FHS with no `bin` - ) - foreach (dir ${DEFAULT_ADDONS_PATHS}) - message(STATUS "MrDocs: Looking for addons in ${dir}") - if (EXISTS ${dir}) - set(MRDOCS_TARGET_ADDONS ${dir}) - break() - endif() - endforeach() - endif() - - if (NOT MRDOCS_TARGET_ADDONS) - message(FATAL_ERROR "MrDocs: ADDONS directory not set and no addons found in ${CMAKE_CURRENT_SOURCE_DIR} or relative to ${MRDOCS_EXECUTABLE}") - endif() - - #------------------------------------------------- - # Comment - #------------------------------------------------- - if (NOT MRDOCS_TARGET_COMMENT) - set(MRDOCS_TARGET_COMMENT "Generate ${CMAKE_PROJECT_NAME} documentation with MrDocs") - endif() - - #------------------------------------------------- - # Output - #------------------------------------------------- - if (NOT MRDOCS_TARGET_OUTPUT) - set(MRDOCS_TARGET_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/mrdocs) - endif() - - - #------------------------------------------------- - # Custom target - #------------------------------------------------- - message(STATUS "MrDocs: Generating documentation for ${CMAKE_PROJECT_NAME} in ${MRDOCS_TARGET_OUTPUT}") - set(MRDOCS_CMD_LINE_OPTIONS --config=${MRDOCS_TARGET_CONFIG} ${MRDOCS_COMPILE_COMMANDS} - --addons=${MRDOCS_TARGET_ADDONS} --output=${MRDOCS_TARGET_OUTPUT}) - string(REPLACE ";" " " MRDOCS_WS_CMD_LINE_OPTIONS "${MRDOCS_CMD_LINE_OPTIONS}") - message(STATUS "mrdocs ${MRDOCS_WS_CMD_LINE_OPTIONS}") - add_custom_target( - ${MRDOCS_TARGET_NAME} - ${MRDOCS_TARGET_ALL_STR} - COMMAND ${CMAKE_COMMAND} -E make_directory ${MRDOCS_TARGET_OUTPUT} - COMMAND ${CMAKE_COMMAND} -E echo "mrdocs ${MRDOCS_WS_CMD_LINE_OPTIONS}" - COMMAND ${MRDOCS_EXECUTABLE} ${MRDOCS_CMD_LINE_OPTIONS} - DEPENDS ${MRDOCS_TARGET_SOURCES} ${MRDOCS_TARGET_CONFIG} ${MRDOCS_COMPILE_COMMANDS} ${MRDOCS_TARGET_ADDONS} - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMENT ${MRDOCS_TARGET_COMMENT} - USES_TERMINAL - ) - if (MRDOCS_EXECUTABLE_DEPENDENCY) - add_dependencies(${MRDOCS_TARGET_NAME} ${MRDOCS_EXECUTABLE_DEPENDENCY}) - endif() -endfunction() \ No newline at end of file diff --git a/data/mrdocs/headers/libc-stubs/stddef.h b/data/mrdocs/headers/libc-stubs/stddef.h index 0927c7ba2d2..8706c67c4cb 100644 --- a/data/mrdocs/headers/libc-stubs/stddef.h +++ b/data/mrdocs/headers/libc-stubs/stddef.h @@ -89,6 +89,14 @@ typedef unsigned int wint_t; using errno_t = int; // max_align_t +// Guarded with clang's own macro so this stub coexists with clang's +// __stddef_max_align_t.h (from the resource directory): whichever is included +// first defines the type and sets the guard, and the other then skips it. +// Without the guard, both define an anonymous-struct max_align_t and clang +// rejects the second as a typedef redefinition with a different type. +#if !defined(__CLANG_MAX_ALIGN_T_DEFINED) && !defined(_GCC_MAX_ALIGN_T) +#define __CLANG_MAX_ALIGN_T_DEFINED +#define _GCC_MAX_ALIGN_T #if defined(_MSC_VER) typedef double max_align_t; #elif defined(__APPLE__) @@ -105,6 +113,7 @@ typedef struct { __attribute__((__aligned__(__alignof__(long double)))); } max_align_t; #endif +#endif // max_align_t guard // offsetof_t #define offsetof(t, d) __builtin_offsetof(t, d) diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index 12ef41e8792..298e0968017 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -11,30 +11,30 @@ # The documentation (Antora) build. This is a regular build: when # MRDOCS_BUILD_DOCS is on we add custom targets that run mrdocs over its own # headers (the reference) and Antora over the site. The separate "compile every -# public header" build is the MrDocs build, MRDOCS_BUILD_HEADERS_ONLY, and lives in -# src/ (it compiles a different target and links nothing). - -if (MRDOCS_BUILD_HEADERS_ONLY OR NOT MRDOCS_BUILD_DOCS) - return() -endif() +# public header" build is the MrDocs build (MRDOCS_MRDOCS_BUILD), and lives in +# docs/mrdocs-build.cmake (it compiles a different target and links nothing). # ---- Reference (mrdocs run over its own headers) ---- if (MRDOCS_GENERATE_REFERENCE) - include(${PROJECT_SOURCE_DIR}/data/cmake/MrDocs.cmake) set(MRDOCS_REFERENCE_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/reference) file(GLOB_RECURSE REFERENCE_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/include/*.hpp ${PROJECT_SOURCE_DIR}/include/*.inc) set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES}) - add_mrdocs(generate_reference - CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/mrdocs.yml + # Run the freshly built mrdocs over its own public headers. In-tree we point + # it at the source-tree addons and the bundled libc++ / libc-stub headers + # (LIBCXX_DIR comes from src/), so extraction does not depend on the host + # toolchain. The config's compilation database drives which sources are read; + # REFERENCE_SOURCES is a DEPENDS list so the target reruns when they change. + add_custom_target(generate_reference ALL + COMMAND ${CMAKE_COMMAND} -E make_directory ${MRDOCS_REFERENCE_OUTPUT_DIR} + COMMAND $ + --config=${CMAKE_CURRENT_SOURCE_DIR}/mrdocs.yml + --output=${MRDOCS_REFERENCE_OUTPUT_DIR} + ${MRDOCS_BUILTIN_DIR_ARGS} + DEPENDS ${REFERENCE_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/mrdocs.yml COMMENT "Generate MrDocs reference" - OUTPUT ${MRDOCS_REFERENCE_OUTPUT_DIR} - # This in-tree target runs before install, so no build-tree share/ - # is staged; point add_mrdocs at the addons in the source tree - # (they moved to data/mrdocs/addons in the layout refactor) instead - # of letting the helper fall through to its "ADDONS not set" error. - ADDONS ${PROJECT_SOURCE_DIR}/data/mrdocs/addons - ${REFERENCE_SOURCES}) + USES_TERMINAL) + add_dependencies(generate_reference mrdocs) if (MRDOCS_GENERATE_ANTORA_REFERENCE) set(MRDOCS_ANTORA_REFERENCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/modules/ROOT/pages/reference) add_custom_target(generate_antora_reference diff --git a/docs/modules/ROOT/attachments/schemas/config/mrdocs.schema.json b/docs/modules/ROOT/attachments/schemas/config/mrdocs.schema.json index beeb44ba482..c8cbbb9b18b 100644 --- a/docs/modules/ROOT/attachments/schemas/config/mrdocs.schema.json +++ b/docs/modules/ROOT/attachments/schemas/config/mrdocs.schema.json @@ -63,6 +63,12 @@ "title": "Base URL for links to source code", "type": "string" }, + "clang-resource-dir": { + "default": "/share/mrdocs/headers/clang", + "description": "Directory holding Clang's builtin headers (`stddef.h`, `stdarg.h`, and the other compiler intrinsics) in its `include/` subdirectory. MrDocs bundles the resource directory that matches its embedded Clang and passes it as `-resource-dir`, so parsing does not depend on a Clang installation on the host. Leave empty to let Clang locate its own resource directory.", + "title": "Clang resource directory", + "type": "string" + }, "cmake": { "default": "", "description": "When the compilation-database option is a CMakeLists.txt file, these arguments are passed to the cmake command to generate the compilation_database.json.", diff --git a/docs/modules/ROOT/pages/contribute/codebase-tour.adoc b/docs/modules/ROOT/pages/contribute/codebase-tour.adoc index 45f0d340a06..b3e57fb1e41 100644 --- a/docs/modules/ROOT/pages/contribute/codebase-tour.adoc +++ b/docs/modules/ROOT/pages/contribute/codebase-tour.adoc @@ -56,7 +56,6 @@ This directory contains shared resources for the documentation generators and ut Its subdirectories are installed in the `share` directory of the installation. * `data/`—Shared resources for the documentation generators -* `data/cmake/`—CMake modules to generate the documentation * `data/gdb/`—GDB pretty printers * `data/mrdocs/`—Shared resources for the documentation generators diff --git a/docs/modules/ROOT/pages/extensions/as-library.adoc b/docs/modules/ROOT/pages/extensions/as-library.adoc index 06d1e8c56af..4039762c3d6 100644 --- a/docs/modules/ROOT/pages/extensions/as-library.adoc +++ b/docs/modules/ROOT/pages/extensions/as-library.adoc @@ -6,14 +6,12 @@ A {cpp} program can also link the cpp:mrdocs[mrdocs-core] library, drive corpus == Build integration -cpp:mrdocs[mrdocs-core] is exported through a CMake package config: +cpp:mrdocs[mrdocs-core] is exported through a CMake package config. The `breaking-changes` example below links it exactly this way: +.`examples/library/breaking-changes/CMakeLists.txt` [source,cmake] ---- -find_package(mrdocs REQUIRED CONFIG) -add_executable(my_tool main.cpp) -target_link_libraries(my_tool PRIVATE mrdocs::mrdocs-core) -target_compile_features(my_tool PRIVATE cxx_std_23) +include::example$examples/library/breaking-changes/CMakeLists.txt[tags=package;target] ---- == Building a corpus diff --git a/docs/mrdocs-build.cmake b/docs/mrdocs-build.cmake new file mode 100644 index 00000000000..3ffb5776803 --- /dev/null +++ b/docs/mrdocs-build.cmake @@ -0,0 +1,79 @@ +# +# Licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +# +# Official repository: https://github.com/cppalliance/mrdocs +# + +# The MrDocs build (MRDOCS_MRDOCS_BUILD): compile a single translation unit that +# includes every public header, producing a compile_commands.json that covers the +# whole API so mrdocs can document itself. The root includes this file and returns +# right after, so none of the real-library CMake runs and nothing is linked. This +# is NOT the Antora docs build (MRDOCS_BUILD_DOCS). + +# ---- Generated headers ---- +# The public headers reference generated ones (e.g. Config.hpp includes +# ), so produce them now, at configure time, before the +# glob below picks them up. utils/codegen is not on the path in this build. +find_program(PYTHON_EXECUTABLE python3 python) +if (NOT PYTHON_EXECUTABLE) + message(FATAL_ERROR "Python is needed to configure mrdocs") +endif() +find_package(Git QUIET) +execute_process( + COMMAND ${PYTHON_EXECUTABLE} ${PROJECT_SOURCE_DIR}/utils/codegen/generate-version-header.py + ${PROJECT_SOURCE_DIR}/include/mrdocs/Version.hpp.in + ${PROJECT_BINARY_DIR}/include/mrdocs/Version.hpp + --version ${PROJECT_VERSION} + --name ${PROJECT_NAME} + --description "${PROJECT_DESCRIPTION}" + --source-dir ${PROJECT_SOURCE_DIR} + --git "${GIT_EXECUTABLE}" + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + COMMAND_ERROR_IS_FATAL ANY) +execute_process( + COMMAND ${PYTHON_EXECUTABLE} ${PROJECT_SOURCE_DIR}/utils/codegen/generate-config-info.py + ${PROJECT_SOURCE_DIR}/src/mrdocs/ConfigOptions.json ${PROJECT_BINARY_DIR} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + COMMAND_ERROR_IS_FATAL ANY) +execute_process( + COMMAND ${PYTHON_EXECUTABLE} ${PROJECT_SOURCE_DIR}/utils/codegen/generate-yaml-schema.py + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + COMMAND_ERROR_IS_FATAL ANY) + +# ---- mrdocs-reference ---- +# One TU including every public header: mrdocs's own (source + generated) plus the +# dom and handlebars libraries it re-exports. +file(GLOB_RECURSE MRDOCS_REFERENCE_HEADERS + "${PROJECT_SOURCE_DIR}/include/*.hpp" + "${PROJECT_BINARY_DIR}/include/*.hpp" + "${PROJECT_SOURCE_DIR}/libs/dom/include/*.hpp" + "${PROJECT_SOURCE_DIR}/libs/handlebars/include/*.hpp") + +set(MRDOCS_REFERENCE_CPP "${PROJECT_BINARY_DIR}/mrdocs-reference.cpp") +file(WRITE "${MRDOCS_REFERENCE_CPP}" "// This file is generated automatically by CMake\n\n") +foreach (header IN LISTS MRDOCS_REFERENCE_HEADERS) + file(TO_CMAKE_PATH "${header}" header) + file(APPEND "${MRDOCS_REFERENCE_CPP}" "#include \"${header}\"\n") +endforeach () + +add_library(mrdocs-reference STATIC "${MRDOCS_REFERENCE_CPP}") +target_compile_features(mrdocs-reference PRIVATE cxx_std_23) +target_include_directories(mrdocs-reference PRIVATE + "${PROJECT_SOURCE_DIR}/include" + "${PROJECT_BINARY_DIR}/include" + "${PROJECT_SOURCE_DIR}/libs/polyfill/include" + "${PROJECT_SOURCE_DIR}/libs/dom/include" + "${PROJECT_SOURCE_DIR}/libs/handlebars/include") +target_compile_definitions(mrdocs-reference PRIVATE MRDOCS_STATIC_LINK) +# This target only needs the headers to compile, not to be warning-clean, so +# silence all warnings. The /Zc flags stay: they are needed to compile, not to +# quiet warnings (Platform.hpp gates on __cplusplus, Describe.hpp uses __VA_OPT__). +if (MSVC) + target_compile_options(mrdocs-reference PRIVATE /Zc:__cplusplus /Zc:preprocessor /W0) +else () + target_compile_options(mrdocs-reference PRIVATE -w) +endif () diff --git a/docs/mrdocs.yml b/docs/mrdocs.yml index 9e2eaaecca0..2f82dca5dcc 100644 --- a/docs/mrdocs.yml +++ b/docs/mrdocs.yml @@ -12,6 +12,7 @@ input: - ../include - ../libs/dom/include - ../libs/handlebars/include + - ../libs/polyfill/include file-patterns: - '*.hpp' include-symbols: @@ -32,5 +33,5 @@ implementation-defined: - '**mrdocs_*_descriptor_fn' multipage: true generator: adoc -cmake: '-D MRDOCS_BUILD_HEADERS_ONLY=ON' +cmake: '-D MRDOCS_MRDOCS_BUILD=ON' diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index bf06b62648e..94c3dc716a0 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -8,8 +8,17 @@ # Official repository: https://github.com/cppalliance/mrdocs # -if (MRDOCS_BUILD_HEADERS_ONLY OR NOT MRDOCS_BUILD_EXAMPLES) - return() +# Examples are ordinary consumer code compiled under the project's /W4 /WX. Some +# include mrdocs's public headers, whose tag_invoke overloads (the ADL tag is an +# unreferenced parameter, C4100) and generated schema (C4245) raise the same /W4 +# diagnostics the libraries do; the rest hit the usual conversion/shadowing +# noise. Suppress that set here at the examples' directory scope, the same way +# libs/ does for its own code, so it stays out of the root list and off +# mrdocs-core's exported interface. LLVM header noise is not needed: examples do +# not compile LLVM headers. +if (MSVC) + add_compile_options(/wd4100 /wd4127 /wd4244 /wd4245 /wd4267 + /wd4456 /wd4457 /wd4458 /wd4459 /wd4701 /wd4702 /wd4703) endif() # Each example subdirectory owns the tests that exercise it. diff --git a/examples/configuration/CMakeLists.txt b/examples/configuration/CMakeLists.txt index b170c83fc49..315e2238d7c 100644 --- a/examples/configuration/CMakeLists.txt +++ b/examples/configuration/CMakeLists.txt @@ -25,8 +25,8 @@ foreach (example IN LISTS _config_examples) add_test( NAME mrdocs-configuration-${_name} COMMAND bash run.sh - --addons=${CMAKE_SOURCE_DIR}/data/mrdocs/addons --output=${CMAKE_CURRENT_BINARY_DIR}/${_name}/reference-output + ${MRDOCS_BUILTIN_DIR_ARGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${example} ) set_property(TEST mrdocs-configuration-${_name} PROPERTY diff --git a/examples/dependencies/CMakeLists.txt b/examples/dependencies/CMakeLists.txt index dbc4154040b..a477b582e75 100644 --- a/examples/dependencies/CMakeLists.txt +++ b/examples/dependencies/CMakeLists.txt @@ -14,8 +14,8 @@ foreach (example IN ITEMS find shim-files shim-snippets accept-missing) add_test( NAME mrdocs-dependencies-${example} COMMAND mrdocs docs/mrdocs.yml - --addons=${CMAKE_SOURCE_DIR}/data/mrdocs/addons --output ${CMAKE_CURRENT_BINARY_DIR}/${example}/reference-output + ${MRDOCS_BUILTIN_DIR_ARGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${example} ) endforeach () diff --git a/examples/generators/CMakeLists.txt b/examples/generators/CMakeLists.txt index 2efa882dfcf..c0c428798e7 100644 --- a/examples/generators/CMakeLists.txt +++ b/examples/generators/CMakeLists.txt @@ -22,9 +22,7 @@ foreach (data_driven IN ITEMS md tex jsonl) "${CMAKE_CURRENT_SOURCE_DIR}/data-driven/${data_driven}/simple.cpp" "--config=${CMAKE_CURRENT_SOURCE_DIR}/data-driven/${data_driven}/mrdocs.yml" "--output=${CMAKE_CURRENT_BINARY_DIR}/data-driven/${data_driven}" - "--addons=${CMAKE_SOURCE_DIR}/data/mrdocs/addons" - "--stdlib-includes=${LIBCXX_DIR}" - "--libc-includes=${CMAKE_SOURCE_DIR}/data/mrdocs/headers/libc-stubs" + ${MRDOCS_BUILTIN_DIR_ARGS} --log-level=warn ) endforeach () @@ -34,8 +32,8 @@ foreach (script_driven IN ITEMS search-index json) add_test( NAME mrdocs-generator-script-driven-${script_driven} COMMAND bash run.sh - --addons=${CMAKE_SOURCE_DIR}/data/mrdocs/addons --output=${CMAKE_CURRENT_BINARY_DIR}/script-driven/${script_driven}/reference-output + ${MRDOCS_BUILTIN_DIR_ARGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/script-driven/${script_driven} ) set_property(TEST mrdocs-generator-script-driven-${script_driven} PROPERTY diff --git a/examples/getting-started/CMakeLists.txt b/examples/getting-started/CMakeLists.txt index 8559204dc11..53cffd7ae6f 100644 --- a/examples/getting-started/CMakeLists.txt +++ b/examples/getting-started/CMakeLists.txt @@ -15,8 +15,8 @@ foreach (example IN ITEMS compilation-database cmake cmake-header-only scanned) add_test( NAME mrdocs-getting-started-${example} COMMAND bash run.sh - --addons=${CMAKE_SOURCE_DIR}/data/mrdocs/addons --output=${CMAKE_CURRENT_BINARY_DIR}/${example}/reference-output + ${MRDOCS_BUILTIN_DIR_ARGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${example} ) set_property(TEST mrdocs-getting-started-${example} PROPERTY diff --git a/examples/library/breaking-changes/CMakeLists.txt b/examples/library/breaking-changes/CMakeLists.txt index d68fe988a4a..ddc347c8cf6 100644 --- a/examples/library/breaking-changes/CMakeLists.txt +++ b/examples/library/breaking-changes/CMakeLists.txt @@ -1,11 +1,32 @@ +# +# Licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +# +# Official repository: https://github.com/cppalliance/mrdocs +# + +# If not inside the Mr.Docs source tree. +if (NOT TARGET mrdocs::mrdocs-core) + cmake_minimum_required(VERSION 3.20) + project(mrdocs-breaking-changes-example LANGUAGES CXX) + # tag::package[] + find_package(mrdocs REQUIRED CONFIG) + # end::package[] + include(CTest) +endif () + +# tag::target[] add_executable(mrdocs-breaking-changes-example src/main.cpp src/Corpus.cpp src/Diff.cpp src/BreakingChangesGenerator.cpp ) -target_link_libraries(mrdocs-breaking-changes-example PRIVATE mrdocs-core) -target_compile_features(mrdocs-breaking-changes-example PRIVATE cxx_std_23) +target_link_libraries(mrdocs-breaking-changes-example PRIVATE mrdocs::mrdocs-core) +# end::target[] if (MRDOCS_CLANG) target_compile_options(mrdocs-breaking-changes-example PRIVATE -Wno-covered-switch-default) endif() @@ -19,8 +40,7 @@ if (BUILD_TESTING) "-DV2_CONFIG=${CMAKE_CURRENT_SOURCE_DIR}/test/v2/mrdocs.yml" "-DEXPECTED=${CMAKE_CURRENT_SOURCE_DIR}/test/expected.txt" "-DACTUAL=${CMAKE_CURRENT_BINARY_DIR}/actual.txt" - "-DDATA_DIR=${PROJECT_SOURCE_DIR}/data" - "-DSTAGE_ROOT=${CMAKE_CURRENT_BINARY_DIR}/mrdocs-root" + "-DBUILTIN_ARGS=${MRDOCS_BUILTIN_DIR_ARGS}" -P "${CMAKE_CURRENT_SOURCE_DIR}/test/run.cmake" ) endif() diff --git a/examples/library/breaking-changes/src/Corpus.cpp b/examples/library/breaking-changes/src/Corpus.cpp index 8a89da46b10..6fc09403cad 100644 --- a/examples/library/breaking-changes/src/Corpus.cpp +++ b/examples/library/breaking-changes/src/Corpus.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace mrdocs::example { @@ -17,13 +18,19 @@ namespace mrdocs::example { Expected loadCorpusFromConfig( std::string const& configPath, - ReferenceDirectories const& dirs) + ReferenceDirectories const& dirs, + char const** argv) { Config config; ReferenceDirectories localDirs = dirs; localDirs.cwd = std::string(files::getParentDir(configPath)); - MRDOCS_TRY(Config::load_file(config, configPath)); - MRDOCS_TRY(config.normalize(localDirs)); + // Command-line overrides (if any) are applied on top of the config file and + // before normalization. Running the example against an installed MrDocs + // needs none; the in-tree test forwards the built-in directory flags here. + MRDOCS_TRY(Config::load_file(config, configPath, localDirs, argv)); + // load_file applies the config's log level; this example only wants the + // breaking-change report, so keep extraction diagnostics quiet. + report::setMinimumLevel(report::Level::error); return Corpus::build(config); } // end::load-corpus[] diff --git a/examples/library/breaking-changes/src/Corpus.hpp b/examples/library/breaking-changes/src/Corpus.hpp index 835734b0da7..97999489872 100644 --- a/examples/library/breaking-changes/src/Corpus.hpp +++ b/examples/library/breaking-changes/src/Corpus.hpp @@ -26,7 +26,8 @@ namespace mrdocs::example { Expected loadCorpusFromConfig( std::string const& configPath, - ReferenceDirectories const& dirs); + ReferenceDirectories const& dirs, + char const** argv = nullptr); } // namespace mrdocs::example diff --git a/examples/library/breaking-changes/src/main.cpp b/examples/library/breaking-changes/src/main.cpp index 657da443425..d9778d80f69 100644 --- a/examples/library/breaking-changes/src/main.cpp +++ b/examples/library/breaking-changes/src/main.cpp @@ -47,18 +47,20 @@ int main(int argc, char const** argv) report::setMinimumLevel(report::Level::error); + // A default-constructed ReferenceDirectories points at MrDocs's installed + // root (linking mrdocs-core bakes it in), so the built-in resources resolve + // with no configuration. Any arguments after the two configs are option + // overrides passed straight through: an installed run supplies none, while + // the in-tree test forwards the built-in directory flags. ReferenceDirectories dirs; - if (char const* root = std::getenv("MRDOCS_ROOT")) - { - dirs.mrdocsRoot = root; - } - auto v1Corpus = example::loadCorpusFromConfig(argv[1], dirs); + char const** overrides = argv + 3; + auto v1Corpus = example::loadCorpusFromConfig(argv[1], dirs, overrides); if (!v1Corpus) { std::println(stderr, "v1: {}", v1Corpus.error().reason()); return 1; } - auto v2Corpus = example::loadCorpusFromConfig(argv[2], dirs); + auto v2Corpus = example::loadCorpusFromConfig(argv[2], dirs, overrides); if (!v2Corpus) { std::println(stderr, "v2: {}", v2Corpus.error().reason()); diff --git a/examples/library/breaking-changes/test/run.cmake b/examples/library/breaking-changes/test/run.cmake index 90bff174f4e..ac6629aa3bc 100644 --- a/examples/library/breaking-changes/test/run.cmake +++ b/examples/library/breaking-changes/test/run.cmake @@ -1,21 +1,23 @@ +# +# Licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +# +# Official repository: https://github.com/cppalliance/mrdocs +# + if(NOT TOOL OR NOT V1_CONFIG OR NOT V2_CONFIG OR NOT EXPECTED OR NOT ACTUAL) message(FATAL_ERROR "run.cmake: missing required variable") endif() -# The example resolves shared assets from /share/mrdocs (the -# installed layout). In the source tree those assets live under data/, so -# stage a root whose share/ mirrors data/ and point the tool at it. This lets -# the example keep demonstrating the default install-layout resolution. -if(DATA_DIR AND STAGE_ROOT) - file(REMOVE_RECURSE "${STAGE_ROOT}/share") - file(COPY "${DATA_DIR}/" DESTINATION "${STAGE_ROOT}/share") - set(ENV{MRDOCS_ROOT} "${STAGE_ROOT}") -else() - set(ENV{MRDOCS_ROOT} "${MRDOCS_ROOT}") -endif() - +# An installed consumer resolves MrDocs's resources from its root with no extra +# arguments. Running in-tree there is no /share/mrdocs yet, so forward the +# built-in directory flags (BUILTIN_ARGS) as option overrides -- no copy, no +# machine paths baked into the example binary. execute_process( - COMMAND "${TOOL}" "${V1_CONFIG}" "${V2_CONFIG}" + COMMAND "${TOOL}" "${V1_CONFIG}" "${V2_CONFIG}" ${BUILTIN_ARGS} OUTPUT_FILE "${ACTUAL}" RESULT_VARIABLE _rc ) diff --git a/include/mrdocs/Config/ReferenceDirectories.hpp b/include/mrdocs/Config/ReferenceDirectories.hpp index 2f117cf6afa..f11bfda331f 100644 --- a/include/mrdocs/Config/ReferenceDirectories.hpp +++ b/include/mrdocs/Config/ReferenceDirectories.hpp @@ -4,7 +4,7 @@ // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // -// Copyright (c) 2024 Alan de Freitas (alandefreitas@gmail.com) +// Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) // // Official repository: https://github.com/cppalliance/mrdocs // @@ -12,9 +12,9 @@ #ifndef MRDOCS_API_CONFIG_REFERENCEDIRECTORIES_HPP #define MRDOCS_API_CONFIG_REFERENCEDIRECTORIES_HPP +#include #include - namespace mrdocs { /** Reference directories used to resolve paths @@ -30,9 +30,25 @@ struct ReferenceDirectories /** Absolute path to the current working directory. */ std::string cwd; - /** Absolute path to the MrDocs repository root. + /** Absolute path to the MrDocs repository/install root. + + All of MrDocs's built-in directories (addons and the parse-time header + sets) derive from this: `/share/mrdocs/...`. */ std::string mrdocsRoot; + + /** Construct and resolve the reference directories. + + The default root is the compile-time MRDOCS_DEFAULT_ROOT, which + mrdocs-config.cmake injects for a downstream project. It is a default + argument, so it is evaluated in the caller's translation unit -- that is + what lets a linking project's find_package-provided prefix reach here. + The constructor then refines it: MRDOCS_ROOT in the environment wins; + otherwise an empty root falls back to the running executable's location. + Defined in mrdocs-core to keep the platform/LLVM lookups out of this + header. + */ + MRDOCS_DECL explicit ReferenceDirectories(std::string root = MRDOCS_DEFAULT_ROOT); }; } // mrdocs diff --git a/include/mrdocs/Metadata/Specifiers/NoexceptInfo.hpp b/include/mrdocs/Metadata/Specifiers/NoexceptInfo.hpp index 00fad3b63e0..54c8edf7839 100644 --- a/include/mrdocs/Metadata/Specifiers/NoexceptInfo.hpp +++ b/include/mrdocs/Metadata/Specifiers/NoexceptInfo.hpp @@ -68,7 +68,7 @@ toString( inline void tag_invoke( - dom::ValueFromTag tag, + dom::ValueFromTag, dom::Value& v, NoexceptInfo const& info) { diff --git a/include/mrdocs/Platform.hpp b/include/mrdocs/Platform.hpp index d7b8e75ab83..b36e1bab308 100644 --- a/include/mrdocs/Platform.hpp +++ b/include/mrdocs/Platform.hpp @@ -85,6 +85,11 @@ namespace mrdocs { # endif #endif +// Expected root of the MrDocs installation when package is found +#ifndef MRDOCS_DEFAULT_ROOT +#define MRDOCS_DEFAULT_ROOT "" +#endif + } // mrdocs diff --git a/include/mrdocs/Support/Reflection/Describe.hpp b/include/mrdocs/Support/Reflection/Describe.hpp index eb2948ecea4..75edf9ca5dc 100644 --- a/include/mrdocs/Support/Reflection/Describe.hpp +++ b/include/mrdocs/Support/Reflection/Describe.hpp @@ -460,7 +460,7 @@ enumeratorKebabSize() { std::string_view name; for_each(describe_enumerators{}, - [&](auto const& D) { if (D.value == V) { name = D.name; } }); + [&](auto const& D) { if constexpr (std::remove_cvref_t::value == V) { name = D.name; } }); return toKebabCase(name).size(); } @@ -471,7 +471,7 @@ enumeratorKebabArray() std::array()> arr{}; std::string_view name; for_each(describe_enumerators{}, - [&](auto const& D) { if (D.value == V) { name = D.name; } }); + [&](auto const& D) { if constexpr (std::remove_cvref_t::value == V) { name = D.name; } }); std::string const kebab = toKebabCase(name); for (std::size_t i = 0; i < arr.size(); ++i) { arr[i] = kebab[i]; } return arr; diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index 1ac50e6c2af..a4c11f86d19 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -8,12 +8,6 @@ # Official repository: https://github.com/cppalliance/mrdocs # -# The header-only "MrDocs build" links nothing, so these libraries are not -# needed there. -if (MRDOCS_BUILD_HEADERS_ONLY) - return() -endif() - # Self-contained libraries, each with its own include/ + src/ (+ tests/) and its # own CMakeLists. These depend ONLY on the standard library, on the `polyfills` # library (standard-library features not yet available on every supported @@ -38,6 +32,13 @@ elseif (MSVC) # puts the compiler in conformance mode so /W4 diagnostics match those under # which mrdocs-core builds this same code. These match mrdocs-core's options. add_compile_options(/permissive- /EHs /MP) + # /W4 diagnostics these libraries' own std-based code raises. Unlike the LLVM + # header suppressions (which ride on mrdocs-llvm in src/), these are about our + # code, so they belong with the libraries. Kept narrow on purpose: C4100 + # unreferenced parameter, C4244/C4245/C4267 conversions, C4127 constant + # condition, C4456/C4457/C4458/C4459 shadowing, C4701/C4702/C4703 flow. + add_compile_options(/wd4100 /wd4127 /wd4244 /wd4245 /wd4267 + /wd4456 /wd4457 /wd4458 /wd4459 /wd4701 /wd4702 /wd4703) # On develop this code lived in mrdocs-core and inherited these Windows # defines from the global add_definitions(${LLVM_DEFINITIONS}); as decoupled # libraries that never link LLVM, they must set them directly. diff --git a/libs/polyfill/include/mrdocs/polyfill/expected.hpp b/libs/polyfill/include/mrdocs/polyfill/expected.hpp index ee57fb49da3..76115a39a57 100644 --- a/libs/polyfill/include/mrdocs/polyfill/expected.hpp +++ b/libs/polyfill/include/mrdocs/polyfill/expected.hpp @@ -301,6 +301,9 @@ unexpected(E) -> unexpected; this single overload and there is no ADL ambiguity across namespaces. It yields exactly `unexpected`, which `expected` recognizes (a derived type would not be, breaking `expected`). + + @param e The error value to wrap. + @return An `unexpected` holding the decayed error value. */ template constexpr unexpected> diff --git a/libs/polyfill/include/mrdocs/polyfill/type_traits.hpp b/libs/polyfill/include/mrdocs/polyfill/type_traits.hpp index 8d33bbc25c5..cb5672725d0 100644 --- a/libs/polyfill/include/mrdocs/polyfill/type_traits.hpp +++ b/libs/polyfill/include/mrdocs/polyfill/type_traits.hpp @@ -18,12 +18,30 @@ #include #include +/** Standard-library polyfills. + + Stand-ins for standard-library features not yet available on every supported + compiler. Each becomes an alias to the standard version once that feature is + universally available. +*/ namespace mrdocs::polyfill { +// The doc comments are repeated in both branches because a doc comment must +// immediately precede its declaration to attach: clang will not associate one +// across the intervening #ifdef/#else text. #ifdef __cpp_lib_reference_from_temporary -using std::reference_constructs_from_temporary_v; +/** `true` if a reference of type `To` would bind to a temporary materialized + from an expression of type `From`. Mirrors `std::reference_converts_from_temporary_v`. +*/ using std::reference_converts_from_temporary_v; +/** `true` if binding a reference of type `To` to an expression of type `From` + would bind to a temporary. Mirrors `std::reference_constructs_from_temporary_v`. +*/ +using std::reference_constructs_from_temporary_v; #else +/** `true` if a reference of type `To` would bind to a temporary materialized + from an expression of type `From`. Mirrors `std::reference_converts_from_temporary_v`. +*/ template concept reference_converts_from_temporary_v = std::is_reference_v @@ -36,6 +54,9 @@ concept reference_converts_from_temporary_v && std::is_convertible_v&&> && !std::is_convertible_v&>) ); +/** `true` if binding a reference of type `To` to an expression of type `From` + would bind to a temporary. Mirrors `std::reference_constructs_from_temporary_v`. +*/ template concept reference_constructs_from_temporary_v = reference_converts_from_temporary_v; diff --git a/mrdocs-config.cmake.in b/mrdocs-config.cmake.in index 346acb91ba2..3f6396794c6 100644 --- a/mrdocs-config.cmake.in +++ b/mrdocs-config.cmake.in @@ -1,41 +1,56 @@ +# +# Licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +# +# Official repository: https://github.com/cppalliance/mrdocs +# + @PACKAGE_INIT@ -# How mrdocs installation was built -set(MRDOCS_BUILT_SHARED "@BUILD_SHARED_LIBS@") +# How this mrdocs installation was built. +# Inspect these to decide if the toolchain matches the binaries. +set(MRDOCS_BUILT_SHARED "@MRDOCS_BUILD_SHARED@") +set(MRDOCS_BUILT_BUNDLED "@MRDOCS_DO_BUNDLE_DEPENDENCIES@") +set(MRDOCS_BUILT_REEXPORT_DEPENDENCIES "@MRDOCS_DO_REEXPORT_DEPENDENCIES@") set(MRDOCS_BUILT_CXX_COMPILER_ID "@CMAKE_CXX_COMPILER_ID@") set(MRDOCS_BUILT_CXX_COMPILER_VERSION "@CMAKE_CXX_COMPILER_VERSION@") -# Paths -set_and_check(MRDOCS_INSTALL_DIR "@PACKAGE_CMAKE_INSTALL_LIBDIR@") -set_and_check(MRDOCS_INCLUDE_DIR "@PACKAGE_INCLUDE_INSTALL_DIR@") -set_and_check(MRDOCS_LIB_DIR "@PACKAGE_LIB_INSTALL_DIR@") - -# Set module paths -include(CMakeFindDependencyMacro) -list(APPEND CMAKE_MODULE_PATH ${MRDOCS_CONFIG_INSTALL_DIR}) -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}") -list(APPEND CMAKE_MODULE_PATH "${MRDOCS_INCLUDE_DIR}") -list(APPEND CMAKE_MODULE_PATH "${MRDOCS_INSTALL_DIR}") -list(APPEND CMAKE_MODULE_PATH "${CMAKE_INSTALL_DATAROOTDIR}/mrdocs/cmake") - -# Find dependencies -find_dependency(LLVM) -find_dependency(Clang) -find_dependency(@DUKTAPE_PACKAGE_NAME@) -find_dependency(fmt) - -# Create imported targets +# Installation paths, relocated relative to this file by @PACKAGE_INIT@. +set_and_check(MRDOCS_INCLUDE_DIR "@PACKAGE_INCLUDE_INSTALL_DIR@") # e.g. "include" +set_and_check(MRDOCS_LIB_DIR "@PACKAGE_LIB_INSTALL_DIR@") # e.g. "lib" or "lib64" +set_and_check(MRDOCS_DATAROOT_DIR "@PACKAGE_DATAROOT_INSTALL_DIR@") # e.g. "share" +set(MRDOCS_STDLIB_INCLUDE_DIR "@PACKAGE_DATAROOT_INSTALL_DIR@/mrdocs/headers/libcxx") +set(MRDOCS_LIBC_INCLUDE_DIR "@PACKAGE_DATAROOT_INSTALL_DIR@/mrdocs/headers/libc-stubs") + +# Dependencies +if (MRDOCS_BUILT_REEXPORT_DEPENDENCIES) + include(CMakeFindDependencyMacro) + find_dependency(LLVM CONFIG) + find_dependency(Clang CONFIG) + find_dependency(jerryscript CONFIG) + find_dependency(Lua CONFIG) +endif () + +# Imported targets. include("${CMAKE_CURRENT_LIST_DIR}/mrdocs-targets.cmake") -check_required_components(mrdocs) -# Set executable path. Only define it when the install actually captured one: -# a defined-but-empty value would suppress the find_program fallback in -# MrDocs.cmake (which guards on `if(NOT DEFINED MRDOCS_EXECUTABLE)`) and leave -# add_mrdocs() consumers with an empty command. When it is empty, leave the -# variable undefined so that fallback locates the installed mrdocs on the -# consumer's CMAKE_PREFIX_PATH. -set(_mrdocs_executable "@MRDOCS_EXECUTABLE@") -if (_mrdocs_executable) +# Bake the relocated MrDocs root into consumers, so a default-constructed +# ReferenceDirectories in a program that links mrdocs-core finds the bundled +# resources under /share/mrdocs with no runtime configuration. +if (TARGET mrdocs::mrdocs-core) + target_compile_definitions(mrdocs::mrdocs-core INTERFACE + "MRDOCS_DEFAULT_ROOT=\"${PACKAGE_PREFIX_DIR}\"") +endif () + +# Locate the installed mrdocs executable so a consumer can invoke it (e.g. from +# its own add_custom_target that generates documentation). +set(_mrdocs_executable "@PACKAGE_BIN_INSTALL_DIR@/mrdocs@CMAKE_EXECUTABLE_SUFFIX@") +if (EXISTS "${_mrdocs_executable}") set(MRDOCS_EXECUTABLE "${_mrdocs_executable}") endif () unset(_mrdocs_executable) + +check_required_components(mrdocs) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f562e088ded..baa59d5b1e9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,123 +9,21 @@ # #------------------------------------------------- -# MrDocs header-compile build (MRDOCS_BUILD_HEADERS_ONLY) +# Dependencies #------------------------------------------------- -# A shortcut that compiles a single translation unit including every public -# header, producing a compile_commands.json that covers the whole API so -# mrdocs can document itself. It links nothing (no LLVM, no libs/), so it is -# defined here before the dependencies are even found, and we return so none of -# the real build runs. This is NOT the Antora docs build (MRDOCS_BUILD_DOCS). -if (MRDOCS_BUILD_HEADERS_ONLY) - set(SRC_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/include") - file(GLOB_RECURSE SRC_HEADER_FILES - "${SRC_INCLUDE_DIR}/*.hpp" - "${PROJECT_SOURCE_DIR}/libs/dom/include/*.hpp" - "${PROJECT_SOURCE_DIR}/libs/handlebars/include/*.hpp") - set(BIN_INCLUDE_DIR "${PROJECT_BINARY_DIR}/include") - file(GLOB_RECURSE BIN_HEADER_FILES "${BIN_INCLUDE_DIR}/*.hpp") - set(ALL_HEADER_FILES ${SRC_HEADER_FILES} ${BIN_HEADER_FILES}) - - set(TEMP_CPP_FILE "${PROJECT_BINARY_DIR}/all_headers.cpp") - file(WRITE "${TEMP_CPP_FILE}" "// This file is generated automatically by CMake\n\n") - foreach(HEADER_FILE IN LISTS ALL_HEADER_FILES) - file(TO_CMAKE_PATH "${HEADER_FILE}" HEADER_FILE_FWD) - file(APPEND "${TEMP_CPP_FILE}" "#include \"${HEADER_FILE_FWD}\"\n") - endforeach() - - add_library(mrdocs-documentation-build STATIC "${TEMP_CPP_FILE}") - target_compile_features(mrdocs-documentation-build PRIVATE cxx_std_23) - target_include_directories(mrdocs-documentation-build - PRIVATE - "${SRC_INCLUDE_DIR}" - "${BIN_INCLUDE_DIR}" - # Public headers reference the libs/ libraries; libs/ is not added in - # this mode, so point at their include dirs directly. - "${PROJECT_SOURCE_DIR}/libs/polyfill/include" - "${PROJECT_SOURCE_DIR}/libs/dom/include" - "${PROJECT_SOURCE_DIR}/libs/handlebars/include" - ) - target_compile_definitions(mrdocs-documentation-build PRIVATE MRDOCS_STATIC_LINK) - return() -endif() - -#------------------------------------------------- -# Dependencies (only mrdocs-core needs these) -#------------------------------------------------- -# Found here, not in the root, because the library is their only direct -# consumer. They are attached to mrdocs-core PRIVATE (see the target settings -# below): implementation details, not usage requirements, so their include -# dirs and definitions never reach any consumer. The tool and tests reach into -# MrDocs's private headers too, so they link the same mrdocs-llvm interface -# target independently, not through mrdocs-core. set(CMAKE_FOLDER Dependencies) - -# LLVM + Clang. CMP0074 makes find_package search _ROOT env vars, but the -# validation and Clang_ROOT derivation want the CMake variable set explicitly. -if (NOT LLVM_ROOT AND DEFINED ENV{LLVM_ROOT}) - set(LLVM_ROOT "$ENV{LLVM_ROOT}") -endif() -if (LLVM_ROOT) - get_filename_component(LLVM_ROOT "${LLVM_ROOT}" ABSOLUTE) - set(LLVM_ROOT "${LLVM_ROOT}" CACHE PATH "Root of LLVM install." FORCE) - if (NOT EXISTS "${LLVM_ROOT}") - message(FATAL_ERROR "LLVM_ROOT (${LLVM_ROOT}) provided does not exist.") - endif() - if (NOT EXISTS "${LLVM_ROOT}/lib/cmake/llvm") - message(FATAL_ERROR "LLVM_ROOT (${LLVM_ROOT}) is invalid: no /lib/cmake/llvm.") - endif() - message(STATUS "LLVM_ROOT: ${LLVM_ROOT}") -endif() -if (Clang_ROOT) - get_filename_component(Clang_ROOT "${Clang_ROOT}" ABSOLUTE) - set(LLVM_ROOT "${LLVM_ROOT}" CACHE PATH "Root of Clang install." FORCE) -elseif (LLVM_ROOT) - set(Clang_ROOT "${LLVM_ROOT}" CACHE PATH "Root of Clang install." FORCE) -endif() - -# Modern usage: find_package(... CONFIG) exposes imported targets. Clang gives -# per-component targets (clangTooling, clangAST, ...) that already carry the -# LLVM+Clang include dirs transitively. LLVM itself still publishes headers and -# definitions only as variables (LLVM_INCLUDE_DIRS, LLVM_DEFINITIONS): there is -# no single interface target for them in this release, so we bundle everything -# into our own mrdocs-llvm interface target below. -find_package(LLVM REQUIRED CONFIG) -find_package(Clang REQUIRED CONFIG) - -set(LIBCXX_DIR "${LLVM_INCLUDE_DIR}/c++/v1" CACHE PATH "Path to libc++ include directory") -message(STATUS "LIBCXX_DIR: ${LIBCXX_DIR}") -if (NOT EXISTS "${LIBCXX_DIR}") - message(FATAL_ERROR "LIBCXX_DIR (${LIBCXX_DIR}) does not exist. Provide an LLVM with libc++ enabled.") -endif() - -list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}") -include(HandleLLVMOptions) -string(REGEX REPLACE " /W[0-4]" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") -string(REGEX REPLACE " /W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") - +include(setup-llvm.cmake) find_package(jerryscript REQUIRED CONFIG) find_package(Lua CONFIG REQUIRED) - -# LLVM/Clang usage requirements, bundled once as an interface target right -# after the dependency is found. LLVM publishes its headers and definitions -# only as variables (no interface target in this release), so we wrap them -# here. Both mrdocs-core and, independently, the in-tree tool and tests that -# reach into private headers pulling in LLVM link this single target. -set_ternary(MRDOCS_CLANG_LIBS CLANG_SIMPLE_LIBS - "LLVM;clang;clang-cpp" - "clangAST;clangBasic;clangFrontend;clangIndex;clangTooling;clangToolingCore;clangToolingInclusions") -add_library(mrdocs-llvm INTERFACE) -target_include_directories(mrdocs-llvm SYSTEM INTERFACE - "${LLVM_INCLUDE_DIRS}" - "${CLANG_INCLUDE_DIRS}") -target_compile_definitions(mrdocs-llvm INTERFACE ${LLVM_DEFINITIONS}) -target_link_libraries(mrdocs-llvm INTERFACE ${MRDOCS_CLANG_LIBS}) - unset(CMAKE_FOLDER) #------------------------------------------------- -# mrdocs-core +# Glob sources #------------------------------------------------- +set(MRDOCS_GENERATED_HEADERS + ${PROJECT_BINARY_DIR}/include/mrdocs/Version.hpp + ${PROJECT_BINARY_DIR}/include/mrdocs/ConfigSchema.hpp) +set_source_files_properties(${MRDOCS_GENERATED_HEADERS} PROPERTIES GENERATED TRUE) file( GLOB_RECURSE LIB_SOURCES CONFIGURE_DEPENDS # Public @@ -139,20 +37,16 @@ file( ${CMAKE_CURRENT_SOURCE_DIR}/mrdocs/*.natvis # Natvis ${PROJECT_SOURCE_DIR}/include/*.natvis + # Generated + ${MRDOCS_GENERATED_HEADERS} ) -list(APPEND LIB_SOURCES - ${PROJECT_BINARY_DIR}/include/mrdocs/Version.hpp - ${PROJECT_BINARY_DIR}/include/mrdocs/ConfigSchema.hpp -) -# These are produced at build time by the mrdocs-codegen target (utils/codegen). -# The GENERATED property set there is directory-scoped (CMP0118 is OLD at our -# cmake_minimum_required 3.13), so it is invisible here; mark them GENERATED in -# this scope or add_library rejects them as missing sources on a clean configure. -set_source_files_properties( - ${PROJECT_BINARY_DIR}/include/mrdocs/Version.hpp - ${PROJECT_BINARY_DIR}/include/mrdocs/ConfigSchema.hpp - PROPERTIES GENERATED TRUE) + +#------------------------------------------------- +# Create target +#------------------------------------------------- add_library(mrdocs-core ${LIB_SOURCES}) +add_library(mrdocs::mrdocs-core ALIAS mrdocs-core) +add_dependencies(mrdocs-core mrdocs-codegen) target_compile_features(mrdocs-core PUBLIC cxx_std_23) target_include_directories(mrdocs-core PUBLIC @@ -161,118 +55,65 @@ target_include_directories(mrdocs-core "$" PRIVATE "${PROJECT_SOURCE_DIR}/src" - "${PROJECT_BINARY_DIR}/src" ) target_compile_definitions( mrdocs-core PUBLIC - # Only the link mode is part of the public API (it drives MRDOCS_DECL). ${MRDOCS_LINK_MODE_DEFINITION} + $<$:_WIN32_WINNT=0x0601> + $<$:_CRT_SECURE_NO_WARNINGS> + $<$:_SILENCE_CXX20_CISO646_REMOVED_WARNING> PRIVATE -DMRDOCS_TOOL ) +target_compile_options( + mrdocs-core + PUBLIC + $<$:/Zc:__cplusplus> # Platform.hpp + $<$:/Zc:preprocessor> # Describe.hpp uses __VA_OPT__ + $<$:-Wno-unused-private-field> + $<$:-Wno-unused-value> + PRIVATE + $<$:/permissive- /W4 /MP> + $<$,$>:/arch:AMD64> + $<$,$>:/Oy-> # Disable frame pointer omission + $<$:-Wno-covered-switch-default> +) -# Public library dependencies: the DOM, polyfills, and handlebars ARE part of -# MrDocs's public API (its public headers include , -# , etc.), so they propagate to consumers. -target_link_libraries(mrdocs-core PUBLIC mrdocs::polyfill) -target_link_libraries(mrdocs-core PUBLIC mrdocs::dom) -target_link_libraries(mrdocs-core PUBLIC mrdocs::handlebars) - -# LLVM/Clang, JerryScript, and Lua are implementation details, not part of the -# public API, so mrdocs-core depends on them PRIVATE and consumers of the -# public API never see them. LLVM/Clang come in through mrdocs-llvm, wrapped in -# $ so the link is build-tree only: mrdocs-core is an -# installed static library, and a plain PRIVATE link would place mrdocs-llvm in -# its exported link interface ($) and force it into the install -# export. The wrap keeps LLVM out of the installed interface while the wiring -# stays defined once, on mrdocs-llvm. -target_link_libraries(mrdocs-core PRIVATE $) +# Args for in-tree invocations of the mrdocs +set(MRDOCS_BUILTIN_DIR_ARGS + "--addons=${CMAKE_SOURCE_DIR}/data/mrdocs/addons" + "--stdlib-includes=${LIBCXX_DIR}" + "--libc-includes=${CMAKE_SOURCE_DIR}/data/mrdocs/headers/libc-stubs" + "--clang-resource-dir=${CLANG_RESOURCE_DIR}" + CACHE INTERNAL "Built-in directory flags for in-tree mrdocs invocations") -# When mrdocs-core is installed as a static archive (the default), it still -# contains unresolved references to LLVM/Clang. A downstream target linking the -# installed mrdocs::mrdocs-core must therefore have LLVM/Clang on its own link -# line to resolve them, even though they are private (not part of the public -# API). The $ link above is build-tree only, so it does not -# carry into the install export; re-expose the same libraries through the -# INSTALL interface. mrdocs-config.cmake.in runs find_dependency(LLVM) and -# find_dependency(Clang), so these targets are available when a consumer -# imports the package. develop linked `LLVM clang clang-cpp` PUBLIC, which had -# the same effect for installed consumers; this keeps that while still keeping -# LLVM out of the build-tree public interface. A shared mrdocs-core absorbs -# these symbols, so consumers of a shared build do not need them. -if (NOT MRDOCS_BUILD_SHARED) - target_link_libraries(mrdocs-core INTERFACE $) +#------------------------------------------------- +# Dependency linking +#------------------------------------------------- +# Public dependencies: linked by name and propagated to consumers +target_link_libraries(mrdocs-core PUBLIC mrdocs::polyfill mrdocs::dom mrdocs::handlebars) + +# Private dependencies +set(MRDOCS_PRIVATE_DEPENDENCIES mrdocs-llvm jerryscript::jerry-core jerryscript::jerry-port Lua::lua) +if (NOT MRDOCS_DO_REEXPORT_DEPENDENCIES) + # Link at build time only. It’ll be bundled when installed. + target_link_libraries(mrdocs-core PRIVATE $) +else () + # Linked by name and propagated to consumers, who will find_dependency() them + target_link_libraries(mrdocs-core PRIVATE ${MRDOCS_PRIVATE_DEPENDENCIES}) + install(TARGETS mrdocs-llvm EXPORT mrdocs-targets) endif () -target_include_directories(mrdocs-core SYSTEM PRIVATE - "${JERRYSCRIPT_INCLUDE_DIRS}" - "${jerryscript_ROOT}/include" - "/usr/local/include") -target_link_libraries(mrdocs-core PRIVATE jerryscript::jerry-core jerryscript::jerry-port) -target_link_libraries(mrdocs-core PRIVATE Lua::lua) - -# Windows, Win64 -if (WIN32) - target_compile_definitions( - mrdocs-core - PUBLIC - -D_WIN32_WINNT=0x0601 - -D_CRT_SECURE_NO_WARNINGS - -D_SILENCE_CXX20_CISO646_REMOVED_WARNING - ) - if(MSVC) - get_target_property(LLVM_CONFIGURATION_TYPE LLVMCore IMPORTED_CONFIGURATIONS) - if (LLVM_CONFIGURATION_TYPE STREQUAL RELWITHDEBINFO) - # Handle Debug/RelWithDebInfo mismatch between mrdocs and LLVM - target_compile_definitions(mrdocs-core PUBLIC $<$:-D_ITERATOR_DEBUG_LEVEL=0>) - target_compile_options(mrdocs-core PUBLIC $<$:/MD>) - endif() - if("${CMAKE_GENERATOR_PLATFORM}" STREQUAL "Win64") # 64-bit - target_compile_options(mrdocs-core PUBLIC /arch:AMD64) - endif() - target_compile_options( - mrdocs-core - PUBLIC - /permissive- # strict C++ - /W4 # enable all warnings - /MP # multi-processor compilation - /EHs # C++ Exception handling - $<$:/Oy-> # Disable frame pointer omission - ) +# You typically should not be doing it, but if someone attempts to link LLVM in MSVC/Release +# with MrDocs in MSVC/Debug, adjust flags to avoid a mismatch and propagate them to consumers. +if(MSVC) + get_target_property(LLVM_CONFIGURATION_TYPE LLVMCore IMPORTED_CONFIGURATIONS) + if (LLVM_CONFIGURATION_TYPE STREQUAL RELWITHDEBINFO) + # Handle Debug/RelWithDebInfo mismatch between mrdocs and LLVM + target_compile_definitions(mrdocs-core PUBLIC $<$:-D_ITERATOR_DEBUG_LEVEL=0>) + target_compile_options(mrdocs-core PUBLIC $<$:/MD>) endif() -endif () - -if (MRDOCS_CLANG) - target_compile_options( - mrdocs-core - PUBLIC - -Wno-unused-private-field - -Wno-unused-value - PRIVATE - -Wno-covered-switch-default - ) -endif () - -# The generated ConfigSchema/Version headers come from utils/codegen; depend on -# its target so generation is ordered before this library builds. -if (TARGET mrdocs-codegen) - add_dependencies(mrdocs-core mrdocs-codegen) -endif () - -#------------------------------------------------- -# Clang resource directory (build tree) -#------------------------------------------------- -# Replicate the clang resource directory inside our build so libclang finds it -# when running mrdocs directly from build/bin. Needs LLVM_BINARY_DIR, so it -# lives here with the LLVM dependency. -file(MAKE_DIRECTORY "${PROJECT_BINARY_DIR}/lib/clang") -set(RESOURCE_DIR "lib/clang/${Clang_VERSION_MAJOR}") -file(CREATE_LINK "${LLVM_BINARY_DIR}/${RESOURCE_DIR}" "${PROJECT_BINARY_DIR}/${RESOURCE_DIR}" SYMBOLIC) -if (NOT EXISTS "${LLVM_BINARY_DIR}/${RESOURCE_DIR}/include") - message(FATAL_ERROR - "Clang resource headers (${LLVM_BINARY_DIR}/${RESOURCE_DIR}/include) do not exist.\n" - "Please provide an LLVM install that contains the clang resource directory.\n") endif() #------------------------------------------------- @@ -301,7 +142,94 @@ if (MRDOCS_INSTALL) install(DIRECTORY ${LIBCXX_DIR}/ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/mrdocs/headers/libcxx FILES_MATCHING PATTERN "*") - install(DIRECTORY ${LLVM_BINARY_DIR}/${RESOURCE_DIR}/include - DESTINATION ${RESOURCE_DIR} + # Clang's builtin headers, bundled like the libcxx/libc-stubs headers. mrdocs + # points -resource-dir here (the clang-resource-dir option) and clang appends + # /include, so the headers install under headers/clang/include. + install(DIRECTORY ${CLANG_RESOURCE_DIR}/include + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/mrdocs/headers/clang FILES_MATCHING PATTERN "*") endif() + +#------------------------------------------------- +# Bundle private dependencies into mrdocs-core +#------------------------------------------------- +# Bundle mrdocs-core library with the private libraries it links +# into one self-contained fat library, so a consumer project can +# link the static mrdocs-core without the dependencies installed. +# This lets a plugin author link against the exact LLVM mrdocs +# was built with without building mrdocs from source. +if (MRDOCS_DO_BUNDLE_DEPENDENCIES AND MRDOCS_INSTALL) + set(_mrdocs_bundled_lib_filename "${CMAKE_STATIC_LIBRARY_PREFIX}mrdocs-core-bundled${CMAKE_STATIC_LIBRARY_SUFFIX}") + set(_mrdocs_bundled_lib_path "${CMAKE_CURRENT_BINARY_DIR}/${_mrdocs_bundled_lib_filename}") + + # Find all transitive static dependencies of mrdocs-core and the system libraries they use + mrdocs_collect_static_libs( + _mrdocs_bundle_deps # The static libraries mrdocs-core references, recursively + _mrdocs_bundle_system_libs # The system libraries those static libraries reference, recursively + ${MRDOCS_PRIVATE_DEPENDENCIES}) + + if (WIN32) + # Windows: lib.exe only concatenates whole .lib files. + # There is no -r / no member-level pruning on COFF), so instead we only merge + # only the libraries mrdocs actually *references* to make this smaller. + # Only the referenced static libraries, so lib.exe does not embed all of LLVM. + if (CMAKE_AR) + set(_mrdocs_bundled_lib_path_tool "${CMAKE_AR}") + else() + find_program(_mrdocs_bundled_lib_path_tool NAMES lib.exe lib llvm-lib REQUIRED) + endif() + add_custom_command( + OUTPUT ${_mrdocs_bundled_lib_path} + DEPENDS mrdocs-core + COMMAND ${CMAKE_COMMAND} -E rm -f ${_mrdocs_bundled_lib_path} + COMMAND ${_mrdocs_bundled_lib_path_tool} /NOLOGO "/OUT:${_mrdocs_bundled_lib_path}" + $ ${_mrdocs_bundle_deps} + COMMENT "Bundling mrdocs-core with its referenced private toolchain" + VERBATIM COMMAND_EXPAND_LISTS) + else() + # Unix: a relocatable link (ld -r) force-loads mrdocs-core and member-selects + # from the collected dependency archives, so only the members mrdocs actually + # reaches survive. The code it never calls is dropped, then re-archived. This + # is the same list Windows merges whole; ld -r just prunes it to what is used. + set(_mrdocs_bundled_obj_path "${CMAKE_CURRENT_BINARY_DIR}/mrdocs-core-bundled${CMAKE_C_OUTPUT_EXTENSION}") + # mrdocs-core is force-loaded whole (it holds the roots). GNU ld resolves + # archives in a single pass, so wrap the rest in --start-group/--end-group + # to resolve the cross-archive references in Clang's vtables regardless of + # order; Apple ld64 is multi-pass and rejects that flag. + if (APPLE) + set(_bundle_inputs + -Wl,-force_load,$ + ${_mrdocs_bundle_deps}) + else() + set(_bundle_inputs + -Wl,--whole-archive $ -Wl,--no-whole-archive + -Wl,--start-group + ${_mrdocs_bundle_deps} + -Wl,--end-group) + endif() + add_custom_command( + OUTPUT ${_mrdocs_bundled_lib_path} + DEPENDS mrdocs-core + COMMAND ${CMAKE_CXX_COMPILER} -r -nostdlib -o ${_mrdocs_bundled_obj_path} ${_bundle_inputs} + COMMAND ${CMAKE_COMMAND} -E rm -f ${_mrdocs_bundled_lib_path} + COMMAND ${CMAKE_AR} qcs ${_mrdocs_bundled_lib_path} ${_mrdocs_bundled_obj_path} + COMMENT "Bundling mrdocs-core with its pruned private toolchain" + VERBATIM COMMAND_EXPAND_LISTS) + endif() + + add_custom_target(mrdocs-core-bundle ALL DEPENDS ${_mrdocs_bundled_lib_path}) + + # Replace the thin libmrdocs-core installed above with the fat one, same path. + # The mrdocs-targets export keeps naming it. + install(FILES ${_mrdocs_bundled_lib_path} + DESTINATION ${CMAKE_INSTALL_LIBDIR} + RENAME ${CMAKE_STATIC_LIBRARY_PREFIX}mrdocs-core${CMAKE_STATIC_LIBRARY_SUFFIX} + COMPONENT development) + + # The fat archive embeds the LLVM/Clang objects but not the OS libraries they + # reference. These are still linked to the installed target. + if (_mrdocs_bundle_system_libs) + target_link_libraries(mrdocs-core INTERFACE "$") + endif () +endif() + diff --git a/src/mrdocs/AST/ASTVisitor.cpp b/src/mrdocs/AST/ASTVisitor.cpp index c47f94e2d73..3b42726e132 100644 --- a/src/mrdocs/AST/ASTVisitor.cpp +++ b/src/mrdocs/AST/ASTVisitor.cpp @@ -1567,7 +1567,7 @@ populate( for (std::size_t i = 0; i < TPL->size(); ++i) { auto& Param = Result->Params.emplace_back(std::in_place_type); - populate(Param, TPL->getParam(i)); + populate(Param, TPL->getParam(static_cast(i))); } if (TTPD->hasDefaultArgument() && !Result->Default) { @@ -1617,7 +1617,7 @@ populate( std::size_t i = 0; while (explicitIt != ExplicitTemplateParameters.end()) { - clang::NamedDecl const* P = TPL->getParam(i); + clang::NamedDecl const* P = TPL->getParam(static_cast(i)); Polymorphic& Param = i < TI.Params.size() ? TI.Params[i] : @@ -2621,7 +2621,7 @@ extractSFINAEInfo(clang::QualType const T) for (std::size_t I = 0; I < Args.size(); ++I) { if (I < SFINAEControl->ControllingParams.size() - && SFINAEControl->ControllingParams[I]) + && SFINAEControl->ControllingParams[static_cast(I)]) { MRDOCS_SYMBOL_TRACE(Args[I], context_); clang::TemplateArgument ArgsI = Args[I]; @@ -2694,7 +2694,7 @@ getSFINAEControlParams( // Find the index of the parameter that represents the SFINAE result // in the primary template arguments - unsigned ParamIdx = FindParam(ATD->getInjectedTemplateArgs(context_), *resultType); + unsigned ParamIdx = static_cast(FindParam(ATD->getInjectedTemplateArgs(context_), *resultType)); // Return the controlling parameters with values corresponding to // the primary template arguments @@ -2706,28 +2706,28 @@ getSFINAEControlParams( for (std::size_t i = 0; i < sfinaeControl->ControllingParams.size(); ++i) { - if (sfinaeControl->ControllingParams[i]) + if (sfinaeControl->ControllingParams[static_cast(i)]) { // Find the index of the parameter that represents the SFINAE // result in the underlying template arguments - auto resultType = tryGetTemplateArgument( + auto argResultType = tryGetTemplateArgument( sfinaeControl->Parameters, underlyingTemplateInfo->Arguments, i); - MRDOCS_CHECK_OR_CONTINUE(resultType); - MRDOCS_SYMBOL_TRACE(*resultType, context_); + MRDOCS_CHECK_OR_CONTINUE(argResultType); + MRDOCS_SYMBOL_TRACE(*argResultType, context_); // Find the index of the parameter that represents the param // in the primary template arguments - auto ParamIdx = FindParam( + auto argParamIdx = FindParam( ATD->getInjectedTemplateArgs(context_), - *resultType); - if (ParamIdx == static_cast(-1)) + *argResultType); + if (argParamIdx == static_cast(-1)) { continue; } - primaryControllingParams.set(ParamIdx); + primaryControllingParams.set(static_cast(argParamIdx)); } } @@ -2753,7 +2753,7 @@ getSFINAEControlParams( // For instance, in the specialization `std::enable_if::type`, // `type` is `T`, which corresponds to the second template parameter // `T`, so `ParamIdx` is `1` to represent the second parameter. - unsigned ParamIdx = -1; + unsigned ParamIdx = static_cast(-1); // The `IsMismatch` function checks if there's a mismatch between the // clang::CXXRecordDecl of the clang::ClassTemplateDecl and the specified template @@ -2883,7 +2883,7 @@ getSFINAEControlParams( // `type` is `T`, which corresponds to the second template // parameter `T`, so `ParamIdx` is `1` to represent the // second parameter. - ParamIdx = FoundIdx; + ParamIdx = static_cast(FoundIdx); // Get this primary template argument as a template // argument of the current type. clang::TemplateArgument MappedPrimary = PrimaryArgs[FoundIdx]; @@ -2940,7 +2940,7 @@ getSFINAEControlParams( // template parameters that control the SFINAE result. The controlling // parameters are expressions that cannot be converted to // non-type template parameters. - llvm::SmallBitVector ControllingParams(PrimaryArgs.size()); + llvm::SmallBitVector ControllingParams(static_cast(PrimaryArgs.size())); for(auto* CTPSD : PartialSpecs) { MRDOCS_SYMBOL_TRACE(CTPSD, context_); auto PartialArgs = CTPSD->getTemplateArgs().asArray(); @@ -2965,7 +2965,7 @@ getSFINAEControlParams( default: continue; } - ControllingParams.set(i); + ControllingParams.set(static_cast(i)); } } @@ -3031,7 +3031,7 @@ tryGetTemplateArgument( MRDOCS_CHECK_OR(Index < Parameters->size(), std::nullopt); // Attempt to get the default argument of the template parameter - clang::NamedDecl* ND = Parameters->getParam(Index); + clang::NamedDecl* ND = Parameters->getParam(static_cast(Index)); MRDOCS_SYMBOL_TRACE(ND, context_); if(auto* TTPD = dyn_cast(ND); TTPD && TTPD->hasDefaultArgument()) diff --git a/src/mrdocs/AST/MrDocsFileSystem.hpp b/src/mrdocs/AST/MrDocsFileSystem.hpp index c55883876bc..1a7c42bab74 100644 --- a/src/mrdocs/AST/MrDocsFileSystem.hpp +++ b/src/mrdocs/AST/MrDocsFileSystem.hpp @@ -98,7 +98,7 @@ class MrDocsFileSystem : public llvm::vfs::FileSystem { shim_macro += '_'; } else { - shim_macro += std::toupper(c); + shim_macro += static_cast(std::toupper(static_cast(c))); } } std::string result; diff --git a/src/mrdocs/Config/ReferenceDirectories.cpp b/src/mrdocs/Config/ReferenceDirectories.cpp new file mode 100644 index 00000000000..19740260068 --- /dev/null +++ b/src/mrdocs/Config/ReferenceDirectories.cpp @@ -0,0 +1,133 @@ +// +// This is a derivative work. originally part of the LLVM Project. +// Licensed under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +// +// Official repository: https://github.com/cppalliance/mrdocs +// + +#include +#include +#include +#include +#include +#include + +// The library-relative step (below) is only meaningful for a shared mrdocs-core, +// which is installed as its own file next to the executable's directory. For the +// static library there is no such file -- the code is fused into the consuming +// executable -- so it is excluded and only the executable location is used. +#if defined(MRDOCS_SHARED_LINK) +# if defined(_WIN32) +# include +# include +# else +# include +# endif +#endif + +namespace mrdocs { + +namespace { + +// Anchor whose address locates the module that provides mrdocs-core. +void mrdocsExecutableAnchor() {} + +#if defined(MRDOCS_SHARED_LINK) +// Path of the shared mrdocs-core library (the module that contains the anchor), +// or empty if it cannot be determined. +std::string +mrdocsLibraryPath() +{ +# if defined(_WIN32) + HMODULE hmod = nullptr; + if (!::GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS + | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&mrdocsExecutableAnchor), + &hmod)) + { + return {}; + } + std::wstring buf(MAX_PATH, L'\0'); + for (;;) + { + DWORD const n = ::GetModuleFileNameW( + hmod, buf.data(), static_cast(buf.size())); + if (n == 0) + { + return {}; + } + if (n < buf.size()) + { + buf.resize(n); + break; + } + if (buf.size() > 65536) + { + return {}; + } + buf.resize(buf.size() * 2); + } + std::string utf8; + if (llvm::convertWideToUTF8(buf, utf8)) + { + return utf8; + } + return {}; +# else + Dl_info info{}; + if (::dladdr(reinterpret_cast(&mrdocsExecutableAnchor), &info) + && info.dli_fname) + { + return info.dli_fname; + } + return {}; +# endif +} +#endif // MRDOCS_SHARED_LINK + +} // (anon) + +ReferenceDirectories:: +ReferenceDirectories(std::string root) + : mrdocsRoot(std::move(root)) +{ + llvm::SmallVector buf; + if (!llvm::sys::fs::current_path(buf)) + { + cwd.assign(buf.data(), buf.size()); + } + + // Precedence for the MrDocs root: MRDOCS_ROOT in the environment, then the + // compile-time default passed as `root` (empty in MrDocs's own build; the + // installed prefix for a downstream project via find_package), then a + // location two levels below the file that provides mrdocs-core -- the shared + // library when built shared (so a shared consumer resolves MrDocs's own + // install), otherwise the running executable. + if (char const* env = std::getenv("MRDOCS_ROOT"); env && *env) + { + mrdocsRoot = env; + } + else if (mrdocsRoot.empty()) + { + std::string self; +#if defined(MRDOCS_SHARED_LINK) + self = mrdocsLibraryPath(); +#endif + if (self.empty()) + { + self = llvm::sys::fs::getMainExecutable( + nullptr, reinterpret_cast(&mrdocsExecutableAnchor)); + } + if (!self.empty()) + { + mrdocsRoot = std::string(files::getParentDir(self, 2)); + } + } +} + +} // mrdocs diff --git a/src/mrdocs/ConfigOptions.json b/src/mrdocs/ConfigOptions.json index 50b1e88d41e..eb5d5b46394 100644 --- a/src/mrdocs/ConfigOptions.json +++ b/src/mrdocs/ConfigOptions.json @@ -80,18 +80,6 @@ "type": "bool", "default": true }, - { - "name": "stdlib-includes", - "brief": "C++ Standard Library include paths", - "details": "When `use-system-stdlib` is disabled, the C++ standard library headers are available in these paths.", - "type": "list", - "default": [ - "/share/mrdocs/headers/libcxx" - ], - "relative-to": "", - "must-exist": false, - "should-exist": true - }, { "name": "use-system-libc", "brief": "Use the system C standard library", @@ -99,18 +87,6 @@ "type": "bool", "default": true }, - { - "name": "libc-includes", - "brief": "Standard Library include paths", - "details": "When `use-system-libc` is disabled, the C standard library headers are available in these paths.", - "type": "list", - "default": [ - "/share/mrdocs/headers/libc-stubs" - ], - "relative-to": "", - "must-exist": false, - "should-exist": true - }, { "name": "system-includes", "brief": "System include paths", @@ -505,15 +481,6 @@ "type": "string", "default": "" }, - { - "name": "addons", - "brief": "Path to the Addons directory", - "details": "The directory of template files the generators use to build the documentation. Leave it unset to use the defaults that ship at `share/mrdocs/addons` in the MrDocs installation. To customize the output, copy those defaults to your own directory and point this option at it.", - "type": "path", - "default": "/share/mrdocs/addons", - "relative-to": "", - "must-exist": true - }, { "name": "addons-supplemental", "brief": "Additional addons layered on top of the base addons", @@ -735,5 +702,55 @@ "default": false } ] + }, + { + "category": "Built-in directories", + "brief": "Where MrDocs finds its own bundled resources", + "details": "These point at the addons and the parse-time header sets that MrDocs ships with. They default to `/share/mrdocs/...`, derived from the MrDocs root, and are not meant for everyday configuration. They are exposed so tests and unusual installs can redirect MrDocs's built-in directories.", + "options": [ + { + "name": "addons", + "brief": "Path to the Addons directory", + "details": "The directory of template files the generators use to build the documentation. Leave it unset to use the defaults that ship at `share/mrdocs/addons` in the MrDocs installation. To customize the output, copy those defaults to your own directory and point this option at it.", + "type": "path", + "default": "/share/mrdocs/addons", + "relative-to": "", + "must-exist": true + }, + { + "name": "stdlib-includes", + "brief": "C++ Standard Library include paths", + "details": "When `use-system-stdlib` is disabled, the C++ standard library headers are available in these paths.", + "type": "list", + "default": [ + "/share/mrdocs/headers/libcxx" + ], + "relative-to": "", + "must-exist": false, + "should-exist": true + }, + { + "name": "libc-includes", + "brief": "Standard Library include paths", + "details": "When `use-system-libc` is disabled, the C standard library headers are available in these paths.", + "type": "list", + "default": [ + "/share/mrdocs/headers/libc-stubs" + ], + "relative-to": "", + "must-exist": false, + "should-exist": true + }, + { + "name": "clang-resource-dir", + "brief": "Clang resource directory", + "details": "Directory holding Clang's builtin headers (`stddef.h`, `stdarg.h`, and the other compiler intrinsics) in its `include/` subdirectory. MrDocs bundles the resource directory that matches its embedded Clang and passes it as `-resource-dir`, so parsing does not depend on a Clang installation on the host. Leave empty to let Clang locate its own resource directory.", + "type": "path", + "default": "/share/mrdocs/headers/clang", + "relative-to": "", + "must-exist": false, + "should-exist": true + } + ] } ] diff --git a/src/mrdocs/Corpus.cpp b/src/mrdocs/Corpus.cpp index f4d62564a4b..94195a9ceca 100644 --- a/src/mrdocs/Corpus.cpp +++ b/src/mrdocs/Corpus.cpp @@ -74,7 +74,7 @@ findFirstParentInfo( MRDOCS_CHECK_OR(contextUniquePtr, SymbolID::invalid); auto& context = *contextUniquePtr; bool const isParent = visit(context, []( - InfoTy const& I) -> bool + InfoTy const&) -> bool { return SymbolParent; }); diff --git a/src/mrdocs/Engines/Lua.cpp b/src/mrdocs/Engines/Lua.cpp index 5e578cb439e..785de544bc5 100644 --- a/src/mrdocs/Engines/Lua.cpp +++ b/src/mrdocs/Engines/Lua.cpp @@ -272,7 +272,7 @@ static void luaM_report( Error const& err, - source_location loc = + [[maybe_unused]] source_location loc = source_location::current()) { SourceLocation Loc(err.location()); @@ -799,7 +799,7 @@ domValue_push( static char const* Reader( - lua_State *L, + lua_State *, void* data, size_t* size) { diff --git a/src/mrdocs/Engines/Lua/Scope.ipp b/src/mrdocs/Engines/Lua/Scope.ipp index 7113dad42dc..f25e814d118 100644 --- a/src/mrdocs/Engines/Lua/Scope.ipp +++ b/src/mrdocs/Engines/Lua/Scope.ipp @@ -72,7 +72,7 @@ Expected Scope:: getGlobal( std::string_view key, - source_location loc) + [[maybe_unused]] source_location loc) { Access A(*this); lua_pushglobaltable(A); diff --git a/src/mrdocs/Generators/noop/NoopGenerator.cpp b/src/mrdocs/Generators/noop/NoopGenerator.cpp index 2f916a6104b..2d8a19d900e 100644 --- a/src/mrdocs/Generators/noop/NoopGenerator.cpp +++ b/src/mrdocs/Generators/noop/NoopGenerator.cpp @@ -15,7 +15,7 @@ namespace noop { Expected NoopGenerator:: -build(Corpus const&, Config const& config) const +build(Corpus const&, Config const&) const { // Extraction has already happened by the time a generator runs; // the no-op generator deliberately writes nothing. diff --git a/src/mrdocs/MrDocsCompilationDatabase.cpp b/src/mrdocs/MrDocsCompilationDatabase.cpp index 4375c5f16f4..148fdfeb9af 100644 --- a/src/mrdocs/MrDocsCompilationDatabase.cpp +++ b/src/mrdocs/MrDocsCompilationDatabase.cpp @@ -421,6 +421,18 @@ adjustCommandLine( } } + // ------------------------------------------------------ + // Clang resource directory + // ------------------------------------------------------ + // Point clang at the bundled resource directory so its builtin headers + // (stddef.h, stdarg.h, ...) come from the embedded clang rather than the + // host. clang appends `/include` to it. Empty lets clang locate its own. + if (!config.clangResourceDir.empty()) + { + new_cmdline.emplace_back("-resource-dir"); + new_cmdline.emplace_back(config.clangResourceDir); + } + // ------------------------------------------------------ // Add user directories to include search path // ------------------------------------------------------ diff --git a/src/setup-llvm.cmake b/src/setup-llvm.cmake new file mode 100644 index 00000000000..e178bbe419e --- /dev/null +++ b/src/setup-llvm.cmake @@ -0,0 +1,103 @@ +# +# Licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +# +# Official repository: https://github.com/cppalliance/mrdocs +# + +# +# Find LLVM + Clang and create a wrapped mrdocs-llvm interface target. +# +include_guard(GLOBAL) + +#------------------------------------------------- +# Ensure directories +#------------------------------------------------- +# Ensure _ROOT variables and give us a strong fatal error +# with a better explanation if they're missing. MrDocs requires an +# explicit path with our specific build. We never use an LLVM installation +# from the system. +if (NOT LLVM_ROOT AND DEFINED ENV{LLVM_ROOT}) + set(LLVM_ROOT "$ENV{LLVM_ROOT}") +endif() +if (LLVM_ROOT) + get_filename_component(LLVM_ROOT "${LLVM_ROOT}" ABSOLUTE) + set(LLVM_ROOT "${LLVM_ROOT}" CACHE PATH "Root of LLVM install." FORCE) + if (NOT EXISTS "${LLVM_ROOT}") + message(FATAL_ERROR "LLVM_ROOT (${LLVM_ROOT}) provided does not exist.") + endif() + if (NOT EXISTS "${LLVM_ROOT}/lib/cmake/llvm") + message(FATAL_ERROR "LLVM_ROOT (${LLVM_ROOT}) is invalid: no /lib/cmake/llvm.") + endif() + message(STATUS "LLVM_ROOT: ${LLVM_ROOT}") +endif() +if (Clang_ROOT) + get_filename_component(Clang_ROOT "${Clang_ROOT}" ABSOLUTE) + set(LLVM_ROOT "${LLVM_ROOT}" CACHE PATH "Root of Clang install." FORCE) +elseif (LLVM_ROOT) + set(Clang_ROOT "${LLVM_ROOT}" CACHE PATH "Root of Clang install." FORCE) +endif() + +#------------------------------------------------- +# Find packages +#------------------------------------------------- +# LLVM publishes headers and definitions as variables. +# There is no single interface target for them. +find_package(LLVM REQUIRED CONFIG) +# Clang gives per-component targets (clangTooling, clangAST, ...) that +# link LLVM libraries transitively but not the headers or definitions. +find_package(Clang REQUIRED CONFIG) +# Find libc++ headers, which are not part of the LLVM/Clang CMake packages. The +# include dir is always /include/c++/v1. +set(LIBCXX_DIR "${LLVM_INCLUDE_DIR}/c++/v1" CACHE PATH "Path to libc++ include directory") +message(STATUS "LIBCXX_DIR: ${LIBCXX_DIR}") +if (NOT EXISTS "${LIBCXX_DIR}") + message(FATAL_ERROR "LIBCXX_DIR (${LIBCXX_DIR}) does not exist. Provide an LLVM with libc++ enabled.") +endif() +# Clang's resource directory: builtin headers (stddef.h, stdarg.h, ...) LLVM ships +# at /lib/clang/. +set(CLANG_RESOURCE_DIR "${LLVM_BINARY_DIR}/lib/clang/${Clang_VERSION_MAJOR}" + CACHE PATH "Path to clang's resource directory (builtin headers under include/)") +message(STATUS "CLANG_RESOURCE_DIR: ${CLANG_RESOURCE_DIR}") +if (NOT EXISTS "${CLANG_RESOURCE_DIR}/include") + message(FATAL_ERROR "CLANG_RESOURCE_DIR (${CLANG_RESOURCE_DIR}) has no include/ subdirectory. Provide an LLVM install that contains the clang resource directory.") +endif() + +#------------------------------------------------- +# Replay flags +#------------------------------------------------- +# Replay the flags LLVM was built with +# LLVM doesn't expose modular targets +list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}") +include(HandleLLVMOptions) +# HandleLLVMOptions forces a /W level and a set of -wd suppressions into the global +# flags. Strip both: mrdocs-core sets its own /W4, and LLVM's header warnings are +# scoped to LLVM headers via /external:W0 on mrdocs-llvm below -- NOT suppressed +# project-wide, so real warnings in mrdocs's own code are never hidden. +if (MSVC) + string(REGEX REPLACE " /W[0-4]" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + string(REGEX REPLACE " /W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + string(REGEX REPLACE " [-/]wd[0-9]+" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + string(REGEX REPLACE " [-/]wd[0-9]+" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +endif() + +#------------------------------------------------- +# Create interface target +#------------------------------------------------- +# Wrap LLVM/Clang's usage requirements into a modular interface target. +add_library(mrdocs-llvm INTERFACE) +target_include_directories(mrdocs-llvm SYSTEM INTERFACE + "$" + "$") +target_compile_definitions(mrdocs-llvm INTERFACE ${LLVM_DEFINITIONS}) +target_link_libraries(mrdocs-llvm INTERFACE clangAST clangBasic clangFrontend clangIndex clangTooling clangToolingCore clangToolingInclusions) +if (MSVC) + # Mark LLVM warnings as external so they don't leak. + # A few of the warnings couldn't be scoped, so they need an explicit /wd. + # - 4701/4702/4703 are codegen warnings + # - 4244/4245/4267 are narrowing warnings from STL templates that end up in + target_compile_options(mrdocs-llvm INTERFACE /external:W0 /wd4701 /wd4702 /wd4703 /wd4244 /wd4245 /wd4267) +endif() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ae2b57ec8a6..33af7d5dd9b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,10 +8,6 @@ # Official repository: https://github.com/cppalliance/mrdocs # -if (MRDOCS_BUILD_HEADERS_ONLY OR NOT MRDOCS_BUILD_TESTS) - return() -endif() - # Each test suite is a separate executable and owns its own CMakeLists: # unit/ - public-API unit tests (mrdocs-unit-tests) # golden/ - reference-output harness (mrdocs-golden-tests) @@ -27,12 +23,8 @@ add_test(NAME mrdocs-self-doc "${CMAKE_SOURCE_DIR}/CMakeLists.txt" "--config=${CMAKE_SOURCE_DIR}/docs/mrdocs.yml" "--output=${MRDOCS_SELF_DOC_OUTPUT}" + ${MRDOCS_BUILTIN_DIR_ARGS} --generator=noop - "--addons=${CMAKE_SOURCE_DIR}/data/mrdocs/addons" - "--stdlib-includes=${LIBCXX_DIR}" - "--stdlib-includes=${STDLIB_INCLUDE_DIR}" - "--libc-includes=${CMAKE_SOURCE_DIR}/data/mrdocs/headers/libc-stubs" - --concurrency=16 --log-level=debug $<$:--warn-as-error=true> ) diff --git a/tests/cmake/CMakeLists.txt b/tests/cmake/CMakeLists.txt new file mode 100644 index 00000000000..e73e366363a --- /dev/null +++ b/tests/cmake/CMakeLists.txt @@ -0,0 +1,37 @@ +# +# Licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +# +# Official repository: https://github.com/cppalliance/mrdocs +# + +cmake_minimum_required(VERSION 3.20) +project(mrdocs_cmake_consumer LANGUAGES CXX C) + +# Use mrdocs as a library and run it as a test. Discovery is driven by the +# exact-case mrdocs_ROOT (honored since CMake 3.12), so no policy opt-in is needed. +find_package(mrdocs REQUIRED) +add_executable(consumer src/main.cpp) +target_link_libraries(consumer PRIVATE mrdocs::mrdocs-core) +target_compile_features(consumer PRIVATE cxx_std_23) +enable_testing() +add_test(NAME consumer-runs COMMAND consumer) + +# A plain library, present so there is some project code to document. +add_library(example_calc src/calculator.cpp) +target_include_directories(example_calc PUBLIC include) +target_compile_features(example_calc PUBLIC cxx_std_20) +add_custom_target(consumer-docs ALL + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/reference + COMMAND ${MRDOCS_EXECUTABLE} + --config=${CMAKE_CURRENT_SOURCE_DIR}/mrdocs.yml + --compilation-database=${CMAKE_CURRENT_SOURCE_DIR}/docs/compile_commands.json + --output=${CMAKE_CURRENT_BINARY_DIR}/reference + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/include/example/calculator.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/mrdocs.yml + ${CMAKE_CURRENT_SOURCE_DIR}/docs/compile_commands.json + COMMENT "Generate consumer docs with MrDocs" + USES_TERMINAL) diff --git a/tests/cmake/docs/compile_commands.json b/tests/cmake/docs/compile_commands.json new file mode 100644 index 00000000000..70d0ec4a10b --- /dev/null +++ b/tests/cmake/docs/compile_commands.json @@ -0,0 +1,7 @@ +[ + { + "directory": "${MRDOCS_SOURCE_ROOT}", + "command": "clang++ -std=c++20 -I${MRDOCS_SOURCE_ROOT}/include -c ${MRDOCS_SOURCE_ROOT}/src/calculator.cpp -o ${MRDOCS_SOURCE_ROOT}/calculator.o", + "file": "${MRDOCS_SOURCE_ROOT}/src/calculator.cpp" + } +] diff --git a/tests/cmake/include/example/calculator.hpp b/tests/cmake/include/example/calculator.hpp new file mode 100644 index 00000000000..93abcbb97d8 --- /dev/null +++ b/tests/cmake/include/example/calculator.hpp @@ -0,0 +1,33 @@ +// +// Licensed under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +// +// Official repository: https://github.com/cppalliance/mrdocs +// + +#ifndef EXAMPLE_CALCULATOR_HPP +#define EXAMPLE_CALCULATOR_HPP + +namespace example { + +/** A tiny calculator. + + This type exists only to give the mrdocs CMake extension a documented + symbol to extract from this consumer project. +*/ +class Calculator +{ +public: + /// Return the sum of two integers. + int add(int a, int b) const; + + /// Return the product of two integers. + int multiply(int a, int b) const; +}; + +} // namespace example + +#endif diff --git a/tests/cmake/mrdocs.yml b/tests/cmake/mrdocs.yml new file mode 100644 index 00000000000..25740218aa9 --- /dev/null +++ b/tests/cmake/mrdocs.yml @@ -0,0 +1,11 @@ +# +# Licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +# +# Official repository: https://github.com/cppalliance/mrdocs +# + +source-root: . diff --git a/tests/cmake/src/calculator.cpp b/tests/cmake/src/calculator.cpp new file mode 100644 index 00000000000..e26184612a8 --- /dev/null +++ b/tests/cmake/src/calculator.cpp @@ -0,0 +1,27 @@ +// +// Licensed under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +// +// Official repository: https://github.com/cppalliance/mrdocs +// + +#include + +namespace example { + +int +Calculator::add(int a, int b) const +{ + return a + b; +} + +int +Calculator::multiply(int a, int b) const +{ + return a * b; +} + +} // namespace example diff --git a/tests/cmake/src/main.cpp b/tests/cmake/src/main.cpp new file mode 100644 index 00000000000..5e4ebd87a8e --- /dev/null +++ b/tests/cmake/src/main.cpp @@ -0,0 +1,51 @@ +// +// Licensed under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +// +// Official repository: https://github.com/cppalliance/mrdocs +// + +#include +#include +#include +#include + +int +main() +{ + // Exercises mrdocs and all its bundled private dependencies, forcing + // the linker to resolve those symbols out of the archive. + + // LLVM / Clang: mrdocs's configuration loader parses YAML with LLVM. + mrdocs::Config config; + if (!mrdocs::Config::load(config, "", nullptr)) + { + std::cerr << "Config::load failed\n"; + return 1; + } + + // JerryScript: compile and run a trivial script. + mrdocs::js::Context jsContext; + mrdocs::js::Scope jsScope(jsContext); + if (!jsScope.script("var x = 1 + 1;")) + { + std::cerr << "JavaScript engine failed\n"; + return 1; + } + + // Lua: compile a trivial chunk. + mrdocs::lua::Context luaContext; + mrdocs::lua::Scope luaScope(luaContext); + if (!luaScope.loadChunk("return 1 + 1")) + { + std::cerr << "Lua engine failed\n"; + return 1; + } + + std::cout << "mrdocs bundled dependencies (LLVM, Clang, JerryScript, Lua) " + "linked and ran from a consumer\n"; + return 0; +} diff --git a/tests/golden/CMakeLists.txt b/tests/golden/CMakeLists.txt index ff6fd54ed3c..dbfab8a935a 100644 --- a/tests/golden/CMakeLists.txt +++ b/tests/golden/CMakeLists.txt @@ -51,9 +51,7 @@ add_test(NAME mrdocs-golden-tests mrdocs-golden-tests --action=test "${CMAKE_CURRENT_SOURCE_DIR}/fixtures" - "--addons=${CMAKE_SOURCE_DIR}/data/mrdocs/addons" - "--stdlib-includes=${LIBCXX_DIR}" - "--libc-includes=${CMAKE_SOURCE_DIR}/data/mrdocs/headers/libc-stubs" + ${MRDOCS_BUILTIN_DIR_ARGS} --log-level=warn ) foreach (action IN ITEMS test create update) @@ -62,9 +60,7 @@ foreach (action IN ITEMS test create update) mrdocs-golden-tests --action=${action} "${CMAKE_CURRENT_SOURCE_DIR}/fixtures" - "--addons=${CMAKE_SOURCE_DIR}/data/mrdocs/addons" - "--stdlib-includes=${LIBCXX_DIR}" - "--libc-includes=${CMAKE_SOURCE_DIR}/data/mrdocs/headers/libc-stubs" + ${MRDOCS_BUILTIN_DIR_ARGS} --log-level=warn DEPENDS mrdocs-golden-tests ) @@ -96,12 +92,13 @@ endif() # where bash is unavailable (Windows). find_program(BASH_PROGRAM bash) if (BASH_PROGRAM AND NOT WIN32) - set(MRDOCS_SCHEMA_STDLIB "--stdlib-includes=${LIBCXX_DIR}") - if (STDLIB_INCLUDE_DIR) - string(APPEND MRDOCS_SCHEMA_STDLIB " --stdlib-includes=${STDLIB_INCLUDE_DIR}") - endif() + # generate-schema.sh passes --addons itself (ADDONS env); feed the remaining + # built-in dir flags through MRDOCS_EXTRA_ARGS as a space-separated string. + set(_schema_extra_args ${MRDOCS_BUILTIN_DIR_ARGS}) + list(FILTER _schema_extra_args EXCLUDE REGEX "^--addons=") + string(REPLACE ";" " " _schema_extra_args "${_schema_extra_args}") add_test(NAME schema-check COMMAND ${BASH_PROGRAM} "${PROJECT_SOURCE_DIR}/utils/codegen/generate-schema.sh" --check) set_tests_properties(schema-check PROPERTIES ENVIRONMENT - "MRDOCS=$;ADDONS=${PROJECT_SOURCE_DIR}/data/mrdocs/addons;MRDOCS_INPUT=${PROJECT_SOURCE_DIR}/CMakeLists.txt;MRDOCS_EXTRA_ARGS=${MRDOCS_SCHEMA_STDLIB} --libc-includes=${PROJECT_SOURCE_DIR}/data/mrdocs/headers/libc-stubs --concurrency=16") + "MRDOCS=$;ADDONS=${PROJECT_SOURCE_DIR}/data/mrdocs/addons;MRDOCS_INPUT=${PROJECT_SOURCE_DIR}/CMakeLists.txt;MRDOCS_EXTRA_ARGS=${_schema_extra_args} --concurrency=16") endif() diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index d4c2f9f11ad..5e8c228b72c 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -8,12 +8,6 @@ # Official repository: https://github.com/cppalliance/mrdocs # -# The tools link mrdocs-core, which the header-only "MrDocs build" does not -# build, so skip tools/ in that mode. -if (MRDOCS_BUILD_HEADERS_ONLY) - return() -endif() - # Command-line tools. Each tool lives in its own subdirectory with its own # CMakeLists; the mrdocs CLI is the only one so far. add_subdirectory(mrdocs) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 405e6a8ccf6..00978053e01 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -21,3 +21,4 @@ add_subdirectory(danger) add_subdirectory(docs) add_subdirectory(linting) add_subdirectory(testing) +add_subdirectory(cmake) diff --git a/utils/cmake/CMakeLists.txt b/utils/cmake/CMakeLists.txt new file mode 100644 index 00000000000..7f2d7e60575 --- /dev/null +++ b/utils/cmake/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# Licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +# +# Official repository: https://github.com/cppalliance/mrdocs +# + +include(helpers.cmake) +include(install.cmake) + diff --git a/utils/cmake/helpers.cmake b/utils/cmake/helpers.cmake index 591da643abc..3f9d052ea92 100644 --- a/utils/cmake/helpers.cmake +++ b/utils/cmake/helpers.cmake @@ -8,6 +8,10 @@ # Official repository: https://github.com/cppalliance/mrdocs # +# Included from the root early, before add_subdirectory(utils), because +# set_ternary is used there. +include_guard(GLOBAL) + # set_ternary( ) # # CMake has no configure-time ternary, so this stands in for the recurring @@ -29,4 +33,94 @@ macro(set_ternary out_var cond val_true val_false) set(${out_var} \"${val_false}\") endif () ") -endmacro() \ No newline at end of file +endmacro() + +# mrdocs_collect_static_libs( ...) +# +# Breadth-first walk of the given targets' link graph, splitting what it finds: +# : the transitive static-library files (as $), for +# merging into a bundle; +# : the transitive OS/system libraries (pthread, ntdll, ...) named +# as non-targets, which cannot be bundled and must be relinked. +function(mrdocs_collect_static_libs out_var out_system_var) + # Breadth-first walk of the link graph. + set(_static_libs "") # the static-library files found, as $ + set(_system_libs "") # non-target link items (system libraries / flags) + set(_visited "") # targets already processed (guards against cycles) + set(_to_visit "${ARGN}") # worklist of targets still to process + + while(_to_visit) + list(POP_FRONT _to_visit _entry) + + # Peel that generator expressions (e.g.: $ -> mrdocs-llvm) + string(REGEX REPLACE "^\\$<[A-Za-z_]+:(.+)>$" "\\1" _entry "${_entry}") + + # A non-target entry is a system library (pthread, ntdll, ...) or a raw + # linker flag: it can't be bundled so we return it so it can be remembered + # and relinked at install time later. + # When the toolchain is bundled the imported targets that would otherwise + # carry these are dropped, so a consumer needs them on its own link line. + if (NOT _entry OR NOT TARGET ${_entry}) + # Genex fragments left by an incomplete peel are not libraries: skip those. + if(_entry AND NOT _entry MATCHES "[$<>]") + list(APPEND _system_libs "${_entry}") + endif() + continue() + endif() + + # Resolve an alias to the target it stands for: (e.g. mrdocs::dom -> mrdocs-dom) + get_target_property(_aliased ${_entry} ALIASED_TARGET) + if(_aliased) + set(_entry ${_aliased}) + endif() + + # Process each target once in graph diamonds + if(_entry IN_LIST _visited) + continue() + endif() + list(APPEND _visited ${_entry}) + + # Store this node in the result when it is a static archive we can merge. + get_target_property(_type ${_entry} TYPE) + if(_type STREQUAL "STATIC_LIBRARY") + list(APPEND _static_libs "$") + elseif(_type STREQUAL "UNKNOWN_LIBRARY") + # A "Find" module located the file by path (find_library) + # Merge it ONLY if that file is actually a static + set(_loc "") + get_target_property(_configs ${_entry} IMPORTED_CONFIGURATIONS) + if(_configs) + list(GET _configs 0 _cfg) + get_target_property(_loc ${_entry} IMPORTED_LOCATION_${_cfg}) + endif() + if(NOT _loc) + get_target_property(_loc ${_entry} IMPORTED_LOCATION) + endif() + string(REPLACE "." "\\." _static_suffix "${CMAKE_STATIC_LIBRARY_SUFFIX}") + if(_loc AND _loc MATCHES "${_static_suffix}$") + list(APPEND _static_libs "$") + endif() + endif() + + # Queue this target's public and private linked libraries + get_target_property(_interface_libs ${_entry} INTERFACE_LINK_LIBRARIES) + if(_interface_libs) + list(APPEND _to_visit ${_interface_libs}) + endif() + if(NOT _type STREQUAL "INTERFACE_LIBRARY") + get_target_property(_private_libs ${_entry} LINK_LIBRARIES) + if(_private_libs) + list(APPEND _to_visit ${_private_libs}) + endif() + endif() + endwhile() + + if(_static_libs) + list(REMOVE_DUPLICATES _static_libs) + endif() + if(_system_libs) + list(REMOVE_DUPLICATES _system_libs) + endif() + set(${out_var} "${_static_libs}" PARENT_SCOPE) + set(${out_system_var} "${_system_libs}" PARENT_SCOPE) +endfunction() diff --git a/utils/cmake/install.cmake b/utils/cmake/install.cmake index 0c5e2220d5a..5cce19a8161 100644 --- a/utils/cmake/install.cmake +++ b/utils/cmake/install.cmake @@ -8,26 +8,25 @@ # Official repository: https://github.com/cppalliance/mrdocs # -# Build-support CMake module. It does nothing on include: it only defines -# functions the project can call. This is where project build/packaging helpers -# live (utils/ is developer/build tooling; utils/cmake/ groups its CMake -# modules, as data/cmake/ groups the shipped consumer-facing ones). Include it -# from the root to get the functions, then call them where appropriate. +# Build-support CMake module. +# +# It does nothing on include: it only defines the mrdocs_install function +# the project can call. +# +include_guard(GLOBAL) # mrdocs_install() # -# Finalizes installation and packaging. The individual targets install -# themselves next to their definitions (mrdocs-core in src/, mrdocs in tools/, -# shared data in data/); this finalizes the single mrdocs-targets export (which -# spans src/ and tools/, so it must be done once, after both are registered), -# writes the package config files, and configures CPack. Call it from the root -# after every target has been added. It self-guards, so calling it is always -# safe. +# Finalizes installation and packaging. +# +# The individual targets install themselves next to their definitions +# (mrdocs-core in src/, mrdocs in tools/, shared data in data/). +# +# This finalizes the single mrdocs-targets export (which spans src/ and tools/, +# so it must be done once, after both are registered). +# +# It writes the package config files, and configures CPack if packages are created. function(mrdocs_install) - if (MRDOCS_BUILD_HEADERS_ONLY OR NOT MRDOCS_INSTALL) - return() - endif() - install(EXPORT mrdocs-targets FILE mrdocs-targets.cmake NAMESPACE mrdocs:: @@ -46,13 +45,15 @@ function(mrdocs_install) DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mrdocs) # mrdocs-config.cmake - set(INCLUDE_INSTALL_DIR include/) - set(LIB_INSTALL_DIR lib/) + set(INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR}) + set(LIB_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR}) + set(BIN_INSTALL_DIR ${CMAKE_INSTALL_BINDIR}) + set(DATAROOT_INSTALL_DIR ${CMAKE_INSTALL_DATAROOTDIR}) configure_package_config_file( ${CMAKE_CURRENT_SOURCE_DIR}/mrdocs-config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/mrdocs-config.cmake INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mrdocs - PATH_VARS CMAKE_INSTALL_LIBDIR INCLUDE_INSTALL_DIR LIB_INSTALL_DIR) + PATH_VARS CMAKE_INSTALL_LIBDIR INCLUDE_INSTALL_DIR LIB_INSTALL_DIR BIN_INSTALL_DIR DATAROOT_INSTALL_DIR) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/mrdocs-config.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mrdocs) @@ -66,20 +67,24 @@ function(mrdocs_install) # CPack packaging. if (MRDOCS_PACKAGE) - set(CPACK_PACKAGE_VENDOR "mrdocs") - set(CPACK_PACKAGE_DESCRIPTION_SUMMARY ${PROJECT_DESCRIPTION}) - set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) - set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) - set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) - set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.txt") - set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.adoc") + mrdocs_package() + endif() +endfunction() - # Ignore files (from .gitignore) - FILE(READ ${CMAKE_CURRENT_SOURCE_DIR}/.gitignore GITIGNORE_CONTENTS) - STRING(REGEX REPLACE ";" "\\\\;" GITIGNORE_CONTENTS "${GITIGNORE_CONTENTS}") - STRING(REGEX REPLACE "\n" ";" GITIGNORE_CONTENTS "${GITIGNORE_CONTENTS}") - set(CPACK_SOURCE_IGNORE_FILES ${GITIGNORE_CONTENTS}) +function(mrdocs_package) + set(CPACK_PACKAGE_VENDOR "mrdocs") + set(CPACK_PACKAGE_DESCRIPTION_SUMMARY ${PROJECT_DESCRIPTION}) + set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) + set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) + set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) + set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.txt") + set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.adoc") - include(CPack) - endif() + # Ignore files (from .gitignore) + FILE(READ ${CMAKE_CURRENT_SOURCE_DIR}/.gitignore GITIGNORE_CONTENTS) + STRING(REGEX REPLACE ";" "\\\\;" GITIGNORE_CONTENTS "${GITIGNORE_CONTENTS}") + STRING(REGEX REPLACE "\n" ";" GITIGNORE_CONTENTS "${GITIGNORE_CONTENTS}") + set(CPACK_SOURCE_IGNORE_FILES ${GITIGNORE_CONTENTS}) + + include(CPack) endfunction() diff --git a/utils/codegen/CMakeLists.txt b/utils/codegen/CMakeLists.txt index e0d5090e4f1..eb382d23c8d 100644 --- a/utils/codegen/CMakeLists.txt +++ b/utils/codegen/CMakeLists.txt @@ -14,75 +14,28 @@ # (mrdocs-core, the mrdocs tool, the golden tests) and before the src/ header # build that parses them. -# ---- include/mrdocs/Version.hpp from Version.hpp.in ---- -if (MRDOCS_REQUIRE_GIT) - find_package(Git REQUIRED) -else() - find_package(Git QUIET) -endif() -set(PROJECT_VERSION_BUILD "") -set(PROJECT_VERSION_WITH_BUILD "${PROJECT_VERSION}") # default: plain semver -if (EXISTS "${PROJECT_SOURCE_DIR}/.git" AND GIT_FOUND) - # Get full SHA - execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse HEAD - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - OUTPUT_VARIABLE GIT_SHA_FULL - OUTPUT_STRIP_TRAILING_WHITESPACE - RESULT_VARIABLE GIT_SHA_FULL_RV - ERROR_QUIET - ) - if (NOT GIT_SHA_FULL_RV EQUAL 0) - message(FATAL_ERROR "Git was found but could not extract commit SHA") - endif() - set(PROJECT_VERSION_BUILD "${GIT_SHA_FULL}") - string(SUBSTRING "${GIT_SHA_FULL}" 0 12 GIT_SHA_SHORT) - - # Are we exactly at a tag? - execute_process( - COMMAND ${GIT_EXECUTABLE} describe --tags --exact-match HEAD - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - OUTPUT_VARIABLE GIT_EXACT_TAG - OUTPUT_STRIP_TRAILING_WHITESPACE - RESULT_VARIABLE GIT_EXACT_TAG_RV - ERROR_QUIET - ) - if (GIT_EXACT_TAG_RV EQUAL 0) - # On a tag: canonical release - set(PROJECT_VERSION_WITH_BUILD "${PROJECT_VERSION}") - else() - # Dirty working tree? (0 = clean, 1 = dirty) - execute_process( - COMMAND ${GIT_EXECUTABLE} diff --quiet --ignore-submodules - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - RESULT_VARIABLE GIT_DIRTY_RV - ERROR_QUIET - ) - set(_dirty_suffix "") - if (GIT_DIRTY_RV EQUAL 1) - set(_dirty_suffix ".modified") - endif() - set(PROJECT_VERSION_WITH_BUILD - "${PROJECT_VERSION}+${GIT_SHA_SHORT}${_dirty_suffix}") - endif() -else() - if (MRDOCS_REQUIRE_GIT) - message(FATAL_ERROR "Git is required to extract the version build") - endif() - set(PROJECT_VERSION_BUILD "") - set(PROJECT_VERSION_WITH_BUILD "${PROJECT_VERSION}") -endif() -configure_file( - ${PROJECT_SOURCE_DIR}/include/mrdocs/Version.hpp.in - ${PROJECT_BINARY_DIR}/include/mrdocs/Version.hpp - @ONLY -) - -# ---- include/mrdocs/ConfigSchema.hpp + related files from the JSON config ---- find_program(PYTHON_EXECUTABLE python3 python) if (NOT PYTHON_EXECUTABLE) message(FATAL_ERROR "Python is needed to configure mrdocs") endif() + +# ---- include/mrdocs/Version.hpp from Version.hpp.in ---- +set_ternary(MRDOCS_REQUIRE_GIT_OPT MRDOCS_REQUIRE_GIT REQUIRED QUIET) +find_package(Git ${MRDOCS_REQUIRE_GIT_OPT}) +add_custom_target(mrdocs-version ALL + BYPRODUCTS ${PROJECT_BINARY_DIR}/include/mrdocs/Version.hpp + COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/generate-version-header.py + ${PROJECT_SOURCE_DIR}/include/mrdocs/Version.hpp.in + ${PROJECT_BINARY_DIR}/include/mrdocs/Version.hpp + --version ${PROJECT_VERSION} + --name ${PROJECT_NAME} + --description "${PROJECT_DESCRIPTION}" + --source-dir ${PROJECT_SOURCE_DIR} + --git "${GIT_EXECUTABLE}" + COMMENT "Generating Version.hpp" + VERBATIM) + +# ---- include/mrdocs/ConfigSchema.hpp from ConfigOptions.json ---- set(CONFIG_GEN_WORKING_DIR "${PROJECT_SOURCE_DIR}") set(CONFIG_GEN_OUTPUT_DIR "${PROJECT_BINARY_DIR}") set(CONFIG_GEN_CONFIG_JSON "${PROJECT_SOURCE_DIR}/src/mrdocs/ConfigOptions.json") @@ -98,36 +51,23 @@ set(CONFIG_GEN_INPUT_FILES set(CONFIG_GEN_OUTPUT_FILES ${CONFIG_GEN_OUTPUT_DIR}/include/mrdocs/ConfigSchema.hpp ) -if (MRDOCS_BUILD_HEADERS_ONLY) - # Create the files at configure time: the src/ header build parses the - # generated public headers immediately and defines no build-time targets. - execute_process( - COMMAND ${CONFIG_GEN_COMMAND_INFO} - WORKING_DIRECTORY ${CONFIG_GEN_WORKING_DIR} - COMMAND_ERROR_IS_FATAL ANY - ) - execute_process( - COMMAND ${CONFIG_GEN_COMMAND_SCHEMA} - WORKING_DIRECTORY ${CONFIG_GEN_WORKING_DIR} - COMMAND_ERROR_IS_FATAL ANY - ) - set_source_files_properties(${CONFIG_GEN_OUTPUT_FILES} PROPERTIES GENERATED TRUE) -else() - # Generate at build time. The outputs are listed as sources by mrdocs-core, - # the mrdocs tool, and the golden tests; mrdocs-codegen gives those targets - # a stable dependency to order against across directories. - add_custom_command( - OUTPUT ${CONFIG_GEN_OUTPUT_FILES} - COMMAND ${CONFIG_GEN_COMMAND_INFO} - COMMAND ${CONFIG_GEN_COMMAND_SCHEMA} - WORKING_DIRECTORY ${CONFIG_GEN_WORKING_DIR} - DEPENDS ${CONFIG_GEN_INPUT_FILES} - COMMENT "Generating Config Source Files" - VERBATIM - COMMAND_EXPAND_LISTS - ) - add_custom_target(mrdocs-codegen DEPENDS ${CONFIG_GEN_OUTPUT_FILES}) -endif() +# Generate at build time. The outputs are listed as sources by mrdocs-core, the +# mrdocs tool, and the golden tests; mrdocs-codegen gives those targets a stable +# dependency to order against across directories. +add_custom_command( + OUTPUT ${CONFIG_GEN_OUTPUT_FILES} + COMMAND ${CONFIG_GEN_COMMAND_INFO} + COMMAND ${CONFIG_GEN_COMMAND_SCHEMA} + WORKING_DIRECTORY ${CONFIG_GEN_WORKING_DIR} + DEPENDS ${CONFIG_GEN_INPUT_FILES} + COMMENT "Generating Config Source Files" + VERBATIM + COMMAND_EXPAND_LISTS +) +add_custom_target(mrdocs-codegen DEPENDS ${CONFIG_GEN_OUTPUT_FILES}) +# mrdocs-core, the mrdocs tool, and the golden tests all depend on mrdocs-codegen; +# chaining the version target here orders it before them without extra wiring. +add_dependencies(mrdocs-codegen mrdocs-version) # ---- schema check (mirrors the CI "Check YAML schema" step) ---- # The published config schema (docs/modules/ROOT/attachments/schemas/ diff --git a/utils/codegen/generate-config-info.py b/utils/codegen/generate-config-info.py index 4e28797c7dd..a25167affb4 100644 --- a/utils/codegen/generate-config-info.py +++ b/utils/codegen/generate-config-info.py @@ -819,7 +819,12 @@ def generate_option_declaration(option, style=None): # Enums should be initialized in the hpp file cpp_default_value = to_cpp_default_value(option, True) if cpp_default_value: - contents += f' = {cpp_default_value}' + # Cast a negative default for an unsigned field so MSVC does not warn + # C4245 (signed/unsigned mismatch), e.g. `unsigned report = -1`. + if option["type"] == 'unsigned' and str(cpp_default_value).startswith('-'): + contents += f' = static_cast({cpp_default_value})' + else: + contents += f' = {cpp_default_value}' contents += ';' return contents diff --git a/utils/codegen/generate-version-header.py b/utils/codegen/generate-version-header.py new file mode 100644 index 00000000000..5b6c46a5c26 --- /dev/null +++ b/utils/codegen/generate-version-header.py @@ -0,0 +1,101 @@ +# +# Licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Copyright (c) 2026 Alan de Freitas (alandefreitas@gmail.com) +# +# Official repository: https://github.com/cppalliance/mrdocs +# + +# Generates Version.hpp from Version.hpp.in by substituting @PROJECT_VERSION@ and +# friends, stamping the git build metadata. Like the other codegen scripts, it is +# meant to run at build time so the SHA reflects the commit being built; it only +# rewrites the output when the content changes, so it never forces a rebuild. + +import argparse +import os +import subprocess + + +def git(git_exe, args, source_dir): + return subprocess.run( + [git_exe, *args], cwd=source_dir, + capture_output=True, text=True) + + +def version_with_build(git_exe, version, source_dir): + """Return (version_with_build, full_sha) from the git state. + + Git is optional here: CMake already found it (or decided it was not required) + before this runs, so on any git failure we just fall back to the plain version. + """ + try: + head = git(git_exe, ["rev-parse", "HEAD"], source_dir) + except OSError: + # git executable not found: fall back to the plain version. + return version, "" + if head.returncode != 0: + return version, "" + + full_sha = head.stdout.strip() + short_sha = full_sha[:12] + + # Exactly on a tag: canonical release, no build metadata. + on_tag = git(git_exe, ["describe", "--tags", "--exact-match", "HEAD"], source_dir).returncode == 0 + if on_tag: + return version, full_sha + + # Off a tag: append the short SHA, plus .modified if the tree is dirty. + dirty = git(git_exe, ["diff", "--quiet", "--ignore-submodules"], source_dir).returncode == 1 + suffix = ".modified" if dirty else "" + return f"{version}+{short_sha}{suffix}", full_sha + + +def main(): + parser = argparse.ArgumentParser(description="Generate Version.hpp from Version.hpp.in") + parser.add_argument("template", help="path to Version.hpp.in") + parser.add_argument("output", help="path to the Version.hpp to generate") + parser.add_argument("--version", required=True, help="project version (MAJOR.MINOR.PATCH)") + parser.add_argument("--name", default="", help="project name") + parser.add_argument("--description", default="", help="project description") + parser.add_argument("--source-dir", default=".", help="repository root, used for the git queries") + parser.add_argument("--git", default="git", + help="git executable (CMake passes the one it found; defaults to PATH)") + args = parser.parse_args() + + git_exe = args.git or "git" + build_version, build_sha = version_with_build(git_exe, args.version, args.source_dir) + # Split MAJOR.MINOR.PATCH, defaulting any missing component to 0. + parts = (args.version.split(".") + ["0", "0", "0"])[:3] + substitutions = { + "PROJECT_NAME": args.name, + "PROJECT_DESCRIPTION": args.description, + "PROJECT_VERSION": args.version, + "PROJECT_VERSION_MAJOR": parts[0], + "PROJECT_VERSION_MINOR": parts[1], + "PROJECT_VERSION_PATCH": parts[2], + "PROJECT_VERSION_WITH_BUILD": build_version, + "PROJECT_VERSION_BUILD": build_sha, + } + + content = '' + with open(args.template, "r") as f: + content = f.read() + for name, value in substitutions.items(): + content = content.replace(f"@{name}@", value) + + # Only write when the content changed, so an unchanged SHA never touches the + # file (and never triggers a rebuild of everything that includes it). + previous = None + if os.path.exists(args.output): + with open(args.output, "r") as f: + previous = f.read() + if content != previous: + os.makedirs(os.path.dirname(args.output), exist_ok=True) + with open(args.output, "w") as f: + f.write(content) + + +if __name__ == "__main__": + main()