#!/bin/sh # Moneymoor installer — macOS and Linux. # # Generated by ariza from App::Moneymoor's ariza.toml. Do not edit this # file: edit ariza's resources/templates/install-posix.sh.j2 and re-run # `ariza installers --app=`. # # curl -fsSL https://raw.githubusercontent.com/m-doughty/App-Moneymoor/HEAD/install.sh | sh # curl -fsSL https://raw.githubusercontent.com/m-doughty/App-Moneymoor/HEAD/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 m-doughty/App-Moneymoor 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/moneymoor, and puts that directory on PATH # only if it is not on it already. # 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. # # Nothing is compiled, nothing needs root, and nothing outside # ${XDG_DATA_HOME:-~/.local/share}/moneymoor and ~/.local/bin is # written — bar one marked block in your shell rc files, which the # uninstaller removes again. set -eu APP_DISPLAY='Moneymoor' APP_EXEC='moneymoor' APP_REPO='m-doughty/App-Moneymoor' # The platforms App::Moneymoor 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='macos-arm64 linux-x86_64-glibc' 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 MONEYMOOR_BUNDLE_URL to install from somewhere other than the # m-doughty/App-Moneymoor releases — a local file, a release candidate, a mirror. # --url overrides it in turn. ARIZA_SRC="${MONEYMOOR_BUNDLE_URL:-}" 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 ################################## ############################################################################### # ariza installer runtime (POSIX) # # Inlined verbatim into install.sh and uninstall.sh at render time, so a # generated installer is one self-contained file with nothing to source # and nothing to download before it can start. # # Adapted from the pre-bundle installers' template/lib/common-posix.sh -- # the registry, the package-manager probes and the terminal detection are # gone (a bundle installs none of that), the PATH marker machinery is # kept, because getting it wrong duplicates blocks in people's rc files. # # Callers must have set: APP_DISPLAY, APP_EXEC, ARIZA_PATH_MARKER, # ARIZA_PATH_END. ############################################################################### # Colour only when someone is watching. `curl … | sh` in a CI log is not. if [ -t 1 ]; then ariza_c_blue='\033[34m'; ariza_c_green='\033[32m'; ariza_c_red='\033[31m' ariza_c_yellow='\033[33m'; ariza_c_reset='\033[0m' else ariza_c_blue=''; ariza_c_green=''; ariza_c_red='' ariza_c_yellow=''; ariza_c_reset='' fi ariza_log() { printf "${ariza_c_blue}==>${ariza_c_reset} %s\n" "$*"; } ariza_ok() { printf "${ariza_c_green}ok${ariza_c_reset} %s\n" "$*"; } ariza_warn() { printf "${ariza_c_yellow}!!${ariza_c_reset} %s\n" "$*" >&2; } ariza_err() { printf "${ariza_c_red}error${ariza_c_reset} %s\n" "$*" >&2; exit 1; } ariza_have() { command -v "$1" >/dev/null 2>&1; } # ---- downloading ------------------------------------------------------------- ariza_download() { # $1 = url, $2 = destination. Fails on any HTTP error rather than # writing a 404 page to disk and calling it an archive. if ariza_have curl; then curl --fail --location --silent --show-error --retry 3 -o "$2" "$1" elif ariza_have wget; then wget --quiet --tries=3 -O "$2" "$1" else ariza_err "need curl or wget to download $1" fi } # ---- checksums --------------------------------------------------------------- ariza_sha256() { # $1 = file. Echoes the lowercase hex digest, or returns 1 if this # machine has no tool that can compute one. if ariza_have sha256sum; then sha256sum "$1" | cut -d' ' -f1 elif ariza_have shasum; then shasum -a 256 "$1" | cut -d' ' -f1 else return 1 fi } ariza_first_word() { # $1 = a ` ` checksum file, as shasum -c reads. awk 'NR == 1 { print $1; exit }' "$1" } ariza_verify_sha256() { # $1 = file, $2 = expected hex. Fatal on any disagreement. _actual=$(ariza_sha256 "$1" || true) [ -n "$_actual" ] || ariza_err "no sha256sum or shasum on this machine, so the download cannot be verified" if [ "$_actual" != "$2" ]; then ariza_err "checksum mismatch for $(basename "$1"): expected $2, got $_actual -- not installing" fi } # ---- shell rc PATH wiring ---------------------------------------------------- # # One marker block, named after the executable rather than after the # project, so uninstalling one app never removes the line another app is # relying on. Re-running never duplicates it: the marker is grepped for # first, and the whole block is skipped when the directory is already on # PATH, which is the common case on a second install. ariza_persist_path() { # $1 = directory to put on PATH. # # Records whether the directory was on PATH BEFORE this function # touched anything, in ARIZA_PATH_WAS_PRESENT. The parting message # keys on that record, not on $PATH: this function's last act is to # export the directory into this script's own PATH, so a check made # after it runs always says "present" and the tell-the-user branch # becomes unreachable — which is exactly how the first real install # ended with a command the user's terminal could not find and no # line telling them why. _dir=$1 case ":${PATH:-}:" in *":$_dir:"*) # Already there for this shell, so a login shell almost # certainly has it too. Adding a block would be one more # thing to clean up later for no gain. ARIZA_PATH_WAS_PRESENT=1 return 0 ;; esac ARIZA_PATH_WAS_PRESENT=0 _block=$(printf '%s\ncase ":$PATH:" in\n *":%s:"*) ;;\n *) PATH="%s:$PATH"; export PATH ;;\nesac\n%s\n' \ "$ARIZA_PATH_MARKER" "$_dir" "$_dir" "$ARIZA_PATH_END") _rc_seen=0 for _rc in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do [ -f "$_rc" ] || continue _rc_seen=$((_rc_seen + 1)) grep -Fq "$ARIZA_PATH_MARKER" "$_rc" 2>/dev/null && continue # An unterminated last line would otherwise swallow the marker, # and a decorative blank line before the block would survive the # uninstaller — accumulating one per install/uninstall cycle. [ -n "$(tail -c 1 "$_rc")" ] && printf '\n' >>"$_rc" printf '%s\n' "$_block" >>"$_rc" ariza_ok "added $_dir to PATH in $_rc" done if [ "$_rc_seen" -eq 0 ]; then ariza_warn "no shell rc file found -- add this to yours by hand:" printf ' export PATH="%s:$PATH"\n' "$_dir" >&2 fi PATH="$_dir:$PATH" export PATH } ariza_unpersist_path() { # Remove our own marker block, and only ours, from every rc file that # has one. Rewritten through the existing file rather than moved over # it, so the rc file keeps its mode and ownership. for _rc in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do [ -f "$_rc" ] || continue grep -Fq "$ARIZA_PATH_MARKER" "$_rc" 2>/dev/null || continue _tmp=$(mktemp "${TMPDIR:-/tmp}/ariza-rc.XXXXXX") awk -v m="$ARIZA_PATH_MARKER" -v em="$ARIZA_PATH_END" ' $0 == m { skip = 1; next } $0 == em { skip = 0; next } !skip { print } ' "$_rc" >"$_tmp" cat "$_tmp" >"$_rc" rm -f "$_tmp" ariza_ok "removed the PATH block from $_rc" done } ############################################################################### ariza_usage() { cat </ 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 "${MONEYMOOR_BUNDLE_URL-}" ] \ || ariza_err "MONEYMOOR_BUNDLE_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 # --. 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":"moneymoor"' "$_compact" \ || ariza_err "private update bundle is for a different application" grep -Fq '"name":"App::Moneymoor"' "$_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":"m-doughty/App-Moneymoor"' "$_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/ directory that does not contain 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}--" 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" } 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='--version' ariza_log 'warming up -- the first launch does the work the rest never repeat' if "$_warm_bin" '--version' >/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 } 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' 'https://raw.githubusercontent.com/m-doughty/App-Moneymoor/HEAD' # 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 ariza_warm ariza_report exit 0 fi ariza_fetch_and_unpack "$ARIZA_SRC" "$_digest_src" ariza_install [ "$ARIZA_PRIVATE" -eq 1 ] && exit 0 ariza_warm 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 "$@"