set -euo pipefail
log() { printf '\033[1;34m[setup-twa]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[setup-twa] WARN:\033[0m %s\n' "$*" >&2; }
fail() { printf '\033[1;31m[setup-twa] FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
SCRIPT_PATH="${BASH_SOURCE[0]}"
chmod +x "${SCRIPT_PATH}" 2>/dev/null || true
require_host_tool() {
local tool="$1"
if ! command -v "${tool}" >/dev/null 2>&1; then
fail "${tool} is required but not installed. Install it via your OS package manager and re-run."
fi
}
require_host_tool curl
require_host_tool unzip
require_host_tool zip
SDKMAN_DIR="${SDKMAN_DIR:-${HOME}/.sdkman}"
JDK_VERSION="17.0.13-tem"
ensure_sdkman() {
if [ -s "${SDKMAN_DIR}/bin/sdkman-init.sh" ]; then
log "SDKMAN already installed at ${SDKMAN_DIR}"
return 0
fi
log "Installing SDKMAN to ${SDKMAN_DIR}"
export SDKMAN_DIR
curl -s "https://get.sdkman.io?rcupdate=false" | bash
if [ ! -s "${SDKMAN_DIR}/bin/sdkman-init.sh" ]; then
fail "SDKMAN install reported success but ${SDKMAN_DIR}/bin/sdkman-init.sh is missing."
fi
}
ensure_jdk17() {
set +u . "${SDKMAN_DIR}/bin/sdkman-init.sh"
set -u
local java_dir="${SDKMAN_DIR}/candidates/java"
local installed=""
if [ -d "${java_dir}" ]; then
installed="$(find "${java_dir}" -mindepth 1 -maxdepth 1 -type d -name '17.*' \
-printf '%f\n' 2>/dev/null | head -n1)"
fi
if [ -n "${installed}" ]; then
log "JDK 17 already installed via SDKMAN (${installed})"
else
log "Installing JDK ${JDK_VERSION} via SDKMAN"
set +u
set +o pipefail
if ! yes n | sdk install java "${JDK_VERSION}"; then
local fallback
fallback="$(sdk list java 2>/dev/null \
| grep -Eo '17\.[0-9.]+-tem' | sort -u | tail -n1 || true)"
if [ -z "${fallback}" ]; then
set -u
set -o pipefail
fail "Could not install JDK 17 via SDKMAN (no 17.x-tem candidate found)."
fi
log "Pinned JDK ${JDK_VERSION} not available; falling back to ${fallback}"
yes n | sdk install java "${fallback}"
fi
set -u
set -o pipefail
installed="$(find "${java_dir}" -mindepth 1 -maxdepth 1 -type d -name '17.*' \
-printf '%f\n' 2>/dev/null | head -n1)"
fi
if [ -n "${installed}" ]; then
sdk use java "${installed}" >/dev/null || true
fi
if ! command -v javac >/dev/null 2>&1; then
warn "javac not on PATH after JDK install. SDKMAN may need a fresh shell."
else
log "javac: $(javac -version 2>&1)"
fi
}
NVM_DIR="${NVM_DIR:-${HOME}/.nvm}"
ensure_nvm() {
if [ -s "${NVM_DIR}/nvm.sh" ]; then
log "nvm already installed at ${NVM_DIR}"
return 0
fi
log "Installing nvm to ${NVM_DIR}"
export NVM_DIR
PROFILE=/dev/null curl -o- \
https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
if [ ! -s "${NVM_DIR}/nvm.sh" ]; then
fail "nvm install reported success but ${NVM_DIR}/nvm.sh is missing."
fi
}
ensure_node_lts() {
set +u
. "${NVM_DIR}/nvm.sh"
if nvm which 'lts/*' >/dev/null 2>&1; then
nvm use --lts >/dev/null 2>&1 || true
log "Node LTS already installed via nvm: $(node --version 2>/dev/null || echo unknown)"
set -u
return 0
fi
log "Installing Node LTS via nvm"
nvm install --lts
nvm use --lts >/dev/null
log "Node: $(node --version)"
set -u
}
ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-${HOME}/.android}"
CMDLINE_TOOLS_DIR="${ANDROID_SDK_ROOT}/cmdline-tools/latest"
ANDROID_API_LEVEL="34"
ANDROID_BUILD_TOOLS="34.0.0"
CMDLINE_TOOLS_VERSION="11076708"
CMDLINE_TOOLS_URL="https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip"
ensure_android_cmdline_tools() {
if [ -x "${CMDLINE_TOOLS_DIR}/bin/sdkmanager" ]; then
log "Android cmdline-tools already installed at ${CMDLINE_TOOLS_DIR}"
return 0
fi
log "Installing Android command-line tools to ${CMDLINE_TOOLS_DIR}"
mkdir -p "${ANDROID_SDK_ROOT}/cmdline-tools"
local tmpdir
tmpdir="$(mktemp -d)"
trap 'rm -rf "${tmpdir}"' RETURN
curl -fsSL -o "${tmpdir}/cmdline-tools.zip" "${CMDLINE_TOOLS_URL}"
unzip -q "${tmpdir}/cmdline-tools.zip" -d "${tmpdir}"
if [ ! -d "${tmpdir}/cmdline-tools" ]; then
fail "Unexpected layout in Android cmdline-tools zip."
fi
mv "${tmpdir}/cmdline-tools" "${CMDLINE_TOOLS_DIR}"
if [ ! -x "${CMDLINE_TOOLS_DIR}/bin/sdkmanager" ]; then
fail "sdkmanager not found at ${CMDLINE_TOOLS_DIR}/bin/sdkmanager after install."
fi
if [ ! -e "${ANDROID_SDK_ROOT}/bin" ]; then
ln -s cmdline-tools/latest/bin "${ANDROID_SDK_ROOT}/bin"
log "Created ${ANDROID_SDK_ROOT}/bin -> cmdline-tools/latest/bin (for bubblewrap)"
fi
}
ensure_android_sdk_packages() {
local sdkmanager="${CMDLINE_TOOLS_DIR}/bin/sdkmanager"
if [ ! -x "${sdkmanager}" ]; then
fail "sdkmanager missing; install Android cmdline-tools first."
fi
export ANDROID_HOME="${ANDROID_SDK_ROOT}"
export ANDROID_SDK_ROOT
local needed=()
[ -d "${ANDROID_SDK_ROOT}/platform-tools" ] \
|| needed+=("platform-tools")
[ -d "${ANDROID_SDK_ROOT}/platforms/android-${ANDROID_API_LEVEL}" ] \
|| needed+=("platforms;android-${ANDROID_API_LEVEL}")
[ -d "${ANDROID_SDK_ROOT}/build-tools/${ANDROID_BUILD_TOOLS}" ] \
|| needed+=("build-tools;${ANDROID_BUILD_TOOLS}")
if [ ${#needed[@]} -eq 0 ]; then
log "Android SDK packages already installed (platform-tools, platforms;android-${ANDROID_API_LEVEL}, build-tools;${ANDROID_BUILD_TOOLS})"
else
log "Installing Android SDK packages: ${needed[*]}"
set +o pipefail
yes | "${sdkmanager}" --licenses >/dev/null 2>&1 || true
yes | "${sdkmanager}" "${needed[@]}" >/dev/null
set -o pipefail
fi
log "Accepting Android SDK licenses (non-interactive)"
set +o pipefail
yes | "${sdkmanager}" --licenses >/dev/null 2>&1 || true
set -o pipefail
}
NPM_PREFIX="${HOME}/.local"
ensure_bubblewrap() {
if ! command -v npm >/dev/null 2>&1; then
fail "npm is not on PATH. nvm install must have failed."
fi
mkdir -p "${NPM_PREFIX}/bin"
export PATH="${NPM_PREFIX}/bin:${PATH}"
if command -v bubblewrap >/dev/null 2>&1; then
local current
current="$(bubblewrap --version 2>/dev/null | head -n1 || true)"
log "@bubblewrap/cli already installed (${current:-unknown version})"
return 0
fi
log "Installing @bubblewrap/cli with npm prefix ${NPM_PREFIX}"
npm install -g --prefix "${NPM_PREFIX}" @bubblewrap/cli
if ! command -v bubblewrap >/dev/null 2>&1; then
warn "bubblewrap installed but not on PATH. Check ${NPM_PREFIX}/bin."
else
log "bubblewrap: $(bubblewrap --version 2>/dev/null | head -n1)"
fi
}
log "mobux TWA toolchain setup starting"
ensure_sdkman
ensure_jdk17
ensure_nvm
ensure_node_lts
ensure_android_cmdline_tools
ensure_android_sdk_packages
ensure_bubblewrap
cat <<EOF
==========================================================================
TWA toolchain setup complete.
All tools are installed user-locally. Add the following to your shell rc
(~/.bashrc or ~/.zshrc) so they're available in new shells:
# SDKMAN (Java)
export SDKMAN_DIR="${SDKMAN_DIR}"
[ -s "\$SDKMAN_DIR/bin/sdkman-init.sh" ] && . "\$SDKMAN_DIR/bin/sdkman-init.sh"
# nvm (Node)
export NVM_DIR="${NVM_DIR}"
[ -s "\$NVM_DIR/nvm.sh" ] && . "\$NVM_DIR/nvm.sh"
# Android SDK
export ANDROID_HOME="${ANDROID_SDK_ROOT}"
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT}"
export PATH="\$ANDROID_HOME/cmdline-tools/latest/bin:\$ANDROID_HOME/platform-tools:\$PATH"
# @bubblewrap/cli (npm prefix)
export PATH="${NPM_PREFIX}/bin:\$PATH"
After updating your rc, open a new shell and verify:
javac -version
node --version
sdkmanager --version
bubblewrap --version
==========================================================================
EOF