run_doxygen.py 9.7 KB

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