Files
sbt/sbt
T
BitToby be305eb3a5 fix: Use sbt script in BSP config instead of hardcoded Java path (#8920)
**Problem**

sbt bspConfig writes the absolute path of the current Java binary into .bsp/sbt.json. When the user switches Java versions (via sdkman, cs java, etc.) or removes that JDK, the IDE fails to start the sbt BSP server because the hardcoded path is stale or gone.

**Solution**

When an sbt launcher script is available (via `sbt.script` system property or PATH lookup), generate:

"argv": ["/path/to/sbt", "bsp"]
2026-03-19 20:57:53 -04:00

1035 lines
34 KiB
Bash
Executable File

#!/usr/bin/env bash
set +e
declare builtin_sbt_version="1.12.0"
declare -a residual_args
declare -a java_args
declare -a scalac_args
declare -a sbt_commands
declare -a sbt_options
declare -a print_help
declare -a print_version
declare -a print_sbt_version
declare -a print_sbt_script_version
declare -a shutdownall
declare -a original_args
declare java_cmd=java
declare java_version
declare init_sbt_version=_to_be_replaced
declare sbt_default_mem=1024
declare -r default_sbt_opts=""
declare -r default_java_opts="-Dfile.encoding=UTF-8"
declare sbt_verbose=
declare sbt_debug=
declare build_props_sbt_version=
declare use_sbtn=
declare use_jvm_client=
declare no_server=
declare sbtn_command="$SBTN_CMD"
declare sbtn_version="1.12.5"
declare use_colors=1
declare is_this_dir_sbt=""
declare hide_jdk_warnings=1
### ------------------------------- ###
### Helper methods for BASH scripts ###
### ------------------------------- ###
# Bash reimplementation of realpath to return the absolute path
realpathish () {
(
TARGET_FILE="$1"
FIX_CYGPATH="$2"
cd "$(dirname "$TARGET_FILE")"
TARGET_FILE=$(basename "$TARGET_FILE")
COUNT=0
while [ -L "$TARGET_FILE" -a $COUNT -lt 100 ]
do
TARGET_FILE=$(readlink "$TARGET_FILE")
cd "$(dirname "$TARGET_FILE")"
TARGET_FILE=$(basename "$TARGET_FILE")
COUNT=$(($COUNT + 1))
done
TARGET_DIR="$(pwd -P)"
if [ "$TARGET_DIR" == "/" ]; then
TARGET_FILE="/$TARGET_FILE"
else
TARGET_FILE="$TARGET_DIR/$TARGET_FILE"
fi
# make sure we grab the actual windows path, instead of cygwin's path.
if [[ "x$FIX_CYGPATH" != "x" ]]; then
echo "$(cygwinpath "$TARGET_FILE")"
else
echo "$TARGET_FILE"
fi
)
}
# Uses uname to detect if we're in the odd cygwin environment.
is_cygwin() {
local os=$(uname -s)
case "$os" in
CYGWIN*) return 0 ;;
MINGW*) return 0 ;;
MSYS*) return 0 ;;
*) return 1 ;;
esac
}
# TODO - Use nicer bash-isms here.
CYGWIN_FLAG=$(if is_cygwin; then echo true; else echo false; fi)
# This can fix cygwin style /cygdrive paths so we get the
# windows style paths.
cygwinpath() {
local file="$1"
if [[ "$CYGWIN_FLAG" == "true" ]]; then
echo $(cygpath -w $file)
else
echo $file
fi
}
# Trim leading and trailing spaces from a string.
# Echos the new trimmed string.
trimString() {
local inputStr="$*"
local modStr="${inputStr#"${inputStr%%[![:space:]]*}"}"
modStr="${modStr%"${modStr##*[![:space:]]}"}"
echo "$modStr"
}
declare -r sbt_bin_dir="$(dirname "$(realpathish "$0")")"
declare -r sbt_home="$(dirname "$sbt_bin_dir")"
echoerr () {
echo 1>&2 "$@"
}
RED='\033[0;31m'
NC='\033[0m' # No Color
echoerr_error () {
if [[ $use_colors == "1" ]]; then
echoerr -e "[${RED}error${NC}] $@"
else
echoerr "[error] $@"
fi #"
}
vlog () {
[[ $sbt_verbose || $sbt_debug ]] && echoerr "$@"
}
dlog () {
[[ $sbt_debug ]] && echoerr "$@"
}
jar_file () {
echo "$(cygwinpath "${sbt_home}/bin/sbt-launch.jar")"
}
jar_url () {
local repo_base="$SBT_LAUNCH_REPO"
if [[ $repo_base == "" ]]; then
repo_base="https://repo1.maven.org/maven2"
fi
echo "$repo_base/org/scala-sbt/sbt-launch/$1/sbt-launch-$1.jar"
}
download_url () {
local url="$1"
local jar="$2"
mkdir -p $(dirname "$jar") && {
if command -v curl > /dev/null; then
curl --silent -L "$url" --output "$jar"
elif command -v wget > /dev/null; then
wget --quiet -O "$jar" "$url"
else
echoerr "failed to download $url: Neither curl nor wget is available"
exit 2
fi
} && [[ -f "$jar" ]]
}
acquire_sbt_jar () {
local launcher_sv="$1"
if [[ "$launcher_sv" == "" ]]; then
if [[ "$init_sbt_version" != "_to_be_replaced" ]]; then
launcher_sv="$init_sbt_version"
else
launcher_sv="$builtin_sbt_version"
fi
fi
local user_home && user_home=$(findProperty user.home)
download_jar="${user_home:-$HOME}/.cache/sbt/boot/sbt-launch/$launcher_sv/sbt-launch-$launcher_sv.jar"
if [[ -f "$download_jar" ]]; then
sbt_jar="$download_jar"
else
sbt_url=$(jar_url "$launcher_sv")
dlog "downloading sbt launcher $launcher_sv"
download_url "$sbt_url" "${download_jar}.temp"
download_url "${sbt_url}.sha1" "${download_jar}.sha1"
if command -v shasum > /dev/null; then
if echo "$(cat "${download_jar}.sha1") ${download_jar}.temp" | shasum -c - > /dev/null; then
mv "${download_jar}.temp" "${download_jar}"
else
echoerr "failed to download launcher jar: $sbt_url (shasum mismatch)"
exit 2
fi
else
mv "${download_jar}.temp" "${download_jar}"
fi
if [[ -f "$download_jar" ]]; then
sbt_jar="$download_jar"
else
echoerr "failed to download launcher jar: $sbt_url"
exit 2
fi
fi
}
acquire_sbtn () {
local sbtn_v="$1"
local user_home && user_home=$(findProperty user.home)
local p="${user_home:-$HOME}/.cache/sbt/boot/sbtn/$sbtn_v"
local target="$p/sbtn"
local archive_target=
local url=
local arch="x86_64"
if [[ "$OSTYPE" == "linux"* ]]; then
arch=$(uname -m)
if [[ "$arch" == "aarch64" ]] || [[ "$arch" == "x86_64" ]]; then
archive_target="$p/sbtn-${arch}-pc-linux-${sbtn_v}.tar.gz"
url="https://github.com/sbt/sbtn-dist/releases/download/v${sbtn_v}/sbtn-${arch}-pc-linux-${sbtn_v}.tar.gz"
else
echoerr_error "sbtn is not supported on $arch"
exit 2
fi
elif [[ "$OSTYPE" == "darwin"* ]]; then
arch="universal"
archive_target="$p/sbtn-universal-apple-darwin-${sbtn_v}.tar.gz"
url="https://github.com/sbt/sbtn-dist/releases/download/v${sbtn_v}/sbtn-universal-apple-darwin-${sbtn_v}.tar.gz"
elif [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then
target="$p/sbtn.exe"
archive_target="$p/sbtn-x86_64-pc-win32-${sbtn_v}.zip"
url="https://github.com/sbt/sbtn-dist/releases/download/v${sbtn_v}/sbtn-x86_64-pc-win32-${sbtn_v}.zip"
else
echoerr_error "sbtn is not supported on $OSTYPE"
exit 2
fi
if [[ -f "$target" ]]; then
sbtn_command="$target"
else
dlog "downloading sbtn ${sbtn_v} for ${arch}"
download_url "$url" "$archive_target"
if [[ "$OSTYPE" == "linux-gnu"* ]] || [[ "$OSTYPE" == "darwin"* ]]; then
tar zxf "$archive_target" --directory "$p"
else
unzip "$archive_target" -d "$p"
fi
sbtn_command="$target"
fi
}
# execRunner should be called only once to give up control to java
execRunner () {
# print the arguments one to a line, quoting any containing spaces
[[ $sbt_verbose || $sbt_debug ]] && echo "# Executing command line:" && {
for arg; do
if printf "%s\n" "$arg" | grep -q ' '; then
printf "\"%s\"\n" "$arg"
else
printf "%s\n" "$arg"
fi
done
echo ""
}
if [[ "$CYGWIN_FLAG" == "true" ]]; then
# In cygwin we loose the ability to re-hook stty if exec is used
# https://github.com/sbt/sbt-launcher-package/issues/53
"$@"
else
exec "$@"
fi
}
addJava () {
dlog "[addJava] arg = '$1'"
java_args=( "${java_args[@]}" "$1" )
}
addSbt () {
dlog "[addSbt] arg = '$1'"
sbt_commands=( "${sbt_commands[@]}" "$1" )
}
addResidual () {
dlog "[residual] arg = '$1'"
residual_args=( "${residual_args[@]}" "$1" )
}
addDebugger () {
addJava "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$1"
}
addMemory () {
dlog "[addMemory] arg = '$1'"
# evict memory related options
local xs=("${java_args[@]}")
java_args=()
for i in "${xs[@]}"; do
if ! [[ "${i}" == *-Xmx* ]] && ! [[ "${i}" == *-Xms* ]] && ! [[ "${i}" == *-Xss* ]] && ! [[ "${i}" == *-XX:MaxPermSize* ]] && ! [[ "${i}" == *-XX:MaxMetaspaceSize* ]] && ! [[ "${i}" == *-XX:ReservedCodeCacheSize* ]]; then
java_args+=("${i}")
fi
done
local ys=("${sbt_options[@]}")
sbt_options=()
for i in "${ys[@]}"; do
if ! [[ "${i}" == *-Xmx* ]] && ! [[ "${i}" == *-Xms* ]] && ! [[ "${i}" == *-Xss* ]] && ! [[ "${i}" == *-XX:MaxPermSize* ]] && ! [[ "${i}" == *-XX:MaxMetaspaceSize* ]] && ! [[ "${i}" == *-XX:ReservedCodeCacheSize* ]]; then
sbt_options+=("${i}")
fi
done
# a ham-fisted attempt to move some memory settings in concert
local mem=$1
local codecache=$(( $mem / 8 ))
(( $codecache > 128 )) || codecache=128
(( $codecache < 512 )) || codecache=512
local class_metadata_size=$(( $codecache * 2 ))
if [[ -z $java_version ]]; then
java_version=$(jdk_version)
fi
addJava "-Xms${mem}m"
addJava "-Xmx${mem}m"
addJava "-Xss4M"
addJava "-XX:ReservedCodeCacheSize=${codecache}m"
(( $java_version >= 8 )) || addJava "-XX:MaxPermSize=${class_metadata_size}m"
}
addDefaultMemory() {
# if we detect any of these settings in ${JAVA_OPTS} or ${JAVA_TOOL_OPTIONS} or ${JDK_JAVA_OPTIONS} we need to NOT output our settings.
# The reason is the Xms/Xmx, if they don't line up, cause errors.
if [[ "${java_args[@]}" == *-Xmx* ]] || \
[[ "${java_args[@]}" == *-Xms* ]] || \
[[ "${java_args[@]}" == *-Xss* ]] || \
[[ "${java_args[@]}" == *-XX:+UseCGroupMemoryLimitForHeap* ]] || \
[[ "${java_args[@]}" == *-XX:MaxRAM* ]] || \
[[ "${java_args[@]}" == *-XX:InitialRAMPercentage* ]] || \
[[ "${java_args[@]}" == *-XX:MaxRAMPercentage* ]] || \
[[ "${java_args[@]}" == *-XX:MinRAMPercentage* ]]; then
:
elif [[ "${JAVA_TOOL_OPTIONS}" == *-Xmx* ]] || \
[[ "${JAVA_TOOL_OPTIONS}" == *-Xms* ]] || \
[[ "${JAVA_TOOL_OPTIONS}" == *-Xss* ]] || \
[[ "${JAVA_TOOL_OPTIONS}" == *-XX:+UseCGroupMemoryLimitForHeap* ]] || \
[[ "${JAVA_TOOL_OPTIONS}" == *-XX:MaxRAM* ]] || \
[[ "${JAVA_TOOL_OPTIONS}" == *-XX:InitialRAMPercentage* ]] || \
[[ "${JAVA_TOOL_OPTIONS}" == *-XX:MaxRAMPercentage* ]] || \
[[ "${JAVA_TOOL_OPTIONS}" == *-XX:MinRAMPercentage* ]] ; then
:
elif [[ "${JDK_JAVA_OPTIONS}" == *-Xmx* ]] || \
[[ "${JDK_JAVA_OPTIONS}" == *-Xms* ]] || \
[[ "${JDK_JAVA_OPTIONS}" == *-Xss* ]] || \
[[ "${JDK_JAVA_OPTIONS}" == *-XX:+UseCGroupMemoryLimitForHeap* ]] || \
[[ "${JDK_JAVA_OPTIONS}" == *-XX:MaxRAM* ]] || \
[[ "${JDK_JAVA_OPTIONS}" == *-XX:InitialRAMPercentage* ]] || \
[[ "${JDK_JAVA_OPTIONS}" == *-XX:MaxRAMPercentage* ]] || \
[[ "${JDK_JAVA_OPTIONS}" == *-XX:MinRAMPercentage* ]] ; then
:
elif [[ "${sbt_options[@]}" == *-Xmx* ]] || \
[[ "${sbt_options[@]}" == *-Xms* ]] || \
[[ "${sbt_options[@]}" == *-Xss* ]] || \
[[ "${sbt_options[@]}" == *-XX:+UseCGroupMemoryLimitForHeap* ]] || \
[[ "${sbt_options[@]}" == *-XX:MaxRAM* ]] || \
[[ "${sbt_options[@]}" == *-XX:InitialRAMPercentage* ]] || \
[[ "${sbt_options[@]}" == *-XX:MaxRAMPercentage* ]] || \
[[ "${sbt_options[@]}" == *-XX:MinRAMPercentage* ]] ; then
:
else
addMemory $sbt_default_mem
fi
}
addSbtScriptProperty () {
if [[ "${java_args[@]}" == *-Dsbt.script=* ]]; then
:
else
sbt_script=$0
# Use // to replace all spaces with %20.
sbt_script=${sbt_script// /%20}
addJava "-Dsbt.script=$sbt_script"
fi
}
addJdkWorkaround () {
local is_25="$(expr $java_version "=" 25)"
if [[ "$hide_jdk_warnings" == "0" ]]; then
:
else
if [[ "$is_25" == "1" ]]; then
addJava "--sun-misc-unsafe-memory-access=allow"
addJava "--enable-native-access=ALL-UNNAMED"
fi
fi
}
require_arg () {
local type="$1"
local opt="$2"
local arg="$3"
if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then
echoerr "$opt requires <$type> argument"
exit 1
fi
}
is_function_defined() {
declare -f "$1" > /dev/null
}
# parses JDK version from the -version output line.
# 8 for 1.8.0_nn, 9 for 9-ea etc, and "no_java" for undetected
jdk_version() {
local result
local lines=$("$java_cmd" -Xms32M -Xmx32M -version 2>&1 | tr '\r' '\n')
local IFS=$'\n'
for line in $lines; do
if [[ (-z $result) && ($line = *"version \""*) ]]
then
local ver=$(echo $line | sed -e 's/.*version "\(.*\)"\(.*\)/\1/; 1q')
# on macOS sed doesn't support '?'
if [[ $ver = "1."* ]]
then
result=$(echo $ver | sed -e 's/1\.\([0-9]*\)\(.*\)/\1/; 1q')
else
result=$(echo $ver | sed -e 's/\([0-9]*\)\(.*\)/\1/; 1q')
fi
fi
done
if [[ -z $result ]]
then
result=no_java
fi
echo "$result"
}
# Find the first occurrence of the given property name and returns its value by looking at:
# - properties set by command-line options,
# - JAVA_OPTS environment variable,
# - SBT_OPTS environment variable,
# - _JAVA_OPTIONS environment variable and
# - JAVA_TOOL_OPTIONS environment variable
# - JDK_JAVA_OPTIONS environment variable
# in that order.
findProperty() {
local -a java_opts_array
local -a sbt_opts_array
local -a _java_options_array
local -a java_tool_options_array
local -a jdk_java_options_array
read -a java_opts_array <<< "$JAVA_OPTS"
read -a sbt_opts_array <<< "$SBT_OPTS"
read -a _java_options_array <<< "$_JAVA_OPTIONS"
read -a java_tool_options_array <<< "$JAVA_TOOL_OPTIONS"
read -a jdk_java_options_array <<< "$JDK_JAVA_OPTIONS"
local args_to_check=(
"${java_args[@]}"
"${java_opts_array[@]}"
"${sbt_opts_array[@]}"
"${_java_options_array[@]}"
"${java_tool_options_array[@]}"
"${jdk_java_options_array[@]}")
for opt in "${args_to_check[@]}"; do
if [[ "$opt" == -D$1=* ]]; then
echo "${opt#-D$1=}"
return
fi
done
}
# Extracts the preloaded directory from -Dsbt.preloaded, -Dsbt.global.base, SBT_CONFIG_HOME,
# XDG_CONFIG_HOME/sbt, or user.home/.sbt in that order.
getPreloaded() {
local preloaded && preloaded=$(findProperty sbt.preloaded)
[ "$preloaded" ] && echo "$preloaded" && return
local global_base && global_base=$(findProperty sbt.global.base)
[ "$global_base" ] && echo "$global_base/preloaded" && return
[ -n "${SBT_CONFIG_HOME}" ] && echo "${SBT_CONFIG_HOME}/preloaded" && return
[ -n "${XDG_CONFIG_HOME}" ] && echo "${XDG_CONFIG_HOME}/sbt/preloaded" && return
local user_home && user_home=$(findProperty user.home)
echo "${user_home:-$HOME}/.sbt/preloaded"
}
syncPreloaded() {
local source_preloaded="$sbt_home/lib/local-preloaded/"
local target_preloaded="$(getPreloaded)"
if [[ "$init_sbt_version" == "" ]]; then
# FIXME: better $init_sbt_version detection
init_sbt_version="$(ls -1 "$source_preloaded/org/scala-sbt/sbt/")"
fi
[[ -f "$target_preloaded/org/scala-sbt/sbt/$init_sbt_version/" ]] || {
# lib/local-preloaded exists (This is optional)
[[ -d "$source_preloaded" ]] && {
command -v rsync >/dev/null 2>&1 && {
mkdir -p "$target_preloaded"
rsync --recursive --links --perms --times --ignore-existing "$source_preloaded" "$target_preloaded" || true
}
}
}
}
# Detect that we have java installed.
checkJava() {
local required_version="$1"
# Now check to see if it's a good enough version
local good_enough="$(expr $java_version ">=" $required_version)"
if [[ "$java_version" == "" ]]; then
echoerr
echoerr "No Java Development Kit (JDK) installation was detected."
echoerr Go to https://adoptium.net/ etc and download.
echoerr
exit 1
elif [[ "$good_enough" != "1" ]]; then
echoerr
echoerr "The Java Development Kit (JDK) installation you have is not up to date."
echoerr $script_name requires at least version $required_version+, you have
echoerr version $java_version
echoerr
echoerr Go to https://adoptium.net/ etc and download
echoerr a valid JDK and install before running $script_name.
echo
exit 1
fi
}
# sbt 2.x requires JDK 17+
checkJava17ForSbt2() {
local sbtV="$build_props_sbt_version"
[[ "$sbtV" == "" ]] && sbtV="$init_sbt_version"
[[ "$sbtV" == "" ]] && return
local sbtMajor=$(echo "$sbtV" | sed 's/^\([0-9]*\).*/\1/')
if (( sbtMajor >= 2 )) && [[ "$java_version" != "no_java" ]] && (( java_version < 17 )); then
echoerr "[error] sbt 2.x requires JDK 17 or above, but you have JDK $java_version"
exit 1
fi
}
copyRt() {
local at_least_9="$(expr $java_version ">=" 9)"
if [[ "$at_least_9" == "1" ]]; then
# The grep for java9-rt-ext- matches the filename prefix printed in Export.java
java9_ext=$("$java_cmd" "${sbt_options[@]}" "${java_args[@]}" \
-jar "$sbt_jar" --rt-ext-dir | grep java9-rt-ext- | tr -d '\r')
java9_rt=$(echo "$java9_ext/rt.jar")
vlog "[copyRt] java9_rt = '$java9_rt'"
if [[ ! -f "$java9_rt" ]]; then
mkdir -p "$java9_ext"
"$java_cmd" \
"${sbt_options[@]}" \
"${java_args[@]}" \
-jar "$sbt_jar" \
--export-rt \
"${java9_rt}"
fi
addJava "-Dscala.ext.dirs=${java9_ext}"
fi
}
detect_working_directory() {
if [[ -f ./build.sbt || -f ./project/build.properties ]]; then
is_this_dir_sbt=1
fi
}
# Confirm a user's intent if the current directory does not look like an sbt
# top-level directory and neither the --allow-empty option nor the "new" command was given.
checkWorkingDirectory() {
if [[ ! -n "$allow_empty" ]]; then
[[ -n "$is_this_dir_sbt" || -n "$sbt_new" ]] || {
echoerr_error "Neither build.sbt nor a 'project' directory in the current directory: $(pwd)"
echoerr_error "run 'sbt new', touch build.sbt, or run 'sbt --allow-empty'."
echoerr_error ""
echoerr_error "To opt out of this check, create ${config_home}/sbtopts with:"
echoerr_error "--allow-empty"
exit 1
}
fi
}
run() {
# Copy preloaded repo to user's preloaded directory
syncPreloaded
# no jar? download it.
[[ -f "$sbt_jar" ]] || acquire_sbt_jar || {
exit 1
}
# TODO - java check should be configurable...
checkJava "8"
# Java 9 support
copyRt
# If we're in cygwin, we should use the windows config, and terminal hacks
if [[ "$CYGWIN_FLAG" == "true" ]]; then
stty -icanon min 1 -echo > /dev/null 2>&1
addJava "-Djline.terminal=jline.UnixTerminal"
addJava "-Dsbt.cygwin=true"
fi
detect_working_directory
if [[ $print_sbt_version ]]; then
execRunner "$java_cmd" -jar "$sbt_jar" "sbtVersion" | tail -1 | sed -e 's/\[info\]//g'
elif [[ $print_version ]]; then
if [[ -n "$is_this_dir_sbt" ]]; then
local project_sbt_version
if project_sbt_version="$(projectSbtVersion)"; then
echo "sbt version in this project: $project_sbt_version"
fi
fi
echo "sbt runner version: $init_sbt_version"
echoerr ""
echoerr "[info] sbt runner (sbt-the-shell-script) is a runner to run any declared version of sbt."
echoerr "[info] Actual version of the sbt is declared using project/build.properties for each build."
elif [[ $shutdownall ]]; then
local sbt_processes=( $(jps -v | grep sbt-launch | cut -f1 -d ' ') )
for procId in "${sbt_processes[@]}"; do
kill -9 $procId
done
echoerr "shutdown ${#sbt_processes[@]} sbt processes"
else
checkWorkingDirectory
# run sbt
execRunner "$java_cmd" \
"${java_args[@]}" \
"${sbt_options[@]}" \
"${java_tool_options[@]}" \
"${jdk_java_options[@]}" \
-jar "$sbt_jar" \
"${sbt_commands[@]}" \
"${residual_args[@]}"
fi
exit_code=$?
# Clean up the terminal from cygwin hacks.
if [[ "$CYGWIN_FLAG" == "true" ]]; then
stty icanon echo > /dev/null 2>&1
fi
exit $exit_code
}
declare -ra noshare_opts=(-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy)
declare -r sbt_opts_file=".sbtopts"
declare -r build_props_file="$(pwd)/project/build.properties"
declare -r etc_sbt_opts_file="/etc/sbt/sbtopts"
# this allows /etc/sbt/sbtopts location to be changed
declare machine_sbt_opts_file="${etc_sbt_opts_file}"
declare config_home="${XDG_CONFIG_HOME:-$HOME/.config}/sbt"
[[ -f "${config_home}/sbtopts" ]] && machine_sbt_opts_file="${config_home}/sbtopts"
[[ -f "$SBT_ETC_FILE" ]] && machine_sbt_opts_file="$SBT_ETC_FILE"
declare -r dist_sbt_opts_file="${sbt_home}/conf/sbtopts"
declare -r win_sbt_opts_file="${sbt_home}/conf/sbtconfig.txt"
declare sbt_jar="$(jar_file)"
usage() {
cat <<EOM
Usage: `basename "$0"` [options]
-h | --help print this message
-v | --verbose this runner is chattier
-V | --version print sbt version information
--numeric-version print the numeric sbt version (sbt sbtVersion)
--script-version print the version of sbt script
shutdownall shutdown all running sbt-launch processes
-d | --debug set sbt log level to debug
-debug-inc | --debug-inc
enable extra debugging for the incremental compiler
--no-colors disable ANSI color codes
--color=auto|always|true|false|never
enable or disable ANSI color codes (sbt 1.3 and above)
--supershell=auto|always|true|false|never
enable or disable supershell (sbt 1.3 and above)
--traces generate Trace Event report on shutdown (sbt 1.3 and above)
--client run native client
--jvm-client run JVM client
--timings display task timings report on shutdown
--allow-empty start sbt even if current directory contains no sbt project
--sbt-dir <path> path to global settings/plugins directory (default: \$XDG_CONFIG_HOME/sbt or ~/.config/sbt)
--sbt-boot <path> path to shared boot directory (default: ~/.sbt/boot in 0.11 series)
--sbt-cache <path> path to global cache directory (default: operating system specific)
--ivy <path> path to local Ivy repository (default: ~/.ivy2)
--mem <integer> set memory options (default: $sbt_default_mem)
--no-share use all local caches; no sharing
--no-global uses global caches, but does not use global ~/.sbt directory.
--jvm-debug <port> Turn on JVM debugging, open at the given port.
--batch disable interactive mode
# sbt version (default: from project/build.properties if present, else latest release)
--sbt-version <version> use the specified version of sbt
--sbt-jar <path> use the specified jar as the sbt launcher
--java-home <path> alternate JAVA_HOME
# jvm options and output control
JAVA_OPTS environment variable, if unset uses "$default_java_opts"
.jvmopts if this file exists in the current directory, its contents
are appended to JAVA_OPTS
SBT_OPTS environment variable, if unset uses "$default_sbt_opts"
.sbtopts if this file exists in the current directory, its contents
are prepended to the runner args
/etc/sbt/sbtopts if this file exists, it is prepended to the runner args
-Dkey=val pass -Dkey=val directly to the java runtime
-J-X pass option -X directly to the java runtime
(-J is stripped)
In the case of duplicated or conflicting options, the order above
shows precedence: JAVA_OPTS lowest, command line options highest.
Getting started with sbt:
- To create a new project run: sbt init
- Once in the sbt shell, type:
help
help <command>
to see available commands and detailed help.
EOM
}
process_my_args () {
while [[ $# -gt 0 ]]; do
case "$1" in
-batch|--batch) exec </dev/null && shift ;; #>
-allow-empty|--allow-empty|-sbt-create|--sbt-create) allow_empty=true && shift ;;
new|init) sbt_new=true && addResidual "$1" && shift ;;
*) addResidual "$1" && shift ;;
esac
done
# Now, ensure sbt version is used.
[[ "${sbt_version}XXX" != "XXX" ]] && addJava "-Dsbt.version=$sbt_version"
}
## map over argument array. this is used to process both command line arguments and SBT_OPTS
map_args () {
local options=()
local commands=()
while [[ $# -gt 0 ]]; do
case "$1" in
-no-colors|--no-colors) options=( "${options[@]}" "-Dsbt.log.noformat=true" ) && shift ;;
-timings|--timings) options=( "${options[@]}" "-Dsbt.task.timings=true" "-Dsbt.task.timings.on.shutdown=true" ) && shift ;;
-traces|--traces) options=( "${options[@]}" "-Dsbt.traces=true" ) && shift ;;
--supershell=*) options=( "${options[@]}" "-Dsbt.supershell=${1:13}" ) && shift ;;
-supershell=*) options=( "${options[@]}" "-Dsbt.supershell=${1:12}" ) && shift ;;
-no-server|--no-server) options=( "${options[@]}" "-Dsbt.io.virtual=false" "-Dsbt.server.autostart=false" ) && shift ;;
--autostart=*) options=( "${options[@]}" "-Dsbt.server.autostart=${1:13}" ) && shift ;;
-autostart=*) options=( "${options[@]}" "-Dsbt.server.autostart=${1:12}" ) && shift ;;
--color=*) options=( "${options[@]}" "-Dsbt.color=${1:8}" ) && shift ;;
-color=*) options=( "${options[@]}" "-Dsbt.color=${1:7}" ) && shift ;;
-no-share|--no-share) options=( "${options[@]}" "${noshare_opts[@]}" ) && shift ;;
-no-global|--no-global) options=( "${options[@]}" "-Dsbt.global.base=$(pwd)/project/.sbtboot" ) && shift ;;
-ivy|--ivy) require_arg path "$1" "$2" && options=( "${options[@]}" "-Dsbt.ivy.home=$2" ) && shift 2 ;;
-sbt-boot|--sbt-boot) require_arg path "$1" "$2" && options=( "${options[@]}" "-Dsbt.boot.directory=$2" ) && shift 2 ;;
-sbt-dir|--sbt-dir) require_arg path "$1" "$2" && options=( "${options[@]}" "-Dsbt.global.base=$2" ) && shift 2 ;;
-debug|--debug) commands=( "${commands[@]}" "-debug" ) && shift ;;
-debug-inc|--debug-inc) options=( "${options[@]}" "-Dxsbt.inc.debug=true" ) && shift ;;
*) options=( "${options[@]}" "$1" ) && shift ;;
esac
done
declare -p options
declare -p commands
}
# Parse a line into words, respecting single and double quotes.
# This is used for .sbtopts so that options like:
# -sbt-dir "/Users/a' dog"
# are split into two arguments: -sbt-dir and /Users/a' dog
parseLineIntoWords() {
local line="$1"
local word=""
local i=0
local len=${#line}
local in_dq=0
local in_sq=0
while (( i < len )); do
local c="${line:$i:1}"
if (( in_dq )); then
if [[ "$c" == '"' ]]; then
in_dq=0
else
word+="$c"
fi
elif (( in_sq )); then
if [[ "$c" == "'" ]]; then
in_sq=0
else
word+="$c"
fi
else
case "$c" in
'"') in_dq=1 ;;
"'") in_sq=1 ;;
' '|$'\t')
[[ -n "$word" ]] && printf '%s\n' "$word"
word=""
;;
*) word+="$c" ;;
esac
fi
((i++))
done
[[ -n "$word" ]] && printf '%s\n' "$word"
}
process_args () {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|-help|--help) print_help=1 && shift ;;
-v|-verbose|--verbose) sbt_verbose=1 && shift ;;
-V|-version|--version) print_version=1 && shift ;;
--numeric-version) print_sbt_version=1 && shift ;;
--script-version) print_sbt_script_version=1 && shift ;;
shutdownall) shutdownall=1 && shift ;;
-d|-debug|--debug) sbt_debug=1 && addSbt "-debug" && shift ;;
-client|--client) use_sbtn=1 && shift ;;
--server) use_sbtn=0 && shift ;;
--jvm-client) use_sbtn=0 && use_jvm_client=1 && addSbt "--client" && shift ;;
--no-hide-jdk-warnings) hide_jdk_warnings=0 && shift ;;
-mem|--mem) require_arg integer "$1" "$2" && addMemory "$2" && shift 2 ;;
-jvm-debug|--jvm-debug) require_arg port "$1" "$2" && addDebugger $2 && shift 2 ;;
-batch|--batch) exec </dev/null && shift ;;
-sbt-jar|--sbt-jar) require_arg path "$1" "$2" && sbt_jar="$2" && shift 2 ;;
-sbt-cache|--sbt-cache) require_arg path "$1" "$2" &&
sbt_cache="$2" &&
addJava "-Dsbt.global.localcache=$2" &&
shift 2 ;;
-sbt-version|--sbt-version) require_arg version "$1" "$2" && addJava "-Dsbt.version=$2" && shift 2 ;;
-java-home|--java-home) require_arg path "$1" "$2" &&
java_cmd="$2/bin/java" &&
export JAVA_HOME="$2" &&
export JDK_HOME="$2" &&
export PATH="$2/bin:$PATH" &&
shift 2 ;;
-Dsbt.color=never|-Dsbt.log.noformat=true) addJava "$1" && use_colors=0 && shift ;;
"-D*"|-D*) addJava "$1" && shift ;;
-J*) addJava "${1:2}" && shift ;;
bsp) use_sbtn=0 && addResidual "-bsp" && shift ;;
*) addResidual "$1" && shift ;;
esac
done
is_function_defined process_my_args && {
myargs=("${residual_args[@]}")
residual_args=()
process_my_args "${myargs[@]}"
}
}
loadConfigFile() {
# Make sure the last line is read even if it doesn't have a terminating \n
# Output lines literally without shell expansion to handle special characters safely
cat "$1" | sed $'/^\#/d;s/[[:space:]]\{1,\}#.*//;s/\r$//' | while read -r line || [[ -n "$line" ]]; do
# Use printf with properly quoted variable to prevent shell expansion
# This safely handles special characters like |, *, &, etc.
printf '%s\n' "$line"
done
}
# Append tokens from an sbtopts-style file into the global sbt_file_opts array.
# Each non-empty, non-comment line is split using parseLineIntoWords so that
# quoted values with spaces (and embedded quotes) are preserved as single arguments.
appendSbtoptsFromFile() {
local file="$1"
[[ ! -f "$file" ]] && return
while IFS= read -r line || [[ -n "$line" ]]; do
line=$(printf '%s' "$line" | sed $'/^\#/d;s/[[:space:]]\{1,\}#.*//;s/\r$//')
[[ -z "$line" ]] && continue
while IFS= read -r token; do
[[ -n "$token" ]] && sbt_file_opts+=("$token")
done < <(parseLineIntoWords "$line")
done < "$file"
}
loadPropFile() {
# trim key and value so as to be more forgiving with spaces around the '=':
k=$(trimString $k)
v=$(trimString $v)
while IFS='=' read -r k v; do
if [[ "$k" == "sbt.version" ]]; then
build_props_sbt_version="$v"
fi
done <<< "$(cat "$1" | sed $'/^\#/d;s/\r$//')"
}
projectSbtVersion() {
local version
version="$(trimString "$build_props_sbt_version")"
if [[ -n "$version" ]]; then
echo "$version"
return 0
fi
return 1
}
detectNativeClient() {
if [[ "$sbtn_command" != "" ]]; then
:
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
arch=$(uname -m)
[[ -f "${sbt_bin_dir}/sbtn-${arch}-pc-linux" ]] && sbtn_command="${sbt_bin_dir}/sbtn-${arch}-pc-linux"
elif [[ "$OSTYPE" == "darwin"* ]]; then
[[ -f "${sbt_bin_dir}/sbtn-universal-apple-darwin" ]] && sbtn_command="${sbt_bin_dir}/sbtn-universal-apple-darwin"
elif [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then
[[ -f "${sbt_bin_dir}/sbtn-x86_64-pc-win32.exe" ]] && sbtn_command="${sbt_bin_dir}/sbtn-x86_64-pc-win32.exe"
elif [[ "$OSTYPE" == "freebsd"* ]]; then
:
else
:
fi
}
# Run native client if build.properties points to 1.4+ and has SBT_NATIVE_CLIENT
isRunNativeClient() {
# sbt new/init should not use native client as it needs to run outside a project
if [[ "$sbt_new" == "true" ]]; then
echo "false"
return
fi
sbtV="$build_props_sbt_version"
[[ "$sbtV" == "" ]] && sbtV="$init_sbt_version"
[[ "$sbtV" == "" ]] && sbtV="0.0.0"
sbtBinaryV_1=$(echo "$sbtV" | sed 's/^\([0-9]*\)\.\([0-9]*\).*$/\1/')
sbtBinaryV_2=$(echo "$sbtV" | sed 's/^\([0-9]*\)\.\([0-9]*\).*$/\2/')
# Default to true for sbt 2.x
if (( $sbtBinaryV_1 >= 2 )); then
if [[ "$use_sbtn" == "0" ]]; then
echo "false"
else
echo "true"
fi
elif ( (( $sbtBinaryV_1 >= 1 )) && (( $sbtBinaryV_2 >= 4 )) ); then
if [[ "$use_sbtn" == "1" ]]; then
echo "true"
else
echo "false"
fi
else
echo "false"
fi
}
runNativeClient() {
vlog "[debug] running native client"
detectNativeClient
[[ -f "$sbtn_command" ]] || acquire_sbtn "$sbtn_version" || {
exit 1
}
for i in "${!original_args[@]}"; do
if [[ "${original_args[i]}" = "--client" ]]; then
unset 'original_args[i]'
fi
done
if [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then
sbt_script="$0.bat"
else
sbt_script="$0"
fi
sbt_script=${sbt_script/ /%20}
execRunner "$sbtn_command" "--sbt-script=$sbt_script" "${original_args[@]}"
}
original_args=("$@")
sbt_file_opts=()
if [[ -f "$JAVACMD" ]]; then
java_cmd="$JAVACMD"
fi
# Pull in the machine-wide settings configuration.
if [[ -f "$machine_sbt_opts_file" ]]; then
appendSbtoptsFromFile "$machine_sbt_opts_file"
else
# Otherwise pull in the default settings configuration.
[[ -f "$dist_sbt_opts_file" ]] && appendSbtoptsFromFile "$dist_sbt_opts_file"
fi
# Pull in the project-level config file, if it exists (highest priority, overrides machine/dist).
[[ -f "$sbt_opts_file" ]] && appendSbtoptsFromFile "$sbt_opts_file"
# Prepend sbtopts so command line args appear last and win for duplicate properties.
if (( ${#sbt_file_opts[@]} > 0 )); then
set -- "${sbt_file_opts[@]}" "$@"
fi
# Pull in the project-level java config, if it exists.
[[ -f ".jvmopts" ]] && export JAVA_OPTS="$JAVA_OPTS $(loadConfigFile .jvmopts)"
# Pull in default JAVA_OPTS
[[ -z "${JAVA_OPTS// }" ]] && export JAVA_OPTS="$default_java_opts"
[[ -f "$build_props_file" ]] && loadPropFile "$build_props_file"
java_args=($JAVA_OPTS)
sbt_options0=(${SBT_OPTS:-$default_sbt_opts})
java_tool_options=($JAVA_TOOL_OPTIONS)
jdk_java_options=($JDK_JAVA_OPTIONS)
if [[ "$SBT_NATIVE_CLIENT" == "true" ]]; then
use_sbtn=1
fi
# Split SBT_OPTS into options/commands
miniscript=$(map_args "${sbt_options0[@]}") && eval "${miniscript/options/sbt_options}" && \
eval "${miniscript/commands/sbt_additional_commands}"
# Combine command line options/commands and commands from SBT_OPTS
miniscript=$(map_args "$@") && eval "${miniscript/options/cli_options}" && eval "${miniscript/commands/cli_commands}"
args1=( "${cli_options[@]}" "${cli_commands[@]}" "${sbt_additional_commands[@]}" )
# process the combined args, then reset "$@" to the residuals
process_args "${args1[@]}"
vlog "[sbt_options] $(declare -p sbt_options)"
# Handle -h/--help/-help at the script level without requiring Java or a project
if [[ $print_help ]]; then
usage
exit 0
fi
# Handle --script-version before native client so it works on sbt 2.x project dirs (#8711)
if [[ $print_sbt_script_version ]]; then
echo "$init_sbt_version"
exit 0
fi
java_version="$(jdk_version)"
vlog "[process_args] java_version = '$java_version'"
checkJava17ForSbt2
if [[ "$(isRunNativeClient)" == "true" ]] && [[ -z "$print_version" ]]; then
set -- "${residual_args[@]}"
argumentCount=$#
runNativeClient
else
addDefaultMemory
addSbtScriptProperty
addJdkWorkaround
set -- "${residual_args[@]}"
argumentCount=$#
run
fi