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