# 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()
