#!/bin/bash
#
# Copyright (C) 2017 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

if [[ ! -d art ]]; then
  echo "Script needs to be run at the root of the android tree"
  exit 1
fi

ALL_CONFIGS=(linux-ia32 linux-x64 linux-armv8 linux-armv7 android-armv8 android-armv7)

usage() {
  local config
  local golem_target

  (cat << EOF
  Usage: $(basename "${BASH_SOURCE[0]}") [--golem=<target>] --machine-type=MACHINE_TYPE
                 [--tarball[=<target>.tar.gz]]

  Build minimal art binaries required to run golem benchmarks either
  locally or on the golem servers.

  Creates the \$MACHINE_TYPE binaries in your \$OUT_DIR, and if --tarball was specified,
  it also tars the results of the build together into your <target.tar.gz> file.
  --------------------------------------------------------
  Required Flags:
    --machine-type=MT   Specify the machine type that will be built.

  Optional Flags":
    --golem=<target>    Builds with identical commands that Golem servers use.
    --tarball[=o.tgz]   Tar/gz the results. File name defaults to <machine_type>.tar.gz
    -j<num>             Specify how many jobs to use for parallelism.
    --help              Print this help listing.
    --showcommands      Show commands as they are being executed.
    --simulate          Print commands only, don't execute commands.
EOF
  ) | sed -e 's/^[[:space:]][[:space:]]//g' >&2 # Strip leading whitespace from heredoc.

  echo >&2 "Available machine types:"
  for config in "${ALL_CONFIGS[@]}"; do
    echo >&2 "  $config"
  done

  echo >&2
  echo >&2 "Available Golem targets:"
  while IFS='' read -r golem_target; do
    echo >&2 "  $golem_target"
  done < <("$(thisdir)/env" --list-targets)
}

# Check if $1 element is in array $2
contains_element() {
  local e
  for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done
  return 1
}

# Display a command, but don't execute it, if --showcommands was set.
show_command() {
  if [[ $showcommands == "showcommands" ]]; then
    echo "$@"
  fi
}

# Execute a command, displaying it if --showcommands was set.
# If --simulate is used, command is not executed.
execute() {
  show_command "$@"
  execute_noshow "$@"
}

# Execute a command unless --simulate was used.
execute_noshow() {
  if [[ $simulate == "simulate" ]]; then
    return 0
  fi

  local prog="$1"
  shift
  "$prog" "$@"
}

# Export environment variable, echoing it to screen.
setenv() {
  local name="$1"
  local value="$2"

  export $name="$value"
  echo export $name="$value"
}

# Export environment variable, echoing $3 to screen ($3 is meant to be unevaluated).
setenv_escape() {
  local name="$1"
  local value="$2"
  local escaped_value="$3"

  export $name="$value"
  echo export $name="$escaped_value"
}

log_usage_error() {
  echo >&2 "ERROR: " "$@"
  echo >&2 "       See --help for the correct usage information."
  exit 1
}

log_fatal() {
  echo >&2 "FATAL: " "$@"
  exit 2
}

# Get the directory of this script.
thisdir() {
  (\cd "$(dirname "${BASH_SOURCE[0]}")" && pwd )
}

# Get the path to the top of the Android source tree.
gettop() {
  if [[ "x$ANDROID_BUILD_TOP" != "x" ]]; then
    echo "$ANDROID_BUILD_TOP";
  else
    echo "$(thisdir)/../../.."
  fi
}

# Get a build variable from the Android build system.
get_build_var() {
  local varname="$1"

  # include the desired target product/build-variant
  # which won't be set in our env if neither we nor the user first executed
  # source build/envsetup.sh (e.g. if simulating from a fresh shell).
  local extras
  [[ -n $target_product ]] && extras+=" TARGET_PRODUCT=$target_product"
  [[ -n $target_build_variant ]] && extras+=" TARGET_BUILD_VARIANT=$target_build_variant"

  # call dumpvar-$name from the makefile system.
  (\cd "$(gettop)";
  CALLED_FROM_SETUP=true BUILD_SYSTEM=build/core \
    command make --no-print-directory -f build/core/config.mk \
    $extras \
    dumpvar-$varname)
}

# Defaults from command-line.

mode=""  # blank or 'golem' if --golem was specified.
golem_target="" # --golem=$golem_target
config="" # --machine-type=$config
j_arg="-j8"
showcommands=""
simulate=""
make_tarball=""
tarball=""

# Parse command line arguments

while [[ "$1" != "" ]]; do
  case "$1" in
    --help)
      usage
      exit 1
      ;;
    --golem=*)
      mode="golem"
      golem_target="${1##--golem=}"

      if [[ "x$golem_target" == x ]]; then
        log_usage_error "Missing --golem target type."
      fi

      shift
      ;;
    --machine-type=*)
      config="${1##--machine-type=}"
      if ! contains_element "$config" "${ALL_CONFIGS[@]}"; then
        log_usage_error "Invalid --machine-type value '$config'"
      fi
      shift
      ;;
    --tarball)
      tarball="" # reuse the machine type name.
      make_tarball="make_tarball"
      shift
      ;;
    --tarball=*)
      tarball="${1##--tarball=}"
      make_tarball="make_tarball"
      shift
      ;;
    -j*)
      j_arg="$1"
      shift
      ;;
    --showcommands)
      showcommands="showcommands"
      shift
      ;;
    --simulate)
      simulate="simulate"
      shift
      ;;
    *)
      log_usage_error "Unknown options $1"
      ;;
  esac
done

###################################
###################################
###################################

if [[ -z $config ]]; then
  log_usage_error "--machine-type option is required."
fi

# --tarball defaults to the --machine-type value with .tar.gz.
tarball="${tarball:-$config.tar.gz}"

target_product="$TARGET_PRODUCT"
target_build_variant="$TARGET_BUILD_VARIANT"

# If not using --golem, use whatever the user had lunch'd prior to this script.
if [[ $mode == "golem" ]]; then
  # This section is intended solely to be executed by a golem build server.

  target_build_variant=eng
  case "$config" in
    *-armv7)
      target_product="arm_krait"
      ;;
    *-armv8)
      target_product="armv8"
      ;;
    *)
      target_product="sdk"
      ;;
  esac

  if [[ $target_product = arm* ]]; then
    # If using the regular manifest, e.g. 'master'
    # The lunch command for arm will assuredly fail because we don't have device/generic/art.
    #
    # Print a human-readable error message instead of trying to lunch and failing there.
    if ! [[ -d "$(gettop)/device/generic/art" ]]; then
      log_fatal "Missing device/generic/art directory. Perhaps try master-art repo manifest?\n" \
                "       Cannot build ARM targets (arm_krait, armv8) for Golem." >&2
    fi
    # We could try to keep on simulating but it seems brittle because we won't have the proper
    # build variables to output the right strings.
  fi

  # Get this particular target's environment variables (e.g. ART read barrier on/off).
  source "$(thisdir)"/env "$golem_target" || exit 1

  lunch_target="$target_product-$target_build_variant"

  execute 'source' build/envsetup.sh
  # Build generic targets (as opposed to something specific like aosp_angler-eng).
  execute lunch "$lunch_target"
  setenv JACK_SERVER false
  setenv_escape JACK_REPOSITORY "$PWD/prebuilts/sdk/tools/jacks" '$PWD/prebuilts/sdk/tools/jacks'
  # Golem uses master-art repository which is missing a lot of other libraries.
  setenv SOONG_ALLOW_MISSING_DEPENDENCIES true
  # Golem may be missing tools such as javac from its path.
  setenv_escape PATH "/usr/lib/jvm/java-8-openjdk-amd64/bin/:$PATH" '/usr/lib/jvm/java-8-openjdk-amd64/bin/:$PATH'
else
  # Look up the default variables from the build system if they weren't set already.
  [[ -z $target_product ]] && target_product="$(get_build_var TARGET_PRODUCT)"
  [[ -z $target_build_variant ]] && target_build_variant="$(get_build_var TARGET_BUILD_VARIANT)"
fi

# Defaults for all machine types.
make_target="build-art-target-golem"
out_dir="out/x86_64"
root_dir_var="PRODUCT_OUT"
strip_symbols=false
bit64_suffix=""
tar_directories=(system data/art-test)

# Per-machine type overrides
if [[ $config == linux-arm* ]]; then
    setenv ART_TARGET_LINUX true
fi

case "$config" in
  linux-ia32|linux-x64)
    root_dir_var="HOST_OUT"
    # Android strips target builds automatically, but not host builds.
    strip_symbols=true
    make_target="build-art-host-golem"

    if [[ $config == linux-ia32 ]]; then
      out_dir="out/x86"
      setenv HOST_PREFER_32_BIT true
    else
      bit64_suffix="64"
    fi

    tar_directories=(bin framework usr lib${bit64_suffix})
    ;;
  *-armv8)
    bit64_suffix="64"
    ;;
  *-armv7)
    ;;
  *)
    log_fatal "Unsupported machine-type '$config'"
esac

# Golem benchmark run commands expect a certain $OUT_DIR to be set,
# so specify it here.
#
# Note: It is questionable if we want to customize this since users
# could alternatively probably use their own build directly (and forgo this script).
setenv OUT_DIR "$out_dir"
root_dir="$(get_build_var "$root_dir_var")"

if [[ $mode == "golem" ]]; then
  # For golem-style running only.
  # Sets the DT_INTERP to this path in every .so we can run the
  # non-system version of dalvikvm with our own copies of the dependencies (e.g. our own libc++).
  if [[ $config == android-* ]]; then
    # TODO: the linker can be relative to the binaries
    # (which is what we do for linux-armv8 and linux-armv7)
    golem_run_path="/data/local/tmp/runner/"
  else
    golem_run_path=""
  fi

  # Only do this for target builds. Host doesn't need this.
  if [[ $config == *-arm* ]]; then
    setenv CUSTOM_TARGET_LINKER "${golem_run_path}${root_dir}/system/bin/linker${bit64_suffix}"
  fi
fi

#
# Main command execution below here.
# (everything prior to this just sets up environment variables,
#  and maybe calls lunch).
#

execute make "${j_arg}" "${make_target}"

if $strip_symbols; then
  # Further reduce size by stripping symbols.
  execute_noshow strip $root_dir/bin/* || true
  show_command strip $root_dir/bin/'*'  '|| true'
  execute_noshow strip $root_dir/lib${bit64_suffix}/'*'
  show_command strip $root_dir/lib${bit64_suffix}/'*'
fi

if [[ "$make_tarball" == "make_tarball" ]]; then
  # Create a tarball which is required for the golem build resource.
  # (In particular, each golem benchmark's run commands depend on a list of resource files
  #  in order to have all the files it needs to actually execute,
  #  and this tarball would satisfy that particular target+machine-type's requirements).
  dirs_rooted=()
  for tar_dir in "${tar_directories[@]}"; do
    dirs_rooted+=("$root_dir/$tar_dir")
  done

  execute tar -czf "${tarball}" "${dirs_rooted[@]}" --exclude .git --exclude .gitignore
  tar_result=$?
  if [[ $tar_result -ne 0 ]]; then
    [[ -f $tarball ]] && rm $tarball
  fi

  show_command '[[ $? -ne 0 ]] && rm' "$tarball"
fi

