#!/bin/sh # Installs Sill: # # curl -fsSL https://sill.burakbuilds.dev/install.sh | sh # # What it does, in order (nothing is installed until every check passes): # 1. reads latest.json from the download host (version, archive, SHA-256, # minimum macOS) and checks this Mac meets the minimum # 2. downloads the archive and checks its SHA-256 # 3. unpacks it and checks the app's code signature: valid, made by team # UV5582B9AL, identifier app.sill.Sill — so a file swapped on the # host cannot install anybody else's app # 4. puts Sill.app in /Applications (or ~/Applications if that isn't # writable) and opens it # To update later, run the same command again. # # Environment, all optional: SILL_INSTALL_DIR (where to install), # SILL_NO_OPEN=1 (don't launch it), SILL_DOWNLOADS_URL (download host). # # Written for /bin/sh, and wrapped in main() so that a download cut short # runs nothing at all. main() { set -eu base="${SILL_DOWNLOADS_URL:-https://sill.burakbuilds.dev}" base="${base%/}" team="UV5582B9AL" identifier="${SILL_EXPECT_IDENTIFIER:-app.sill.Sill}" say() { printf '\033[1m==>\033[0m %s\n' "$*"; } fail() { printf '\033[31mSill install: %s\033[0m\n' "$*" >&2; exit 1; } [ "$(uname -s)" = "Darwin" ] || fail "Sill is a Mac app." for tool in curl shasum ditto codesign plutil; do command -v "$tool" >/dev/null 2>&1 || fail "missing $tool" done tmp="$(mktemp -d "${TMPDIR:-/tmp}/sill-install.XXXXXX")" trap 'rm -rf "$tmp"' EXIT INT TERM fetch() { curl -fsSL --proto '=https' --tlsv1.2 --retry 2 -o "$2" "$1"; } # Tests may point at a local http server; anything else must be https. case "$base" in https://*) ;; http://127.0.0.1:*) fetch() { curl -fsSL --retry 2 -o "$2" "$1"; } ;; *) fail "download host must be https: $base" ;; esac say "Checking the latest Sill" fetch "$base/latest.json" "$tmp/latest.json" || fail "could not reach $base" field() { plutil -extract "$1" raw -o - "$tmp/latest.json" 2>/dev/null; } version="$(field version)" || fail "latest.json has no version" archive="$(field archive)" || fail "latest.json has no archive" sha="$(field sha256)" || fail "latest.json has no sha256" minimum="$(field minimumSystemVersion)" || fail "latest.json has no minimumSystemVersion" printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || fail "odd version in latest.json" printf '%s' "$archive" | grep -Eq '^updates/Sill-[0-9]+\.[0-9]+\.[0-9]+\.zip$' || fail "odd archive path in latest.json" printf '%s' "$sha" | grep -Eq '^[0-9a-f]{64}$' || fail "odd checksum in latest.json" macos="$(sw_vers -productVersion)" at_least() { # $1 >= $2, dotted versions awk -v a="$1" -v b="$2" 'BEGIN { n = split(a, x, "."); m = split(b, y, "."); k = n > m ? n : m for (i = 1; i <= k; i++) { p = (i <= n) ? x[i] + 0 : 0; q = (i <= m) ? y[i] + 0 : 0 if (p > q) exit 0; if (p < q) exit 1 } exit 0 }' } at_least "$macos" "$minimum" || fail "Sill $version needs macOS $minimum or later (this Mac has $macos)." say "Downloading Sill $version" fetch "$base/$archive" "$tmp/Sill.zip" || fail "download failed" [ "$(shasum -a 256 "$tmp/Sill.zip" | cut -d' ' -f1)" = "$sha" ] || fail "checksum mismatch — download corrupted or tampered with" mkdir "$tmp/x" ditto -x -k "$tmp/Sill.zip" "$tmp/x" || fail "could not unpack" app="$tmp/x/Sill.app" [ -d "$app" ] || fail "the archive holds no Sill.app" say "Verifying the signature" codesign --verify --deep --strict "$app" 2>/dev/null || fail "Sill.app's code signature is invalid" details="$(codesign -dv "$app" 2>&1)" printf '%s\n' "$details" | grep -qx "TeamIdentifier=$team" || fail "Sill.app is not signed by team $team" printf '%s\n' "$details" | grep -qx "Identifier=$identifier" || fail "this is not Sill ($identifier)" dir="${SILL_INSTALL_DIR:-/Applications}" if [ -z "${SILL_INSTALL_DIR:-}" ] && [ ! -w "$dir" ]; then dir="$HOME/Applications" fi mkdir -p "$dir" || fail "cannot create $dir" dest="$dir/Sill.app" if pgrep -f "^$dest/Contents/MacOS/Sill\$" >/dev/null 2>&1; then fail "Sill is running from $dest. Quit it (Sill ▸ Settings ▸ Quit), then run this again." fi say "Installing to $dest" staged="$dir/.Sill.app.installing.$$" rm -rf "$staged" ditto "$app" "$staged" || { rm -rf "$staged"; fail "cannot write to $dir"; } if [ -e "$dest" ]; then mv "$dest" "$tmp/previous.app" || { rm -rf "$staged"; fail "cannot replace $dest (is it yours? try: sudo rm -rf \"$dest\")"; } fi if ! mv "$staged" "$dest"; then [ -e "$tmp/previous.app" ] && mv "$tmp/previous.app" "$dest" rm -rf "$staged" fail "could not move Sill.app into $dir" fi printf '\n\033[1mSill %s is installed\033[0m in %s. Move the pointer to the notch.\n' "$version" "$dir" if [ "${SILL_NO_OPEN:-}" != "1" ]; then open "$dest" || true fi } main "$@"