[{"id":35803,"web_url":"https://patchwork.libcamera.org/comment/35803/","msgid":"<b9db2aa7-8f84-41e6-8833-4ba8002ad1d4@ideasonboard.com>","date":"2025-09-12T11:24:59","subject":"Re: [PATCH v2 06/10] Documentation: Install API documentation within\n\tHTML directory","submitter":{"id":216,"url":"https://patchwork.libcamera.org/api/people/216/","name":"Barnabás Pőcze","email":"barnabas.pocze@ideasonboard.com"},"content":"2025. 09. 12. 1:01 keltezéssel, Laurent Pinchart írta:\n> The libcamera documentation is made of high-level documentation and\n> guides, written as ReStructuredText and compiled to HTML by Sphinx, and\n> API reference documentation, written as comments in the code and\n> compiled to HTML by Doxygen.\n> \n> Due to meson limitations that make it difficult to place output files in\n> subdirectories, the compilation process produces an html/ directory for\n> the Sphinx documentation, and api-html/ and internal-api-html/\n> directories for the Doxygen documentation. When deploying the\n> documentation to the libcamera.org website, the api-html and\n> internal-api-html/ directories are moved within html/ to make the\n> documentation self-contained.\n> \n> The Sphinx and Doxygen documentation link to each other. The links are\n> generated using relative paths, based on the directory hierarchy on the\n> website. This makes them broken when navigating the documentation in the\n> build tree or in the directory where libcamera is installed.\n> \n> Fix this by standardizing on the directories hierarchy of the website in\n> the build and install directories:\n> \n> - For the build directory, we can't easily build the Doxygen\n>    documentation in a subdirectory of the Sphinx documentation due to\n>    limitations of meson. Keep the existing output directories, and\n>    replace the html/api-html/ and html/internal-api-html/ placeholder\n>    directories with symlinks to the Doxygen output directories. This is\n>    handled by a small custom Sphinx extension.\n> \n> - For the install directory, install the Doxygen documentation to\n>    html/api-html/ and html/internal-api-html/. This overwrites the\n>    placeholders.\n\nDoes it need a special meson option? Installation fails for me:\n\n   Tried to copy file /tmp/test/usr/share/doc/libcamera-0.5.2/html/internal-api but a directory of that name already exists.\n\n\nRegards,\nBarnabás Pőcze\n\n> \n> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n> ---\n> Changes since v1:\n> \n> - Split from \"[PATCH 4/6] documentation: Include API docs in the sphinx\n>    documentation\"\n> - Avoid copying the doxygen documentation\n> - Use a sphinx extension to generate symlinks\n> ---\n>   Documentation/conf.py.in                 | 26 +++++----\n>   Documentation/extensions/doxygen-link.py | 73 ++++++++++++++++++++++++\n>   Documentation/meson.build                | 19 ++++--\n>   3 files changed, 104 insertions(+), 14 deletions(-)\n>   create mode 100644 Documentation/extensions/doxygen-link.py\n> \n> diff --git a/Documentation/conf.py.in b/Documentation/conf.py.in\n> index 34fa3956f49e..5e0a7cff2984 100644\n> --- a/Documentation/conf.py.in\n> +++ b/Documentation/conf.py.in\n> @@ -9,14 +9,8 @@\n>   \n>   # -- Path setup --------------------------------------------------------------\n>   \n> -# If extensions (or modules to document with autodoc) are in another directory,\n> -# add these directories to sys.path here. If the directory is relative to the\n> -# documentation root, use os.path.abspath to make it absolute, like shown here.\n> -#\n> -# import os\n> -# import sys\n> -# sys.path.insert(0, os.path.abspath('.'))\n> -\n> +import sys\n> +sys.path.insert(0, \"@CURRENT_SRCDIR@/extensions\")\n>   \n>   # -- Project information -----------------------------------------------------\n>   \n> @@ -39,6 +33,7 @@ author = 'The libcamera documentation authors'\n>   extensions = [\n>       'sphinx.ext.graphviz',\n>       'sphinxcontrib.doxylink',\n> +    'doxygen-link',\n>   ]\n>   \n>   graphviz_output_format = 'svg'\n> @@ -75,14 +70,25 @@ pygments_style = None\n>   doxylink = {\n>       'doxy-pub': (\n>           '@TOP_BUILDDIR@/Documentation/api-html/tagfile.xml',\n> -        '../api-html/',\n> +        'api-html/',\n>       ),\n>       'doxy-int': (\n>           '@TOP_BUILDDIR@/Documentation/internal-api-html/tagfile.xml',\n> -        '../internal-api-html/',\n> +        'internal-api-html/',\n>       ),\n>   }\n>   \n> +doxygen_links = [\n> +    [\n> +        '@TOP_BUILDDIR@/Documentation/html/api-html',\n> +        '@TOP_BUILDDIR@/Documentation/api-html',\n> +    ],\n> +    [\n> +        '@TOP_BUILDDIR@/Documentation/html/internal-api-html',\n> +        '@TOP_BUILDDIR@/Documentation/internal-api-html',\n> +    ],\n> +]\n> +\n>   # -- Options for HTML output -------------------------------------------------\n>   \n>   # The theme to use for HTML and HTML Help pages.  See the documentation for\n> diff --git a/Documentation/extensions/doxygen-link.py b/Documentation/extensions/doxygen-link.py\n> new file mode 100644\n> index 000000000000..8b94a4dd7f0b\n> --- /dev/null\n> +++ b/Documentation/extensions/doxygen-link.py\n> @@ -0,0 +1,73 @@\n> +# SPDX-License-Identifier: GPL-2.0-or-later\n> +# Copyright (C) 2025, Ideas on Board Oy\n> +\n> +import contextlib\n> +import os\n> +import shutil\n> +from pathlib import Path\n> +from sphinx.util import logging\n> +\n> +__version__ = \"0.0.0\"\n> +\n> +logger = logging.getLogger(__name__)\n> +\n> +\n> +def on_config_inited(app, config):\n> +    entries = []\n> +\n> +    outdir = Path(app.outdir).absolute()\n> +\n> +    for index, items in enumerate(config.doxygen_links):\n> +        err_msg_prefix = f'Config variable `doxygen_links` contains invalid entry {index} (`{items}`)'\n> +\n> +        if len(items) != 2:\n> +            raise ValueError(f'{err_msg_prefix}: expected (path, target)')\n> +\n> +        path = Path(items[0]).absolute()\n> +        target = Path(items[1]).relative_to(path.parent, walk_up=True)\n> +\n> +        if not path.is_relative_to(outdir):\n> +            raise ValueError(f'{err_msg_prefix}: path `{items[0]}` is outside of output directory `{outdir}`')\n> +\n> +        entries.append([path, target])\n> +\n> +    config.doxygen_links = entries\n> +\n> +\n> +def on_builder_inited(app):\n> +    # Remove the symlinks if they exist, to avoid overwriting the index.html\n> +    # generated by Doxygen with the placeholder index from Sphinx.\n> +\n> +    for path, target in app.config.doxygen_links:\n> +        if path.is_symlink():\n> +            logger.info(f'Removing existing symlink {path}')\n> +            os.unlink(path)\n> +\n> +\n> +def on_build_finished(app, exc):\n> +    # Create the symlinks. Remove any existing placeholder directory\n> +    # recursively first.\n> +\n> +    if exc:\n> +        return\n> +\n> +    for path, target in app.config.doxygen_links:\n> +        logger.info(f'Creating symlink {path} -> {target}')\n> +\n> +        if path.is_dir():\n> +            shutil.rmtree(path)\n> +\n> +        os.symlink(target, path)\n> +\n> +\n> +def setup(app):\n> +    app.add_config_value('doxygen_links', [], 'env', frozenset({list, tuple}))\n> +    app.connect('config-inited', on_config_inited)\n> +    app.connect('builder-inited', on_builder_inited)\n> +    app.connect('build-finished', on_build_finished)\n> +\n> +    return {\n> +        \"version\": __version__,\n> +        \"parallel_read_safe\": True,\n> +        \"parallel_write_safe\": True,\n> +    }\n> diff --git a/Documentation/meson.build b/Documentation/meson.build\n> index f73407432fff..82d76b257b8c 100644\n> --- a/Documentation/meson.build\n> +++ b/Documentation/meson.build\n> @@ -12,6 +12,7 @@ dot = find_program('dot', required : get_option('documentation'))\n>   if doxygen.found() and dot.found()\n>       cdata = configuration_data()\n>       cdata.set('VERSION', 'v@0@'.format(libcamera_git_version))\n> +    cdata.set('CURRENT_SRCDIR', meson.current_source_dir())\n>       cdata.set('TOP_SRCDIR', meson.project_source_root())\n>       cdata.set('TOP_BUILDDIR', meson.project_build_root())\n>       cdata.set('OUTPUT_DIR', meson.current_build_dir())\n> @@ -89,7 +90,7 @@ if doxygen.found() and dot.found()\n>                                      output : 'api-html',\n>                                      command : [doxygen, doxyfile],\n>                                      install : true,\n> -                                   install_dir : doc_install_dir,\n> +                                   install_dir : doc_install_dir / 'html',\n>                                      install_tag : 'doc')\n>   \n>       # This is the internal documentation, which hard-codes a list of directories\n> @@ -109,7 +110,7 @@ if doxygen.found() and dot.found()\n>                                        output : 'internal-api-html',\n>                                        command : [doxygen, doxyfile],\n>                                        install : true,\n> -                                     install_dir : doc_install_dir,\n> +                                     install_dir : doc_install_dir / 'html',\n>                                        install_tag : 'doc-internal')\n>   endif\n>   \n> @@ -149,7 +150,11 @@ if sphinx.found()\n>       fs = import('fs')\n>       sphinx_conf_dir = fs.parent(sphinx_conf)\n>   \n> +    sphinx_env = environment()\n> +    sphinx_env.set('PYTHONDONTWRITEBYTECODE', '1')\n> +\n>       docs_sources = [\n> +        'api-html/index.rst',\n>           'camera-sensor-model.rst',\n>           'code-of-conduct.rst',\n>           'coding-style.rst',\n> @@ -164,6 +169,7 @@ if sphinx.found()\n>           'guides/pipeline-handler.rst',\n>           'guides/tracing.rst',\n>           'index.rst',\n> +        'internal-api-html/index.rst',\n>           'introduction.rst',\n>           'lens_driver_requirements.rst',\n>           'libcamera_architecture.rst',\n> @@ -183,10 +189,14 @@ if sphinx.found()\n>                     input : docs_sources,\n>                     output : 'html',\n>                     build_by_default : true,\n> +                  depend_files : [\n> +                      'extensions/doxygen-link.py',\n> +                  ],\n>                     depends : [\n>                         doxygen_public,\n>                         doxygen_internal,\n>                     ],\n> +                  env : sphinx_env,\n>                     install : true,\n>                     install_dir : doc_install_dir,\n>                     install_tag : 'doc')\n> @@ -195,7 +205,8 @@ if sphinx.found()\n>                     command : [sphinx, '-W', '-b', 'linkcheck',\n>                                '-c', sphinx_conf_dir,\n>                                meson.current_source_dir(), '@OUTPUT@'],\n> -                  build_always_stale : true,\n>                     input : docs_sources,\n> -                  output : 'linkcheck')\n> +                  output : 'linkcheck',\n> +                  build_always_stale : true,\n> +                  env : sphinx_env)\n>   endif","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 2AA24C324E\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 12 Sep 2025 11:25:07 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 2F5E76936F;\n\tFri, 12 Sep 2025 13:25:06 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id B6D6F69367\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 12 Sep 2025 13:25:04 +0200 (CEST)","from [192.168.33.2] (185.221.142.115.nat.pool.zt.hu\n\t[185.221.142.115])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 5D34F22B;\n\tFri, 12 Sep 2025 13:23:49 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"irnix5xc\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1757676229;\n\tbh=e6d2c1pOKIpcVObIIMVX/E/u90rWiQ/vH18lnGdH7VQ=;\n\th=Date:Subject:To:References:From:In-Reply-To:From;\n\tb=irnix5xcwJLefrfmdfGht/73+d0if0nMK9W4pZqUOz1ipN7X70HeHDY3YcsIe8F2T\n\tLQG+FaLudZlK+rim28xsv5+Y8bI4E5C7MfD084JK7mKxT+8+lndTJ+FVhJlZufb7jg\n\t//tAJx2lq8hu0YEZASy1cx9B0Wph1HuxyXBCWSaM=","Message-ID":"<b9db2aa7-8f84-41e6-8833-4ba8002ad1d4@ideasonboard.com>","Date":"Fri, 12 Sep 2025 13:24:59 +0200","MIME-Version":"1.0","User-Agent":"Mozilla Thunderbird","Subject":"Re: [PATCH v2 06/10] Documentation: Install API documentation within\n\tHTML directory","To":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>,\n\tlibcamera-devel@lists.libcamera.org","References":"<20250911230115.25335-1-laurent.pinchart@ideasonboard.com>\n\t<20250911230115.25335-7-laurent.pinchart@ideasonboard.com>","From":"=?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= <barnabas.pocze@ideasonboard.com>","Content-Language":"en-US, hu-HU","In-Reply-To":"<20250911230115.25335-7-laurent.pinchart@ideasonboard.com>","Content-Type":"text/plain; charset=UTF-8; format=flowed","Content-Transfer-Encoding":"8bit","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":35810,"web_url":"https://patchwork.libcamera.org/comment/35810/","msgid":"<20250912162811.GA28587@pendragon.ideasonboard.com>","date":"2025-09-12T16:28:11","subject":"Re: [PATCH v2 06/10] Documentation: Install API documentation within\n\tHTML directory","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"On Fri, Sep 12, 2025 at 02:01:07AM +0300, Laurent Pinchart wrote:\n> The libcamera documentation is made of high-level documentation and\n> guides, written as ReStructuredText and compiled to HTML by Sphinx, and\n> API reference documentation, written as comments in the code and\n> compiled to HTML by Doxygen.\n> \n> Due to meson limitations that make it difficult to place output files in\n> subdirectories, the compilation process produces an html/ directory for\n> the Sphinx documentation, and api-html/ and internal-api-html/\n> directories for the Doxygen documentation. When deploying the\n> documentation to the libcamera.org website, the api-html and\n> internal-api-html/ directories are moved within html/ to make the\n> documentation self-contained.\n> \n> The Sphinx and Doxygen documentation link to each other. The links are\n> generated using relative paths, based on the directory hierarchy on the\n> website. This makes them broken when navigating the documentation in the\n> build tree or in the directory where libcamera is installed.\n> \n> Fix this by standardizing on the directories hierarchy of the website in\n> the build and install directories:\n> \n> - For the build directory, we can't easily build the Doxygen\n>   documentation in a subdirectory of the Sphinx documentation due to\n>   limitations of meson. Keep the existing output directories, and\n>   replace the html/api-html/ and html/internal-api-html/ placeholder\n>   directories with symlinks to the Doxygen output directories. This is\n>   handled by a small custom Sphinx extension.\n> \n> - For the install directory, install the Doxygen documentation to\n>   html/api-html/ and html/internal-api-html/. This overwrites the\n>   placeholders.\n> \n> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n> ---\n> Changes since v1:\n> \n> - Split from \"[PATCH 4/6] documentation: Include API docs in the sphinx\n>   documentation\"\n> - Avoid copying the doxygen documentation\n> - Use a sphinx extension to generate symlinks\n> ---\n>  Documentation/conf.py.in                 | 26 +++++----\n>  Documentation/extensions/doxygen-link.py | 73 ++++++++++++++++++++++++\n>  Documentation/meson.build                | 19 ++++--\n>  3 files changed, 104 insertions(+), 14 deletions(-)\n>  create mode 100644 Documentation/extensions/doxygen-link.py\n> \n> diff --git a/Documentation/conf.py.in b/Documentation/conf.py.in\n> index 34fa3956f49e..5e0a7cff2984 100644\n> --- a/Documentation/conf.py.in\n> +++ b/Documentation/conf.py.in\n> @@ -9,14 +9,8 @@\n>  \n>  # -- Path setup --------------------------------------------------------------\n>  \n> -# If extensions (or modules to document with autodoc) are in another directory,\n> -# add these directories to sys.path here. If the directory is relative to the\n> -# documentation root, use os.path.abspath to make it absolute, like shown here.\n> -#\n> -# import os\n> -# import sys\n> -# sys.path.insert(0, os.path.abspath('.'))\n> -\n> +import sys\n> +sys.path.insert(0, \"@CURRENT_SRCDIR@/extensions\")\n>  \n>  # -- Project information -----------------------------------------------------\n>  \n> @@ -39,6 +33,7 @@ author = 'The libcamera documentation authors'\n>  extensions = [\n>      'sphinx.ext.graphviz',\n>      'sphinxcontrib.doxylink',\n> +    'doxygen-link',\n>  ]\n>  \n>  graphviz_output_format = 'svg'\n> @@ -75,14 +70,25 @@ pygments_style = None\n>  doxylink = {\n>      'doxy-pub': (\n>          '@TOP_BUILDDIR@/Documentation/api-html/tagfile.xml',\n> -        '../api-html/',\n> +        'api-html/',\n>      ),\n>      'doxy-int': (\n>          '@TOP_BUILDDIR@/Documentation/internal-api-html/tagfile.xml',\n> -        '../internal-api-html/',\n> +        'internal-api-html/',\n>      ),\n>  }\n>  \n> +doxygen_links = [\n> +    [\n> +        '@TOP_BUILDDIR@/Documentation/html/api-html',\n> +        '@TOP_BUILDDIR@/Documentation/api-html',\n> +    ],\n> +    [\n> +        '@TOP_BUILDDIR@/Documentation/html/internal-api-html',\n> +        '@TOP_BUILDDIR@/Documentation/internal-api-html',\n> +    ],\n> +]\n> +\n>  # -- Options for HTML output -------------------------------------------------\n>  \n>  # The theme to use for HTML and HTML Help pages.  See the documentation for\n> diff --git a/Documentation/extensions/doxygen-link.py b/Documentation/extensions/doxygen-link.py\n> new file mode 100644\n> index 000000000000..8b94a4dd7f0b\n> --- /dev/null\n> +++ b/Documentation/extensions/doxygen-link.py\n> @@ -0,0 +1,73 @@\n> +# SPDX-License-Identifier: GPL-2.0-or-later\n> +# Copyright (C) 2025, Ideas on Board Oy\n> +\n> +import contextlib\n> +import os\n> +import shutil\n> +from pathlib import Path\n> +from sphinx.util import logging\n> +\n> +__version__ = \"0.0.0\"\n> +\n> +logger = logging.getLogger(__name__)\n> +\n> +\n> +def on_config_inited(app, config):\n> +    entries = []\n> +\n> +    outdir = Path(app.outdir).absolute()\n> +\n> +    for index, items in enumerate(config.doxygen_links):\n> +        err_msg_prefix = f'Config variable `doxygen_links` contains invalid entry {index} (`{items}`)'\n> +\n> +        if len(items) != 2:\n> +            raise ValueError(f'{err_msg_prefix}: expected (path, target)')\n> +\n> +        path = Path(items[0]).absolute()\n> +        target = Path(items[1]).relative_to(path.parent, walk_up=True)\n\nThis fails in CI :-(\n\nHandler <function on_config_inited at 0x7fa697693e20> for event 'config-inited' threw an exception (exception: PurePath.relative_to() got an unexpected keyword argument 'walk_up')\n\nThe walk_up argument to relative_to() has been added in Python 3.12.\nThis needs to switch to os.path.relpath().\n\n> +\n> +        if not path.is_relative_to(outdir):\n> +            raise ValueError(f'{err_msg_prefix}: path `{items[0]}` is outside of output directory `{outdir}`')\n> +\n> +        entries.append([path, target])\n> +\n> +    config.doxygen_links = entries\n> +\n> +\n> +def on_builder_inited(app):\n> +    # Remove the symlinks if they exist, to avoid overwriting the index.html\n> +    # generated by Doxygen with the placeholder index from Sphinx.\n> +\n> +    for path, target in app.config.doxygen_links:\n> +        if path.is_symlink():\n> +            logger.info(f'Removing existing symlink {path}')\n> +            os.unlink(path)\n> +\n> +\n> +def on_build_finished(app, exc):\n> +    # Create the symlinks. Remove any existing placeholder directory\n> +    # recursively first.\n> +\n> +    if exc:\n> +        return\n> +\n> +    for path, target in app.config.doxygen_links:\n> +        logger.info(f'Creating symlink {path} -> {target}')\n> +\n> +        if path.is_dir():\n> +            shutil.rmtree(path)\n> +\n> +        os.symlink(target, path)\n> +\n> +\n> +def setup(app):\n> +    app.add_config_value('doxygen_links', [], 'env', frozenset({list, tuple}))\n> +    app.connect('config-inited', on_config_inited)\n> +    app.connect('builder-inited', on_builder_inited)\n> +    app.connect('build-finished', on_build_finished)\n> +\n> +    return {\n> +        \"version\": __version__,\n> +        \"parallel_read_safe\": True,\n> +        \"parallel_write_safe\": True,\n> +    }\n> diff --git a/Documentation/meson.build b/Documentation/meson.build\n> index f73407432fff..82d76b257b8c 100644\n> --- a/Documentation/meson.build\n> +++ b/Documentation/meson.build\n> @@ -12,6 +12,7 @@ dot = find_program('dot', required : get_option('documentation'))\n>  if doxygen.found() and dot.found()\n>      cdata = configuration_data()\n>      cdata.set('VERSION', 'v@0@'.format(libcamera_git_version))\n> +    cdata.set('CURRENT_SRCDIR', meson.current_source_dir())\n>      cdata.set('TOP_SRCDIR', meson.project_source_root())\n>      cdata.set('TOP_BUILDDIR', meson.project_build_root())\n>      cdata.set('OUTPUT_DIR', meson.current_build_dir())\n> @@ -89,7 +90,7 @@ if doxygen.found() and dot.found()\n>                                     output : 'api-html',\n>                                     command : [doxygen, doxyfile],\n>                                     install : true,\n> -                                   install_dir : doc_install_dir,\n> +                                   install_dir : doc_install_dir / 'html',\n>                                     install_tag : 'doc')\n>  \n>      # This is the internal documentation, which hard-codes a list of directories\n> @@ -109,7 +110,7 @@ if doxygen.found() and dot.found()\n>                                       output : 'internal-api-html',\n>                                       command : [doxygen, doxyfile],\n>                                       install : true,\n> -                                     install_dir : doc_install_dir,\n> +                                     install_dir : doc_install_dir / 'html',\n>                                       install_tag : 'doc-internal')\n>  endif\n>  \n> @@ -149,7 +150,11 @@ if sphinx.found()\n>      fs = import('fs')\n>      sphinx_conf_dir = fs.parent(sphinx_conf)\n>  \n> +    sphinx_env = environment()\n> +    sphinx_env.set('PYTHONDONTWRITEBYTECODE', '1')\n> +\n>      docs_sources = [\n> +        'api-html/index.rst',\n>          'camera-sensor-model.rst',\n>          'code-of-conduct.rst',\n>          'coding-style.rst',\n> @@ -164,6 +169,7 @@ if sphinx.found()\n>          'guides/pipeline-handler.rst',\n>          'guides/tracing.rst',\n>          'index.rst',\n> +        'internal-api-html/index.rst',\n>          'introduction.rst',\n>          'lens_driver_requirements.rst',\n>          'libcamera_architecture.rst',\n> @@ -183,10 +189,14 @@ if sphinx.found()\n>                    input : docs_sources,\n>                    output : 'html',\n>                    build_by_default : true,\n> +                  depend_files : [\n> +                      'extensions/doxygen-link.py',\n> +                  ],\n>                    depends : [\n>                        doxygen_public,\n>                        doxygen_internal,\n>                    ],\n> +                  env : sphinx_env,\n>                    install : true,\n>                    install_dir : doc_install_dir,\n>                    install_tag : 'doc')\n> @@ -195,7 +205,8 @@ if sphinx.found()\n>                    command : [sphinx, '-W', '-b', 'linkcheck',\n>                               '-c', sphinx_conf_dir,\n>                               meson.current_source_dir(), '@OUTPUT@'],\n> -                  build_always_stale : true,\n>                    input : docs_sources,\n> -                  output : 'linkcheck')\n> +                  output : 'linkcheck',\n> +                  build_always_stale : true,\n> +                  env : sphinx_env)\n>  endif","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 11A60BDB13\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 12 Sep 2025 16:28:40 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 432696936F;\n\tFri, 12 Sep 2025 18:28:39 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 6707769367\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 12 Sep 2025 18:28:37 +0200 (CEST)","from pendragon.ideasonboard.com (81-175-209-231.bb.dnainternet.fi\n\t[81.175.209.231])\n\tby perceval.ideasonboard.com (Postfix) with UTF8SMTPSA id BC4A550A\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 12 Sep 2025 18:27:21 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"OZ25TkMT\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1757694442;\n\tbh=Ro191BmyXPaceTKZ0n73BM5nxqsvG43ilU1LRNzqfjc=;\n\th=Date:From:To:Subject:References:In-Reply-To:From;\n\tb=OZ25TkMTha3HOCIk39jAkaqlGZPp0D4/FUVyzjAGWzKnvFwzxc2ib3D6iYtxi4ybG\n\t5ESyrg7f+9o5QxBVMUWtLi40ph/8mjI0tQE39fy2QnpXYB5HSDY724rieKn8JC5EDW\n\t88ArykEwkHWqfuJf2+BqkFNT7uq12qp9tYwEYG/M=","Date":"Fri, 12 Sep 2025 19:28:11 +0300","From":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","To":"libcamera-devel@lists.libcamera.org","Subject":"Re: [PATCH v2 06/10] Documentation: Install API documentation within\n\tHTML directory","Message-ID":"<20250912162811.GA28587@pendragon.ideasonboard.com>","References":"<20250911230115.25335-1-laurent.pinchart@ideasonboard.com>\n\t<20250911230115.25335-7-laurent.pinchart@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20250911230115.25335-7-laurent.pinchart@ideasonboard.com>","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":35811,"web_url":"https://patchwork.libcamera.org/comment/35811/","msgid":"<20250912170417.GA29172@pendragon.ideasonboard.com>","date":"2025-09-12T17:04:17","subject":"Re: [PATCH v2 06/10] Documentation: Install API documentation within\n\tHTML directory","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"On Fri, Sep 12, 2025 at 01:24:59PM +0200, Barnabás Pőcze wrote:\n> 2025. 09. 12. 1:01 keltezéssel, Laurent Pinchart írta:\n> > The libcamera documentation is made of high-level documentation and\n> > guides, written as ReStructuredText and compiled to HTML by Sphinx, and\n> > API reference documentation, written as comments in the code and\n> > compiled to HTML by Doxygen.\n> > \n> > Due to meson limitations that make it difficult to place output files in\n> > subdirectories, the compilation process produces an html/ directory for\n> > the Sphinx documentation, and api-html/ and internal-api-html/\n> > directories for the Doxygen documentation. When deploying the\n> > documentation to the libcamera.org website, the api-html and\n> > internal-api-html/ directories are moved within html/ to make the\n> > documentation self-contained.\n> > \n> > The Sphinx and Doxygen documentation link to each other. The links are\n> > generated using relative paths, based on the directory hierarchy on the\n> > website. This makes them broken when navigating the documentation in the\n> > build tree or in the directory where libcamera is installed.\n> > \n> > Fix this by standardizing on the directories hierarchy of the website in\n> > the build and install directories:\n> > \n> > - For the build directory, we can't easily build the Doxygen\n> >    documentation in a subdirectory of the Sphinx documentation due to\n> >    limitations of meson. Keep the existing output directories, and\n> >    replace the html/api-html/ and html/internal-api-html/ placeholder\n> >    directories with symlinks to the Doxygen output directories. This is\n> >    handled by a small custom Sphinx extension.\n> > \n> > - For the install directory, install the Doxygen documentation to\n> >    html/api-html/ and html/internal-api-html/. This overwrites the\n> >    placeholders.\n> \n> Does it need a special meson option? Installation fails for me:\n> \n>    Tried to copy file /tmp/test/usr/share/doc/libcamera-0.5.2/html/internal-api but a directory of that name already exists.\n\nThat's new to me :-( What meson version are you using ?\n\n> > Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n> > ---\n> > Changes since v1:\n> > \n> > - Split from \"[PATCH 4/6] documentation: Include API docs in the sphinx\n> >    documentation\"\n> > - Avoid copying the doxygen documentation\n> > - Use a sphinx extension to generate symlinks\n> > ---\n> >   Documentation/conf.py.in                 | 26 +++++----\n> >   Documentation/extensions/doxygen-link.py | 73 ++++++++++++++++++++++++\n> >   Documentation/meson.build                | 19 ++++--\n> >   3 files changed, 104 insertions(+), 14 deletions(-)\n> >   create mode 100644 Documentation/extensions/doxygen-link.py\n> > \n> > diff --git a/Documentation/conf.py.in b/Documentation/conf.py.in\n> > index 34fa3956f49e..5e0a7cff2984 100644\n> > --- a/Documentation/conf.py.in\n> > +++ b/Documentation/conf.py.in\n> > @@ -9,14 +9,8 @@\n> >   \n> >   # -- Path setup --------------------------------------------------------------\n> >   \n> > -# If extensions (or modules to document with autodoc) are in another directory,\n> > -# add these directories to sys.path here. If the directory is relative to the\n> > -# documentation root, use os.path.abspath to make it absolute, like shown here.\n> > -#\n> > -# import os\n> > -# import sys\n> > -# sys.path.insert(0, os.path.abspath('.'))\n> > -\n> > +import sys\n> > +sys.path.insert(0, \"@CURRENT_SRCDIR@/extensions\")\n> >   \n> >   # -- Project information -----------------------------------------------------\n> >   \n> > @@ -39,6 +33,7 @@ author = 'The libcamera documentation authors'\n> >   extensions = [\n> >       'sphinx.ext.graphviz',\n> >       'sphinxcontrib.doxylink',\n> > +    'doxygen-link',\n> >   ]\n> >   \n> >   graphviz_output_format = 'svg'\n> > @@ -75,14 +70,25 @@ pygments_style = None\n> >   doxylink = {\n> >       'doxy-pub': (\n> >           '@TOP_BUILDDIR@/Documentation/api-html/tagfile.xml',\n> > -        '../api-html/',\n> > +        'api-html/',\n> >       ),\n> >       'doxy-int': (\n> >           '@TOP_BUILDDIR@/Documentation/internal-api-html/tagfile.xml',\n> > -        '../internal-api-html/',\n> > +        'internal-api-html/',\n> >       ),\n> >   }\n> >   \n> > +doxygen_links = [\n> > +    [\n> > +        '@TOP_BUILDDIR@/Documentation/html/api-html',\n> > +        '@TOP_BUILDDIR@/Documentation/api-html',\n> > +    ],\n> > +    [\n> > +        '@TOP_BUILDDIR@/Documentation/html/internal-api-html',\n> > +        '@TOP_BUILDDIR@/Documentation/internal-api-html',\n> > +    ],\n> > +]\n> > +\n> >   # -- Options for HTML output -------------------------------------------------\n> >   \n> >   # The theme to use for HTML and HTML Help pages.  See the documentation for\n> > diff --git a/Documentation/extensions/doxygen-link.py b/Documentation/extensions/doxygen-link.py\n> > new file mode 100644\n> > index 000000000000..8b94a4dd7f0b\n> > --- /dev/null\n> > +++ b/Documentation/extensions/doxygen-link.py\n> > @@ -0,0 +1,73 @@\n> > +# SPDX-License-Identifier: GPL-2.0-or-later\n> > +# Copyright (C) 2025, Ideas on Board Oy\n> > +\n> > +import contextlib\n> > +import os\n> > +import shutil\n> > +from pathlib import Path\n> > +from sphinx.util import logging\n> > +\n> > +__version__ = \"0.0.0\"\n> > +\n> > +logger = logging.getLogger(__name__)\n> > +\n> > +\n> > +def on_config_inited(app, config):\n> > +    entries = []\n> > +\n> > +    outdir = Path(app.outdir).absolute()\n> > +\n> > +    for index, items in enumerate(config.doxygen_links):\n> > +        err_msg_prefix = f'Config variable `doxygen_links` contains invalid entry {index} (`{items}`)'\n> > +\n> > +        if len(items) != 2:\n> > +            raise ValueError(f'{err_msg_prefix}: expected (path, target)')\n> > +\n> > +        path = Path(items[0]).absolute()\n> > +        target = Path(items[1]).relative_to(path.parent, walk_up=True)\n> > +\n> > +        if not path.is_relative_to(outdir):\n> > +            raise ValueError(f'{err_msg_prefix}: path `{items[0]}` is outside of output directory `{outdir}`')\n> > +\n> > +        entries.append([path, target])\n> > +\n> > +    config.doxygen_links = entries\n> > +\n> > +\n> > +def on_builder_inited(app):\n> > +    # Remove the symlinks if they exist, to avoid overwriting the index.html\n> > +    # generated by Doxygen with the placeholder index from Sphinx.\n> > +\n> > +    for path, target in app.config.doxygen_links:\n> > +        if path.is_symlink():\n> > +            logger.info(f'Removing existing symlink {path}')\n> > +            os.unlink(path)\n> > +\n> > +\n> > +def on_build_finished(app, exc):\n> > +    # Create the symlinks. Remove any existing placeholder directory\n> > +    # recursively first.\n> > +\n> > +    if exc:\n> > +        return\n> > +\n> > +    for path, target in app.config.doxygen_links:\n> > +        logger.info(f'Creating symlink {path} -> {target}')\n> > +\n> > +        if path.is_dir():\n> > +            shutil.rmtree(path)\n> > +\n> > +        os.symlink(target, path)\n> > +\n> > +\n> > +def setup(app):\n> > +    app.add_config_value('doxygen_links', [], 'env', frozenset({list, tuple}))\n> > +    app.connect('config-inited', on_config_inited)\n> > +    app.connect('builder-inited', on_builder_inited)\n> > +    app.connect('build-finished', on_build_finished)\n> > +\n> > +    return {\n> > +        \"version\": __version__,\n> > +        \"parallel_read_safe\": True,\n> > +        \"parallel_write_safe\": True,\n> > +    }\n> > diff --git a/Documentation/meson.build b/Documentation/meson.build\n> > index f73407432fff..82d76b257b8c 100644\n> > --- a/Documentation/meson.build\n> > +++ b/Documentation/meson.build\n> > @@ -12,6 +12,7 @@ dot = find_program('dot', required : get_option('documentation'))\n> >   if doxygen.found() and dot.found()\n> >       cdata = configuration_data()\n> >       cdata.set('VERSION', 'v@0@'.format(libcamera_git_version))\n> > +    cdata.set('CURRENT_SRCDIR', meson.current_source_dir())\n> >       cdata.set('TOP_SRCDIR', meson.project_source_root())\n> >       cdata.set('TOP_BUILDDIR', meson.project_build_root())\n> >       cdata.set('OUTPUT_DIR', meson.current_build_dir())\n> > @@ -89,7 +90,7 @@ if doxygen.found() and dot.found()\n> >                                      output : 'api-html',\n> >                                      command : [doxygen, doxyfile],\n> >                                      install : true,\n> > -                                   install_dir : doc_install_dir,\n> > +                                   install_dir : doc_install_dir / 'html',\n> >                                      install_tag : 'doc')\n> >   \n> >       # This is the internal documentation, which hard-codes a list of directories\n> > @@ -109,7 +110,7 @@ if doxygen.found() and dot.found()\n> >                                        output : 'internal-api-html',\n> >                                        command : [doxygen, doxyfile],\n> >                                        install : true,\n> > -                                     install_dir : doc_install_dir,\n> > +                                     install_dir : doc_install_dir / 'html',\n> >                                        install_tag : 'doc-internal')\n> >   endif\n> >   \n> > @@ -149,7 +150,11 @@ if sphinx.found()\n> >       fs = import('fs')\n> >       sphinx_conf_dir = fs.parent(sphinx_conf)\n> >   \n> > +    sphinx_env = environment()\n> > +    sphinx_env.set('PYTHONDONTWRITEBYTECODE', '1')\n> > +\n> >       docs_sources = [\n> > +        'api-html/index.rst',\n> >           'camera-sensor-model.rst',\n> >           'code-of-conduct.rst',\n> >           'coding-style.rst',\n> > @@ -164,6 +169,7 @@ if sphinx.found()\n> >           'guides/pipeline-handler.rst',\n> >           'guides/tracing.rst',\n> >           'index.rst',\n> > +        'internal-api-html/index.rst',\n> >           'introduction.rst',\n> >           'lens_driver_requirements.rst',\n> >           'libcamera_architecture.rst',\n> > @@ -183,10 +189,14 @@ if sphinx.found()\n> >                     input : docs_sources,\n> >                     output : 'html',\n> >                     build_by_default : true,\n> > +                  depend_files : [\n> > +                      'extensions/doxygen-link.py',\n> > +                  ],\n> >                     depends : [\n> >                         doxygen_public,\n> >                         doxygen_internal,\n> >                     ],\n> > +                  env : sphinx_env,\n> >                     install : true,\n> >                     install_dir : doc_install_dir,\n> >                     install_tag : 'doc')\n> > @@ -195,7 +205,8 @@ if sphinx.found()\n> >                     command : [sphinx, '-W', '-b', 'linkcheck',\n> >                                '-c', sphinx_conf_dir,\n> >                                meson.current_source_dir(), '@OUTPUT@'],\n> > -                  build_always_stale : true,\n> >                     input : docs_sources,\n> > -                  output : 'linkcheck')\n> > +                  output : 'linkcheck',\n> > +                  build_always_stale : true,\n> > +                  env : sphinx_env)","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id D7316C328C\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 12 Sep 2025 17:04:46 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id D463F6936F;\n\tFri, 12 Sep 2025 19:04:45 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 94DD469367\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 12 Sep 2025 19:04:44 +0200 (CEST)","from pendragon.ideasonboard.com (81-175-209-231.bb.dnainternet.fi\n\t[81.175.209.231])\n\tby perceval.ideasonboard.com (Postfix) with UTF8SMTPSA id 66DF9520;\n\tFri, 12 Sep 2025 19:03:28 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"YXvqLj02\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1757696608;\n\tbh=F4YYvnlHYoNh92sbB90rpYuUIwCirAg37Dsz9kRFlcY=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=YXvqLj02kFsKdsoG8D3H9y71xiLMa0P8rqYUpRX17bq0k9L4CdOQwUX0N9Jy+Ov/C\n\tsJRMCNdRtSF/ZTltmmbU+DaE6+08KF5PeEf9BA2j37BHC1wRnA/nFm3Agj5odBmQmF\n\t4Xe0zmLZUTIy7Qg0DLDCn6D1MxHYWSlBXbKzOaso=","Date":"Fri, 12 Sep 2025 20:04:17 +0300","From":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","To":"=?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= <barnabas.pocze@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Subject":"Re: [PATCH v2 06/10] Documentation: Install API documentation within\n\tHTML directory","Message-ID":"<20250912170417.GA29172@pendragon.ideasonboard.com>","References":"<20250911230115.25335-1-laurent.pinchart@ideasonboard.com>\n\t<20250911230115.25335-7-laurent.pinchart@ideasonboard.com>\n\t<b9db2aa7-8f84-41e6-8833-4ba8002ad1d4@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","Content-Transfer-Encoding":"8bit","In-Reply-To":"<b9db2aa7-8f84-41e6-8833-4ba8002ad1d4@ideasonboard.com>","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":35818,"web_url":"https://patchwork.libcamera.org/comment/35818/","msgid":"<20250913174549.GA29152@pendragon.ideasonboard.com>","date":"2025-09-13T17:45:49","subject":"Re: [PATCH v2 06/10] Documentation: Install API documentation within\n\tHTML directory","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"On Fri, Sep 12, 2025 at 08:04:17PM +0300, Laurent Pinchart wrote:\n> On Fri, Sep 12, 2025 at 01:24:59PM +0200, Barnabás Pőcze wrote:\n> > 2025. 09. 12. 1:01 keltezéssel, Laurent Pinchart írta:\n> > > The libcamera documentation is made of high-level documentation and\n> > > guides, written as ReStructuredText and compiled to HTML by Sphinx, and\n> > > API reference documentation, written as comments in the code and\n> > > compiled to HTML by Doxygen.\n> > > \n> > > Due to meson limitations that make it difficult to place output files in\n> > > subdirectories, the compilation process produces an html/ directory for\n> > > the Sphinx documentation, and api-html/ and internal-api-html/\n> > > directories for the Doxygen documentation. When deploying the\n> > > documentation to the libcamera.org website, the api-html and\n> > > internal-api-html/ directories are moved within html/ to make the\n> > > documentation self-contained.\n> > > \n> > > The Sphinx and Doxygen documentation link to each other. The links are\n> > > generated using relative paths, based on the directory hierarchy on the\n> > > website. This makes them broken when navigating the documentation in the\n> > > build tree or in the directory where libcamera is installed.\n> > > \n> > > Fix this by standardizing on the directories hierarchy of the website in\n> > > the build and install directories:\n> > > \n> > > - For the build directory, we can't easily build the Doxygen\n> > >    documentation in a subdirectory of the Sphinx documentation due to\n> > >    limitations of meson. Keep the existing output directories, and\n> > >    replace the html/api-html/ and html/internal-api-html/ placeholder\n> > >    directories with symlinks to the Doxygen output directories. This is\n> > >    handled by a small custom Sphinx extension.\n> > > \n> > > - For the install directory, install the Doxygen documentation to\n> > >    html/api-html/ and html/internal-api-html/. This overwrites the\n> > >    placeholders.\n> > \n> > Does it need a special meson option? Installation fails for me:\n> > \n> >    Tried to copy file /tmp/test/usr/share/doc/libcamera-0.5.2/html/internal-api but a directory of that name already exists.\n> \n> That's new to me :-( What meson version are you using ?\n\nThe behaviour of \"meson install\" changed with\n\ncommit 028abfe87c5d3b4dfe8a29472119aa1581beb215\nAuthor: Daan De Meyer <daan.j.demeyer@gmail.com>\nDate:   Fri Apr 11 11:57:12 2025 +0200\n\n    minstall: Don't treat symlinks to directories as directories in do_copydir()\n\nmerged in v1.8.0.\n\n*sigh*\n\nOh well. This was the last remaining part of this patch where I thought\nwe may relied a bit on luck, so I suppose it's good we caught it now.\n\nI'm tempted to install the doxygen documentation to api-html/ instead of\nhtml/api-html/, and keep the symlink. The install tree will be exactly\nthe same as the build tree. Packagers will always have the option to\nremove the symlink and move api-html/ to html/api-html/, like we'll do\nwhen publishing on the website.\n\n> > > Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n> > > ---\n> > > Changes since v1:\n> > > \n> > > - Split from \"[PATCH 4/6] documentation: Include API docs in the sphinx\n> > >    documentation\"\n> > > - Avoid copying the doxygen documentation\n> > > - Use a sphinx extension to generate symlinks\n> > > ---\n> > >   Documentation/conf.py.in                 | 26 +++++----\n> > >   Documentation/extensions/doxygen-link.py | 73 ++++++++++++++++++++++++\n> > >   Documentation/meson.build                | 19 ++++--\n> > >   3 files changed, 104 insertions(+), 14 deletions(-)\n> > >   create mode 100644 Documentation/extensions/doxygen-link.py\n> > > \n> > > diff --git a/Documentation/conf.py.in b/Documentation/conf.py.in\n> > > index 34fa3956f49e..5e0a7cff2984 100644\n> > > --- a/Documentation/conf.py.in\n> > > +++ b/Documentation/conf.py.in\n> > > @@ -9,14 +9,8 @@\n> > >   \n> > >   # -- Path setup --------------------------------------------------------------\n> > >   \n> > > -# If extensions (or modules to document with autodoc) are in another directory,\n> > > -# add these directories to sys.path here. If the directory is relative to the\n> > > -# documentation root, use os.path.abspath to make it absolute, like shown here.\n> > > -#\n> > > -# import os\n> > > -# import sys\n> > > -# sys.path.insert(0, os.path.abspath('.'))\n> > > -\n> > > +import sys\n> > > +sys.path.insert(0, \"@CURRENT_SRCDIR@/extensions\")\n> > >   \n> > >   # -- Project information -----------------------------------------------------\n> > >   \n> > > @@ -39,6 +33,7 @@ author = 'The libcamera documentation authors'\n> > >   extensions = [\n> > >       'sphinx.ext.graphviz',\n> > >       'sphinxcontrib.doxylink',\n> > > +    'doxygen-link',\n> > >   ]\n> > >   \n> > >   graphviz_output_format = 'svg'\n> > > @@ -75,14 +70,25 @@ pygments_style = None\n> > >   doxylink = {\n> > >       'doxy-pub': (\n> > >           '@TOP_BUILDDIR@/Documentation/api-html/tagfile.xml',\n> > > -        '../api-html/',\n> > > +        'api-html/',\n> > >       ),\n> > >       'doxy-int': (\n> > >           '@TOP_BUILDDIR@/Documentation/internal-api-html/tagfile.xml',\n> > > -        '../internal-api-html/',\n> > > +        'internal-api-html/',\n> > >       ),\n> > >   }\n> > >   \n> > > +doxygen_links = [\n> > > +    [\n> > > +        '@TOP_BUILDDIR@/Documentation/html/api-html',\n> > > +        '@TOP_BUILDDIR@/Documentation/api-html',\n> > > +    ],\n> > > +    [\n> > > +        '@TOP_BUILDDIR@/Documentation/html/internal-api-html',\n> > > +        '@TOP_BUILDDIR@/Documentation/internal-api-html',\n> > > +    ],\n> > > +]\n> > > +\n> > >   # -- Options for HTML output -------------------------------------------------\n> > >   \n> > >   # The theme to use for HTML and HTML Help pages.  See the documentation for\n> > > diff --git a/Documentation/extensions/doxygen-link.py b/Documentation/extensions/doxygen-link.py\n> > > new file mode 100644\n> > > index 000000000000..8b94a4dd7f0b\n> > > --- /dev/null\n> > > +++ b/Documentation/extensions/doxygen-link.py\n> > > @@ -0,0 +1,73 @@\n> > > +# SPDX-License-Identifier: GPL-2.0-or-later\n> > > +# Copyright (C) 2025, Ideas on Board Oy\n> > > +\n> > > +import contextlib\n> > > +import os\n> > > +import shutil\n> > > +from pathlib import Path\n> > > +from sphinx.util import logging\n> > > +\n> > > +__version__ = \"0.0.0\"\n> > > +\n> > > +logger = logging.getLogger(__name__)\n> > > +\n> > > +\n> > > +def on_config_inited(app, config):\n> > > +    entries = []\n> > > +\n> > > +    outdir = Path(app.outdir).absolute()\n> > > +\n> > > +    for index, items in enumerate(config.doxygen_links):\n> > > +        err_msg_prefix = f'Config variable `doxygen_links` contains invalid entry {index} (`{items}`)'\n> > > +\n> > > +        if len(items) != 2:\n> > > +            raise ValueError(f'{err_msg_prefix}: expected (path, target)')\n> > > +\n> > > +        path = Path(items[0]).absolute()\n> > > +        target = Path(items[1]).relative_to(path.parent, walk_up=True)\n> > > +\n> > > +        if not path.is_relative_to(outdir):\n> > > +            raise ValueError(f'{err_msg_prefix}: path `{items[0]}` is outside of output directory `{outdir}`')\n> > > +\n> > > +        entries.append([path, target])\n> > > +\n> > > +    config.doxygen_links = entries\n> > > +\n> > > +\n> > > +def on_builder_inited(app):\n> > > +    # Remove the symlinks if they exist, to avoid overwriting the index.html\n> > > +    # generated by Doxygen with the placeholder index from Sphinx.\n> > > +\n> > > +    for path, target in app.config.doxygen_links:\n> > > +        if path.is_symlink():\n> > > +            logger.info(f'Removing existing symlink {path}')\n> > > +            os.unlink(path)\n> > > +\n> > > +\n> > > +def on_build_finished(app, exc):\n> > > +    # Create the symlinks. Remove any existing placeholder directory\n> > > +    # recursively first.\n> > > +\n> > > +    if exc:\n> > > +        return\n> > > +\n> > > +    for path, target in app.config.doxygen_links:\n> > > +        logger.info(f'Creating symlink {path} -> {target}')\n> > > +\n> > > +        if path.is_dir():\n> > > +            shutil.rmtree(path)\n> > > +\n> > > +        os.symlink(target, path)\n> > > +\n> > > +\n> > > +def setup(app):\n> > > +    app.add_config_value('doxygen_links', [], 'env', frozenset({list, tuple}))\n> > > +    app.connect('config-inited', on_config_inited)\n> > > +    app.connect('builder-inited', on_builder_inited)\n> > > +    app.connect('build-finished', on_build_finished)\n> > > +\n> > > +    return {\n> > > +        \"version\": __version__,\n> > > +        \"parallel_read_safe\": True,\n> > > +        \"parallel_write_safe\": True,\n> > > +    }\n> > > diff --git a/Documentation/meson.build b/Documentation/meson.build\n> > > index f73407432fff..82d76b257b8c 100644\n> > > --- a/Documentation/meson.build\n> > > +++ b/Documentation/meson.build\n> > > @@ -12,6 +12,7 @@ dot = find_program('dot', required : get_option('documentation'))\n> > >   if doxygen.found() and dot.found()\n> > >       cdata = configuration_data()\n> > >       cdata.set('VERSION', 'v@0@'.format(libcamera_git_version))\n> > > +    cdata.set('CURRENT_SRCDIR', meson.current_source_dir())\n> > >       cdata.set('TOP_SRCDIR', meson.project_source_root())\n> > >       cdata.set('TOP_BUILDDIR', meson.project_build_root())\n> > >       cdata.set('OUTPUT_DIR', meson.current_build_dir())\n> > > @@ -89,7 +90,7 @@ if doxygen.found() and dot.found()\n> > >                                      output : 'api-html',\n> > >                                      command : [doxygen, doxyfile],\n> > >                                      install : true,\n> > > -                                   install_dir : doc_install_dir,\n> > > +                                   install_dir : doc_install_dir / 'html',\n> > >                                      install_tag : 'doc')\n> > >   \n> > >       # This is the internal documentation, which hard-codes a list of directories\n> > > @@ -109,7 +110,7 @@ if doxygen.found() and dot.found()\n> > >                                        output : 'internal-api-html',\n> > >                                        command : [doxygen, doxyfile],\n> > >                                        install : true,\n> > > -                                     install_dir : doc_install_dir,\n> > > +                                     install_dir : doc_install_dir / 'html',\n> > >                                        install_tag : 'doc-internal')\n> > >   endif\n> > >   \n> > > @@ -149,7 +150,11 @@ if sphinx.found()\n> > >       fs = import('fs')\n> > >       sphinx_conf_dir = fs.parent(sphinx_conf)\n> > >   \n> > > +    sphinx_env = environment()\n> > > +    sphinx_env.set('PYTHONDONTWRITEBYTECODE', '1')\n> > > +\n> > >       docs_sources = [\n> > > +        'api-html/index.rst',\n> > >           'camera-sensor-model.rst',\n> > >           'code-of-conduct.rst',\n> > >           'coding-style.rst',\n> > > @@ -164,6 +169,7 @@ if sphinx.found()\n> > >           'guides/pipeline-handler.rst',\n> > >           'guides/tracing.rst',\n> > >           'index.rst',\n> > > +        'internal-api-html/index.rst',\n> > >           'introduction.rst',\n> > >           'lens_driver_requirements.rst',\n> > >           'libcamera_architecture.rst',\n> > > @@ -183,10 +189,14 @@ if sphinx.found()\n> > >                     input : docs_sources,\n> > >                     output : 'html',\n> > >                     build_by_default : true,\n> > > +                  depend_files : [\n> > > +                      'extensions/doxygen-link.py',\n> > > +                  ],\n> > >                     depends : [\n> > >                         doxygen_public,\n> > >                         doxygen_internal,\n> > >                     ],\n> > > +                  env : sphinx_env,\n> > >                     install : true,\n> > >                     install_dir : doc_install_dir,\n> > >                     install_tag : 'doc')\n> > > @@ -195,7 +205,8 @@ if sphinx.found()\n> > >                     command : [sphinx, '-W', '-b', 'linkcheck',\n> > >                                '-c', sphinx_conf_dir,\n> > >                                meson.current_source_dir(), '@OUTPUT@'],\n> > > -                  build_always_stale : true,\n> > >                     input : docs_sources,\n> > > -                  output : 'linkcheck')\n> > > +                  output : 'linkcheck',\n> > > +                  build_always_stale : true,\n> > > +                  env : sphinx_env)","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id BB9FDC324E\n\tfor <parsemail@patchwork.libcamera.org>;\n\tSat, 13 Sep 2025 17:46:17 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 9545E6936F;\n\tSat, 13 Sep 2025 19:46:16 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id A1FBD69338\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSat, 13 Sep 2025 19:46:14 +0200 (CEST)","from pendragon.ideasonboard.com (81-175-209-231.bb.dnainternet.fi\n\t[81.175.209.231])\n\tby perceval.ideasonboard.com (Postfix) with UTF8SMTPSA id 4FEDD9CA;\n\tSat, 13 Sep 2025 19:44:58 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"sDZNEA2B\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1757785498;\n\tbh=mhVZDPzbqYzW1Y5L4xHSveiDl4RKNC4YCmFfolS0X9s=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=sDZNEA2BYPQ841VggCfjsV2VpDakrncYWDTbjn6SMH5OhMe2IWXpiXX55D1auekRC\n\t1it4/1Q66nKwDBDCvx+uW0zfmzEd4yoSYJITSmeVgBu/tiwFPppMa2COVoWtR66/h8\n\t4dqGOqGtEL7ELyd5qTR9sdupQeFqdS6cucZym32Q=","Date":"Sat, 13 Sep 2025 20:45:49 +0300","From":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","To":"=?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= <barnabas.pocze@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Subject":"Re: [PATCH v2 06/10] Documentation: Install API documentation within\n\tHTML directory","Message-ID":"<20250913174549.GA29152@pendragon.ideasonboard.com>","References":"<20250911230115.25335-1-laurent.pinchart@ideasonboard.com>\n\t<20250911230115.25335-7-laurent.pinchart@ideasonboard.com>\n\t<b9db2aa7-8f84-41e6-8833-4ba8002ad1d4@ideasonboard.com>\n\t<20250912170417.GA29172@pendragon.ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","Content-Transfer-Encoding":"8bit","In-Reply-To":"<20250912170417.GA29172@pendragon.ideasonboard.com>","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":35819,"web_url":"https://patchwork.libcamera.org/comment/35819/","msgid":"<20250914003012.GC25759@pendragon.ideasonboard.com>","date":"2025-09-14T00:30:12","subject":"Re: [PATCH v2 06/10] Documentation: Install API documentation within\n\tHTML directory","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"On Sat, Sep 13, 2025 at 08:45:49PM +0300, Laurent Pinchart wrote:\n> On Fri, Sep 12, 2025 at 08:04:17PM +0300, Laurent Pinchart wrote:\n> > On Fri, Sep 12, 2025 at 01:24:59PM +0200, Barnabás Pőcze wrote:\n> > > 2025. 09. 12. 1:01 keltezéssel, Laurent Pinchart írta:\n> > > > The libcamera documentation is made of high-level documentation and\n> > > > guides, written as ReStructuredText and compiled to HTML by Sphinx, and\n> > > > API reference documentation, written as comments in the code and\n> > > > compiled to HTML by Doxygen.\n> > > > \n> > > > Due to meson limitations that make it difficult to place output files in\n> > > > subdirectories, the compilation process produces an html/ directory for\n> > > > the Sphinx documentation, and api-html/ and internal-api-html/\n> > > > directories for the Doxygen documentation. When deploying the\n> > > > documentation to the libcamera.org website, the api-html and\n> > > > internal-api-html/ directories are moved within html/ to make the\n> > > > documentation self-contained.\n> > > > \n> > > > The Sphinx and Doxygen documentation link to each other. The links are\n> > > > generated using relative paths, based on the directory hierarchy on the\n> > > > website. This makes them broken when navigating the documentation in the\n> > > > build tree or in the directory where libcamera is installed.\n> > > > \n> > > > Fix this by standardizing on the directories hierarchy of the website in\n> > > > the build and install directories:\n> > > > \n> > > > - For the build directory, we can't easily build the Doxygen\n> > > >    documentation in a subdirectory of the Sphinx documentation due to\n> > > >    limitations of meson. Keep the existing output directories, and\n> > > >    replace the html/api-html/ and html/internal-api-html/ placeholder\n> > > >    directories with symlinks to the Doxygen output directories. This is\n> > > >    handled by a small custom Sphinx extension.\n> > > > \n> > > > - For the install directory, install the Doxygen documentation to\n> > > >    html/api-html/ and html/internal-api-html/. This overwrites the\n> > > >    placeholders.\n> > > \n> > > Does it need a special meson option? Installation fails for me:\n> > > \n> > >    Tried to copy file /tmp/test/usr/share/doc/libcamera-0.5.2/html/internal-api but a directory of that name already exists.\n> > \n> > That's new to me :-( What meson version are you using ?\n> \n> The behaviour of \"meson install\" changed with\n> \n> commit 028abfe87c5d3b4dfe8a29472119aa1581beb215\n> Author: Daan De Meyer <daan.j.demeyer@gmail.com>\n> Date:   Fri Apr 11 11:57:12 2025 +0200\n> \n>     minstall: Don't treat symlinks to directories as directories in do_copydir()\n> \n> merged in v1.8.0.\n> \n> *sigh*\n> \n> Oh well. This was the last remaining part of this patch where I thought\n> we may relied a bit on luck, so I suppose it's good we caught it now.\n> \n> I'm tempted to install the doxygen documentation to api-html/ instead of\n> html/api-html/, and keep the symlink. The install tree will be exactly\n> the same as the build tree. Packagers will always have the option to\n> remove the symlink and move api-html/ to html/api-html/, like we'll do\n> when publishing on the website.\n\nThat's not going to work. Meson 1.9.0 chokes on installing symlinks to\ndirectories, throwing an exception:\n\nTraceback (most recent call last):\n  File \"/home/laurent/src/tools/meson/mesonbuild/mesonmain.py\", line 193, in run\n    return options.run_func(options)\n           ~~~~~~~~~~~~~~~~^^^^^^^^^\n  File \"/home/laurent/src/tools/meson/mesonbuild/minstall.py\", line 888, in run\n    installer.do_install(datafilename)\n    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^\n  File \"/home/laurent/src/tools/meson/mesonbuild/minstall.py\", line 563, in do_install\n    self.install_targets(d, dm, destdir, fullprefix)\n    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/home/laurent/src/tools/meson/mesonbuild/minstall.py\", line 782, in install_targets\n    self.do_copydir(d, fname, outname, None, install_mode, dm)\n    ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/home/laurent/src/tools/meson/mesonbuild/minstall.py\", line 537, in do_copydir\n    self.do_copyfile(abs_src, abs_dst, follow_symlinks=follow_symlinks)\n    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/home/laurent/src/tools/meson/mesonbuild/minstall.py\", line 437, in do_copyfile\n    self.copy2(from_file, to_file, follow_symlinks=follow_symlinks)\n    ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/home/laurent/src/tools/meson/mesonbuild/minstall.py\", line 336, in copy2\n    shutil.copy2(*args, **kwargs)\n    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3.13/shutil.py\", line 468, in copy2\n    copyfile(src, dst, follow_symlinks=follow_symlinks)\n    ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3.13/shutil.py\", line 260, in copyfile\n    with open(src, 'rb') as fsrc:\n         ~~~~^^^^^^^^^^^\nIsADirectoryError: [Errno 21] Is a directory: '/home/laurent/src/iob/oss/libcamera/libcamera/build/x86-gcc-13/Documentation/html/internal-api'\n\nThat may be a bug in meson, but the fact that it's present in both\nv1.8.0 and v1.9.0 means that it is probably too dangerous to ignore,\neven if we can get it fixed. So if we want to work around the problem,\nwe can't have symlinks to directories, which brings us back to square\none.\n\nAaargghhhhh.\n\n*deep breath*\n\nAaargghhhhh.\n\nOne option is to keep the current output directories, with the doxygen\nand sphinx documentation side by side, and override that when publishing\nto the website. That will require overriding the doxylink configuration\noption in Documentation/conf.py.  We could patch the file manually in\nthe website deployment scripts, or add a meson option.\n\nThere will then also be the issue of links from doxygen to sphinx, as\nthey currently use hardcoded paths.\n\nI'm starting to think we may be better off giving up on this, and\nkeeping sphinx and doxygen documentation side by side on the website as\nwell.\n\n> > > > Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n> > > > ---\n> > > > Changes since v1:\n> > > > \n> > > > - Split from \"[PATCH 4/6] documentation: Include API docs in the sphinx\n> > > >    documentation\"\n> > > > - Avoid copying the doxygen documentation\n> > > > - Use a sphinx extension to generate symlinks\n> > > > ---\n> > > >   Documentation/conf.py.in                 | 26 +++++----\n> > > >   Documentation/extensions/doxygen-link.py | 73 ++++++++++++++++++++++++\n> > > >   Documentation/meson.build                | 19 ++++--\n> > > >   3 files changed, 104 insertions(+), 14 deletions(-)\n> > > >   create mode 100644 Documentation/extensions/doxygen-link.py\n> > > > \n> > > > diff --git a/Documentation/conf.py.in b/Documentation/conf.py.in\n> > > > index 34fa3956f49e..5e0a7cff2984 100644\n> > > > --- a/Documentation/conf.py.in\n> > > > +++ b/Documentation/conf.py.in\n> > > > @@ -9,14 +9,8 @@\n> > > >   \n> > > >   # -- Path setup --------------------------------------------------------------\n> > > >   \n> > > > -# If extensions (or modules to document with autodoc) are in another directory,\n> > > > -# add these directories to sys.path here. If the directory is relative to the\n> > > > -# documentation root, use os.path.abspath to make it absolute, like shown here.\n> > > > -#\n> > > > -# import os\n> > > > -# import sys\n> > > > -# sys.path.insert(0, os.path.abspath('.'))\n> > > > -\n> > > > +import sys\n> > > > +sys.path.insert(0, \"@CURRENT_SRCDIR@/extensions\")\n> > > >   \n> > > >   # -- Project information -----------------------------------------------------\n> > > >   \n> > > > @@ -39,6 +33,7 @@ author = 'The libcamera documentation authors'\n> > > >   extensions = [\n> > > >       'sphinx.ext.graphviz',\n> > > >       'sphinxcontrib.doxylink',\n> > > > +    'doxygen-link',\n> > > >   ]\n> > > >   \n> > > >   graphviz_output_format = 'svg'\n> > > > @@ -75,14 +70,25 @@ pygments_style = None\n> > > >   doxylink = {\n> > > >       'doxy-pub': (\n> > > >           '@TOP_BUILDDIR@/Documentation/api-html/tagfile.xml',\n> > > > -        '../api-html/',\n> > > > +        'api-html/',\n> > > >       ),\n> > > >       'doxy-int': (\n> > > >           '@TOP_BUILDDIR@/Documentation/internal-api-html/tagfile.xml',\n> > > > -        '../internal-api-html/',\n> > > > +        'internal-api-html/',\n> > > >       ),\n> > > >   }\n> > > >   \n> > > > +doxygen_links = [\n> > > > +    [\n> > > > +        '@TOP_BUILDDIR@/Documentation/html/api-html',\n> > > > +        '@TOP_BUILDDIR@/Documentation/api-html',\n> > > > +    ],\n> > > > +    [\n> > > > +        '@TOP_BUILDDIR@/Documentation/html/internal-api-html',\n> > > > +        '@TOP_BUILDDIR@/Documentation/internal-api-html',\n> > > > +    ],\n> > > > +]\n> > > > +\n> > > >   # -- Options for HTML output -------------------------------------------------\n> > > >   \n> > > >   # The theme to use for HTML and HTML Help pages.  See the documentation for\n> > > > diff --git a/Documentation/extensions/doxygen-link.py b/Documentation/extensions/doxygen-link.py\n> > > > new file mode 100644\n> > > > index 000000000000..8b94a4dd7f0b\n> > > > --- /dev/null\n> > > > +++ b/Documentation/extensions/doxygen-link.py\n> > > > @@ -0,0 +1,73 @@\n> > > > +# SPDX-License-Identifier: GPL-2.0-or-later\n> > > > +# Copyright (C) 2025, Ideas on Board Oy\n> > > > +\n> > > > +import contextlib\n> > > > +import os\n> > > > +import shutil\n> > > > +from pathlib import Path\n> > > > +from sphinx.util import logging\n> > > > +\n> > > > +__version__ = \"0.0.0\"\n> > > > +\n> > > > +logger = logging.getLogger(__name__)\n> > > > +\n> > > > +\n> > > > +def on_config_inited(app, config):\n> > > > +    entries = []\n> > > > +\n> > > > +    outdir = Path(app.outdir).absolute()\n> > > > +\n> > > > +    for index, items in enumerate(config.doxygen_links):\n> > > > +        err_msg_prefix = f'Config variable `doxygen_links` contains invalid entry {index} (`{items}`)'\n> > > > +\n> > > > +        if len(items) != 2:\n> > > > +            raise ValueError(f'{err_msg_prefix}: expected (path, target)')\n> > > > +\n> > > > +        path = Path(items[0]).absolute()\n> > > > +        target = Path(items[1]).relative_to(path.parent, walk_up=True)\n> > > > +\n> > > > +        if not path.is_relative_to(outdir):\n> > > > +            raise ValueError(f'{err_msg_prefix}: path `{items[0]}` is outside of output directory `{outdir}`')\n> > > > +\n> > > > +        entries.append([path, target])\n> > > > +\n> > > > +    config.doxygen_links = entries\n> > > > +\n> > > > +\n> > > > +def on_builder_inited(app):\n> > > > +    # Remove the symlinks if they exist, to avoid overwriting the index.html\n> > > > +    # generated by Doxygen with the placeholder index from Sphinx.\n> > > > +\n> > > > +    for path, target in app.config.doxygen_links:\n> > > > +        if path.is_symlink():\n> > > > +            logger.info(f'Removing existing symlink {path}')\n> > > > +            os.unlink(path)\n> > > > +\n> > > > +\n> > > > +def on_build_finished(app, exc):\n> > > > +    # Create the symlinks. Remove any existing placeholder directory\n> > > > +    # recursively first.\n> > > > +\n> > > > +    if exc:\n> > > > +        return\n> > > > +\n> > > > +    for path, target in app.config.doxygen_links:\n> > > > +        logger.info(f'Creating symlink {path} -> {target}')\n> > > > +\n> > > > +        if path.is_dir():\n> > > > +            shutil.rmtree(path)\n> > > > +\n> > > > +        os.symlink(target, path)\n> > > > +\n> > > > +\n> > > > +def setup(app):\n> > > > +    app.add_config_value('doxygen_links', [], 'env', frozenset({list, tuple}))\n> > > > +    app.connect('config-inited', on_config_inited)\n> > > > +    app.connect('builder-inited', on_builder_inited)\n> > > > +    app.connect('build-finished', on_build_finished)\n> > > > +\n> > > > +    return {\n> > > > +        \"version\": __version__,\n> > > > +        \"parallel_read_safe\": True,\n> > > > +        \"parallel_write_safe\": True,\n> > > > +    }\n> > > > diff --git a/Documentation/meson.build b/Documentation/meson.build\n> > > > index f73407432fff..82d76b257b8c 100644\n> > > > --- a/Documentation/meson.build\n> > > > +++ b/Documentation/meson.build\n> > > > @@ -12,6 +12,7 @@ dot = find_program('dot', required : get_option('documentation'))\n> > > >   if doxygen.found() and dot.found()\n> > > >       cdata = configuration_data()\n> > > >       cdata.set('VERSION', 'v@0@'.format(libcamera_git_version))\n> > > > +    cdata.set('CURRENT_SRCDIR', meson.current_source_dir())\n> > > >       cdata.set('TOP_SRCDIR', meson.project_source_root())\n> > > >       cdata.set('TOP_BUILDDIR', meson.project_build_root())\n> > > >       cdata.set('OUTPUT_DIR', meson.current_build_dir())\n> > > > @@ -89,7 +90,7 @@ if doxygen.found() and dot.found()\n> > > >                                      output : 'api-html',\n> > > >                                      command : [doxygen, doxyfile],\n> > > >                                      install : true,\n> > > > -                                   install_dir : doc_install_dir,\n> > > > +                                   install_dir : doc_install_dir / 'html',\n> > > >                                      install_tag : 'doc')\n> > > >   \n> > > >       # This is the internal documentation, which hard-codes a list of directories\n> > > > @@ -109,7 +110,7 @@ if doxygen.found() and dot.found()\n> > > >                                        output : 'internal-api-html',\n> > > >                                        command : [doxygen, doxyfile],\n> > > >                                        install : true,\n> > > > -                                     install_dir : doc_install_dir,\n> > > > +                                     install_dir : doc_install_dir / 'html',\n> > > >                                        install_tag : 'doc-internal')\n> > > >   endif\n> > > >   \n> > > > @@ -149,7 +150,11 @@ if sphinx.found()\n> > > >       fs = import('fs')\n> > > >       sphinx_conf_dir = fs.parent(sphinx_conf)\n> > > >   \n> > > > +    sphinx_env = environment()\n> > > > +    sphinx_env.set('PYTHONDONTWRITEBYTECODE', '1')\n> > > > +\n> > > >       docs_sources = [\n> > > > +        'api-html/index.rst',\n> > > >           'camera-sensor-model.rst',\n> > > >           'code-of-conduct.rst',\n> > > >           'coding-style.rst',\n> > > > @@ -164,6 +169,7 @@ if sphinx.found()\n> > > >           'guides/pipeline-handler.rst',\n> > > >           'guides/tracing.rst',\n> > > >           'index.rst',\n> > > > +        'internal-api-html/index.rst',\n> > > >           'introduction.rst',\n> > > >           'lens_driver_requirements.rst',\n> > > >           'libcamera_architecture.rst',\n> > > > @@ -183,10 +189,14 @@ if sphinx.found()\n> > > >                     input : docs_sources,\n> > > >                     output : 'html',\n> > > >                     build_by_default : true,\n> > > > +                  depend_files : [\n> > > > +                      'extensions/doxygen-link.py',\n> > > > +                  ],\n> > > >                     depends : [\n> > > >                         doxygen_public,\n> > > >                         doxygen_internal,\n> > > >                     ],\n> > > > +                  env : sphinx_env,\n> > > >                     install : true,\n> > > >                     install_dir : doc_install_dir,\n> > > >                     install_tag : 'doc')\n> > > > @@ -195,7 +205,8 @@ if sphinx.found()\n> > > >                     command : [sphinx, '-W', '-b', 'linkcheck',\n> > > >                                '-c', sphinx_conf_dir,\n> > > >                                meson.current_source_dir(), '@OUTPUT@'],\n> > > > -                  build_always_stale : true,\n> > > >                     input : docs_sources,\n> > > > -                  output : 'linkcheck')\n> > > > +                  output : 'linkcheck',\n> > > > +                  build_always_stale : true,\n> > > > +                  env : sphinx_env)","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 5B57BC328C\n\tfor <parsemail@patchwork.libcamera.org>;\n\tSun, 14 Sep 2025 00:30:42 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 5EFE36936F;\n\tSun, 14 Sep 2025 02:30:41 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 5824F62C39\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSun, 14 Sep 2025 02:30:39 +0200 (CEST)","from pendragon.ideasonboard.com (81-175-209-231.bb.dnainternet.fi\n\t[81.175.209.231])\n\tby perceval.ideasonboard.com (Postfix) with UTF8SMTPSA id DEA79C67;\n\tSun, 14 Sep 2025 02:29:22 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"PQNk3XQW\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1757809763;\n\tbh=X5WLOlYgtua0M43tFHo4tBxQgcMl/Uut701TapGUK1E=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=PQNk3XQWi02FeQHaUSyMo5pw0ufWXHXrzesgfpxIQzoNCQt3C0EO71KkqiwfeQkUs\n\tD4wxkk+w7/rccJxX12brEgRKhEDmjHhzbXeVpxykzzPWXr6Rn5os46rznsZ5zRjdL9\n\t/448FCtW7fPCCaJxAAmL0W+eMDh8Gyv4A5RPZr54=","Date":"Sun, 14 Sep 2025 03:30:12 +0300","From":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","To":"=?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= <barnabas.pocze@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Subject":"Re: [PATCH v2 06/10] Documentation: Install API documentation within\n\tHTML directory","Message-ID":"<20250914003012.GC25759@pendragon.ideasonboard.com>","References":"<20250911230115.25335-1-laurent.pinchart@ideasonboard.com>\n\t<20250911230115.25335-7-laurent.pinchart@ideasonboard.com>\n\t<b9db2aa7-8f84-41e6-8833-4ba8002ad1d4@ideasonboard.com>\n\t<20250912170417.GA29172@pendragon.ideasonboard.com>\n\t<20250913174549.GA29152@pendragon.ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","Content-Transfer-Encoding":"8bit","In-Reply-To":"<20250913174549.GA29152@pendragon.ideasonboard.com>","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]