From 874407e589201e9bdcfe76a0bd3180cfe3ef5b91 Mon Sep 17 00:00:00 2001 From: Jeremy Retailleau Date: Mon, 17 Nov 2025 15:13:44 -0800 Subject: [PATCH] Add env support to Sphinx docs target --- cmake/FindSphinx.cmake | 91 ++++++++++++++++++-- doc/api_reference.rst | 47 +++++++++++ doc/environment_variables.rst | 28 ++++++ doc/index.rst | 1 + doc/release/release_notes.rst | 7 ++ doc/tutorial.rst | 103 +++++++++++++++++++---- test/08-prepend-paths/CMakeLists.txt | 22 +++++ test/08-prepend-paths/conf.py | 11 +++ test/08-prepend-paths/index.html | 18 ++++ test/08-prepend-paths/index.rst | 2 + test/09-environment/CMakeLists.txt | 13 +++ test/09-environment/conf.py | 11 +++ test/09-environment/index.html | 18 ++++ test/09-environment/index.rst | 6 ++ test/CMakeLists.txt | 2 + test/resources/_extensions/sphinx_env.py | 47 +++++++++++ 16 files changed, 406 insertions(+), 21 deletions(-) create mode 100644 doc/environment_variables.rst create mode 100644 test/08-prepend-paths/CMakeLists.txt create mode 100644 test/08-prepend-paths/conf.py create mode 100644 test/08-prepend-paths/index.html create mode 100644 test/08-prepend-paths/index.rst create mode 100644 test/09-environment/CMakeLists.txt create mode 100644 test/09-environment/conf.py create mode 100644 test/09-environment/index.html create mode 100644 test/09-environment/index.rst create mode 100644 test/resources/_extensions/sphinx_env.py diff --git a/cmake/FindSphinx.cmake b/cmake/FindSphinx.cmake index cd383b0..58a2d56 100644 --- a/cmake/FindSphinx.cmake +++ b/cmake/FindSphinx.cmake @@ -50,12 +50,39 @@ if (Sphinx_FOUND AND NOT TARGET Sphinx::Build) PROPERTIES IMPORTED_LOCATION "${SPHINX_EXECUTABLE}") + # Helper function to register a Sphinx documentation target. function(sphinx_add_docs NAME) + set(_BOOL_ARGS + ALL + SHOW_TRACEBACK + WRITE_ALL + FRESH_ENV + ISOLATED + ) + + set(_SINGLE_VALUE_ARGS + COMMENT + BUILDER + CONFIG_DIRECTORY + SOURCE_DIRECTORY + OUTPUT_DIRECTORY + WORKING_DIRECTORY + ) + + set(_MULTI_VALUE_ARGS + DEFINE + DEPENDS + LIBRARY_PATH_PREPEND + PYTHON_PATH_PREPEND + ENVIRONMENT + ) + cmake_parse_arguments( PARSE_ARGV 1 "" - "ALL;SHOW_TRACEBACK;WRITE_ALL;FRESH_ENV;ISOLATED" - "COMMENT;BUILDER;CONFIG_DIRECTORY;SOURCE_DIRECTORY;OUTPUT_DIRECTORY" - "DEFINE;DEPENDS") + "${_BOOL_ARGS}" + "${_SINGLE_VALUE_ARGS}" + "${_MULTI_VALUE_ARGS}" + ) # Ensure that target should be added to the default build target, # if required. @@ -65,6 +92,61 @@ if (Sphinx_FOUND AND NOT TARGET Sphinx::Build) set(_ALL "") endif() + # Set platform-specific library path environment variable. + if (CMAKE_SYSTEM_NAME STREQUAL Windows) + set(LIBRARY_ENV_NAME PATH) + elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin) + set(LIBRARY_ENV_NAME DYLD_LIBRARY_PATH) + else() + set(LIBRARY_ENV_NAME LD_LIBRARY_PATH) + endif() + + # Convert paths to CMake-friendly format. + if(DEFINED ENV{${LIBRARY_ENV_NAME}}) + cmake_path(CONVERT "$ENV{${LIBRARY_ENV_NAME}}" TO_CMAKE_PATH_LIST LIBRARY_PATH) + else() + set(LIBRARY_PATH "") + endif() + if(DEFINED ENV{PYTHONPATH}) + cmake_path(CONVERT "$ENV{PYTHONPATH}" TO_CMAKE_PATH_LIST PYTHON_PATH) + else() + set(PYTHON_PATH "") + endif() + + # Prepend specified paths to the library and Python paths. + if (_LIBRARY_PATH_PREPEND) + list(PREPEND LIBRARY_PATH ${_LIBRARY_PATH_PREPEND}) + endif() + + if (_PYTHON_PATH_PREPEND) + list(PREPEND PYTHON_PATH ${_PYTHON_PATH_PREPEND}) + endif() + + # Build environment arguments for cmake -E env. + set(_env_args "") + + if (LIBRARY_PATH) + if (CMAKE_SYSTEM_NAME STREQUAL Windows) + list(JOIN LIBRARY_PATH "\\;" _LIBRARY_PATH_STRING) + else() + list(JOIN LIBRARY_PATH ":" _LIBRARY_PATH_STRING) + endif() + list(APPEND _env_args "${LIBRARY_ENV_NAME}=${_LIBRARY_PATH_STRING}") + endif() + + if (PYTHON_PATH) + if (CMAKE_SYSTEM_NAME STREQUAL Windows) + list(JOIN PYTHON_PATH "\\;" _PYTHON_PATH_STRING) + else() + list(JOIN PYTHON_PATH ":" _PYTHON_PATH_STRING) + endif() + list(APPEND _env_args "PYTHONPATH=${_PYTHON_PATH_STRING}") + endif() + + foreach(_env ${_ENVIRONMENT}) + list(APPEND _env_args "${_env}") + endforeach() + # Default working directory to current source path if none is provided. if (NOT _WORKING_DIRECTORY) set(_WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) @@ -128,7 +210,6 @@ if (Sphinx_FOUND AND NOT TARGET Sphinx::Build) COMMENT ${_COMMENT} DEPENDS ${_DEPENDS} COMMAND ${CMAKE_COMMAND} -E make_directory ${_OUTPUT_DIRECTORY} - COMMAND Sphinx::Build ${_args} - COMMAND_EXPAND_LISTS) + COMMAND ${CMAKE_COMMAND} -E env ${_env_args} "${SPHINX_EXECUTABLE}" ${_args}) endfunction() endif() diff --git a/doc/api_reference.rst b/doc/api_reference.rst index c598b57..2ad3a04 100644 --- a/doc/api_reference.rst +++ b/doc/api_reference.rst @@ -21,6 +21,9 @@ API Reference [OUTPUT_DIRECTORY dir] [DEFINE setting1=value1 setting2=value2...] [DEPENDS target1 target2...] + [LIBRARY_PATH_PREPEND path1 path2...] + [PYTHON_PATH_PREPEND path1 path2...] + [ENVIRONMENT env1 env2...] [SHOW_TRACEBACK] [WRITE_ALL] [FRESH_ENV] @@ -120,6 +123,50 @@ API Reference DEPENDS lib1 lib2 ) + * ``LIBRARY_PATH_PREPEND`` + + List of library paths to prepend to the corresponding environment + variable (:envvar:`LD_LIBRARY_PATH` on Linux, + :envvar:`DYLD_LIBRARY_PATH` on macOS, and :envvar:`PATH` on Windows) + when building the documentation. Each path can be defined literally or + as a CMake expression generator for convenience:: + + sphinx_add_docs( + ... + LIBRARY_PATH_PREPEND + $ + $ + /path/to/libs/ + ) + + * ``PYTHON_PATH_PREPEND`` + + List of Python paths to prepend to the :envvar:`PYTHONPATH` environment + variable when building the documentation. Each path can be defined + literally or as a CMake expression generator for convenience:: + + sphinx_add_docs( + ... + PYTHON_PATH_PREPEND + $ + $ + /path/to/python/ + ) + + * ``ENVIRONMENT`` + + List of custom environment variables with associated values to set when + building the documentation:: + + sphinx_add_docs( + ... + ENVIRONMENT + "ENV_VAR1=VALUE1" + "ENV_VAR2=VALUE2" + "ENV_VAR3=VALUE3" + ) + + * ``SHOW_TRACEBACK`` Display the full traceback when an unhandled exception occurs. diff --git a/doc/environment_variables.rst b/doc/environment_variables.rst new file mode 100644 index 0000000..47b37d9 --- /dev/null +++ b/doc/environment_variables.rst @@ -0,0 +1,28 @@ +.. _environment_variables: + +********************* +Environment variables +********************* + +Environment variables directly defined or referenced by this package. + +.. envvar:: CMAKE_PREFIX_PATH + + Environment variable (or :term:`CMake` option) used to locate directory + to look for configurations. + + .. seealso:: https://cmake.org/cmake/help/latest/envvar/CMAKE_PREFIX_PATH.html + +.. envvar:: LD_LIBRARY_PATH + + Environment variable used on Linux/UNIX System to locate shared libraries. + +.. envvar:: DYLD_LIBRARY_PATH + + Environment variable used on macOS System to locate shared libraries. + +.. envvar:: PATH + + Environment variable used to specifies the directories to be searched to + find a command. On Windows system, this environment variable is also used + to locate shared libraries. diff --git a/doc/index.rst b/doc/index.rst index f80210f..45bf8b1 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -14,6 +14,7 @@ Generate documentation for :term:`Sphinx` with :term:`CMake`. integration tutorial api_reference + environment_variables release/index Source Code @ GitHub glossary diff --git a/doc/release/release_notes.rst b/doc/release/release_notes.rst index 0d9ef39..1a0e054 100644 --- a/doc/release/release_notes.rst +++ b/doc/release/release_notes.rst @@ -4,6 +4,13 @@ Release Notes ************* +.. release:: Upcoming + + .. change:: new + + Added support for prepending library and Python paths, and for passing + arbitrary environment variables to Sphinx builds. + .. release:: 1.0.1 :date: 2025-08-14 diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 7b3ea06..d9727b3 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -4,8 +4,8 @@ Tutorial ******** -Once :ref:`integrated in your project `, the ``Sphinx::Build`` -target and the :func:`sphinx_add_docs` function are available for using. +Once :ref:`integrated into your project `, the ``Sphinx::Build`` +target and the :func:`sphinx_add_docs` function are available for use. .. _tutorial/target: @@ -14,8 +14,9 @@ Using the target Let's consider a project that includes a :file:`doc` folder containing :term:`Sphinx` documentation. We need to add a :file:`CMakeLists.txt` -configuration file to add Python tests within the same directory. The -"sphinx-build" command can be easily implemented using a custom target: +configuration file to build the documentation as part of the project. + +The ``sphinx-build`` command can be implemented directly using a custom target: .. code-block:: cmake @@ -26,29 +27,61 @@ configuration file to add Python tests within the same directory. The ) Building the project will generate an HTML version of the documentation within -the build folder, making it ready for installation. +the build directory, making it ready for installation or deployment. + +In some cases, the documentation build may depend on Python modules or shared +libraries produced by the project itself. These must be made available through +the build environment. + +This can be achieved by explicitly defining environment variables on the target, +for example: + +.. code-block:: cmake + + set_target_properties(doc PROPERTIES + ENVIRONMENT + PYTHONPATH=$:$ENV{PYTHONPATH} + ) + +Similarly, shared libraries required at runtime must be discoverable through the +platform-specific library search path. + +.. code-block:: cmake + + set_target_properties(doc PROPERTIES + APPEND ENVIRONMENT + LD_LIBRARY_PATH=$:$ENV{LD_LIBRARY_PATH} + ) + +.. warning:: + + The environment variable used to locate shared libraries depends on the + platform. :envvar:`LD_LIBRARY_PATH` is used on Linux, + :envvar:`DYLD_LIBRARY_PATH` on macOS, and :envvar:`PATH` on Windows. .. _tutorial/function: Using the function ================== -A :func:`sphinx_add_docs` function is provided to create a target which will -generate the documentation. Therefore, the custom target added in the previous -section could be replaced by the following: +A :func:`sphinx_add_docs` function is provided to simplify the creation of a +documentation target. The configuration above can therefore be replaced by the +following: .. code-block:: cmake sphinx_add_docs(doc ALL) -By default, the :term:`builder` used is "html". Another builder can be defined +By default, the :term:`builder` used is ``html``. Another builder can be selected as follows: .. code-block:: cmake - sphinx_add_docs(doc ALL BUILDER latex) + sphinx_add_docs(doc ALL + BUILDER latex + ) -You can define different source and output directories as follows: +Different source and output directories can be specified: .. code-block:: cmake @@ -57,7 +90,7 @@ You can define different source and output directories as follows: OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/sphinx-doc ) -You can also define a separate directory to fetch the :file:`conf.py` file: +A separate directory can also be used to locate the :file:`conf.py` file: .. code-block:: cmake @@ -65,18 +98,56 @@ You can also define a separate directory to fetch the :file:`conf.py` file: CONFIG_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/config ) -Defining configuration setting directly within the :term:`CMake` configuration -file to override the :file:`conf.py` file can be done as follows: +Configuration values can be overridden directly from the :term:`CMake` +configuration using the ``DEFINE`` option: .. code-block:: cmake sphinx_add_docs(doc ALL DEFINE - version=${MAKE_PROJECT_VERSION} + version=${PROJECT_VERSION} ) -If necessary, you can also ignore the :file:`conf.py` file: +The :file:`conf.py` file can be ignored entirely if required: .. code-block:: cmake sphinx_add_docs(doc ALL ISOLATED) + +When the documentation depends on project-built libraries or Python modules, +the build environment can be configured declaratively using dedicated options. + +The ``LIBRARY_PATH_PREPEND`` option prepends directories to the platform-specific +library search path, selecting the appropriate environment variable +automatically: + +.. code-block:: cmake + + sphinx_add_docs(doc ALL + LIBRARY_PATH_PREPEND + $ + ) + +Python modules can similarly be made available by prepending directories to the +Python module search path: + +.. code-block:: cmake + + sphinx_add_docs(doc ALL + PYTHON_PATH_PREPEND + ${CMAKE_CURRENT_SOURCE_DIR}/python + ) + +Custom environment variables can also be defined explicitly: + +.. code-block:: cmake + + sphinx_add_docs(doc ALL + ENVIRONMENT + "MY_PROJECT_DOCS_MODE=1" + "CUSTOM_FLAG=enabled" + ) + +These options allow the documentation build environment to be described +declaratively and consistently across platforms, without manually handling +platform-specific environment variables. diff --git a/test/08-prepend-paths/CMakeLists.txt b/test/08-prepend-paths/CMakeLists.txt new file mode 100644 index 0000000..0ed9355 --- /dev/null +++ b/test/08-prepend-paths/CMakeLists.txt @@ -0,0 +1,22 @@ +if(WIN32) + set(EXPECTED "/path1;/path2;/path3") +else() + set(EXPECTED "/path1:/path2:/path3") +endif() +configure_file(index.html index.html @ONLY) + +# NOTE: configure_file() adds a trailing newline; Sphinx doesn’t. +# Strip it so compare_files remains stable. +file(READ ${CMAKE_CURRENT_BINARY_DIR}/index.html _content) +string(REGEX REPLACE "\n$" "" _content "${_content}") +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/index.html "${_content}") + +sphinx_add_docs(doc8 ALL + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + LIBRARY_PATH_PREPEND "/path1" "/path2" "/path3" + PYTHON_PATH_PREPEND "/path1" "/path2" "/path3") + +add_test(NAME PrependPaths + COMMAND ${CMAKE_COMMAND} -E compare_files + ${CMAKE_CURRENT_BINARY_DIR}/index.html + ${CMAKE_CURRENT_BINARY_DIR}/doc/index.html) diff --git a/test/08-prepend-paths/conf.py b/test/08-prepend-paths/conf.py new file mode 100644 index 0000000..776831f --- /dev/null +++ b/test/08-prepend-paths/conf.py @@ -0,0 +1,11 @@ +import sys +from pathlib import Path + +root = Path(__file__).resolve().parent +sys.path.insert(0, str((root / "../resources/_extensions").resolve())) + +project = "foo" +copyright = "2026, john-doe" +extensions = ["sphinx_env"] +templates_path = ["../resources/_templates"] +html_permalinks = False diff --git a/test/08-prepend-paths/index.html b/test/08-prepend-paths/index.html new file mode 100644 index 0000000..fe703a8 --- /dev/null +++ b/test/08-prepend-paths/index.html @@ -0,0 +1,18 @@ + + + + + foo + + +
+

foo

+
+
+ @EXPECTED@@EXPECTED@ +
+
+

©2026, john-doe

+
+ + diff --git a/test/08-prepend-paths/index.rst b/test/08-prepend-paths/index.rst new file mode 100644 index 0000000..36b946f --- /dev/null +++ b/test/08-prepend-paths/index.rst @@ -0,0 +1,2 @@ +.. env:: LIBRARY_PATH 3 +.. env:: PYTHONPATH 3 diff --git a/test/09-environment/CMakeLists.txt b/test/09-environment/CMakeLists.txt new file mode 100644 index 0000000..24b4a26 --- /dev/null +++ b/test/09-environment/CMakeLists.txt @@ -0,0 +1,13 @@ +sphinx_add_docs(doc9 ALL + ENVIRONMENT + "KEY1=VALUE1" + "KEY2=VALUE2" + "KEY3=PATH1:PATH2:PATH3" + "KEY4=PATH1\;PATH2\;PATH3" + "KEY5=C:\\Path\\To\\Dir1\;C:\\Path\\To\\Dir2" + "K3Y4=SPECIAL$VALUE!@#%^&*") + +add_test(NAME Environment + COMMAND ${CMAKE_COMMAND} -E compare_files + ${CMAKE_CURRENT_SOURCE_DIR}/index.html + ${CMAKE_CURRENT_BINARY_DIR}/doc/index.html) diff --git a/test/09-environment/conf.py b/test/09-environment/conf.py new file mode 100644 index 0000000..776831f --- /dev/null +++ b/test/09-environment/conf.py @@ -0,0 +1,11 @@ +import sys +from pathlib import Path + +root = Path(__file__).resolve().parent +sys.path.insert(0, str((root / "../resources/_extensions").resolve())) + +project = "foo" +copyright = "2026, john-doe" +extensions = ["sphinx_env"] +templates_path = ["../resources/_templates"] +html_permalinks = False diff --git a/test/09-environment/index.html b/test/09-environment/index.html new file mode 100644 index 0000000..3b6a18f --- /dev/null +++ b/test/09-environment/index.html @@ -0,0 +1,18 @@ + + + + + foo + + +
+

foo

+
+
+ VALUE1VALUE2PATH1:PATH2:PATH3PATH1;PATH2;PATH3C:\Path\To\Dir1;C:\Path\To\Dir2SPECIAL$VALUE!@#%^&* +
+ + + \ No newline at end of file diff --git a/test/09-environment/index.rst b/test/09-environment/index.rst new file mode 100644 index 0000000..877c838 --- /dev/null +++ b/test/09-environment/index.rst @@ -0,0 +1,6 @@ +.. env:: KEY1 +.. env:: KEY2 +.. env:: KEY3 +.. env:: KEY4 +.. env:: KEY5 +.. env:: K3Y4 diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ff06698..d4c4f56 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -14,3 +14,5 @@ add_subdirectory(04-set-conf-dir) add_subdirectory(05-set-source-dir) add_subdirectory(06-set-output-dir) add_subdirectory(07-use-define) +add_subdirectory(08-prepend-paths) +add_subdirectory(09-environment) diff --git a/test/resources/_extensions/sphinx_env.py b/test/resources/_extensions/sphinx_env.py new file mode 100644 index 0000000..309958c --- /dev/null +++ b/test/resources/_extensions/sphinx_env.py @@ -0,0 +1,47 @@ +import os +import sys +from docutils import nodes +from docutils.parsers.rst import Directive + + +_LIBRARY_PATH_MAP = { + "win32": "PATH", + "darwin": "DYLD_LIBRARY_PATH", +} + + +class Env(Directive): + """ + Usage: + .. env:: VAR_NAME + .. env:: VAR_NAME N + + Special case: + LIBRARY_PATH -> platform-specific library path variable + + If N is provided and the value is a list, only the first N entries + are shown. + """ + required_arguments = 1 + optional_arguments = 1 + has_content = False + + def run(self): + name = self.arguments[0] + + if name == "LIBRARY_PATH": + env_name = _LIBRARY_PATH_MAP.get(sys.platform, "LD_LIBRARY_PATH") + else: + env_name = name + + value = os.environ.get(env_name, "") + + if len(self.arguments) == 2: + sep = ";" if sys.platform == "win32" else ":" + value = sep.join(value.split(sep)[: int(self.arguments[1])]) + + return [nodes.inline(text=value)] + + +def setup(app): + app.add_directive("env", Env)