#!/usr/bin/env python
# Copyright 2015 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.

import argparse
import logging
import os
import platform
import subprocess
import sys
import tempfile


# The path to the "infra/bootstrap/" directory.
BOOTSTRAP_DIR = os.path.dirname(os.path.abspath(__file__))
# The path to the "infra/" directory.
ROOT = os.path.dirname(BOOTSTRAP_DIR)
# The path where CIPD install lists are stored.
CIPD_LIST_DIR = os.path.join(BOOTSTRAP_DIR, 'cipd')
# Default sysroot install root.
DEFAULT_INSTALL_ROOT = os.path.join(ROOT, 'cipd')
# Default CIPD server url
DEFAULT_SERVER_URL='https://chrome-infra-packages.appspot.com'


# function to find the cipd binary. On POSIX this is simple ('cipd'!), but
# for windows this is more complex. depot_tools exposes this as a .bat, but
# buildbot and swarming expose it as an exe. So we have to look for it.
if sys.platform == 'win32':
  def cipd_binary():
    for p in os.environ.get('PATH', '').split(os.pathsep):
      base = os.path.join(p, 'cipd')
      for ext in ('.exe', '.bat'):
        candidate = base+ext
        if os.path.isfile(candidate):
          return candidate
    # if we didn't find it, guess 'cipd.exe'
    return 'cipd.exe'
else:
  def cipd_binary():
    return 'cipd'


# Map of CIPD configuration based on the current architecture/platform. If a
# platform is not listed here, the bootstrap will be a no-op.
#
# This is keyed on the platform's (system, machine).
ARCH_CONFIG_MAP = {
  ('Linux', 'x86_64'): {
    'cipd_install_list': 'cipd_linux_amd64.txt',
  },
  ('Linux', 'x86'): {
    'cipd_install_list': None,
  },
  ('Darwin', 'x86_64'): {
    'cipd_install_list': 'cipd_mac_amd64.txt',
  },
  ('Windows', 'x86_64'): {
    'cipd_install_list': 'cipd_windows_amd64.txt',
  },
  ('Windows', 'x86'): {
    'cipd_install_list': None,
  },
}


def get_platform_config():
  key = get_platform()
  return key, ARCH_CONFIG_MAP.get(key)


def get_platform():
  machine = platform.machine().lower()
  system = platform.system()
  machine = ({
    'amd64': 'x86_64',
    'i686': 'x86',
  }).get(machine, machine)
  if (machine == 'x86_64' and system == 'Linux' and
      sys.maxsize == (2 ** 31) - 1):
    # This is 32bit python on 64bit CPU on linux, which probably means the
    # entire userland is 32bit and thus we should play along and install 32bit
    # packages.
    machine = 'x86'
  return system, machine


def ensure_directory(path):
  # Ensure the parent directory exists.
  if os.path.isdir(path):
    return
  if os.path.exists(path):
    raise ValueError("Target file's directory [%s] exists, but is not a "
                     "directory." % (path,))
  logging.debug('Creating directory: [%s]', path)
  os.makedirs(path)


def execute(*cmd):
  if not logging.getLogger().isEnabledFor(logging.DEBUG):
    code = subprocess.call(cmd)
  else:
    # Execute the process, passing STDOUT/STDERR through our logger.
    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT)
    for line in proc.stdout:
      logging.debug('%s: %s', cmd[0], line.rstrip())
    code = proc.wait()
  if code:
    logging.error('Process failed with exit code: %d', code)
  return code


class CipdError(Exception):
  """Raised by install_cipd_client on fatal error."""


def cipd_ensure(root, ensure_file, cipd_backend_url=None):
  """Invoke `cipd ensure` with the provided ensure_file in the given root.

  Args:
    ensure_file (str) - file containing the packages to ensure
    root (str) - directory to ensure packages in. Will be created if doesn't
      exist.
  """
  cipd_backend_url = cipd_backend_url or DEFAULT_SERVER_URL

  assert os.path.isfile(ensure_file)
  ensure_directory(root)
  logging.debug('Installing CIPD packages from [%s] to [%s]', ensure_file, root)
  args = [
    'ensure',
    '-ensure-file', ensure_file,
    '-root', root,
    '-service-url', cipd_backend_url
  ]
  if execute(cipd_binary(), *args):
    raise CipdError('Failed to execute CIPD client: %s', ' '.join(args))


def cipd_ensure_list(root, ensure_data, cipd_backend_url=None):
  """Invoke `cipd ensure` with the provided ensure_data in the given root.

  Args:
    ensure_data (list(tuple)) - a list of (package_pattern, version). The
      package_patterns may contain the ${platform} and ${arch} directives
      as defined by the cipd client.
    root (str) - directory to ensure packages in
  """
  with tempfile.NamedTemporaryFile(prefix="cipd_ensure", delete=False) as tf:
    for item in ensure_data:
      print >> tf, "%s %s" % item
  try:
    cipd_ensure(root, tf.name, cipd_backend_url)
  finally:
    try:
      os.remove(tf.name)
    except OSError:
      logging.exception("failed to remove tempfile %r", tf.name)


def main(argv):
  parser = argparse.ArgumentParser('Installs CIPD bootstrap packages.')
  parser.add_argument('-v', '--verbose', action='count', default=0,
      help='Increase logging verbosity. Can be specified multiple times.')
  parser.add_argument('--cipd-backend-url', metavar='URL',
      help='Specify the CIPD backend URL (default is %(default)s)')
  parser.add_argument('-d', '--cipd-root-dir', metavar='PATH',
      default=DEFAULT_INSTALL_ROOT,
      help='Specify the root CIPD package installation directory.')

  opts = parser.parse_args(argv)

  # Setup logging verbosity.
  if opts.verbose == 0:
    level = logging.WARNING
  elif opts.verbose == 1:
    level = logging.INFO
  else:
    level = logging.DEBUG
  logging.getLogger().setLevel(level)

  root = os.path.abspath(opts.cipd_root_dir)

  # 2017/02/15 we used to bootstrap the cipd client into <infra.git>/cipd. This
  # deletes it, if it's there. This is important because otherwise the stale
  # client will end up at the front of $PATH and get used instead of the correct
  # one (i.e. the one that depot_tools / swarming / buildbot put there).
  try:
    EXE_SFX = '.exe' if sys.platform == 'win32' else ''
    os.remove(os.path.join(root, 'cipd'+EXE_SFX))
  except OSError:
    pass

  platform_key, config = get_platform_config()
  if not config:
    logging.info('No bootstrap configuration for platform [%s].', platform_key)
    return 0

  # Install the CIPD list for this configuration.
  cipd_install_list = config.get('cipd_install_list')
  if cipd_install_list:
    cipd_ensure(root,
                os.path.join(CIPD_LIST_DIR, cipd_install_list),
                opts.cipd_backend_url)
  return 0


if __name__ == '__main__':
  logging.basicConfig()
  logging.getLogger().setLevel(logging.INFO)
  sys.exit(main(sys.argv[1:]))
