Files
lvgl/docs/doxygen_xml.py

1748 lines
60 KiB
Python

"""doxygen_xml.py
Python Interface to Doxygen XML Output
DoxygenXml is the primary class defined herein.
It uses all the other classes, and is used by:
- ./docs/build.py via ./docs/api_doc_builder.py for the LVGL doc build to:
- edit Doxyfile to replace 2 string tokens
- run Doxygen
- load Doxygen XML output and use it to:
- generate API `.rst` pages,
- populate a set of `doxygen_xml.<...>` dictionaries, and
- use those dictionaries to determine in what LVGL documents
hyperlinks to API pages might be useful and appends them if they
are not already present.
- ./scripts/gen_json/gen_json.py => pycparser_monkeypatch.py to
- edit Doxyfile to replace 2 string tokens
- silently run Doxygen
- load Doxygen XML output and use it to:
- populate a set of `doxygen_xml.<...>` dictionaries, and
- monkey-patch pycparser AST (Abstract Syntax Tree) output to
extract API documentation into its JSON output.
"Loading Doxygen XML output" consists of
- opening and parsing the XML `index.xml` file generated by Doxygen which
is a summary of Doxygen-generated documentation linked to additional `.xml`
filenames that have more details. Note that `index.xml` has a complete list
of documented symbol-names from the C code. `./docs/api_doc_builder.py`
searches those names using search term(s) to determine in what LVGL
documents hyperlinks to API pages might be useful, and appends them
to those documents if they are not already present.
`index.xml` structure is described in the `doxygen/doxygen` repository
on GitHub under `./templates/xml/index.xsd`, and the structure of the
other files is described in `./templates/xml/compound.xsd`.
<doxygenindex ...>
Unlimited-length list of <compound> nodes.
<compound refid="..." kind="..."><name>...</name>
<member refid="..." kind="..."><name>...</name></member>
...
</compound>
...
</doxygenindex>
In `index.xml`, every <compound> element:
- always has attributes "refid" and "kind",
- always has exactly 1 <name> element, and
- is followed by an unbounded number of <member> elements.
Each <compound> element's 'kind' attribute will be one of the following:
class struct union interface
protocol category exception file
namespace group page example
dir type concept module
which is a group that applies to many languages. Since Doxygen
is parsing a C-language project, we will only find these in the
`index.xml` file:
- struct
- union
- file
- page (unused)
- dir (unused)
In `'index.xml`, every <member> element:
- always has attributes "refid" and "kind",
- always has exactly 1 <name> element.
The contents of the <member> elements differ based on the "kind" of
<compound> element they are contained by.
kind="struct"
- <member> sub-elements only have `kind="variable"` attributes.
kind="union"
- <member> sub-elements only have `kind="variable"` attributes.
kind="file"
- <member> sub-elements can have `kind` attributes:
- define
- enum
- enumvalue has refid that begins with containing enum's refid
- variable
- function
- typedef
- <member kind="define"...>
Has <name> child element with macro name. refid contains something
that looks like this:
"lv__obj_8h_1a8ec0fe9743c5c015162d64df591cd719"
/____________________________________________/
/_________/ |
| +-- ``id`` attribute of applicable item
|
+-- stem of `.xml` filename containing details
lv__obj_8h.xml has this format:
<doxygen ...>
<compounddef ...>
<compoundname>lv_obj.h</compoundname>
Section dealing with #include's (list, dependency graph, etc.)
<includes list>
<includedby list>
<incdepgraph ...>
</incdepgraph>
<invincdepgraph ...>
</invincdepgraph>
<sectiondef kind="...">
</sectiondef>
...List of <sectiondef> elements.
...kind can be:
- define
- enum
- var
- func
<sectiondef kind="define">
<memberdef kind="define" id="refid from index.xml">...</memberdef>
<memberdef>...</memberdef>
<memberdef>...</memberdef>
...List of all defines in file.
</sectiondef>
<sectiondef kind="enum">
<memberdef kind="enum" id="refid from index.xml">...</memberdef>
<type></type>
<name></name>
<enumvalue id="refid from index.xml" prot="public">
<name>LV_STATE_DEFAULT</name>
<initializer>= 0x0000</initializer>
<briefdescription>
</briefdescription>
<detaileddescription>
</detaileddescription>
</enumvalue>
...List of all the enumeration's enumvalue's.
</memberdef>
...List of all enums in the file.
</sectiondef>
<sectiondef kind="var">
<memberdef kind="variable" id="refid from index.xml" prot="public" static="no" extern="yes" mutable="no">
...List of all variables in file.
</sectiondef>
<sectiondef kind="func">
<memberdef kind="function" id="refid from index.xml" ...>
...List of all functions in file.
</sectiondef>
<briefdescription> @file @brief description
</briefdescription>
<detaileddescription> @file detailed description
</detaileddescription>
<programlisting> Only included if XML_PROGRAMLISTING = YES.
<codeline lineno="1">...</codeline>
<codeline lineno="2">...</codeline>
<codeline lineno="3">...</codeline>
</programlisting>
</compounddef>
</doxygen>
kind="page"
- Has no child members, but does have a child element <name>
(e.g. "deprecated" and "todo").
kind="dir"
- do not have <member> sub-elements
- contents of <name> element is a full path to a directory
- If sorted alphabetically, they form a sequence that can be
used to create something:
E:
E:/Dev
E:/Dev/lvgl
E:/Dev/lvgl/lvgl
E:/Dev/lvgl/lvgl/src
etc.
- refid="..." contains the stem of another `.xml` file that
contains more information on the directory, including any
documentation on it found by Doxygen. That XML file's structure is:
<refid>.xml:
------------
<compounddef id="<refid_from_above" kind="dir">
<compoundname>full_path_of_dir_wo_trailing_slash</compoundname>
<innerdir refid="stem_of_xml_file_containing_dir_details">subdirectory_path</innerdir>
<innerdir refid="stem_of_xml_file_containing_dir_details">subdirectory_path</innerdir>
<innerdir refid="stem_of_xml_file_containing_dir_details">subdirectory_path</innerdir>
... (list <innerdir> elements --- one for each subdirectory in directory)
...
<innerfile refid="stem_of_xml_file_containing_file_details">filename</innerfile>
<innerfile refid="stem_of_xml_file_containing_file_details">filename</innerfile>
<innerfile refid="stem_of_xml_file_containing_file_details">filename</innerfile>
... (list <innerfile> elements --- one for each file in directory)
...
<briefdescription>
Brief description of directory (Doxygen documentation)
</briefdescription>
<detaileddescription>
Detailed description of directory (Doxygen documentation)
</detaileddescription>
<location file="full_path_of_dir_with_trailing_slash"/>
</compounddef>
list
- define
- dir (unused)
- enum
- enumvalue
- example (unused)
- file
- function
- page (unused)
- struct
- typedef
- union
- variable
Most <compound> elements have child <member> elements with their own contents
depending on the 'kind' of <compound> element they are in.
This file defines classes, whose class names are the upper-case names above
except for the unused ones. These classes are used to build a data structure
that matches that of the `index.xml` file.
The list of classes is:
+-----------+----------------------------------------------------+
| Class | Adds New Instance to this Dictionary in __init__() |
+===========+====================================================+
| DEFINE | defines |
+-----------+----------------------------------------------------+
| ENUM | enums (have child ENUMVALUE objects) |
+-----------+----------------------------------------------------+
| VARIABLE | variables |
+-----------+----------------------------------------------------+
| NAMESPACE | namespaces |
+-----------+----------------------------------------------------+
| STRUCT | structures (have child STRUCT_FIELD objects) |
+-----------+----------------------------------------------------+
| UNION | unions (have child STRUCT_FIELD objects) |
+-----------+----------------------------------------------------+
| TYPEDEF | typedefs |
+-----------+----------------------------------------------------+
| FUNCTION | functions (have child FUNC_ARG objects) |
+-----------+----------------------------------------------------+
| GROUP | groups |
+-----------+----------------------------------------------------+
| FILE | files |
+-----------+----------------------------------------------------+
| CLASS | classes |
+-----------+----------------------------------------------------+
Additional classes:
- FUNC_ARG(object): (becomes child members of FUNCTION objects)
- STRUCT_FIELD(object): (becomes child members of STRUCT objects)
- ENUMVALUE(object): (becomes child members of ENUM objects)
- XMLSearch(object): (used only by ./scripts/gen_json/gen_json.py)
Each of the above Dictionary variables has entries with
- keys = documented symbol name from C code that Doxygen found
- values = instances of classes named above
Samples:
'defines': {'ZERO_MEM_SENTINEL': <doxygen_xml.DEFINE object at 0x000001FB5D866420>,
'LV_GLOBAL_DEFAULT': <doxygen_xml.DEFINE object at 0x000001FB5D866210>,
'LV_ASSERT_OBJ': <doxygen_xml.DEFINE object at 0x000001FB5D1EC080>,
'LV_TRACE_OBJ_CREATE': <doxygen_xml.DEFINE object at 0x000001FB5D8660F0>,...}
'enums': {'lv_key_t': <doxygen_xml.ENUM object at 0x000001FB5D1EEB40>,
'lv_group_refocus_policy_t': <doxygen_xml.ENUM object at 0x000001FB5D1E3DA0>,
'lv_obj_flag_t': <doxygen_xml.ENUM object at 0x000001FB5D29F830>,
'lv_obj_class_editable_t': <doxygen_xml.ENUM object at 0x000001FB5D29E300>,...}
'variables': {'lv_global': <doxygen_xml.VARIABLE object at 0x000001FB5D1E3FE0>,
'lv_obj_class': <doxygen_xml.VARIABLE object at 0x000001FB5D1EE1E0>,
'lv_font_montserrat_8': <doxygen_xml.VARIABLE object at 0x000001FB5DAB41A0>,
'lv_font_montserrat_10': <doxygen_xml.VARIABLE object at 0x000001FB5D99D040>,...}
'namespaces': {},
'structures': {'_lv_anim_t::_lv_anim_path_para_t': <doxygen_xml.UNION object at 0x000001FB5C4240E0>,
'_lv_anim_t': <doxygen_xml.STRUCT object at 0x000001FB5C45F680>,
'_lv_animimg_t': <doxygen_xml.STRUCT object at 0x000001FB5C4FE390>,
'_lv_arc_t': <doxygen_xml.STRUCT object at 0x000001FB59D350A0>,...}
'unions': {},
'typedefs': {'lv_global_t': <doxygen_xml.TYPEDEF object at 0x000001FB5D1EFFE0>,
'lv_group_focus_cb_t': <doxygen_xml.TYPEDEF object at 0x000001FB5D1F1CA0>,
'lv_group_edge_cb_t': <doxygen_xml.TYPEDEF object at 0x000001FB5D1EE7E0>,...}
'functions': {'lv_group_create': <doxygen_xml.FUNCTION object at 0x000001FB5D1E0470>,
'lv_group_delete': <doxygen_xml.FUNCTION object at 0x000001FB5D1F3800>,
'lv_group_set_default': <doxygen_xml.FUNCTION object at 0x000001FB5D1ECAA0>,...}
Additional dictionaries:
'files': {'lv_global.h': <doxygen_xml.FILE object at 0x000001FB5D864E00>,
'lv_group.h': <doxygen_xml.FILE object at 0x000001FB5D1EFD40>,
'lv_group_private.h': <doxygen_xml.FILE object at 0x000001FB5D0D7DD0>,...}
"""
import os
import sys
import subprocess
from xml.etree import ElementTree
import doxygen_config
from announce import *
EMIT_WARNINGS = True
DOXYGEN_OUTPUT = True
MISSING_FUNC = 'MissingFunctionDoc'
MISSING_FUNC_ARG = 'MissingFunctionArgDoc'
MISSING_FUNC_RETURN = 'MissingFunctionReturnDoc'
MISSING_FUNC_ARG_MISMATCH = 'FunctionArgMissing'
MISSING_STRUCT = 'MissingStructureDoc'
MISSING_STRUCT_FIELD = 'MissingStructureFieldDoc'
MISSING_UNION = 'MissingUnionDoc'
MISSING_UNION_FIELD = 'MissingUnionFieldDoc'
MISSING_ENUM = 'MissingEnumDoc'
MISSING_ENUM_ITEM = 'MissingEnumItemDoc'
MISSING_TYPEDEF = 'MissingTypedefDoc'
MISSING_VARIABLE = 'MissingVariableDoc'
MISSING_MACRO = 'MissingMacroDoc'
# Dictionaries built from Doxygen XML output via `xml.etree.ElementTree`
defines = {} # dictionary of doxygen_xml.DEFINE objects
enums = {} # dictionary of doxygen_xml.ENUM objects
variables = {} # dictionary of doxygen_xml.VARIABLE objects
namespaces = {} # dictionary of doxygen_xml.NAMESPACE objects
structures = {} # dictionary of doxygen_xml.STRUCT objects
typedefs = {} # dictionary of doxygen_xml.TYPEDEF objects
functions = {} # dictionary of doxygen_xml.FUNCTION objects
groups = {} # dictionary of doxygen_xml.GROUP objects
files = {} # dictionary of doxygen_xml.FILE objects
classes = {} # dictionary of doxygen_xml.CLASS objects
unions = {} # appears to be unused at this time (unions => structures dict).
# Module-Global Variables
xml_path = ''
def run_ext_cmd(cmd_str: str, start_dir: str = None, quiet: bool = False, exit_on_error: bool = True):
"""
Run external command `cmd_str` (possibly silently) in directory
`start_dir` (if provided), and optionally abort execution on error.
:param cmd_str: String to pass to OS.
:param start_dir: Directory to start in, or `None` to run in "cwd".
:param quiet: Should STDOUT and STDERR be suppressed?
:param exit_on_error: Should this function abort execution on
a non-zero exit status?
:return: n/a
"""
saved_dir = None
if start_dir is not None:
saved_dir = os.getcwd()
os.chdir(start_dir)
if quiet:
# This method of running Doxygen is used because we do not
# want anything going to STDOUT. Running it via `os.system()`
# would send its output to STDOUT.
p = subprocess.Popen(
cmd_str,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True
)
out, err = p.communicate()
if p.returncode:
if out:
# Note the `.decode("utf-8")` is required because
# `sys.stdout.write()` requires a string, and `out` is
# a byte array; generates an exception if passed alone.
sys.stdout.write(out.decode("utf-8"))
sys.stdout.flush()
if err:
sys.stderr.write(err.decode("utf-8"))
sys.stdout.flush()
if exit_on_error:
sys.exit(p.returncode)
else:
announce_start(__file__, f'Running [{cmd_str}] in [{os.getcwd()}]...')
return_code = os.system(cmd_str)
announce_finish()
if return_code != 0 and exit_on_error:
print(f'Exiting due to error [{return_code}] running [{cmd_str}].')
sys.exit(return_code)
if saved_dir is not None:
os.chdir(saved_dir)
def warn(warning_type, *args):
if EMIT_WARNINGS:
args = ' '.join(str(arg) for arg in args)
if warning_type is None:
output = f'\033[31;1m {args}\033[0m\n'
else:
output = f'\033[31;1m{warning_type}: {args}\033[0m\n'
sys.stdout.write(output)
sys.stdout.flush()
def build_docstring(element):
docstring = None
if element.tag == 'parameterlist':
return None
if element.text:
docstring = element.text.strip()
for item in element:
ds = build_docstring(item)
if ds:
if docstring:
docstring += ' ' + ds
else:
docstring = ds.strip()
if element.tag == 'para':
if docstring:
docstring = '\n\n' + docstring
if element.tag == 'ref':
docstring = f':ref:`{docstring}`'
if element.tail:
if docstring:
docstring += ' ' + element.tail.strip()
else:
docstring = element.tail.strip()
return docstring
def read_as_xml(d):
try:
return ElementTree.fromstring(d)
except: # NOQA
return None
def load_xml_etree(fle):
fle = os.path.join(xml_path, fle + '.xml')
with open(fle, 'rb') as f:
d = f.read().decode('utf-8')
# This code is to correct a bug in Doxygen. That bug incorrectly parses
# a typedef, and it causes an error to occur building the docs. The Error
# doesn't stop the documentation from being generated, I just don't want
# to see the ugly red output.
#
# if 'typedef void() lv_lru_free_t(void *v)' in d:
# d = d.replace(
# '<type>void()</type>\n '
# '<definition>typedef void() lv_lru_free_t(void *v)</definition>',
# '<type>void</type>\n '
# '<definition>typedef void(lv_lru_free_t)(void *v)</definition>'
# )
# with open(fle, 'wb') as f:
# f.write(d.encode('utf-8'))
return ElementTree.fromstring(d)
def get_type(node):
def gt(n):
for c in n:
if c.tag == 'ref':
t = c.text.strip()
break
else:
t = node.text.strip()
return t.replace('*', '').replace('(', '').replace(')', '').strip()
for child in node:
if child.tag == 'type':
return gt(child)
def build_define(element):
define = None
if element.text:
define = element.text.strip()
for item in element:
ds = build_define(item)
if ds:
if define:
define += ' ' + ds
else:
define = ds.strip()
if element.tail:
if define:
define += ' ' + element.tail.strip()
else:
define = element.tail.strip()
return define
class STRUCT_FIELD(object):
def __init__(self, name, _type, description, file_name, line_no):
self.name = name
self.type = _type
self.description = description
self.file_name = file_name
self.line_no = line_no
class STRUCT(object):
"""<compound kind="struct"> elements in Doxygen `index.xml`"""
_missing = MISSING_STRUCT
_missing_field = MISSING_STRUCT_FIELD
template = '''\
.. doxygenstruct:: {name}
:project: lvgl
:members:
:protected-members:
:private-members:
:undoc-members:
'''
def __init__(self, parent, refid, name, **_):
global structures
global unions
if type(self) is UNION:
# UNION inherits from STRUCT.
if name in unions:
self.__dict__.update(unions[name].__dict__)
else:
unions[name] = self
self.parent = parent
self.refid = refid
self.name = name
self.types = set()
self._deps = None
self.header_file = ''
self.description = None
self.fields = []
self.file_name = None
self.line_no = None
else:
# STRUCT type
if name in structures:
self.__dict__.update(structures[name].__dict__)
else:
structures[name] = self
self.parent = parent
self.refid = refid
self.name = name
self.types = set()
self._deps = None
self.header_file = ''
self.description = None
self.fields = []
self.file_name = None
self.line_no = None
# Prior to 9-Mar-2025, the code below was never executing since this
# __init__() was never called with a `parent` value other than `None`.
# Reason: `kind="struct"` only occurs in `index.xml` as a top-level
# entry, and not as a child element of `kind="file"` as do <sectiondef>
# elements with kind = define, var, enum and func.
# Original code:
# if parent and refid:
if refid:
root = load_xml_etree(refid)
for compounddef in root:
if compounddef.attrib['id'] != self.refid:
continue
for child in compounddef:
if child.tag == 'includes':
self.header_file = os.path.splitext(child.text)[0]
continue
elif child.tag == 'location':
self.file_name = child.attrib['file']
self.line_no = child.attrib['line']
elif child.tag == 'detaileddescription':
self.description = build_docstring(child)
elif child.tag == 'sectiondef':
for memberdef in child:
t = get_type(memberdef)
description = None
name = ''
file_name = None
line_no = None
# For each struct member...
for element in memberdef:
if element.tag == 'location':
file_name = element.attrib['file']
line_no = element.attrib['line']
elif element.tag == 'name':
name = element.text
elif element.tag == 'detaileddescription':
description = build_docstring(element)
field = STRUCT_FIELD(name, t, description, file_name, line_no)
self.fields.append(field)
if t is None:
continue
self.types.add(t)
if EMIT_WARNINGS:
if not self.description:
warn(self._missing, self.name)
warn(None, 'FILE:', self.file_name)
warn(None, 'LINE:', self.line_no)
warn(None)
for field in self.fields:
if not field.description:
warn(self._missing_field, self.name)
warn(None, 'FIELD:', field.name)
warn(None, 'FILE:', field.file_name)
warn(None, 'LINE:', field.line_no)
warn(None)
def get_field(self, name):
for field in self.fields:
if field.name == name:
return field
@property
def deps(self):
if self._deps is None:
self._deps = dict(
typedefs=set(),
functions=set(),
enums=set(),
structures=set(),
unions=set(),
namespaces=set(),
variables=set(),
)
for type_ in self.types:
if type_ in typedefs:
self._deps['typedefs'].add(typedefs[type_])
elif type_ in structures:
self._deps['structures'].add(structures[type_])
elif type_ in unions:
self._deps['unions'].add(unions[type_])
elif type_ in enums:
self._deps['enums'].add(enums[type_])
elif type_ in functions:
self._deps['functions'].add(functions[type_])
elif type_ in variables:
self._deps['variables'].add(variables[type_])
elif type_ in namespaces:
self._deps['namespaces'].add(namespaces[type_])
return self._deps
def __str__(self):
return self.template.format(name=self.name)
class UNION(STRUCT):
"""<compound kind="union"> elements in Doxygen `index.xml`"""
_missing = MISSING_UNION
_missing_field = MISSING_UNION_FIELD
template = '''\
.. doxygenunion:: {name}
:project: lvgl
'''
class VARIABLE(object):
"""<compound kind="struct" ...>
<member ... kind="variable"> elements in Doxygen `index.xml`"""
template = '''\
.. doxygenvariable:: {name}
:project: lvgl
'''
def __init__(self, parent, refid, name, **_):
global variables
if name in variables:
self.__dict__.update(variables[name].__dict__)
else:
variables[name] = self
self.parent = parent
self.refid = refid
self.name = name
self.description = None
self.type = ''
self.file_name = None
self.line_no = None
if parent is not None:
root = load_xml_etree(parent.refid)
for compounddef in root:
if compounddef.attrib['id'] != parent.refid:
continue
for child in compounddef:
if (
child.tag == 'sectiondef' and
child.attrib['kind'] == 'var'
):
for memberdef in child:
if memberdef.attrib['id'] == refid:
break
else:
continue
self.type = get_type(memberdef)
for element in memberdef:
if element.tag == 'location':
self.file_name = element.attrib['file']
self.line_no = element.attrib['line']
elif element.tag == 'detaileddescription':
self.description = build_docstring(element)
if not self.description:
warn(MISSING_VARIABLE, self.name)
warn(None, 'FILE:', self.file_name)
warn(None, 'LINE:', self.line_no)
warn(None)
def __str__(self):
return self.template.format(name=self.name)
class NAMESPACE(object):
"""<compound kind="namespace"> elements in Doxygen `index.xml`"""
template = '''\
.. doxygennamespace:: {name}
:project: lvgl
:members:
:protected-members:
:private-members:
:undoc-members:
'''
def __init__(self, parent, refid, name, **_):
global namespaces
if name in namespaces:
self.__dict__.update(namespaces[name].__dict__)
else:
namespaces[name] = self
self.parent = parent
self.refid = refid
self.name = name
self.description = None
self.line_no = None
self.file_name = None
self.enums = []
self.funcs = []
self.vars = []
self.typedefs = []
self.structs = []
self.unions = []
self.classes = []
# root = load_xml(refid)
#
# for compounddef in root:
# if compounddef.attrib['id'] != refid:
# continue
#
# for sectiondef in compounddef:
# if sectiondef.tag != 'sectiondef':
# continue
#
# enum
# typedef
# func
# struct
# union
#
#
# cls = globals()[sectiondef.attrib['kind'].upper()]
# if cls == ENUM:
# if sectiondef[0].text:
# sectiondef.attrib['name'] = sectiondef[0].text.strip()
# enums_.append(cls(self, **sectiondef.attrib))
# else:
# sectiondef.attrib['name'] = None
# enums_.append(cls(self, **sectiondef.attrib))
#
# elif cls == ENUMVALUE:
# if enums_[-1].is_member(sectiondef):
# enums_[-1].add_member(sectiondef)
#
# else:
# sectiondef.attrib['name'] = sectiondef[0].text.strip()
# cls(self, **sectiondef.attrib)
def __str__(self):
return self.template.format(name=self.name)
class GROUP(object):
"""<compound kind="group"> elements in Doxygen `index.xml`"""
template = '''\
.. doxygengroup:: {name}
:project: lvgl
'''
def __init__(self, parent, refid, name, **_):
global groups
if name in groups:
self.__dict__.update(functions[name].__dict__)
else:
functions[name] = self
self.parent = parent
self.refid = refid
self.name = name
self.description = None
def __str__(self):
return self.template.format(name=self.name)
class FUNC_ARG(object):
"""<compound kind="?"><member ...> elements in Doxygen `index.xml`"""
def __init__(self, name, _type):
self.name = name
self.type = _type
self.description = None
class FUNCTION(object):
"""<compound kind="file" ...>
<member ... kind="function"> elements in Doxygen `index.xml`"""
template = '''\
.. doxygenfunction:: {name}
:project: lvgl
'''
def __init__(self, parent, refid, name, **_):
global functions
if name in functions:
self.__dict__.update(functions[name].__dict__)
else:
functions[name] = self
self.parent = parent
self.refid = refid
self.name = name
self.types = set()
self.restype = None
self.args = []
self._deps = None
self.description = None
self.res_description = None
self.file_name = None
self.line_no = None
self.void_return = False
if parent is not None:
root = load_xml_etree(parent.refid)
for compounddef in root:
if compounddef.attrib['id'] != parent.refid:
continue
for child in compounddef:
if child.tag != 'sectiondef':
continue
if child.attrib['kind'] != 'func':
continue
for memberdef in child:
if 'id' not in memberdef.attrib:
continue
if memberdef.attrib['id'] == refid:
break
else:
continue
break
else:
continue
break
else:
return
self.restype = get_type(memberdef)
for child in memberdef:
if child.tag == 'type':
if child.text and child.text.strip() == 'void':
self.void_return = True
if child.tag == 'param':
t = get_type(child)
if t is not None:
self.types.add(t)
for element in child:
if element.tag == 'declname':
arg = FUNC_ARG(element.text, t)
self.args.append(arg)
for child in memberdef:
if child.tag == 'location':
self.file_name = child.attrib['file']
self.line_no = child.attrib['line']
elif child.tag == 'detaileddescription':
self.description = build_docstring(child)
for element in child:
if element.tag != 'para':
continue
for desc_element in element:
if desc_element.tag == 'simplesect' and desc_element.attrib['kind'] == 'return':
self.res_description = build_docstring(desc_element)
if desc_element.tag != 'parameterlist':
continue
for parameter_item in desc_element:
parameternamelist = parameter_item[0]
if parameternamelist.tag != 'parameternamelist':
continue
parameter_name = parameternamelist[0].text
try:
parameterdescription = parameter_item[1]
if parameterdescription.tag == 'parameterdescription':
parameter_description = build_docstring(parameterdescription)
else:
parameter_description = None
except IndexError:
parameter_description = None
if parameter_name is not None:
for arg in self.args:
if arg.name != parameter_name:
continue
arg.description = parameter_description
break
else:
warn(MISSING_FUNC_ARG_MISMATCH, self.name)
warn(None, 'ARG:', parameter_name)
warn(None, 'FILE:', self.file_name)
warn(None, 'LINE:', self.line_no)
warn(None)
if not self.description:
warn(MISSING_FUNC, self.name)
warn(None, 'FILE:', self.file_name)
warn(None, 'LINE:', self.line_no)
warn(None)
else:
for arg in self.args:
if not arg.description:
warn(MISSING_FUNC_ARG, self.name)
warn(None, 'ARG:', arg.name)
warn(None, 'FILE:', self.file_name)
warn(None, 'LINE:', self.line_no)
warn(None)
if not self.res_description and not self.void_return:
warn(MISSING_FUNC_RETURN, self.name)
warn(None, 'FILE:', self.file_name)
warn(None, 'LINE:', self.line_no)
warn(None)
if self.restype in self.types:
self.restype = None
@property
def deps(self):
if self._deps is None:
self._deps = dict(
typedefs=set(),
functions=set(),
enums=set(),
structures=set(),
unions=set(),
namespaces=set(),
variables=set(),
)
if self.restype is not None:
self.types.add(self.restype)
for type_ in self.types:
if type_ in typedefs:
self._deps['typedefs'].add(typedefs[type_])
elif type_ in structures:
self._deps['structures'].add(structures[type_])
elif type_ in unions:
self._deps['unions'].add(unions[type_])
elif type_ in enums:
self._deps['enums'].add(enums[type_])
elif type_ in functions:
self._deps['functions'].add(functions[type_])
elif type_ in variables:
self._deps['variables'].add(variables[type_])
elif type_ in namespaces:
self._deps['namespaces'].add(namespaces[type_])
return self._deps
def __str__(self):
return self.template.format(name=self.name)
class FILE(object):
"""<compound kind="file"> elements in Doxygen `index.xml`"""
def __init__(self, _, refid, name, node, **__):
global files
if name.endswith('lv_types.h'):
return
if name in files:
self.__dict__.update(files[name].__dict__)
return
files[name] = self
self.refid = refid
self.name = name
self.header_file = os.path.splitext(name)[0]
self.types_contained = set()
enums_ = []
for member in node:
if member.tag != 'member':
continue
cls_name = member.attrib['kind'].upper()
self.types_contained.add(cls_name)
cls = globals()[cls_name]
if cls == ENUM:
if member[0].text:
member.attrib['name'] = member[0].text.strip()
enums_.append(cls(self, **member.attrib))
else:
member.attrib['name'] = None
enums_.append(cls(self, **member.attrib))
elif cls == ENUMVALUE:
if enums_[-1].is_member(member):
enums_[-1].add_member(member)
else:
member.attrib['name'] = member[0].text.strip()
cls(self, **member.attrib)
class ENUMVALUE(object):
"""<compound kind="file"...>
<member kind="enum"...>
<member kind="enumvalue"> elements in Doxygen `index.xml`"""
template = '''\
.. doxygenenumvalue:: {name}
:project: lvgl
'''
def __init__(self, parent, refid, name, **_):
self.parent = parent
self.refid = refid
self.name = name
self.description = None
self.file_name = None
self.line_no = None
if parent is not None:
if parent.file_name is not None:
self.file_name = parent.file_name
elif hasattr(parent, 'parent') and parent.parent is not None:
if parent.parent.name is not None:
self.file_name = parent.parent.name
def __str__(self):
return self.template.format(name=self.name)
class ENUM(object):
"""<compound kind="file"...>
<member kind="enum"...> elements in Doxygen `index.xml`"""
template = '''\
.. doxygenenum:: {name}
:project: lvgl
'''
def __init__(self, parent, refid, name, **_):
global enums
if name in enums:
# This happens when `name` is `None`, for example.
# This is true for unnamed enumerations.
self.__dict__.update(enums[name].__dict__)
else:
enums[name] = self
self.parent = parent
self.refid = refid
self.name = name
self.members = []
self.description = None
self.file_name = None
self.line_no = None
if parent is not None:
root = load_xml_etree(parent.refid)
for compounddef in root:
if compounddef.attrib['id'] != parent.refid:
continue
for child in compounddef:
if child.tag != 'sectiondef':
continue
if child.attrib['kind'] != 'enum':
continue
for memberdef in child:
if 'id' not in memberdef.attrib:
continue
if memberdef.attrib['id'] == refid:
break
else:
continue
break
else:
continue
break
else:
return
# raise RuntimeError(f'not able to locate enum {name} ({refid})')
for element in memberdef:
if element.tag == 'location':
self.file_name = element.attrib['file']
self.line_no = element.attrib['line']
if element.tag == 'detaileddescription':
self.description = build_docstring(element)
elif element.tag == 'enumvalue':
item_name = None
item_description = None
item_file_name = None
item_line_no = None
for s_element in element:
if s_element.tag == 'name':
item_name = s_element.text
elif s_element.tag == 'detaileddescription':
item_description = build_docstring(s_element)
elif s_element.tag == 'location':
item_file_name = child.attrib['file']
item_line_no = child.attrib['line']
if item_name is not None:
for ev in self.members:
if ev.name != item_name:
continue
break
else:
ev = ENUMVALUE(
self,
element.attrib['id'],
item_name
)
self.members.append(ev)
ev.description = item_description
if not self.description:
warn(MISSING_ENUM, self.name)
warn(None, 'FILE:', self.file_name)
warn(None, 'LINE:', self.line_no)
warn(None)
for member in self.members:
if not member.description:
warn(MISSING_ENUM_ITEM, self.name)
warn(None, 'MEMBER:', member.name)
warn(None, 'FILE:', self.file_name)
warn(None, 'LINE:', self.line_no)
warn(None)
def is_member(self, member):
return (
member.attrib['kind'] == 'enumvalue' and
member.attrib['refid'].startswith(self.refid)
)
def add_member(self, member):
name = member[0].text.strip()
for ev in self.members:
if ev.name == name:
return
self.members.append(
ENUMVALUE(
self,
member.attrib['refid'],
name
)
)
def __str__(self):
template = [self.template.format(name=self.name)]
template.extend(list(str(member) for member in self.members))
return '\n'.join(template)
class DEFINE(object):
"""<compound kind="file"...>
<member kind="define"...> elements in Doxygen `index.xml`"""
template = '''\
.. doxygendefine:: {name}
:project: lvgl
'''
def __init__(self, parent, refid, name, **_):
global defines
if name in defines:
self.__dict__.update(defines[name].__dict__)
else:
defines[name] = self
self.parent = parent
self.refid = refid
self.name = name
self.description = None
self.file_name = None
self.line_no = None
self.params = None
self.initializer = None
if parent is not None:
root = load_xml_etree(parent.refid)
memberdef = []
for compounddef in root:
if compounddef.attrib['id'] != parent.refid:
continue
for child in compounddef:
if child.tag != 'sectiondef':
continue
if child.attrib['kind'] != 'define':
continue
for memberdef in child:
if memberdef.attrib['id'] == refid:
break
else:
continue
break
else:
continue
break
else:
return
for element in memberdef:
if element.tag == 'location':
self.file_name = element.attrib['file']
self.line_no = element.attrib['line']
elif element.tag == 'detaileddescription':
self.description = build_docstring(element)
elif element.tag == 'param':
for child in element:
if child.tag == 'defname':
if self.params is None:
self.params = []
if child.text:
self.params.append(child.text)
elif element.tag == 'initializer':
initializer = build_define(element)
if initializer is None:
self.initializer = ''
else:
self.initializer = initializer
if not self.description:
warn(MISSING_MACRO, self.name)
warn(None, 'FILE:', self.file_name)
warn(None, 'LINE:', self.line_no)
warn(None)
def __str__(self):
return self.template.format(name=self.name)
class TYPEDEF(object):
template = '''\
.. doxygentypedef:: {name}
:project: lvgl
'''
def __init__(self, parent, refid, name, **_):
global typedefs
if name in typedefs:
self.__dict__.update(typedefs[name].__dict__)
else:
typedefs[name] = self
self.parent = parent
self.refid = refid
self.name = name
self.type = None
self._deps = None
self.description = None
self.file_name = None
self.line_no = None
if parent is not None:
root = load_xml_etree(parent.refid)
for compounddef in root:
if compounddef.attrib['id'] != parent.refid:
continue
for child in compounddef:
if child.tag != 'sectiondef':
continue
if child.attrib['kind'] != 'typedef':
continue
for memberdef in child:
if 'id' not in memberdef.attrib:
continue
if memberdef.attrib['id'] == refid:
break
else:
continue
break
else:
continue
break
else:
return
for element in memberdef:
if element.tag == 'location':
self.file_name = element.attrib['file']
self.line_no = element.attrib['line']
if element.tag == 'detaileddescription':
self.description = build_docstring(element)
if not self.description:
warn(MISSING_TYPEDEF, self.name)
warn(None, 'FILE:', self.file_name)
warn(None, 'LINE:', self.line_no)
warn(None)
self.type = get_type(memberdef)
@property
def deps(self):
if self._deps is None:
self._deps = dict(
typedefs=set(),
functions=set(),
enums=set(),
structures=set(),
unions=set(),
namespaces=set(),
variables=set(),
)
if self.type is not None:
type_ = self.type
if type_ in typedefs:
self._deps['typedefs'].add(typedefs[type_])
elif type_ in structures:
self._deps['structures'].add(structures[type_])
elif type_ in unions:
self._deps['unions'].add(unions[type_])
elif type_ in enums:
self._deps['enums'].add(enums[type_])
elif type_ in functions:
self._deps['functions'].add(functions[type_])
elif type_ in variables:
self._deps['variables'].add(variables[type_])
elif type_ in namespaces:
self._deps['namespaces'].add(namespaces[type_])
return self._deps
def __str__(self):
return self.template.format(name=self.name)
class CLASS(object):
"""<compound kind="file" ...>
<member ... kind="class"> elements in Doxygen `index.xml`"""
def __init__(self, _, refid, name, node, **__):
global classes
if name in classes:
self.__dict__.update(classes[name].__dict__)
return
classes[name] = self
self.refid = refid
self.name = name
enums_ = []
for member in node:
if member.tag != 'member':
continue
cls = globals()[member.attrib['kind'].upper()]
if cls == ENUM:
member.attrib['name'] = member[0].text.strip()
enums_.append(cls(self, **member.attrib))
elif cls == ENUMVALUE:
if enums_[-1].is_member(member):
enums_[-1].add_member(member)
else:
member.attrib['name'] = member[0].text.strip()
cls(self, **member.attrib)
class DoxygenXml(object):
"""Opens, parses and loads a Doxygen-generated `index.xml` file
and makes it available as a set of dictionary attributes of this
module, documented at the top of this file and named below in
`global` statements.
"""
def __init__(self,
lvgl_src_dir: str,
intermediate_dir: str,
doxyfile_src_file: str,
silent_mode=False,
doxy_tagfile=''):
"""
- Prepare and run Doxygen, generating XML output.
- Load that XML output, and use it to populate
:param lvgl_src_dir:
:param intermediate_dir:
:param doxyfile_src_file:
:param silent_mode:
"""
# Dictionaries to Be Populated:
global defines
global enums
global variables
global namespaces
global structures
global typedefs
global functions
global groups
global files
global classes
global unions
global xml_path
announce_set_silent_mode(silent_mode)
base_dir = os.path.abspath(os.path.dirname(__file__))
doxyfile_filename = str(os.path.split(doxyfile_src_file)[1])
doxyfile_dst_file = str(os.path.join(intermediate_dir, doxyfile_filename))
lv_conf_file = os.path.join(intermediate_dir, 'lv_conf.h')
xml_path = os.path.join(intermediate_dir, 'xml')
# In case DoxygenXml() is ever instantiated twice in 1 session,
# clear these dictionaries before they are (re-)populated below.
defines.clear()
enums.clear()
variables.clear()
namespaces.clear()
structures.clear()
typedefs.clear()
functions.clear()
groups.clear()
files.clear()
classes.clear()
unions.clear()
# -----------------------------------------------------------------
# Prep and run Doxygen
# -----------------------------------------------------------------
# Generate Doxyfile into `intermediate_dir` replacing certain
# config options for this run.
# 1. Load from Doxyfile
cfg = doxygen_config.DoxygenConfig()
cfg.load(doxyfile_src_file)
# 2. Update cfg.
cfg.set('OUTPUT_DIRECTORY', '.')
cfg.set('XML_OUTPUT', "xml")
cfg.set('HTML_OUTPUT', 'doxygen_html')
cfg.set('INPUT', lvgl_src_dir)
cfg.set('QUIET', 'YES')
cfg.set('GENERATE_HTML', 'NO')
cfg.set('GENERATE_DOCSET', 'NO')
cfg.set('GENERATE_HTMLHELP', 'NO')
cfg.set('GENERATE_CHI', 'NO')
cfg.set('GENERATE_QHP', 'NO')
cfg.set('GENERATE_ECLIPSEHELP', 'NO')
cfg.set('GENERATE_LATEX', 'NO')
cfg.set('GENERATE_RTF', 'NO')
cfg.set('GENERATE_MAN', 'NO')
cfg.set('GENERATE_XML', 'YES')
cfg.set('GENERATE_DOCBOOK', 'NO')
cfg.set('GENERATE_PERLMOD', 'NO')
# The predefined definitions are:
# - DOXYGEN (defines it as 1 and allows conditional directives [e.g. #if]
# to do special things when DOXYGEN is processing it, as opposed to
# a C compiler.
# - LV_CONF_PATH predefines the path where `lv_conf_internal.h` will
# include the `lv_conf.h` file.
# - All the others here are used when macros prefix a line of code like this:
#
# LV_ATTRIBUTE_EXTERN_DATA extern const lv_obj_class_t lv_obj_class;
#
# which occurs in `lv_obj.h`. When these are added to the PREDEFINED list as
# "MACRO_NAME=" with no value, Doxygen expands the macro to an empty string
# allowing Doxygen to correctly parse the line. The additional macros that
# are treated that way are found in the middle of `lv_conf_template.h` in a
# section called "COMPILER SETTINGS" with no definitions, made for prefixing
# various code. (This works around Doxygen's failure to correctly deal with
# macros that are defined with no values, as these are in lv_conf.h.)
# This list is current as of 28-Apr-2025.
predefined_symbols = [
'DOXYGEN',
f'LV_CONF_PATH="{lv_conf_file}"',
'LV_ATTRIBUTE_TICK_INC=',
'LV_ATTRIBUTE_TIMER_HANDLER=',
'LV_ATTRIBUTE_FLUSH_READY=',
'LV_ATTRIBUTE_MEM_ALIGN=',
'LV_ATTRIBUTE_LARGE_CONST=',
'LV_ATTRIBUTE_LARGE_RAM_ARRAY=',
'LV_ATTRIBUTE_FAST_MEM=',
'LV_ATTRIBUTE_EXTERN_DATA=',
'LV_FORMAT_ATTRIBUTE(fmt,va)=',
'FASTGLTF_EXPORT=',
]
cfg.set('PREDEFINED', predefined_symbols)
# Exclude OSAL `.h` files except for `lv_os_none.h`. Reason: they define
# lv_mutex_t, lv_thread_t and lv_thread_sync_t in multiple places. Doxygen
# must only see one.
osal_dir = 'osal'
osal_exclude_list = [
'lv_cmsis_rtos2.h',
'lv_freertos.h',
'lv_mqx.h',
'lv_pthread.h',
'lv_rtthread.h',
'lv_sdl2.h',
'lv_windows.h',
]
exclude_paths = []
for osal_h in osal_exclude_list:
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.
if doxy_tagfile:
cfg.set('GENERATE_TAGFILE', doxy_tagfile)
# 3. Store it for use by Doxygen in intermediate directory.
cfg.save(doxyfile_dst_file)
# Run Doxygen in intermediate directory.
run_ext_cmd('doxygen Doxyfile', intermediate_dir, quiet=silent_mode)
# -----------------------------------------------------------------
# Load root of Doxygen output (index.xml) as an `xml.etree.ElementTree`.
# -----------------------------------------------------------------
index_xml_etree = load_xml_etree('index')
# Populate these dictionaries.
# Keys : C-code-element names (str) found by Doxygen.
# Values: The <compound> XML-node created by `xml.etree::ElementTree` in `load_xml()` above.
#
# defines, enums, variables,
# namespaces, structures, typedefs,
# functions, unions, groups,
# files, classes.
announce_start(__file__, "Building source-code symbol dictionaries...")
module_namespace = globals()
for compound in index_xml_etree:
# Here we will encounter these "kind" in the index.xml
# <compound> elements: dir, file, page, struct, union.
compound.attrib['name'] = compound[0].text.strip()
# Filter out dir, page, example.
if compound.attrib['kind'] not in ('example', 'page', 'dir'):
class_name = compound.attrib['kind'].upper()
class_obj = module_namespace[class_name]
_ = class_obj(None, node=compound, **compound.attrib)
announce_finish()
# Additional data: the above instantiates the class, but doesn't
# store the resulting object anywhere, since each class' __init__()
# function adds the new object to the appropriate dictionary,
# and uses the arguments it gets to "build out" additional
# structure and/or populate additional dictionaries based on
# content. Each class __init__() function specifies args it
# in its parameter list. They match needed key values in the
# `**compound.attrib` dictionary. The remaining (unused)
# keys in that dictionary are accepted in the `**_` parameter
# at the end of the parameter list.
#
# FILE class populates `files` dict plus:
# - `defines` dictionary
# - `enums` dictionary with children:
# - ENUMVALUEs
# - `variables` dictionary
# - `typedefs` dictionary
# - `functions` dictionary with children:
# - FUNC_ARGs
# STRUCT class populates the `structures` dictionary with children:
# - STRUCT_FIELDs
#
# and possibly `namespaces`, `groups` and `classes` if
# they are present in `index.xml`.
def get_macros(self):
global defines
return list(defines.values())
def get_enum_item(self, e_name):
global enums
for enum, obj in enums.items():
for enum_item in obj.members:
if enum_item.name == e_name:
return enum_item
def get_enum(self, e_name):
global enums
return enums.get(e_name, None)
def get_function(self, f_name):
global functions
return functions.get(f_name, None)
def get_variable(self, v_name):
global variables
return variables.get(v_name, None)
def get_union(self, u_name):
global unions
return unions.get(u_name, None)
def get_structure(self, s_name):
global structures
return structures.get(s_name, None)
def get_typedef(self, t_name):
global typedefs
return typedefs.get(t_name, None)
def get_macro(self, m_name):
global defines
return defines.get(m_name, None)