1#!/usr/bin/env python3
2#
3# Copyright (c) 2017 Intel Corporation
4#
5# SPDX-License-Identifier: Apache-2.0
6
7"""
8Script to scan Zephyr include directories and emit system call and subsystem metadata
9
10System calls require a great deal of boilerplate code in order to implement
11completely. This script is the first step in the build system's process of
12auto-generating this code by doing a text scan of directories containing
13C or header files, and building up a database of system calls and their
14function call prototypes. This information is emitted to a generated
15JSON file for further processing.
16
17This script also scans for struct definitions such as __subsystem and
18__net_socket, emitting a JSON dictionary mapping tags to all the struct
19declarations found that were tagged with them.
20
21If the output JSON file already exists, its contents are checked against
22what information this script would have outputted; if the result is that the
23file would be unchanged, it is not modified to prevent unnecessary
24incremental builds.
25"""
26
27import sys
28import re
29import argparse
30import os
31import json
32
33regex_flags = re.MULTILINE | re.VERBOSE
34
35syscall_regex = re.compile(r'''
36(?:__syscall|__syscall_always_inline)\s+   # __syscall attribute, must be first
37([^(]+)                                    # type and name of system call (split later)
38[(]                                        # Function opening parenthesis
39([^)]*)                                    # Arg list (split later)
40[)]                                        # Closing parenthesis
41''', regex_flags)
42
43struct_tags = ["__subsystem", "__net_socket"]
44
45tagged_struct_decl_template = r'''
46%s\s+                           # tag, must be first
47struct\s+                       # struct keyword is next
48([^{]+)                         # name of subsystem
49[{]                             # Open curly bracket
50'''
51
52def tagged_struct_update(target_list, tag, contents):
53    regex = re.compile(tagged_struct_decl_template % tag, regex_flags)
54    items = [mo.groups()[0].strip() for mo in regex.finditer(contents)]
55    target_list.extend(items)
56
57
58def analyze_headers(multiple_directories):
59    syscall_ret = []
60    tagged_ret = {}
61
62    for tag in struct_tags:
63        tagged_ret[tag] = []
64
65    for base_path in multiple_directories:
66        for root, dirs, files in os.walk(base_path, topdown=True):
67            dirs.sort()
68            files.sort()
69            for fn in files:
70
71                # toolchain/common.h has the definitions of these tags which we
72                # don't want to trip over
73                path = os.path.join(root, fn)
74                if (not (path.endswith(".h") or path.endswith(".c")) or
75                        path.endswith(os.path.join(os.sep, 'toolchain',
76                                                   'common.h'))):
77                    continue
78
79                with open(path, "r", encoding="utf-8") as fp:
80                    try:
81                        contents = fp.read()
82                    except Exception:
83                        sys.stderr.write("Error decoding %s\n" % path)
84                        raise
85
86                try:
87                    syscall_result = [(mo.groups(), fn)
88                                      for mo in syscall_regex.finditer(contents)]
89                    for tag in struct_tags:
90                        tagged_struct_update(tagged_ret[tag], tag, contents)
91                except Exception:
92                    sys.stderr.write("While parsing %s\n" % fn)
93                    raise
94
95                syscall_ret.extend(syscall_result)
96
97    return syscall_ret, tagged_ret
98
99
100def update_file_if_changed(path, new):
101    if os.path.exists(path):
102        with open(path, 'r') as fp:
103            old = fp.read()
104
105        if new != old:
106            with open(path, 'w') as fp:
107                fp.write(new)
108    else:
109        with open(path, 'w') as fp:
110            fp.write(new)
111
112
113def parse_args():
114    global args
115    parser = argparse.ArgumentParser(
116        description=__doc__,
117        formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False)
118
119    parser.add_argument("-i", "--include", required=True, action='append',
120                        help='''include directories recursively scanned
121                        for .h files.  Can be specified multiple times:
122                        -i topdir1 -i topdir2 ...''')
123    parser.add_argument(
124        "-j", "--json-file", required=True,
125        help="Write system call prototype information as json to file")
126    parser.add_argument(
127        "-t", "--tag-struct-file", required=True,
128        help="Write tagged struct name information as json to file")
129
130    args = parser.parse_args()
131
132
133def main():
134    parse_args()
135
136    syscalls, tagged = analyze_headers(args.include)
137
138    # Only write json files if they don't exist or have changes since
139    # they will force an incremental rebuild.
140
141    syscalls_in_json = json.dumps(
142        syscalls,
143        indent=4,
144        sort_keys=True
145    )
146    update_file_if_changed(args.json_file, syscalls_in_json)
147
148    tagged_struct_in_json = json.dumps(
149        tagged,
150        indent=4,
151        sort_keys=True
152    )
153    update_file_if_changed(args.tag_struct_file, tagged_struct_in_json)
154
155
156if __name__ == "__main__":
157    main()
158