""" This file opens the XML `index.xml` file generated by Doxygen which has the following structure: ... Each element has a 'kind' attribute that is one of the following: - define - dir - enum - enumvalue - example - file - function - page - struct - typedef - union - variable Most elements have child elements with their own contents depending on the 'kind' of element they are in. This file defines classes for each of these except for - dir - page - example The remaining 'kind' values are: - define - enum - enumvalue - file - function - struct - typedef - union - variable The list of classes is: Class | Adds Self to Dictionary in __init__() --------- | -------------------------------------- DEFINE => `defines` ENUM => `enums` VARIABLE => `variables` NAMESPACE => `namespaces` STRUCT => `structures` UNION appears to have a different purpose TYPEDEF => `typedefs` FUNCTION => `functions` GROUP => `groups` FILE => `files` CLASS => `classes` Additional classes: - NAMESPACE(object): - FUNC_ARG(object): (becomes members of FUNCTION objects) - STRUCT_FIELD(object): (becomes members of STRUCT objects) - GROUP(object): - CLASS(object): - XMLSearch(object): Each of the above Dictionary variables has entries with - keys = actual name of the code elements Doxygen found in the .H files. - values = XML node generated by `xml.etree::ElementTree` Samples: 'defines': {'ZERO_MEM_SENTINEL': , 'LV_GLOBAL_DEFAULT': , 'LV_ASSERT_OBJ': , 'LV_TRACE_OBJ_CREATE': , 'enums': {'lv_key_t': , 'lv_group_refocus_policy_t': , 'lv_obj_flag_t': , 'lv_obj_class_editable_t': , 'variables': {'lv_global': , 'lv_obj_class': , 'lv_font_montserrat_8': , 'lv_font_montserrat_10': , 'namespaces': {}, 'structures': {'_lv_anim_t::_lv_anim_path_para_t': , '_lv_anim_t': , '_lv_animimg_t': , '_lv_arc_t': , 'unions': {}, 'typedefs': {'lv_global_t': , 'lv_group_focus_cb_t': , 'lv_group_edge_cb_t': , 'functions': {'lv_group_create': , 'lv_group_delete': , 'lv_group_set_default': , Additional dictionaries: 'files': {'lv_global.h': , 'lv_group.h': , 'lv_group_private.h': , 'html_files': {'lvgl': 'lvgl.html', 'lv_api_map_v8': 'lv_api_map_v8.html', 'lv_api_map_v9_0': 'lv_api_map_v9_0.html', 'lv_api_map_v9_1': 'lv_api_map_v9_1.html', """ import os import sys from xml.etree import ElementTree as ET base_path = '' xml_path = '' 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' 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 ET.fromstring(d) except: # NOQA return None def load_xml(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( # 'void()\n ' # 'typedef void() lv_lru_free_t(void *v)', # 'void\n ' # 'typedef void(lv_lru_free_t)(void *v)' # ) # with open(fle, 'wb') as f: # f.write(d.encode('utf-8')) return ET.fromstring(d) structures = {} functions = {} enums = {} typedefs = {} variables = {} unions = {} namespaces = {} files = {} # things to remove from description # 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): _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, **_): 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 if parent and refid: root = load_xml(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 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 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): _missing = MISSING_UNION _missing_field = MISSING_UNION_FIELD template = '''\ .. doxygenunion:: {name} :project: lvgl ''' 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) class VARIABLE(object): template = '''\ .. doxygenvariable:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): 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(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): template = '''\ .. doxygennamespace:: {name} :project: lvgl :members: :protected-members: :private-members: :undoc-members: ''' def __init__(self, parent, refid, name, **_): 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 FUNC_ARG(object): def __init__(self, name, type): self.name = name self.type = type self.description = None groups = {} class GROUP(object): template = '''\ .. doxygengroup:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): 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 FUNCTION(object): template = '''\ .. doxygenfunction:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): 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(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): def __init__(self, _, refid, name, node, **__): 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] enums_ = [] for member in node: if member.tag != 'member': continue cls = globals()[member.attrib['kind'].upper()] 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 ENUM(object): template = '''\ .. doxygenenum:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): if name in enums: 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(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) defines = {} 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 DEFINE(object): template = '''\ .. doxygendefine:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): 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(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'] != '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 ENUMVALUE(object): 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 def __str__(self): return self.template.format(name=self.name) class TYPEDEF(object): template = '''\ .. doxygentypedef:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): 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(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) classes = {} class CLASS(object): def __init__(self, _, refid, name, node, **__): 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) lvgl_src_path = '' api_path = '' html_files = {} def iter_src(n, p): if p: out_path = os.path.join(api_path, p) else: out_path = api_path index_file = None if p: src_path = os.path.join(lvgl_src_path, p) else: src_path = lvgl_src_path folders = [] for file in os.listdir(src_path): if 'private' in file: continue if os.path.isdir(os.path.join(src_path, file)): folders.append((file, os.path.join(p, file))) continue if not file.endswith('.h'): continue if not os.path.exists(out_path): os.makedirs(out_path) if index_file is None: index_file = open(os.path.join(out_path, 'index.rst'), 'w') if n: index_file.write('=' * len(n)) index_file.write('\n' + n + '\n') index_file.write('=' * len(n)) index_file.write('\n\n\n') index_file.write('.. toctree::\n :maxdepth: 2\n\n') name = os.path.splitext(file)[0] index_file.write(' ' + name + '\n') rst_file = os.path.join(out_path, name + '.rst') html_file = os.path.join(p, name + '.html') html_files[name] = html_file with open(rst_file, 'w') as f: f.write('.. _{0}_h:'.format(name)) f.write('\n\n') f.write('=' * len(file)) f.write('\n') f.write(file) f.write('\n') f.write('=' * len(file)) f.write('\n\n\n') f.write('.. doxygenfile:: ' + file) f.write('\n') f.write(' :project: lvgl') f.write('\n\n') for name, folder in folders: if iter_src(name, folder): if index_file is None: index_file = open(os.path.join(out_path, 'index.rst'), 'w') if n: index_file.write('=' * len(n)) index_file.write('\n' + n + '\n') index_file.write('=' * len(n)) index_file.write('\n\n\n') index_file.write('.. toctree::\n :maxdepth: 2\n\n') index_file.write(' ' + os.path.split(folder)[-1] + '/index\n') if index_file is not None: index_file.write('\n') index_file.close() return True return False def clean_name(nme): # 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 is_name_match(item_name, obj_name): # Handle error: # AttributeError: 'NoneType' object has no attribute 'split' if obj_name is None: return False u_num = 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) < u_num: return False obj_name = '_'.join(obj_name[:u_num]) return item_name == obj_name def get_includes(name1, name2, obj, includes): name2 = clean_name(name2) if not 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 html_files: return includes.add((header_file, html_files[header_file])) class XMLSearch(object): def __init__(self, temp_directory): global xml_path import subprocess import re import sys bp = os.path.abspath(os.path.dirname(__file__)) lvgl_path = os.path.join(temp_directory, 'lvgl') src_path = os.path.join(lvgl_path, 'src') doxy_path = os.path.join(bp, 'Doxyfile') with open(doxy_path, 'rb') as f: data = f.read().decode('utf-8') data = data.replace( '#*#*LV_CONF_PATH*#*#', os.path.join(temp_directory, 'lv_conf.h') ) data = data.replace('*#*#SRC#*#*', '"{0}"'.format(src_path)) with open(os.path.join(temp_directory, 'Doxyfile'), 'wb') as f: f.write(data.encode('utf-8')) # ----------------------------------------------------------------- # Populate LVGL_URLPATH and LVGL_GITCOMMIT environment variables: # - LVGL_URLPATH <= 'master' or '8.4' '9.2' etc. # - LVGL_GITCOMMIT <= commit hash of HEAD. # The previous version of this was populating LVGL_URLPATH with # the multi-line list of all existing branches in the repository, # which was not what was intended. # ----------------------------------------------------------------- status, branch = subprocess.getstatusoutput("git branch --show-current") _, gitcommit = subprocess.getstatusoutput("git rev-parse HEAD") # If above failed (i.e. `branch` not valid), default to 'master'. if status != 0: branch = 'master' elif branch == 'master': # Expected in most cases. Nothing to change. pass else: # `branch` is valid. Capture release version if in a 'release/' branch. if branch.startswith('release/'): branch = branch[8:] else: # Default to 'master'. branch = 'master' os.environ['LVGL_URLPATH'] = branch os.environ['LVGL_GITCOMMIT'] = gitcommit # --------------------------------------------------------------------- # Provide a way to run an external command and abort build on error. # # This is necessary because when tempdir created by tempfile.mkdtemp()` # is on a different drive, the "cd tmpdir && doxygen Doxyfile" syntax # fails because of the different semantics of the `cd` command on # Windows: it doesn't change the default DRIVE if `cd` is executed # from a different drive. The result, when this is the case, is that # Doxygen runs in the current working directory instead of in the # temporary directory as was intended. # --------------------------------------------------------------------- def cmd(cmd_str, start_dir=None): if start_dir is None: start_dir = os.getcwd() saved_dir = os.getcwd() os.chdir(start_dir) # This method of running Doxygen is used because if it # succeeds, 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 here # because `sys.stdout.write()` requires a string, # and `out` by itself is a byte array -- it causes # it to generate an exception and abort the script. sys.stdout.write(out.decode("utf-8")) sys.stdout.flush() if err: sys.stderr.write(err.decode("utf-8")) sys.stdout.flush() sys.exit(p.returncode) # If execution arrived here, Doxygen exited with code 0. os.chdir(saved_dir) # ----------------------------------------------------------------- # Run Doxygen in temporary directory. # ----------------------------------------------------------------- cmd('doxygen Doxyfile', temp_directory) xml_path = os.path.join(temp_directory, 'xml') self.index = load_xml('index') for compound in self.index: compound.attrib['name'] = compound[0].text.strip() if compound.attrib['kind'] in ('example', 'page', 'dir'): continue globals()[compound.attrib['kind'].upper()]( None, node=compound, **compound.attrib ) def get_macros(self): return list(defines.values()) def get_enum_item(self, e_name): 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): return enums.get(e_name, None) def get_function(self, f_name): return functions.get(f_name, None) def get_variable(self, v_name): return variables.get(v_name, None) def get_union(self, u_name): return unions.get(u_name, None) def get_structure(self, s_name): return structures.get(s_name, None) def get_typedef(self, t_name): return typedefs.get(t_name, None) def get_macro(self, m_name): return defines.get(m_name, None) def announce(*args): args = ' '.join(repr(arg) for arg in args) print(f'{os.path.basename(__file__)}: ', args) def run(project_path, temp_directory, *doc_paths): """ This function does 2 things: 1. Generates .RST files for the LVGL header files that will have API pages generated for them. It places these in /API/... following the /src/ directory structure. 2. Add Sphinx hyperlinks to the end of source .RST files found in the `doc_paths` array directories, whose file-name stems match code-element names found by Doxygen. :param project_path: platform-appropriate path to LVGL root directory :param temp_directory: platform-appropriate path to temp dir being operated on :param doc_paths: list of platform-appropriate paths to find source .RST files. :return: n/a """ global base_path global xml_path global lvgl_src_path global api_path base_path = temp_directory xml_path = os.path.join(base_path, 'xml') api_path = os.path.join(base_path, 'API') lvgl_src_path = os.path.join(project_path, 'src') announce("Generating API documentation .RST files...") if not os.path.exists(api_path): os.makedirs(api_path) # Generate .RST files for API pages. iter_src('API', '') # Load index.xml -- core of what was generated by Doxygen. index = load_xml('index') # Populate these dictionaries. # Keys : C-code-element names (str) found by Doxygen. # Values: The XML-node created by `xml.etree::ElementTree` in `load_xml()` above. # # - defines, # - enums, # - variables, # - namespaces, # - structures, # - unions, # - typedefs, # - functions. announce("Building source-code symbol tables...") for compound in index: compound.attrib['name'] = compound[0].text.strip() if compound.attrib['kind'] in ('example', 'page', 'dir'): continue # This below highly-compressed command effectively does this: # # namespace_dict = globals() # compound_elem_kind_upper = compound.attrib['kind'].upper() # e.g. 'FUNCTION' # class_obj = namespace_dict['FUNCTION'] # # In each case of `class_obj`, the __init__ args are: # # (self, parent, refid, name, **_) # # So we get... # attrib_keyword_args = **compound.attrib # # Passing (**compound.attrib) as an argument creates and # # passes a set of keyword arguments produced from the # # dictionary `compound.attrib`. # new_obj = class_obj(None, node=compound, attrib_keyword_args) # # Note carefully that `new_obj` gets thrown away, but the new object created # doesn't go away because during its execution of __init__(), the new object # adds itself to the global dictionary matching its "kind": # # Class Dictionary New Object Adds Itself To # ------------ | ------------------------------------ # - DEFINE => `defines` # - ENUM => `enums` # - VARIABLE => `variables` # - NAMESPACE => `namespaces` # - STRUCT => `structures` # - UNION appears to have a different purpose # - TYPEDEF => `typedefs` # - FUNCTION => `functions` # - GROUP => `groups` # - FILE => `files` # - CLASS => `classes` # # Populating these dictionaries takes quite a while: # ~18-seconds on a medium-speed system. globals()[compound.attrib['kind'].upper()]( None, node=compound, **compound.attrib ) # For each directory entry in `doc_paths` array... announce("Adding API-page hyperlinks to source docs...") for folder in doc_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 container in ( defines, enums, variables, namespaces, structures, unions, typedefs, functions ): for n, o in container.items(): get_includes(stem, n, o, html_includes) if html_includes: # Convert `html_includes` set to a list of strings containing the # Sphinx hyperlink syntax "link references". 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 # references 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('.. Autogenerated', 1)[0] data += '.. Autogenerated\n\n' data += output with open(path, 'wb') as f: f.write(data.encode('utf-8'))