#! /usr/bin/env python
# Copyright 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Updates the chromium binaries used by devil.

This currently must be called from the top-level chromium src directory.
"""

import argparse
import collections
import json
import logging
import os
import sys

_DEVIL_ROOT_DIR = os.path.abspath(
    os.path.join(os.path.dirname(__file__), '..', '..'))

sys.path.append(_DEVIL_ROOT_DIR)
from devil import base_error
from devil import devil_env
from devil.utils import cmd_helper

_DEVICE_ARCHS = [
    {
        'cpu': 'arm',
        'platform': 'android_armeabi-v7a',
    },
    {
        'cpu': 'arm64',
        'platform': 'android_arm64-v8a',
    },
    {
        'cpu': 'x86',
        'platform': 'android_x86',
    },
    {
        'cpu': 'x64',
        'platform': 'android_x86_64',
    },
]
_HOST_ARCH = [{
    # Host binaries use x86_64, not arm, but they build with the
    # host toolchain within a target_cpu="arm" build.
    'cpu': 'arm',
    'platform': 'linux2_x86_64',
}]

_CHROMIUM_DEPS = {
    'chromium_commands': {
        'archs': _HOST_ARCH,
        'build_path': 'lib.java/chromium_commands.dex.jar',
        'target_name': 'chromium_commands_java',
    },
    'forwarder_device': {
        'archs': _DEVICE_ARCHS,
        'build_path': 'device_forwarder',
        'target_name': 'forwarder2',
    },
    'forwarder_host': {
        'archs': _HOST_ARCH,
        'build_path': 'clang_x64/host_forwarder',
        'target_name': 'forwarder2',
    },
    'md5sum_device': {
        'archs': _DEVICE_ARCHS,
        'build_path': 'md5sum_bin',
        'target_name': 'md5sum',
    },
    'md5sum_host': {
        'archs': _HOST_ARCH,
        'build_path': 'clang_x64/md5sum_bin',
        'target_name': 'md5sum',
    },
}


def BuildTargetsForCpu(targets, cpu, output_dir):
  logging.info('Building %s', cpu)

  gn_args = [
      'ffmpeg_branding="Chrome"',
      'is_component_build=false',
      'is_debug=false',
      'proprietary_codecs=true',
      'symbol_level=1',
      'target_cpu="%s"' % cpu,
      'target_os="android"',
      'use_goma=true',
  ]

  cmd = ['gn', 'gen', '--args=%s' % (' '.join(gn_args)), output_dir]
  ec = cmd_helper.RunCmd(cmd)
  if ec:
    raise base_error.BaseError('%s failed with %d' % (cmd, ec))

  ec = cmd_helper.RunCmd(['autoninja', '-C', output_dir] + targets)
  if ec:
    raise base_error.BaseError('building %s failed with %d' % (cpu, ec))


def UpdateDependency(dependency_name, dependency_info, local_path, platform):
  bucket = dependency_info['cloud_storage_bucket']
  folder = dependency_info['cloud_storage_base_folder']

  # determine the hash
  ec, sha1sum_output = cmd_helper.GetCmdStatusAndOutput(['sha1sum', local_path])
  if ec:
    raise base_error.BaseError(
        'Failed to determine SHA1 for %s: %s' % (local_path, sha1sum_output))

  dependency_sha1 = sha1sum_output.split()[0]

  # upload
  remote_path = '%s_%s' % (dependency_name, dependency_sha1)
  gs_dest = 'gs://%s/%s/%s' % (bucket, folder, remote_path)
  ec, gsutil_output = cmd_helper.GetCmdStatusAndOutput(
      ['gsutil.py', 'cp', local_path, gs_dest])
  if ec:
    raise base_error.BaseError(
        'Failed to upload %s to %s: %s' % (remote_path, gs_dest, gsutil_output))

  # update entry in json
  file_info = dependency_info['file_info']
  if platform not in file_info:
    file_info[platform] = {
        'cloud_storage_hash': '',
        # the user will need to manually update the download path after
        # uploading a previously unknown dependency.
        'download_path': 'FIXME',
    }
  file_info[platform]['cloud_storage_hash'] = dependency_sha1


def UpdateChromiumDependencies(dependencies, args):
  deps_by_platform = collections.defaultdict(list)
  for dep_name, dep_info in _CHROMIUM_DEPS.iteritems():
    archs = dep_info.get('archs', [])
    for a in archs:
      deps_by_platform[(a.get('cpu'), a.get('platform'))].append(
          (dep_name, dep_info.get('build_path'), dep_info.get('target_name')))

  for arch, arch_deps in deps_by_platform.iteritems():
    targets = [target_name for _n, _b, target_name in arch_deps]
    cpu, platform = arch
    output_dir = os.path.join(args.chromium_src_dir, 'out-devil-deps', platform)
    BuildTargetsForCpu(targets, cpu, output_dir)

    for dep_name, build_path, _ in arch_deps:
      local_path = os.path.abspath(os.path.join(output_dir, build_path))
      UpdateDependency(dep_name,
                       dependencies.get('dependencies', {}).get(dep_name, {}),
                       local_path, platform)

  return dependencies


def UpdateGivenDependency(dependencies, args):
  dep_name = args.name or os.path.basename(args.path)
  if not dep_name in dependencies.get('dependencies', {}):
    raise base_error.BaseError('Could not find dependency "%s" in %s' %
                               (dep_name, args.dependencies_json))

  UpdateDependency(dep_name,
                   dependencies.get('dependencies', {}).get(dep_name, {}),
                   args.path, args.platform)

  return dependencies


def main(raw_args):
  parser = argparse.ArgumentParser(description=__doc__)

  # pylint: disable=protected-access
  parser.add_argument(
      '--dependencies-json',
      type=os.path.abspath,
      default=devil_env._DEVIL_DEFAULT_CONFIG,
      help='Binary dependency configuration file to update.')
  # pylint: enable=protected-access

  subparsers = parser.add_subparsers()
  chromium_parser = subparsers.add_parser('chromium')
  chromium_parser.add_argument(
      '--chromium-src-dir',
      type=os.path.realpath,
      default=os.getcwd(),
      help='Path to chromium/src checkout root.')
  chromium_parser.set_defaults(update_dependencies=UpdateChromiumDependencies)

  dependency_parser = subparsers.add_parser('dependency')
  dependency_parser.add_argument('--name', help='Name of dependency to update.')
  dependency_parser.add_argument(
      '--path',
      type=os.path.abspath,
      help='Path to file to upload as new version of dependency.')
  dependency_parser.add_argument(
      '--platform', help='Platform of dependency to update.')
  dependency_parser.set_defaults(update_dependencies=UpdateGivenDependency)

  args = parser.parse_args(raw_args)

  logging.getLogger().setLevel(logging.INFO)

  with open(args.dependencies_json) as f:
    dependencies = json.load(f)

  dependencies = args.update_dependencies(dependencies, args)

  with open(args.dependencies_json, 'w') as f:
    json.dump(dependencies, f, indent=2, separators=(',', ': '), sort_keys=True)

  return 0


if __name__ == '__main__':
  sys.exit(main(sys.argv[1:]))
