xref: /optee_os/scripts/gen_compile_commands.py (revision fe5af822179a7b4b7a9c8109aaa09e9441e9f5bb)
18d541aeeSJoakim Bech#!/usr/bin/env python3
28d541aeeSJoakim Bech# SPDX-License-Identifier: GPL-2.0
38d541aeeSJoakim Bech#
48d541aeeSJoakim Bech# Copyright (C) Google LLC, 2018
58d541aeeSJoakim Bech#
68d541aeeSJoakim Bech# Author: Tom Roeder <tmroeder@google.com>
78d541aeeSJoakim Bech# Ported and modified for U-Boot by Joao Marcos Costa <jmcosta944@gmail.com>
88d541aeeSJoakim Bech# Briefly documented at doc/build/gen_compile_commands.rst
98d541aeeSJoakim Bech#
108d541aeeSJoakim Bech# Ported and modified for OP-TEE by Joakim Bech <joakim.bech@linaro.org>
118d541aeeSJoakim Bech"""A tool for generating compile_commands.json in OP-TEE."""
128d541aeeSJoakim Bech
138d541aeeSJoakim Bechimport argparse
148d541aeeSJoakim Bechimport json
158d541aeeSJoakim Bechimport logging
168d541aeeSJoakim Bechimport os
178d541aeeSJoakim Bechimport re
188d541aeeSJoakim Bechimport subprocess
198d541aeeSJoakim Bechimport sys
208d541aeeSJoakim Bech
218d541aeeSJoakim Bech_DEFAULT_OUTPUT = 'compile_commands.json'
228d541aeeSJoakim Bech_DEFAULT_LOG_LEVEL = 'WARNING'
238d541aeeSJoakim Bech
248d541aeeSJoakim Bech_FILENAME_PATTERN = r'^\..*\.cmd$'
258d541aeeSJoakim Bech_LINE_PATTERN = r'^old-cmd[^ ]* := (?:/usr/bin/ccache )?(.*) -c (\S+)'
268d541aeeSJoakim Bech
278d541aeeSJoakim Bech_VALID_LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
288d541aeeSJoakim Bech# The tools/ directory adopts a different build system, and produces .cmd
298d541aeeSJoakim Bech# files in a different format. Do not support it.
308d541aeeSJoakim Bech_EXCLUDE_DIRS = ['.git', 'Documentation', 'include', 'tools']
318d541aeeSJoakim Bech
328d541aeeSJoakim Bech
338d541aeeSJoakim Bechdef parse_arguments():
348d541aeeSJoakim Bech    """Sets up and parses command-line arguments.
358d541aeeSJoakim Bech
368d541aeeSJoakim Bech    Returns:
378d541aeeSJoakim Bech        log_level: A logging level to filter log output.
388d541aeeSJoakim Bech        directory: The work directory where the objects were built.
398d541aeeSJoakim Bech        ar: Command used for parsing .a archives.
408d541aeeSJoakim Bech        output: Where to write the compile-commands JSON file.
418d541aeeSJoakim Bech        paths: The list of files/directories to handle to find .cmd files.
428d541aeeSJoakim Bech    """
438d541aeeSJoakim Bech    usage = 'Creates a compile_commands.json database from OP-TEE .cmd files'
448d541aeeSJoakim Bech    parser = argparse.ArgumentParser(description=usage)
458d541aeeSJoakim Bech
468d541aeeSJoakim Bech    directory_help = ('specify the output directory used for the OP-TEE build '
478d541aeeSJoakim Bech                      '(defaults to the working directory)')
488d541aeeSJoakim Bech    parser.add_argument('-d', '--directory', type=str, default='.',
498d541aeeSJoakim Bech                        help=directory_help)
508d541aeeSJoakim Bech
518d541aeeSJoakim Bech    output_help = ('path to the output command database (defaults to ' +
528d541aeeSJoakim Bech                   _DEFAULT_OUTPUT + ')')
538d541aeeSJoakim Bech    parser.add_argument('-o', '--output', type=str, default=_DEFAULT_OUTPUT,
548d541aeeSJoakim Bech                        help=output_help)
558d541aeeSJoakim Bech
568d541aeeSJoakim Bech    log_level_help = ('the level of log messages to produce (defaults to ' +
578d541aeeSJoakim Bech                      _DEFAULT_LOG_LEVEL + ')')
588d541aeeSJoakim Bech    parser.add_argument('--log_level', choices=_VALID_LOG_LEVELS,
598d541aeeSJoakim Bech                        default=_DEFAULT_LOG_LEVEL, help=log_level_help)
608d541aeeSJoakim Bech
618d541aeeSJoakim Bech    ar_help = 'command used for parsing .a archives'
628d541aeeSJoakim Bech    parser.add_argument('-a', '--ar', type=str, default='llvm-ar',
638d541aeeSJoakim Bech                        help=ar_help)
648d541aeeSJoakim Bech
658d541aeeSJoakim Bech    paths_help = ('directories to search or files to parse '
668d541aeeSJoakim Bech                  '(files should be *.o, *.a, or modules.order). '
678d541aeeSJoakim Bech                  'If nothing is specified, the current directory is searched')
688d541aeeSJoakim Bech    parser.add_argument('paths', type=str, nargs='*', help=paths_help)
698d541aeeSJoakim Bech
708d541aeeSJoakim Bech    args = parser.parse_args()
718d541aeeSJoakim Bech
728d541aeeSJoakim Bech    return (args.log_level,
738d541aeeSJoakim Bech            os.path.abspath(args.directory),
748d541aeeSJoakim Bech            args.output,
758d541aeeSJoakim Bech            args.ar,
768d541aeeSJoakim Bech            args.paths if len(args.paths) > 0 else [args.directory])
778d541aeeSJoakim Bech
788d541aeeSJoakim Bech
798d541aeeSJoakim Bechdef cmdfiles_in_dir(directory):
808d541aeeSJoakim Bech    """Generate the iterator of .cmd files found under the directory.
818d541aeeSJoakim Bech
828d541aeeSJoakim Bech    Walk under the given directory, and yield every .cmd file found.
838d541aeeSJoakim Bech
848d541aeeSJoakim Bech    Args:
858d541aeeSJoakim Bech        directory: The directory to search for .cmd files.
868d541aeeSJoakim Bech
878d541aeeSJoakim Bech    Yields:
888d541aeeSJoakim Bech        The path to a .cmd file.
898d541aeeSJoakim Bech    """
908d541aeeSJoakim Bech
918d541aeeSJoakim Bech    filename_matcher = re.compile(_FILENAME_PATTERN)
928d541aeeSJoakim Bech    exclude_dirs = [os.path.join(directory, d) for d in _EXCLUDE_DIRS]
938d541aeeSJoakim Bech
948d541aeeSJoakim Bech    for dirpath, dirnames, filenames in os.walk(directory, topdown=True):
958d541aeeSJoakim Bech        # Prune unwanted directories.
968d541aeeSJoakim Bech        if dirpath in exclude_dirs:
978d541aeeSJoakim Bech            dirnames[:] = []
988d541aeeSJoakim Bech            continue
998d541aeeSJoakim Bech
1008d541aeeSJoakim Bech        for filename in filenames:
1018d541aeeSJoakim Bech            if filename_matcher.match(filename):
1028d541aeeSJoakim Bech                yield os.path.join(dirpath, filename)
1038d541aeeSJoakim Bech
1048d541aeeSJoakim Bech
1058d541aeeSJoakim Bechdef to_cmdfile(path):
1068d541aeeSJoakim Bech    """Return the path of .cmd file used for the given build artifact
1078d541aeeSJoakim Bech
1088d541aeeSJoakim Bech    Args:
1098d541aeeSJoakim Bech        Path: file path
1108d541aeeSJoakim Bech
1118d541aeeSJoakim Bech    Returns:
1128d541aeeSJoakim Bech        The path to .cmd file
1138d541aeeSJoakim Bech    """
1148d541aeeSJoakim Bech    dir, base = os.path.split(path)
1158d541aeeSJoakim Bech    return os.path.join(dir, '.' + base + '.cmd')
1168d541aeeSJoakim Bech
1178d541aeeSJoakim Bech
1188d541aeeSJoakim Bechdef cmdfiles_for_a(archive, ar):
1198d541aeeSJoakim Bech    """Generate the iterator of .cmd files associated with the archive.
1208d541aeeSJoakim Bech
1218d541aeeSJoakim Bech    Parse the given archive, and yield every .cmd file used to build it.
1228d541aeeSJoakim Bech
1238d541aeeSJoakim Bech    Args:
1248d541aeeSJoakim Bech        archive: The archive to parse
1258d541aeeSJoakim Bech
1268d541aeeSJoakim Bech    Yields:
1278d541aeeSJoakim Bech        The path to every .cmd file found
1288d541aeeSJoakim Bech    """
1298d541aeeSJoakim Bech    for obj in subprocess.check_output([ar, '-t', archive]).decode().split():
1308d541aeeSJoakim Bech        yield to_cmdfile(obj)
1318d541aeeSJoakim Bech
1328d541aeeSJoakim Bech
1338d541aeeSJoakim Bechdef cmdfiles_for_modorder(modorder):
1348d541aeeSJoakim Bech    """Generate the iterator of .cmd files associated with the modules.order.
1358d541aeeSJoakim Bech
1368d541aeeSJoakim Bech    Parse the given modules.order, and yield every .cmd file used to build the
1378d541aeeSJoakim Bech    contained modules.
1388d541aeeSJoakim Bech
1398d541aeeSJoakim Bech    Args:
1408d541aeeSJoakim Bech        modorder: The modules.order file to parse
1418d541aeeSJoakim Bech
1428d541aeeSJoakim Bech    Yields:
1438d541aeeSJoakim Bech        The path to every .cmd file found
1448d541aeeSJoakim Bech    """
1458d541aeeSJoakim Bech    with open(modorder) as f:
1468d541aeeSJoakim Bech        for line in f:
1478d541aeeSJoakim Bech            obj = line.rstrip()
1488d541aeeSJoakim Bech            base, ext = os.path.splitext(obj)
1498d541aeeSJoakim Bech            if ext != '.o':
1508d541aeeSJoakim Bech                sys.exit('{}: module path must end with .o'.format(obj))
1518d541aeeSJoakim Bech            mod = base + '.mod'
1528d541aeeSJoakim Bech            # Read from *.mod, to get a list of objects that compose the
1538d541aeeSJoakim Bech            # module.
1548d541aeeSJoakim Bech            with open(mod) as m:
1558d541aeeSJoakim Bech                for mod_line in m:
1568d541aeeSJoakim Bech                    yield to_cmdfile(mod_line.rstrip())
1578d541aeeSJoakim Bech
1588d541aeeSJoakim Bech
1598d541aeeSJoakim Bechdef process_line(root_directory, command_prefix, file_path):
1608d541aeeSJoakim Bech    """Extracts information from a .cmd line and creates an entry from it.
1618d541aeeSJoakim Bech
1628d541aeeSJoakim Bech    Args:
1638d541aeeSJoakim Bech        root_directory: The directory that was searched for .cmd files. Usually
1648d541aeeSJoakim Bech            used directly in the "directory" entry in compile_commands.json.
1658d541aeeSJoakim Bech        command_prefix: The extracted command line, up to the last element.
1668d541aeeSJoakim Bech        file_path: The .c file from the end of the extracted command.
1678d541aeeSJoakim Bech            Usually relative to root_directory, but sometimes absolute.
1688d541aeeSJoakim Bech
1698d541aeeSJoakim Bech    Returns:
1708d541aeeSJoakim Bech        An entry to append to compile_commands.
1718d541aeeSJoakim Bech
1728d541aeeSJoakim Bech    Raises:
1738d541aeeSJoakim Bech        ValueError: Could not find the extracted file based on file_path and
1748d541aeeSJoakim Bech            root_directory or file_directory.
1758d541aeeSJoakim Bech    """
1768d541aeeSJoakim Bech    # The .cmd files are intended to be included directly by Make, so they
1778d541aeeSJoakim Bech    # escape the pound sign '#', either as '\#' or '$(pound)' (depending on the
1788d541aeeSJoakim Bech    # kernel version). The compile_commands.json file is not interpreted
1798d541aeeSJoakim Bech    # by Make, so this code replaces the escaped version with '#'.
180*fe5af822SShen Jiamin    prefix = command_prefix.replace(r'\#', '#').replace('$(pound)', '#')
1818d541aeeSJoakim Bech
1828d541aeeSJoakim Bech    # Use os.path.abspath() to normalize the path resolving '.' and '..' .
1838d541aeeSJoakim Bech    abs_path = os.path.abspath(os.path.join(root_directory, file_path))
1848d541aeeSJoakim Bech    if not os.path.exists(abs_path):
1858d541aeeSJoakim Bech        raise ValueError('File %s not found' % abs_path)
1868d541aeeSJoakim Bech    return {
1878d541aeeSJoakim Bech        'directory': root_directory,
1888d541aeeSJoakim Bech        'file': abs_path,
1898d541aeeSJoakim Bech        'command': prefix + file_path,
1908d541aeeSJoakim Bech    }
1918d541aeeSJoakim Bech
1928d541aeeSJoakim Bech
1938d541aeeSJoakim Bechdef main():
1948d541aeeSJoakim Bech    """Walks through the directory and finds and parses .cmd files."""
1958d541aeeSJoakim Bech    log_level, directory, output, ar, paths = parse_arguments()
1968d541aeeSJoakim Bech
1978d541aeeSJoakim Bech    level = getattr(logging, log_level)
1988d541aeeSJoakim Bech    logging.basicConfig(format='%(levelname)s: %(message)s', level=level)
1998d541aeeSJoakim Bech
2008d541aeeSJoakim Bech    line_matcher = re.compile(_LINE_PATTERN)
2018d541aeeSJoakim Bech
2028d541aeeSJoakim Bech    compile_commands = []
2038d541aeeSJoakim Bech
2048d541aeeSJoakim Bech    for path in paths:
2058d541aeeSJoakim Bech        # If 'path' is a directory, handle all .cmd files under it.
2068d541aeeSJoakim Bech        # Otherwise, handle .cmd files associated with the file.
2078d541aeeSJoakim Bech        # built-in objects are linked via vmlinux.a
2088d541aeeSJoakim Bech        # Modules are listed in modules.order.
2098d541aeeSJoakim Bech        if os.path.isdir(path):
2108d541aeeSJoakim Bech            cmdfiles = cmdfiles_in_dir(path)
2118d541aeeSJoakim Bech        elif path.endswith('.a'):
2128d541aeeSJoakim Bech            cmdfiles = cmdfiles_for_a(path, ar)
2138d541aeeSJoakim Bech        elif path.endswith('modules.order'):
2148d541aeeSJoakim Bech            cmdfiles = cmdfiles_for_modorder(path)
2158d541aeeSJoakim Bech        else:
2168d541aeeSJoakim Bech            sys.exit('{}: unknown file type'.format(path))
2178d541aeeSJoakim Bech
2188d541aeeSJoakim Bech        for cmdfile in cmdfiles:
2198d541aeeSJoakim Bech            with open(cmdfile, 'rt') as f:
2208d541aeeSJoakim Bech                try:
2218d541aeeSJoakim Bech                    result = line_matcher.match(f.readline())
2228d541aeeSJoakim Bech                    if result:
2238d541aeeSJoakim Bech                        entry = process_line(directory, result.group(1),
2248d541aeeSJoakim Bech                                             result.group(2))
2258d541aeeSJoakim Bech                        compile_commands.append(entry)
2268d541aeeSJoakim Bech                except ValueError as err:
2278d541aeeSJoakim Bech                    logging.info('Could not add line from %s: %s',
2288d541aeeSJoakim Bech                                 cmdfile, err)
2298d541aeeSJoakim Bech                except AttributeError as err:
2308d541aeeSJoakim Bech                    continue
2318d541aeeSJoakim Bech
2328d541aeeSJoakim Bech    with open(output, 'wt') as f:
2338d541aeeSJoakim Bech        json.dump(compile_commands, f, indent=2, sort_keys=True)
2348d541aeeSJoakim Bech
2358d541aeeSJoakim Bech
2368d541aeeSJoakim Bechif __name__ == '__main__':
2378d541aeeSJoakim Bech    main()
238