Files
lvgl/docs/api_doc_builder.py
2025-12-20 15:12:25 +08:00

891 lines
35 KiB
Python

"""api_doc_builder.py
Create and provide links to API pages in LVGL doc build.
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 5 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 target names for :ref:`link_ref_name` link
target names 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 target names. Prior to 11-Aug-2025, link target 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
target names to #2 and #4 were identical, and Sphinx (correctly) does not allow
that. So after 11-Aug-2025, these link target names are now differentiated via 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
import doxygen_xml
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
# at the end including all \r\n's. This will match UP TO:
# - the next non-blank character (could be many blank lines), or
# - to the end of the file, whichever comes first.
_re_api_section_sep = re.compile(r'(?mi)^API *\r?\n^\*\*\*\s*')
# Regex to identify '.. API equals: lv_obj_t, lv_array_t' directives.
_re_api_equals = re.compile(r'(?mi)^\s*\.\.\s+API\s+equals:\s*([\w,\s]+)')
# Regex to identify '.. API startswith: lv_obj, lv_array' directives.
_re_api_startswith = re.compile(r'(?mi)^\s*\.\.\s+API\s+startswith:\s*([\w,\s]+)')
# Regex to match comma and whitespace list-item separators on multiple lines.
_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/\.]+)`')
# Separator to mark place where this script added hyperlinks.
_auto_gen_sep = '.. Autogenerated'
# List of symbol dictionaries
_defines = {}
_enums = {}
_variables = {}
_namespaces = {}
_structs = {}
_unions = {}
_typedefs = {}
_functions = {}
_symbol_dict_list = [
_defines,
_enums,
_variables,
_namespaces,
_structs,
_unions,
_typedefs,
_functions
]
def old_clean_name(nme):
"""Strip beginning "_lv" and ending "_t"."""
# Handle error:
# AttributeError: 'NoneType' object has no attribute 'startswith'
if nme is None:
return nme
if nme.startswith('_lv_'):
nme = nme[4:]
elif nme.startswith('lv_'):
nme = nme[3:]
if nme.endswith('_t'):
nme = nme[:-2]
return nme
# Definitions:
# - "section" => The name "abc_def" has 2 sections.
# - N = number of sections in `item_name`.
# After removing leading '_lv_', 'lv_' and trailing '_t' from `obj_name`,
# do the remaining first N "sections" of `obj_name` match `item_name`
# (case sensitive)?
def old_is_name_match(item_name, obj_name):
# Handle error:
# AttributeError: 'NoneType' object has no attribute 'split'
if obj_name is None:
return False
sect_count = item_name.count('_') + 1
obj_name = obj_name.split('_')
# Reject (False) if `obj_name` doesn't have as many sections as `item_name`.
if len(obj_name) < sect_count:
return False
obj_name = '_'.join(obj_name[:sect_count])
return item_name == obj_name
def old_get_includes(name1, name2, obj, includes):
name2 = old_clean_name(name2)
if not old_is_name_match(name1, name2):
return
if obj.parent is not None and hasattr(obj.parent, 'header_file'):
header_file = obj.parent.header_file
elif hasattr(obj, 'header_file'):
header_file = obj.header_file
else:
return
if not header_file:
return
if header_file not in old_html_files:
return
includes.add((header_file, old_html_files[header_file]))
def _conditionally_add_hyperlink(obj, genned_link_set: set, exclude_set: set):
"""
Add hyperlink names to `link_set` if:
- not in `exclude_set`, and
- not already in `link_set`.
:param obj: "thing" from dictionary with matching symbol.
These are objects instantiated from classes
in `doxygen_xml` module such as
STRUCT, FUNCTION, DEFINE, ENUMVALUE, etc.
:param genned_link_set: Set in which to accumulate link names
:param exclude_set: Set with link names not to add to `link_set`
:return:
"""
if obj.file_name is not None:
# 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)
def _add_startswith_matches(strings: [str], genned_link_set, editor_link_set):
"""
Add set of hyperlinks to `genned_link_set` that are not already in
`editor_link_set`, for C symbols that start with strings in `strings`.
:param strings: List of strings to match against
:param genned_link_set: Generated link set
:param editor_link_set: Hyperlinks added by editor
:return: n/a
"""
for partial_symbol in strings:
for symbol_dict in _symbol_dict_list:
for key in symbol_dict:
if key is None:
# Dictionary `enums` has a key `None` which contains
# all enum values from all unnamed enums, and each
# enum value has a `file_name` field.
enum_values_list = symbol_dict[None].members
for enum_val in enum_values_list:
if enum_val.name.startswith(partial_symbol):
_conditionally_add_hyperlink(enum_val, genned_link_set, editor_link_set)
else:
if key.startswith(partial_symbol):
obj = symbol_dict[key]
_conditionally_add_hyperlink(obj, genned_link_set, editor_link_set)
def _add_exact_matches(symbols: [str], genned_link_set, editor_link_set):
"""
Add set of hyperlinks to `genned_link_set` that are not already in
`editor_link_set`, for exact C symbol matches.
:param symbols: List of C symbols to match against
:param genned_link_set: Generated link set
:param editor_link_set: Hyperlinks added by editor
:return: n/a
"""
for symbol in symbols:
for symbol_dict in _symbol_dict_list:
if symbol in symbol_dict:
obj = symbol_dict[symbol]
_conditionally_add_hyperlink(obj, genned_link_set, editor_link_set)
def _hyperlink_sort_value(init_value: str):
if init_value.endswith('.h'):
result = init_value[:-2]
else:
result = init_value
return result
def _process_end_of_eligible_doc(b: str, rst_file: str) -> (str, str, int):
"""
Edit end section after API section heading.
3. Initialize:
- links_added_count = 0
- editor_link_set = set()
- genned_link_set = set()
- C = '' # string for generated hyperlinks
4. Remove `_auto_gen_sep` and everything after it:
- new_B = B.split(_auto_gen_sep, 1)[0]
5. With `new_B, add any editor-added hyperlinks to set:
`editor_link_set`.
6. If `_re_api_equals` match present:
- build list of symbols
- compute list of hyperlinks from symbols
- add hyperlinks to `genned_link_set` if not in `editor_link_set`.
7. If `_re_api_startswith` match present:
- build list of symbols
- compute list of hyperlinks from symbols
- add hyperlinks to `genned_link_set` if not in `editor_link_set`.
8. Lacking either of the above custom directives, use the lower-case
stem of the filename and prefix it with "lv_" and try an
"API startswith" search with it.
9. If len(genned_link_set) > 0:
- `C` = _auto_gen_sep + '\n\n' + `genned_link_set`
(with a blank line between each).
10. Return tuple: (new_B, C, links_added_count).
:param b: End of document after API section heading + whitespace.
:return: Tuple: (new_B, C, links_added_count)
"""
# 3. Initialize:
editor_link_set = set()
genned_link_set = set()
c = ''
api_directives_found_count = 0
# 4. Remove `_auto_gen_sep` and everything after it:
new_b = b.split(_auto_gen_sep, 1)[0]
# 5. With `new_b`, add any editor-added hyperlinks to set:
# `editor_link_set`.
for line in new_b.splitlines():
match = _re_editor_added_hyperlink.match(line)
if match is not None:
editor_link_set.add(match[1])
# 6. If `_re_api_equals` present:
# - build list of symbols
# - compute list of hyperlinks from symbols
# - add hyperlinks to `genned_link_set` if not in `editor_link_set`.
match = _re_api_equals.search(new_b)
if match is not None:
api_directives_found_count += 1
comma_sep_list = match[1].strip()
symbols = _re_multi_line_comma_sep.split(comma_sep_list)
_add_exact_matches(symbols, genned_link_set, editor_link_set)
# 7. If `_re_api_startswith` present:
# - build list of symbols
# - compute list of hyperlinks from symbols
# - add hyperlinks to `genned_link_set` if not in `editor_link_set`.
match = _re_api_startswith.search(new_b)
if match is not None:
api_directives_found_count += 1
comma_sep_list = match[1].strip()
symbols = _re_multi_line_comma_sep.split(comma_sep_list)
_add_startswith_matches(symbols, genned_link_set, editor_link_set)
# 8. Lacking either of the above custom directives, use the lower-case
# stem of the filename and prefix it with "lv_" and try an
# "API startswith" with it.
if api_directives_found_count == 0:
base = os.path.basename(rst_file)
stem = os.path.splitext(base)[0]
_add_startswith_matches(['lv_' + stem], genned_link_set, editor_link_set)
# 9. If len(genned_link_set) > 0:
# - `C` = _auto_gen_sep + '\n\n' + `genned_link_set`
# (with a blank line between each).
links_added_count = len(genned_link_set)
if links_added_count > 0:
c = '\n' + _auto_gen_sep + '\n\n'
for link_name in sorted(genned_link_set, key=_hyperlink_sort_value):
c += ':ref:`' + link_name + '`\n\n'
# 10. Return tuple: (new_B, C, links_added_count).
return new_b, c, links_added_count
def _process_one_file(rst_file: str):
"""
Add applicable API hyperlinks to one file.
Eligible
An `.rst` file is eligible if it contains an API section at
its end. This can happen also in `index.rst` files when
they head a single subject for which an API section is
appropriate there and not in the sub-docs. So `index.rst`
files are included, whereas they were not included previously.
Algorithm:
----------
A. Doc editors may have already added a set of hyperlinks of their
own. This routine takes note of and does not duplicate what is
already there.
B. Doc editors may also have added specifications for this routine
that look like this:
.. API equals: lv_obj_t, lv_arc_t, lv_barcode_t,
lv_win_t, lv_list_t,
lv_button_t
.. API startswith: lv_obj, lv_arc, lv_barcode,
lv_win, lv_list,
lv_button
as directives for this routine to build a set of applicable
hyperlinks.
C. Lacking any of the above custom directives, use the lower-case
stem of the filename and prefix it with "lv_" and try an
"API startswith" search with it.
Any hyperlinks added by this routine are prefixed with the
reStructuredText comment defined by the `_auto_gen_sep`
variable, normally:
.. Autogenerated
If `rst_file` is eligible, edit after API section heading such that:
- any editor-added hyperlinks are retained at the top of the list;
- `_auto_gen_sep` (looked for in case a source file ends up having it;
anything after it is replaced);
- applicable hyperlinks added such that they do not repeat those
added by editors of `.rst` file.
Steps to Implement:
-------------------
0. links_added_count = 0
1. Determine if eligible.
- If not, skip to step 12.
- If so, continue.
2. Split doc into 2 parts:
A. beginning through API section heading and subsequent
whitespace including subsequent blank lines;
B. anything after that which may include editor-added hyperlinks.
3-10. new_B, C, links_added_count = _process_end_of_eligible_doc()
11. Write `A` + `new_B` + `C` back to `rst_file`.
12. Return links_added_count.
:param rst_file: Full path to `.rst` file in question.
It may or may not be eligible.
:return: Number of links added.
"""
links_added_count = 0
with open(rst_file, 'rb') as f:
try:
rst_contents = f.read().decode('utf-8')
except UnicodeDecodeError:
announce(__file__, f'Error: UnicodeDecodeError in [{rst_file}].')
raise
eligible_match = _re_api_section_sep.search(rst_contents)
if eligible_match is not None:
# Eligible (API section found).
i = eligible_match.end()
# Split just after the API section heading + whitespace.
a = rst_contents[:i]
b = rst_contents[i:]
new_b, c, links_added_count = _process_end_of_eligible_doc(b, rst_file)
if links_added_count > 0:
rst_contents = a + new_b + c
with open(rst_file, 'wb') as f:
f.write(rst_contents.encode('utf-8'))
return links_added_count
def _build_one_local_dictionary(local_dict, remote_dict):
"""
Remove '_' prefix in symbols beginning with '_lv' to make
symbols like `lv_obj_t` actually connect with the struct
in `lv_obj_private.h`, and not the typedef in `lv_types.h`.
:param local_dict: Local (adjusted) symbol dictionary
:param remote_dict: Dictionary from `doxygen_xml` module
:return: n/a
"""
for symbol in remote_dict:
# Note: symbol `None` is actually a valid symbol in the
# `enums` dictionary, containing all enum-value symbols
# for enums without names.
if symbol is None or not symbol.startswith('_lv'):
loc_symbol = symbol
else:
# Remove '_' prefix.
loc_symbol = symbol[1:]
local_dict[loc_symbol] = remote_dict[symbol]
def _build_local_symbol_dictionaries():
"""
Build "work-around" dictionaries so that a symbol like `lv_obj_t`
actually connects with the struct in `lv_obj_private.h`, and not
the typedef in `lv_types.h`.
:return: n/a
"""
_build_one_local_dictionary(_defines, doxygen_xml.defines)
_build_one_local_dictionary(_enums, doxygen_xml.enums)
_build_one_local_dictionary(_variables, doxygen_xml.variables)
_build_one_local_dictionary(_namespaces, doxygen_xml.namespaces)
_build_one_local_dictionary(_structs, doxygen_xml.structures)
_build_one_local_dictionary(_unions, doxygen_xml.unions)
_build_one_local_dictionary(_typedefs, doxygen_xml.typedefs)
_build_one_local_dictionary(_functions, doxygen_xml.functions)
def _add_hyperlinks_to_eligible_files(intermediate_dir: str,
new_algorithm: bool,
*doc_rel_paths: [str]):
"""
Add applicable hyperlinks to eligible docs found joining
`intermediate_dir` with each relative path in `doc_rel_paths`.
See API-link algorithm documented under `_process_one_file()`.
:param intermediate_dir: Top directory where hyperlinks are to be added.
:param doc_rel_paths: Tuple of relative paths from `intermediate_dir` to
walk to find docs eligible for API hyperlinks.
:return:
"""
if new_algorithm:
# Populate local symbol dictionary set with
# symbols WITHOUT any '_' prefixes.
_build_local_symbol_dictionaries()
# Build `.rst` file list.
file_list = []
for rel_path in doc_rel_paths:
top_dir = os.path.join(intermediate_dir, rel_path)
for dir_bep, sub_dirs, files in os.walk(top_dir, topdown=False):
for file in files:
if file.lower().endswith('.rst'):
file_list.append(os.path.join(dir_bep, file))
total_eligible_doc_count = 0
total_links_added_count = 0
# For each `.rst` file, add appropriate API hyperlinks.
for rst_file in file_list:
links_added_count = _process_one_file(rst_file)
if links_added_count > 0:
total_links_added_count += links_added_count
total_eligible_doc_count += 1
# announce(__file__, f'Eligible doc: [{rst_file}].')
announce(__file__, f'Docs eligible for API hyperlinks: {total_eligible_doc_count:>4}')
announce(__file__, f'API hyperlinks added : {total_links_added_count:>4}')
else:
for folder in doc_rel_paths:
# Fetch a list of '.rst' files excluding 'index.rst'.
rst_files = list(
(os.path.splitext(item)[0], os.path.join(folder, item))
for item in os.listdir(folder)
if item.endswith('.rst') and 'index.rst' not in item
)
# For each .RST file in that directory...
for stem, path in rst_files:
# Start with an empty set.
html_includes = set()
# Build `html_includes` set as a list of tuples containing
# (name, html_file). Example: "draw.rst" has `stem` == 'draw',
# and generates a list of tuples from .H files where matching
# C-code-element names were found. Example:
# {('lv_draw_line', 'draw\\lv_draw_line.html'),
# ('lv_draw_sdl', 'draw\\sdl\\lv_draw_sdl.html'),
# ('lv_draw_sw_blend_to_i1', 'draw\\sw\\blend\\lv_draw_sw_blend_to_i1.html'),
# etc.}
for symbol_dict in (
doxygen_xml.defines,
doxygen_xml.enums,
doxygen_xml.variables,
doxygen_xml.namespaces,
doxygen_xml.structures,
doxygen_xml.unions,
doxygen_xml.typedefs,
doxygen_xml.functions
):
for key, obj in symbol_dict.items():
old_get_includes(stem, key, obj, html_includes)
if html_includes:
# Convert `html_includes` set to a list of strings containing the
# Sphinx hyperlink syntax "link target names". Example from above:
# [':ref:`lv_draw_line_h`\n',
# ':ref:`lv_draw_sdl_h`\n',
# ':ref:`lv_draw_sw_blend_to_i1_h`\n',
# etc.]
html_includes = list(
':ref:`{0}_h`\n'.format(inc)
for inc, _ in html_includes
)
# Convert that list to a single string of Sphinx hyperlink
# target names with blank lines between them.
# :ref:`lv_draw_line_h`
#
# :ref:`lv_draw_sdl_h`
#
# :ref:`lv_draw_sw_blend_to_i1_h`
#
# etc.
output = ('\n'.join(html_includes)) + '\n'
# Append that string to the source .RST file being processed.
with open(path, 'rb') as f:
try:
data = f.read().decode('utf-8')
except UnicodeDecodeError:
print(path)
raise
data = data.split(_auto_gen_sep, 1)[0]
data += f'{_auto_gen_sep}\n\n'
data += output
with open(path, 'wb') as f:
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],
elig_sub_dirs: [str],
out_root_dir: str):
"""
- Create `index.rst` file and add its top section.
- For each file in `elig_h_files`:
- Create one `.rst` file.
- Add reference to it in `index.rst`.
- For each subdir in `elig_sub_dirs`:
- 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*
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
# since `.rst` files are created in bottom-up sequence.
if not os.path.isdir(out_dir):
os.makedirs(out_dir)
# For top-level directory only... (the last index.rst created,
# since they are created in bottom-up sequence)
if len(sub_path) == 0 and out_dir.endswith(os.sep):
# Trim trailing slash from `out_dir`.
out_dir = out_dir[:-1]
# index.rst
with open(os.path.join(out_dir, 'index.rst'), 'w') as f:
subdir_stem = os.path.split(out_dir)[-1]
section_line = (rst_section_line_char * len(subdir_stem)) + '\n'
f.write(section_line)
f.write(subdir_stem + '\n')
f.write(section_line)
f.write('\n')
f.write('.. toctree::\n :maxdepth: 1\n :class: toctree-1-deep\n\n')
# One entry per `.rst` file
for h_file in elig_h_files:
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:
stem = os.path.split(sub_dir)[-1]
f.write(indent + stem + '/index\n')
# One .rst file per h_file
for h_file in elig_h_files:
filename = os.path.basename(h_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.
# 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)
f.write(filename + '\n')
f.write(section_line)
f.write('\n')
# Content for `breathe`.
f.write(f'.. doxygenfile:: {filename}\n')
f.write(' :project: lvgl\n\n')
def _recursively_create_api_rst_files(depth: int,
src_root_len: int,
src_dir_bep: str,
out_root_dir: str) -> int:
"""
Create `.rst` files for the eligible C source files found in `src_dir_bep` and
recursively for subdirectories below it. ("bep" = being processed.)
Eligible
An input file (e.g. `.h*`) file is eligible if Doxygen generated
documentation for it. The combination of these configuration items in
the Doxyfile:
- INPUT
- FILE_PATTERNS
- EXCLUDE
- EXCLUDE_PATTERNS
controls the files Doxygen processes. Files not processed are not eligible.
Whether a subdirectory is eligible to be included in an `index.rst`
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
taken within are:
- 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`).
- If `elig_h_file_count > 0`:
- call _create_rst_files_for_dir() to generate appropriate
`.rst` files for this directory.
- Return `elig_h_file_count`.
Once we have accumulated this information, then we can generate
all the `.rst` files for the current directory without any further
directory-tree walking.
:param depth: Only used for testing/debugging
: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
whether that directory recursively held any
eligible `.h*` files, to know whether to include
"subdir/index" in caller's local `index.rst` file).
"""
elig_h_files = []
sub_dirs = []
elig_sub_dirs = []
elig_h_file_count = 0
# For each "thing" found in `src_dir_bep`, build lists:
# `elig_sub_dirs` and `elig_h_files`.
# By design change, we are including files with 'private'
# in their names. Reason: advanced users who need to use
# the structs defined within will need the documentation
# in those API pages!
for dir_item in os.listdir(src_dir_bep):
path_bep = os.path.join(src_dir_bep, dir_item)
if os.path.isdir(path_bep):
sub_dirs.append(path_bep) # Add to sub-dir list.
else:
_, 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.
elig_h_file_count += 1
# For each subdir...
for sub_dir in sub_dirs:
subdir_eligible_h_file_count = \
_recursively_create_api_rst_files(depth + 1,
src_root_len,
sub_dir,
out_root_dir)
if subdir_eligible_h_file_count > 0:
elig_sub_dirs.append(sub_dir)
elig_h_file_count += subdir_eligible_h_file_count
if elig_h_file_count > 0:
# Sort both lists.
# Evidently the Linux-Python's implementation of `listdir()` does not
# automatically produce a sorted list.
elig_sub_dirs.sort()
elig_h_files.sort()
# 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,
elig_sub_dirs,
out_root_dir)
return elig_h_file_count
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
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 generated no documentation for). Output the `.rst` files
into `out_root_dir` mirroring the `a_src_root` directory structure.
:param src_root_dir: root source directory to walk
:param out_root_dir: output directory
:return: n/a
"""
src_root_len = len(src_root_dir) + 1
_recursively_create_api_rst_files(0, src_root_len, src_root_dir, out_root_dir)
def build_api_docs(lvgl_src_dir, intermediate_dir, doxyfile_src_file, *doc_rel_paths):
"""
- Prep and run Doxygen, outputting XML.
- Load that XML in a form that can quickly tie C symbols to the
source files they came from.
- Generate API page `.rst` files for source files Doxygen generated
documentation for.
- Add hyperlinks to these API pages for `.rst` files in `*doc_rel_paths`
that are eligible.
:param lvgl_src_dir: Path to LVGL src directory
:param intermediate_dir: Path to intermediate dir being built
:param doxyfile_src_file: Full path to src doxygen configuration file
:param doc_rel_paths: List of relative paths from `intermediate_dir` to
walk to find docs eligible for API hyperlinks.
"""
# ---------------------------------------------------------------------
# - Generate Doxyfile replacing tokens,
# - run Doxygen generating XML, and
# - load the generated XML from Doxygen output.
# ---------------------------------------------------------------------
doxygen_xml.EMIT_WARNINGS = EMIT_WARNINGS
xml_parser = doxygen_xml.DoxygenXml(lvgl_src_dir,
intermediate_dir,
doxyfile_src_file,
silent_mode=False
)
# ---------------------------------------------------------------------
# Generate .RST files for API pages.
# ---------------------------------------------------------------------
announce(__file__, "Generating API documentation .RST files...")
api_out_root_dir = os.path.join(intermediate_dir, 'API')
create_api_rst_files(lvgl_src_dir, api_out_root_dir)
# ---------------------------------------------------------------------
# For each directory entry in `doc_rel_paths` array...
# - add API hyperlinks to .RST files in the directories in passed array.
# ---------------------------------------------------------------------
announce(__file__, "Adding API-page hyperlinks to source docs...")
_add_hyperlinks_to_eligible_files(intermediate_dir,
True,
*doc_rel_paths)