1# Extension to generate Doxygen XML include files, with IDF config & soc macros included 2from __future__ import print_function, unicode_literals 3 4import os 5import os.path 6import re 7import subprocess 8from io import open 9 10from .util import copy_if_modified 11 12ALL_KINDS = [ 13 ('function', 'Functions'), 14 ('union', 'Unions'), 15 ('struct', 'Structures'), 16 ('define', 'Macros'), 17 ('typedef', 'Type Definitions'), 18 ('enum', 'Enumerations') 19] 20"""list of items that will be generated for a single API file 21""" 22 23 24def setup(app): 25 # The idf_build_system extension will emit this event once it has generated documentation macro definitions 26 app.connect('idf-defines-generated', generate_doxygen) 27 return {'parallel_read_safe': True, 'parallel_write_safe': True, 'version': '0.2'} 28 29 30def generate_doxygen(app, defines): 31 build_dir = os.path.dirname(app.doctreedir.rstrip(os.sep)) 32 33 # Call Doxygen to get XML files from the header files 34 print('Calling Doxygen to generate latest XML files') 35 doxy_env = os.environ 36 doxy_env.update({ 37 'ENV_DOXYGEN_DEFINES': ' '.join('{}={}'.format(key, value) for key, value in defines.items()), 38 'IDF_PATH': app.config.idf_path, 39 'IDF_TARGET': app.config.idf_target, 40 }) 41 doxyfile_dir = os.path.join(app.config.docs_root, 'doxygen') 42 doxyfile_main = os.path.join(doxyfile_dir, 'Doxyfile_common') 43 doxyfile_target = os.path.join(doxyfile_dir, 'Doxyfile_' + app.config.idf_target) 44 print('Running doxygen with doxyfiles {} and {}'.format(doxyfile_main, doxyfile_target)) 45 46 # It's possible to have doxygen log warnings to a file using WARN_LOGFILE directive, 47 # but in some cases it will still log an error to stderr and return success! 48 # 49 # So take all of stderr and redirect it to a logfile (will contain warnings and errors) 50 logfile = os.path.join(build_dir, 'doxygen-warning-log.txt') 51 52 with open(logfile, 'w') as f: 53 # note: run Doxygen in the build directory, so the xml & xml_in files end up in there 54 subprocess.check_call(['doxygen', doxyfile_main], env=doxy_env, cwd=build_dir, stderr=f) 55 56 # Doxygen has generated XML files in 'xml' directory. 57 # Copy them to 'xml_in', only touching the files which have changed. 58 copy_if_modified(os.path.join(build_dir, 'xml/'), os.path.join(build_dir, 'xml_in/')) 59 60 # Generate 'api_name.inc' files from the Doxygen XML files 61 doxygen_paths = [doxyfile_main, doxyfile_target] 62 convert_api_xml_to_inc(app, doxygen_paths) 63 64 65def convert_api_xml_to_inc(app, doxyfiles): 66 """ Generate header_file.inc files 67 with API reference made of doxygen directives 68 for each header file 69 specified in the 'INPUT' statement of the Doxyfile. 70 """ 71 build_dir = app.config.build_dir 72 73 xml_directory_path = '{}/xml'.format(build_dir) 74 inc_directory_path = '{}/inc'.format(build_dir) 75 76 if not os.path.isdir(xml_directory_path): 77 raise RuntimeError('Directory {} does not exist!'.format(xml_directory_path)) 78 79 if not os.path.exists(inc_directory_path): 80 os.makedirs(inc_directory_path) 81 82 header_paths = [p for d in doxyfiles for p in get_doxyfile_input_paths(app, d)] 83 84 print("Generating 'api_name.inc' files with Doxygen directives") 85 for header_file_path in header_paths: 86 api_name = get_api_name(header_file_path) 87 inc_file_path = inc_directory_path + '/' + api_name + '.inc' 88 rst_output = generate_directives(header_file_path, xml_directory_path) 89 90 previous_rst_output = '' 91 if os.path.isfile(inc_file_path): 92 with open(inc_file_path, 'r', encoding='utf-8') as inc_file_old: 93 previous_rst_output = inc_file_old.read() 94 95 if previous_rst_output != rst_output: 96 with open(inc_file_path, 'w', encoding='utf-8') as inc_file: 97 inc_file.write(rst_output) 98 99 100def get_doxyfile_input_paths(app, doxyfile_path): 101 """Get contents of Doxyfile's INPUT statement. 102 103 Returns: 104 Contents of Doxyfile's INPUT. 105 106 """ 107 if not os.path.isfile(doxyfile_path): 108 raise RuntimeError("Doxyfile '{}' does not exist!".format(doxyfile_path)) 109 110 print("Getting Doxyfile's INPUT") 111 112 with open(doxyfile_path, 'r', encoding='utf-8') as input_file: 113 line = input_file.readline() 114 # read contents of Doxyfile until 'INPUT' statement 115 while line: 116 if line.find('INPUT') == 0: 117 break 118 line = input_file.readline() 119 120 doxyfile_INPUT = [] 121 line = input_file.readline() 122 # skip input_file contents until end of 'INPUT' statement 123 while line: 124 if line.isspace(): 125 # we have reached the end of 'INPUT' statement 126 break 127 # process only lines that are not comments 128 if line.find('#') == -1: 129 # extract header file path inside components folder 130 m = re.search('components/(.*\.h)', line) # noqa: W605 - regular expression 131 header_file_path = m.group(1) 132 133 # Replace env variable used for multi target header 134 header_file_path = header_file_path.replace('$(IDF_TARGET)', app.config.idf_target) 135 136 doxyfile_INPUT.append(header_file_path) 137 138 # proceed reading next line 139 line = input_file.readline() 140 141 return doxyfile_INPUT 142 143 144def get_api_name(header_file_path): 145 """Get name of API from header file path. 146 147 Args: 148 header_file_path: path to the header file. 149 150 Returns: 151 The name of API. 152 153 """ 154 api_name = '' 155 regex = r'.*/(.*)\.h' 156 m = re.search(regex, header_file_path) 157 if m: 158 api_name = m.group(1) 159 160 return api_name 161 162 163def generate_directives(header_file_path, xml_directory_path): 164 """Generate API reference with Doxygen directives for a header file. 165 166 Args: 167 header_file_path: a path to the header file with API. 168 169 Returns: 170 Doxygen directives for the header file. 171 172 """ 173 174 api_name = get_api_name(header_file_path) 175 176 # in XLT file name each "_" in the api name is expanded by Doxygen to "__" 177 xlt_api_name = api_name.replace('_', '__') 178 xml_file_path = '%s/%s_8h.xml' % (xml_directory_path, xlt_api_name) 179 180 rst_output = '' 181 rst_output = ".. File automatically generated by 'gen-dxd.py'\n" 182 rst_output += '\n' 183 rst_output += get_rst_header('Header File') 184 rst_output += '* :component_file:`' + header_file_path + '`\n' 185 rst_output += '\n' 186 187 try: 188 import xml.etree.cElementTree as ET 189 except ImportError: 190 import xml.etree.ElementTree as ET 191 192 tree = ET.ElementTree(file=xml_file_path) 193 for kind, label in ALL_KINDS: 194 rst_output += get_directives(tree, kind) 195 196 return rst_output 197 198 199def get_rst_header(header_name): 200 """Get rst formatted code with a header. 201 202 Args: 203 header_name: name of header. 204 205 Returns: 206 Formatted rst code with the header. 207 208 """ 209 210 rst_output = '' 211 rst_output += header_name + '\n' 212 rst_output += '^' * len(header_name) + '\n' 213 rst_output += '\n' 214 215 return rst_output 216 217 218def select_unions(innerclass_list): 219 """Select unions from innerclass list. 220 221 Args: 222 innerclass_list: raw list with unions and structures 223 extracted from Dogygen's xml file. 224 225 Returns: 226 Doxygen directives with unions selected from the list. 227 228 """ 229 230 rst_output = '' 231 for line in innerclass_list.splitlines(): 232 # union is denoted by "union" at the beginning of line 233 if line.find('union') == 0: 234 union_id, union_name = re.split(r'\t+', line) 235 rst_output += '.. doxygenunion:: ' 236 rst_output += union_name 237 rst_output += '\n' 238 239 return rst_output 240 241 242def select_structs(innerclass_list): 243 """Select structures from innerclass list. 244 245 Args: 246 innerclass_list: raw list with unions and structures 247 extracted from Dogygen's xml file. 248 249 Returns: 250 Doxygen directives with structures selected from the list. 251 Note: some structures are excluded as described on code below. 252 253 """ 254 255 rst_output = '' 256 for line in innerclass_list.splitlines(): 257 # structure is denoted by "struct" at the beginning of line 258 if line.find('struct') == 0: 259 # skip structures that are part of union 260 # they are documented by 'doxygenunion' directive 261 if line.find('::') > 0: 262 continue 263 struct_id, struct_name = re.split(r'\t+', line) 264 rst_output += '.. doxygenstruct:: ' 265 rst_output += struct_name 266 rst_output += '\n' 267 rst_output += ' :members:\n' 268 rst_output += '\n' 269 270 return rst_output 271 272 273def get_directives(tree, kind): 274 """Get directives for specific 'kind'. 275 276 Args: 277 tree: the ElementTree 'tree' of XML by Doxygen 278 kind: name of API "kind" to be generated 279 280 Returns: 281 Doxygen directives for selected 'kind'. 282 Note: the header with "kind" name is included. 283 284 """ 285 286 rst_output = '' 287 if kind in ['union', 'struct']: 288 innerclass_list = '' 289 for elem in tree.iterfind('compounddef/innerclass'): 290 innerclass_list += elem.attrib['refid'] + '\t' + elem.text + '\n' 291 if kind == 'union': 292 rst_output += select_unions(innerclass_list) 293 else: 294 rst_output += select_structs(innerclass_list) 295 else: 296 for elem in tree.iterfind( 297 'compounddef/sectiondef/memberdef[@kind="%s"]' % kind): 298 name = elem.find('name') 299 rst_output += '.. doxygen%s:: ' % kind 300 rst_output += name.text + '\n' 301 if rst_output: 302 all_kinds_dict = dict(ALL_KINDS) 303 rst_output = get_rst_header(all_kinds_dict[kind]) + rst_output + '\n' 304 305 return rst_output 306