#!/usr/bin/env python

"""
  This source file is part of the Swift.org open source project

  Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
  Licensed under Apache License v2.0 with Runtime Library Exception

  See https://swift.org/LICENSE.txt for license information
  See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors

 ------------------------------------------------------------------------------
 This is a helper script for the main swift repository's build-script.py that
 knows how to build and install the stress tester utilities given a swift
 workspace.

"""

from __future__ import print_function

import argparse
import sys
import os, platform
import subprocess

def printerr(message):
    print(message, file=sys.stderr)

def main(argv_prefix = []):
  args = parse_args(argv_prefix + sys.argv[1:])
  run(args)

def parse_args(args):
  parser = argparse.ArgumentParser(prog='BUILD-SCRIPT-HELPER.PY')

  parser.add_argument('--package-dir', default='SourceKitStressTester')
  parser.add_argument('-v', '--verbose', action='store_true', help='log executed commands')
  parser.add_argument('--prefix', help='install path')
  parser.add_argument('--config', default='debug')
  parser.add_argument('--build-dir', default='.build')
  parser.add_argument('--multiroot-data-file', help='Path to an Xcode workspace to create a unified build of SwiftSyntax with other projects.')
  parser.add_argument('--toolchain', required=True, help='the toolchain to use when building this package')
  parser.add_argument('--update', action='store_true', help='update all SwiftPM dependencies')
  parser.add_argument('--no-local-deps', action='store_true', help='use normal remote dependencies when building')
  parser.add_argument('build_actions', help="Extra actions to perform. Can be any number of the following", choices=['all', 'build', 'test', 'install', 'generate-xcodeproj'], nargs="*", default=['build'])

  parsed = parser.parse_args(args)

  if ("install" in parsed.build_actions or "all" in parsed.build_actions) and not parsed.prefix:
    ArgumentParser.error("'--prefix' is required with the install action")
  parsed.swift_exec = os.path.join(parsed.toolchain, 'usr', 'bin', 'swift')

  parsed.sourcekitd_dir = os.path.join(parsed.toolchain, 'usr', 'lib')

  # Convert package_dir to absolute path, relative to root of repo.
  repo_path = os.path.dirname(__file__)
  parsed.package_dir = os.path.realpath(
                        os.path.join(repo_path, parsed.package_dir))

  # Convert build_dir to absolute path, relative to package_dir.
  parsed.build_dir = os.path.join(parsed.package_dir, parsed.build_dir)

  return parsed

def run(args):
  sourcekit_searchpath=args.sourcekitd_dir
  package_name = os.path.basename(args.package_dir)

  env = dict(os.environ)
  # Use local dependencies (i.e. checked out next sourcekit-lsp).
  if not args.no_local_deps:
    env['SWIFTCI_USE_LOCAL_DEPS'] = "1"

  if args.update:
    print("** Updating dependencies of %s **" % package_name)
    try:
      update_swiftpm_dependencies(package_dir=args.package_dir,
        swift_exec=args.swift_exec,
        build_dir=args.build_dir,
        env=env,
        verbose=args.verbose)
    except subprocess.CalledProcessError as e:
      printerr('FAIL: Updating dependencies of %s failed' % package_name)
      printerr('Executing: %s' % ' '.join(e.cmd))
      sys.exit(1)

  # The test action creates its own build. No need to build if we are just testing
  if should_run_any_action(['build', 'install'], args.build_actions):
    print("** Building %s **" % package_name)
    try:
      invoke_swift(package_dir=args.package_dir,
        swift_exec=args.swift_exec,
        action='build',
        products=get_products(args.package_dir),
        sourcekit_searchpath=sourcekit_searchpath,
        build_dir=args.build_dir,
        multiroot_data_file=args.multiroot_data_file,
        config=args.config,
        env=env,
        verbose=args.verbose)
    except subprocess.CalledProcessError as e:
      printerr('FAIL: Building %s failed' % package_name)
      printerr('Executing: %s' % ' '.join(e.cmd))
      sys.exit(1)

  output_dir = os.path.realpath(os.path.join(args.build_dir, args.config))

  if should_run_action("generate-xcodeproj", args.build_actions):
    print("** Generating Xcode project for %s **" % package_name)
    try:
      generate_xcodeproj(args.package_dir,
        swift_exec=args.swift_exec,
        sourcekit_searchpath=sourcekit_searchpath,
        env=env,
        verbose=args.verbose)
    except subprocess.CalledProcessError as e:
      printerr('FAIL: Generating the Xcode project failed')
      printerr('Executing: %s' % ' '.join(e.cmd))
      sys.exit(1)

  if should_run_action("test", args.build_actions):
    print("** Testing %s **" % package_name)
    try:
      invoke_swift(package_dir=args.package_dir,
        swift_exec=args.swift_exec,
        action='test',
        products=['%sPackageTests' % package_name],
        sourcekit_searchpath=sourcekit_searchpath,
        build_dir=args.build_dir,
        multiroot_data_file=args.multiroot_data_file,
        config=args.config,
        env=env,
        verbose=args.verbose)
    except subprocess.CalledProcessError as e:
      printerr('FAIL: Testing %s failed' % package_name)
      printerr('Executing: %s' % ' '.join(e.cmd))
      sys.exit(1)

  if should_run_action("install", args.build_actions):
    print("** Installing %s **" % package_name)
    stdlib_dir = os.path.join(args.toolchain, 'usr', 'lib', 'swift', 'macosx')
    try:
      install_package(args.package_dir,
        install_dir=args.prefix,
        sourcekit_searchpath=sourcekit_searchpath,
        build_dir=output_dir,
        rpaths_to_delete=[stdlib_dir],
        verbose=args.verbose)
    except subprocess.CalledProcessError as e:
      printerr('FAIL: Installing %s failed' % package_name)
      printerr('Executing: %s' % ' '.join(e.cmd))
      sys.exit(1)


# Returns true if any of the actions in `action_names` should be run.
def should_run_any_action(action_names, selected_actions):
  for action_name in action_names:
    if should_run_action(action_name, selected_actions):
      return True
  return False


def should_run_action(action_name, selected_actions):
  if action_name in selected_actions:
    return True
  elif "all" in selected_actions:
    return True
  else:
    return False


def update_swiftpm_dependencies(package_dir, swift_exec, build_dir, env, verbose):
  args = [swift_exec, 'package', '--package-path', package_dir, '--build-path', build_dir, 'update']
  check_call(args, env=env, verbose=verbose)


def invoke_swift(package_dir, swift_exec, action, products, sourcekit_searchpath, build_dir, multiroot_data_file, config, env, verbose):
  # Until rdar://53881101 is implemented, we cannot request a build of multiple 
  # targets simultaneously. For now, just build one product after the other.
  for product in products:
    invoke_swift_single_product(package_dir, swift_exec, action, product, sourcekit_searchpath, build_dir, multiroot_data_file, config, env, verbose)


def invoke_swift_single_product(package_dir, swift_exec, action, product, sourcekit_searchpath, build_dir, multiroot_data_file, config, env, verbose):
  args = [swift_exec, action, '--package-path', package_dir, '-c', config, '--build-path', build_dir]

  if multiroot_data_file:
    args.extend(['--multiroot-data-file', multiroot_data_file])

  if action == 'test':
    args.extend(['--test-product', product])
  else:
    args.extend(['--product', product])

  # Tell SwiftSyntax that we are building in a build-script environment so that
  # it does not need to rebuilt if it has already been built before.
  env['SWIFT_BUILD_SCRIPT_ENVIRONMENT'] = '1'
  env['SWIFT_STRESS_TESTER_SOURCEKIT_SEARCHPATH'] = sourcekit_searchpath

  check_call(args, env=env, verbose=verbose)


def install_package(package_dir, install_dir, sourcekit_searchpath, build_dir, rpaths_to_delete, verbose):
  bin_dir = os.path.join(install_dir, 'bin')
  lib_dir = os.path.join(install_dir, 'lib', 'swift', 'macosx')

  for directory in [bin_dir, lib_dir]:
    if not os.path.exists(directory):
      os.makedirs(directory)

  # Install sk-stress-test and sk-swiftc-wrapper
  for product in get_products(package_dir):
    src = os.path.join(build_dir, product)
    dest = os.path.join(bin_dir, product)

    # Create a copy of the list since we modify it
    rpaths_to_delete_for_this_product = list(rpaths_to_delete)
    # Add the rpath to the stdlib in in the toolchain
    rpaths_to_add = ['@executable_path/../lib/swift/macosx']
    
    if product in ['sk-stress-test', 'swift-evolve']:
      # Make the rpath to sourcekitd relative in the toolchain
      rpaths_to_delete_for_this_product += [sourcekit_searchpath]
      rpaths_to_add += ['@executable_path/../lib']

    install(src, dest,
      rpaths_to_delete=rpaths_to_delete_for_this_product,
      rpaths_to_add=rpaths_to_add,
      verbose=verbose)

def install(src, dest, rpaths_to_delete, rpaths_to_add, verbose):
  copy_cmd=['rsync', '-a', src, dest]
  print('installing %s to %s' % (os.path.basename(src), dest))
  check_call(copy_cmd, verbose=verbose)

  for rpath in rpaths_to_delete:
    remove_rpath(dest, rpath, verbose=verbose)
  for rpath in rpaths_to_add:
    add_rpath(dest, rpath, verbose=verbose)

def generate_xcodeproj(package_dir, swift_exec, sourcekit_searchpath, env, verbose):
  package_name = os.path.basename(package_dir)
  config_path = os.path.join(package_dir, 'Config.xcconfig')
  with open(config_path, 'w') as config_file:
    config_file.write('''
      SYSTEM_FRAMEWORK_SEARCH_PATHS = {sourcekit_searchpath} $(inherited)
      LD_RUNPATH_SEARCH_PATHS = {sourcekit_searchpath} $(inherited)
    '''.format(sourcekit_searchpath=sourcekit_searchpath))

  xcodeproj_path = os.path.join(package_dir, '%s.xcodeproj' % package_name)

  args = [swift_exec, 'package', '--package-path', package_dir, 'generate-xcodeproj', '--xcconfig-overrides', config_path, '--output', xcodeproj_path]
  check_call(args, env=env, verbose=verbose)

def add_rpath(binary, rpath, verbose):
  cmd = ['install_name_tool', '-add_rpath', rpath, binary]
  check_call(cmd, verbose=verbose)


def remove_rpath(binary, rpath, verbose):
  cmd = ['install_name_tool', '-delete_rpath', rpath, binary]
  check_call(cmd, verbose=verbose)


def check_call(cmd, verbose, env=os.environ, **kwargs):
  if verbose:
    print(' '.join([escape_cmd_arg(arg) for arg in cmd]))
  return subprocess.check_call(cmd, env=env, stderr=subprocess.STDOUT, **kwargs)


def interleave(value, list):
    return [item for pair in zip([value] * len(list), list) for item in pair]


def escape_cmd_arg(arg):
  if '"' in arg or ' ' in arg:
    return '"%s"' % arg.replace('"', '\\"')
  else:
    return arg

def get_products(package_dir):
  # FIXME: We ought to be able to query SwiftPM for this info.
  if package_dir.endswith("/SourceKitStressTester"):
    return ['sk-stress-test', 'sk-swiftc-wrapper']
  elif package_dir.endswith("/SwiftEvolve"):
    return ['swift-evolve']
  else:
    return []

if __name__ == '__main__':
  main()

[((34, 11, 34, 65), 'argparse.ArgumentParser', 'argparse.ArgumentParser', (), '', False, 'import argparse\n'), ((51, 22, 51, 75), 'os.path.join', 'os.path.join', ({(51, 35, 51, 51): 'parsed.toolchain', (51, 53, 51, 58): '"""usr"""', (51, 60, 51, 65): '"""bin"""', (51, 67, 51, 74): '"""swift"""'}, {}), "(parsed.toolchain, 'usr', 'bin', 'swift')", False, 'import os, platform\n'), ((53, 26, 53, 70), 'os.path.join', 'os.path.join', ({(53, 39, 53, 55): 'parsed.toolchain', (53, 57, 53, 62): '"""usr"""', (53, 64, 53, 69): '"""lib"""'}, {}), "(parsed.toolchain, 'usr', 'lib')", False, 'import os, platform\n'), ((56, 14, 56, 39), 'os.path.dirname', 'os.path.dirname', ({(56, 30, 56, 38): '__file__'}, {}), '(__file__)', False, 'import os, platform\n'), ((61, 21, 61, 71), 'os.path.join', 'os.path.join', ({(61, 34, 61, 52): 'parsed.package_dir', (61, 54, 61, 70): 'parsed.build_dir'}, {}), '(parsed.package_dir, parsed.build_dir)', False, 'import os, platform\n'), ((67, 17, 67, 51), 'os.path.basename', 'os.path.basename', ({(67, 34, 67, 50): 'args.package_dir'}, {}), '(args.package_dir)', False, 'import os, platform\n'), ((204, 12, 204, 44), 'os.path.join', 'os.path.join', ({(204, 25, 204, 36): 'install_dir', (204, 38, 204, 43): '"""bin"""'}, {}), "(install_dir, 'bin')", False, 'import os, platform\n'), ((205, 12, 205, 63), 'os.path.join', 'os.path.join', ({(205, 25, 205, 36): 'install_dir', (205, 38, 205, 43): '"""lib"""', (205, 45, 205, 52): '"""swift"""', (205, 54, 205, 62): '"""macosx"""'}, {}), "(install_dir, 'lib', 'swift', 'macosx')", False, 'import os, platform\n'), ((242, 17, 242, 46), 'os.path.basename', 'os.path.basename', ({(242, 34, 242, 45): 'package_dir'}, {}), '(package_dir)', False, 'import os, platform\n'), ((243, 16, 243, 60), 'os.path.join', 'os.path.join', ({(243, 29, 243, 40): 'package_dir', (243, 42, 243, 59): '"""Config.xcconfig"""'}, {}), "(package_dir, 'Config.xcconfig')", False, 'import os, platform\n'), ((250, 19, 250, 75), 'os.path.join', 'os.path.join', ({(250, 32, 250, 43): 'package_dir', (250, 45, 250, 74): "'%s.xcodeproj' % package_name"}, {}), "(package_dir, '%s.xcodeproj' % package_name)", False, 'import os, platform\n'), ((268, 9, 268, 80), 'subprocess.check_call', 'subprocess.check_call', (), '', False, 'import subprocess\n'), ((58, 24, 58, 67), 'os.path.join', 'os.path.join', ({(58, 37, 58, 46): 'repo_path', (58, 48, 58, 66): 'parsed.package_dir'}, {}), '(repo_path, parsed.package_dir)', False, 'import os, platform\n'), ((106, 32, 106, 73), 'os.path.join', 'os.path.join', ({(106, 45, 106, 59): 'args.build_dir', (106, 61, 106, 72): 'args.config'}, {}), '(args.build_dir, args.config)', False, 'import os, platform\n'), ((141, 17, 141, 78), 'os.path.join', 'os.path.join', ({(141, 30, 141, 44): 'args.toolchain', (141, 46, 141, 51): '"""usr"""', (141, 53, 141, 58): '"""lib"""', (141, 60, 141, 67): '"""swift"""', (141, 69, 141, 77): '"""macosx"""'}, {}), "(args.toolchain, 'usr', 'lib', 'swift', 'macosx')", False, 'import os, platform\n'), ((213, 10, 213, 42), 'os.path.join', 'os.path.join', ({(213, 23, 213, 32): 'build_dir', (213, 34, 213, 41): 'product'}, {}), '(build_dir, product)', False, 'import os, platform\n'), ((214, 11, 214, 41), 'os.path.join', 'os.path.join', ({(214, 24, 214, 31): 'bin_dir', (214, 33, 214, 40): 'product'}, {}), '(bin_dir, product)', False, 'import os, platform\n'), ((208, 11, 208, 36), 'os.path.exists', 'os.path.exists', ({(208, 26, 208, 35): 'directory'}, {}), '(directory)', False, 'import os, platform\n'), ((209, 6, 209, 28), 'os.makedirs', 'os.makedirs', ({(209, 18, 209, 27): 'directory'}, {}), '(directory)', False, 'import os, platform\n'), ((85, 6, 85, 17), 'sys.exit', 'sys.exit', ({(85, 15, 85, 16): '(1)'}, {}), '(1)', False, 'import sys\n'), ((104, 6, 104, 17), 'sys.exit', 'sys.exit', ({(104, 15, 104, 16): '(1)'}, {}), '(1)', False, 'import sys\n'), ((119, 6, 119, 17), 'sys.exit', 'sys.exit', ({(119, 15, 119, 16): '(1)'}, {}), '(1)', False, 'import sys\n'), ((137, 6, 137, 17), 'sys.exit', 'sys.exit', ({(137, 15, 137, 16): '(1)'}, {}), '(1)', False, 'import sys\n'), ((152, 6, 152, 17), 'sys.exit', 'sys.exit', ({(152, 15, 152, 16): '(1)'}, {}), '(1)', False, 'import sys\n'), ((233, 33, 233, 54), 'os.path.basename', 'os.path.basename', ({(233, 50, 233, 53): 'src'}, {}), '(src)', False, 'import os, platform\n')]