App-Ariza.git | resources/templates/ | install-posix.sh.j2
#!/bin/sh
# {{ app_display }} installer — macOS and Linux.
#
# Generated by ariza from {{ app_name }}'s ariza.toml. Do not edit this
# file: edit ariza's resources/templates/install-posix.sh.j2 and re-run
# `ariza installers --app=<this repository>`.
#
# curl -fsSL {{ raw_base }}/install.sh | sh
# curl -fsSL {{ raw_base }}/install.sh | sh -s -- --version v1.2.3
# sh install.sh --help
#
# What it does:
#
# 1. Works out which prebuilt bundle this machine takes.
# 2. Downloads it from the {{ repo }} releases and verifies its sha256.
# 3. Unpacks it beside any version already installed, then flips a
# `current` symlink — so a failed download cannot break a working
# install.
# 4. Links ~/.local/bin/{{ app_exec }}, and puts that directory on PATH
# only if it is not on it already.
{%- if warm %}
# 5. Runs it once, so that whatever the first launch has to do is done
# here rather than the first time you want the program.
{%- endif %}
#
# Nothing is compiled, nothing needs root, and nothing outside
# ${XDG_DATA_HOME:-~/.local/share}/{{ app_exec }} and ~/.local/bin is
# written — bar one marked block in your shell rc files, which the
# uninstaller removes again.
set -eu
APP_DISPLAY='{{ app_display }}'
APP_EXEC='{{ app_exec }}'
APP_REPO='{{ repo }}'
# The platforms {{ app_name }} publishes bundles for. Detection produces
# one of these strings or nothing at all; there is deliberately no
# "closest match", because a bundle for the wrong libc does not run.
ARIZA_SLUGS='{{ slugs }}'
ARIZA_PATH_MARKER="# >>> $APP_EXEC PATH >>>"
ARIZA_PATH_END="# <<< $APP_EXEC PATH <<<"
ARIZA_ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/$APP_EXEC"
ARIZA_VERSIONS="$ARIZA_ROOT/versions"
ARIZA_BIN_DIR="$HOME/.local/bin"
# Set {{ env_url }} to install from somewhere other than the
# {{ repo }} releases — a local file, a release candidate, a mirror.
# --url overrides it in turn.
ARIZA_SRC={{ env_url_expr }}
ARIZA_TAG=''
ARIZA_VERSION=''
ARIZA_OVERRIDE=0
ARIZA_NO_VERIFY=0
ARIZA_TMP=''
ARIZA_STAGING=''
ARIZA_PRIVATE=0
ARIZA_CANDIDATE=''
ARIZA_HANDOFF=''
ARIZA_NONCE=''
ARIZA_EXPECTED_CURRENT=''
ARIZA_PREVIOUS_PHYSICAL=''
ARIZA_STATE="$ARIZA_ROOT/.ariza/update-v1"
############################# common runtime ##################################
{{ lib_posix | safe }}
###############################################################################
ariza_usage() {
cat <<USAGE
Usage: install.sh [options]
--version TAG install a specific release tag (e.g. --version v1.2.3)
instead of the latest one
--url FILE-OR-URL install this archive instead, bypassing GitHub
entirely. Also settable as {{ env_url }}.
--insecure-no-verify with --url only: proceed even when the source has
no sibling .sha256 to check it against
-h, --help this message
Installs to $ARIZA_ROOT/versions/<version>/ and links
$ARIZA_BIN_DIR/$APP_EXEC. Re-running with a version already installed
repairs the links and exits without downloading anything.
USAGE
}
ariza_parse_args() {
while [ $# -gt 0 ]; do
case $1 in
--version)
shift
[ $# -gt 0 ] || ariza_err "--version needs a release tag, e.g. --version v1.2.3"
ARIZA_TAG=$1
;;
--version=*) ARIZA_TAG=${1#*=} ;;
--url)
shift
[ $# -gt 0 ] || ariza_err "--url needs a file path or a URL"
ARIZA_SRC=$1
;;
--url=*) ARIZA_SRC=${1#*=} ;;
--insecure-no-verify) ARIZA_NO_VERIFY=1 ;;
--ariza-update-candidate)
shift; [ $# -gt 0 ] || ariza_err "--ariza-update-candidate needs a version"
ARIZA_PRIVATE=1; ARIZA_CANDIDATE=$1 ;;
--ariza-handoff)
shift; [ $# -gt 0 ] || ariza_err "--ariza-handoff needs a path"
ARIZA_PRIVATE=1; ARIZA_HANDOFF=$1 ;;
--ariza-nonce)
shift; [ $# -gt 0 ] || ariza_err "--ariza-nonce needs a nonce"
ARIZA_PRIVATE=1; ARIZA_NONCE=$1 ;;
--ariza-expected-current)
shift; [ $# -gt 0 ] || ariza_err "--ariza-expected-current needs a path"
ARIZA_PRIVATE=1; ARIZA_EXPECTED_CURRENT=$1 ;;
-h|--help) ariza_usage; exit 0 ;;
*) ariza_err "unknown option '$1' -- run with --help" ;;
esac
shift
done
}
ariza_validate_private() {
[ "$ARIZA_PRIVATE" -eq 1 ] || return 0
[ -n "$ARIZA_CANDIDATE" ] && [ -n "$ARIZA_HANDOFF" ] \
&& [ -n "$ARIZA_NONCE" ] && [ -n "$ARIZA_EXPECTED_CURRENT" ] \
|| ariza_err "private update mode requires candidate, handoff, nonce, and expected-current"
case $ARIZA_CANDIDATE in
*[!0-9.]*|.*|*..*|*.) ariza_err "private update candidate must be a bare ASCII x.y.z version" ;;
esac
[ "$(printf '%s' "$ARIZA_CANDIDATE" | awk -F. '{ print NF }')" -eq 3 ] \
|| ariza_err "private update candidate must be a bare ASCII x.y.z version"
case $ARIZA_NONCE in
*[!0-9a-f]*|'') ariza_err "private update nonce must be 64 lowercase hexadecimal characters" ;;
esac
[ "$(printf '%s' "$ARIZA_NONCE" | awk '{ print length($0) }')" -eq 64 ] \
|| ariza_err "private update nonce must be 64 lowercase hexadecimal characters"
[ -d "$ARIZA_EXPECTED_CURRENT" ] \
|| ariza_err "expected current bundle does not exist"
ARIZA_EXPECTED_CURRENT=$(cd "$ARIZA_EXPECTED_CURRENT" && pwd -P)
[ ! -L "$ARIZA_ROOT/.ariza" ] && [ ! -L "$ARIZA_STATE" ] \
|| ariza_err "private update state must not be a symlink"
mkdir -p "$ARIZA_STATE"
_state_physical=$(cd "$ARIZA_STATE" && pwd -P)
_handoff_parent=$(dirname "$ARIZA_HANDOFF")
[ -d "$_handoff_parent" ] || ariza_err "private handoff must be inside update-v1 state"
[ ! -L "$_handoff_parent" ] || ariza_err "private handoff directory must not be a symlink"
_handoff_parent=$(cd "$_handoff_parent" && pwd -P)
if [ "$_handoff_parent" = "$_state_physical" ]; then
case $(basename "$ARIZA_HANDOFF") in
handoff|handoff.*) ;;
*) ariza_err "private handoff has an invalid name" ;;
esac
else
_handoff_grandparent=$(cd "$_handoff_parent/.." && pwd -P)
[ "$_handoff_grandparent" = "$_state_physical" ] \
|| ariza_err "private handoff must be inside update-v1 state"
case $(basename "$_handoff_parent")/$(basename "$ARIZA_HANDOFF") in
handoff-*/result) ;;
*) ariza_err "private handoff has an invalid name" ;;
esac
fi
[ ! -e "$ARIZA_HANDOFF" ] && [ ! -L "$ARIZA_HANDOFF" ] \
|| ariza_err "private handoff path already exists"
# Private mode has no public escape hatches: it installs exactly the
# coordinator-approved GitHub candidate, with its mandatory checksum.
[ -z "$ARIZA_TAG" ] && [ "$ARIZA_NO_VERIFY" -eq 0 ] \
|| ariza_err "public version/insecure options are forbidden in private update mode"
[ -z {{ env_url_unset_expr }} ] \
|| ariza_err "{{ env_url }} is forbidden in private update mode"
ARIZA_SRC=''
}
ariza_cleanup() {
if [ -n "$ARIZA_TMP" ] && [ -d "$ARIZA_TMP" ]; then
rm -rf "$ARIZA_TMP"
fi
if [ -n "$ARIZA_STAGING" ] && [ -d "$ARIZA_STAGING" ]; then
rm -rf "$ARIZA_STAGING"
fi
return 0
}
# ---- which bundle this machine takes -----------------------------------------
ariza_detect_slug() {
_sys=$(uname -s)
_mach=$(uname -m)
case $_sys in
Darwin)
case $_mach in
arm64|aarch64) printf 'macos-arm64\n' ;;
x86_64|amd64) printf 'macos-x86_64\n' ;;
*) return 1 ;;
esac
;;
Linux)
# A glibc binary does not run on Alpine and a musl binary does
# not run on Debian, so the libc is part of the name. The musl
# loader's presence is conclusive; nothing else needs asking.
_libc=glibc
for _loader in /lib/ld-musl-*.so.1 /usr/lib/ld-musl-*.so.1; do
if [ -e "$_loader" ]; then
_libc=musl
break
fi
done
case $_mach in
aarch64|arm64) printf 'linux-aarch64-%s\n' "$_libc" ;;
x86_64|amd64) printf 'linux-x86_64-%s\n' "$_libc" ;;
*) return 1 ;;
esac
;;
*) return 1 ;;
esac
}
ariza_slug_supported() {
for _s in $ARIZA_SLUGS; do
[ "$_s" = "$1" ] && return 0
done
return 1
}
ariza_version_from_name() {
# $1 = a bundle directory or archive basename, which ariza names
# <exec>-<version>-<slug>. Stripping a known slug off the end rather
# than splitting on dashes is what makes a version containing one
# (1.0-rc1) parse correctly.
_n=$1
_n=${_n%.tar.gz}
_n=${_n%.tgz}
_n=${_n%.zip}
case $_n in
"$APP_EXEC-"*) _n=${_n#"$APP_EXEC-"} ;;
*) return 1 ;;
esac
for _s in $ARIZA_SLUGS; do
case $_n in
*"-$_s") printf '%s\n' "${_n%"-$_s"}"; return 0 ;;
esac
done
return 1
}
ariza_latest_tag() {
# The `location:` header of the releases/latest redirect names the
# tag. That is one HEAD request against a static host, and needs no
# JSON parser on a machine that may not have one.
_url="https://github.com/$APP_REPO/releases/latest"
if ariza_have curl; then
_loc=$(curl -fsSI "$_url" | tr -d '\r' \
| awk 'tolower($1) == "location:" { print $2 }' | tail -n 1)
elif ariza_have wget; then
_loc=$(wget --max-redirect=0 --spider --server-response "$_url" 2>&1 \
| tr -d '\r' | awk 'tolower($1) == "location:" { print $2 }' | tail -n 1)
else
ariza_err "need curl or wget to find the latest $APP_DISPLAY release"
fi
[ -n "$_loc" ] || return 1
printf '%s\n' "${_loc##*/}"
}
# ---- the install itself ------------------------------------------------------
ariza_replace_link() {
# GNU mv needs -T not to follow a destination symlink-to-directory; BSD
# mv spells that -h. Both forms are atomic renames on the same filesystem.
if mv -Tf "$1" "$2" 2>/dev/null; then
return 0
fi
if mv -fh "$1" "$2" 2>/dev/null; then
return 0
fi
# Last-resort POSIX spelling for older implementations. There is a short
# missing-link window, but the transaction journal makes it recoverable.
rm -f "$2"
mv -f "$1" "$2"
}
ariza_point_current() {
# $1 = version. The target is relative, so the whole data directory
# can be moved without breaking it.
_link="$ARIZA_ROOT/current"
if [ -e "$_link" ] && [ ! -L "$_link" ]; then
ariza_err "$_link exists and is not a symlink -- move it aside and re-run"
fi
_new="$_link.new.$$"
rm -f "$_new"
ln -s "versions/$1" "$_new"
ariza_replace_link "$_new" "$_link"
}
ariza_physical_current() {
_link="$ARIZA_ROOT/current"
[ -L "$_link" ] || return 1
(cd "$_link" && pwd -P)
}
ariza_version_for_physical() {
_physical=$1
_versions=$(cd "$ARIZA_VERSIONS" 2>/dev/null && pwd -P) || return 1
case $_physical in
"$_versions"/*)
_leaf=${_physical#"$_versions"/}
case $_leaf in */*) return 1 ;; esac
[ -d "$ARIZA_VERSIONS/$_leaf" ] || return 1
printf '%s\n' "$_leaf"
;;
*) return 1 ;;
esac
}
ariza_point_previous() {
# $1 is the exact physical pre-switch bundle, not the newest directory.
# This is what makes A -> B -> rollback A -> C retain A rather than B.
_v=$(ariza_version_for_physical "$1") \
|| ariza_err "pre-switch current is outside the managed versions directory"
_link="$ARIZA_ROOT/previous"
if [ -e "$_link" ] && [ ! -L "$_link" ]; then
ariza_err "$_link exists and is not a symlink -- move it aside and re-run"
fi
_new="$_link.new.$$"
rm -f "$_new"
ln -s "versions/$_v" "$_new"
ariza_replace_link "$_new" "$_link"
}
ariza_link_bin() {
mkdir -p "$ARIZA_BIN_DIR"
_bin="$ARIZA_BIN_DIR/$APP_EXEC"
if [ -e "$_bin" ] && [ ! -L "$_bin" ]; then
ariza_err "$_bin exists and is not a symlink -- move it aside and re-run"
fi
ln -sfn "$ARIZA_ROOT/current/bin/$APP_EXEC" "$_bin"
}
ariza_prune() {
# Keep exactly the new current and exact physical pre-switch current.
_keep=$1
_previous=$2
for _v_path in "$ARIZA_VERSIONS"/*; do
[ -d "$_v_path" ] || continue
_v=$(basename "$_v_path")
[ -d "$ARIZA_VERSIONS/$_v" ] || continue
[ "$_v" = "$_keep" ] && continue
[ "$_v" = "$_previous" ] && continue
if rm -rf "$ARIZA_VERSIONS/$_v"; then
ariza_log "removed superseded version $_v"
else
ariza_warn "could not remove superseded version $_v; it will be retried by a later install"
fi
done
}
ariza_check_existing() {
# $1 = version. Returns 0 when it is already installed and usable, in
# which case the links and PATH are repaired on the way past: a
# re-run is the obvious thing to try when someone has deleted
# $ARIZA_BIN_DIR/$APP_EXEC, and it should fix it.
_v=$1
_dir="$ARIZA_VERSIONS/$_v"
[ -d "$_dir" ] || return 1
if [ ! -x "$_dir/bin/$APP_EXEC" ]; then
ariza_warn "$_dir is incomplete -- installing it again"
rm -rf "$_dir"
return 1
fi
_current=$(ariza_physical_current 2>/dev/null || true)
_existing=$(cd "$_dir" && pwd -P)
[ "$_current" = "$_existing" ] || ariza_commit "$_v"
ariza_link_bin
ariza_persist_path "$ARIZA_BIN_DIR"
ariza_ok "$APP_DISPLAY $_v is already installed"
return 0
}
ariza_fetch_and_unpack() {
# $1 = source (path or URL), $2 = digest source or empty.
ARIZA_TMP=$(mktemp -d "${TMPDIR:-/tmp}/$APP_EXEC-install.XXXXXX")
_archive="$ARIZA_TMP/$(basename "$1")"
if [ -f "$1" ]; then
cp "$1" "$_archive"
else
ariza_log "downloading $1"
ariza_download "$1" "$_archive"
fi
_expected=''
if [ -n "$2" ]; then
if [ -f "$2" ]; then
_expected=$(ariza_first_word "$2")
elif [ ! -f "$1" ] && ariza_download "$2" "$_archive.sha256" 2>/dev/null; then
_expected=$(ariza_first_word "$_archive.sha256")
fi
fi
if [ -n "$_expected" ]; then
ariza_verify_sha256 "$_archive" "$_expected"
ariza_ok "sha256 verified"
elif [ "$ARIZA_OVERRIDE" -eq 1 ] && [ "$ARIZA_NO_VERIFY" -eq 1 ]; then
# Only reachable for a source the user named themselves, and only
# when they asked for it in as many words. A published release
# always has a checksum beside it, so its absence there is a
# tampered or half-uploaded release and stays fatal.
ariza_warn "NOT VERIFIED: no $(basename "$2") beside the source you gave, and --insecure-no-verify was passed"
else
ariza_err "no checksum published at $2 -- refusing to install a bundle that cannot be verified"
fi
# Unpacked inside the data directory rather than in /tmp, so the move
# into place is a rename on the same filesystem: the new version
# appears complete or not at all, and `current` is flipped only after
# it is there.
mkdir -p "$ARIZA_VERSIONS"
ARIZA_STAGING="$ARIZA_ROOT/.staging.$$"
rm -rf "$ARIZA_STAGING"
mkdir -p "$ARIZA_STAGING"
case $_archive in
*.zip)
ariza_have unzip || ariza_err "need unzip to unpack $(basename "$_archive")"
unzip -q -o "$_archive" -d "$ARIZA_STAGING"
;;
*)
tar -x -z -f "$_archive" -C "$ARIZA_STAGING"
;;
esac
rm -rf "$ARIZA_TMP"
ARIZA_TMP=''
}
ariza_validate_private_manifest() {
[ "$ARIZA_PRIVATE" -eq 1 ] || return 0
_manifest="$1/ariza-manifest.json"
[ -f "$_manifest" ] || ariza_err "private update bundle has no ariza-manifest.json"
# Bundle manifests are generated as one compact JSON line. Match only
# fixed, generated identity fields here; no value from the manifest is
# ever evaluated as shell code.
_compact="$ARIZA_STAGING/.ariza-manifest.compact"
tr -d ' \t\r\n' <"$_manifest" >"$_compact"
grep -Fq '"ariza-manifest":1' "$_compact" \
|| ariza_err "private update bundle has an unsupported manifest"
grep -Fq '"exec":"{{ app_exec }}"' "$_compact" \
|| ariza_err "private update bundle is for a different application"
grep -Fq '"name":"{{ app_name }}"' "$_compact" \
|| ariza_err "private update bundle is for a different application"
grep -Fq "\"version\":\"$ARIZA_CANDIDATE\"" "$_compact" \
|| ariza_err "private update bundle version does not match the candidate"
grep -Fq '"updates":{"application-target":' "$_compact" \
|| ariza_err "private update bundle has no update protocol metadata"
grep -Fq '"coordinator":"libexec/ariza/update.raku"' "$_compact" \
|| ariza_err "private update bundle names an invalid coordinator"
grep -Fq '"enabled":true' "$_compact" \
|| ariza_err "private update bundle is not update-enabled"
grep -Fq '"installer":"libexec/ariza/install.sh"' "$_compact" \
|| ariza_err "private update bundle names an invalid installer"
grep -Fq '"protocol":1' "$_compact" \
|| ariza_err "private update bundle uses an unsupported protocol"
grep -Fq '"repository":"{{ repo }}"' "$_compact" \
|| ariza_err "private update bundle names a different repository"
[ -f "$1/libexec/ariza/update.raku" ] && [ -f "$1/libexec/ariza/install.sh" ] \
|| ariza_err "private update bundle is missing its trusted updater files"
_target=$(sed -n 's/.*"application-target":"\([^"]*\)".*/\1/p' "$_compact")
case $_target in
''|/*|*'../'*|*'/..'|../*) ariza_err "private update bundle names an unsafe application target" ;;
esac
[ -f "$1/$_target" ] \
|| ariza_err "private update bundle is missing its application target"
}
ariza_write_handoff() {
umask 077
_record="$ARIZA_HANDOFF.tmp.$$"
rm -f "$_record"
if ! printf 'protocol=1\nnonce=%s\ncandidate=%s\n' \
"$ARIZA_NONCE" "$ARIZA_CANDIDATE" >"$_record" \
|| ! mv -f "$_record" "$ARIZA_HANDOFF" \
|| [ ! -f "$ARIZA_HANDOFF" ] || [ -L "$ARIZA_HANDOFF" ]; then
rm -f "$_record"
return 1
fi
}
ariza_restore_previous() {
if [ -n "$1" ]; then
ariza_point_previous "$1"
else
rm -f "$ARIZA_ROOT/previous"
fi
}
ariza_recover_transaction() {
_journal="$ARIZA_STATE/transaction"
[ -f "$_journal" ] || return 0
[ ! -L "$_journal" ] \
|| ariza_err "installer transaction journal must not be a symlink"
[ "$(wc -c <"$_journal" 2>/dev/null || printf 99999)" -le 4096 ] \
|| ariza_err "installer transaction journal is oversized"
[ "$(grep -c '^protocol=1$' "$_journal" 2>/dev/null || :)" -eq 1 ] \
&& [ "$(grep -c '^old=' "$_journal" 2>/dev/null || :)" -eq 1 ] \
&& [ "$(grep -c '^previous=' "$_journal" 2>/dev/null || :)" -eq 1 ] \
&& [ "$(grep -c '^new=' "$_journal" 2>/dev/null || :)" -eq 1 ] \
|| ariza_err "installer transaction journal is corrupt; refusing to change managed pointers"
_old=$(sed -n 's/^old=//p' "$_journal" | head -n 1)
_old_previous=$(sed -n 's/^previous=//p' "$_journal" | head -n 1)
if [ -n "$_old" ]; then
_old_version=$(ariza_version_for_physical "$_old" 2>/dev/null || true)
[ -n "$_old_version" ] \
|| ariza_err "installer transaction journal names an unsafe old target"
ariza_point_current "$_old_version"
else
rm -f "$ARIZA_ROOT/current"
fi
ariza_restore_previous "$_old_previous"
rm -f "$_journal"
ariza_warn "recovered an interrupted installer transaction"
}
ariza_commit() {
# $1 = new version. Validate optimistic concurrency immediately before
# pointer mutation; all download/extract/identity work is pre-commit.
_new_version=$1
_before=$(ariza_physical_current 2>/dev/null || true)
if [ "$ARIZA_PRIVATE" -eq 1 ]; then
[ -n "$_before" ] || ariza_err "managed current is missing"
[ "$_before" = "$ARIZA_EXPECTED_CURRENT" ] \
|| ariza_err "managed current changed while the update was being prepared"
fi
_old_previous=''
if [ -L "$ARIZA_ROOT/previous" ]; then
_old_previous=$(cd "$ARIZA_ROOT/previous" 2>/dev/null && pwd -P || true)
fi
_previous_version=''
if [ -n "$_before" ]; then
_previous_version=$(ariza_version_for_physical "$_before") \
|| ariza_err "managed current is outside the versions directory"
fi
umask 077
_journal="$ARIZA_STATE/transaction"
_journal_tmp="$_journal.tmp.$$"
printf 'protocol=1\nold=%s\nprevious=%s\nnew=%s\n' \
"$_before" "$_old_previous" "$ARIZA_VERSIONS/$_new_version" >"$_journal_tmp"
mv -f "$_journal_tmp" "$_journal"
ariza_point_current "$_new_version"
if [ -n "$_before" ]; then
ariza_point_previous "$_before"
else
rm -f "$ARIZA_ROOT/previous"
fi
ARIZA_PREVIOUS_PHYSICAL=$_before
# Once the journal is gone, the only remaining fallible commit step in
# private mode is the authenticated record. If either final write fails,
# restore both pointers while every rollback target is still retained.
if ! rm -f "$_journal"; then
if [ -n "$_previous_version" ]; then
ariza_point_current "$_previous_version"
else
rm -f "$ARIZA_ROOT/current"
fi
ariza_restore_previous "$_old_previous"
ariza_err "could not finalize the installer transaction; restored the previous install"
fi
if [ "$ARIZA_PRIVATE" -eq 1 ] && ! ariza_write_handoff; then
ariza_point_current "$_previous_version"
ariza_restore_previous "$_old_previous"
ariza_err "could not write authenticated update handoff; restored the previous install"
fi
# No fallible operation follows the private handoff. Retention cleanup is
# best-effort and reports anything it must leave for a later install.
ariza_prune "$_new_version" "$_previous_version"
}
ariza_install() {
# The archive holds exactly one directory, named after the bundle.
set -- "$ARIZA_STAGING"/*
if [ $# -ne 1 ] || [ ! -d "$1" ]; then
ariza_err "that archive does not look like a $APP_DISPLAY bundle (expected one directory inside it)"
fi
_top=$1
# What the bundle says it is beats what the archive was called: a
# versions/<x> directory that does not contain <x> would make every
# later "already installed" check a lie.
_actual=$(ariza_version_from_name "$(basename "$_top")" || true)
if [ -z "$_actual" ]; then
[ -n "$ARIZA_VERSION" ] || ariza_err "cannot tell which version $(basename "$_top") is -- expected ${APP_EXEC}-<version>-<platform>"
else
if [ -n "$ARIZA_VERSION" ] && [ "$_actual" != "$ARIZA_VERSION" ]; then
[ "$ARIZA_PRIVATE" -eq 0 ] \
|| ariza_err "private update archive contains $_actual, expected $ARIZA_VERSION"
ariza_warn "that archive is named $ARIZA_VERSION but contains $_actual -- installing it as $_actual"
fi
ARIZA_VERSION=$_actual
fi
ariza_validate_private_manifest "$_top"
if [ "$ARIZA_PRIVATE" -eq 0 ] && ariza_check_existing "$ARIZA_VERSION"; then
rm -rf "$ARIZA_STAGING"
ARIZA_STAGING=''
return 0
fi
_dest="$ARIZA_VERSIONS/$ARIZA_VERSION"
rm -rf "$_dest"
mv "$_top" "$_dest"
# Stamped with the install time, not the build time: tar restores the
# directory's original mtime, which is the same for every bundle cut
# from one source tree, and pruning by age has to mean "the one I
# installed before this one".
touch "$_dest"
rm -rf "$ARIZA_STAGING"
ARIZA_STAGING=''
[ -x "$_dest/bin/$APP_EXEC" ] || ariza_err "the unpacked bundle has no runnable bin/$APP_EXEC -- not touching your existing install"
ariza_commit "$ARIZA_VERSION"
if [ "$ARIZA_PRIVATE" -eq 1 ]; then
ariza_ok "$APP_DISPLAY $ARIZA_VERSION installed" || :
return 0
fi
ariza_link_bin
ariza_persist_path "$ARIZA_BIN_DIR"
ariza_ok "$APP_DISPLAY $ARIZA_VERSION installed"
}
{%- if warm %}
ariza_warm() {
# Run the app once, now, through the same path the user's shell will
# take: the `current` symlink this script has just repointed. A first
# launch pages a few hundred megabytes off a cold disk and builds
# whatever per-user state the app keeps; doing it here means it
# happens while an installer is on screen saying so, instead of the
# first time somebody actually wants the program.
#
# Never fatal. The bundle is installed and its sha256 was checked
# before anything was moved into place, so a warm-up that fails on
# one machine is far more likely to be that machine — no terminal, a
# sandbox, a policy — than a broken release. Refusing to finish the
# install over it would take a working program away from a user who
# has one.
_warm_bin="$ARIZA_ROOT/current/bin/$APP_EXEC"
[ -x "$_warm_bin" ] || return 0
# The arguments come from the app's ariza.toml and are written out
# already quoted, once as the command line and once as the single
# word the message names.
_warm_what={{ warm_display }}
ariza_log 'warming up -- the first launch does the work the rest never repeat'
if "$_warm_bin" {{ warm_args }} >/dev/null 2>&1; then
ariza_ok 'ready'
else
ariza_warn "warm-up failed: $APP_EXEC $_warm_what did not complete"
ariza_warn "$APP_DISPLAY is installed and its download was verified -- try running it; the first launch may just take longer"
fi
}
{%- endif %}
ariza_report() {
printf '\n'
printf ' run it: %s\n' "$APP_EXEC"
printf ' installed in: %s\n' "$ARIZA_VERSIONS/$ARIZA_VERSION"
printf ' uninstall: curl -fsSL %s/uninstall.sh | sh\n' '{{ raw_base }}'
# Keyed on what ariza_persist_path found BEFORE it ran, never on
# $PATH here: persisting exports the directory into this script's
# own PATH, so checking now would always answer "present" and this
# message would never print for the one user who needs it. The
# default is the quiet arm, so a run that never persisted (--help,
# an early exit) stays quiet.
if [ "${ARIZA_PATH_WAS_PRESENT:-1}" -eq 0 ]; then
printf '\n %s is now on your PATH for new terminals.\n' "$ARIZA_BIN_DIR"
printf ' To use %s in THIS one first, paste:\n' "$APP_EXEC"
printf '\n export PATH="%s:$PATH"\n' "$ARIZA_BIN_DIR"
fi
}
main() {
ariza_parse_args "$@"
trap ariza_cleanup EXIT HUP INT TERM
if [ "$ARIZA_PRIVATE" -eq 1 ]; then
ariza_validate_private
else
mkdir -p "$ARIZA_STATE"
fi
ariza_recover_transaction
_digest_src=''
if [ "$ARIZA_PRIVATE" -eq 1 ]; then
_slug=$(ariza_detect_slug 2>/dev/null || true)
[ -n "$_slug" ] && ariza_slug_supported "$_slug" \
|| ariza_err "no private update bundle for this platform"
ARIZA_VERSION=$ARIZA_CANDIDATE
ARIZA_TAG=$ARIZA_CANDIDATE
ARIZA_SRC="https://github.com/$APP_REPO/releases/download/$ARIZA_CANDIDATE/$APP_EXEC-$ARIZA_CANDIDATE-$_slug.tar.gz"
_digest_src="$ARIZA_SRC.sha256"
ariza_log "$APP_DISPLAY $ARIZA_CANDIDATE update for $_slug"
elif [ -n "$ARIZA_SRC" ]; then
# An explicit source bypasses GitHub completely. It is how a
# release candidate, an air-gapped copy or an offline test gets
# installed, so it accepts a plain file path as readily as a URL.
ARIZA_OVERRIDE=1
_digest_src="$ARIZA_SRC.sha256"
ARIZA_VERSION=$(ariza_version_from_name "$(basename "$ARIZA_SRC")" || true)
ariza_log "$APP_DISPLAY from $ARIZA_SRC"
else
_slug=$(ariza_detect_slug 2>/dev/null || true)
if [ -z "$_slug" ] || ! ariza_slug_supported "$_slug"; then
ariza_err "no prebuilt $APP_DISPLAY bundle for $(uname -s) $(uname -m) yet -- see https://github.com/$APP_REPO/releases for what is published"
fi
if [ -z "$ARIZA_TAG" ]; then
ARIZA_TAG=$(ariza_latest_tag || true)
[ -n "$ARIZA_TAG" ] || ariza_err "could not read the latest release tag from https://github.com/$APP_REPO/releases/latest -- pass --version to name one"
fi
ARIZA_VERSION=${ARIZA_TAG#v}
ARIZA_SRC="https://github.com/$APP_REPO/releases/download/$ARIZA_TAG/$APP_EXEC-$ARIZA_VERSION-$_slug.tar.gz"
_digest_src="$ARIZA_SRC.sha256"
ariza_log "$APP_DISPLAY $ARIZA_VERSION for $_slug"
fi
# Both arms end the same way: warm the install, then say the piece.
# Every path that reaches the parting message goes through the
# warm-up first, including the one where nothing was downloaded --
# a re-run is what somebody tries when the last one did not take.
if [ "$ARIZA_PRIVATE" -eq 0 ] && [ -n "$ARIZA_VERSION" ] && ariza_check_existing "$ARIZA_VERSION"; then
{%- if warm %}
ariza_warm
{%- endif %}
ariza_report
exit 0
fi
ariza_fetch_and_unpack "$ARIZA_SRC" "$_digest_src"
ariza_install
[ "$ARIZA_PRIVATE" -eq 1 ] && exit 0
{%- if warm %}
ariza_warm
{%- endif %}
ariza_report
}
# Everything above is a definition, and nothing has run yet: a script
# read from a pipe is executed as it arrives, so a single call at the end
# is what makes `curl … | sh` safe against a truncated download.
main "$@"