gen_esp_err_to_name.py 14 KB

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