run_doxygen.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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 glob
  6. import os
  7. import os.path
  8. import re
  9. import subprocess
  10. from .util import copy_if_modified
  11. ALL_KINDS = [
  12. ("function", "Functions"),
  13. ("union", "Unions"),
  14. ("struct", "Structures"),
  15. ("define", "Macros"),
  16. ("typedef", "Type Definitions"),
  17. ("enum", "Enumerations")
  18. ]
  19. """list of items that will be generated for a single API file
  20. """
  21. def setup(app):
  22. # The idf_build_system extension will emit this event once it
  23. app.connect('idf-info', generate_doxygen)
  24. return {'parallel_read_safe': True, 'parallel_write_safe': True, 'version': '0.1'}
  25. def _parse_defines(header_path):
  26. defines = {}
  27. # Note: we run C preprocessor here without any -I arguments, so assumption is
  28. # that these headers are all self-contained and don't include any other headers
  29. # not in the same directory
  30. print("Reading macros from %s..." % (header_path))
  31. processed_output = subprocess.check_output(["xtensa-esp32-elf-gcc", "-dM", "-E", header_path]).decode()
  32. for line in processed_output.split("\n"):
  33. line = line.strip()
  34. m = re.search("#define ([^ ]+) ?(.*)", line)
  35. if m and not m.group(1).startswith("_"):
  36. defines[m.group(1)] = m.group(2)
  37. return defines
  38. def generate_doxygen(app, project_description):
  39. build_dir = os.path.dirname(app.doctreedir.rstrip(os.sep))
  40. # Parse kconfig macros to pass into doxygen
  41. #
  42. # TODO: this should use the set of "config which can't be changed" eventually,
  43. # not the header
  44. defines = _parse_defines(os.path.join(project_description["build_dir"],
  45. "config", "sdkconfig.h"))
  46. # Add all SOC _caps.h headers to the defines
  47. #
  48. # kind of a hack, be nicer to add a component info dict in project_description.json
  49. soc_path = [p for p in project_description["build_component_paths"] if p.endswith("/soc")][0]
  50. for soc_header in glob.glob(os.path.join(soc_path, project_description["target"],
  51. "include", "soc", "*_caps.h")):
  52. defines.update(_parse_defines(soc_header))
  53. # Call Doxygen to get XML files from the header files
  54. print("Calling Doxygen to generate latest XML files")
  55. doxy_env = os.environ
  56. doxy_env.update({
  57. "ENV_DOXYGEN_DEFINES": " ".join(defines),
  58. "IDF_PATH": app.config.idf_path,
  59. "IDF_TARGET": app.config.idf_target,
  60. })
  61. doxyfile = os.path.join(app.config.docs_root, "Doxyfile")
  62. print("Running doxygen with doxyfile {}".format(doxyfile))
  63. # note: run Doxygen in the build directory, so the xml & xml_in files end up in there
  64. subprocess.check_call(["doxygen", doxyfile], env=doxy_env, cwd=build_dir)
  65. # Doxygen has generated XML files in 'xml' directory.
  66. # Copy them to 'xml_in', only touching the files which have changed.
  67. copy_if_modified(os.path.join(build_dir, 'xml/'), os.path.join(build_dir, 'xml_in/'))
  68. # Generate 'api_name.inc' files from the Doxygen XML files
  69. convert_api_xml_to_inc(app, doxyfile)
  70. def convert_api_xml_to_inc(app, doxyfile):
  71. """ Generate header_file.inc files
  72. with API reference made of doxygen directives
  73. for each header file
  74. specified in the 'INPUT' statement of the Doxyfile.
  75. """
  76. build_dir = app.config.build_dir
  77. xml_directory_path = "{}/xml".format(build_dir)
  78. inc_directory_path = "{}/inc".format(build_dir)
  79. if not os.path.isdir(xml_directory_path):
  80. raise RuntimeError("Directory {} does not exist!".format(xml_directory_path))
  81. if not os.path.exists(inc_directory_path):
  82. os.makedirs(inc_directory_path)
  83. header_paths = get_doxyfile_input_paths(app, doxyfile)
  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. previous_rst_output = ''
  90. if os.path.isfile(inc_file_path):
  91. with open(inc_file_path, "r", encoding='utf-8') as inc_file_old:
  92. previous_rst_output = inc_file_old.read()
  93. if previous_rst_output != rst_output:
  94. with open(inc_file_path, "w", encoding='utf-8') as inc_file:
  95. inc_file.write(rst_output)
  96. def get_doxyfile_input_paths(app, doxyfile_path):
  97. """Get contents of Doxyfile's INPUT statement.
  98. Returns:
  99. Contents of Doxyfile's INPUT.
  100. """
  101. if not os.path.isfile(doxyfile_path):
  102. raise RuntimeError("Doxyfile '{}' does not exist!".format(doxyfile_path))
  103. print("Getting Doxyfile's INPUT")
  104. with open(doxyfile_path, "r", encoding='utf-8') as input_file:
  105. line = input_file.readline()
  106. # read contents of Doxyfile until 'INPUT' statement
  107. while line:
  108. if line.find("INPUT") == 0:
  109. break
  110. line = input_file.readline()
  111. doxyfile_INPUT = []
  112. line = input_file.readline()
  113. # skip input_file contents until end of 'INPUT' statement
  114. while line:
  115. if line.isspace():
  116. # we have reached the end of 'INPUT' statement
  117. break
  118. # process only lines that are not comments
  119. if line.find("#") == -1:
  120. # extract header file path inside components folder
  121. m = re.search("components/(.*\.h)", line) # noqa: W605 - regular expression
  122. header_file_path = m.group(1)
  123. # Replace env variable used for multi target header
  124. header_file_path = header_file_path.replace("$(IDF_TARGET)", app.config.idf_target)
  125. doxyfile_INPUT.append(header_file_path)
  126. # proceed reading next line
  127. line = input_file.readline()
  128. return doxyfile_INPUT
  129. def get_api_name(header_file_path):
  130. """Get name of API from header file path.
  131. Args:
  132. header_file_path: path to the header file.
  133. Returns:
  134. The name of API.
  135. """
  136. api_name = ""
  137. regex = r".*/(.*)\.h"
  138. m = re.search(regex, header_file_path)
  139. if m:
  140. api_name = m.group(1)
  141. return api_name
  142. def generate_directives(header_file_path, xml_directory_path):
  143. """Generate API reference with Doxygen directives for a header file.
  144. Args:
  145. header_file_path: a path to the header file with API.
  146. Returns:
  147. Doxygen directives for the header file.
  148. """
  149. api_name = get_api_name(header_file_path)
  150. # in XLT file name each "_" in the api name is expanded by Doxygen to "__"
  151. xlt_api_name = api_name.replace("_", "__")
  152. xml_file_path = "%s/%s_8h.xml" % (xml_directory_path, xlt_api_name)
  153. rst_output = ""
  154. rst_output = ".. File automatically generated by 'gen-dxd.py'\n"
  155. rst_output += "\n"
  156. rst_output += get_rst_header("Header File")
  157. rst_output += "* :component_file:`" + header_file_path + "`\n"
  158. rst_output += "\n"
  159. try:
  160. import xml.etree.cElementTree as ET
  161. except ImportError:
  162. import xml.etree.ElementTree as ET
  163. tree = ET.ElementTree(file=xml_file_path)
  164. for kind, label in ALL_KINDS:
  165. rst_output += get_directives(tree, kind)
  166. return rst_output
  167. def get_rst_header(header_name):
  168. """Get rst formatted code with a header.
  169. Args:
  170. header_name: name of header.
  171. Returns:
  172. Formatted rst code with the header.
  173. """
  174. rst_output = ""
  175. rst_output += header_name + "\n"
  176. rst_output += "^" * len(header_name) + "\n"
  177. rst_output += "\n"
  178. return rst_output
  179. def select_unions(innerclass_list):
  180. """Select unions from innerclass list.
  181. Args:
  182. innerclass_list: raw list with unions and structures
  183. extracted from Dogygen's xml file.
  184. Returns:
  185. Doxygen directives with unions selected from the list.
  186. """
  187. rst_output = ""
  188. for line in innerclass_list.splitlines():
  189. # union is denoted by "union" at the beginning of line
  190. if line.find("union") == 0:
  191. union_id, union_name = re.split(r"\t+", line)
  192. rst_output += ".. doxygenunion:: "
  193. rst_output += union_name
  194. rst_output += "\n"
  195. return rst_output
  196. def select_structs(innerclass_list):
  197. """Select structures from innerclass list.
  198. Args:
  199. innerclass_list: raw list with unions and structures
  200. extracted from Dogygen's xml file.
  201. Returns:
  202. Doxygen directives with structures selected from the list.
  203. Note: some structures are excluded as described on code below.
  204. """
  205. rst_output = ""
  206. for line in innerclass_list.splitlines():
  207. # structure is denoted by "struct" at the beginning of line
  208. if line.find("struct") == 0:
  209. # skip structures that are part of union
  210. # they are documented by 'doxygenunion' directive
  211. if line.find("::") > 0:
  212. continue
  213. struct_id, struct_name = re.split(r"\t+", line)
  214. rst_output += ".. doxygenstruct:: "
  215. rst_output += struct_name
  216. rst_output += "\n"
  217. rst_output += " :members:\n"
  218. rst_output += "\n"
  219. return rst_output
  220. def get_directives(tree, kind):
  221. """Get directives for specific 'kind'.
  222. Args:
  223. tree: the ElementTree 'tree' of XML by Doxygen
  224. kind: name of API "kind" to be generated
  225. Returns:
  226. Doxygen directives for selected 'kind'.
  227. Note: the header with "kind" name is included.
  228. """
  229. rst_output = ""
  230. if kind in ["union", "struct"]:
  231. innerclass_list = ""
  232. for elem in tree.iterfind('compounddef/innerclass'):
  233. innerclass_list += elem.attrib["refid"] + "\t" + elem.text + "\n"
  234. if kind == "union":
  235. rst_output += select_unions(innerclass_list)
  236. else:
  237. rst_output += select_structs(innerclass_list)
  238. else:
  239. for elem in tree.iterfind(
  240. 'compounddef/sectiondef/memberdef[@kind="%s"]' % kind):
  241. name = elem.find('name')
  242. rst_output += ".. doxygen%s:: " % kind
  243. rst_output += name.text + "\n"
  244. if rst_output:
  245. all_kinds_dict = dict(ALL_KINDS)
  246. rst_output = get_rst_header(all_kinds_dict[kind]) + rst_output + "\n"
  247. return rst_output