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 # add directories here which should not be parsed
29 ignore_dirs = ( 'examples' )
31 # macros from here have higher priorities in case of collisions
32 priority_headers = [ 'components/esp32/include/esp_err.h' ]
34 err_dict = collections.defaultdict(list) #identified errors are stored here; mapped by the error code
35 rev_err_dict = dict() #map of error string to error code
36 unproc_list = list() #errors with unknown codes which depend on other errors
40 Contains information about the error:
42 - file - relative path inside the IDF project to the file which defines this error
43 - comment - (optional) comment for the error
44 - rel_str - (optional) error string which is a base for the error
45 - rel_off - (optional) offset in relation to the base error
47 def __init__(self, name, file, comment, rel_str = "", rel_off = 0):
50 self.comment = comment
51 self.rel_str = rel_str
52 self.rel_off = rel_off
54 ret = self.name + " from " + self.file
55 if (self.rel_str != ""):
56 ret += " is (" + self.rel_str + " + " + str(self.rel_off) + ")"
57 if self.comment != "":
58 ret += " // " + self.comment
60 def __cmp__(self, other):
61 if self.file in priority_headers and other.file not in priority_headers:
63 elif self.file not in priority_headers and other.file in priority_headers:
68 if self.file == other.file:
69 if self.name.endswith(base) and not(other.name.endswith(base)):
71 elif not(self.name.endswith(base)) and other.name.endswith(base):
74 self_key = self.file + self.name
75 other_key = other.file + other.name
76 if self_key < other_key:
78 elif self_key > other_key:
83 class InputError(RuntimeError):
85 Represents and error on the input
87 def __init__(self, p, e):
88 super(InputError, self).__init__(p + ": " + e)
90 def process(line, idf_path):
92 Process a line of text from file idf_path (relative to IDF project).
93 Fills the global list unproc_list and dictionaries err_dict, rev_err_dict
95 if idf_path.endswith(".c"):
96 # We would not try to include a C file
97 raise InputError(idf_path, "This line should be in a header file: %s" % line)
99 words = re.split(r' +', line, 2)
100 # words[1] is the error name
101 # words[2] is the rest of the line (value, base + value, comment)
103 raise InputError(idf_path, "Error at line %s" % line)
109 # identify possible comment
110 m = re.search(r'/\*!<(.+?(?=\*/))', todo_str)
112 comment = string.strip(m.group(1))
113 todo_str = string.strip(todo_str[:m.start()]) # keep just the part before the comment
115 # identify possible parentheses ()
116 m = re.search(r'\((.+)\)', todo_str)
118 todo_str = m.group(1) #keep what is inside the parentheses
120 # identify BASE error code, e.g. from the form BASE + 0x01
121 m = re.search(r'\s*(\w+)\s*\+(.+)', todo_str)
123 related = m.group(1) # BASE
124 todo_str = m.group(2) # keep and process only what is after "BASE +"
126 # try to match a hexadecimal number
127 m = re.search(r'0x([0-9A-Fa-f]+)', todo_str)
129 num = int(m.group(1), 16)
131 # Try to match a decimal number. Negative value is possible for some numbers, e.g. ESP_FAIL
132 m = re.search(r'(-?[0-9]+)', todo_str)
134 num = int(m.group(1), 10)
135 elif re.match(r'\w+', todo_str):
136 # It is possible that there is no number, e.g. #define ERROR BASE
137 related = todo_str # BASE error
140 raise InputError(idf_path, "Cannot parse line %s" % line)
145 # The value of the error is known at this moment because it do not depends on some other BASE error code
146 err_dict[num].append(ErrItem(words[1], idf_path, comment))
147 rev_err_dict[words[1]] = num
149 # Store the information available now and compute the error code later
150 unproc_list.append(ErrItem(words[1], idf_path, comment, related, num))
152 def process_remaining_errors():
154 Create errors which could not be processed before because the error code
155 for the BASE error code wasn't known.
156 This works for sure only if there is no multiple-time dependency, e.g.:
158 #define BASE2 (BASE1 + 10)
159 #define ERROR (BASE2 + 10) - ERROR will be processed successfully only if it processed later than BASE2
161 for item in unproc_list:
162 if item.rel_str in rev_err_dict:
163 base_num = rev_err_dict[item.rel_str]
164 base = err_dict[base_num][0]
165 num = base_num + item.rel_off
166 err_dict[num].append(ErrItem(item.name, item.file, item.comment))
167 rev_err_dict[item.name] = num
169 print(item.rel_str + " referenced by " + item.name + " in " + item.file + " is unknown")
173 def path_to_include(path):
175 Process the path (relative to the IDF project) in a form which can be used
176 to include in a C file. Using just the filename does not work all the
177 time because some files are deeper in the tree. This approach tries to
178 find an 'include' parent directory an include its subdirectories, e.g.
179 "components/XY/include/esp32/file.h" will be transported into "esp32/file.h"
180 So this solution works only works when the subdirectory or subdirectories
181 are inside the "include" directory. Other special cases need to be handled
182 here when the compiler gives an unknown header file error message.
184 spl_path = string.split(path, os.sep)
186 i = spl_path.index('include')
188 # no include in the path -> use just the filename
189 return os.path.basename(path)
191 return str(os.sep).join(spl_path[i+1:]) # subdirectories and filename in "include"
193 def print_warning(error_list, error_code):
195 Print warning about errors with the same error code
197 print("[WARNING] The following errors have the same code (%d):" % error_code)
201 def max_string_width():
203 for k in err_dict.keys():
204 for e in err_dict[k]:
210 def generate_c_output(fin, fout):
212 Writes the output to fout based on th error dictionary err_dict and
215 # make includes unique by using a set
217 for k in err_dict.keys():
218 for e in err_dict[k]:
219 includes.add(path_to_include(e.file))
221 # The order in a set in non-deterministic therefore it could happen that the
222 # include order will be different in other machines and false difference
223 # in the output file could be reported. In order to avoid this, the items
224 # are sorted in a list.
225 include_list = list(includes)
228 max_width = max_string_width() + 17 + 1 # length of " ERR_TBL_IT()," with spaces is 17
229 max_decdig = max(len(str(k)) for k in err_dict.keys())
232 if re.match(r'@COMMENT@', line):
233 fout.write("//Do not edit this file because it is autogenerated by " + os.path.basename(__file__) + "\n")
235 elif re.match(r'@HEADERS@', line):
236 for i in include_list:
237 fout.write("#if __has_include(\"" + i + "\")\n#include \"" + i + "\"\n#endif\n")
238 elif re.match(r'@ERROR_ITEMS@', line):
240 for k in sorted(err_dict.keys()):
241 if len(err_dict[k]) > 1:
243 print_warning(err_dict[k], k)
244 for e in err_dict[k]:
245 if e.file != last_file:
247 fout.write(" // %s\n" % last_file)
248 table_line = (" ERR_TBL_IT(" + e.name + "), ").ljust(max_width) + "/* " + str(k).rjust(max_decdig)
249 fout.write("# ifdef %s\n" % e.name)
250 fout.write(table_line)
252 if k > 0: # negative number and zero should be only ESP_FAIL and ESP_OK
254 hexnum_length = len(hexnum)
257 if len(e.comment) < 50:
258 fout.write(" %s" % e.comment)
260 indent = " " * (len(table_line) + hexnum_length + 1)
261 w = textwrap.wrap(e.comment, width=120, initial_indent = indent, subsequent_indent = indent)
262 # this couldn't be done with initial_indent because there is no initial_width option
263 fout.write(" %s" % w[0].strip())
264 for i in range(1, len(w)):
265 fout.write("\n%s" % w[i])
266 fout.write(" */\n# endif\n")
270 def generate_rst_output(fout):
271 for k in sorted(err_dict.keys()):
273 fout.write(':c:macro:`{}` '.format(v.name))
275 fout.write('**(0x{:x})**'.format(k))
277 fout.write('({:d})'.format(k))
278 if len(v.comment) > 0:
279 fout.write(': {}'.format(v.comment))
283 if 'IDF_PATH' in os.environ:
284 idf_path = os.environ['IDF_PATH']
286 idf_path = os.path.realpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
288 parser = argparse.ArgumentParser(description='ESP32 esp_err_to_name lookup generator for esp_err_t')
289 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')
290 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')
291 parser.add_argument('--rst_output', help='Generate .rst output and save it into this file')
292 args = parser.parse_args()
294 for root, dirnames, filenames in os.walk(idf_path):
295 for filename in fnmatch.filter(filenames, '*.[ch]'):
296 full_path = os.path.join(root, filename)
297 path_in_idf = os.path.relpath(full_path, idf_path)
298 if path_in_idf in ignore_files or path_in_idf.startswith(ignore_dirs):
300 with open(full_path, "r+") as f:
302 # match also ESP_OK and ESP_FAIL because some of ESP_ERRs are referencing them
303 if re.match(r"\s*#define\s+(ESP_ERR_|ESP_OK|ESP_FAIL)", line):
305 process(str.strip(line), path_in_idf)
306 except InputError as e:
309 process_remaining_errors()
311 if args.rst_output is not None:
312 with open(args.rst_output, 'w') as fout:
313 generate_rst_output(fout)
315 with open(args.c_input, 'r') as fin, open(args.c_output, 'w') as fout:
316 generate_c_output(fin, fout)
318 if __name__ == "__main__":