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 "$@"