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