diff --git a/README.md b/README.md index 9bcd24e..cfb88ed 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,20 @@ version from `main.go`. `../t/corpus`, the same corpus `t/select.t` uses, and fails if the two implementations start disagreeing. +## The first one + +`upd --update` needs an upd to run it. `loadupd.sh` fetches that first one - +POSIX sh, curl or wget, no upd required: + +```sh +./loadupd.sh # into the current directory +./loadupd.sh ~/bin # into a directory of your choosing +``` + +It reads the newest release, picks the asset matching `uname -s`/`uname -m`, +runs it once before putting it in place, and says if the directory is not in +`$PATH`. `$UPD_REPO_URL` points it at a mirror. From there on upd takes over. + ## Updating itself upd installs itself the same way it installs everything else - same forge diff --git a/loadupd.sh b/loadupd.sh new file mode 100755 index 0000000..e035728 --- /dev/null +++ b/loadupd.sh @@ -0,0 +1,111 @@ +#!/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"