#!/usr/bin/python
# Copyright (c) 2011 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.


from optparse import OptionParser
import os
import subprocess
import sys

sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import pynacl.platform

NATIVE_CLIENT_DIR = os.path.dirname(os.path.dirname(__file__))
TOOLCHAIN_DIR = os.path.join(NATIVE_CLIENT_DIR, 'toolchain')

"""Building verification script

This module will take the output directory path, base nexe name and
architecture and combine them to generate a sel_ldr execution command
line using irt_core of the appropriate architecture.
"""


def RelativePath(filepath):
  """Return a POSIX style relative path."""
  cwdpath = os.path.abspath(os.getcwd())
  cwdparts = cwdpath.split(os.path.sep)

  filepath = os.path.abspath(filepath)

  # For Windows, convert to POSIX style path
  if sys.platform == 'win32':
    filepath = filepath.replace('\\', '/')
  fileparts = filepath.split('/')
  for index in range(len(fileparts)):
    if index >= len(cwdparts):
      break
    if cwdparts[index] != fileparts[index]:
      break

  # Add 'cd ..' the rest of cwd that doesn't match
  out = '../' * (len(cwdparts) - index)
  out += '/'.join(fileparts[index:])
  return out


def UseWin64():
  """Check if we are on 64 bit windows."""
  if sys.platform != 'win32':
    return False
  arch32 = os.environ.get('PROCESSOR_ARCHITECTURE', 'unk')
  arch64 = os.environ.get('PROCESSOR_ARCHITEW6432', 'unk')

  if arch32 == 'AMD64' or arch64 == 'AMD64':
    return True
  return False


def ErrOut(text):
  """ErrOut prints an error message and the command-line that caused it.

  Prints to standard err, both the command-line normally, and separated by
  >>...<< to make it easier to copy and paste the command, or to
  find command formating issues.
  """
  sys.stderr.write('\n\n')
  sys.stderr.write( '>>>' + '>> <<'.join(sys.argv) + '<<\n\n')
  sys.stderr.write(' '.join(sys.argv) + '<<\n\n')
  sys.stderr.write(text + '\n')
  sys.exit(1)

def Run(cmd_line):
  """Run the provided command-line returning the error code.

  Uses supbrocess to execute the command line, printing debugging information
  as well as error messages as appropriate.
  """
  sys.stdout.write('Run: %s\n' % ' '.join(cmd_line))
  sys.stdout.flush()
  try:
    ecode = subprocess.call(cmd_line)
  except Exception, err:
    ErrOut('\nFAILED: %s\n' % str(err))

  if ecode:
    ErrOut('FAILED with return value: %d\n' % ecode)
  else:
    sys.stdout.write('Success\n')
    sys.stdout.flush()
  return ecode


def GetToolchainPath(tool):
  """Given the requested tool, compute the path to that toolchain."""
  os_name = pynacl.platform.GetOS()

  if tool == 'newlib':
    lib_name = 'newlib'
  else:
    lib_name = 'glibc'

  return os.path.join(
      TOOLCHAIN_DIR,
      '%s_x86' % (os_name),
      'nacl_x86_%s_raw' % lib_name
  )


def GetLibPath(tool, arch):
  """For a given tool and architecture, determine the library path."""
  arch_map = {
    'ia32': 'lib32',
    'x64': 'lib',
  }
  lib = arch_map[arch]

  # For 64bit win, the arch is incorrectly reported as 32bit, so override it.
  if UseWin64():
    lib = 'lib'
  return os.path.join(GetToolchainPath(tool), 'x86_64-nacl', lib)


def GetNexe(path, name, arch, tool):
  """For a given nexe name, architecture, and tool generate a path to it."""
  arch_map = {
    'ia32': 'x32',
    'x64': 'x64',
  }
  # For 64bit win, the arch is incorrectly reported as 32bit, so override it.
  if UseWin64():
    arch = 'x64'
  return os.path.join(path, '%s_%s_%s.nexe' % (name, tool, arch_map[arch]))


def GetLdrIrtNexe(path, nexe, arch, tool):
  sel_ldr = os.path.join(path, 'sel_ldr')
  # If incorrectly reported use sel_ldr64 otherwise assume sel_ldr is already
  # the correct architecture.
  if UseWin64() and arch == 'ia32':
    sel_ldr = os.path.join(path, 'sel_ldr64')

  # Get IRT_CORE which is built with newlib
  irt_core = GetNexe(path, 'irt_core', arch, 'newlib')
  nexe = GetNexe(path, nexe, arch, tool)
  return (sel_ldr, irt_core, nexe)


def BuildCmdLine(path, nexe, arch, tool):
  (sel_ldr, irt_core, nexe) = GetLdrIrtNexe(path, nexe, arch, tool)
  if tool in ('newlib', 'pnacl_newlib'):
    return [sel_ldr, '-B', irt_core, '--', nexe]

  # For GLIBC we need to add 'runnable-ld.so' and allow sel_ldr access to
  # the file system.
  libpath = GetLibPath(tool, arch)
  ldso = os.path.join(libpath, 'runnable-ld.so')
  # Force POSIX style paths required by runnable-ld
  nexe = nexe.replace('\\', '/')
  return [sel_ldr, '-a', '-B', irt_core, '--', ldso, '--library-path',
          libpath, nexe]


def RunLoader(path, nexe, arch, tools, out):
  fail = 0
  for tool in tools:
    print '\n\nRunning: %s %s %s' % (nexe, arch, tool)
    cmd_line = BuildCmdLine(path, nexe, arch, tool)
    err = Run(cmd_line)
    if err:
      fail = fail + 1
      if out:
        out.write('%s on %s built with %s, failed returning %s.\n' %
                  (nexe, arch, tool, err))
    else:
      if out:
        out.write('SUCCESS: %s on %s built with %s.\n' % (nexe, arch, tool))
  return fail


def LogLoader(path, nexe, arch, tools):
  for tool in tools:
    cmd_line = BuildCmdLine(path, nexe, arch, tool)
    print ' '.join(cmd_line)
  return 0


def LogDeps(path, nexe, arch, tools):
  out_set = set()
  out_set.add(RelativePath(__file__))

  for tool in tools:
    out_set |= set(GetLdrIrtNexe(path, nexe, arch, tool))
  for out in out_set:
    # Emit forward slashes here as that's what gyp expects.
    print out.replace('\\', '/')


def Main(argv):
  parser = OptionParser()

  # Test build modes
  parser.add_option('-i', '--input', help='Generate input list.',
      action='store_const', const='i', dest='mode', default='')
  parser.add_option('-r', '--run', help='Run the nexe.',
      action='store_const', const='r', dest='mode', default='')
  parser.add_option('-l', '--log', help='Log the command-lines to run.',
      action='store_const', const='l', dest='mode', default='')

  # Options
  parser.add_option('-v', '--verbose', dest='verbose', default=False,
                    help='Enable verbosity', action='store_true')
  parser.add_option('-o', '--output', dest='output', default='',
                    help='Set output file name.', action='store')
  parser.add_option('-p', '--path', dest='path', default='.',
                    help='Set path to sel_ldr and NEXEs.', action='store')
  parser.add_option('-n', '--name', dest='name', action='store', default='',
                    help='Base name of the NEXE.')
  parser.add_option('-x', '--arch', dest='arch', action='store', default='',
                    help='Run architecture.')
  parser.add_option('-t', '--tools', dest='tools', action='append', default=[],
                    help='Select which toolchains versions to run.')
  parser.add_option('-s', '--stamp', dest='stamp',
                    help='Select path to write a stamp file to on success.')

  options, nexe_args = parser.parse_args(argv[1:])
  if not options.name:
    parser.error('Missing NEXE name.')
  if not options.mode or options.mode not in ['i', 'r', 'l']:
    parser.error('Missing run mode.')
  if not options.arch:
    parser.error('Missing run architecture.')
  if not options.tools:
    parser.error('Missing toolchains.')

  # TODO(bradnelson): Hack to prevent gyp generated path ending with \" from
  #               being interpreted as escaped quote. Here we strip the extra
  #               '/hack' we added in the gyp file.
  # http://code.google.com/p/chromium/issues/detail?id=141463
  if options.mode is 'r':
    options.path = os.path.dirname(options.path)

  if options.output:
    try:
      out = open(options.output, 'w')
    except EnvironmentError:
      print 'Failed to open %s.\n' % options.output
      return 1
  else:
    out = None

  tools = ','.join(options.tools)
  tools = tools.split(',')
  fail = 0
  for tool in tools:
    if tool not in ['newlib', 'glibc', 'pnacl_newlib']:
      parser.error('Unknown tool: %s\n' % tool)

  path = options.path
  nexe = options.name
  arch = options.arch

  if options.mode == 'r':
    fail = RunLoader(path, nexe, arch, tools, out)
  if options.mode == 'l':
    fail = LogLoader(path, nexe, arch, tools)
  if options.mode == 'i':
    fail = LogDeps('<(PRODUCT_DIR)', nexe, arch, tools)

  if out:
    out.close()

  if fail == 0 and options.stamp is not None:
    with open(options.stamp, 'w') as fh:
      fh.write('ok')

  return fail


if __name__ == '__main__':
  sys.exit(Main(sys.argv))
