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.
25 # list files here which should not be parsed
26 ignore_files = [ 'components/mdns/test_afl_fuzz_host/esp32_compat.h' ]
28 # macros from here have higher priorities in case of collisions
29 priority_headers = [ 'components/esp32/include/esp_err.h' ]
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
37 Contains information about the error:
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
44 def __init__(self, name, file, comment, rel_str = "", rel_off = 0):
47 self.comment = comment
48 self.rel_str = rel_str
49 self.rel_off = rel_off
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
57 def __cmp__(self, other):
58 if self.file in priority_headers and other.file not in priority_headers:
60 elif self.file not in priority_headers and other.file in priority_headers:
65 if self.file == other.file:
66 if self.name.endswith(base) and not(other.name.endswith(base)):
68 elif not(self.name.endswith(base)) and other.name.endswith(base):
71 self_key = self.file + self.name
72 other_key = other.file + other.name
73 if self_key < other_key:
75 elif self_key > other_key:
80 class InputError(RuntimeError):
82 Represents and error on the input
84 def __init__(self, p, e):
85 super(InputError, self).__init__(p + ": " + e)
87 def process(line, idf_path):
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
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)
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)
100 raise InputError(idf_path, "Error at line %s" % line)
106 # identify possible comment
107 m = re.search(r'/\*!<(.+?(?=\*/))', todo_str)
109 comment = string.strip(m.group(1))
110 todo_str = string.strip(todo_str[:m.start()]) # keep just the part before the comment
112 # identify possible parentheses ()
113 m = re.search(r'\((.+)\)', todo_str)
115 todo_str = m.group(1) #keep what is inside the parentheses
117 # identify BASE error code, e.g. from the form BASE + 0x01
118 m = re.search(r'\s*(\w+)\s*\+(.+)', todo_str)
120 related = m.group(1) # BASE
121 todo_str = m.group(2) # keep and process only what is after "BASE +"
123 # try to match a hexadecimal number
124 m = re.search(r'0x([0-9A-Fa-f]+)', todo_str)
126 num = int(m.group(1), 16)
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)
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
137 raise InputError(idf_path, "Cannot parse line %s" % line)
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
146 # Store the information available now and compute the error code later
147 unproc_list.append(ErrItem(words[1], idf_path, comment, related, num))
149 def process_remaining_errors():
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.:
155 #define BASE2 (BASE1 + 10)
156 #define ERROR (BASE2 + 10) - ERROR will be processed successfully only if it processed later than BASE2
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
166 print(item.rel_str + " referenced by " + item.name + " in " + item.file + " is unknown")
170 def path_to_include(path):
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.
181 spl_path = string.split(path, os.sep)
183 i = spl_path.index('include')
185 # no include in the path -> use just the filename
186 return os.path.basename(path)
188 return str(os.sep).join(spl_path[i+1:]) # subdirectories and filename in "include"
190 def print_warning(error_list, error_code):
192 Print warning about errors with the same error code
194 print("[WARNING] The following errors have the same code (%d):" % error_code)
198 def max_string_width():
200 for k in err_dict.keys():
201 for e in err_dict[k]:
207 def generate_c_output(fin, fout):
209 Writes the output to fout based on th error dictionary err_dict and
212 # make includes unique by using a set
214 for k in err_dict.keys():
215 for e in err_dict[k]:
216 includes.add(path_to_include(e.file))
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)
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())
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")
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):
237 for k in sorted(err_dict.keys()):
238 if len(err_dict[k]) > 1:
240 print_warning(err_dict[k], k)
241 for e in err_dict[k]:
242 if e.file != last_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)
249 if k > 0: # negative number and zero should be only ESP_FAIL and ESP_OK
251 hexnum_length = len(hexnum)
254 if len(e.comment) < 50:
255 fout.write(" %s" % e.comment)
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")
267 def generate_rst_output(fout):
268 for k in sorted(err_dict.keys()):
270 fout.write(':c:macro:`{}` '.format(v.name))
272 fout.write('**(0x{:x})**'.format(k))
274 fout.write('({:d})'.format(k))
275 if len(v.comment) > 0:
276 fout.write(': {}'.format(v.comment))
280 if 'IDF_PATH' in os.environ:
281 idf_path = os.environ['IDF_PATH']
283 idf_path = os.path.realpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
285 parser = argparse.ArgumentParser(description='ESP32 esp_err_to_name lookup generator for esp_err_t')
286 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')
287 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')
288 parser.add_argument('--rst_output', help='Generate .rst output and save it into this file')
289 args = parser.parse_args()
291 for root, dirnames, filenames in os.walk(idf_path):
292 for filename in fnmatch.filter(filenames, '*.[ch]'):
293 full_path = os.path.join(root, filename)
294 path_in_idf = os.path.relpath(full_path, idf_path)
295 if path_in_idf in ignore_files:
297 with open(full_path, "r+") as f:
299 # match also ESP_OK and ESP_FAIL because some of ESP_ERRs are referencing them
300 if re.match(r"\s*#define\s+(ESP_ERR_|ESP_OK|ESP_FAIL)", line):
302 process(str.strip(line), path_in_idf)
303 except InputError as e:
306 process_remaining_errors()
308 if args.rst_output is not None:
309 with open(args.rst_output, 'w') as fout:
310 generate_rst_output(fout)
312 with open(args.c_input, 'r') as fin, open(args.c_output, 'w') as fout:
313 generate_c_output(fin, fout)
315 if __name__ == "__main__":