# Copyright 2023 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ Script for building a Chromium CodeQL database.""" import argparse import functools import json import multiprocessing import subprocess import logging import time import os import traceback import gn_sources_tools import targets_to_index from collections import namedtuple CommandOutput = namedtuple("CommandOutput", "output traceback") def log_subprocess_output(output, logger=None): """ Reads from a subprocess's stdout and writes it to `logger`, or, if none given, to stdout. """ if not logger: print(output) else: logger.info(output) class CodeQLDatabase: def __init__(self, src_path, db_path, codeql_binary_path): """ Construct a new `CodeQLDatabase` object. :param src_path: The path to the chromium/src tree. :param db_path: The path where the CodeQL database will be created. :return: returns nothing """ self.db_path = db_path try: process_stdout = subprocess.check_output([ codeql_binary_path, 'database', 'init', f'--source-root={src_path}', '--language=cpp', db_path, '--overwrite' ]) log_subprocess_output(process_stdout) except subprocess.CalledProcessError: # Presumably failed due to an invalid value for db_path. raise ValueError def index_one_target(target_name, src_path, db_path, codeql_binary_path, out_path, logger, ninja_path='ninja', gn_path='gn', logfile=None, reduce_cores_used=False): try: process_stdout = subprocess.check_output([gn_path, 'clean', out_path]) log_subprocess_output(process_stdout) except subprocess.CalledProcessError as e: print("Failed to clean build directory between targets") print("stdout: %s" % e.stdout) print("stderr: %s" % e.stderr) exit(1) db_path = os.path.join(db_path, target_name) os.mkdir(os.path.join(db_path)) start_time = time.time() print("Initializing codeql.") codeql_db = "" try: codeql_db = CodeQLDatabase(src_path, db_path, codeql_binary_path) except ValueError: print("Could not initialize CodeQL database at %s" % db_path) exit(1) print("Tracing compilation.") trace_command = [ codeql_binary_path, 'database', 'trace-command', db_path, f'--working-dir={src_path}', '--', ninja_path, '-C', out_path, target_name ] if reduce_cores_used: usable_cpu_count = int(multiprocessing.cpu_count() / 2) trace_command.extend(['-j', str(usable_cpu_count)]) try: process_stdout = subprocess.check_output(trace_command) log_subprocess_output(process_stdout) except subprocess.CalledProcessError as e: print("CodeQL trace-process failed with return code %s" % e.returncode) print("stdout: %s" % e.stdout) print("stderr: %s" % e.stderr) exit(1) print("Finalizing codeql db.") try: process_stdout = subprocess.check_output( [codeql_binary_path, 'database', 'finalize', '-j=-1', db_path]) log_subprocess_output(process_stdout) except subprocess.CalledProcessError as e: print("CodeQL DB finalization failed with return code %s" % e.returncode) print("stdout: %s" % e.stdout) print("stderr: %s" % e.stderr) print("Database creation complete.") total_time = time.time() - start_time print("Time elapsed:") print(str(total_time)) def main(): logger = logging.getLogger('log') logger.setLevel(logging.INFO) actual_cwd = os.getcwd() script_directory = os.path.dirname(os.path.realpath(__file__)) src_path = os.path.join(script_directory, '..', '..') if actual_cwd != os.path.normpath(src_path): print("Failure: Script must be executed from `chromium/src`. Exiting.") print(actual_cwd) print(src_path) exit(1) print("Parsing command line arguments.") parser = argparse.ArgumentParser( description='Build CodeQL database for Chromium browser process') parser.add_argument( '--out_path', '-o', type=str, default='out/release', help='Relative path inside chromium checkout to build directory') parser.add_argument('--db_path', '-d', type=str, required=True, help='Path to output database') parser.add_argument( '--logfile', '-l', type=str, help="absolute path to logfile for `trace` calls, if desired") parser.add_argument( '--gn_targets', '-g', action='append', type=str, help=( 'name for the specific GN target you want a CodeQL database for ' '(e.g. `//components:components_unittests`); if left blank, indexes ' 'everything')) parser.add_argument( '--codeql_binary_path', '-c', type=str, default='codeql', help=('Path to the codeql binary. If this is not set, the script assumes ' 'it is located at `codeql` somewhere in the user\'s PATH.')) parser.add_argument( '--gn_path', type=str, default='gn', help=('Path to the gn executable. If this is not set, the script assumes ' 'it is located at `gn` somehwere in the user\'s PATH.')) parser.add_argument( '--ninja_path', type=str, default='ninja', help=('Path to the ninja executable. If this is not set, the script ' 'assumes it is located at `ninja` somehwere in the user\'s PATH.')) parser.add_argument( '--reduce_cores_used', default=False, action='store_true', help=('If set, reduces the number of cores used when building a target.')) args = parser.parse_args() if (args.logfile): ch = logging.FileHandler(args.logfile) ch.setFormatter(logging.Formatter('%(message)s')) logger.addHandler(ch) src_path = os.path.abspath(os.path.expanduser(src_path)) args.db_path = os.path.abspath(os.path.expanduser(args.db_path)) # If an args.gn_target is given, index those targets. # Otherwise, index the targets in targets_to_index. actual_targets_to_index = [] if not args.gn_targets: actual_targets_to_index = targets_to_index.full_targets else: actual_targets_to_index = args.gn_targets for target in actual_targets_to_index: index_one_target(target, src_path, args.db_path, args.codeql_binary_path, args.out_path, logger, args.ninja_path, args.gn_path, args.logfile, args.reduce_cores_used) if __name__ == '__main__': main()