3 # Copyright 2018 Espressif Systems (Shanghai) PTE LTD
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 from __future__ import print_function
18 from __future__ import unicode_literals
21 from builtins import str
22 from builtins import range
23 from builtins import object
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).
42 # list files here which should not be parsed
43 ignore_files = [ 'components/mdns/test_afl_fuzz_host/esp32_compat.h' ]
45 # add directories here which should not be parsed
46 ignore_dirs = ( 'examples' )
48 # macros from here have higher priorities in case of collisions
49 priority_headers = [ 'components/esp32/include/esp_err.h' ]
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
55 class ErrItem(object):
57 Contains information about the error:
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
65 def __init__(self, name, file, include_as = None, comment = "", rel_str = "", rel_off = 0):
68 self.include_as = include_as
69 self.comment = comment
70 self.rel_str = rel_str
71 self.rel_off = rel_off
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
79 def __cmp__(self, other):
80 if self.file in priority_headers and other.file not in priority_headers:
82 elif self.file not in priority_headers and other.file in priority_headers:
87 if self.file == other.file:
88 if self.name.endswith(base) and not(other.name.endswith(base)):
90 elif not(self.name.endswith(base)) and other.name.endswith(base):
93 self_key = self.file + self.name
94 other_key = other.file + other.name
95 if self_key < other_key:
97 elif self_key > other_key:
102 class InputError(RuntimeError):
104 Represents and error on the input
106 def __init__(self, p, e):
107 super(InputError, self).__init__(p + ": " + e)
109 def process(line, idf_path, include_as):
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
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)
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)
122 raise InputError(idf_path, "Error at line %s" % line)
128 # identify possible comment
129 m = re.search(r'/\*!<(.+?(?=\*/))', todo_str)
131 comment = m.group(1).strip()
132 todo_str = todo_str[:m.start()].strip() # keep just the part before the comment
134 # identify possible parentheses ()
135 m = re.search(r'\((.+)\)', todo_str)
137 todo_str = m.group(1) #keep what is inside the parentheses
139 # identify BASE error code, e.g. from the form BASE + 0x01
140 m = re.search(r'\s*(\w+)\s*\+(.+)', todo_str)
142 related = m.group(1) # BASE
143 todo_str = m.group(2) # keep and process only what is after "BASE +"
145 # try to match a hexadecimal number
146 m = re.search(r'0x([0-9A-Fa-f]+)', todo_str)
148 num = int(m.group(1), 16)
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)
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
159 raise InputError(idf_path, "Cannot parse line %s" % line)
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
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))
171 def process_remaining_errors():
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.:
177 #define BASE2 (BASE1 + 10)
178 #define ERROR (BASE2 + 10) - ERROR will be processed successfully only if it processed later than BASE2
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
188 print(item.rel_str + " referenced by " + item.name + " in " + item.file + " is unknown")
192 def path_to_include(path):
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.
203 spl_path = path.split(os.sep)
205 i = spl_path.index('include')
207 # no include in the path -> use just the filename
208 return os.path.basename(path)
210 return os.sep.join(spl_path[i+1:]) # subdirectories and filename in "include"
212 def print_warning(error_list, error_code):
214 Print warning about errors with the same error code
216 print("[WARNING] The following errors have the same code (%d):" % error_code)
220 def max_string_width():
223 for e in err_dict[k]:
229 def generate_c_output(fin, fout):
231 Writes the output to fout based on th error dictionary err_dict and
234 # make includes unique by using a set
237 for e in err_dict[k]:
239 includes.add(e.include_as)
241 includes.add(path_to_include(e.file))
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)
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)
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")
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):
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:
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)
274 if k > 0: # negative number and zero should be only ESP_FAIL and ESP_OK
276 hexnum_length = len(hexnum)
279 if len(e.comment) < 50:
280 fout.write(" %s" % e.comment)
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")
292 def generate_rst_output(fout):
293 for k in sorted(err_dict.keys()):
295 fout.write(':c:macro:`{}` '.format(v.name))
297 fout.write('**(0x{:x})**'.format(k))
299 fout.write('({:d})'.format(k))
300 if len(v.comment) > 0:
301 fout.write(': {}'.format(v.comment))
305 if 'IDF_PATH' in os.environ:
306 idf_path = os.environ['IDF_PATH']
308 idf_path = os.path.realpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
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()
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)')
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):
325 with open(full_path, encoding='utf-8') as f:
330 m = include_as_pattern.search(line)
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):
336 process(line, path_in_idf, include_as)
337 except InputError as e:
339 except UnicodeDecodeError:
340 raise ValueError("The encoding of {} is not Unicode.".format(path_in_idf))
342 process_remaining_errors()
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)
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)
351 if __name__ == "__main__":