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