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