#!/usr/bin/env bash # ElyFTB installer/updater. Run as your regular user, never with sudo bash. # Downloads are pinned to one release and verified before installation. set -euo pipefail # Parse the whole program before executing it, including when piped into bash. main() { umask 022 API='https://veryultrasupereasyupdater.xxanqw.me' method=auto channel='' check_only=false for argument in "$@"; do case "$argument" in --check) check_only=true ;; --portable|--method=portable) method=portable ;; --method=arch) method=arch ;; --method=auto) method=auto ;; --channel=stable) channel=stable ;; --channel=prerelease) channel=prerelease ;; --help|-h) printf '%s\n' 'ElyFTB Linux installer' \ 'Usage: bash install.sh [--check] [--portable] [--channel=stable|prerelease]' \ 'Arch: pacman package. Other supported x86_64 glibc distros: per-user install.' \ '--check never downloads a package or requests administrator privileges.' exit 0 ;; *) printf 'Unknown option: %s\n' "$argument" >&2; exit 2 ;; esac done fail() { printf 'ElyFTB: %s\n' "$*" >&2; exit 1; } terminal_fd='' open_terminal() { if [[ -n $terminal_fd ]]; then return; fi if ! { exec {terminal_fd}<> /dev/tty; } 2>/dev/null; then fail 'An interactive terminal is required for package installation. Download install.sh, then run bash install.sh from a terminal. Use --check for unattended version checks.' fi } run_privileged() { open_terminal local command_status=0 # Never let sudo, pacman or another package manager read this script's stdin. sudo "$@" <&"$terminal_fd" || command_status=$? if (( command_status != 0 )); then printf 'ElyFTB: Package installation failed or was cancelled (exit %s). No successful installation was confirmed.\n' "$command_status" >&2 exit "$command_status" fi } [[ $EUID -ne 0 ]] || fail 'Run as your regular user. Only package-manager operations use sudo.' [[ $(uname -m) == x86_64 ]] || fail 'This Linux build currently supports x86_64 only.' for dependency in curl sha256sum mktemp tar awk wc; do command -v "$dependency" >/dev/null || fail "Missing required command: $dependency" done if [[ $method == auto ]]; then distro='' if [[ -r /etc/os-release ]]; then distro="$(. /etc/os-release; printf '%s %s' "${ID:-}" "${ID_LIKE:-}")" fi if [[ " $distro " == *' arch '* ]] && command -v pacman >/dev/null && command -v vercmp >/dev/null; then method=arch else method=portable fi fi base="$HOME/.local/opt/elyftb" installed='' current='' if [[ $method == arch ]]; then command -v pacman >/dev/null && command -v vercmp >/dev/null || fail 'Arch mode requires pacman and vercmp.' platform='arch/x86_64' installed="$(pacman -Q elyftb 2>/dev/null | awk '{print $2}')" || true if [[ -n $installed ]]; then current="${installed%-*}"; current="${current//_/-}"; fi else platform='linux/x64' glibc="$(getconf GNU_LIBC_VERSION 2>/dev/null)" || fail 'A glibc distribution is required; Alpine/musl is not supported.' glibc="${glibc#glibc }" [[ $glibc =~ ^([0-9]+)\.([0-9]+) ]] || fail 'Cannot determine the glibc version.' (( BASH_REMATCH[1] > 2 || (BASH_REMATCH[1] == 2 && BASH_REMATCH[2] >= 28) )) || fail 'glibc 2.28 or newer is required.' if [[ -f "$base/current/version" ]]; then IFS= read -r current < "$base/current/version"; installed="$current"; fi [[ ! -e "$base/current" || -L "$base/current" ]] || fail "Unexpected non-symlink directory: $base/current" fi version_pattern='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$' [[ -z $current || $current =~ $version_pattern ]] || fail 'Installed version is not recognized; refusing to overwrite it.' if [[ -z $channel ]]; then if [[ -n $current && $current != *-* ]]; then channel=stable; else channel=prerelease; fi fi printf 'ElyFTB · %s · channel: %s\n' "$platform" "$channel" if [[ -n $installed ]]; then printf 'Installed: %s\n' "$installed"; else printf 'Not installed yet.\n'; fi work="$(mktemp -d)" stage='' cleanup() { rm -rf -- "$work" if [[ -n $stage ]]; then rm -rf -- "$stage"; fi if [[ -n $terminal_fd ]]; then exec {terminal_fd}>&-; fi } trap cleanup EXIT query="channel=$channel" if [[ -n $current ]]; then query+="¤t=$current"; fi if ! code="$(curl --proto '=https' --proto-redir '=https' --tlsv1.2 --fail --silent --show-error --location \ --connect-timeout 15 --max-time 40 --retry 2 --output "$work/manifest" --write-out '%{http_code}' \ "$API/manifest/$platform?$query")"; then if [[ $code == 404 ]]; then printf 'No complete build is available for this channel yet. Nothing changed.\n'; exit 0; fi fail 'Cannot retrieve release metadata. Nothing changed.' fi [[ $(wc -c < "$work/manifest") -le 8192 ]] || fail 'Release manifest is too large.' version='' package_version='' status='' url='' sha256='' size='' remote_platform='' # Deliberately parse fields; never source or eval a remote manifest. while IFS='=' read -r key value; do case "$key" in platform) remote_platform="$value" ;; version) version="$value" ;; package_version) package_version="$value" ;; status) status="$value" ;; url) url="$value" ;; sha256) sha256="$value" ;; size) size="$value" ;; *) fail 'Unknown release manifest field.' ;; esac done < "$work/manifest" [[ $remote_platform == "$platform" && $version =~ $version_pattern ]] || fail 'Invalid release identity.' [[ $sha256 =~ ^[a-f0-9]{64}$ && $size =~ ^[1-9][0-9]{0,9}$ ]] || fail 'Invalid download checksum or size.' (( size <= 2147483648 )) || fail 'Download exceeds the maximum expected size.' [[ $url == "$API/download/$platform?tag=v$version" ]] || fail 'Untrusted or unpinned download URL.' [[ $status == up-to-date || $status == update-available || $status == not-installed ]] || fail 'Invalid version-check result.' if [[ $method == arch ]]; then expected_prefix="${version//-/_}-" [[ $package_version == "$expected_prefix"* && ${package_version#"$expected_prefix"} =~ ^[1-9][0-9]*$ ]] || fail 'Invalid pacman version.' if [[ -n $installed ]] && { [[ $current != "$version" && $status == up-to-date ]] || { [[ $current == "$version" ]] && [[ $(vercmp "$package_version" "$installed") -le 0 ]]; }; }; then printf 'ElyFTB is already up to date (%s). No package download needed.\n' "$installed"; exit 0 fi else [[ $package_version == "$version" ]] || fail 'Invalid portable version.' if [[ -n $installed && $status == up-to-date ]]; then printf 'ElyFTB is already up to date (%s). No package download needed.\n' "$installed"; exit 0 fi fi printf 'Available: %s\n' "$version" if $check_only; then printf 'An installation/update is available. Check only: nothing downloaded or installed.\n'; exit 0; fi if [[ $method == arch ]]; then command -v sudo >/dev/null || fail 'sudo is required to install the pacman package.' open_terminal # Fail before a large download when no prompt can be answered. fi package="$work/elyftb-download" printf 'Downloading the verified release…\n' curl --proto '=https' --proto-redir '=https' --tlsv1.2 --fail --show-error --location --retry 2 \ --connect-timeout 15 --max-time 900 --output "$package" "$url" [[ $(wc -c < "$package") -eq $size ]] || fail 'Download size mismatch. Nothing installed.' printf '%s %s\n' "$sha256" "$package" | sha256sum --check --status || fail 'SHA-256 mismatch. Nothing installed.' if [[ $method == arch ]]; then [[ $(pacman -Qp "$package") == "elyftb $package_version" ]] || fail 'The package name/version does not match the release.' run_privileged pacman -U --needed "$package" confirmed="$(pacman -Q elyftb 2>/dev/null | awk '{print $2}')" || true [[ $confirmed == "$package_version" ]] || fail "pacman did not install the expected version $package_version (installed: ${confirmed:-not installed}). Installation was not confirmed." else # Reject absolute paths and traversal before extraction. Extraction is never privileged. tar -tzf "$package" > "$work/archive-files" while IFS= read -r entry; do [[ $entry == elyftb || $entry == elyftb/ || $entry == elyftb/* ]] || fail 'Unexpected archive layout.' [[ /$entry/ != *'/../'* ]] || fail 'Unsafe path in archive.' done < "$work/archive-files" mkdir -p "$base/releases" stage="$(mktemp -d "$base/releases/.staging.XXXXXXXX")" tar --extract --gzip --file "$package" --directory "$stage" --strip-components=1 --no-same-owner --no-same-permissions [[ -x "$stage/elyftb" && -f "$stage/resources/app.asar" && -f "$stage/version" && -f "$stage/elyftb.png" ]] || fail 'Incomplete Linux payload.' IFS= read -r payload_version < "$stage/version" [[ $payload_version == "$version" ]] || fail 'Archive version does not match the release.' command -v ldd >/dev/null || fail 'ldd is required to check runtime libraries.' ldd "$stage/elyftb" > "$work/libraries" 2>&1 || true if grep -q 'not found' "$work/libraries"; then printf 'Some desktop runtime libraries are missing. Your package manager will ask for approval.\n' command -v sudo >/dev/null || fail 'Install the missing runtime libraries and retry (sudo is unavailable).' if command -v apt-get >/dev/null; then apt_library() { if apt-cache show "${1}t64" >/dev/null 2>&1; then printf '%s' "${1}t64"; else printf '%s' "$1"; fi; } run_privileged apt-get update run_privileged apt-get install "$(apt_library libgtk-3-0)" libnss3 "$(apt_library libasound2)" libgbm1 libxss1 libxkbcommon0 "$(apt_library libcups2)" elif command -v dnf >/dev/null; then run_privileged dnf install gtk3 nss alsa-lib libXScrnSaver mesa-libgbm libxkbcommon cups-libs elif command -v zypper >/dev/null; then run_privileged zypper install libgtk-3-0 mozilla-nss libasound2 libgbm1 libXss1 libxkbcommon0 libcups2 elif command -v pacman >/dev/null; then run_privileged pacman -S --needed gtk3 nss alsa-lib libxss mesa libxkbcommon libcups else fail 'Unsupported package manager. Install the runtime libraries shown by ldd and retry.' fi ldd "$stage/elyftb" > "$work/libraries" 2>&1 || true if grep -q 'not found' "$work/libraries"; then fail 'Runtime libraries are still missing. The existing app has not been replaced.'; fi fi release_dir="$base/releases/$version-$$" mv -- "$stage" "$release_dir" stage='' bin="$HOME/.local/bin" data="${XDG_DATA_HOME:-$HOME/.local/share}" mkdir -p "$bin" "$data/applications" "$data/icons/hicolor/512x512/apps" install -m644 "$release_dir/elyftb.png" "$data/icons/hicolor/512x512/apps/elyftb.png" launcher="$bin/elyftb" quoted_launcher="${launcher//\\/\\\\}" quoted_launcher="${quoted_launcher//\"/\\\"}" quoted_launcher="${quoted_launcher//\$/\\\$}" quoted_launcher="${quoted_launcher//\`/\\\`}" printf '[Desktop Entry]\nType=Application\nName=ElyFTB\nComment=Ely.by-enabled modpack launcher\nExec="%s" %%u\nIcon=elyftb\nTerminal=false\nCategories=Game;\nMimeType=x-scheme-handler/ftb;\nStartupWMClass=elyftb\n' \ "$quoted_launcher" > "$data/applications/elyftb.desktop" ln -sfn "$base/current/elyftb" "$launcher" ln -s "releases/$(basename "$release_dir")" "$base/.current-$$" mv -Tf "$base/.current-$$" "$base/current" if command -v update-desktop-database >/dev/null; then update-desktop-database "$data/applications" >/dev/null 2>&1 || true; fi printf 'Per-user installation: %s\n' "$base/current" printf 'Start from your application menu, or run: %s\n' "$launcher" printf 'Previous release directories are retained so a running launcher is not disrupted.\n' fi printf 'ElyFTB %s installed. Restart the launcher if it is already open. Your modpacks and worlds are unchanged.\n' "$version" } main "$@"