#!/bin/sh # loadupd.sh - fetch the newest upd binary for this machine. # # The one thing upd cannot do for itself: `upd --update` needs an upd to run # it. Everything after this first one is upd's own job. # # ./loadupd.sh # into the current directory # ./loadupd.sh ~/bin # into a directory of your choosing # # Plain POSIX sh, and nothing beyond uname, sed, grep and one of curl or wget: # it has to run on the machines upd exists for, before upd is on them. set -e REPO=${UPD_REPO_URL:-https://git.micw.org/mike/upd} NAME=upd DEST=${1:-.} die() { echo "loadupd: $*" >&2 exit 1 } # https://host/owner/repo -> https://host/api/v1/repos/owner/repo api=$(echo "$REPO" | sed -e 's#/*$##' -e 's#\.git$##' \ -e 's#^\(https\{0,1\}://[^/]*\)/\(.*\)$#\1/api/v1/repos/\2#') case "$api" in */api/v1/repos/*/*) ;; *) die "cannot read $REPO, expected https://host/owner/repo" ;; esac os=$(uname -s | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz') case "$os" in darwin | linux | freebsd) ;; *) die "unsupported system: $(uname -s)" ;; esac # The names are Go's, not uname's, because that is what the release assets are # called. case "$(uname -m)" in x86_64 | amd64) arch=amd64 ;; aarch64 | arm64) arch=arm64 ;; i386 | i686) arch=386 ;; armv7* | armv6*) arch=arm ;; *) die "unsupported architecture: $(uname -m)" ;; esac asset="$NAME-$os-$arch" # curl on an old machine may be the very thing that cannot speak TLS 1.2 any # more - which is half the reason upd exists - so wget gets its turn before we # give up. fetch() { # url outfile if command -v curl >/dev/null 2>&1; then curl -fsSL -o "$2" "$1" && return 0 fi if command -v wget >/dev/null 2>&1; then wget -q -O "$2" "$1" && return 0 fi return 1 } tmp=$(mktemp -d "${TMPDIR:-/tmp}/loadupd.XXXXXX") || die "cannot create a temporary directory" trap 'rm -rf "$tmp"' EXIT INT TERM echo "Looking for the newest release of $NAME ..." fetch "$api/releases/latest" "$tmp/release.json" || die "cannot reach $api If the TLS handshake failed, this machine is too old to fetch it itself - copy $asset over from somewhere else." tag=$(sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$tmp/release.json" | head -n 1) [ -n "$tag" ] || die "no release found in $REPO" # One asset per line, then pick ours by name. The JSON arrives as one long # line, so the comma is what separates the fields. urls=$(tr ',' '\n' <"$tmp/release.json" | sed -n 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') url=$(echo "$urls" | grep "/$asset\$" | head -n 1) if [ -z "$url" ]; then die "release $tag has no $asset (only: $(echo "$urls" | sed 's#.*/##' | tr '\n' ' '))" fi echo "Downloading $asset $tag ..." fetch "$url" "$tmp/$NAME" || die "download failed: $url" chmod 755 "$tmp/$NAME" # A truncated file or one built for the wrong platform shows up here, not on # first use. "$tmp/$NAME" --version >/dev/null 2>&1 || die "the downloaded binary does not run" [ -d "$DEST" ] || mkdir -p "$DEST" || die "cannot create $DEST" # Spelled out in full: "." says nothing in a message, and even less in the # comparison against $PATH below. dir=$(cd "$DEST" && pwd) || die "cannot use $DEST" target="$dir/$NAME" # Via a file alongside, so that replacing an upd that is currently running is # one atomic step and not half a copy. cp "$tmp/$NAME" "$target.new.$$" || die "no write permission in $dir" chmod 755 "$target.new.$$" mv "$target.new.$$" "$target" || { rm -f "$target.new.$$" die "cannot replace $target" } echo "$NAME $tag installed at $target" case ":$PATH:" in *":$dir:"*) ;; *) echo "Note: $dir is not in \$PATH." ;; esac echo "From here on: $NAME --update"