#!/bin/sh # Install oxob (beta) on this Mac: # # curl -fsSL https://oxobagents.com/install.sh | sh # # What it does, and nothing more: checks this is an Apple Silicon Mac; reads # which release is current from latest.json, over HTTPS, into a temporary # folder; checks macOS is new enough for it; downloads its .dmg and checks its # SHA-256; copies oxob.app into /Applications (or ~/Applications when you cannot # write there); opens oxob. No sudo, no password. Run it again to update. This # file holds no version or checksum, so a release needs no change to it. set -eu FEED=https://downloads.oxobagents.com/release/ SYSTEM_APPS=/Applications USER_APPS="$HOME/Applications" stop() { printf '%s\n' "$1" >&2 exit 1 } bad_feed() { stop "The release information is damaged. Nothing was changed; try again later." } # Which computer, before anything else: another one stops here, before plutil is # needed. Apple Silicon: an Intel Mac has no hw.optional.arm64; uname -m says # x86_64 in a Rosetta Terminal, so it is not asked. check_computer() { [ "$(uname -s)" = Darwin ] || stop "oxob is not available for this computer yet." [ "$(sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" = 1 ] || stop "oxob is not available for this computer yet." } # A temporary folder for everything downloaded, removed (and the image closed) on any exit. make_tmp() { tmp=$(mktemp -d) trap 'hdiutil detach -force "$tmp/mnt" >/dev/null 2>&1 || true; rm -rf "$tmp"' EXIT trap 'exit 130' INT TERM } # Which release: latest.json names it. Only strings count: plutil's raw gives a # dictionary's key or an array's length otherwise. The letters are spelled out # because a UTF-8 locale lets a range such as A-Z match accented ones. file ends # up in a path and a URL, and . and .. are not files. read_feed() { curl -fsSL --proto '=https' --tlsv1.2 -o "$tmp/latest.json" "${FEED}latest.json" || stop "Could not read the release information. Nothing was changed; try again." version=$(plutil -extract version raw -expect string -o - "$tmp/latest.json" 2>/dev/null) || bad_feed file=$(plutil -extract file raw -expect string -o - "$tmp/latest.json" 2>/dev/null) || bad_feed sha256=$(plutil -extract sha256 raw -expect string -o - "$tmp/latest.json" 2>/dev/null) || bad_feed min_macos=$(plutil -extract min_macos raw -expect string -o - "$tmp/latest.json" 2>/dev/null) || bad_feed safe='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-' case $version in '' | *[!$safe]*) bad_feed ;; esac case $file in '' | .* | *[!$safe]*) bad_feed ;; esac case $sha256 in *[!0123456789abcdef]*) bad_feed ;; esac [ "${#sha256}" -eq 64 ] || bad_feed case $min_macos in '' | *[!0123456789]*) bad_feed ;; esac } # A new enough macOS for this release: min_macos is a major version. check_macos() { major=$(sw_vers -productVersion | cut -d. -f1) case $major in '' | *[!0123456789]*) stop "oxob is not available for this computer yet." ;; esac [ "$major" -ge "$min_macos" ] || stop "oxob is not available for this computer yet." } # Where, before the app is downloaded: an existing oxob.app is replaced where it # is, if this user can (the folder writable and the app their own); else # /Applications when writable; else ~/Applications. Renaming the app needs write # permission on it (F10). choose_target() { for dir in "$SYSTEM_APPS" "$USER_APPS"; do [ -e "$dir/oxob.app" ] || continue if [ -w "$dir" ] && [ -O "$dir/oxob.app" ] && [ -w "$dir/oxob.app" ]; then target=$dir return fi stop "oxob is installed in $dir by another user: ask an administrator to update or remove it." done if [ -w "$SYSTEM_APPS" ]; then target=$SYSTEM_APPS; else target=$USER_APPS; fi } # A running oxob is never replaced: only this copy counts, not another account's. check_not_running() { # -a: without it pgrep leaves out its own ancestors, so a terminal inside oxob would miss oxob. if pgrep -af "$target/oxob.app/Contents/MacOS/oxob" >/dev/null 2>&1; then stop "Quit oxob first, then run this again." fi } # Download the release into the temporary folder, check it and open the image. download() { printf 'Downloading oxob %s...\n' "$version" curl -fSL --proto '=https' --tlsv1.2 --progress-bar -o "$tmp/$file" "$FEED$file" [ "$(shasum -a 256 "$tmp/$file" | cut -d' ' -f1)" = "$sha256" ] || stop "The download is damaged (its checksum does not match). Nothing was changed; try again." hdiutil attach -nobrowse -readonly -mountpoint "$tmp/mnt" "$tmp/$file" >/dev/null } # Replace: the new copy is complete before the old one moves aside; on a failed # swap the old one comes back. ditto merges into an existing folder and mv onto # one nests inside it, so leftovers from a cut-off run go first. replace() { mkdir -p "$target" rm -rf "$target/oxob.app.new" "$target/oxob.app.old" ditto "$1" "$target/oxob.app.new" if [ -e "$target/oxob.app" ]; then mv "$target/oxob.app" "$target/oxob.app.old"; fi if ! mv "$target/oxob.app.new" "$target/oxob.app"; then if [ -e "$target/oxob.app.old" ]; then mv "$target/oxob.app.old" "$target/oxob.app"; fi stop "The new oxob could not be put in place; the old one is back." fi rm -rf "$target/oxob.app.old" || true } # All work in one function called on the last line: a download of this script # that is cut off midway runs nothing. main() { check_computer make_tmp read_feed check_macos choose_target check_not_running download replace "$tmp/mnt/oxob.app" open "$target/oxob.app" printf 'oxob %s is installed in %s and opening.\n' "$version" "$target" } main