#!/usr/bin/env python

import os
import re
import tempfile
import shutil
import sys
import subprocess
import zipfile

# Return True if running on Windows
def IsWindows():
  return os.name == 'nt'

def GetWindowsPathWithUNCPrefix(path):
  """
  Adding UNC prefix after getting a normalized absolute Windows path,
  it's no-op for non-Windows platforms or if running under python2.
  """
  path = path.strip()

  # No need to add prefix for non-Windows platforms.
  # And \\?\ doesn't work in python 2
  if not IsWindows() or sys.version_info[0] < 3:
    return path

  # Lets start the unicode fun
  unicode_prefix = "\\\\?\\"
  if path.startswith(unicode_prefix):
    return path

  # os.path.abspath returns a normalized absolute path
  return unicode_prefix + os.path.abspath(path)

PYTHON_BINARY = '%python_binary%'
if IsWindows() and not PYTHON_BINARY.endswith('.exe'):
  PYTHON_BINARY = PYTHON_BINARY + '.exe'

# Find a file in a given search path.
def SearchPath(name):
  search_path = os.getenv('PATH', os.defpath).split(os.pathsep)
  for directory in search_path:
    if directory == '': continue
    path = os.path.join(directory, name)
    if os.path.isfile(path) and os.access(path, os.X_OK):
      return path
  return None

def IsRunningFromZip():
  return %is_zipfile%

# Find the real Python binary if it's not a normal absolute path
def FindPythonBinary(module_space):
  if PYTHON_BINARY.startswith('//'):
    # Case 1: Path is a label. Not supported yet.
    raise AssertionError(
      'Bazel does not support execution of Python interpreters via labels yet')
  elif os.path.isabs(PYTHON_BINARY):
    # Case 2: Absolute path.
    return PYTHON_BINARY
  elif os.sep in PYTHON_BINARY:
    # Case 3: Path is relative to the repo root.
    return os.path.join(module_space, PYTHON_BINARY)
  else:
    # Case 4: Path has to be looked up in the search path.
    return SearchPath(PYTHON_BINARY)

def CreatePythonPathEntries(python_imports, module_space):
  parts = python_imports.split(':');
  return [module_space] + ["%s/%s" % (module_space, path) for path in parts]

# Find the runfiles tree
def FindModuleSpace():
  stub_filename = sys.argv[0]
  if not os.path.isabs(stub_filename):
    stub_filename = os.path.join(os.getcwd(), stub_filename)

  while True:
    module_space = stub_filename + ('.exe' if IsWindows() else '') + '.runfiles'
    if os.path.isdir(module_space):
      return module_space

    runfiles_pattern = r'(.*\.runfiles)' + (r'\\' if IsWindows() else '/') + '.*'
    matchobj = re.match(runfiles_pattern, stub_filename)
    if matchobj:
      return matchobj.group(1)

    if not os.path.islink(stub_filename):
      break
    target = os.readlink(stub_filename)
    if os.path.isabs(target):
      stub_filename = target
    else:
      stub_filename = os.path.join(os.path.dirname(stub_filename), target)

  raise AssertionError('Cannot find .runfiles directory for %s' % sys.argv[0])

# Create the runfiles tree by extracting the zip file
def CreateModuleSpace():
  ZIP_RUNFILES_DIRECTORY_NAME = "runfiles"
  temp_dir = tempfile.mkdtemp("", "Bazel.runfiles_")
  zf = zipfile.ZipFile(GetWindowsPathWithUNCPrefix(os.path.dirname(__file__)))
  zf.extractall(GetWindowsPathWithUNCPrefix(temp_dir))
  return os.path.join(temp_dir, ZIP_RUNFILES_DIRECTORY_NAME)

# Returns repository roots to add to the import path.
def GetRepositoriesImports(module_space, import_all):
  if import_all:
    repo_dirs = [os.path.join(module_space, d) for d in os.listdir(module_space)]
    return [d for d in repo_dirs if os.path.isdir(d)]
  return [os.path.join(module_space, "%workspace_name%")]

# Finds the runfiles manifest or the runfiles directory.
def RunfilesEnvvar(module_space):
  # If this binary is the data-dependency of another one, the other sets
  # RUNFILES_MANIFEST_FILE or RUNFILES_DIR for our sake.
  runfiles = os.environ.get('RUNFILES_MANIFEST_FILE', None)
  if runfiles:
    return ('RUNFILES_MANIFEST_FILE', runfiles)

  runfiles = os.environ.get('RUNFILES_DIR', None)
  if runfiles:
    return ('RUNFILES_DIR', runfiles)

  # If running from a zip, there's no manifest file.
  if IsRunningFromZip():
    return ('RUNFILES_DIR', module_space)

  # Look for the runfiles "output" manifest, argv[0] + ".runfiles_manifest"
  runfiles = module_space + '_manifest'
  if os.path.exists(runfiles):
    return ('RUNFILES_MANIFEST_FILE', runfiles)

  # Look for the runfiles "input" manifest, argv[0] + ".runfiles/MANIFEST"
  runfiles = os.path.join(module_space, 'MANIFEST')
  if os.path.exists(runfiles):
    return ('RUNFILES_DIR', runfiles)

  # If running in a sandbox and no environment variables are set, then
  # Look for the runfiles  next to the binary.
  if module_space.endswith('.runfiles') and os.path.isdir(module_space):
    return ('RUNFILES_DIR', module_space)

  return (None, None)

def Main():
  args = sys.argv[1:]

  new_env = {}

  if IsRunningFromZip():
    module_space = CreateModuleSpace()
  else:
    module_space = FindModuleSpace()

  python_imports = '%imports%'
  python_path_entries = CreatePythonPathEntries(python_imports, module_space)
  python_path_entries += GetRepositoriesImports(module_space, %import_all%)

  python_path_entries = [GetWindowsPathWithUNCPrefix(d) for d in python_path_entries]

  old_python_path = os.environ.get('PYTHONPATH')
  python_path = os.pathsep.join(python_path_entries)
  if old_python_path:
    python_path += os.pathsep + old_python_path

  if IsWindows():
    python_path = python_path.replace("/", os.sep)

  new_env['PYTHONPATH'] = python_path
  runfiles_envkey, runfiles_envvalue = RunfilesEnvvar(module_space)
  if runfiles_envkey:
    new_env[runfiles_envkey] = runfiles_envvalue

  # Now look for my main python source file.
  # The magic string percent-main-percent is replaced with the filename of the
  # main file of the Python binary in BazelPythonSemantics.java.
  rel_path = '%main%'
  if IsWindows():
    rel_path = rel_path.replace("/", os.sep)

  main_filename = os.path.join(module_space, rel_path)
  main_filename = GetWindowsPathWithUNCPrefix(main_filename)
  assert os.path.exists(main_filename), \
         'Cannot exec() %r: file not found.' % main_filename
  assert os.access(main_filename, os.R_OK), \
         'Cannot exec() %r: file not readable.' % main_filename

  program = python_program = FindPythonBinary(module_space)
  if python_program is None:
    raise AssertionError('Could not find python binary: ' + PYTHON_BINARY)
  args = [python_program, main_filename] + args

  os.environ.update(new_env)

  try:
    sys.stdout.flush()
    if IsRunningFromZip():
      # If RUN_UNDER_RUNFILES equals 1, it means we need to
      # change directory to the right runfiles directory.
      # (So that the data files are accessible)
      if os.environ.get("RUN_UNDER_RUNFILES") == "1":
        os.chdir(os.path.join(module_space, "%workspace_name%"))
      retCode = subprocess.call(args)
      shutil.rmtree(os.path.dirname(module_space), True)
      exit(retCode)
    else:
      if IsWindows():
        # On Windows, os.execv doesn't handle arguments with spaces correctly,
        # and it actually starts a subprocess just like subprocess.call.
        exit(subprocess.call(args))
      else:
        os.execv(args[0], args)
  except EnvironmentError:
    # This works from Python 2.4 all the way to 3.x.
    e = sys.exc_info()[1]
    # This exception occurs when os.execv() fails for some reason.
    if not getattr(e, 'filename', None):
      e.filename = program  # Add info to error message
    raise

if __name__ == '__main__':
  Main()
