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