gen_esp_err_to_name.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. #!/usr/bin/env python
  2. #
  3. # SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
  4. # SPDX-License-Identifier: Apache-2.0
  5. import argparse
  6. import collections
  7. import fnmatch
  8. import functools
  9. import os
  10. import re
  11. import textwrap
  12. from io import open
  13. from typing import Any, List, Optional, TextIO
  14. # list files here which should not be parsed
  15. ignore_files: list = list()
  16. # add directories here which should not be parsed, this is a tuple since it will be used with *.startswith()
  17. ignore_dirs = (os.path.join('examples'),
  18. os.path.join('components', 'cmock', 'CMock', 'test'),
  19. os.path.join('components', 'spi_flash', 'sim'))
  20. # macros from here have higher priorities in case of collisions
  21. priority_headers = [os.path.join('components', 'esp_common', 'include', 'esp_err.h')]
  22. # The following headers won't be included. This is useful if they are permanently included from esp_err_to_name.c.in.
  23. dont_include = [os.path.join('soc', 'soc.h'),
  24. os.path.join('esp_err.h')]
  25. # Don't search the following directories, e.g. test directories
  26. exclude_search_dirs = ['test_apps', 'unit-test-app']
  27. err_dict = collections.defaultdict(list) # identified errors are stored here; mapped by the error code
  28. rev_err_dict = dict() # map of error string to error code
  29. unproc_list = list() # errors with unknown codes which depend on other errors
  30. class ErrItem(object):
  31. """
  32. Contains information about the error:
  33. - name - error string
  34. - file - relative path inside the IDF project to the file which defines this error
  35. - include_as - (optional) overwrites the include determined from file
  36. - comment - (optional) comment for the error
  37. - rel_str - (optional) error string which is a base for the error
  38. - rel_off - (optional) offset in relation to the base error
  39. """
  40. def __init__(self, name: str, file: str, include_as: Optional[Any]=None, comment: str='', rel_str: str='', rel_off: int=0) -> None:
  41. self.name = name
  42. self.file = file
  43. self.include_as = include_as
  44. self.comment = comment
  45. self.rel_str = rel_str
  46. self.rel_off = rel_off
  47. def __str__(self) -> str:
  48. ret = self.name + ' from ' + self.file
  49. if (self.rel_str != ''):
  50. ret += ' is (' + self.rel_str + ' + ' + str(self.rel_off) + ')'
  51. if self.comment != '':
  52. ret += ' // ' + self.comment
  53. return ret
  54. def __cmp__(self, other) -> int:
  55. if self.file in priority_headers and other.file not in priority_headers:
  56. return -1
  57. elif self.file not in priority_headers and other.file in priority_headers:
  58. return 1
  59. base = '_BASE'
  60. if self.file == other.file:
  61. if self.name.endswith(base) and not other.name.endswith(base):
  62. return 1
  63. elif not self.name.endswith(base) and other.name.endswith(base):
  64. return -1
  65. self_key = self.file + self.name
  66. other_key = other.file + other.name
  67. if self_key < other_key:
  68. return -1
  69. elif self_key > other_key:
  70. return 1
  71. else:
  72. return 0
  73. class InputError(RuntimeError):
  74. """
  75. Represents and error on the input
  76. """
  77. def __init__(self, p: str, e: str) -> None:
  78. super(InputError, self).__init__(p + ': ' + e)
  79. def process(line: str, idf_path: str, include_as: Any) -> None:
  80. """
  81. Process a line of text from file idf_path (relative to IDF project).
  82. Fills the global list unproc_list and dictionaries err_dict, rev_err_dict
  83. """
  84. if idf_path.endswith('.c'):
  85. # We would not try to include a C file
  86. raise InputError(idf_path, 'This line should be in a header file: %s' % line)
  87. words = re.split(r' +', line, 2)
  88. # words[1] is the error name
  89. # words[2] is the rest of the line (value, base + value, comment)
  90. if len(words) < 3:
  91. raise InputError(idf_path, 'Error at line %s' % line)
  92. line = ''
  93. todo_str = words[2]
  94. comment = ''
  95. # identify possible comment
  96. m = re.search(r'/\*!<(.+?(?=\*/))', todo_str)
  97. if m:
  98. comment = m.group(1).strip()
  99. todo_str = todo_str[:m.start()].strip() # keep just the part before the comment
  100. # identify possible parentheses ()
  101. m = re.search(r'\((.+)\)', todo_str)
  102. if m:
  103. todo_str = m.group(1) # keep what is inside the parentheses
  104. # identify BASE error code, e.g. from the form BASE + 0x01
  105. m = re.search(r'\s*(\w+)\s*\+(.+)', todo_str)
  106. if m:
  107. related = m.group(1) # BASE
  108. todo_str = m.group(2) # keep and process only what is after "BASE +"
  109. # try to match a hexadecimal number
  110. m = re.search(r'0x([0-9A-Fa-f]+)', todo_str)
  111. if m:
  112. num = int(m.group(1), 16)
  113. else:
  114. # Try to match a decimal number. Negative value is possible for some numbers, e.g. ESP_FAIL
  115. m = re.search(r'(-?[0-9]+)', todo_str)
  116. if m:
  117. num = int(m.group(1), 10)
  118. elif re.match(r'\w+', todo_str):
  119. # It is possible that there is no number, e.g. #define ERROR BASE
  120. related = todo_str # BASE error
  121. num = 0 # (BASE + 0)
  122. else:
  123. raise InputError(idf_path, 'Cannot parse line %s' % line)
  124. try:
  125. related
  126. except NameError:
  127. # The value of the error is known at this moment because it do not depends on some other BASE error code
  128. err_dict[num].append(ErrItem(words[1], idf_path, include_as, comment))
  129. rev_err_dict[words[1]] = num
  130. else:
  131. # Store the information available now and compute the error code later
  132. unproc_list.append(ErrItem(words[1], idf_path, include_as, comment, related, num))
  133. def process_remaining_errors() -> None:
  134. """
  135. Create errors which could not be processed before because the error code
  136. for the BASE error code wasn't known.
  137. This works for sure only if there is no multiple-time dependency, e.g.:
  138. #define BASE1 0
  139. #define BASE2 (BASE1 + 10)
  140. #define ERROR (BASE2 + 10) - ERROR will be processed successfully only if it processed later than BASE2
  141. """
  142. for item in unproc_list:
  143. if item.rel_str in rev_err_dict:
  144. base_num = rev_err_dict[item.rel_str]
  145. num = base_num + item.rel_off
  146. err_dict[num].append(ErrItem(item.name, item.file, item.include_as, item.comment))
  147. rev_err_dict[item.name] = num
  148. else:
  149. print(item.rel_str + ' referenced by ' + item.name + ' in ' + item.file + ' is unknown')
  150. del unproc_list[:]
  151. def path_to_include(path: str) -> str:
  152. """
  153. Process the path (relative to the IDF project) in a form which can be used
  154. to include in a C file. Using just the filename does not work all the
  155. time because some files are deeper in the tree. This approach tries to
  156. find an 'include' parent directory an include its subdirectories, e.g.
  157. "components/XY/include/esp32/file.h" will be transported into "esp32/file.h"
  158. So this solution works only works when the subdirectory or subdirectories
  159. are inside the "include" directory. Other special cases need to be handled
  160. here when the compiler gives an unknown header file error message.
  161. """
  162. spl_path = path.split(os.sep)
  163. try:
  164. i = spl_path.index('include')
  165. except ValueError:
  166. # no include in the path -> use just the filename
  167. return os.path.basename(path)
  168. else:
  169. return os.sep.join(spl_path[i + 1:]) # subdirectories and filename in "include"
  170. def print_warning(error_list: List, error_code: int) -> None:
  171. """
  172. Print warning about errors with the same error code
  173. """
  174. print('[WARNING] The following errors have the same code (%d):' % error_code)
  175. for e in error_list:
  176. print(' ' + str(e))
  177. def max_string_width() -> int:
  178. max = 0
  179. for k in err_dict:
  180. for e in err_dict[k]:
  181. x = len(e.name)
  182. if x > max:
  183. max = x
  184. return max
  185. def generate_c_output(fin: TextIO, fout: TextIO) -> None:
  186. """
  187. Writes the output to fout based on th error dictionary err_dict and
  188. template file fin.
  189. """
  190. # make includes unique by using a set
  191. includes = set()
  192. for k in err_dict:
  193. for e in err_dict[k]:
  194. if e.include_as:
  195. includes.add(e.include_as)
  196. else:
  197. includes.add(path_to_include(e.file))
  198. # The order in a set in non-deterministic therefore it could happen that the
  199. # include order will be different in other machines and false difference
  200. # in the output file could be reported. In order to avoid this, the items
  201. # are sorted in a list.
  202. include_list = list(includes)
  203. include_list.sort()
  204. max_width = max_string_width() + 17 + 1 # length of " ERR_TBL_IT()," with spaces is 17
  205. max_decdig = max(len(str(k)) for k in err_dict)
  206. for line in fin:
  207. if re.match(r'@COMMENT@', line):
  208. fout.write('//Do not edit this file because it is autogenerated by ' + os.path.basename(__file__) + '\n')
  209. elif re.match(r'@HEADERS@', line):
  210. for i in include_list:
  211. if i not in dont_include:
  212. fout.write("#if __has_include(\"" + i + "\")\n#include \"" + i + "\"\n#endif\n")
  213. elif re.match(r'@ERROR_ITEMS@', line):
  214. last_file = ''
  215. for k in sorted(err_dict.keys()):
  216. if len(err_dict[k]) > 1:
  217. err_dict[k].sort(key=functools.cmp_to_key(ErrItem.__cmp__))
  218. print_warning(err_dict[k], k)
  219. for e in err_dict[k]:
  220. if e.file != last_file:
  221. last_file = e.file
  222. fout.write(' // %s\n' % last_file)
  223. table_line = (' ERR_TBL_IT(' + e.name + '), ').ljust(max_width) + '/* ' + str(k).rjust(max_decdig)
  224. fout.write('# ifdef %s\n' % e.name)
  225. fout.write(table_line)
  226. hexnum_length = 0
  227. if k > 0: # negative number and zero should be only ESP_FAIL and ESP_OK
  228. hexnum = ' 0x%x' % k
  229. hexnum_length = len(hexnum)
  230. fout.write(hexnum)
  231. if e.comment != '':
  232. if len(e.comment) < 50:
  233. fout.write(' %s' % e.comment)
  234. else:
  235. indent = ' ' * (len(table_line) + hexnum_length + 1)
  236. w = textwrap.wrap(e.comment, width=120, initial_indent=indent, subsequent_indent=indent)
  237. # this couldn't be done with initial_indent because there is no initial_width option
  238. fout.write(' %s' % w[0].strip())
  239. for i in range(1, len(w)):
  240. fout.write('\n%s' % w[i])
  241. fout.write(' */\n# endif\n')
  242. else:
  243. fout.write(line)
  244. def generate_rst_output(fout: TextIO) -> None:
  245. for k in sorted(err_dict.keys()):
  246. v = err_dict[k][0]
  247. fout.write(':c:macro:`{}` '.format(v.name))
  248. if k > 0:
  249. fout.write('**(0x{:x})**'.format(k))
  250. else:
  251. fout.write('({:d})'.format(k))
  252. if len(v.comment) > 0:
  253. fout.write(': {}'.format(v.comment))
  254. fout.write('\n\n')
  255. def main() -> None:
  256. if 'IDF_PATH' in os.environ:
  257. idf_path = os.environ['IDF_PATH']
  258. else:
  259. idf_path = os.path.realpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
  260. parser = argparse.ArgumentParser(description='ESP32 esp_err_to_name lookup generator for esp_err_t')
  261. parser.add_argument('--c_input', help='Path to the esp_err_to_name.c.in template input.',
  262. default=idf_path + '/components/esp_common/src/esp_err_to_name.c.in')
  263. parser.add_argument('--c_output', help='Path to the esp_err_to_name.c output.', default=idf_path + '/components/esp_common/src/esp_err_to_name.c')
  264. parser.add_argument('--rst_output', help='Generate .rst output and save it into this file')
  265. args = parser.parse_args()
  266. include_as_pattern = re.compile(r'\s*//\s*{}: [^"]* "([^"]+)"'.format(os.path.basename(__file__)))
  267. define_pattern = re.compile(r'\s*#define\s+(ESP_ERR_|ESP_OK|ESP_FAIL)')
  268. for root, dirnames, filenames in os.walk(idf_path, topdown=True):
  269. # When topdown is True, we can modify the dirnames list in-place
  270. # walk() will only recurse into the subdirectories whose names remain in dirnames
  271. dirnames[:] = [d for d in dirnames if d not in exclude_search_dirs]
  272. for filename in fnmatch.filter(filenames, '*.[ch]'):
  273. full_path = os.path.join(root, filename)
  274. path_in_idf = os.path.relpath(full_path, idf_path)
  275. if path_in_idf in ignore_files or path_in_idf.startswith(ignore_dirs):
  276. continue
  277. with open(full_path, encoding='utf-8') as f:
  278. try:
  279. include_as = None
  280. for line in f:
  281. line = line.strip()
  282. m = include_as_pattern.search(line)
  283. if m:
  284. include_as = m.group(1)
  285. # match also ESP_OK and ESP_FAIL because some of ESP_ERRs are referencing them
  286. elif define_pattern.match(line):
  287. try:
  288. process(line, path_in_idf, include_as)
  289. except InputError as e:
  290. print(e)
  291. except UnicodeDecodeError:
  292. raise ValueError('The encoding of {} is not Unicode.'.format(path_in_idf))
  293. process_remaining_errors()
  294. if args.rst_output is not None:
  295. with open(args.rst_output, 'w', encoding='utf-8') as fout:
  296. generate_rst_output(fout)
  297. else:
  298. with open(args.c_input, 'r', encoding='utf-8') as fin, open(args.c_output, 'w', encoding='utf-8') as fout:
  299. generate_c_output(fin, fout)
  300. if __name__ == '__main__':
  301. main()