1#!/usr/bin/env python 2# 3# Copyright 2018 Espressif Systems (Shanghai) PTE LTD 4# 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 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 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. 16 17from __future__ import print_function, unicode_literals 18 19import sys 20 21try: 22 from builtins import object, range, str 23except ImportError: 24 # This should not happen because the Python packages are checked before invoking this script. However, here is 25 # some output which should help if we missed something. 26 print('Import has failed probably because of the missing "future" package. Please install all the packages for ' 27 'interpreter {} from the requirements.txt file.'.format(sys.executable)) 28 # The path to requirements.txt is not provided because this script could be invoked from an IDF project (then the 29 # requirements.txt from the IDF_PATH should be used) or from the documentation project (then the requirements.txt 30 # for the documentation directory should be used). 31 sys.exit(1) 32import argparse 33import collections 34import fnmatch 35import functools 36import os 37import re 38import textwrap 39from io import open 40 41# list files here which should not be parsed 42ignore_files = [os.path.join('components', 'mdns', 'test_afl_fuzz_host', 'esp32_mock.h'), 43 # tcpip_adapter in compatibility mode from 4.1 (errors reused in esp-netif) 44 os.path.join('components', 'tcpip_adapter', 'include', 'tcpip_adapter_types.h') 45 ] 46 47# add directories here which should not be parsed, this is a tuple since it will be used with *.startswith() 48ignore_dirs = (os.path.join('examples'), 49 os.path.join('components', 'cmock', 'CMock', 'test'), 50 os.path.join('components', 'spi_flash', 'sim')) 51 52# macros from here have higher priorities in case of collisions 53priority_headers = [os.path.join('components', 'esp_common', 'include', 'esp_err.h')] 54 55# The following headers won't be included. This is useful if they are permanently included from esp_err_to_name.c.in. 56dont_include = [os.path.join('soc', 'soc.h'), 57 os.path.join('esp_err.h')] 58 59err_dict = collections.defaultdict(list) # identified errors are stored here; mapped by the error code 60rev_err_dict = dict() # map of error string to error code 61unproc_list = list() # errors with unknown codes which depend on other errors 62 63 64class ErrItem(object): 65 """ 66 Contains information about the error: 67 - name - error string 68 - file - relative path inside the IDF project to the file which defines this error 69 - include_as - (optional) overwrites the include determined from file 70 - comment - (optional) comment for the error 71 - rel_str - (optional) error string which is a base for the error 72 - rel_off - (optional) offset in relation to the base error 73 """ 74 def __init__(self, name, file, include_as=None, comment='', rel_str='', rel_off=0): 75 self.name = name 76 self.file = file 77 self.include_as = include_as 78 self.comment = comment 79 self.rel_str = rel_str 80 self.rel_off = rel_off 81 82 def __str__(self): 83 ret = self.name + ' from ' + self.file 84 if (self.rel_str != ''): 85 ret += ' is (' + self.rel_str + ' + ' + str(self.rel_off) + ')' 86 if self.comment != '': 87 ret += ' // ' + self.comment 88 return ret 89 90 def __cmp__(self, other): 91 if self.file in priority_headers and other.file not in priority_headers: 92 return -1 93 elif self.file not in priority_headers and other.file in priority_headers: 94 return 1 95 96 base = '_BASE' 97 98 if self.file == other.file: 99 if self.name.endswith(base) and not(other.name.endswith(base)): 100 return 1 101 elif not(self.name.endswith(base)) and other.name.endswith(base): 102 return -1 103 104 self_key = self.file + self.name 105 other_key = other.file + other.name 106 if self_key < other_key: 107 return -1 108 elif self_key > other_key: 109 return 1 110 else: 111 return 0 112 113 114class InputError(RuntimeError): 115 """ 116 Represents and error on the input 117 """ 118 def __init__(self, p, e): 119 super(InputError, self).__init__(p + ': ' + e) 120 121 122def process(line, idf_path, include_as): 123 """ 124 Process a line of text from file idf_path (relative to IDF project). 125 Fills the global list unproc_list and dictionaries err_dict, rev_err_dict 126 """ 127 if idf_path.endswith('.c'): 128 # We would not try to include a C file 129 raise InputError(idf_path, 'This line should be in a header file: %s' % line) 130 131 words = re.split(r' +', line, 2) 132 # words[1] is the error name 133 # words[2] is the rest of the line (value, base + value, comment) 134 if len(words) < 3: 135 raise InputError(idf_path, 'Error at line %s' % line) 136 137 line = '' 138 todo_str = words[2] 139 140 comment = '' 141 # identify possible comment 142 m = re.search(r'/\*!<(.+?(?=\*/))', todo_str) 143 if m: 144 comment = m.group(1).strip() 145 todo_str = todo_str[:m.start()].strip() # keep just the part before the comment 146 147 # identify possible parentheses () 148 m = re.search(r'\((.+)\)', todo_str) 149 if m: 150 todo_str = m.group(1) # keep what is inside the parentheses 151 152 # identify BASE error code, e.g. from the form BASE + 0x01 153 m = re.search(r'\s*(\w+)\s*\+(.+)', todo_str) 154 if m: 155 related = m.group(1) # BASE 156 todo_str = m.group(2) # keep and process only what is after "BASE +" 157 158 # try to match a hexadecimal number 159 m = re.search(r'0x([0-9A-Fa-f]+)', todo_str) 160 if m: 161 num = int(m.group(1), 16) 162 else: 163 # Try to match a decimal number. Negative value is possible for some numbers, e.g. ESP_FAIL 164 m = re.search(r'(-?[0-9]+)', todo_str) 165 if m: 166 num = int(m.group(1), 10) 167 elif re.match(r'\w+', todo_str): 168 # It is possible that there is no number, e.g. #define ERROR BASE 169 related = todo_str # BASE error 170 num = 0 # (BASE + 0) 171 else: 172 raise InputError(idf_path, 'Cannot parse line %s' % line) 173 174 try: 175 related 176 except NameError: 177 # The value of the error is known at this moment because it do not depends on some other BASE error code 178 err_dict[num].append(ErrItem(words[1], idf_path, include_as, comment)) 179 rev_err_dict[words[1]] = num 180 else: 181 # Store the information available now and compute the error code later 182 unproc_list.append(ErrItem(words[1], idf_path, include_as, comment, related, num)) 183 184 185def process_remaining_errors(): 186 """ 187 Create errors which could not be processed before because the error code 188 for the BASE error code wasn't known. 189 This works for sure only if there is no multiple-time dependency, e.g.: 190 #define BASE1 0 191 #define BASE2 (BASE1 + 10) 192 #define ERROR (BASE2 + 10) - ERROR will be processed successfully only if it processed later than BASE2 193 """ 194 for item in unproc_list: 195 if item.rel_str in rev_err_dict: 196 base_num = rev_err_dict[item.rel_str] 197 num = base_num + item.rel_off 198 err_dict[num].append(ErrItem(item.name, item.file, item.include_as, item.comment)) 199 rev_err_dict[item.name] = num 200 else: 201 print(item.rel_str + ' referenced by ' + item.name + ' in ' + item.file + ' is unknown') 202 203 del unproc_list[:] 204 205 206def path_to_include(path): 207 """ 208 Process the path (relative to the IDF project) in a form which can be used 209 to include in a C file. Using just the filename does not work all the 210 time because some files are deeper in the tree. This approach tries to 211 find an 'include' parent directory an include its subdirectories, e.g. 212 "components/XY/include/esp32/file.h" will be transported into "esp32/file.h" 213 So this solution works only works when the subdirectory or subdirectories 214 are inside the "include" directory. Other special cases need to be handled 215 here when the compiler gives an unknown header file error message. 216 """ 217 spl_path = path.split(os.sep) 218 try: 219 i = spl_path.index('include') 220 except ValueError: 221 # no include in the path -> use just the filename 222 return os.path.basename(path) 223 else: 224 return os.sep.join(spl_path[i + 1:]) # subdirectories and filename in "include" 225 226 227def print_warning(error_list, error_code): 228 """ 229 Print warning about errors with the same error code 230 """ 231 print('[WARNING] The following errors have the same code (%d):' % error_code) 232 for e in error_list: 233 print(' ' + str(e)) 234 235 236def max_string_width(): 237 max = 0 238 for k in err_dict: 239 for e in err_dict[k]: 240 x = len(e.name) 241 if x > max: 242 max = x 243 return max 244 245 246def generate_c_output(fin, fout): 247 """ 248 Writes the output to fout based on th error dictionary err_dict and 249 template file fin. 250 """ 251 # make includes unique by using a set 252 includes = set() 253 for k in err_dict: 254 for e in err_dict[k]: 255 if e.include_as: 256 includes.add(e.include_as) 257 else: 258 includes.add(path_to_include(e.file)) 259 260 # The order in a set in non-deterministic therefore it could happen that the 261 # include order will be different in other machines and false difference 262 # in the output file could be reported. In order to avoid this, the items 263 # are sorted in a list. 264 include_list = list(includes) 265 include_list.sort() 266 267 max_width = max_string_width() + 17 + 1 # length of " ERR_TBL_IT()," with spaces is 17 268 max_decdig = max(len(str(k)) for k in err_dict) 269 270 for line in fin: 271 if re.match(r'@COMMENT@', line): 272 fout.write('//Do not edit this file because it is autogenerated by ' + os.path.basename(__file__) + '\n') 273 274 elif re.match(r'@HEADERS@', line): 275 for i in include_list: 276 if i not in dont_include: 277 fout.write("#if __has_include(\"" + i + "\")\n#include \"" + i + "\"\n#endif\n") 278 elif re.match(r'@ERROR_ITEMS@', line): 279 last_file = '' 280 for k in sorted(err_dict.keys()): 281 if len(err_dict[k]) > 1: 282 err_dict[k].sort(key=functools.cmp_to_key(ErrItem.__cmp__)) 283 print_warning(err_dict[k], k) 284 for e in err_dict[k]: 285 if e.file != last_file: 286 last_file = e.file 287 fout.write(' // %s\n' % last_file) 288 table_line = (' ERR_TBL_IT(' + e.name + '), ').ljust(max_width) + '/* ' + str(k).rjust(max_decdig) 289 fout.write('# ifdef %s\n' % e.name) 290 fout.write(table_line) 291 hexnum_length = 0 292 if k > 0: # negative number and zero should be only ESP_FAIL and ESP_OK 293 hexnum = ' 0x%x' % k 294 hexnum_length = len(hexnum) 295 fout.write(hexnum) 296 if e.comment != '': 297 if len(e.comment) < 50: 298 fout.write(' %s' % e.comment) 299 else: 300 indent = ' ' * (len(table_line) + hexnum_length + 1) 301 w = textwrap.wrap(e.comment, width=120, initial_indent=indent, subsequent_indent=indent) 302 # this couldn't be done with initial_indent because there is no initial_width option 303 fout.write(' %s' % w[0].strip()) 304 for i in range(1, len(w)): 305 fout.write('\n%s' % w[i]) 306 fout.write(' */\n# endif\n') 307 else: 308 fout.write(line) 309 310 311def generate_rst_output(fout): 312 for k in sorted(err_dict.keys()): 313 v = err_dict[k][0] 314 fout.write(':c:macro:`{}` '.format(v.name)) 315 if k > 0: 316 fout.write('**(0x{:x})**'.format(k)) 317 else: 318 fout.write('({:d})'.format(k)) 319 if len(v.comment) > 0: 320 fout.write(': {}'.format(v.comment)) 321 fout.write('\n\n') 322 323 324def main(): 325 if 'IDF_PATH' in os.environ: 326 idf_path = os.environ['IDF_PATH'] 327 else: 328 idf_path = os.path.realpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) 329 330 parser = argparse.ArgumentParser(description='ESP32 esp_err_to_name lookup generator for esp_err_t') 331 parser.add_argument('--c_input', help='Path to the esp_err_to_name.c.in template input.', 332 default=idf_path + '/components/esp_common/src/esp_err_to_name.c.in') 333 parser.add_argument('--c_output', help='Path to the esp_err_to_name.c output.', default=idf_path + '/components/esp_common/src/esp_err_to_name.c') 334 parser.add_argument('--rst_output', help='Generate .rst output and save it into this file') 335 args = parser.parse_args() 336 337 include_as_pattern = re.compile(r'\s*//\s*{}: [^"]* "([^"]+)"'.format(os.path.basename(__file__))) 338 define_pattern = re.compile(r'\s*#define\s+(ESP_ERR_|ESP_OK|ESP_FAIL)') 339 340 for root, dirnames, filenames in os.walk(idf_path): 341 for filename in fnmatch.filter(filenames, '*.[ch]'): 342 full_path = os.path.join(root, filename) 343 path_in_idf = os.path.relpath(full_path, idf_path) 344 if path_in_idf in ignore_files or path_in_idf.startswith(ignore_dirs): 345 continue 346 with open(full_path, encoding='utf-8') as f: 347 try: 348 include_as = None 349 for line in f: 350 line = line.strip() 351 m = include_as_pattern.search(line) 352 if m: 353 include_as = m.group(1) 354 # match also ESP_OK and ESP_FAIL because some of ESP_ERRs are referencing them 355 elif define_pattern.match(line): 356 try: 357 process(line, path_in_idf, include_as) 358 except InputError as e: 359 print(e) 360 except UnicodeDecodeError: 361 raise ValueError('The encoding of {} is not Unicode.'.format(path_in_idf)) 362 363 process_remaining_errors() 364 365 if args.rst_output is not None: 366 with open(args.rst_output, 'w', encoding='utf-8') as fout: 367 generate_rst_output(fout) 368 else: 369 with open(args.c_input, 'r', encoding='utf-8') as fin, open(args.c_output, 'w', encoding='utf-8') as fout: 370 generate_c_output(fin, fout) 371 372 373if __name__ == '__main__': 374 main() 375