docs(api_doc_builder.py): fix frail assumptions in API-page generation (#8694)

This commit is contained in:
Victor Wheeler
2025-08-15 14:24:43 -06:00
committed by GitHub
parent b09507b49c
commit 1af3da0139
20 changed files with 169 additions and 54 deletions
+5 -5
View File
@@ -815,11 +815,11 @@ INPUT_ENCODING = UTF-8
# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, # *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf. # *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf.
FILE_PATTERNS = lv*.h \ FILE_PATTERNS = lv*.h \
*.hh \ lv*.hh \
*.hxx \ lv*.hxx \
*.hpp \ lv*.hpp \
*.h++ lv*.h++
# The RECURSIVE tag can be used to specify whether or not subdirectories should # The RECURSIVE tag can be used to specify whether or not subdirectories should
# be searched for input files as well. # be searched for input files as well.
+130 -23
View File
@@ -7,6 +7,46 @@ Uses doxygen_xml.py module to:
- Prep and run Doxygen - Prep and run Doxygen
- make Doxygen XML output available, and - make Doxygen XML output available, and
- make Doxygen-documented symbols from the C code available. - make Doxygen-documented symbols from the C code available.
Because these 3 files are acceptable in a C project:
1. ./path/to/one/aaa.c
2. ./path/to/one/aaa.h
3. ./path/to/one/aaa.cpp
4. ./path/to/one/aaa.hpp
5. ./path/to/another/aaa.h
we see that:
- duplicate filename stems ('aaa' above) are acceptable, and
- they must not only be differentiated by their extensions, but also by their path.
On the other hand, Sphinx link reference names for :ref:`link_ref_name` link
references must be unique throughout a document project. Since API pages are
generated from C source files, to make this effective, they must include:
A. at least part of the path,
B. filename stem, and
C. extension
in the link reference names. Prior to 11-Aug-2025, link reference names were formed
using ONLY the filename stem. This created a conflict when an example of #4 above
appeared in the LVGL code, and caused a doc-build failure because the API-page link
reference names to #2 and #4 were identical, and Sphinx (correctly) does not allow
that. So after 11-Aug-2025, these link reference names are now differentiated all 3
of A, B and C above.
.. note::
Since they are generated and dealt with programmatically, using the full path
[below the docs source directory] will not be a problem. Even though it makes
the names long, they never have to be hand-typed.
Likewise, API-page ``.rst`` filename stems also need to be unique within the directory
they are in, so the stems must contain the C source file's filename stem plus the
extension in them, in order to be unique in that directory, since they *must* end in
``.rst``.
""" """
import os import os
import re import re
@@ -16,6 +56,17 @@ from announce import *
old_html_files = {} old_html_files = {}
EMIT_WARNINGS = True EMIT_WARNINGS = True
rst_section_line_char = '=' rst_section_line_char = '='
on_windows = (( os.name == 'nt' ))
# `doxy_src_file_ext_list` must
# - match the extensions in the Doxyfile FILE_PATTERNS list, and
# - be in lower case.
doxy_src_file_ext_list = [
'.h',
'.hh',
'.hxx',
'.hpp',
'.h++'
]
# Multi-line match ``API + newline + \*\*\* + whitespace``. # Multi-line match ``API + newline + \*\*\* + whitespace``.
# NB: the ``\s*`` at the end forces the regex to match the whitespace # NB: the ``\s*`` at the end forces the regex to match the whitespace
@@ -34,7 +85,7 @@ _re_api_startswith = re.compile(r'(?mi)^\s*\.\.\s+API\s+startswith:\s*([\w,\s]+)
_re_multi_line_comma_sep = re.compile(r'(?m)[,\s]+') _re_multi_line_comma_sep = re.compile(r'(?m)[,\s]+')
# Regex to identify editor-added hyperlinks: :ref:`lv_obj_h` # Regex to identify editor-added hyperlinks: :ref:`lv_obj_h`
_re_editor_added_hyperlink = re.compile(r'^\s*:ref:`(\w+)`') _re_editor_added_hyperlink = re.compile(r'^\s*:ref:`([\w/\.]+)`')
# Separator to mark place where this script added hyperlinks. # Separator to mark place where this script added hyperlinks.
_auto_gen_sep = '.. Autogenerated' _auto_gen_sep = '.. Autogenerated'
@@ -140,7 +191,15 @@ def _conditionally_add_hyperlink(obj, genned_link_set: set, exclude_set: set):
:return: :return:
""" """
if obj.file_name is not None: if obj.file_name is not None:
link_name = os.path.basename(obj.file_name).replace('.', '_') # The link name should look like this: draw/sw/blend/lv_draw_sw_blend.h
# including when running under Windows.
# `obj.file_name` contains a path that looks like this:
# '/path/to/lvgl/src/draw/sw/blend/lv_draw_sw_blend.h'
# Note: we can't use 'lvgl/src' because there is no guarantee
# that the user's LVGL repo directory will be called that.
src_root = '/src/'
i = obj.file_name.index(src_root) + len(src_root)
link_name = obj.file_name[i:]
if link_name not in genned_link_set: if link_name not in genned_link_set:
if link_name not in exclude_set: if link_name not in exclude_set:
genned_link_set.add(link_name) genned_link_set.add(link_name)
@@ -191,7 +250,7 @@ def _add_exact_matches(symbols: [str], genned_link_set, editor_link_set):
def _hyperlink_sort_value(init_value: str): def _hyperlink_sort_value(init_value: str):
if init_value.endswith('_h'): if init_value.endswith('.h'):
result = init_value[:-2] result = init_value[:-2]
else: else:
result = init_value result = init_value
@@ -544,6 +603,12 @@ def _add_hyperlinks_to_eligible_files(intermediate_dir: str,
f.write(data.encode('utf-8')) f.write(data.encode('utf-8'))
def _extended_stem_from_path(path: str) -> str:
filename = os.path.basename(path)
stem, ext = os.path.splitext(filename)
return stem + '_' + ext[1:] # Remove leading '.' from ext.
def _create_rst_files_for_dir(src_root_dir_len: int, def _create_rst_files_for_dir(src_root_dir_len: int,
src_dir_bep: str, src_dir_bep: str,
elig_h_files: [str], elig_h_files: [str],
@@ -558,14 +623,40 @@ def _create_rst_files_for_dir(src_root_dir_len: int,
- add reference "sub_dir_name/index" in `index.rst`. - add reference "sub_dir_name/index" in `index.rst`.
:param src_root_dir_len: Length of source-root path string, used with `out_root_dir` to build paths :param src_root_dir_len: Length of source-root path string, used with `out_root_dir` to build paths
Example: 17
:param src_dir_bep: Directory currently *being processed* :param src_dir_bep: Directory currently *being processed*
:param elig_h_files: Eligible `.h` files directly contained in `src_dir_bep` Example: '/path/to/lvgl/src/draw/sw/blend'
:param elig_sub_dirs: List of sub-dirs that contained eligible `.h` files :param elig_h_files: Eligible `.h*` files directly contained in `src_dir_bep`
Example: [] or
[
'/path/to/lvgl/src/draw/sw/blend/lv_draw_sw_blend.h',
'/path/to/lvgl/src/draw/sw/blend/lv_draw_sw_blend_private.h',
'/path/to/lvgl/src/draw/sw/blend/lv_draw_sw_blend_to_al88.h',
'/path/to/lvgl/src/draw/sw/blend/lv_draw_sw_blend_to_argb8888.h',
'/path/to/lvgl/src/draw/sw/blend/lv_draw_sw_blend_to_argb8888_premultiplied.h',
'/path/to/lvgl/src/draw/sw/blend/lv_draw_sw_blend_to_i1.h',
'/path/to/lvgl/src/draw/sw/blend/lv_draw_sw_blend_to_l8.h',
'/path/to/lvgl/src/draw/sw/blend/lv_draw_sw_blend_to_rgb565.h',
'/path/to/lvgl/src/draw/sw/blend/lv_draw_sw_blend_to_rgb565_swapped.h',
'/path/to/lvgl/src/draw/sw/blend/lv_draw_sw_blend_to_rgb888.h'
]
:param elig_sub_dirs: List of sub-dirs that contained eligible `.h*` files
Example: [] or
[
'/path/to/lvgl/src/draw/sw/blend/arm2d',
'/path/to/lvgl/src/draw/sw/blend/helium',
'/path/to/lvgl/src/draw/sw/blend/neon'
]
:param out_root_dir: Root of output directory, used with to build paths. :param out_root_dir: Root of output directory, used with to build paths.
Example: '/path/to/lvgl/src/docs/intermediate/API'
:return: n/a :return: n/a
""" """
indent = ' ' indent = ' '
# Keep only sub-path under 'lvgl/src/'.
# Example: 'draw/sw/blend'
sub_path = src_dir_bep[src_root_dir_len:] sub_path = src_dir_bep[src_root_dir_len:]
# Build full path to output directory for 'index.rst' file.
# Example: '/path/to/lvgl/src/docs/intermediate/API/draw/sw/blend'
out_dir = str(os.path.join(out_root_dir, sub_path)) out_dir = str(os.path.join(out_root_dir, sub_path))
# Ensure dir exists. Multiple dirs MAY have to be created # Ensure dir exists. Multiple dirs MAY have to be created
@@ -591,9 +682,8 @@ def _create_rst_files_for_dir(src_root_dir_len: int,
# One entry per `.rst` file # One entry per `.rst` file
for h_file in elig_h_files: for h_file in elig_h_files:
filename = os.path.basename(h_file) extended_stem = _extended_stem_from_path(h_file)
stem = os.path.splitext(filename)[0] f.write(indent + extended_stem + '\n')
f.write(indent + stem + '\n')
# One entry per eligible subdirectory. # One entry per eligible subdirectory.
for sub_dir in elig_sub_dirs: for sub_dir in elig_sub_dirs:
@@ -603,14 +693,28 @@ def _create_rst_files_for_dir(src_root_dir_len: int,
# One .rst file per h_file # One .rst file per h_file
for h_file in elig_h_files: for h_file in elig_h_files:
filename = os.path.basename(h_file) filename = os.path.basename(h_file)
stem = os.path.splitext(filename)[0] extended_stem = _extended_stem_from_path(h_file)
rst_file = os.path.join(out_dir, stem + '.rst') rst_file = os.path.join(out_dir, extended_stem + '.rst')
html_file = os.path.join(sub_path, stem + '.html') html_file = os.path.join(sub_path, extended_stem + '.html')
old_html_files[stem] = html_file old_html_files[extended_stem] = html_file
with open(rst_file, 'w') as f: with open(rst_file, 'w') as f:
# Sphinx link target. # Sphinx link target.
f.write(f'.. _{stem}_h:\n\n') # The link name should look like this: draw/sw/blend/lv_draw_sw_blend.h
# including when running under Windows.
if on_windows:
link_name_path = sub_path.replace(os.sep, '/')
else:
link_name_path = sub_path
if len(link_name_path) > 0:
link_name = link_name_path + '/' + filename
else:
link_name = filename
line = f'.. _{link_name}:\n\n'
f.write(line)
# Doc title. # Doc title.
section_line = (rst_section_line_char * len(filename)) + '\n' section_line = (rst_section_line_char * len(filename)) + '\n'
f.write(section_line) f.write(section_line)
@@ -631,7 +735,7 @@ def _recursively_create_api_rst_files(depth: int,
recursively for subdirectories below it. ("bep" = being processed.) recursively for subdirectories below it. ("bep" = being processed.)
Eligible Eligible
An input file (e.g. `.h` or `.c`) file is eligible if Doxygen generated An input file (e.g. `.h*`) file is eligible if Doxygen generated
documentation for it. The combination of these configuration items in documentation for it. The combination of these configuration items in
the Doxyfile: the Doxyfile:
@@ -646,12 +750,12 @@ def _recursively_create_api_rst_files(depth: int,
file depends upon whether any eligible files were recursively file depends upon whether any eligible files were recursively
found within it. And that isn't known until this function finishes found within it. And that isn't known until this function finishes
(recursively) processing a directory and returns the number of (recursively) processing a directory and returns the number of
eligible `.h` files found in its subdirectory tree. Thus, the steps eligible `.h*` files found in its subdirectory tree. Thus, the steps
taken within are: taken within are:
- Discover all eligible `.h` files directly contained in `src_dir_bep`. - Discover all eligible `.h*` files directly contained in `src_dir_bep`.
- Recursively do the same for each subdirectory, adding the returned - Recursively do the same for each subdirectory, adding the returned
count of eligible `.h` files to the sum (`elig_h_file_count`). count of eligible `.h*` files to the sum (`elig_h_file_count`).
- If `elig_h_file_count > 0`: - If `elig_h_file_count > 0`:
- call _create_rst_files_for_dir() to generate appropriate - call _create_rst_files_for_dir() to generate appropriate
`.rst` files for this directory. `.rst` files for this directory.
@@ -665,9 +769,9 @@ def _recursively_create_api_rst_files(depth: int,
:param src_root_len: Length of source-root path :param src_root_len: Length of source-root path
:param src_dir_bep: Source directory *being processed* :param src_dir_bep: Source directory *being processed*
:param out_root_dir: Output root directory (used to build output paths) :param out_root_dir: Output root directory (used to build output paths)
:return: Number of `.h` files encountered (so caller knows :return: Number of `.h*` files encountered (so caller knows
whether that directory recursively held any whether that directory recursively held any
eligible `.h` files, to know whether to include eligible `.h*` files, to know whether to include
"subdir/index" in caller's local `index.rst` file). "subdir/index" in caller's local `index.rst` file).
""" """
elig_h_files = [] elig_h_files = []
@@ -686,7 +790,10 @@ def _recursively_create_api_rst_files(depth: int,
if os.path.isdir(path_bep): if os.path.isdir(path_bep):
sub_dirs.append(path_bep) # Add to sub-dir list. sub_dirs.append(path_bep) # Add to sub-dir list.
else: else:
if dir_item.lower().endswith('.h'): _, ext = os.path.splitext(dir_item)
# `ext` is converted to lower case so that any incidental case change
# in the extension on Windows will not break this algorithm.
if ext.lower() in doxy_src_file_ext_list:
eligible = (dir_item in doxygen_xml.files) eligible = (dir_item in doxygen_xml.files)
if eligible: if eligible:
elig_h_files.append(path_bep) # Add file to list. elig_h_files.append(path_bep) # Add file to list.
@@ -711,7 +818,7 @@ def _recursively_create_api_rst_files(depth: int,
elig_sub_dirs.sort() elig_sub_dirs.sort()
elig_h_files.sort() elig_h_files.sort()
# Create index.rst plus .RST files for any .H file directly in in dir. # Create index.rst plus .RST files for any .H* file directly in in dir.
_create_rst_files_for_dir(src_root_len, _create_rst_files_for_dir(src_root_len,
src_dir_bep, src_dir_bep,
elig_h_files, elig_h_files,
@@ -723,9 +830,9 @@ def _recursively_create_api_rst_files(depth: int,
def create_api_rst_files(src_root_dir: str, out_root_dir: str): def create_api_rst_files(src_root_dir: str, out_root_dir: str):
""" """
Create `.rst` files for API pages based on the `.h` files found Create `.rst` files for API pages based on the `.h*` files found
in a tree-walk of `a_src_root` and the current contents of the in a tree-walk of `a_src_root` and the current contents of the
`doxygen_xml.files` dictionary (used to filter out `.h` files that `doxygen_xml.files` dictionary (used to filter out `.h*` files that
Doxygen generated no documentation for). Output the `.rst` files Doxygen generated no documentation for). Output the `.rst` files
into `out_root_dir` mirroring the `a_src_root` directory structure. into `out_root_dir` mirroring the `a_src_root` directory structure.
+1 -1
View File
@@ -743,7 +743,7 @@ def run(args):
if debugging_breathe: if debugging_breathe:
from sphinx.cmd.build import main as sphinx_build from sphinx.cmd.build import main as sphinx_build
# Don't allow parallel processing while debugging (the '-j' arg is removed). # Don't allow parallel processing while debugging (the '-j' arg is removed).
sphinx_args = ['-M', 'html', f'{src}', f'{dst}', '-D', f'version={ver}'] sphinx_args = ['-M', 'html', f'{src}', f'{dst}']
if len(env_opt) > 0: if len(env_opt) > 0:
sphinx_args.append(f'{env_opt}') sphinx_args.append(f'{env_opt}')
+6
View File
@@ -1606,6 +1606,7 @@ class DoxygenXml(object):
'LV_ATTRIBUTE_FAST_MEM=', 'LV_ATTRIBUTE_FAST_MEM=',
'LV_ATTRIBUTE_EXTERN_DATA=', 'LV_ATTRIBUTE_EXTERN_DATA=',
'LV_FORMAT_ATTRIBUTE(fmt,va)=', 'LV_FORMAT_ATTRIBUTE(fmt,va)=',
'FASTGLTF_EXPORT=',
] ]
cfg.set('PREDEFINED', predefined_symbols) cfg.set('PREDEFINED', predefined_symbols)
@@ -1631,9 +1632,14 @@ class DoxygenXml(object):
full_path = os.path.join(lvgl_src_dir, osal_dir, osal_h) full_path = os.path.join(lvgl_src_dir, osal_dir, osal_h)
exclude_paths.append(full_path) exclude_paths.append(full_path)
# Exclude as a workaround for Breathe parsing problems.
full_path = os.path.join(lvgl_src_dir, 'core', 'lv_obj_property.h') full_path = os.path.join(lvgl_src_dir, 'core', 'lv_obj_property.h')
exclude_paths.append(full_path) exclude_paths.append(full_path)
# Exclude GLTF templates that Breathe appears to not know how to parse.
full_path = os.path.join(lvgl_src_dir, 'libs', 'gltf', 'fastgltf', 'lv_fastgltf.hpp')
exclude_paths.append(full_path)
cfg.set('EXCLUDE', exclude_paths) cfg.set('EXCLUDE', exclude_paths)
# Include TAGFILES if requested. # Include TAGFILES if requested.
+3 -2
View File
@@ -38,9 +38,10 @@ if "%SPHINXOPTS%" == "" (
rem python ./src/lvgl_version.py >_version_temp.txt rem python ./src/lvgl_version.py >_version_temp.txt
rem set /p VER=<_version_temp.txt rem set /p VER=<_version_temp.txt
rem del _version_temp.txt rem del _version_temp.txt
for /F %%v in ('python lvgl_version.py') do set VER=%%v for /F %%v in ('python src\lvgl_version.py') do set VER=%%v
echo VERSION [!VER!] echo VERSION [!VER!]
set SPHINXOPTS=-D version="!VER!" -j 4 rem set SPHINXOPTS=-D version="!VER!" -j 4
set SPHINXOPTS=-j 4
set VER= set VER=
) )
+2 -1
View File
@@ -117,7 +117,8 @@ by getting the Y coordinate of a child.
int32_t y_end = lv_obj_get_y(child); int32_t y_end = lv_obj_get_y(child);
if(y_start + 100 != y_end) fail(); if(y_start + 100 != y_end) fail();
Please refer to :ref:`lv_test_indev_h` for the list of supported input device emulation functions. Please refer to :ref:`others/test/lv_test_indev.h` for the list of supported input
device emulation functions.
Screenshot Comparison Screenshot Comparison
--------------------- ---------------------
@@ -159,4 +159,5 @@ See :ref:`indev_creation` to see how to do this.
API API
*** ***
:ref:`lv_tick_h`
.. API startswith: lv_tick_
@@ -65,6 +65,6 @@ Examples
API API
*** ***
:ref:`lv_draw_sw_arm2d_h` :ref:`draw/sw/arm2d/lv_draw_sw_arm2d.h`
:ref:`lv_blend_arm2d_h` :ref:`draw/sw/blend/arm2d/lv_blend_arm2d.h`
@@ -5,4 +5,4 @@ Espressif Pixel Processing Accelerator
API API
*** ***
:ref:`lv_draw_ppa_h` .. API startswith: lv_draw_ppa_
@@ -109,6 +109,6 @@ See the LVGL :ref:`DMA2D support <dma2d>`.
API API
*** ***
:ref:`lv_draw_nema_gfx_h` .. API startswith: lv_draw_nema_
:ref:`lv_draw_nema_gfx_utils_h` .. API startswith: lv_nemagfx_
@@ -5,4 +5,4 @@ NXP G2D GPU
API API
*** ***
:ref:`lv_draw_g2d_h` .. API startswith: lv_draw_g2d_
@@ -5,6 +5,6 @@ NXP PXP GPU
API API
*** ***
:ref:`lv_draw_pxp_h` .. API startswith: lv_draw_pxp_
:ref:`lv_pxp_cfg_h` .. API startswith: lv_pxp_
@@ -5,4 +5,4 @@ NXP VGLite GPU
API API
*** ***
:ref:`lv_draw_vglite_h` .. API startswith: lv_draw_vglite_
@@ -5,5 +5,5 @@ SDL Renderer
API API
*** ***
:ref:`lv_draw_sdl_h` .. API startswith: lv_draw_sdl_
@@ -5,5 +5,5 @@ STM32 DMA2D GPU
API API
*** ***
:ref:`lv_draw_dma2d_h` .. API startswith: lv_draw_dma2d_
@@ -73,14 +73,13 @@ For detailed instructions, see :ref:`vg_lite_tvg`.
API API
*** ***
:ref:`lv_draw_vglite_h` .. API startswith: lv_draw_vglite_
:ref:`lv_vglite_buf_h` .. API equals: vglite_set_buf
:ref:`lv_vglite_matrix_h` .. API equals: vglite_set_translation_matrix
:ref:`lv_vglite_path_h` .. API equals: vglite_create_rect_path_data
:ref:`lv_vglite_utils_h`
.. API equals: vglite_get_color
+1 -1
View File
@@ -62,7 +62,7 @@ Example
API API
*** ***
:ref:`lv_fsdrv_h` :ref:`libs/fsdrv/lv_fsdrv.h`
See also: `lvgl/src/libs/fsdrv/lv_fs_littlefs.c <https://github.com/lvgl/lvgl/blob/master/src/libs/fsdrv/lv_fs_littlefs.c>`__ See also: `lvgl/src/libs/fsdrv/lv_fs_littlefs.c <https://github.com/lvgl/lvgl/blob/master/src/libs/fsdrv/lv_fs_littlefs.c>`__
+1 -1
View File
@@ -76,5 +76,5 @@ Example
API API
*** ***
:ref:`lv_libjpeg_turbo_h` :ref:`libs/libjpeg_turbo/lv_libjpeg_turbo.h`
+1 -1
View File
@@ -101,4 +101,4 @@ the output to ``./output/cogwheel.bin``.
API API
*** ***
:ref:`lv_rle_h` :ref:`libs/rle/lv_rle.h`
@@ -26,4 +26,4 @@ Display (lv_display)
API API
*** ***
:ref:`lv_display_h` :ref:`display/lv_display.h`