FindDoxygen: Create and clean recursive subdirectories for output formats

Teach `doxygen_add_docs()` to recursively create subdirectories for each
output format (HTML, XML, ...) and add each subdirectory to the `clean`
target.  Previously, only the HTML output directory was added to the
`clean` target.

Fixes: #27970, #22570
This commit is contained in:
James Miro
2026-08-10 10:32:14 -04:00
committed by Brad King
parent 09967a7368
commit 8fc8f564c9
4 changed files with 214 additions and 21 deletions
+65 -15
View File
@@ -1172,6 +1172,51 @@ function(doxygen_list_to_quoted_strings LIST_VARIABLE)
endif()
endfunction()
function(_doxygen_collect_output_dirs outputDirs)
# Collect user-provided output directories for known output formats
set(_doxygen_output_formats DOCBOOK HTML LATEX MAN RTF SQLITE3 XML)
set(_doxygen_output_dirs "")
foreach(_format IN LISTS _doxygen_output_formats)
set(_doxygen_generate_format "DOXYGEN_GENERATE_${_format}")
if(DEFINED "${_doxygen_generate_format}" AND ${${_doxygen_generate_format}})
set(_doxygen_format_output "DOXYGEN_${_format}_OUTPUT")
if(DEFINED _doxygen_format_output)
list(APPEND _doxygen_output_dirs "${${_doxygen_format_output}}" )
endif()
unset(_doxygen_format_output)
endif()
unset(_doxygen_generate_format)
endforeach()
set(${outputDirs} ${_doxygen_output_dirs} PARENT_SCOPE)
endfunction()
function(_doxygen_resolve_output_dirs parentDir childDirs outputDirs)
# Resolve relative and absolute paths for output directories
# parentDir: parent directory (e.g., DOXYGEN_OUTPUT_DIRECTORY -> docs)
# childDirs: child directories list (e.g., "html;foo/xml")
# outputDirs: output list of resolved directories
# (e.g., "docs/html;docs/foo/xml")
if(NOT DEFINED parentDir)
message(FATAL_ERROR "Undefined parent output directory given")
endif()
if(NOT parentDir)
message(FATAL_ERROR "Empty parent output directory given")
endif()
set(_doxygen_output_dirs "")
foreach(_dir IN LISTS childDirs)
cmake_path(IS_RELATIVE _dir _is_rel)
if(_is_rel)
cmake_path(ABSOLUTE_PATH _dir
BASE_DIRECTORY "${parentDir}"
NORMALIZE
OUTPUT_VARIABLE _doxygen_resolved_dir)
list(APPEND _doxygen_output_dirs "${_doxygen_resolved_dir}")
endif()
endforeach()
set(${outputDirs} "${_doxygen_output_dirs}" PARENT_SCOPE)
endfunction()
function(doxygen_add_docs targetName)
set(_options ALL USE_STAMP_FILE USES_TERMINAL)
set(_one_value_args WORKING_DIRECTORY COMMENT CONFIG_FILE)
@@ -1336,19 +1381,6 @@ doxygen_add_docs() for target ${targetName}")
# already set and we have not provided above
include("${CMAKE_BINARY_DIR}/CMakeDoxygenDefaults.cmake" OPTIONAL)
# Cleanup built HTMLs on "make clean"
# TODO Any other dirs?
if(DOXYGEN_GENERATE_HTML)
if(IS_ABSOLUTE "${DOXYGEN_HTML_OUTPUT}")
set(_args_clean_html_dir "${DOXYGEN_HTML_OUTPUT}")
else()
set(_args_clean_html_dir
"${DOXYGEN_OUTPUT_DIRECTORY}/${DOXYGEN_HTML_OUTPUT}")
endif()
set_property(DIRECTORY APPEND PROPERTY
ADDITIONAL_CLEAN_FILES "${_args_clean_html_dir}")
endif()
# Build up a list of files we can identify from the inputs so we can list
# them as DEPENDS and SOURCES in the custom command/target (the latter
# makes them display in IDEs). This must be done before we transform the
@@ -1470,6 +1502,19 @@ doxygen_add_docs() for target ${targetName}")
# later in the custom target's commands.
set( _original_doxygen_output_dir ${DOXYGEN_OUTPUT_DIRECTORY} )
# Collect the output directories for each DOXYGEN_${format}_OUTPUT
# if the corresponding DOXYGEN_GENERATE_${format} is YES
_doxygen_collect_output_dirs(_doxygen_output_dirs)
# Resolve the output directories as absolute paths
# that can be given to make_directory() and appended to
# the ADDITIONAL_CLEAN_FILES target property
_doxygen_resolve_output_dirs("${_original_doxygen_output_dir}"
"${_doxygen_output_dirs}"
_doxygen_resolved_output_dirs)
unset(_doxygen_output_dirs)
foreach(_item IN LISTS _doxygen_quoted_options)
doxygen_quote_value(DOXYGEN_${_item})
endforeach()
@@ -1503,7 +1548,7 @@ doxygen_add_docs() for target ${targetName}")
add_custom_command(
VERBATIM
OUTPUT ${__stamp_file}
COMMAND ${CMAKE_COMMAND} -E make_directory ${_original_doxygen_output_dir}
COMMAND ${CMAKE_COMMAND} -E make_directory ${_doxygen_resolved_output_dirs}
COMMAND "${DOXYGEN_EXECUTABLE}" "${_target_doxyfile}"
COMMAND ${CMAKE_COMMAND} -E touch ${__stamp_file}
WORKING_DIRECTORY "${_args_WORKING_DIRECTORY}"
@@ -1522,7 +1567,7 @@ doxygen_add_docs() for target ${targetName}")
add_custom_target( ${targetName}
${_all}
VERBATIM
COMMAND ${CMAKE_COMMAND} -E make_directory ${_original_doxygen_output_dir}
COMMAND ${CMAKE_COMMAND} -E make_directory ${_doxygen_resolved_output_dirs}
COMMAND "${DOXYGEN_EXECUTABLE}" "${_target_doxyfile}"
WORKING_DIRECTORY "${_args_WORKING_DIRECTORY}"
DEPENDS "${_target_doxyfile}" ${_sources}
@@ -1532,4 +1577,9 @@ doxygen_add_docs() for target ${targetName}")
)
endif()
# Add the DOXYGEN_${format}_OUTPUT directories as additional clean files
# to the Doxygen target (rather than as a directory property)
set_property(TARGET ${targetName} APPEND PROPERTY
ADDITIONAL_CLEAN_FILES "${_doxygen_resolved_output_dirs}")
endfunction()
+16 -6
View File
@@ -1,5 +1,5 @@
add_test(NAME FindDoxygen.SimpleTest COMMAND
${CMAKE_CTEST_COMMAND} -C $<CONFIGURATION>
${CMAKE_CTEST_COMMAND} -C $<CONFIG>
--build-and-test
"${CMake_SOURCE_DIR}/Tests/FindDoxygen/SimpleTest"
"${CMake_BINARY_DIR}/Tests/FindDoxygen/SimpleTest"
@@ -9,7 +9,7 @@ add_test(NAME FindDoxygen.SimpleTest COMMAND
)
add_test(NAME FindDoxygen.QuotingTest COMMAND
${CMAKE_CTEST_COMMAND} -C $<CONFIGURATION>
${CMAKE_CTEST_COMMAND} -C $<CONFIG>
--build-and-test
"${CMake_SOURCE_DIR}/Tests/FindDoxygen/QuotingTest"
"${CMake_BINARY_DIR}/Tests/FindDoxygen/QuotingTest"
@@ -19,17 +19,27 @@ add_test(NAME FindDoxygen.QuotingTest COMMAND
)
add_test(NAME FindDoxygen.AllTarget COMMAND
${CMAKE_CTEST_COMMAND} -C $<CONFIGURATION>
${CMAKE_CTEST_COMMAND} -C $<CONFIG>
--build-and-test
"${CMake_SOURCE_DIR}/Tests/FindDoxygen/AllTarget"
"${CMake_BINARY_DIR}/Tests/FindDoxygen/AllTarget"
${build_generator_args}
--build-options ${build_options}
--test-command ${CMAKE_CTEST_COMMAND} -C $<CONFIGURATION>
--test-command ${CMAKE_CTEST_COMMAND} -C $<CONFIG>
)
add_test(NAME FindDoxygen.CleanTarget COMMAND
${CMAKE_CTEST_COMMAND} -C $<CONFIG>
--build-and-test
"${CMake_SOURCE_DIR}/Tests/FindDoxygen/CleanTarget"
"${CMake_BINARY_DIR}/Tests/FindDoxygen/CleanTarget"
${build_generator_args}
--build-options ${build_options}
--test-command ${CMAKE_CTEST_COMMAND} -C $<CONFIG>
)
add_test(NAME FindDoxygen.StampFile COMMAND
${CMAKE_CTEST_COMMAND} -C $<CONFIGURATION>
${CMAKE_CTEST_COMMAND} -C $<CONFIG>
--build-and-test
"${CMake_SOURCE_DIR}/Tests/FindDoxygen/StampFile"
"${CMake_BINARY_DIR}/Tests/FindDoxygen/StampFile"
@@ -40,7 +50,7 @@ add_test(NAME FindDoxygen.StampFile COMMAND
if(CMake_TEST_FindDoxygen_Dot)
add_test(NAME FindDoxygen.DotComponentTest COMMAND
${CMAKE_CTEST_COMMAND} -C $<CONFIGURATION>
${CMAKE_CTEST_COMMAND} -C $<CONFIG>
--build-and-test
"${CMake_SOURCE_DIR}/Tests/FindDoxygen/DotComponentTestTest"
"${CMake_BINARY_DIR}/Tests/FindDoxygen/DotComponentTestTest"
@@ -0,0 +1,85 @@
cmake_minimum_required(VERSION 3.10)
project(TestFindDoxygen VERSION 1.0 LANGUAGES NONE)
enable_testing()
#[[
The ADDITIONAL_CLEAN_FILES property "only works for the Ninja
and the Makefile generators. It is ignored by other generators."
Therefore, we must skip this test for other generators.
https://cmake.org/cmake/help/latest/prop_tgt/ADDITIONAL_CLEAN_FILES.html#prop_tgt:ADDITIONAL_CLEAN_FILES
]]
set(DISABLE_THIS_TEST TRUE)
if(CMAKE_GENERATOR MATCHES "Ninja|Makefile")
set(DISABLE_THIS_TEST FALSE)
endif()
# Skip calling Doxygen if the generator does not support the 'clean' target
if(NOT DISABLE_THIS_TEST)
find_package(Doxygen REQUIRED)
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
[[/**
* \file
* \brief One C++ file w/ sample Doxygen comment just to produce any docs...
*/
]])
set(OUTPUT_FORMATS DOCBOOK HTML LATEX MAN RTF SQLITE3 XML)
# test one target with direct parent/child directories
set(DOXYGEN_OUTPUT_DIRECTORY "docs dir with spaces")
set(DOXYGEN_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/${DOXYGEN_OUTPUT_DIRECTORY}")
file(REMOVE_RECURSE "${DOXYGEN_OUTPUT_DIR}")
foreach(format IN LISTS OUTPUT_FORMATS)
set(DOXYGEN_GENERATE_${format} YES)
set(DOXYGEN_${format}_OUTPUT "my ${format} output")
cmake_path(ABSOLUTE_PATH DOXYGEN_${format}_OUTPUT
BASE_DIRECTORY "${DOXYGEN_OUTPUT_DIR}"
NORMALIZE
OUTPUT_VARIABLE EXPECTED_${format}_DIR)
list(APPEND DIRS_TO_CLEAN "${EXPECTED_${format}_DIR}")
endforeach()
doxygen_add_docs(docsClean ALL ${CMAKE_CURRENT_BINARY_DIR}/main.cpp)
# test another target with relative paths
set(DOXYGEN_OUTPUT_DIRECTORY "docs dir/with/subdirs")
set(DOXYGEN_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/${DOXYGEN_OUTPUT_DIRECTORY}")
file(REMOVE_RECURSE "${DOXYGEN_OUTPUT_DIR}")
SET(DOXYGEN_DOCBOOK_OUTPUT "docbook")
SET(DOXYGEN_HTML_OUTPUT "make/this/dir/html")
SET(DOXYGEN_LATEX_OUTPUT "../latex")
SET(DOXYGEN_MAN_OUTPUT "../r/t/f/m")
SET(DOXYGEN_RTF_OUTPUT "../../r t f")
SET(DOXYGEN_SQLITE3_OUTPUT "../../../sqlite3")
SET(DOXYGEN_XML_OUTPUT "./../xml")
foreach(format IN LISTS OUTPUT_FORMATS)
set(_doxygen_format_output "DOXYGEN_${format}_OUTPUT")
cmake_path(ABSOLUTE_PATH DOXYGEN_${format}_OUTPUT
BASE_DIRECTORY "${DOXYGEN_OUTPUT_DIR}"
NORMALIZE
OUTPUT_VARIABLE EXPECTED_${format}_DIR)
list(APPEND DIRS_TO_CLEAN "${EXPECTED_${format}_DIR}")
endforeach()
doxygen_add_docs(docsCleanRelative ALL ${CMAKE_CURRENT_BINARY_DIR}/main.cpp)
endif()
add_test(NAME testDocsClean
COMMAND ${CMAKE_COMMAND}
-D BUILD_DIR=${CMAKE_CURRENT_BINARY_DIR}
-D "OUTPUT_DIRS=${DIRS_TO_CLEAN}"
-P "${CMAKE_CURRENT_SOURCE_DIR}/TestClean.cmake"
)
set_tests_properties(testDocsClean PROPERTIES DISABLED ${DISABLE_THIS_TEST})
@@ -0,0 +1,48 @@
message(STATUS "Checking whether Doxygen wrote "
"output to expected directories...")
foreach(DIR IN LISTS OUTPUT_DIRS)
file(GLOB DIR_CONTENTS "${DIR}/*")
list(LENGTH DIR_CONTENTS DIR_SIZE)
if(DIR_SIZE EQUAL 0)
message(FATAL_ERROR "Target directory \"${DIR}\" is empty. "
"Doxygen failed to generate output.")
else()
message(STATUS "Target directory \"${DIR}\" is not empty. "
"Doxygen generated output.")
endif()
endforeach()
message(STATUS "Building 'clean' target...")
execute_process(
COMMAND ${CMAKE_COMMAND} --build ${BUILD_DIR} --target clean
RESULT_VARIABLE CLEAN_RESULT
ERROR_QUIET
)
if(NOT CLEAN_RESULT EQUAL 0)
message(FATAL_ERROR "The 'clean' target failed to build")
endif()
message(STATUS "Successfully built 'clean' target")
message(STATUS "Checking whether the 'clean' target "
"successfully cleaned the Doxygen output...")
foreach(DIR IN LISTS OUTPUT_DIRS)
file(GLOB DIR_CONTENTS "${DIR}/*")
list(LENGTH DIR_CONTENTS DIR_SIZE)
if(DIR_SIZE EQUAL 0)
message(STATUS "Target directory \"${DIR}\" is empty. "
"The 'clean' target succeeded.")
else()
message(FATAL_ERROR "Target directory \"${DIR}\" is not empty. "
"The 'clean' target failed.")
endif()
endforeach()