#!/bin/sh # # Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. # # Possible environment variables: # KOTLIN_CLI_DOWNLOAD_ROOT Maven repository to download the Kotlin CLI dist from. # default: https://packages.jetbrains.team/maven/p/amper/amper # KOTLIN_CLI_JRE_DOWNLOAD_ROOT Url prefix to download the JRE to run the Kotlin CLI # default: https:/ # KOTLIN_CLI_BOOTSTRAP_CACHE_DIR Cache directory to store the extracted JRE and Kotlin CLI distribution # KOTLIN_CLI_JAVA_HOME JRE to run the Kotlin CLI itself (optional, does not affect compilation) # KOTLIN_CLI_JAVA_OPTIONS JVM options to pass to the JVM running the Kotlin CLI (does not affect the user's application) # KOTLIN_CLI_NO_WELCOME_BANNER Disables the first-run welcome message if set to a non-empty value set -e -u # The version of the Kotlin Toolchain (and CLI) distribution to provision and use kotlin_cli_version=0.13.0-dev-4444 # Establish chain of trust from here by specifying exact checksum of Kotlin Toolchain (and CLI) distribution to be run kotlin_cli_sha256=13c2448146ac6dba0fa3a4e5d367dab73d8cfd7400bab348bebe905c165adecf KOTLIN_CLI_DOWNLOAD_ROOT="${KOTLIN_CLI_DOWNLOAD_ROOT:-https://packages.jetbrains.team/maven/p/amper/amper}" die() { echo >&2 echo "$@" >&2 echo >&2 exit 1 } # 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 # discard the output as sha*sum may print redundant warnings in some versions echo "$3 *$2" | $shaNsumCommand -w -c >/dev/null 2>&1 || { 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 } 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) # We use `kill -0` instead of `ps -p` as the first one is more portable if [ -n "$lock_owner" ] && kill -0 "$lock_owner" >/dev/null; then echo "Another Kotlin CLI 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 Kotlin CLI 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" ] && [ -z "${KOTLIN_CLI_NO_WELCOME_BANNER:-}" ]; then echo cat </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 one of them is required 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', which is required 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', which is required 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 } # ********** Project-local version detection ********** # 1. Search upwards for an executable `amper` file and/or `project.yaml` # Sets wrapper_script to the found wrapper path, or empty string if not found. find_project_context() { wrapper_script="" this_script="$(realpath "$0")" project_dir=$(pwd) while [ "$project_dir" != "/" ] && [ -n "$project_dir" ]; do wrapper_candidate="$project_dir/kotlin" if [ "$this_script" = "$wrapper_candidate" ]; then # Found itself (local wrapper case), no need to update any version or search further. return 1 fi if [ -f "$wrapper_candidate" ] && [ -x "$wrapper_candidate" ]; then # Found the wrapper — check that a project context exists alongside it if [ -f "$project_dir/project.yaml" ] || [ -f "$project_dir/module.yaml" ]; then wrapper_script="$wrapper_candidate" return 0 else echo "WARNING: Found wrapper script '$wrapper_candidate', but no project.yaml or module.yaml near it. Skipping." >&2 # Continue the search fi elif [ -f "$project_dir/project.yaml" ]; then # Found project.yaml but no executable wrapper alongside it echo "WARNING: Found a project.yaml in '$project_dir', but the wrapper script is missing; using Kotlin Toolchain v$kotlin_cli_version." >&2 return 1 fi project_dir=$(dirname "$project_dir") done # Do not check root '/' - it's an unlikely candidate for a project return 1 } parse_project_context() { # Parse kotlin_cli_version and kotlin_cli_sha256 from "$wrapper_script" without executing it. parsed_kotlin_cli_version=$( sed -n 's/^kotlin_cli_version=\([A-Za-z0-9._+-]\{1,\}\)[[:space:]]*$/\1/p' "$wrapper_script" \ | head -n 1 ) parsed_kotlin_cli_sha256=$( sed -n 's/^kotlin_cli_sha256=\([0-9a-fA-F]\{64\}\)[[:space:]]*$/\1/p' "$wrapper_script" \ | head -n 1 ) if [ -z "$parsed_kotlin_cli_version" ]; then echo "ERROR: Suspicious local wrapper script: failed to detect the distribution version in '$wrapper_script'" >&2 return 1 fi if [ -z "$parsed_kotlin_cli_sha256" ]; then echo "ERROR: Suspicious local wrapper script: failed to detect the distribution checksum in '$wrapper_script'" >&2 return 1 fi # overwrite builtin values and proceed kotlin_cli_version=$parsed_kotlin_cli_version kotlin_cli_sha256=$parsed_kotlin_cli_sha256 return 0 } if [ -z "${KOTLIN_CLI_WRAPPER_ALWAYS_USE_INTRINSIC_VERSION:-}" ]; then find_project_context && parse_project_context fi # ********** System detection ********** kernelName=$(uname -s) case "$kernelName" in Darwin* ) default_kotlin_cli_cache_dir="$HOME/Library/Caches/JetBrains/Kotlin/cli" ;; Linux* ) default_kotlin_cli_cache_dir="$HOME/.cache/JetBrains/Kotlin/cli" ;; CYGWIN* | MSYS* | MINGW* ) if command -v cygpath >/dev/null 2>&1; then default_kotlin_cli_cache_dir=$(cygpath -u "$LOCALAPPDATA\JetBrains\Kotlin\cli") else die "The 'cypath' command is not available, but the Kotlin CLI needs it. Use kotlin.bat instead, or try a Cygwin or MSYS environment." fi ;; *) die "Unsupported platform $kernelName" ;; esac kotlin_cli_cache_dir="${KOTLIN_CLI_BOOTSTRAP_CACHE_DIR:-$default_kotlin_cli_cache_dir}" # ********** Provision the Kotlin Toolchain distribution ********** kotlin_cli_url="$KOTLIN_CLI_DOWNLOAD_ROOT/org/jetbrains/kotlin/kotlin-cli/$kotlin_cli_version/kotlin-cli-$kotlin_cli_version-dist.tgz" kotlin_cli_target_dir="$kotlin_cli_cache_dir/kotlin-cli-$kotlin_cli_version" download_and_extract "Kotlin Toolchain distribution v$kotlin_cli_version" "$kotlin_cli_url" "$kotlin_cli_sha256" 256 "$kotlin_cli_cache_dir" "$kotlin_cli_target_dir" "true" # ********** Launch the Kotlin CLI ********** launcher_script="$kotlin_cli_target_dir/bin/launcher.sh" KOTLIN_CLI_WRAPPER_PATH="$(realpath "$0")" \ exec /bin/sh "$launcher_script" "$@"