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
19 from builtins import str
20 from builtins import range
21 from builtins import object
31 # list files here which should not be parsed
32 ignore_files = [ 'components/mdns/test_afl_fuzz_host/esp32_compat.h' ]
34 # add directories here which should not be parsed
35 ignore_dirs = ( 'examples' )
37 # macros from here have higher priorities in case of collisions
38 priority_headers = [ 'components/esp32/include/esp_err.h' ]
40 err_dict = collections.defaultdict(list) #identified errors are stored here; mapped by the error code
41 rev_err_dict = dict() #map of error string to error code
42 unproc_list = list() #errors with unknown codes which depend on other errors
44 class ErrItem(object):
46 Contains information about the error:
48 - file - relative path inside the IDF project to the file which defines this error
49 - comment - (optional) comment for the error
50 - rel_str - (optional) error string which is a base for the error
51 - rel_off - (optional) offset in relation to the base error
53 def __init__(self, name, file, comment, rel_str = "", rel_off = 0):
56 self.comment = comment
57 self.rel_str = rel_str
58 self.rel_off = rel_off
60 ret = self.name + " from " + self.file
61 if (self.rel_str != ""):
62 ret += " is (" + self.rel_str + " + " + str(self.rel_off) + ")"
63 if self.comment != "":
64 ret += " // " + self.comment
66 def __cmp__(self, other):
67 if self.file in priority_headers and other.file not in priority_headers:
69 elif self.file not in priority_headers and other.file in priority_headers:
74 if self.file == other.file:
75 if self.name.endswith(base) and not(other.name.endswith(base)):
77 elif not(self.name.endswith(base)) and other.name.endswith(base):
80 self_key = self.file + self.name
81 other_key = other.file + other.name
82 if self_key < other_key:
84 elif self_key > other_key:
89 class InputError(RuntimeError):
91 Represents and error on the input
93 def __init__(self, p, e):
94 super(InputError, self).__init__(p + ": " + e)
96 def process(line, idf_path):
98 Process a line of text from file idf_path (relative to IDF project).
99 Fills the global list unproc_list and dictionaries err_dict, rev_err_dict
101 if idf_path.endswith(".c"):
102 # We would not try to include a C file
103 raise InputError(idf_path, "This line should be in a header file: %s" % line)
105 words = re.split(r' +', line, 2)
106 # words[1] is the error name
107 # words[2] is the rest of the line (value, base + value, comment)
109 raise InputError(idf_path, "Error at line %s" % line)
115 # identify possible comment
116 m = re.search(r'/\*!<(.+?(?=\*/))', todo_str)
118 comment = m.group(1).strip()
119 todo_str = todo_str[:m.start()].strip() # keep just the part before the comment
121 # identify possible parentheses ()
122 m = re.search(r'\((.+)\)', todo_str)
124 todo_str = m.group(1) #keep what is inside the parentheses
126 # identify BASE error code, e.g. from the form BASE + 0x01
127 m = re.search(r'\s*(\w+)\s*\+(.+)', todo_str)
129 related = m.group(1) # BASE
130 todo_str = m.group(2) # keep and process only what is after "BASE +"
132 # try to match a hexadecimal number
133 m = re.search(r'0x([0-9A-Fa-f]+)', todo_str)
135 num = int(m.group(1), 16)
137 # Try to match a decimal number. Negative value is possible for some numbers, e.g. ESP_FAIL
138 m = re.search(r'(-?[0-9]+)', todo_str)
140 num = int(m.group(1), 10)
141 elif re.match(r'\w+', todo_str):
142 # It is possible that there is no number, e.g. #define ERROR BASE
143 related = todo_str # BASE error
146 raise InputError(idf_path, "Cannot parse line %s" % line)
151 # The value of the error is known at this moment because it do not depends on some other BASE error code
152 err_dict[num].append(ErrItem(words[1], idf_path, comment))
153 rev_err_dict[words[1]] = num
155 # Store the information available now and compute the error code later
156 unproc_list.append(ErrItem(words[1], idf_path, comment, related, num))
158 def process_remaining_errors():
160 Create errors which could not be processed before because the error code
161 for the BASE error code wasn't known.
162 This works for sure only if there is no multiple-time dependency, e.g.:
164 #define BASE2 (BASE1 + 10)
165 #define ERROR (BASE2 + 10) - ERROR will be processed successfully only if it processed later than BASE2
167 for item in unproc_list:
168 if item.rel_str in rev_err_dict:
169 base_num = rev_err_dict[item.rel_str]
170 base = err_dict[base_num][0]
171 num = base_num + item.rel_off
172 err_dict[num].append(ErrItem(item.name, item.file, item.comment))
173 rev_err_dict[item.name] = num
175 print(item.rel_str + " referenced by " + item.name + " in " + item.file + " is unknown")
179 def path_to_include(path):
181 Process the path (relative to the IDF project) in a form which can be used
182 to include in a C file. Using just the filename does not work all the
183 time because some files are deeper in the tree. This approach tries to
184 find an 'include' parent directory an include its subdirectories, e.g.
185 "components/XY/include/esp32/file.h" will be transported into "esp32/file.h"
186 So this solution works only works when the subdirectory or subdirectories
187 are inside the "include" directory. Other special cases need to be handled
188 here when the compiler gives an unknown header file error message.
190 spl_path = path.split(os.sep)
192 i = spl_path.index('include')
194 # no include in the path -> use just the filename
195 return os.path.basename(path)
197 return os.sep.join(spl_path[i+1:]) # subdirectories and filename in "include"
199 def print_warning(error_list, error_code):
201 Print warning about errors with the same error code
203 print("[WARNING] The following errors have the same code (%d):" % error_code)
207 def max_string_width():
210 for e in err_dict[k]:
216 def generate_c_output(fin, fout):
218 Writes the output to fout based on th error dictionary err_dict and
221 # make includes unique by using a set
224 for e in err_dict[k]:
225 includes.add(path_to_include(e.file))
227 # The order in a set in non-deterministic therefore it could happen that the
228 # include order will be different in other machines and false difference
229 # in the output file could be reported. In order to avoid this, the items
230 # are sorted in a list.
231 include_list = list(includes)
234 max_width = max_string_width() + 17 + 1 # length of " ERR_TBL_IT()," with spaces is 17
235 max_decdig = max(len(str(k)) for k in err_dict)
238 if re.match(r'@COMMENT@', line):
239 fout.write("//Do not edit this file because it is autogenerated by " + os.path.basename(__file__) + "\n")
241 elif re.match(r'@HEADERS@', line):
242 for i in include_list:
243 fout.write("#if __has_include(\"" + i + "\")\n#include \"" + i + "\"\n#endif\n")
244 elif re.match(r'@ERROR_ITEMS@', line):
246 for k in sorted(err_dict.keys()):
247 if len(err_dict[k]) > 1:
248 err_dict[k].sort(key=functools.cmp_to_key(ErrItem.__cmp__))
249 print_warning(err_dict[k], k)
250 for e in err_dict[k]:
251 if e.file != last_file:
253 fout.write(" // %s\n" % last_file)
254 table_line = (" ERR_TBL_IT(" + e.name + "), ").ljust(max_width) + "/* " + str(k).rjust(max_decdig)
255 fout.write("# ifdef %s\n" % e.name)
256 fout.write(table_line)
258 if k > 0: # negative number and zero should be only ESP_FAIL and ESP_OK
260 hexnum_length = len(hexnum)
263 if len(e.comment) < 50:
264 fout.write(" %s" % e.comment)
266 indent = " " * (len(table_line) + hexnum_length + 1)
267 w = textwrap.wrap(e.comment, width=120, initial_indent = indent, subsequent_indent = indent)
268 # this couldn't be done with initial_indent because there is no initial_width option
269 fout.write(" %s" % w[0].strip())
270 for i in range(1, len(w)):
271 fout.write("\n%s" % w[i])
272 fout.write(" */\n# endif\n")
276 def generate_rst_output(fout):
277 for k in sorted(err_dict.keys()):
279 fout.write(':c:macro:`{}` '.format(v.name))
281 fout.write('**(0x{:x})**'.format(k))
283 fout.write('({:d})'.format(k))
284 if len(v.comment) > 0:
285 fout.write(': {}'.format(v.comment))
289 if 'IDF_PATH' in os.environ:
290 idf_path = os.environ['IDF_PATH']
292 idf_path = os.path.realpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
294 parser = argparse.ArgumentParser(description='ESP32 esp_err_to_name lookup generator for esp_err_t')
295 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')
296 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')
297 parser.add_argument('--rst_output', help='Generate .rst output and save it into this file')
298 args = parser.parse_args()
300 for root, dirnames, filenames in os.walk(idf_path):
301 for filename in fnmatch.filter(filenames, '*.[ch]'):
302 full_path = os.path.join(root, filename)
303 path_in_idf = os.path.relpath(full_path, idf_path)
304 if path_in_idf in ignore_files or path_in_idf.startswith(ignore_dirs):
306 with open(full_path, encoding='utf-8') as f:
309 # match also ESP_OK and ESP_FAIL because some of ESP_ERRs are referencing them
310 if re.match(r"\s*#define\s+(ESP_ERR_|ESP_OK|ESP_FAIL)", line):
312 process(line.strip(), path_in_idf)
313 except InputError as e:
315 except UnicodeDecodeError:
316 raise ValueError("The encoding of {} is not Unicode.".format(path_in_idf))
318 process_remaining_errors()
320 if args.rst_output is not None:
321 with open(args.rst_output, 'w', encoding='utf-8') as fout:
322 generate_rst_output(fout)
324 with open(args.c_input, 'r', encoding='utf-8') as fin, open(args.c_output, 'w', encoding='utf-8') as fout:
325 generate_c_output(fin, fout)
327 if __name__ == "__main__":