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