#!/usr/bin/python
# Copyright (c) 2012 The Native Client Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# Enable 'with' statements in Python 2.5
from __future__ import with_statement

import os.path
import re
import subprocess
import sys
import time

from buildbot_lib import (
    BuildContext, BuildStatus, Command, EnsureDirectoryExists,
    ParseStandardCommandLine, RemoveDirectory, RemoveGypBuildDirectories,
    RemoveSconsBuildDirectories, RunBuild, SCons, SetupLinuxEnvironment,
    SetupMacEnvironment, SetupWindowsEnvironment, SetupAndroidEnvironment,
    Step, StepLink, StepText, TryToCleanContents, RunningOnBuildbot)


def SetupContextVars(context):
  # The branch is set to native_client on the main bots, on the trybots it's
  # set to ''.  Otherwise, we should assume a particular branch is being used.
  context['branch'] = os.environ.get('BUILDBOT_BRANCH', 'native_client')
  context['off_trunk'] = context['branch'] not in ['native_client', '']


def ValidatorTest(context, architecture, validator, warn_only=False):
  cmd = [
      sys.executable,
      'tests/abi_corpus/validator_regression_test.py',
      '--keep-going',
      '--validator', validator,
      '--arch', architecture
  ]
  if warn_only:
    cmd.append('--warn-only')
  Command(context, cmd=cmd)


def SummarizeCoverage(context):
  Command(context, [
      sys.executable,
      'tools/coverage_summary.py',
      context['platform'] + '-' + context['default_scons_platform'],
  ])


def ArchiveCoverage(context):
  gsutil = '/b/build/third_party/gsutil/gsutil'
  gsd_url = 'http://gsdview.appspot.com/nativeclient-coverage2/revs'
  variant_name = ('coverage-' + context['platform'] + '-' +
                  context['default_scons_platform'])
  coverage_path = variant_name + '/html/index.html'
  revision = os.environ.get('BUILDBOT_REVISION', 'None')
  link_url = gsd_url + '/' + revision + '/' + coverage_path
  gsd_base = 'gs://nativeclient-coverage2/revs'
  gs_path = gsd_base + '/' + revision + '/' + variant_name
  cov_dir = 'scons-out/' + variant_name + '/coverage'
  # Copy lcov file.
  Command(context, [
      sys.executable, gsutil,
      'cp', '-a', 'public-read',
      cov_dir + '/coverage.lcov',
      gs_path + '/coverage.lcov',
  ])
  # Copy html.
  Command(context, [
      sys.executable, gsutil,
      'cp', '-R', '-a', 'public-read',
      'html', gs_path,
  ], cwd=cov_dir)
  print '@@@STEP_LINK@view@%s@@@' % link_url


def CommandGypBuild(context):
  use_goma = RunningOnBuildbot() and not context['no_goma']

  if use_goma:
    # Since this is for buildbot, it should not be good to use the result
    # generated by the different version compiler.
    os.environ['GOMA_HERMETIC'] = 'fallback'
    Command(context, cmd=[
        sys.executable, '/b/build/goma/goma_ctl.py', 'restart'])
  try:
    cmd = ['ninja', '-v', '-k', '0', '-C', '../out/' + context['gyp_mode']]
    if use_goma:
      cmd += ['-j50']
    Command(context, cmd=cmd)
  finally:
    if use_goma:
      Command(context, cmd=[
          sys.executable, '/b/build/goma/goma_ctl.py', 'stop'])


def CommandGypGenerate(context):
  Command(
          context,
          cmd=[
              sys.executable,
              'native_client/build/gyp_nacl',
              'native_client/build/all.gyp',
              ],
          cwd='..')


def CommandGclientRunhooks(context):
  if context.Windows():
    gclient = 'gclient.bat'
  else:
    gclient = 'gclient'
  print 'Running gclient runhooks...'
  print 'GYP_CROSSCOMPILE=' + context.GetEnv('GYP_CROSSCOMPILE', '')
  print 'GYP_GENERATORS=' + context.GetEnv('GYP_GENERATORS', '')
  print 'GYP_MSVS_VERSION=' + context.GetEnv('GYP_MSVS_VERSION', '')
  print 'GYP_DEFINES=' + context.GetEnv('GYP_DEFINES', '')
  Command(context, cmd=[gclient, 'runhooks', '--force'])


def BuildScript(status, context):
  inside_toolchain = context['inside_toolchain']

  # Clean out build directories.
  with Step('clobber', status):
    RemoveSconsBuildDirectories()
    RemoveGypBuildDirectories()

  with Step('cleanup_temp', status):
    # Picking out drive letter on which the build is happening so we can use
    # it for the temp directory.
    if context.Windows():
      build_drive = os.path.splitdrive(os.path.abspath(__file__))[0]
      tmp_dir = os.path.join(build_drive, os.path.sep + 'temp')
      context.SetEnv('TEMP', tmp_dir)
      context.SetEnv('TMP', tmp_dir)
    else:
      tmp_dir = '/tmp'
    print 'Making sure %s exists...' % tmp_dir
    EnsureDirectoryExists(tmp_dir)
    print 'Cleaning up the contents of %s...' % tmp_dir
    # Only delete files and directories like:
    # a) C:\temp\83C4.tmp
    # b) /tmp/.org.chromium.Chromium.EQrEzl
    file_name_re = re.compile(
        r'[\\/]([0-9a-fA-F]+\.tmp|\.org\.chrom\w+\.Chrom\w+\..+)$')
    file_name_filter = lambda fn: file_name_re.search(fn) is not None
    TryToCleanContents(tmp_dir, file_name_filter)

    # Mac has an additional temporary directory; clean it up.
    # TODO(bradnelson): Fix Mac Chromium so that these temp files are created
    #     with open() + unlink() so that they will not get left behind.
    if context.Mac():
      subprocess.call(
          "find /var/folders -name '.org.chromium.*' -exec rm -rfv '{}' ';'",
          shell=True)
      subprocess.call(
          "find /var/folders -name '.com.google.Chrome*' -exec rm -rfv '{}' ';'",
          shell=True)

  # Skip over hooks when run inside the toolchain build because
  # package_version would overwrite the toolchain build.
  if inside_toolchain:
    with Step('gyp_generate_only', status):
      CommandGypGenerate(context)
  else:
    with Step('gclient_runhooks', status):
      CommandGclientRunhooks(context)

  # Make sure our GN build is working.
  can_use_gn = context.Linux() and context['arch'] != 'arm'
  gn_out = '../out'

  if can_use_gn:
    def BoolFlag(cond):
      return 'true' if cond else 'false'

    gn_x86 = 'false'
    gn_x64 = 'false'
    gn_arm = 'false'

    if context['arch'] == '32':
      gn_x86 = 'true'
    elif context['arch'] == '64':
      gn_x64 = 'true'
    elif context['arch'] == 'arm':
      gn_arm = 'true'
    else:
      raise Exception("Unexpected arch: " + context['arch'])

    gn_newlib = BoolFlag(not context['use_glibc'])
    gn_glibc = BoolFlag(context['use_glibc'])

    gn_gen_args = [
      'is_debug=' + context['gn_is_debug'],
      'use_trusted_x86=' + gn_x86,
      'use_nacl_x86=' + gn_x86,
      'use_trusted_x64=' + gn_x64,
      'use_nacl_x64=' + gn_x64,
      'use_trusted_arm=' + gn_arm,
      'use_nacl_arm=' + gn_arm,
      'use_gcc_newlib=' + gn_newlib,
      'use_gcc_glibc=' + gn_glibc,
      'use_clang_newlib=' + gn_newlib,
    ]

    with Step('gn_compile', status):
      Command(context,
              cmd=['gn', '--dotfile=../native_client/.gn', '--root=..',
                   # Note: quotes are not needed around this space-separated
                   # list of args.  The shell would remove them before passing
                   # them to a program, and Python bypasses the shell.  Adding
                   # quotes will cause an error because GN will see unexpected
                   # double quotes.
                   '--args=%s' % ' '.join(gn_gen_args),
                   'gen', gn_out])
      Command(context, cmd=['ninja', '-C', gn_out])

  if context['clang']:
    with Step('update_clang', status):
      Command(context, cmd=['../tools/clang/scripts/update.sh'])

  # Just build both bitages of validator and test for --validator mode.
  if context['validator']:
    with Step('build ragel_validator-32', status):
      SCons(context, platform='x86-32', parallel=True, args=['ncval_new'])
    with Step('build ragel_validator-64', status):
      SCons(context, platform='x86-64', parallel=True, args=['ncval_new'])

    with Step('predownload validator corpus', status):
      Command(context,
          cmd=[sys.executable,
               'tests/abi_corpus/validator_regression_test.py',
               '--download-only'])

    with Step('validator_regression_test ragel x86-32', status,
        halt_on_fail=False):
      ValidatorTest(
          context, 'x86-32',
          'scons-out/opt-linux-x86-32/staging/ncval_new')
    with Step('validator_regression_test ragel x86-64', status,
        halt_on_fail=False):
      ValidatorTest(
          context, 'x86-64',
          'scons-out/opt-linux-x86-64/staging/ncval_new')

    return

  # Run checkdeps script to vet #includes.
  with Step('checkdeps', status):
    Command(context, cmd=[sys.executable, 'tools/checkdeps/checkdeps.py'])

  # Make sure our Gyp build is working.
  if not context['no_gyp']:
    with Step('gyp_compile', status):
      CommandGypBuild(context)

  # On a subset of Linux builds, build Breakpad tools for testing.
  if context['use_breakpad_tools']:
    with Step('breakpad configure', status):
      Command(context, cmd=['mkdir', '-p', 'breakpad-out'])
      Command(context, cwd='breakpad-out',
              cmd=['bash', '../../breakpad/configure',
                   'CXXFLAGS=-I../..'])  # For third_party/lss
    with Step('breakpad make', status):
      Command(context, cmd=['make', '-j%d' % context['max_jobs']],
              cwd='breakpad-out')

  # The main compile step.
  with Step('scons_compile', status):
    SCons(context, parallel=True, args=[])

  if context['coverage']:
    with Step('collect_coverage', status, halt_on_fail=True):
      SCons(context, args=['coverage'])
    with Step('summarize_coverage', status, halt_on_fail=False):
      SummarizeCoverage(context)
    slave_type = os.environ.get('BUILDBOT_SLAVE_TYPE')
    if slave_type != 'Trybot' and slave_type is not None:
      with Step('archive_coverage', status, halt_on_fail=True):
        ArchiveCoverage(context)
    return

  # Android bots don't run tests for now.
  if context['android']:
    return

  ### BEGIN tests ###
  with Step('small_tests', status, halt_on_fail=False):
    SCons(context, args=['small_tests'])
  with Step('medium_tests', status, halt_on_fail=False):
    SCons(context, args=['medium_tests'])
  with Step('large_tests', status, halt_on_fail=False):
    SCons(context, args=['large_tests'])

  with Step('compile IRT tests', status):
    SCons(context, parallel=True, mode=['nacl_irt_test'])

  with Step('small_tests under IRT', status, halt_on_fail=False):
    SCons(context, mode=context['default_scons_mode'] + ['nacl_irt_test'],
          args=['small_tests_irt'])
  with Step('medium_tests under IRT', status, halt_on_fail=False):
    SCons(context, mode=context['default_scons_mode'] + ['nacl_irt_test'],
          args=['medium_tests_irt'])
  ### END tests ###

  ### BEGIN GN tests ###
  if can_use_gn:
    arch_name = {
      'arm': 'arm',
      '32': 'x86',
      '64': 'x64'
    }[context['arch']]
    gn_sel_ldr = os.path.join(gn_out, 'trusted_' + arch_name, 'sel_ldr')
    gn_extra = [
        'force_sel_ldr=' + gn_sel_ldr,
        'perf_prefix=gn_',
    ]
    with Step('small_tests under GN', status, halt_on_fail=False):
      SCons(context, args=['small_tests'] + gn_extra)
    with Step('medium_tests under GN', status, halt_on_fail=False):
      SCons(context, args=['medium_tests'] + gn_extra)
    with Step('large_tests under GN', status, halt_on_fail=False):
      SCons(context, args=['large_tests'] + gn_extra)
  ### END GN tests ###


def Main():
  # TODO(ncbray) make buildbot scripts composable to support toolchain use case.
  context = BuildContext()
  status = BuildStatus(context)
  ParseStandardCommandLine(context)
  SetupContextVars(context)
  if context.Windows():
    SetupWindowsEnvironment(context)
  elif context.Linux():
    if context['android']:
      SetupAndroidEnvironment(context)
    else:
      SetupLinuxEnvironment(context)
  elif context.Mac():
    SetupMacEnvironment(context)
  else:
    raise Exception("Unsupported platform.")
  RunBuild(BuildScript, status)


def TimedMain():
  start_time = time.time()
  try:
    Main()
  finally:
    time_taken = time.time() - start_time
    print 'RESULT BuildbotTime: total= %.3f minutes' % (time_taken / 60)


if __name__ == '__main__':
  TimedMain()
