sources/amper-wrapper/resources/wrappers/amper.template.sh
#!/bin/sh

#
# Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
#

# Possible environment variables:
#   AMPER_DOWNLOAD_ROOT        Maven repository to download Amper dist from.
#                              default: https://packages.jetbrains.team/maven/p/amper/amper
#   AMPER_JRE_DOWNLOAD_ROOT    Url prefix to download Amper JRE from.
#                              default: https:/
#   AMPER_BOOTSTRAP_CACHE_DIR  Cache directory to store extracted JRE and Amper distribution
#   AMPER_JAVA_HOME            JRE to run Amper itself (optional, does not affect compilation)
#   AMPER_JAVA_OPTIONS         JVM options to pass to the JVM running Amper (does not affect the user's application)

set -e -u

# The version of the Amper distribution to provision and use
amper_version=@AMPER_VERSION@
# Establish chain of trust from here by specifying exact checksum of Amper distribution to be run
amper_sha256=@AMPER_DIST_TGZ_SHA256@

AMPER_DOWNLOAD_ROOT="${AMPER_DOWNLOAD_ROOT:-https://packages.jetbrains.team/maven/p/amper/amper}"
AMPER_JRE_DOWNLOAD_ROOT="${AMPER_JRE_DOWNLOAD_ROOT:-https:/}"

die() {
  echo >&2
  echo "$@" >&2
  echo >&2
  exit 1
}

download_and_extract() {
  moniker="$1"
  file_url="$2"
  file_sha="$3"
  sha_size="$4"
  cache_dir="$5"
  extract_dir="$6"
  show_banner_on_cache_miss="$7"

  if [ -e "$extract_dir/.flag" ] && [ "$(cat "$extract_dir/.flag")" = "${file_sha}" ]; then
    # Everything is up-to-date in $extract_dir, do nothing
    return 0;
  fi

  mkdir -p "$cache_dir"

  # Take a lock for the download of this file
  short_sha=$(echo "$file_sha" | cut -c1-32) # cannot use the ${short_sha:0:32} syntax in regular /bin/sh
  download_lock_file="$cache_dir/download-${short_sha}.lock"
  process_lock_file="$cache_dir/download-${short_sha}.$$.lock"
  echo $$ >"$process_lock_file"
  while ! ln "$process_lock_file" "$download_lock_file" 2>/dev/null; do
    lock_owner=$(cat "$download_lock_file" 2>/dev/null || true)
    if [ -n "$lock_owner" ] && ps -p "$lock_owner" >/dev/null; then
      echo "Another Amper instance (pid $lock_owner) is downloading $moniker. Awaiting the result..."
      sleep 1
    elif [ -n "$lock_owner" ] && [ "$(cat "$download_lock_file" 2>/dev/null)" = "$lock_owner" ]; then
      rm -f "$download_lock_file"
      # We don't want to simply loop again here, because multiple concurrent processes may face this at the same time,
      # which means the 'rm' command above from another script could delete our new valid lock file. Instead, we just
      # ask the user to try again. This doesn't 100% eliminate the race, but the probability of issues is drastically
      # reduced because it would involve 4 processes with perfect timing. We can revisit this later.
      die "Another Amper instance (pid $lock_owner) locked the download of $moniker, but is no longer running. The lock file is now removed, please try again."
    fi
  done

  # shellcheck disable=SC2064
  trap "rm -f \"$download_lock_file\"" EXIT
  rm -f "$process_lock_file"

  unlock_and_cleanup() {
    rm -f "$download_lock_file"
    trap - EXIT
    return 0
  }

  if [ -e "$extract_dir/.flag" ] && [ "$(cat "$extract_dir/.flag")" = "${file_sha}" ]; then
    # Everything is up-to-date in $extract_dir, just release the lock
    unlock_and_cleanup
    return 0;
  fi

  if [ "$show_banner_on_cache_miss" = "true" ]; then
      echo
      echo '        _____  Welcome to                                  '
      echo '       /:::::|  ____   ___     ____      ____    __  ___   '
      echo '      /::/|::| |::::\_|:::\   |:::::\   /::::\  |::|/:::|  '
      echo '     /::/ |::| |::|\:::|\::\  |::|\::\ /:/__\:\ |:::/      '
      echo '    /::/__|::| |::| |::| |::| |::| |::|:::::::/ |::|       '
      echo '   /:::::::::| |::| |::| |::| |::|/::/ \::\__   |::|       '
      echo '  /::/    |::| |::| |::| |::| |:::::/   \::::|  |::|       '
      echo '                              |::|                         '
      echo "                              |::|  v.$amper_version       "
      echo
      echo "This is the first run of this version, so we need to download the actual Amper distribution."
      echo "Please give us a few seconds, subsequent runs will be faster."
      echo
  fi

  echo "Downloading $moniker..."

  temp_file="$cache_dir/download-file-$$.bin"
  rm -f "$temp_file"
  if command -v curl >/dev/null 2>&1; then
    if [ -t 1 ]; then CURL_PROGRESS="--progress-bar"; else CURL_PROGRESS="--silent --show-error"; fi
    # shellcheck disable=SC2086
    curl $CURL_PROGRESS -L --fail --retry 5 --connect-timeout 30 --output "${temp_file}" "$file_url"
  elif command -v wget >/dev/null 2>&1; then
    if [ -t 1 ]; then WGET_PROGRESS=""; else WGET_PROGRESS="-nv"; fi
    wget $WGET_PROGRESS --tries=5 --connect-timeout=30 --read-timeout=120 -O "${temp_file}" "$file_url"
  else
    die "ERROR: Please install 'wget' or 'curl', as Amper needs one of them to download $moniker"
  fi

  check_sha "$file_url" "$temp_file" "$file_sha" "$sha_size"

  rm -rf "$extract_dir"
  mkdir -p "$extract_dir"

  case "$file_url" in
    *".zip")
      if command -v unzip >/dev/null 2>&1; then
        unzip -q "$temp_file" -d "$extract_dir"
      else
        die "ERROR: Please install 'unzip', as Amper needs it to extract $moniker"
      fi ;;
    *)
      if command -v tar >/dev/null 2>&1; then
        tar -x -f "$temp_file" -C "$extract_dir"
      else
        die "ERROR: Please install 'tar', as Amper needs it to extract $moniker"
      fi ;;
  esac

  rm -f "$temp_file"

  echo "$file_sha" >"$extract_dir/.flag"

  # Unlock and cleanup the lock file
  unlock_and_cleanup

  echo "Download complete."
  echo
}

# usage: check_sha SOURCE_MONIKER FILE SHA_CHECKSUM SHA_SIZE
# $1 SOURCE_MONIKER (e.g. url)
# $2 FILE
# $3 SHA hex string
# $4 SHA size in bits (256, 512, ...)
check_sha() {
  sha_size=$4
  if command -v shasum >/dev/null 2>&1; then
    echo "$3 *$2" | shasum -a "$sha_size" --status -c || {
      die "ERROR: Checksum mismatch for $2 (downloaded from $1): expected checksum $3 but got $(shasum --binary -a "$sha_size" "$2" | awk '{print $1}')"
    }
    return 0
  fi

  shaNsumCommand="sha${sha_size}sum"
  if command -v "$shaNsumCommand" >/dev/null 2>&1; then
    echo "$3 *$2" | $shaNsumCommand -w -c || {
      die "ERROR: Checksum mismatch for $2 (downloaded from $1): expected checksum $3 but got $($shaNsumCommand "$2" | awk '{print $1}')"
    }
    return 0
  fi

  echo "Both 'shasum' and 'sha${sha_size}sum' utilities are missing. Please install one of them"
  return 1
}

# ********** System detection **********

kernelName=$(uname -s)
arch=$(uname -m)
case "$kernelName" in
  Darwin* )
    simpleOs="macos"
    jbr_os="osx"
    default_amper_cache_dir="$HOME/Library/Caches/JetBrains/Amper"
    ;;
  Linux* )
    simpleOs="linux"
    jbr_os="linux"
    default_amper_cache_dir="$HOME/.cache/JetBrains/Amper"
    # If linux runs in 32-bit mode, we want the "fake" 32-bit architecture, not the real hardware,
    # because in this mode linux cannot run 64-bit binaries.
    # shellcheck disable=SC2046
    arch=$(linux$(getconf LONG_BIT) uname -m)
    ;;
  CYGWIN* | MSYS* | MINGW* )
    simpleOs="windows"
    jbr_os="windows"
    if command -v cygpath >/dev/null 2>&1; then
      default_amper_cache_dir=$(cygpath -u "$LOCALAPPDATA\JetBrains\Amper")
    else
      die "The 'cypath' command is not available, but Amper needs it. Use amper.bat instead, or try a Cygwin or MSYS environment."
    fi
    ;;
  *)
    die "Unsupported platform $kernelName"
    ;;
esac

amper_cache_dir="${AMPER_BOOTSTRAP_CACHE_DIR:-$default_amper_cache_dir}"

# ********** Provision Amper distribution **********

amper_url="$AMPER_DOWNLOAD_ROOT/org/jetbrains/amper/amper-cli/$amper_version/amper-cli-$amper_version-dist.tgz"
amper_target_dir="$amper_cache_dir/amper-cli-$amper_version"
download_and_extract "Amper distribution v$amper_version" "$amper_url" "$amper_sha256" 256 "$amper_cache_dir" "$amper_target_dir" "true"

# ********** Provision JRE for Amper **********

if [ "x${AMPER_JAVA_HOME:-}" = "x" ]; then
  case $arch in
    x86_64 | x64)    jbr_arch="x64" ;;
    aarch64 | arm64) jbr_arch="aarch64" ;;
    *) die "Unsupported architecture $arch" ;;
  esac

  # Auto-updated from syncVersions.main.kts, do not modify directly here
  jbr_version=21.0.8
  jbr_build=b1038.68

  # URL for JBR (vanilla) - see https://github.com/JetBrains/JetBrainsRuntime/releases
  jbr_url="$AMPER_JRE_DOWNLOAD_ROOT/cache-redirector.jetbrains.com/intellij-jbr/jbr-$jbr_version-$jbr_os-$jbr_arch-$jbr_build.tar.gz"
  jbr_target_dir="$amper_cache_dir/jbr-$jbr_version-$jbr_os-$jbr_arch-$jbr_build"

  platform="$jbr_os $jbr_arch"
  case $platform in
    "osx x64")         jbr_sha512=40b57086c22a8feaea30b541a5e45b4c5ceb6ac2df11a7caae81f272e713be401a0a0e0241db271041e995805b68f2684f0deccc3a044a4b7d4c7562d74e9839 ;;
    "osx aarch64")     jbr_sha512=e1212681d12d7b75a6fec39180d54b94d5cd63949c433f7552821c90783b0388f3a27eda712a6339fd2c21a25cc0b8a0846c5808ba7901e97e5c8a543dc6a55f ;;
    "linux x64")       jbr_sha512=796337b4ff95e91fa0812d62d68f6952fcb19e2f62174756d33ebcbbade08137bd92169a96688f02f4131831baa0132210a4289d8e216757cb895f44ea9b2cd7 ;;
    "linux aarch64")   jbr_sha512=cb047bf912395dd843c306e90ce4b4600c88f9bcb7742341d809d67d762bbb4e4edc936bc2be2589f7e5b050ad0513ba1a8deb6e0b481009d31e14aab9c2fe3b ;;
    "windows x64")     jbr_sha512=b59b6c9dd5194c93c3b8512e788fea08cef97e6c1b9108591f72ecdddc6fdbd95999db1e44b382cb5c74202b5ae016da5aa1c21883cefe50c23f8d2d0f3f7434 ;;
    "windows aarch64") jbr_sha512=fb784bf1c0842ff788479e05d65e7d6e7c92545d006a753a158e3f00d17a52fe0f8e4b0aa6081fabe0443219682ec03f726daacd7bf2ea28ddd31bd3ae7b7f22 ;;
    *) die "Unsupported platform $platform" ;;
  esac

  download_and_extract "JetBrains Runtime v$jbr_version$jbr_build" "$jbr_url" "$jbr_sha512" 512 "$amper_cache_dir" "$jbr_target_dir" "false"

  AMPER_JAVA_HOME=
  for d in "$jbr_target_dir" "$jbr_target_dir"/* "$jbr_target_dir"/Contents/Home "$jbr_target_dir"/*/Contents/Home; do
    if [ -e "$d/bin/java" ]; then
      AMPER_JAVA_HOME="$d"
    fi
  done

  if [ "x${AMPER_JAVA_HOME:-}" = "x" ]; then
    die "Unable to find bin/java under $jbr_target_dir"
  fi
fi

java_exe="$AMPER_JAVA_HOME/bin/java"
if [ '!' -x "$java_exe" ]; then
  die "Unable to find bin/java executable at $java_exe"
fi

# ********** Script path detection **********

# We might need to resolve symbolic links here
wrapper_path=$(realpath "$0")

# ********** Launch Amper **********

# In this section we construct the command line by prepending arguments from biggest to lowest precedence:
#   1. basic main class and classpath
#   2. user JVM args (AMPER_JAVA_OPTIONS)
#   3. default JVM args (prepended last, which means they appear first, so they are overridden by user args)

# 1. Prepend basic launch arguments
if [ "$simpleOs" = "windows" ]; then
  libDirSlash="$(cygpath -w "$amper_target_dir")\lib\\"
else
  libDirSlash="$amper_target_dir/lib/"
fi

set -- -cp "$libDirSlash*" "-javaagent:${libDirSlash}kotlinx-coroutines-debug-@COROUTINES_DEBUG_VERSION@.jar" org.jetbrains.amper.cli.MainKt "$@"

# 2. Prepend user JVM args from AMPER_JAVA_OPTS
#
# We use "xargs" to parse quoted JVM args from inside AMPER_JAVA_OPTS.
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
#   set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
if [ -n "${AMPER_JAVA_OPTIONS:-}" ]; then
  eval "set -- $(
    printf '%s\n' "$AMPER_JAVA_OPTIONS" |
    xargs -n1 |
    sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
    tr '\n' ' '
  )" '"$@"'
fi

# 3. Prepend default JVM args
set -- \
    "-ea" \
    "-XX:+EnableDynamicAgentLoading" \
    "-Damper.wrapper.dist.sha256=$amper_sha256" \
    "-Damper.dist.path=$amper_target_dir" \
    "-Damper.wrapper.path=$wrapper_path" \
    "$@"

# Then we can launch with the overridden $@ arguments
exec "$java_exe" "$@"
