From 6a5890b01217125086215544eece0c71cea749e5 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 8 Oct 2026 11:27:55 -0700 Subject: [PATCH 1/4] Add consumer documentation extensions and runtime DLL helper Expose additive Doxygen inputs and options while preserving custom configurations. Share Windows runtime DLL deployment with custom executables and cover both features with fixtures and CI checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 13 ++++ CLAUDE.md | 9 ++- README.md | 52 +++++++++++++ cmake/cpp-library-docs.cmake | 23 ++++++ cmake/cpp-library-testing.cmake | 16 ++++ cpp-library.cmake | 19 ++--- templates/Doxyfile.in | 12 +-- tests/docs/fixtures/CMakeLists.txt | 52 +++++++++++++ tests/docs/fixtures/absolute docs/group.dox | 3 + tests/docs/fixtures/extra docs/mainpage.dox | 3 + tests/docs/fixtures/include/fixture/api.hpp | 12 +++ tests/docs/test_docs.cmake | 78 +++++++++++++++++++ .../fixtures/runtime_dlls/CMakeLists.txt | 21 +++++ tests/setup/fixtures/runtime_dlls/main.cpp | 2 + tests/setup/fixtures/runtime_dlls/runtime.cpp | 1 + tests/setup/test_runtime_dlls.cmake | 34 ++++++++ 16 files changed, 333 insertions(+), 17 deletions(-) create mode 100644 tests/docs/fixtures/CMakeLists.txt create mode 100644 tests/docs/fixtures/absolute docs/group.dox create mode 100644 tests/docs/fixtures/extra docs/mainpage.dox create mode 100644 tests/docs/fixtures/include/fixture/api.hpp create mode 100644 tests/docs/test_docs.cmake create mode 100644 tests/setup/fixtures/runtime_dlls/CMakeLists.txt create mode 100644 tests/setup/fixtures/runtime_dlls/main.cpp create mode 100644 tests/setup/fixtures/runtime_dlls/runtime.cpp create mode 100644 tests/setup/test_runtime_dlls.cmake diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0eedee..f2a346b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,9 @@ jobs: - name: Run target type tests run: cmake -P tests/setup/test_target_type.cmake + - name: Run custom executable runtime deployment tests + run: cmake -P tests/setup/test_runtime_dlls.cmake + - name: Run setup.cmake version resolution test run: cmake -P tests/setup/test_setup_version_resolution.cmake @@ -44,6 +47,9 @@ jobs: - name: Run target type tests run: cmake -P tests/setup/test_target_type.cmake + - name: Run custom executable runtime deployment tests + run: cmake -P tests/setup/test_runtime_dlls.cmake + - name: Run nested static and shared installation round trips run: cmake -P tests/install/test_nested_install.cmake @@ -165,6 +171,13 @@ jobs: # [DEPENDENCY] https://github.com/actions/checkout/releases - uses: actions/checkout@v7 + - name: Install Doxygen + # [DEPENDENCY] https://github.com/ssciwr/doxygen-install/releases + uses: ssciwr/doxygen-install@fa3e94d34131d3990b6b8ba939ab982964aa3758 + + - name: Test documentation configuration and rendered output + run: cmake -P tests/docs/test_docs.cmake + - name: Check README examples run: | # Extract and validate code blocks from README diff --git a/CLAUDE.md b/CLAUDE.md index 5b533f6..1880ad9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,15 +12,20 @@ Two ways consumers use this repo: ## Commands -Run cpp-library's own test suite (pure CMake scripts, no C++ compilation involved): +Run cpp-library's tests through CMake scripts. Integration fixtures require a +C++ compiler and Ninja; documentation rendering also requires Doxygen: ```bash cmake -P tests/install/CMakeLists.txt # dependency mapping/merging unit tests cmake -P tests/install/test_provider_merge.cmake # dependency provider merge tests cmake -P tests/setup/test_setup_version_resolution.cmake # setup.cmake version-resolution integration test +cmake -P tests/setup/test_runtime_dlls.cmake # custom target DLL deployment; requires compiler and Ninja +cmake -P tests/docs/test_docs.cmake # docs extensions and rendering; requires Doxygen and Ninja ``` -These are the same commands CI (`.github/workflows/ci.yml`, job `unit-tests`) runs. There is no build step for cpp-library itself — `cmake -P` executes the scripts directly. +CI (`.github/workflows/ci.yml`) runs these scripts in its unit-test, Windows +package, and documentation jobs. There is no library build step for cpp-library +itself; integration scripts configure and build their own consumer fixtures. CI also runs an `integration-tests` job that generates a throwaway consumer project, configures/builds/installs it against this repo via `CPMAddPackage(... SOURCE_DIR ...)`, then verifies `find_package()` works against the installed package — this is the best reference for the full round-trip a downstream project goes through. diff --git a/README.md b/README.md index c354a42..e653ddd 100644 --- a/README.md +++ b/README.md @@ -361,6 +361,8 @@ cpp_library_setup( [EXAMPLES example_list] # Example source files to build (e.g., "example.cpp example_fail.cpp") [TESTS test_list] # Test source files to build (e.g., "tests.cpp") [DOCS_EXCLUDE_SYMBOLS symbols] # Symbols to exclude from docs + [DOCS_INPUTS input_list] # Additional documentation files/directories + [DOCS_OPTIONS setting_list] # Additive single-line Doxygen assignments [REQUIRES_CPP_VERSION 17|20|23] # C++ version (default: 17) [INSTALL_OPTION option_name] # Independent installation control (default: uppercase NAMESPACE_INSTALL) ) @@ -374,6 +376,56 @@ cpp_library_setup( - Installation is controlled by `INSTALL_OPTION`, or the uppercase `${NAMESPACE}_INSTALL` option when omitted. Options default to `PROJECT_IS_TOP_LEVEL`. +### Documentation configuration + +Keep the toolkit's Doxyfile template instead of copying it into your project. +`DOCS_INPUTS` adds files or directories to the default `include` input. Relative +paths are resolved against the directory calling `cpp_library_setup`; absolute +paths and paths containing spaces are supported. Missing inputs fail configuration. + +`DOCS_OPTIONS` applies project-specific Doxygen assignments after the shared +template. Each quoted CMake argument must be a single `TAG = value` or +`TAG += value` assignment, with Doxygen quoting inside the value when needed. +Use spaces, not CMake semicolons, to separate Doxygen list values. The toolkit +validates assignment syntax; Doxygen validates tag names and values. Settings +are literal, not `configure_file` templates: use `${...}` for CMake variables. + +```cmake +cpp_library_setup( + DESCRIPTION "My library" + NAMESPACE mylib + HEADERS api.hpp + DOCS_INPUTS docs/doxygen "${PROJECT_BINARY_DIR}/generated docs" + DOCS_EXCLUDE_SYMBOLS "mylib::detail" + DOCS_OPTIONS + "WARN_IF_UNDOCUMENTED = NO" + "INCLUDE_PATH = \"${PROJECT_BINARY_DIR}/include\"" + "MACRO_EXPANSION = YES" + "PREDEFINED = MYLIB_API= MYLIB_NAMESPACE_BEGIN()= MYLIB_NAMESPACE_END()=" +) +``` + +Existing `docs/Doxyfile` overrides remain supported. Additional inputs and options +are applied after that configuration too, but a full override does not inherit +future toolkit-template changes. Prefer the shared template with only the +project-specific deltas above. Documentation is configured only for top-level +projects with `BUILD_DOCS` enabled. + +### `cpp_library_copy_runtime_dlls` + +```cmake +add_executable(custom-test main.cpp support.cpp) +target_link_libraries(custom-test PRIVATE mylib::my-library) +cpp_library_copy_runtime_dlls(custom-test) +``` + +Call this helper in the directory that created the target. On Windows, it deploys +`TARGET_RUNTIME_DLLS` beside the target after building, including transitive +runtime DLLs. Static targets with no runtime DLLs require no copy; on other +platforms the helper does nothing. It uses CMake 3.24-compatible copy operations +and handles DLLs already in the destination. Toolkit-created test/example +executables use the same helper automatically. + ### Target Naming Use the component name as your project name, and specify the organizational namespace separately: diff --git a/cmake/cpp-library-docs.cmake b/cmake/cpp-library-docs.cmake index 72cc1bc..a2595cb 100644 --- a/cmake/cpp-library-docs.cmake +++ b/cmake/cpp-library-docs.cmake @@ -13,10 +13,29 @@ function(_cpp_library_setup_docs) ) set(multiValueArgs DOCS_EXCLUDE_SYMBOLS + DOCS_INPUTS + DOCS_OPTIONS ) cmake_parse_arguments(ARG "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + set(docs_extensions "") + foreach(input IN LISTS ARG_DOCS_INPUTS) + get_filename_component(input "${input}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + if(NOT EXISTS "${input}") + message(FATAL_ERROR "cpp-library: Documentation input does not exist: ${input}") + endif() + string(REPLACE "\"" "\\\"" input "${input}") + string(APPEND docs_extensions "INPUT += \"${input}\"\n") + endforeach() + foreach(option IN LISTS ARG_DOCS_OPTIONS) + if(NOT option MATCHES "^[A-Z][A-Z0-9_]*[ \t]*\\+?=[^\r\n]*$") + message(FATAL_ERROR + "cpp-library: DOCS_OPTIONS requires single-line Doxygen assignments: ${option}") + endif() + string(APPEND docs_extensions "${option}\n") + endforeach() + find_package(Doxygen REQUIRED) # Download doxygen-awesome-css theme via CPM @@ -60,6 +79,10 @@ function(_cpp_library_setup_docs) else() configure_file(${DOXYFILE_IN} ${DOXYFILE_OUT} @ONLY) endif() + if(NOT docs_extensions STREQUAL "") + file(APPEND "${DOXYFILE_OUT}" + "\n# Project documentation settings supplied through cpp_library_setup.\n${docs_extensions}") + endif() # Add custom target for documentation with proper stderr capture if(WIN32) diff --git a/cmake/cpp-library-testing.cmake b/cmake/cpp-library-testing.cmake index 8072871..eb7c7c2 100644 --- a/cmake/cpp-library-testing.cmake +++ b/cmake/cpp-library-testing.cmake @@ -6,6 +6,22 @@ # This file is kept for backward compatibility but the actual implementation # is now in the _cpp_library_setup_executables function. +# Deploys runtime DLLs beside a custom target on Windows; empty lists need no copy. +# Call in the directory that created the target, as required by POST_BUILD commands. +function(cpp_library_copy_runtime_dlls target) + if(NOT TARGET "${target}") + message(FATAL_ERROR "cpp_library_copy_runtime_dlls: Target does not exist: ${target}") + endif() + if(WIN32) + add_custom_command(TARGET ${target} POST_BUILD + COMMAND "${CMAKE_COMMAND}" + "-DCPP_LIBRARY_RUNTIME_DLLS=$" + "-DCPP_LIBRARY_RUNTIME_DESTINATION=$" + -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/cpp-library-copy-runtime-dlls.cmake" + VERBATIM) + endif() +endfunction() + # Delegates to _cpp_library_setup_executables for backward compatibility. # - Postcondition: test executables configured via _cpp_library_setup_executables function(_cpp_library_setup_testing) diff --git a/cpp-library.cmake b/cpp-library.cmake index ef397ec..88ab48d 100644 --- a/cpp-library.cmake +++ b/cpp-library.cmake @@ -134,15 +134,7 @@ function(_cpp_library_setup_executables) add_executable(${executable_base} "${source_dir}/${executable}") target_link_libraries(${executable_base} PRIVATE ${ARG_NAMESPACE}::${CLEAN_NAME} doctest::doctest) - if(WIN32) - # A script handles empty DLL lists without requiring newer copy commands. - add_custom_command(TARGET ${executable_base} POST_BUILD - COMMAND "${CMAKE_COMMAND}" - "-DCPP_LIBRARY_RUNTIME_DLLS=$" - "-DCPP_LIBRARY_RUNTIME_DESTINATION=$" - -P "${CPP_LIBRARY_ROOT}/cmake/cpp-library-copy-runtime-dlls.cmake" - VERBATIM) - endif() + cpp_library_copy_runtime_dlls(${executable_base}) # Register as CTest test add_test(NAME ${executable_base} COMMAND ${executable_base}) @@ -182,6 +174,8 @@ function(cpp_library_setup) EXAMPLES # Example source files to build (e.g., "example.cpp example_fail.cpp") TESTS # Test source files to build (e.g., "tests.cpp") DOCS_EXCLUDE_SYMBOLS # Symbols to exclude from docs + DOCS_INPUTS # Additional documentation files or directories + DOCS_OPTIONS # Single-line Doxygen assignments applied after the template ) cmake_parse_arguments(ARG "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) @@ -189,6 +183,11 @@ function(cpp_library_setup) if("INSTALL_OPTION" IN_LIST ARG_KEYWORDS_MISSING_VALUES) message(FATAL_ERROR "cpp_library_setup: INSTALL_OPTION requires a value") endif() + foreach(keyword DOCS_INPUTS DOCS_OPTIONS) + if(keyword IN_LIST ARG_KEYWORDS_MISSING_VALUES) + message(FATAL_ERROR "cpp_library_setup: ${keyword} requires a value") + endif() + endforeach() set(install_args) if(DEFINED ARG_INSTALL_OPTION) list(APPEND install_args INSTALL_OPTION "${ARG_INSTALL_OPTION}") @@ -333,6 +332,8 @@ function(cpp_library_setup) VERSION "${ARG_VERSION}" DESCRIPTION "${ARG_DESCRIPTION}" DOCS_EXCLUDE_SYMBOLS "${ARG_DOCS_EXCLUDE_SYMBOLS}" + DOCS_INPUTS "${ARG_DOCS_INPUTS}" + DOCS_OPTIONS "${ARG_DOCS_OPTIONS}" ) endif() diff --git a/templates/Doxyfile.in b/templates/Doxyfile.in index 7d786e7..99a2b0d 100644 --- a/templates/Doxyfile.in +++ b/templates/Doxyfile.in @@ -4,18 +4,18 @@ PROJECT_NAME = "@PROJECT_NAME@" PROJECT_BRIEF = "@PROJECT_BRIEF@" PROJECT_NUMBER = @PROJECT_VERSION@ -OUTPUT_DIRECTORY = @OUTPUT_DIR@ +OUTPUT_DIRECTORY = "@OUTPUT_DIR@" GENERATE_LATEX = NO QUIET = YES WARN_IF_UNDOCUMENTED = YES -INPUT = @INPUT_DIR@ +INPUT = "@INPUT_DIR@" RECURSIVE = YES EXCLUDE_SYMBOLS = @EXCLUDE_SYMBOLS@ -EXAMPLE_PATH = @EXAMPLES_PATH@ +EXAMPLE_PATH = "@EXAMPLES_PATH@" HIDE_UNDOC_NAMESPACES = NO -HTML_EXTRA_STYLESHEET = @AWESOME_CSS_PATH@/doxygen-awesome.css \ - @AWESOME_CSS_PATH@/doxygen-awesome-sidebar-only.css \ - @CPP_LIBRARY_ROOT@/templates/custom.css +HTML_EXTRA_STYLESHEET = "@AWESOME_CSS_PATH@/doxygen-awesome.css" \ + "@AWESOME_CSS_PATH@/doxygen-awesome-sidebar-only.css" \ + "@CPP_LIBRARY_ROOT@/templates/custom.css" HTML_COLORSTYLE = LIGHT GENERATE_TREEVIEW = YES DISABLE_INDEX = NO diff --git a/tests/docs/fixtures/CMakeLists.txt b/tests/docs/fixtures/CMakeLists.txt new file mode 100644 index 0000000..89d485c --- /dev/null +++ b/tests/docs/fixtures/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.24) +project(docs-fixture LANGUAGES CXX) +include("${TOOLKIT_SOURCE}/cpp-library.cmake") +set(CPP_LIBRARY_VERSION 1.2.3) +set(FIXTURE_INSTALL OFF CACHE BOOL "") +set(BUILD_DOCS ON) + +# Replace only the network theme download; exercise real configuration and Doxygen. +function(CPMAddPackage) + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/theme") + foreach(stylesheet doxygen-awesome.css doxygen-awesome-sidebar-only.css) + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/theme/${stylesheet}" "") + endforeach() + set(doxygen-awesome-css_SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/theme" PARENT_SCOPE) +endfunction() + +set(docs_args) +if(CASE STREQUAL "extended") + list(APPEND docs_args + DOCS_INPUTS "extra docs" "${CMAKE_CURRENT_SOURCE_DIR}/absolute docs/group.dox" + DOCS_OPTIONS + "PROJECT_BRIEF = \"Customized documentation\"" + "WARN_IF_UNDOCUMENTED = NO" + "EXCLUDE_SYMBOLS += fixture::extra" + DOCS_EXCLUDE_SYMBOLS "fixture::hidden") +elseif(CASE STREQUAL "missing-input") + list(APPEND docs_args DOCS_INPUTS "missing docs") +elseif(CASE STREQUAL "malformed-option") + list(APPEND docs_args DOCS_OPTIONS "not a Doxygen assignment") +elseif(CASE STREQUAL "multiline-option") + list(APPEND docs_args DOCS_OPTIONS "QUIET = YES\nGENERATE_XML = YES") +elseif(CASE STREQUAL "missing-input-value") + list(APPEND docs_args DOCS_INPUTS) +elseif(CASE STREQUAL "missing-option-value") + list(APPEND docs_args DOCS_OPTIONS) +elseif(CASE STREQUAL "custom") + file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/docs") + file(WRITE "${CMAKE_CURRENT_SOURCE_DIR}/docs/Doxyfile" + "PROJECT_NAME = \"Legacy configuration\"\n" + "INPUT = \"@INPUT_DIR@\"\n" + "OUTPUT_DIRECTORY = \"@OUTPUT_DIR@\"\n" + "GENERATE_LATEX = NO\nQUIET = YES\nWARN_AS_ERROR = YES\n") + list(APPEND docs_args DOCS_INPUTS "extra docs" + DOCS_OPTIONS "PROJECT_BRIEF = \"Customized documentation\"") +endif() + +cpp_library_setup( + DESCRIPTION "Documentation fixture" + NAMESPACE fixture + INSTALL_OPTION FIXTURE_INSTALL + HEADERS api.hpp + ${docs_args}) diff --git a/tests/docs/fixtures/absolute docs/group.dox b/tests/docs/fixtures/absolute docs/group.dox new file mode 100644 index 0000000..f0d0960 --- /dev/null +++ b/tests/docs/fixtures/absolute docs/group.dox @@ -0,0 +1,3 @@ +/** @defgroup extra_group Absolute fixture group + * @brief This group comes from an absolute input file. + */ diff --git a/tests/docs/fixtures/extra docs/mainpage.dox b/tests/docs/fixtures/extra docs/mainpage.dox new file mode 100644 index 0000000..2f209d0 --- /dev/null +++ b/tests/docs/fixtures/extra docs/mainpage.dox @@ -0,0 +1,3 @@ +/** @mainpage Extra fixture content + * This page comes from an additional, space-containing input directory. + */ diff --git a/tests/docs/fixtures/include/fixture/api.hpp b/tests/docs/fixtures/include/fixture/api.hpp new file mode 100644 index 0000000..ad3e3aa --- /dev/null +++ b/tests/docs/fixtures/include/fixture/api.hpp @@ -0,0 +1,12 @@ +/** @file + * @brief Fixture public API. + */ +/** @brief Fixture namespace. */ +namespace fixture { +/** @brief Return the fixture value. */ +inline int value() { return 42; } +/** @brief Internal API omitted from the extended documentation. */ +struct hidden {}; +/** @brief Internal API omitted through an additive Doxygen option. */ +struct extra {}; +} diff --git a/tests/docs/test_docs.cmake b/tests/docs/test_docs.cmake new file mode 100644 index 0000000..8b115b8 --- /dev/null +++ b/tests/docs/test_docs.cmake @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: BSL-1.0 +# Run as: cmake -P tests/docs/test_docs.cmake (requires Doxygen and Ninja). +cmake_minimum_required(VERSION 3.24) +get_filename_component(toolkit_source "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +string(RANDOM LENGTH 16 ALPHABET 0123456789abcdef test_id) +set(test_root "${toolkit_source}/build/docs-${test_id}") + +foreach(case default extended custom missing-input malformed-option multiline-option + missing-input-value missing-option-value) + set(source "${test_root}/${case}/source with spaces") + set(binary "${test_root}/${case}/build with spaces") + file(MAKE_DIRECTORY "${source}") + file(COPY "${CMAKE_CURRENT_LIST_DIR}/fixtures/" DESTINATION "${source}") + execute_process(COMMAND "${CMAKE_COMMAND}" -S "${source}" -B "${binary}" -G Ninja + "-DTOOLKIT_SOURCE=${toolkit_source}" "-DCASE=${case}" + RESULT_VARIABLE result OUTPUT_VARIABLE out ERROR_VARIABLE err) + if(case MATCHES "^(missing-|malformed-|multiline-)") + if(case STREQUAL "missing-input") + set(expected "cpp-library: Documentation input does not exist") + elseif(case STREQUAL "missing-input-value") + set(expected "cpp_library_setup: DOCS_INPUTS requires a value") + elseif(case STREQUAL "missing-option-value") + set(expected "cpp_library_setup: DOCS_OPTIONS requires a value") + else() + set(expected "cpp-library: DOCS_OPTIONS requires single-line Doxygen assignments") + endif() + string(FIND "${out}\n${err}" "${expected}" diagnostic) + if(result EQUAL 0 OR diagnostic LESS 0) + message(FATAL_ERROR "${case}: Expected '${expected}':\n${out}\n${err}") + endif() + else() + if(NOT result EQUAL 0) + message(FATAL_ERROR "${case}: Configure failed:\n${out}\n${err}") + endif() + execute_process(COMMAND "${CMAKE_COMMAND}" --build "${binary}" --target docs + RESULT_VARIABLE result OUTPUT_VARIABLE out ERROR_VARIABLE err) + if(NOT result EQUAL 0) + message(FATAL_ERROR "${case}: Doxygen failed:\n${out}\n${err}") + endif() + file(READ "${binary}/html/index.html" index) + if(case STREQUAL "default") + string(FIND "${index}" "Documentation fixture" brief) + else() + string(FIND "${index}" "Customized documentation" brief) + string(FIND "${index}" "Extra fixture content" page) + if(page LESS 0) + message(FATAL_ERROR "${case}: Additional main page is missing") + endif() + endif() + if(brief LESS 0) + message(FATAL_ERROR "${case}: Project brief is missing") + endif() + if(case STREQUAL "default" OR case STREQUAL "extended") + foreach(symbol hidden extra) + set(page "${binary}/html/structfixture_1_1${symbol}.html") + if(case STREQUAL "extended" AND EXISTS "${page}") + message(FATAL_ERROR "Excluded symbol is present: ${symbol}") + elseif(case STREQUAL "default" AND NOT EXISTS "${page}") + message(FATAL_ERROR "Default documentation lost a public symbol: ${symbol}") + endif() + endforeach() + endif() + if(case STREQUAL "extended") + if(NOT EXISTS "${binary}/html/group__extra__group.html") + message(FATAL_ERROR "Absolute input group is missing") + endif() + endif() + if(case STREQUAL "custom") + string(FIND "${index}" "Legacy configuration" legacy) + if(legacy LESS 0) + message(FATAL_ERROR "Custom Doxyfile compatibility was lost") + endif() + endif() + endif() + message(STATUS "PASS documentation ${case}") +endforeach() + +file(REMOVE_RECURSE "${test_root}") diff --git a/tests/setup/fixtures/runtime_dlls/CMakeLists.txt b/tests/setup/fixtures/runtime_dlls/CMakeLists.txt new file mode 100644 index 0000000..7e41fb3 --- /dev/null +++ b/tests/setup/fixtures/runtime_dlls/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.24) +project(runtime-dlls LANGUAGES CXX) +include("${TOOLKIT_SOURCE}/cpp-library.cmake") +enable_testing() +add_library(runtime SHARED runtime.cpp) +set_target_properties(runtime PROPERTIES + WINDOWS_EXPORT_ALL_SYMBOLS ON + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/runtime") +add_executable(client main.cpp) +target_link_libraries(client PRIVATE runtime) +set_target_properties(client PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/client apps") +cpp_library_copy_runtime_dlls(client) +add_test(NAME runtime-consumer COMMAND client) +add_executable(static_client main.cpp runtime.cpp) +set_target_properties(static_client PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/static apps") +cpp_library_copy_runtime_dlls(static_client) +add_test(NAME static-consumer COMMAND static_client) +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/runtime-dlls.cmake" + CONTENT "set(runtime_dlls [[$]])\n") diff --git a/tests/setup/fixtures/runtime_dlls/main.cpp b/tests/setup/fixtures/runtime_dlls/main.cpp new file mode 100644 index 0000000..9babfac --- /dev/null +++ b/tests/setup/fixtures/runtime_dlls/main.cpp @@ -0,0 +1,2 @@ +int runtime_value(); +int main() { return runtime_value() == 42 ? 0 : 1; } diff --git a/tests/setup/fixtures/runtime_dlls/runtime.cpp b/tests/setup/fixtures/runtime_dlls/runtime.cpp new file mode 100644 index 0000000..7cc4f2c --- /dev/null +++ b/tests/setup/fixtures/runtime_dlls/runtime.cpp @@ -0,0 +1 @@ +int runtime_value() { return 42; } diff --git a/tests/setup/test_runtime_dlls.cmake b/tests/setup/test_runtime_dlls.cmake new file mode 100644 index 0000000..2c03a03 --- /dev/null +++ b/tests/setup/test_runtime_dlls.cmake @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: BSL-1.0 +# Run as: cmake -P tests/setup/test_runtime_dlls.cmake +cmake_minimum_required(VERSION 3.24) +get_filename_component(toolkit_source "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +string(RANDOM LENGTH 16 ALPHABET 0123456789abcdef test_id) +set(test_root "${toolkit_source}/build/runtime-dlls-${test_id}") +find_program(ctest NAMES ctest REQUIRED) + +function(run) + execute_process(COMMAND ${ARGV} RESULT_VARIABLE result + OUTPUT_VARIABLE out ERROR_VARIABLE err) + if(NOT result EQUAL 0) + message(FATAL_ERROR "Command failed (${result}): ${ARGV}\n${out}\n${err}") + endif() +endfunction() + +run("${CMAKE_COMMAND}" -S "${CMAKE_CURRENT_LIST_DIR}/fixtures/runtime_dlls" + -B "${test_root}" -G Ninja "-DTOOLKIT_SOURCE=${toolkit_source}") +run("${CMAKE_COMMAND}" --build "${test_root}") +if(WIN32) + include("${test_root}/runtime-dlls.cmake") + if(NOT runtime_dlls) + message(FATAL_ERROR "Shared runtime fixture did not produce a DLL") + endif() + foreach(dll IN LISTS runtime_dlls) + get_filename_component(filename "${dll}" NAME) + if(NOT EXISTS "${test_root}/client apps/${filename}") + message(FATAL_ERROR "Runtime DLL was not deployed: ${filename}") + endif() + endforeach() +endif() +run("${ctest}" --test-dir "${test_root}" --output-on-failure) +file(REMOVE_RECURSE "${test_root}") +message(STATUS "PASS custom shared and static executable runtime deployment") From 735e22d6b5738af837970a6fb89982cb130d9545 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 8 Oct 2026 11:48:20 -0700 Subject: [PATCH 2/4] Reject continued Doxygen option assignments Reject trailing continuation backslashes, including trailing spaces or tabs, and cover terminal and whitespace continuations through consumer fixtures. Keep quoted backslashes valid. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 3 ++- cmake/cpp-library-docs.cmake | 3 ++- tests/docs/fixtures/CMakeLists.txt | 7 +++++++ tests/docs/test_docs.cmake | 3 ++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e653ddd..d72e1bf 100644 --- a/README.md +++ b/README.md @@ -387,7 +387,8 @@ paths and paths containing spaces are supported. Missing inputs fail configurati template. Each quoted CMake argument must be a single `TAG = value` or `TAG += value` assignment, with Doxygen quoting inside the value when needed. Use spaces, not CMake semicolons, to separate Doxygen list values. The toolkit -validates assignment syntax; Doxygen validates tag names and values. Settings +validates assignment syntax and rejects line-continuation backslashes, including +those followed by whitespace; Doxygen validates tag names and values. Settings are literal, not `configure_file` templates: use `${...}` for CMake variables. ```cmake diff --git a/cmake/cpp-library-docs.cmake b/cmake/cpp-library-docs.cmake index a2595cb..3372d8f 100644 --- a/cmake/cpp-library-docs.cmake +++ b/cmake/cpp-library-docs.cmake @@ -29,7 +29,8 @@ function(_cpp_library_setup_docs) string(APPEND docs_extensions "INPUT += \"${input}\"\n") endforeach() foreach(option IN LISTS ARG_DOCS_OPTIONS) - if(NOT option MATCHES "^[A-Z][A-Z0-9_]*[ \t]*\\+?=[^\r\n]*$") + if(NOT option MATCHES "^[A-Z][A-Z0-9_]*[ \t]*\\+?=[^\r\n]*$" + OR option MATCHES "\\\\[ \t]*$") message(FATAL_ERROR "cpp-library: DOCS_OPTIONS requires single-line Doxygen assignments: ${option}") endif() diff --git a/tests/docs/fixtures/CMakeLists.txt b/tests/docs/fixtures/CMakeLists.txt index 89d485c..dfe58c8 100644 --- a/tests/docs/fixtures/CMakeLists.txt +++ b/tests/docs/fixtures/CMakeLists.txt @@ -21,12 +21,19 @@ if(CASE STREQUAL "extended") DOCS_OPTIONS "PROJECT_BRIEF = \"Customized documentation\"" "WARN_IF_UNDOCUMENTED = NO" + [=[PREDEFINED = "FIXTURE_PATH=C:\\docs\\"]=] "EXCLUDE_SYMBOLS += fixture::extra" DOCS_EXCLUDE_SYMBOLS "fixture::hidden") elseif(CASE STREQUAL "missing-input") list(APPEND docs_args DOCS_INPUTS "missing docs") elseif(CASE STREQUAL "malformed-option") list(APPEND docs_args DOCS_OPTIONS "not a Doxygen assignment") +elseif(CASE STREQUAL "malformed-terminal-continuation-option") + list(APPEND docs_args DOCS_OPTIONS [=[PREDEFINED = A= \]=]) +elseif(CASE STREQUAL "malformed-continuation-option") + list(APPEND docs_args DOCS_OPTIONS + [=[PREDEFINED = A= \ ]=] + "WARN_IF_UNDOCUMENTED = NO") elseif(CASE STREQUAL "multiline-option") list(APPEND docs_args DOCS_OPTIONS "QUIET = YES\nGENERATE_XML = YES") elseif(CASE STREQUAL "missing-input-value") diff --git a/tests/docs/test_docs.cmake b/tests/docs/test_docs.cmake index 8b115b8..6609f4e 100644 --- a/tests/docs/test_docs.cmake +++ b/tests/docs/test_docs.cmake @@ -5,7 +5,8 @@ get_filename_component(toolkit_source "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE string(RANDOM LENGTH 16 ALPHABET 0123456789abcdef test_id) set(test_root "${toolkit_source}/build/docs-${test_id}") -foreach(case default extended custom missing-input malformed-option multiline-option +foreach(case default extended custom missing-input malformed-option + malformed-terminal-continuation-option malformed-continuation-option multiline-option missing-input-value missing-option-value) set(source "${test_root}/${case}/source with spaces") set(binary "${test_root}/${case}/build with spaces") From 09dee6069307a258a394a6e3781ae4c342fa8cb9 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 8 Oct 2026 12:49:31 -0700 Subject: [PATCH 3/4] Normalize Scout configuration quoting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scout/scout.config.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.scout/scout.config.yaml b/.scout/scout.config.yaml index cdb25bb..7155365 100644 --- a/.scout/scout.config.yaml +++ b/.scout/scout.config.yaml @@ -5,12 +5,13 @@ # ignore / dampen / boost / tune overrides; defaults for # repository/markdown/ranking/index/projects are filled in at load time. ignore: - - '**/CHANGELOG.md' - - '**/CHANGELOG*' - - '**/CHANGES.md' - - '**/HISTORY.md' + - "**/CHANGELOG.md" + - "**/CHANGELOG*" + - "**/CHANGES.md" + - "**/HISTORY.md" + - .claude/worktrees/ dampen: - factor: 0.3 patterns: - - '**/package.json' + - "**/package.json" From e7494f77e699a694aaa4b95651d2b9a108ca5221 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 8 Oct 2026 12:56:52 -0700 Subject: [PATCH 4/4] Document runtime deployment module responsibilities Describe the public runtime DLL helper alongside the legacy testing wrapper in the module header and architecture guidance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CLAUDE.md | 2 +- cmake/cpp-library-testing.cmake | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1880ad9..00565fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,7 +50,7 @@ or pin to a specific commit: `CPMAddPackage("gh:stlab/cpp-library#") - **`cpp-library-dependency-provider.cmake`** — implements the CMake dependency-provider callback (`SET_DEPENDENCY_PROVIDER`) that intercepts every `find_package()`/`FetchContent_MakeAvailable()` call to record the exact package/version/components used, so `Config.cmake` can regenerate accurate `find_dependency()` calls at install time. State is stored in `GLOBAL` properties (`_CPP_LIBRARY_TRACKED_DEP_*`, `_CPP_LIBRARY_PKG_KEYS`, etc.) — there's no other persistence mechanism, so tests reset these properties between cases (see `tests/install/CMakeLists.txt`). - **`cpp-library-docs.cmake`** — Doxygen + doxygen-awesome-css target (`docs`). - **`cpp-library-ci.cmake`** — generates `.github/workflows/ci.yml` for the *consumer* project from `templates/.github/workflows/ci.yml.in`, substituting the package name. -- **`cpp-library-testing.cmake`** — thin backward-compat wrapper; real test/example executable logic lives in `_cpp_library_setup_executables()` in `cpp-library.cmake`. +- **`cpp-library-testing.cmake`** — public `cpp_library_copy_runtime_dlls()` helper and backward-compatibility `_cpp_library_setup_testing()` wrapper; test/example executable creation lives in `_cpp_library_setup_executables()` in `cpp-library.cmake`. ### Key control flow diff --git a/cmake/cpp-library-testing.cmake b/cmake/cpp-library-testing.cmake index eb7c7c2..fdc015f 100644 --- a/cmake/cpp-library-testing.cmake +++ b/cmake/cpp-library-testing.cmake @@ -1,10 +1,9 @@ # SPDX-License-Identifier: BSL-1.0 # -# cpp-library-testing.cmake - Testing setup with doctest +# cpp-library-testing.cmake - Runtime DLL deployment and testing compatibility # -# Note: Testing logic has been consolidated into the main cpp-library.cmake file -# This file is kept for backward compatibility but the actual implementation -# is now in the _cpp_library_setup_executables function. +# Provides cpp_library_copy_runtime_dlls for custom and toolkit-created executables. +# _cpp_library_setup_testing delegates to _cpp_library_setup_executables for compatibility. # Deploys runtime DLLs beside a custom target on Windows; empty lists need no copy. # Call in the directory that created the target, as required by POST_BUILD commands.