#!/bin/bash # Cedar installer. Installs the latest release into ~/Applications, or updates an existing install. # curl -fsSL https://cedar.kartikkabadi.com/install.sh | bash # The download is checked against the SHA-256 in the release feed before anything is installed. # Environment, for testing: CEDAR_FEED_URL overrides the feed, CEDAR_INSTALL_DIR the folder (default ~/Applications), # and CEDAR_NO_LAUNCH=1 installs without opening the app. set -euo pipefail FEED_URL="${CEDAR_FEED_URL:-https://cedar.kartikkabadi.com/releases/latest.json}" BUNDLE_ID="dev.cedar.local" APPS="${CEDAR_INSTALL_DIR:-$HOME/Applications}" # Every release is signed with the same self-signed certificate; this is its SHA-1. A download signed by # anything else is not ours, even if its checksum matches a tampered feed. CERT_ROOT="5a69b4875d8c3e9eb59fc8ebd76cdd42a737e75c" DEST="$APPS/Cedar.app" WORK="" say() { printf '==> %s\n' "$1"; } die() { printf 'error: %s\n' "$1" >&2; exit 1; } cleanup() { if [ -n "$WORK" ]; then rm -rf "$WORK"; fi } trap cleanup EXIT # Reads one key from a JSON or plist file. plutil ships with macOS, so nothing else is needed. read_value() { local value value=$(/usr/bin/plutil -extract "$1" raw -o - "$2" 2>/dev/null) || return 1 printf '%s' "$value" } # True when dotted version $1 is at least $2, such as 14.5.1 and 13.0. Works in the bash 3.2 that macOS ships. version_at_least() { local IFS=. # shellcheck disable=SC2206 local -a have=($1) need=($2) local i h n for ((i = 0; i < ${#have[@]} || i < ${#need[@]}; i++)); do h=${have[i]:-0} n=${need[i]:-0} if ((10#$h > 10#$n)); then return 0; fi if ((10#$h < 10#$n)); then return 1; fi done return 0 } WORK=$(mktemp -d "${TMPDIR:-/tmp}/cedar-install.XXXXXX") say "Checking for the latest Cedar" MANIFEST="$WORK/latest.json" curl -fsSL --max-time 30 -o "$MANIFEST" "$FEED_URL" || die "could not reach the release feed. Check your connection and try again." VERSION=$(read_value version "$MANIFEST") || die "the release feed has no version" URL=$(read_value url "$MANIFEST") || die "the release feed has no download URL" SHA=$(read_value sha256 "$MANIFEST" | tr '[:upper:]' '[:lower:]') || die "the release feed has no SHA-256" MINIMUM=$(read_value minimumSystemVersion "$MANIFEST") || die "the release feed has no minimum macOS version" [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+)*$ ]] || die "the release feed has an invalid version" [[ "$MINIMUM" =~ ^[0-9]+(\.[0-9]+)*$ ]] || die "the release feed has an invalid minimum macOS version" # file:// downloads are only for testing a local feed, so a remote feed can never point at a local file. case "$URL" in https://*) ;; file://*) case "$FEED_URL" in file://*) ;; *) die "the release feed has an insecure download URL, so nothing was installed" ;; esac ;; *) die "the release feed has an insecure download URL, so nothing was installed" ;; esac if [[ ! "$SHA" =~ ^[0-9a-f]+$ ]] || [ "${#SHA}" -ne 64 ]; then die "the release feed has an invalid SHA-256" fi RUNNING=$(sw_vers -productVersion) if ! version_at_least "$RUNNING" "$MINIMUM"; then die "Cedar $VERSION needs macOS $MINIMUM or later. This Mac runs macOS $RUNNING." fi say "Downloading Cedar $VERSION" ZIP="$WORK/Cedar.zip" curl -fsSL --retry 2 --max-time 600 -o "$ZIP" "$URL" || die "the download failed. Nothing was installed." ACTUAL=$(shasum -a 256 "$ZIP" | awk '{print $1}') if [ "$ACTUAL" != "$SHA" ]; then die "the download does not match the published checksum, so nothing was installed" fi say "Verifying the app" STAGE="$WORK/stage" mkdir "$STAGE" ditto -x -k "$ZIP" "$STAGE" || die "could not unpack the download" NEW_APP="$STAGE/Cedar.app" [ -d "$NEW_APP" ] || die "the download does not contain Cedar.app" PLIST="$NEW_APP/Contents/Info.plist" [ "$(read_value CFBundleIdentifier "$PLIST")" = "$BUNDLE_ID" ] || die "the download is not Cedar" [ "$(read_value CFBundleShortVersionString "$PLIST")" = "$VERSION" ] || die "the app's version does not match the release feed" codesign --verify --deep --strict "$NEW_APP" 2>/dev/null || die "the app's signature is damaged, so nothing was installed" codesign -d -r- "$NEW_APP" 2>&1 | grep -qiE "certificate (root|leaf) = H\"$CERT_ROOT\"" \ || die "the app is not signed by Cedar's release certificate, so nothing was installed" if pgrep -f "$DEST/Contents/MacOS/Cedar" >/dev/null 2>&1; then say "Quitting the running Cedar" pkill -f "$DEST/Contents/MacOS/Cedar" >/dev/null 2>&1 || true sleep 1 fi say "Installing to $DEST" mkdir -p "$APPS" # Copy beside the old app, clear quarantine, then swap so a failed install leaves the working app in place. PENDING="$APPS/.Cedar-installing.app" OLD="$APPS/.Cedar-previous.app" if [ -e "$OLD" ]; then die "a previous install left $OLD behind. Move it back to $DEST or keep it aside, then rerun." fi rm -rf "$PENDING" if ! ditto "$NEW_APP" "$PENDING"; then rm -rf "$PENDING" die "could not copy the app into $APPS" fi if ! xattr -dr com.apple.quarantine "$PENDING" 2>/dev/null; then rm -rf "$PENDING" die "could not prepare the app to open. Nothing was installed; try again or contact support." fi if [ -e "$DEST" ] && ! mv "$DEST" "$OLD"; then rm -rf "$PENDING" die "could not replace the existing app in $APPS" fi if ! mv "$PENDING" "$DEST"; then # Put the previous app back so the Mac is not left without one. if [ -e "$OLD" ]; then mv "$OLD" "$DEST" || die "could not install and could not restore the previous app; it is at $OLD" fi rm -rf "$PENDING" die "could not install the new app into $APPS" fi if [ -z "${CEDAR_NO_LAUNCH:-}" ]; then say "Opening Cedar $VERSION" open "$DEST" || true sleep 2 if ! pgrep -f "$DEST/Contents/MacOS/Cedar" >/dev/null 2>&1; then if [ -e "$OLD" ]; then rm -rf "$DEST" mv "$OLD" "$DEST" || die "Cedar $VERSION did not launch, and the previous app could not be restored; it is at $OLD" say "Reopening the previous version" open "$DEST" || true die "Cedar $VERSION did not stay running, so the previous app was restored." fi die "Cedar $VERSION did not stay running. Try opening '$DEST' yourself." fi fi rm -rf "$OLD" if [ -n "${CEDAR_NO_LAUNCH:-}" ]; then say "Installed Cedar $VERSION" fi if [ -z "${CEDAR_NO_LAUNCH:-}" ]; then printf 'Done. Cedar is open. On a first install, a short welcome helps you bring your tabs, bookmarks and passwords over.\n' fi if [ "$APPS" != "/Applications" ] && [ -d "/Applications/Cedar.app" ]; then printf 'Note: an older copy is also in /Applications. Quit it and move it to the Trash so only one runs.\n' fi