#!/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."