run_doxygen.py 10 KB

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