xref: /optee_os/scripts/gen_compile_commands.py (revision 8d541aee2e0fe7242ec6fb57aabc7a04471b2237)
1*8d541aeeSJoakim Bech#!/usr/bin/env python3
2*8d541aeeSJoakim Bech# SPDX-License-Identifier: GPL-2.0
3*8d541aeeSJoakim Bech#
4*8d541aeeSJoakim Bech# Copyright (C) Google LLC, 2018
5*8d541aeeSJoakim Bech#
6*8d541aeeSJoakim Bech# Author: Tom Roeder <tmroeder@google.com>
7*8d541aeeSJoakim Bech# Ported and modified for U-Boot by Joao Marcos Costa <jmcosta944@gmail.com>
8*8d541aeeSJoakim Bech# Briefly documented at doc/build/gen_compile_commands.rst
9*8d541aeeSJoakim Bech#
10*8d541aeeSJoakim Bech# Ported and modified for OP-TEE by Joakim Bech <joakim.bech@linaro.org>
11*8d541aeeSJoakim Bech"""A tool for generating compile_commands.json in OP-TEE."""
12*8d541aeeSJoakim Bech
13*8d541aeeSJoakim Bechimport argparse
14*8d541aeeSJoakim Bechimport json
15*8d541aeeSJoakim Bechimport logging
16*8d541aeeSJoakim Bechimport os
17*8d541aeeSJoakim Bechimport re
18*8d541aeeSJoakim Bechimport subprocess
19*8d541aeeSJoakim Bechimport sys
20*8d541aeeSJoakim Bech
21*8d541aeeSJoakim Bech_DEFAULT_OUTPUT = 'compile_commands.json'
22*8d541aeeSJoakim Bech_DEFAULT_LOG_LEVEL = 'WARNING'
23*8d541aeeSJoakim Bech
24*8d541aeeSJoakim Bech_FILENAME_PATTERN = r'^\..*\.cmd$'
25*8d541aeeSJoakim Bech_LINE_PATTERN = r'^old-cmd[^ ]* := (?:/usr/bin/ccache )?(.*) -c (\S+)'
26*8d541aeeSJoakim Bech
27*8d541aeeSJoakim Bech_VALID_LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
28*8d541aeeSJoakim Bech# The tools/ directory adopts a different build system, and produces .cmd
29*8d541aeeSJoakim Bech# files in a different format. Do not support it.
30*8d541aeeSJoakim Bech_EXCLUDE_DIRS = ['.git', 'Documentation', 'include', 'tools']
31*8d541aeeSJoakim Bech
32*8d541aeeSJoakim Bech
33*8d541aeeSJoakim Bechdef parse_arguments():
34*8d541aeeSJoakim Bech    """Sets up and parses command-line arguments.
35*8d541aeeSJoakim Bech
36*8d541aeeSJoakim Bech    Returns:
37*8d541aeeSJoakim Bech        log_level: A logging level to filter log output.
38*8d541aeeSJoakim Bech        directory: The work directory where the objects were built.
39*8d541aeeSJoakim Bech        ar: Command used for parsing .a archives.
40*8d541aeeSJoakim Bech        output: Where to write the compile-commands JSON file.
41*8d541aeeSJoakim Bech        paths: The list of files/directories to handle to find .cmd files.
42*8d541aeeSJoakim Bech    """
43*8d541aeeSJoakim Bech    usage = 'Creates a compile_commands.json database from OP-TEE .cmd files'
44*8d541aeeSJoakim Bech    parser = argparse.ArgumentParser(description=usage)
45*8d541aeeSJoakim Bech
46*8d541aeeSJoakim Bech    directory_help = ('specify the output directory used for the OP-TEE build '
47*8d541aeeSJoakim Bech                      '(defaults to the working directory)')
48*8d541aeeSJoakim Bech    parser.add_argument('-d', '--directory', type=str, default='.',
49*8d541aeeSJoakim Bech                        help=directory_help)
50*8d541aeeSJoakim Bech
51*8d541aeeSJoakim Bech    output_help = ('path to the output command database (defaults to ' +
52*8d541aeeSJoakim Bech                   _DEFAULT_OUTPUT + ')')
53*8d541aeeSJoakim Bech    parser.add_argument('-o', '--output', type=str, default=_DEFAULT_OUTPUT,
54*8d541aeeSJoakim Bech                        help=output_help)
55*8d541aeeSJoakim Bech
56*8d541aeeSJoakim Bech    log_level_help = ('the level of log messages to produce (defaults to ' +
57*8d541aeeSJoakim Bech                      _DEFAULT_LOG_LEVEL + ')')
58*8d541aeeSJoakim Bech    parser.add_argument('--log_level', choices=_VALID_LOG_LEVELS,
59*8d541aeeSJoakim Bech                        default=_DEFAULT_LOG_LEVEL, help=log_level_help)
60*8d541aeeSJoakim Bech
61*8d541aeeSJoakim Bech    ar_help = 'command used for parsing .a archives'
62*8d541aeeSJoakim Bech    parser.add_argument('-a', '--ar', type=str, default='llvm-ar',
63*8d541aeeSJoakim Bech                        help=ar_help)
64*8d541aeeSJoakim Bech
65*8d541aeeSJoakim Bech    paths_help = ('directories to search or files to parse '
66*8d541aeeSJoakim Bech                  '(files should be *.o, *.a, or modules.order). '
67*8d541aeeSJoakim Bech                  'If nothing is specified, the current directory is searched')
68*8d541aeeSJoakim Bech    parser.add_argument('paths', type=str, nargs='*', help=paths_help)
69*8d541aeeSJoakim Bech
70*8d541aeeSJoakim Bech    args = parser.parse_args()
71*8d541aeeSJoakim Bech
72*8d541aeeSJoakim Bech    return (args.log_level,
73*8d541aeeSJoakim Bech            os.path.abspath(args.directory),
74*8d541aeeSJoakim Bech            args.output,
75*8d541aeeSJoakim Bech            args.ar,
76*8d541aeeSJoakim Bech            args.paths if len(args.paths) > 0 else [args.directory])
77*8d541aeeSJoakim Bech
78*8d541aeeSJoakim Bech
79*8d541aeeSJoakim Bechdef cmdfiles_in_dir(directory):
80*8d541aeeSJoakim Bech    """Generate the iterator of .cmd files found under the directory.
81*8d541aeeSJoakim Bech
82*8d541aeeSJoakim Bech    Walk under the given directory, and yield every .cmd file found.
83*8d541aeeSJoakim Bech
84*8d541aeeSJoakim Bech    Args:
85*8d541aeeSJoakim Bech        directory: The directory to search for .cmd files.
86*8d541aeeSJoakim Bech
87*8d541aeeSJoakim Bech    Yields:
88*8d541aeeSJoakim Bech        The path to a .cmd file.
89*8d541aeeSJoakim Bech    """
90*8d541aeeSJoakim Bech
91*8d541aeeSJoakim Bech    filename_matcher = re.compile(_FILENAME_PATTERN)
92*8d541aeeSJoakim Bech    exclude_dirs = [os.path.join(directory, d) for d in _EXCLUDE_DIRS]
93*8d541aeeSJoakim Bech
94*8d541aeeSJoakim Bech    for dirpath, dirnames, filenames in os.walk(directory, topdown=True):
95*8d541aeeSJoakim Bech        # Prune unwanted directories.
96*8d541aeeSJoakim Bech        if dirpath in exclude_dirs:
97*8d541aeeSJoakim Bech            dirnames[:] = []
98*8d541aeeSJoakim Bech            continue
99*8d541aeeSJoakim Bech
100*8d541aeeSJoakim Bech        for filename in filenames:
101*8d541aeeSJoakim Bech            if filename_matcher.match(filename):
102*8d541aeeSJoakim Bech                yield os.path.join(dirpath, filename)
103*8d541aeeSJoakim Bech
104*8d541aeeSJoakim Bech
105*8d541aeeSJoakim Bechdef to_cmdfile(path):
106*8d541aeeSJoakim Bech    """Return the path of .cmd file used for the given build artifact
107*8d541aeeSJoakim Bech
108*8d541aeeSJoakim Bech    Args:
109*8d541aeeSJoakim Bech        Path: file path
110*8d541aeeSJoakim Bech
111*8d541aeeSJoakim Bech    Returns:
112*8d541aeeSJoakim Bech        The path to .cmd file
113*8d541aeeSJoakim Bech    """
114*8d541aeeSJoakim Bech    dir, base = os.path.split(path)
115*8d541aeeSJoakim Bech    return os.path.join(dir, '.' + base + '.cmd')
116*8d541aeeSJoakim Bech
117*8d541aeeSJoakim Bech
118*8d541aeeSJoakim Bechdef cmdfiles_for_a(archive, ar):
119*8d541aeeSJoakim Bech    """Generate the iterator of .cmd files associated with the archive.
120*8d541aeeSJoakim Bech
121*8d541aeeSJoakim Bech    Parse the given archive, and yield every .cmd file used to build it.
122*8d541aeeSJoakim Bech
123*8d541aeeSJoakim Bech    Args:
124*8d541aeeSJoakim Bech        archive: The archive to parse
125*8d541aeeSJoakim Bech
126*8d541aeeSJoakim Bech    Yields:
127*8d541aeeSJoakim Bech        The path to every .cmd file found
128*8d541aeeSJoakim Bech    """
129*8d541aeeSJoakim Bech    for obj in subprocess.check_output([ar, '-t', archive]).decode().split():
130*8d541aeeSJoakim Bech        yield to_cmdfile(obj)
131*8d541aeeSJoakim Bech
132*8d541aeeSJoakim Bech
133*8d541aeeSJoakim Bechdef cmdfiles_for_modorder(modorder):
134*8d541aeeSJoakim Bech    """Generate the iterator of .cmd files associated with the modules.order.
135*8d541aeeSJoakim Bech
136*8d541aeeSJoakim Bech    Parse the given modules.order, and yield every .cmd file used to build the
137*8d541aeeSJoakim Bech    contained modules.
138*8d541aeeSJoakim Bech
139*8d541aeeSJoakim Bech    Args:
140*8d541aeeSJoakim Bech        modorder: The modules.order file to parse
141*8d541aeeSJoakim Bech
142*8d541aeeSJoakim Bech    Yields:
143*8d541aeeSJoakim Bech        The path to every .cmd file found
144*8d541aeeSJoakim Bech    """
145*8d541aeeSJoakim Bech    with open(modorder) as f:
146*8d541aeeSJoakim Bech        for line in f:
147*8d541aeeSJoakim Bech            obj = line.rstrip()
148*8d541aeeSJoakim Bech            base, ext = os.path.splitext(obj)
149*8d541aeeSJoakim Bech            if ext != '.o':
150*8d541aeeSJoakim Bech                sys.exit('{}: module path must end with .o'.format(obj))
151*8d541aeeSJoakim Bech            mod = base + '.mod'
152*8d541aeeSJoakim Bech            # Read from *.mod, to get a list of objects that compose the
153*8d541aeeSJoakim Bech            # module.
154*8d541aeeSJoakim Bech            with open(mod) as m:
155*8d541aeeSJoakim Bech                for mod_line in m:
156*8d541aeeSJoakim Bech                    yield to_cmdfile(mod_line.rstrip())
157*8d541aeeSJoakim Bech
158*8d541aeeSJoakim Bech
159*8d541aeeSJoakim Bechdef process_line(root_directory, command_prefix, file_path):
160*8d541aeeSJoakim Bech    """Extracts information from a .cmd line and creates an entry from it.
161*8d541aeeSJoakim Bech
162*8d541aeeSJoakim Bech    Args:
163*8d541aeeSJoakim Bech        root_directory: The directory that was searched for .cmd files. Usually
164*8d541aeeSJoakim Bech            used directly in the "directory" entry in compile_commands.json.
165*8d541aeeSJoakim Bech        command_prefix: The extracted command line, up to the last element.
166*8d541aeeSJoakim Bech        file_path: The .c file from the end of the extracted command.
167*8d541aeeSJoakim Bech            Usually relative to root_directory, but sometimes absolute.
168*8d541aeeSJoakim Bech
169*8d541aeeSJoakim Bech    Returns:
170*8d541aeeSJoakim Bech        An entry to append to compile_commands.
171*8d541aeeSJoakim Bech
172*8d541aeeSJoakim Bech    Raises:
173*8d541aeeSJoakim Bech        ValueError: Could not find the extracted file based on file_path and
174*8d541aeeSJoakim Bech            root_directory or file_directory.
175*8d541aeeSJoakim Bech    """
176*8d541aeeSJoakim Bech    # The .cmd files are intended to be included directly by Make, so they
177*8d541aeeSJoakim Bech    # escape the pound sign '#', either as '\#' or '$(pound)' (depending on the
178*8d541aeeSJoakim Bech    # kernel version). The compile_commands.json file is not interpreted
179*8d541aeeSJoakim Bech    # by Make, so this code replaces the escaped version with '#'.
180*8d541aeeSJoakim Bech    prefix = command_prefix.replace('\#', '#').replace('$(pound)', '#')  # noqa: W605
181*8d541aeeSJoakim Bech
182*8d541aeeSJoakim Bech    # Use os.path.abspath() to normalize the path resolving '.' and '..' .
183*8d541aeeSJoakim Bech    abs_path = os.path.abspath(os.path.join(root_directory, file_path))
184*8d541aeeSJoakim Bech    if not os.path.exists(abs_path):
185*8d541aeeSJoakim Bech        raise ValueError('File %s not found' % abs_path)
186*8d541aeeSJoakim Bech    return {
187*8d541aeeSJoakim Bech        'directory': root_directory,
188*8d541aeeSJoakim Bech        'file': abs_path,
189*8d541aeeSJoakim Bech        'command': prefix + file_path,
190*8d541aeeSJoakim Bech    }
191*8d541aeeSJoakim Bech
192*8d541aeeSJoakim Bech
193*8d541aeeSJoakim Bechdef main():
194*8d541aeeSJoakim Bech    """Walks through the directory and finds and parses .cmd files."""
195*8d541aeeSJoakim Bech    log_level, directory, output, ar, paths = parse_arguments()
196*8d541aeeSJoakim Bech
197*8d541aeeSJoakim Bech    level = getattr(logging, log_level)
198*8d541aeeSJoakim Bech    logging.basicConfig(format='%(levelname)s: %(message)s', level=level)
199*8d541aeeSJoakim Bech
200*8d541aeeSJoakim Bech    line_matcher = re.compile(_LINE_PATTERN)
201*8d541aeeSJoakim Bech
202*8d541aeeSJoakim Bech    compile_commands = []
203*8d541aeeSJoakim Bech
204*8d541aeeSJoakim Bech    for path in paths:
205*8d541aeeSJoakim Bech        # If 'path' is a directory, handle all .cmd files under it.
206*8d541aeeSJoakim Bech        # Otherwise, handle .cmd files associated with the file.
207*8d541aeeSJoakim Bech        # built-in objects are linked via vmlinux.a
208*8d541aeeSJoakim Bech        # Modules are listed in modules.order.
209*8d541aeeSJoakim Bech        if os.path.isdir(path):
210*8d541aeeSJoakim Bech            cmdfiles = cmdfiles_in_dir(path)
211*8d541aeeSJoakim Bech        elif path.endswith('.a'):
212*8d541aeeSJoakim Bech            cmdfiles = cmdfiles_for_a(path, ar)
213*8d541aeeSJoakim Bech        elif path.endswith('modules.order'):
214*8d541aeeSJoakim Bech            cmdfiles = cmdfiles_for_modorder(path)
215*8d541aeeSJoakim Bech        else:
216*8d541aeeSJoakim Bech            sys.exit('{}: unknown file type'.format(path))
217*8d541aeeSJoakim Bech
218*8d541aeeSJoakim Bech        for cmdfile in cmdfiles:
219*8d541aeeSJoakim Bech            with open(cmdfile, 'rt') as f:
220*8d541aeeSJoakim Bech                try:
221*8d541aeeSJoakim Bech                    result = line_matcher.match(f.readline())
222*8d541aeeSJoakim Bech                    if result:
223*8d541aeeSJoakim Bech                        entry = process_line(directory, result.group(1),
224*8d541aeeSJoakim Bech                                             result.group(2))
225*8d541aeeSJoakim Bech                        compile_commands.append(entry)
226*8d541aeeSJoakim Bech                except ValueError as err:
227*8d541aeeSJoakim Bech                    logging.info('Could not add line from %s: %s',
228*8d541aeeSJoakim Bech                                 cmdfile, err)
229*8d541aeeSJoakim Bech                except AttributeError as err:
230*8d541aeeSJoakim Bech                    continue
231*8d541aeeSJoakim Bech
232*8d541aeeSJoakim Bech    with open(output, 'wt') as f:
233*8d541aeeSJoakim Bech        json.dump(compile_commands, f, indent=2, sort_keys=True)
234*8d541aeeSJoakim Bech
235*8d541aeeSJoakim Bech
236*8d541aeeSJoakim Bechif __name__ == '__main__':
237*8d541aeeSJoakim Bech    main()
238