| #!/usr/bin/env python3 |
| # |
| # Copyright (c) 2017 Intel Corporation |
| # |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| """ |
| Script to scan Zephyr include directories and emit system call and subsystem metadata |
| |
| System calls require a great deal of boilerplate code in order to implement |
| completely. This script is the first step in the build system's process of |
| auto-generating this code by doing a text scan of directories containing |
| C or header files, and building up a database of system calls and their |
| function call prototypes. This information is emitted to a generated |
| JSON file for further processing. |
| |
| This script also scans for struct definitions such as __subsystem and |
| __net_socket, emitting a JSON dictionary mapping tags to all the struct |
| declarations found that were tagged with them. |
| |
| If the output JSON file already exists, its contents are checked against |
| what information this script would have outputted; if the result is that the |
| file would be unchanged, it is not modified to prevent unnecessary |
| incremental builds. |
| """ |
| |
| import sys |
| import re |
| import argparse |
| import os |
| import json |
| from pathlib import PurePath |
| |
| regex_flags = re.MULTILINE | re.VERBOSE |
| |
| syscall_regex = re.compile(r''' |
| (?:__syscall|__syscall_always_inline)\s+ # __syscall attribute, must be first |
| ([^(]+) # type and name of system call (split later) |
| [(] # Function opening parenthesis |
| ([^)]*) # Arg list (split later) |
| [)] # Closing parenthesis |
| ''', regex_flags) |
| |
| struct_tags = ["__subsystem", "__net_socket"] |
| |
| tagged_struct_decl_template = r''' |
| %s\s+ # tag, must be first |
| struct\s+ # struct keyword is next |
| ([^{]+) # name of subsystem |
| [{] # Open curly bracket |
| ''' |
| |
| def tagged_struct_update(target_list, tag, contents): |
| regex = re.compile(tagged_struct_decl_template % tag, regex_flags) |
| items = [mo.groups()[0].strip() for mo in regex.finditer(contents)] |
| target_list.extend(items) |
| |
| |
| def analyze_headers(include_dir, scan_dir, file_list): |
| syscall_ret = [] |
| tagged_ret = {} |
| |
| for tag in struct_tags: |
| tagged_ret[tag] = [] |
| |
| syscall_files = dict() |
| |
| # Get the list of header files which contains syscalls to be emitted. |
| # If file_list does not exist, we emit all syscalls. |
| if file_list: |
| with open(file_list, "r", encoding="utf-8") as fp: |
| contents = fp.read() |
| |
| for one_file in contents.split(";"): |
| if os.path.isfile(one_file): |
| syscall_files[one_file] = {"emit": True} |
| else: |
| sys.stderr.write(f"{one_file} does not exists!\n") |
| sys.exit(1) |
| |
| multiple_directories = set() |
| if include_dir: |
| multiple_directories |= set(include_dir) |
| if scan_dir: |
| multiple_directories |= set(scan_dir) |
| |
| # Convert to a list to keep the output deterministic |
| multiple_directories = sorted(multiple_directories) |
| |
| # Look for source files under various directories. |
| # Due to "syscalls/*.h" being included unconditionally in various |
| # other header files. We must generate the associated syscall |
| # header files (e.g. for function stubs). |
| for base_path in multiple_directories: |
| for root, dirs, files in os.walk(base_path, topdown=True): |
| dirs.sort() |
| files.sort() |
| for fn in files: |
| |
| # toolchain/common.h has the definitions of these tags which we |
| # don't want to trip over |
| path = os.path.join(root, fn) |
| if (not (path.endswith(".h") or path.endswith(".c")) or |
| path.endswith(os.path.join(os.sep, 'toolchain', |
| 'common.h'))): |
| continue |
| |
| path = PurePath(os.path.normpath(path)).as_posix() |
| |
| if path not in syscall_files: |
| if include_dir and base_path in include_dir: |
| syscall_files[path] = {"emit" : True} |
| else: |
| syscall_files[path] = {"emit" : False} |
| |
| # Parse files to extract syscall functions |
| for one_file in syscall_files: |
| with open(one_file, "r", encoding="utf-8") as fp: |
| try: |
| contents = fp.read() |
| except Exception: |
| sys.stderr.write("Error decoding %s (included in %s)\n" % (one_file, path)) |
| raise |
| |
| fn = os.path.basename(one_file) |
| |
| try: |
| to_emit = syscall_files[one_file]["emit"] | args.emit_all_syscalls |
| |
| syscall_result = [(mo.groups(), fn, to_emit) |
| for mo in syscall_regex.finditer(contents)] |
| for tag in struct_tags: |
| tagged_struct_update(tagged_ret[tag], tag, contents) |
| except Exception: |
| sys.stderr.write("While parsing %s\n" % fn) |
| raise |
| |
| syscall_ret.extend(syscall_result) |
| |
| return syscall_ret, tagged_ret |
| |
| |
| def update_file_if_changed(path, new): |
| if os.path.exists(path): |
| with open(path, 'r') as fp: |
| old = fp.read() |
| |
| if new != old: |
| with open(path, 'w') as fp: |
| fp.write(new) |
| else: |
| with open(path, 'w') as fp: |
| fp.write(new) |
| |
| |
| def parse_args(): |
| global args |
| parser = argparse.ArgumentParser( |
| description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False) |
| |
| parser.add_argument( |
| "-i", "--include", required=False, action="append", |
| help="Include directories recursively scanned for .h files " |
| "containing syscalls that must be present in final binary. " |
| "Can be specified multiple times: -i topdir1 -i topdir2 ...") |
| parser.add_argument( |
| "--scan", required=False, action="append", |
| help="Scan directories recursively for .h files containing " |
| "syscalls that need stubs generated but may not need to " |
| "be present in final binary. Can be specified multiple " |
| "times.") |
| parser.add_argument( |
| "-j", "--json-file", required=True, |
| help="Write system call prototype information as json to file") |
| parser.add_argument( |
| "-t", "--tag-struct-file", required=True, |
| help="Write tagged struct name information as json to file") |
| parser.add_argument( |
| "--file-list", required=False, |
| help="Text file containing semi-colon separated list of " |
| "header file where only syscalls in these files " |
| "are emitted.") |
| parser.add_argument( |
| "--emit-all-syscalls", required=False, action="store_true", |
| help="Emit all potential syscalls in the tree") |
| |
| args = parser.parse_args() |
| |
| |
| def main(): |
| parse_args() |
| |
| syscalls, tagged = analyze_headers(args.include, args.scan, |
| args.file_list) |
| |
| # Only write json files if they don't exist or have changes since |
| # they will force an incremental rebuild. |
| |
| syscalls_in_json = json.dumps( |
| syscalls, |
| indent=4, |
| sort_keys=True |
| ) |
| update_file_if_changed(args.json_file, syscalls_in_json) |
| |
| tagged_struct_in_json = json.dumps( |
| tagged, |
| indent=4, |
| sort_keys=True |
| ) |
| update_file_if_changed(args.tag_struct_file, tagged_struct_in_json) |
| |
| |
| if __name__ == "__main__": |
| main() |