# This find module originally created by Jeroen Koekkoek. # Copyright (c) 2023, Jeroen Koekkoek # # This file is covered by a BSD 3-Clause License. # See https://github.com/k0ekk0ek/cmake-sphinx/blob/master/LICENSE for details. include(FindPackageHandleStandardArgs) macro(_Sphinx_find_executable _exe) string(TOUPPER "${_exe}" _uc) # sphinx-(build|quickstart)-3 x.x.x # FIXME: This works on Fedora (and probably most other UNIX like targets). # Windows targets and PIP installs might need some work. find_program( SPHINX_${_uc}_EXECUTABLE NAMES "sphinx-${_exe}-3" "sphinx-${_exe}" "sphinx-${_exe}.exe") mark_as_advanced(SPHINX_${_uc}_EXECUTABLE) if(SPHINX_${_uc}_EXECUTABLE) execute_process( COMMAND "${SPHINX_${_uc}_EXECUTABLE}" --version RESULT_VARIABLE _result OUTPUT_VARIABLE _output OUTPUT_STRIP_TRAILING_WHITESPACE) if(_result EQUAL 0 AND _output MATCHES " v?([0-9]+\\.[0-9]+\\.[0-9]+)$") set(SPHINX_${_uc}_VERSION "${CMAKE_MATCH_1}") endif() if(NOT TARGET Sphinx::${_exe}) add_executable(Sphinx::${_exe} IMPORTED GLOBAL) set_target_properties(Sphinx::${_exe} PROPERTIES IMPORTED_LOCATION "${SPHINX_${_uc}_EXECUTABLE}") endif() set(Sphinx_${_exe}_FOUND TRUE) else() set(Sphinx_${_exe}_FOUND FALSE) endif() unset(_uc) endmacro() macro(_Sphinx_find_module _name _module) string(TOUPPER "${_name}" _Sphinx_uc) if(SPHINX_PYTHON_EXECUTABLE) execute_process( COMMAND ${SPHINX_PYTHON_EXECUTABLE} -m ${_module} --version RESULT_VARIABLE _result OUTPUT_VARIABLE _output OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) if(_result EQUAL 0) if(_output MATCHES " v?([0-9]+\\.[0-9]+\\.[0-9]+)$") set(SPHINX_${_Sphinx_uc}_VERSION "${CMAKE_MATCH_1}") endif() if(NOT TARGET Sphinx::${_name}) set(SPHINX_${_Sphinx_uc}_EXECUTABLE "${SPHINX_PYTHON_EXECUTABLE} -m ${_module}") add_executable(Sphinx::${_name} IMPORTED GLOBAL) set_target_properties(Sphinx::${_name} PROPERTIES IMPORTED_LOCATION "${SPHINX_PYTHON_EXECUTABLE}") endif() set(Sphinx_${_name}_ARGS -m ${_module}) set(Sphinx_${_name}_FOUND TRUE) else() set(Sphinx_${_name}_FOUND FALSE) endif() else() set(Sphinx_${_name}_FOUND FALSE) endif() unset(_Sphinx_uc) endmacro() macro(_Sphinx_find_extension _ext) if(SPHINX_PYTHON_EXECUTABLE) execute_process( COMMAND ${SPHINX_PYTHON_EXECUTABLE} -c "import ${_ext}" RESULT_VARIABLE _result) if(_result EQUAL 0) set(Sphinx_${_ext}_FOUND TRUE) else() set(Sphinx_${_ext}_FOUND FALSE) endif() endif() endmacro() # # Find sphinx-build and sphinx-quickstart. # # Find sphinx-build shim. _Sphinx_find_executable(build) if(SPHINX_BUILD_EXECUTABLE) # Find sphinx-quickstart shim. _Sphinx_find_executable(quickstart) # Locate Python executable if(CMAKE_HOST_WIN32) # script-build on Windows located under (when PIP is used): # C:/Program Files/PythonXX/Scripts # C:/Users/username/AppData/Roaming/Python/PythonXX/Sripts # # Python modules are installed under: # C:/Program Files/PythonXX/Lib # C:/Users/username/AppData/Roaming/Python/PythonXX/site-packages # # To verify a given module is installed, use the Python base directory # and test if either Lib/module.py or site-packages/module.py exists. get_filename_component(_Sphinx_directory "${SPHINX_BUILD_EXECUTABLE}" DIRECTORY) get_filename_component(_Sphinx_directory "${_Sphinx_directory}" DIRECTORY) if(EXISTS "${_Sphinx_directory}/python.exe") set(SPHINX_PYTHON_EXECUTABLE "${_Sphinx_directory}/python.exe") endif() unset(_Sphinx_directory) else() file(READ "${SPHINX_BUILD_EXECUTABLE}" _Sphinx_script) if(_Sphinx_script MATCHES "^#!([^\n]+)") string(STRIP "${CMAKE_MATCH_1}" _Sphinx_shebang) if(EXISTS "${_Sphinx_shebang}") set(SPHINX_PYTHON_EXECUTABLE "${_Sphinx_shebang}") endif() endif() unset(_Sphinx_script) unset(_Sphinx_shebang) endif() endif() if(NOT SPHINX_PYTHON_EXECUTABLE) # Python executable cannot be extracted from shim shebang or path if e.g. # virtual environments are used, fallback to find package. Assume the # correct installation is found, the setup is probably broken in more ways # than one otherwise. find_package(Python3 QUIET COMPONENTS Interpreter) if(TARGET Python3::Interpreter) set(SPHINX_PYTHON_EXECUTABLE ${Python3_EXECUTABLE}) # Revert to "python -m sphinx" if shim cannot be found. if(NOT SPHINX_BUILD_EXECUTABLE) _Sphinx_find_module(build sphinx) _Sphinx_find_module(quickstart sphinx.cmd.quickstart) endif() endif() endif() # # Verify components are available. # if(SPHINX_BUILD_VERSION) # Breathe is required for Exhale if("exhale" IN_LIST Sphinx_FIND_COMPONENTS AND NOT "breathe" IN_LIST Sphinx_FIND_COMPONENTS) list(APPEND Sphinx_FIND_COMPONENTS "breathe") endif() foreach(_Sphinx_component IN LISTS Sphinx_FIND_COMPONENTS) if(_Sphinx_component STREQUAL "build") # Do nothing, sphinx-build is always required. continue() elseif(_Sphinx_component STREQUAL "quickstart") # Do nothing, sphinx-quickstart is optional, but looked up by default. continue() endif() _Sphinx_find_extension(${_Sphinx_component}) endforeach() unset(_Sphinx_component) # # Verify both executables are part of the Sphinx distribution. # if(SPHINX_QUICKSTART_VERSION AND NOT SPHINX_BUILD_VERSION STREQUAL SPHINX_QUICKSTART_VERSION) message(FATAL_ERROR "Versions for sphinx-build (${SPHINX_BUILD_VERSION}) " "and sphinx-quickstart (${SPHINX_QUICKSTART_VERSION}) " "do not match") endif() endif() find_package_handle_standard_args( Sphinx VERSION_VAR SPHINX_BUILD_VERSION REQUIRED_VARS SPHINX_BUILD_EXECUTABLE SPHINX_BUILD_VERSION HANDLE_COMPONENTS) # Generate a conf.py template file using sphinx-quickstart. # # sphinx-quickstart allows for quiet operation and a lot of settings can be # specified as command line arguments, therefore its not required to parse the # generated conf.py. function(_Sphinx_generate_confpy _target _cachedir) if(NOT TARGET Sphinx::quickstart) message(FATAL_ERROR "sphinx-quickstart is not available, needed by" "sphinx_add_docs for target ${_target}") endif() if(NOT DEFINED SPHINX_PROJECT) set(SPHINX_PROJECT ${PROJECT_NAME}) endif() if(NOT DEFINED SPHINX_AUTHOR) set(SPHINX_AUTHOR "${SPHINX_PROJECT} committers") endif() if(NOT DEFINED SPHINX_COPYRIGHT) string(TIMESTAMP "%Y, ${SPHINX_AUTHOR}" SPHINX_COPYRIGHT) endif() if(NOT DEFINED SPHINX_VERSION) set(SPHINX_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") endif() if(NOT DEFINED SPHINX_RELEASE) set(SPHINX_RELEASE "${PROJECT_VERSION}") endif() if(NOT DEFINED SPHINX_LANGUAGE) set(SPHINX_LANGUAGE "en") endif() if(NOT DEFINED SPHINX_MASTER) set(SPHINX_MASTER "index") endif() set(_known_exts autodoc doctest intersphinx todo coverage imgmath mathjax ifconfig viewcode githubpages) if(DEFINED SPHINX_EXTENSIONS) foreach(_ext ${SPHINX_EXTENSIONS}) set(_is_known_ext FALSE) foreach(_known_ext ${_known_exsts}) if(_ext STREQUAL _known_ext) set(_opts "${opts} --ext-${_ext}") set(_is_known_ext TRUE) break() endif() endforeach() if(NOT _is_known_ext) if(_exts) set(_exts "${_exts},${_ext}") else() set(_exts "${_ext}") endif() endif() endforeach() endif() if(_exts) set(_exts "--extensions=${_exts}") endif() set(_templatedir "${CMAKE_CURRENT_BINARY_DIR}/${_target}.template") file(MAKE_DIRECTORY "${_templatedir}") string(REPLACE " " ";" _Sphinx_executable ${SPHINX_QUICKSTART_EXECUTABLE}) execute_process( COMMAND ${_Sphinx_executable} -q --no-makefile --no-batchfile -p "${SPHINX_PROJECT}" -a "${SPHINX_AUTHOR}" -v "${SPHINX_VERSION}" -r "${SPHINX_RELEASE}" -l "${SPHINX_LANGUAGE}" --master "${SPHINX_MASTER}" ${_opts} ${_exts} "${_templatedir}" RESULT_VARIABLE _result OUTPUT_QUIET) unset(_Sphinx_executable) if(_result EQUAL 0 AND EXISTS "${_templatedir}/conf.py") file(COPY "${_templatedir}/conf.py" DESTINATION "${_cachedir}") endif() file(REMOVE_RECURSE "${_templatedir}") if(NOT _result EQUAL 0 OR NOT EXISTS "${_cachedir}/conf.py") message(FATAL_ERROR "Sphinx configuration file not generated for " "target ${_target}") endif() endfunction() function(sphinx_add_docs _target) set(_opts) set(_single_opts BUILDER OUTPUT_DIRECTORY SOURCE_DIRECTORY) set(_multi_opts BREATHE_PROJECTS) cmake_parse_arguments(_args "${_opts}" "${_single_opts}" "${_multi_opts}" ${ARGN}) unset(SPHINX_BREATHE_PROJECTS) if(NOT _args_BUILDER) message(FATAL_ERROR "Sphinx builder not specified for target ${_target}") elseif(NOT _args_SOURCE_DIRECTORY) message(FATAL_ERROR "Sphinx source directory not specified for target ${_target}") else() if(NOT IS_ABSOLUTE "${_args_SOURCE_DIRECTORY}") get_filename_component(_sourcedir "${_args_SOURCE_DIRECTORY}" ABSOLUTE) else() set(_sourcedir "${_args_SOURCE_DIRECTORY}") endif() if(NOT IS_DIRECTORY "${_sourcedir}") message(FATAL_ERROR "Sphinx source directory '${_sourcedir}' for" "target ${_target} does not exist") endif() endif() set(_builder "${_args_BUILDER}") if(_args_OUTPUT_DIRECTORY) set(_outputdir "${_args_OUTPUT_DIRECTORY}") else() set(_outputdir "${CMAKE_CURRENT_BINARY_DIR}/${_target}") endif() if(_args_BREATHE_PROJECTS) if(NOT Sphinx_breathe_FOUND) message(FATAL_ERROR "Sphinx extension 'breathe' is not available. Needed" "by sphinx_add_docs for target ${_target}") endif() list(APPEND SPHINX_EXTENSIONS breathe) foreach(_doxygen_target ${_args_BREATHE_PROJECTS}) if(TARGET ${_doxygen_target}) list(APPEND _depends ${_doxygen_target}) # Doxygen targets are supported. Verify that a Doxyfile exists. get_target_property(_dir ${_doxygen_target} BINARY_DIR) set(_doxyfile "${_dir}/Doxyfile.${_doxygen_target}") if(NOT EXISTS "${_doxyfile}") message(FATAL_ERROR "Target ${_doxygen_target} is not a Doxygen" "target, needed by sphinx_add_docs for target" "${_target}") endif() # Read the Doxyfile, verify XML generation is enabled and retrieve the # output directory. file(READ "${_doxyfile}" _contents) if(NOT _contents MATCHES "GENERATE_XML *= *YES") message(FATAL_ERROR "Doxygen target ${_doxygen_target} does not" "generate XML, needed by sphinx_add_docs for" "target ${_target}") elseif(_contents MATCHES "OUTPUT_DIRECTORY *= *([^ ][^\n]*)") string(STRIP "${CMAKE_MATCH_1}" _dir) set(_name "${_doxygen_target}") set(_dir "${_dir}/xml") else() message(FATAL_ERROR "Cannot parse Doxyfile generated by Doxygen" "target ${_doxygen_target}, needed by" "sphinx_add_docs for target ${_target}") endif() elseif(_doxygen_target MATCHES "([^: ]+) *: *(.*)") set(_name "${CMAKE_MATCH_1}") string(STRIP "${CMAKE_MATCH_2}" _dir) endif() if(_name AND _dir) if(_breathe_projects) set(_breathe_projects "${_breathe_projects}, \"${_name}\": \"${_dir}\"") else() set(_breathe_projects "\"${_name}\": \"${_dir}\"") endif() if(NOT _breathe_default_project) set(_breathe_default_project "${_name}") endif() endif() endforeach() endif() set(_cachedir "${CMAKE_CURRENT_BINARY_DIR}/${_target}.cache") file(MAKE_DIRECTORY "${_cachedir}") if(EXISTS "${_sourcedir}/_static") file(COPY "${_sourcedir}/_static" DESTINATION "${_cachedir}") endif() if(EXISTS "${_sourcedir}/_templates") file(COPY "${_sourcedir}/_templates" DESTINATION "${_cachedir}") endif() if(EXISTS "${_sourcedir}/conf.py") configure_file("${_sourcedir}/conf.py" "${_cachedir}/conf.py" @ONLY) else() _Sphinx_generate_confpy(${_target} "${_cachedir}") endif() if(_breathe_projects) file(APPEND "${_cachedir}/conf.py" "\nbreathe_projects = { ${_breathe_projects} }" "\nbreathe_default_project = '${_breathe_default_project}'") endif() string(REPLACE " " ";" _Sphinx_executable ${SPHINX_BUILD_EXECUTABLE}) add_custom_target( ${_target} ALL COMMAND ${_Sphinx_executable} -b ${_builder} -d "${CMAKE_CURRENT_BINARY_DIR}/${_target}.cache/_doctrees" -c "${CMAKE_CURRENT_BINARY_DIR}/${_target}.cache" "${_sourcedir}" "${_outputdir}" COMMENT "Building ${_target} Sphinx document" DEPENDS ${_depends}) unset(_Sphinx_executable) endfunction()