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