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 - 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
64 def __init__(self, name, file, comment, rel_str = "", rel_off = 0):
67 self.comment = comment
68 self.rel_str = rel_str
69 self.rel_off = rel_off
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
77 def __cmp__(self, other):
78 if self.file in priority_headers and other.file not in priority_headers:
80 elif self.file not in priority_headers and other.file in priority_headers:
85 if self.file == other.file:
86 if self.name.endswith(base) and not(other.name.endswith(base)):
88 elif not(self.name.endswith(base)) and other.name.endswith(base):
91 self_key = self.file + self.name
92 other_key = other.file + other.name
93 if self_key < other_key:
95 elif self_key > other_key:
100 class InputError(RuntimeError):
102 Represents and error on the input
104 def __init__(self, p, e):
105 super(InputError, self).__init__(p + ": " + e)
107 def process(line, idf_path):
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
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)
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)
120 raise InputError(idf_path, "Error at line %s" % line)
126 # identify possible comment
127 m = re.search(r'/\*!<(.+?(?=\*/))', todo_str)
129 comment = m.group(1).strip()
130 todo_str = todo_str[:m.start()].strip() # keep just the part before the comment
132 # identify possible parentheses ()
133 m = re.search(r'\((.+)\)', todo_str)
135 todo_str = m.group(1) #keep what is inside the parentheses
137 # identify BASE error code, e.g. from the form BASE + 0x01
138 m = re.search(r'\s*(\w+)\s*\+(.+)', todo_str)
140 related = m.group(1) # BASE
141 todo_str = m.group(2) # keep and process only what is after "BASE +"
143 # try to match a hexadecimal number
144 m = re.search(r'0x([0-9A-Fa-f]+)', todo_str)
146 num = int(m.group(1), 16)
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)
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
157 raise InputError(idf_path, "Cannot parse line %s" % line)
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
166 # Store the information available now and compute the error code later
167 unproc_list.append(ErrItem(words[1], idf_path, comment, related, num))
169 def process_remaining_errors():
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.:
175 #define BASE2 (BASE1 + 10)
176 #define ERROR (BASE2 + 10) - ERROR will be processed successfully only if it processed later than BASE2
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
186 print(item.rel_str + " referenced by " + item.name + " in " + item.file + " is unknown")
190 def path_to_include(path):
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.
201 spl_path = path.split(os.sep)
203 i = spl_path.index('include')
205 # no include in the path -> use just the filename
206 return os.path.basename(path)
208 return os.sep.join(spl_path[i+1:]) # subdirectories and filename in "include"
210 def print_warning(error_list, error_code):
212 Print warning about errors with the same error code
214 print("[WARNING] The following errors have the same code (%d):" % error_code)
218 def max_string_width():
221 for e in err_dict[k]:
227 def generate_c_output(fin, fout):
229 Writes the output to fout based on th error dictionary err_dict and
232 # make includes unique by using a set
235 for e in err_dict[k]:
236 includes.add(path_to_include(e.file))
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)
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)
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")
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):
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:
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)
269 if k > 0: # negative number and zero should be only ESP_FAIL and ESP_OK
271 hexnum_length = len(hexnum)
274 if len(e.comment) < 50:
275 fout.write(" %s" % e.comment)
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")
287 def generate_rst_output(fout):
288 for k in sorted(err_dict.keys()):
290 fout.write(':c:macro:`{}` '.format(v.name))
292 fout.write('**(0x{:x})**'.format(k))
294 fout.write('({:d})'.format(k))
295 if len(v.comment) > 0:
296 fout.write(': {}'.format(v.comment))
300 if 'IDF_PATH' in os.environ:
301 idf_path = os.environ['IDF_PATH']
303 idf_path = os.path.realpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
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()
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):
317 with open(full_path, encoding='utf-8') as 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):
323 process(line.strip(), path_in_idf)
324 except InputError as e:
326 except UnicodeDecodeError:
327 raise ValueError("The encoding of {} is not Unicode.".format(path_in_idf))
329 process_remaining_errors()
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)
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)
338 if __name__ == "__main__":