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,
# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf.
FILE_PATTERNS = lv*.h \
*.hh \
*.hxx \
*.hpp \
*.h++
FILE_PATTERNS = lv*.h \
lv*.hh \
lv*.hxx \
lv*.hpp \
lv*.h++
# The RECURSIVE tag can be used to specify whether or not subdirectories should
# 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
- make Doxygen XML output available, and
- 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 re
@@ -16,6 +56,17 @@ from announce import *
old_html_files = {}
EMIT_WARNINGS = True
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``.
# 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]+')
# 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.
_auto_gen_sep = '.. Autogenerated'
@@ -140,7 +191,15 @@ def _conditionally_add_hyperlink(obj, genned_link_set: set, exclude_set: set):
:return:
"""
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 exclude_set:
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):
if init_value.endswith('_h'):
if init_value.endswith('.h'):
result = init_value[:-2]
else:
result = init_value
@@ -544,6 +603,12 @@ def _add_hyperlinks_to_eligible_files(intermediate_dir: str,
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,
src_dir_bep: 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`.
: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 elig_h_files: Eligible `.h` files directly contained in `src_dir_bep`
:param elig_sub_dirs: List of sub-dirs that contained eligible `.h` files
Example: '/path/to/lvgl/src/draw/sw/blend'
: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.
Example: '/path/to/lvgl/src/docs/intermediate/API'
:return: n/a
"""
indent = ' '
# Keep only sub-path under 'lvgl/src/'.
# Example: 'draw/sw/blend'
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))
# 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
for h_file in elig_h_files:
filename = os.path.basename(h_file)
stem = os.path.splitext(filename)[0]
f.write(indent + stem + '\n')
extended_stem = _extended_stem_from_path(h_file)
f.write(indent + extended_stem + '\n')
# One entry per eligible subdirectory.
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
for h_file in elig_h_files:
filename = os.path.basename(h_file)
stem = os.path.splitext(filename)[0]
rst_file = os.path.join(out_dir, stem + '.rst')
html_file = os.path.join(sub_path, stem + '.html')
old_html_files[stem] = html_file
extended_stem = _extended_stem_from_path(h_file)
rst_file = os.path.join(out_dir, extended_stem + '.rst')
html_file = os.path.join(sub_path, extended_stem + '.html')
old_html_files[extended_stem] = html_file
with open(rst_file, 'w') as f:
# 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.
section_line = (rst_section_line_char * len(filename)) + '\n'
f.write(section_line)
@@ -631,7 +735,7 @@ def _recursively_create_api_rst_files(depth: int,
recursively for subdirectories below it. ("bep" = being processed.)
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
the Doxyfile:
@@ -646,12 +750,12 @@ def _recursively_create_api_rst_files(depth: int,
file depends upon whether any eligible files were recursively
found within it. And that isn't known until this function finishes
(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:
- 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
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`:
- call _create_rst_files_for_dir() to generate appropriate
`.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_dir_bep: Source directory *being processed*
: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
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).
"""
elig_h_files = []
@@ -686,7 +790,10 @@ def _recursively_create_api_rst_files(depth: int,
if os.path.isdir(path_bep):
sub_dirs.append(path_bep) # Add to sub-dir list.
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)
if eligible:
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_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,
src_dir_bep,
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):
"""
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
`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
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:
from sphinx.cmd.build import main as sphinx_build
# 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:
sphinx_args.append(f'{env_opt}')
+6
View File
@@ -1606,6 +1606,7 @@ class DoxygenXml(object):
'LV_ATTRIBUTE_FAST_MEM=',
'LV_ATTRIBUTE_EXTERN_DATA=',
'LV_FORMAT_ATTRIBUTE(fmt,va)=',
'FASTGLTF_EXPORT=',
]
cfg.set('PREDEFINED', predefined_symbols)
@@ -1631,9 +1632,14 @@ class DoxygenXml(object):
full_path = os.path.join(lvgl_src_dir, osal_dir, osal_h)
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')
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)
# Include TAGFILES if requested.
+3 -2
View File
@@ -38,9 +38,10 @@ if "%SPHINXOPTS%" == "" (
rem python ./src/lvgl_version.py >_version_temp.txt
rem set /p VER=<_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!]
set SPHINXOPTS=-D version="!VER!" -j 4
rem set SPHINXOPTS=-D version="!VER!" -j 4
set SPHINXOPTS=-j 4
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);
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
---------------------
@@ -159,4 +159,5 @@ See :ref:`indev_creation` to see how to do this.
API
***
:ref:`lv_tick_h`
.. API startswith: lv_tick_
@@ -65,6 +65,6 @@ Examples
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
***
:ref:`lv_draw_ppa_h`
.. API startswith: lv_draw_ppa_
@@ -109,6 +109,6 @@ See the LVGL :ref:`DMA2D support <dma2d>`.
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
***
:ref:`lv_draw_g2d_h`
.. API startswith: lv_draw_g2d_
@@ -5,6 +5,6 @@ NXP PXP GPU
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
***
:ref:`lv_draw_vglite_h`
.. API startswith: lv_draw_vglite_
@@ -5,5 +5,5 @@ SDL Renderer
API
***
:ref:`lv_draw_sdl_h`
.. API startswith: lv_draw_sdl_
@@ -5,5 +5,5 @@ STM32 DMA2D GPU
API
***
:ref:`lv_draw_dma2d_h`
.. API startswith: lv_draw_dma2d_
@@ -73,14 +73,13 @@ For detailed instructions, see :ref:`vg_lite_tvg`.
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`
:ref:`lv_vglite_utils_h`
.. API equals: vglite_create_rect_path_data
.. API equals: vglite_get_color
+1 -1
View File
@@ -62,7 +62,7 @@ Example
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>`__
+1 -1
View File
@@ -76,5 +76,5 @@ Example
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
***
:ref:`lv_rle_h`
:ref:`libs/rle/lv_rle.h`
@@ -26,4 +26,4 @@ Display (lv_display)
API
***
:ref:`lv_display_h`
:ref:`display/lv_display.h`