1# Extension to generate Doxygen XML include files, with IDF config & soc macros included
2from __future__ import print_function, unicode_literals
3
4import os
5import os.path
6import re
7import subprocess
8from io import open
9
10from .util import copy_if_modified
11
12ALL_KINDS = [
13    ('function', 'Functions'),
14    ('union', 'Unions'),
15    ('struct', 'Structures'),
16    ('define', 'Macros'),
17    ('typedef', 'Type Definitions'),
18    ('enum', 'Enumerations')
19]
20"""list of items that will be generated for a single API file
21"""
22
23
24def setup(app):
25    # The idf_build_system extension will emit this event once it has generated documentation macro definitions
26    app.connect('idf-defines-generated', generate_doxygen)
27    return {'parallel_read_safe': True, 'parallel_write_safe': True, 'version': '0.2'}
28
29
30def generate_doxygen(app, defines):
31    build_dir = os.path.dirname(app.doctreedir.rstrip(os.sep))
32
33    # Call Doxygen to get XML files from the header files
34    print('Calling Doxygen to generate latest XML files')
35    doxy_env = os.environ
36    doxy_env.update({
37        'ENV_DOXYGEN_DEFINES': ' '.join('{}={}'.format(key, value) for key, value in defines.items()),
38        'IDF_PATH': app.config.idf_path,
39        'IDF_TARGET': app.config.idf_target,
40    })
41    doxyfile_dir = os.path.join(app.config.docs_root, 'doxygen')
42    doxyfile_main = os.path.join(doxyfile_dir, 'Doxyfile_common')
43    doxyfile_target = os.path.join(doxyfile_dir, 'Doxyfile_' + app.config.idf_target)
44    print('Running doxygen with doxyfiles {} and {}'.format(doxyfile_main, doxyfile_target))
45
46    # It's possible to have doxygen log warnings to a file using WARN_LOGFILE directive,
47    # but in some cases it will still log an error to stderr and return success!
48    #
49    # So take all of stderr and redirect it to a logfile (will contain warnings and errors)
50    logfile = os.path.join(build_dir, 'doxygen-warning-log.txt')
51
52    with open(logfile, 'w') as f:
53        # note: run Doxygen in the build directory, so the xml & xml_in files end up in there
54        subprocess.check_call(['doxygen', doxyfile_main], env=doxy_env, cwd=build_dir, stderr=f)
55
56    # Doxygen has generated XML files in 'xml' directory.
57    # Copy them to 'xml_in', only touching the files which have changed.
58    copy_if_modified(os.path.join(build_dir, 'xml/'), os.path.join(build_dir, 'xml_in/'))
59
60    # Generate 'api_name.inc' files from the Doxygen XML files
61    doxygen_paths = [doxyfile_main, doxyfile_target]
62    convert_api_xml_to_inc(app, doxygen_paths)
63
64
65def convert_api_xml_to_inc(app, doxyfiles):
66    """ Generate header_file.inc files
67    with API reference made of doxygen directives
68    for each header file
69    specified in the 'INPUT' statement of the Doxyfile.
70    """
71    build_dir = app.config.build_dir
72
73    xml_directory_path = '{}/xml'.format(build_dir)
74    inc_directory_path = '{}/inc'.format(build_dir)
75
76    if not os.path.isdir(xml_directory_path):
77        raise RuntimeError('Directory {} does not exist!'.format(xml_directory_path))
78
79    if not os.path.exists(inc_directory_path):
80        os.makedirs(inc_directory_path)
81
82    header_paths = [p for d in doxyfiles for p in get_doxyfile_input_paths(app, d)]
83
84    print("Generating 'api_name.inc' files with Doxygen directives")
85    for header_file_path in header_paths:
86        api_name = get_api_name(header_file_path)
87        inc_file_path = inc_directory_path + '/' + api_name + '.inc'
88        rst_output = generate_directives(header_file_path, xml_directory_path)
89
90        previous_rst_output = ''
91        if os.path.isfile(inc_file_path):
92            with open(inc_file_path, 'r', encoding='utf-8') as inc_file_old:
93                previous_rst_output = inc_file_old.read()
94
95        if previous_rst_output != rst_output:
96            with open(inc_file_path, 'w', encoding='utf-8') as inc_file:
97                inc_file.write(rst_output)
98
99
100def get_doxyfile_input_paths(app, doxyfile_path):
101    """Get contents of Doxyfile's INPUT statement.
102
103    Returns:
104        Contents of Doxyfile's INPUT.
105
106    """
107    if not os.path.isfile(doxyfile_path):
108        raise RuntimeError("Doxyfile '{}' does not exist!".format(doxyfile_path))
109
110    print("Getting Doxyfile's INPUT")
111
112    with open(doxyfile_path, 'r', encoding='utf-8') as input_file:
113        line = input_file.readline()
114        # read contents of Doxyfile until 'INPUT' statement
115        while line:
116            if line.find('INPUT') == 0:
117                break
118            line = input_file.readline()
119
120        doxyfile_INPUT = []
121        line = input_file.readline()
122        # skip input_file contents until end of 'INPUT' statement
123        while line:
124            if line.isspace():
125                # we have reached the end of 'INPUT' statement
126                break
127            # process only lines that are not comments
128            if line.find('#') == -1:
129                # extract header file path inside components folder
130                m = re.search('components/(.*\.h)', line)  # noqa: W605 - regular expression
131                header_file_path = m.group(1)
132
133                # Replace env variable used for multi target header
134                header_file_path = header_file_path.replace('$(IDF_TARGET)', app.config.idf_target)
135
136                doxyfile_INPUT.append(header_file_path)
137
138            # proceed reading next line
139            line = input_file.readline()
140
141    return doxyfile_INPUT
142
143
144def get_api_name(header_file_path):
145    """Get name of API from header file path.
146
147    Args:
148        header_file_path: path to the header file.
149
150    Returns:
151        The name of API.
152
153    """
154    api_name = ''
155    regex = r'.*/(.*)\.h'
156    m = re.search(regex, header_file_path)
157    if m:
158        api_name = m.group(1)
159
160    return api_name
161
162
163def generate_directives(header_file_path, xml_directory_path):
164    """Generate API reference with Doxygen directives for a header file.
165
166    Args:
167        header_file_path: a path to the header file with API.
168
169    Returns:
170        Doxygen directives for the header file.
171
172    """
173
174    api_name = get_api_name(header_file_path)
175
176    # in XLT file name each "_" in the api name is expanded by Doxygen to "__"
177    xlt_api_name = api_name.replace('_', '__')
178    xml_file_path = '%s/%s_8h.xml' % (xml_directory_path, xlt_api_name)
179
180    rst_output = ''
181    rst_output = ".. File automatically generated by 'gen-dxd.py'\n"
182    rst_output += '\n'
183    rst_output += get_rst_header('Header File')
184    rst_output += '* :component_file:`' + header_file_path + '`\n'
185    rst_output += '\n'
186
187    try:
188        import xml.etree.cElementTree as ET
189    except ImportError:
190        import xml.etree.ElementTree as ET
191
192    tree = ET.ElementTree(file=xml_file_path)
193    for kind, label in ALL_KINDS:
194        rst_output += get_directives(tree, kind)
195
196    return rst_output
197
198
199def get_rst_header(header_name):
200    """Get rst formatted code with a header.
201
202    Args:
203        header_name: name of header.
204
205    Returns:
206        Formatted rst code with the header.
207
208    """
209
210    rst_output = ''
211    rst_output += header_name + '\n'
212    rst_output += '^' * len(header_name) + '\n'
213    rst_output += '\n'
214
215    return rst_output
216
217
218def select_unions(innerclass_list):
219    """Select unions from innerclass list.
220
221    Args:
222        innerclass_list: raw list with unions and structures
223                         extracted from Dogygen's xml file.
224
225    Returns:
226        Doxygen directives with unions selected from the list.
227
228    """
229
230    rst_output = ''
231    for line in innerclass_list.splitlines():
232        # union is denoted by "union" at the beginning of line
233        if line.find('union') == 0:
234            union_id, union_name = re.split(r'\t+', line)
235            rst_output += '.. doxygenunion:: '
236            rst_output += union_name
237            rst_output += '\n'
238
239    return rst_output
240
241
242def select_structs(innerclass_list):
243    """Select structures from innerclass list.
244
245    Args:
246        innerclass_list: raw list with unions and structures
247                         extracted from Dogygen's xml file.
248
249    Returns:
250        Doxygen directives with structures selected from the list.
251        Note: some structures are excluded as described on code below.
252
253    """
254
255    rst_output = ''
256    for line in innerclass_list.splitlines():
257        # structure is denoted by "struct" at the beginning of line
258        if line.find('struct') == 0:
259            # skip structures that are part of union
260            # they are documented by 'doxygenunion' directive
261            if line.find('::') > 0:
262                continue
263            struct_id, struct_name = re.split(r'\t+', line)
264            rst_output += '.. doxygenstruct:: '
265            rst_output += struct_name
266            rst_output += '\n'
267            rst_output += '    :members:\n'
268            rst_output += '\n'
269
270    return rst_output
271
272
273def get_directives(tree, kind):
274    """Get directives for specific 'kind'.
275
276    Args:
277        tree: the ElementTree 'tree' of XML by Doxygen
278        kind: name of API "kind" to be generated
279
280    Returns:
281        Doxygen directives for selected 'kind'.
282        Note: the header with "kind" name is included.
283
284    """
285
286    rst_output = ''
287    if kind in ['union', 'struct']:
288        innerclass_list = ''
289        for elem in tree.iterfind('compounddef/innerclass'):
290            innerclass_list += elem.attrib['refid'] + '\t' + elem.text + '\n'
291        if kind == 'union':
292            rst_output += select_unions(innerclass_list)
293        else:
294            rst_output += select_structs(innerclass_list)
295    else:
296        for elem in tree.iterfind(
297                'compounddef/sectiondef/memberdef[@kind="%s"]' % kind):
298            name = elem.find('name')
299            rst_output += '.. doxygen%s:: ' % kind
300            rst_output += name.text + '\n'
301    if rst_output:
302        all_kinds_dict = dict(ALL_KINDS)
303        rst_output = get_rst_header(all_kinds_dict[kind]) + rst_output + '\n'
304
305    return rst_output
306