#!/bin/sh # Lamport Build for the Mac, installed from the terminal: # curl -fsSL https://lamport.si/install | bash # # What it does: reads the current release from the update feed, downloads the signed zip, checks the # signature is Lamport Robotics', puts "Lamport Build.app" in /Applications and opens it. The same build # the app later updates itself with (Sparkle), so nothing here differs from the dmg. # # Knobs: INSTALL_DIR=/some/dir (default /Applications), NO_OPEN=1 (do not launch), VERSION=0.2.0 (pin). set -eu FEED="https://lamport.si/mac/appcast.xml" APP_NAME="Lamport Build.app" INSTALL_DIR="${INSTALL_DIR:-/Applications}" say() { printf '%s\n' "$*"; } die() { printf 'lamport: %s\n' "$*" >&2; exit 1; } [ "$(uname -s)" = "Darwin" ] || die "Lamport Build is a Mac app. On Linux and Windows use https://lamport.si/build in the browser." [ "$(uname -m)" = "arm64" ] || die "this build is for Apple silicon only (M1 and later)." OSV=$(sw_vers -productVersion 2>/dev/null || echo 0) case "$OSV" in 1[0-3].*) die "macOS 14 or newer is needed (this Mac runs $OSV).";; esac command -v curl >/dev/null || die "curl is missing." TMP=$(mktemp -d /tmp/lamport-install.XXXXXX) trap 'rm -rf "$TMP"' EXIT say "reading the release feed" curl -fsSL "$FEED" -o "$TMP/appcast.xml" || die "could not read $FEED" # items are newest first; the first enclosure is the current release unless VERSION pins one if [ -n "${VERSION:-}" ]; then ITEM=$(awk -v v="$VERSION" 'index($0,""){buf=""} {buf=buf $0 "\n"} index($0,""){ if (index(buf,"" v "<")) {print buf; exit} }' "$TMP/appcast.xml") [ -n "$ITEM" ] || die "version $VERSION is not in the feed." else ITEM=$(awk 'index($0,""){on=1} on{buf=buf $0 "\n"} index($0,""){print buf; exit}' "$TMP/appcast.xml") fi URL=$(printf '%s' "$ITEM" | sed -n 's/.*]*url="\([^"]*\)".*/\1/p' | head -n1) LEN=$(printf '%s' "$ITEM" | sed -n 's/.*]*length="\([0-9]*\)".*/\1/p' | head -n1) VER=$(printf '%s' "$ITEM" | sed -n 's/.*\([^<]*\)<.*/\1/p' | head -n1) [ -n "$URL" ] || die "the feed has no download." case "$URL" in https://lamport.si/*) ;; *) die "unexpected download host in the feed: $URL";; esac say "downloading Lamport Build ${VER:-} " curl -fL --progress-bar "$URL" -o "$TMP/app.zip" || die "download failed." if [ -n "$LEN" ]; then GOT=$(stat -f %z "$TMP/app.zip") [ "$GOT" = "$LEN" ] || die "download is $GOT bytes, the feed says $LEN; try again." fi say "unpacking" ditto -x -k "$TMP/app.zip" "$TMP/out" || die "could not unpack the archive." [ -d "$TMP/out/$APP_NAME" ] || die "the archive does not contain $APP_NAME." say "checking the signature" codesign --verify --strict --deep "$TMP/out/$APP_NAME" 2>/dev/null || die "the app's signature does not verify; not installing." AUTH=$(codesign -dvv "$TMP/out/$APP_NAME" 2>&1 | sed -n 's/^Authority=//p' | head -n1) case "$AUTH" in *"Lamport Robotics"*) ;; *) die "the app is signed by \"$AUTH\", not Lamport Robotics; not installing.";; esac [ -d "$INSTALL_DIR" ] || mkdir -p "$INSTALL_DIR" 2>/dev/null || die "cannot create $INSTALL_DIR." [ -w "$INSTALL_DIR" ] || die "no permission to write to $INSTALL_DIR (try INSTALL_DIR=\$HOME/Applications)." DEST="$INSTALL_DIR/$APP_NAME" if [ -d "$DEST" ]; then if pgrep -xq "Lamport Build" 2>/dev/null; then say "closing the running copy" osascript -e 'tell application "Lamport Build" to quit' >/dev/null 2>&1 || true sleep 1 fi rm -rf "$DEST" fi mv "$TMP/out/$APP_NAME" "$DEST" # a terminal download carries no quarantine flag; clear it anyway so Gatekeeper does not hold a signed app xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true say "installed $DEST${VER:+ ($VER)}" if [ -z "${NO_OPEN:-}" ]; then open "$DEST" || say "open it from $INSTALL_DIR" fi say "updates arrive inside the app from now on."