#!/bin/sh # # evoglyph installer — https://evoglyph.com/install.sh # # One-line developer install for the evoglyph macOS dictation app: # # curl -fsSL https://evoglyph.com/install.sh | sh # # This script is meant to be read before you run it. It: # - downloads the latest notarized, code-signed evoglyph build, # - verifies its SHA-256 against the signed release manifest, # - verifies the Apple code signature, notarization, and Team ID, # - and only then installs evoglyph.app into /Applications. # # It is an ALTERNATIVE to the .dmg download at https://evoglyph.com/#download # for people who prefer the terminal. Nothing here is privileged unless your # install prefix needs it; in a piped (no-TTY) run it never silently escalates. # # evoglyph requires Apple Silicon (M-series) and macOS 14 (Sonoma) or later. # # Flags / environment overrides: # -h, --help Show usage and exit. # --version X Install a specific version instead of the latest. # (env: EVOGLYPH_VERSION) # --prefix DIR Install directory (default /Applications). # (env: EVOGLYPH_PREFIX) # --no-verify Skip ONLY the code-signature / notarization checks # (the SHA-256 check still runs). For testing against # self-signed local builds. (env: EVOGLYPH_NO_VERIFY=1) # --no-open Do not print/launch hints that open the app. # EVOGLYPH_MANIFEST_URL Override the release manifest URL # (default https://downloads.evoglyph.com/latest.json). # set -eu # ── Constants ─────────────────────────────────────────────────────────────── DEFAULT_MANIFEST_URL="https://downloads.evoglyph.com/latest.json" DEFAULT_PREFIX="/Applications" APP_NAME="evoglyph.app" EXPECTED_TEAM_ID="QEZY9653ME" DOWNLOAD_PAGE="https://evoglyph.com/#download" CANONICAL_URL="https://evoglyph.com/install.sh" # ── Output helpers ────────────────────────────────────────────────────────── info() { printf '%s\n' "$*"; } warn() { printf 'warning: %s\n' "$*" >&2; } # die: print an error plus a pointer to the DMG, then exit non-zero. die() { printf 'error: %s\n' "$*" >&2 printf 'If this keeps failing, download the .dmg instead: %s\n' "$DOWNLOAD_PAGE" >&2 exit 1 } # ── Usage ─────────────────────────────────────────────────────────────────── usage() { cat </dev/null 2>&1; then _missing="$_missing $_t" fi done if [ -n "$_missing" ]; then die "missing required tool(s):$_missing (all ship with macOS — is this a Mac?)" fi } check_platform() { _os="$(uname -s)" if [ "$_os" != "Darwin" ]; then die "evoglyph is a macOS app; this looks like '$_os'." fi _arch="$(uname -m)" if [ "$_arch" != "arm64" ]; then die "evoglyph requires Apple Silicon (M-series). Intel Macs are not supported." fi _osver="$(sw_vers -productVersion 2>/dev/null || echo 0)" _major="${_osver%%.*}" case "$_major" in '' | *[!0-9]*) die "could not determine macOS version (got '$_osver')." ;; esac if [ "$_major" -lt 14 ]; then die "evoglyph requires macOS 14 (Sonoma) or later; this is macOS $_osver." fi } require_tools check_platform # ── Workdir + cleanup trap ────────────────────────────────────────────────── WORKDIR="$(mktemp -d "${TMPDIR:-/tmp}/evoglyph-install.XXXXXX")" \ || die "could not create a temporary working directory." cleanup() { [ -n "${WORKDIR:-}" ] && rm -rf "$WORKDIR" } trap cleanup EXIT INT TERM # ── Fetch + parse the release manifest ────────────────────────────────────── MANIFEST_FILE="$WORKDIR/latest.json" info "Fetching release manifest: $MANIFEST_URL" curl -fsSL "$MANIFEST_URL" -o "$MANIFEST_FILE" \ || die "could not download the release manifest from $MANIFEST_URL (it may not be published yet)." M_VERSION="$(manifest_field "$MANIFEST_FILE" version)" M_MIN_MACOS="$(manifest_field "$MANIFEST_FILE" min_macos)" M_ARCH="$(manifest_field "$MANIFEST_FILE" arch)" M_TEAM_ID="$(manifest_field "$MANIFEST_FILE" team_id)" M_ZIP_URL="$(manifest_field "$MANIFEST_FILE" zip_url)" M_ZIP_SHA="$(manifest_field "$MANIFEST_FILE" zip_sha256)" [ -n "$M_VERSION" ] || die "manifest is missing 'version'." [ -n "$M_ZIP_URL" ] || die "manifest is missing 'zip_url'." # Re-check the machine against the manifest's declared target. if [ -n "$M_ARCH" ] && [ "$M_ARCH" != "$(uname -m)" ]; then die "release targets arch '$M_ARCH' but this machine is '$(uname -m)'." fi if [ -n "$M_MIN_MACOS" ]; then _mmaj="${M_MIN_MACOS%%.*}" _cmaj="$(sw_vers -productVersion | cut -d. -f1)" case "$_mmaj" in '' | *[!0-9]*) : ;; # unparseable min — already enforced >=14 above *) if [ "$_cmaj" -lt "$_mmaj" ]; then die "release requires macOS $M_MIN_MACOS or later; this is $(sw_vers -productVersion)." fi ;; esac fi # ── Resolve the zip URL + expected hash (manifest vs. --version override) ──── ZIP_URL="$M_ZIP_URL" ZIP_SHA="$M_ZIP_SHA" INSTALL_VERSION="$M_VERSION" if [ -n "$VERSION_OVERRIDE" ] && [ "$VERSION_OVERRIDE" != "$M_VERSION" ]; then # Derive the target URL by substituting the requested version into the # manifest's zip_url, replacing the manifest's own version token. We never # install an explicitly-requested version without a verifiable hash, so we # require a matching `.sha256` sidecar to be fetchable. case "$M_ZIP_URL" in *"$M_VERSION"*) ZIP_URL="$(printf '%s' "$M_ZIP_URL" | sed "s/$M_VERSION/$VERSION_OVERRIDE/g")" ;; *) die "cannot derive a URL for version $VERSION_OVERRIDE — the manifest zip_url does not contain its own version token." ;; esac INSTALL_VERSION="$VERSION_OVERRIDE" info "Requested version $VERSION_OVERRIDE; fetching its checksum sidecar ..." SIDECAR_FILE="$WORKDIR/sidecar.sha256" curl -fsSL "${ZIP_URL}.sha256" -o "$SIDECAR_FILE" \ || die "no checksum sidecar at ${ZIP_URL}.sha256 — refusing to install version $VERSION_OVERRIDE unverified." # The sidecar is either "" or " filename"; take the first field. ZIP_SHA="$(awk 'NR==1{print $1}' "$SIDECAR_FILE")" [ -n "$ZIP_SHA" ] || die "checksum sidecar at ${ZIP_URL}.sha256 was empty." fi [ -n "$ZIP_SHA" ] || die "no SHA-256 available for the build — refusing to install unverified." # ── Download the build ────────────────────────────────────────────────────── ZIP_FILE="$WORKDIR/evoglyph.zip" info "Downloading evoglyph $INSTALL_VERSION ..." info " $ZIP_URL" curl -fL --progress-bar "$ZIP_URL" -o "$ZIP_FILE" \ || die "download failed from $ZIP_URL." # ── Verify: SHA-256 (always) ───────────────────────────────────────────────── info "Verifying checksum ..." ACTUAL_SHA="$(shasum -a 256 "$ZIP_FILE" | awk '{print $1}')" if [ "$(lower "$ACTUAL_SHA")" != "$(lower "$ZIP_SHA")" ]; then die "checksum mismatch — refusing to install. expected: $ZIP_SHA actual: $ACTUAL_SHA" fi info " checksum OK ($ACTUAL_SHA)" # ── Extract + locate the app ───────────────────────────────────────────────── EXTRACT_DIR="$WORKDIR/extracted" mkdir -p "$EXTRACT_DIR" info "Extracting ..." ditto -x -k "$ZIP_FILE" "$EXTRACT_DIR" \ || die "could not extract the downloaded archive (it may be corrupt)." APP_PATH="" if [ -d "$EXTRACT_DIR/$APP_NAME" ]; then APP_PATH="$EXTRACT_DIR/$APP_NAME" else # Some zips nest the app one level down; find the first matching bundle. APP_PATH="$(find "$EXTRACT_DIR" -maxdepth 3 -type d -name "$APP_NAME" 2>/dev/null | head -n 1)" fi [ -n "$APP_PATH" ] && [ -d "$APP_PATH" ] \ || die "could not find $APP_NAME inside the downloaded archive." # ── Verify: code signature, notarization, Team ID (unless --no-verify) ─────── if [ "$NO_VERIFY" -eq 1 ]; then warn "--no-verify set: SKIPPING code-signature and notarization checks." warn "Only do this for trusted local/self-signed builds." else info "Verifying code signature ..." codesign --verify --deep --strict --verbose=2 "$APP_PATH" >/dev/null 2>&1 \ || die "code signature verification failed — refusing to install." info "Verifying notarization (Gatekeeper) ..." spctl --assess --type execute --verbose=2 "$APP_PATH" >/dev/null 2>&1 \ || die "notarization / Gatekeeper assessment failed — refusing to install." info "Verifying Team Identifier ..." ACTUAL_TEAM="$(codesign -dvv "$APP_PATH" 2>&1 | grep 'TeamIdentifier=' | head -n 1 | sed 's/.*TeamIdentifier=//')" EXPECT_TEAM="${M_TEAM_ID:-$EXPECTED_TEAM_ID}" if [ -z "$ACTUAL_TEAM" ] || [ "$ACTUAL_TEAM" != "$EXPECT_TEAM" ]; then die "Team Identifier mismatch — refusing to install. expected: $EXPECT_TEAM actual: ${ACTUAL_TEAM:-}" fi info " signature, notarization, and Team ID ($ACTUAL_TEAM) OK" fi # ── Install ─────────────────────────────────────────────────────────────────── DEST="$PREFIX/$APP_NAME" # move_app SRC DEST [sudo] — clean-replace the app at DEST with SRC. move_app() { _src="$1" _dest="$2" _use_sudo="${3:-}" if [ -n "$_use_sudo" ]; then [ -e "$_dest" ] && { sudo rm -rf "$_dest" || return 1; } sudo ditto "$_src" "$_dest" || return 1 else [ -e "$_dest" ] && { rm -rf "$_dest" || return 1; } ditto "$_src" "$_dest" || return 1 fi } # A custom (non-default) prefix that does not exist yet is created on demand — # naming a directory implies you want it. /Applications always exists, so this # only fires for explicit --prefix overrides. if [ "$PREFIX" != "$DEFAULT_PREFIX" ] && [ ! -d "$PREFIX" ]; then mkdir -p "$PREFIX" || die "could not create install prefix '$PREFIX'." fi # Determine whether we can write to the prefix, and how to install if not. if [ -d "$PREFIX" ] && [ -w "$PREFIX" ]; then info "Installing to $DEST ..." move_app "$APP_PATH" "$DEST" || die "failed to install to $DEST." elif [ "$PREFIX" = "$DEFAULT_PREFIX" ]; then # /Applications is not writable. Use sudo only when a TTY is present; # otherwise (the piped `curl | sh` case) fall back to ~/Applications. if [ -t 0 ] && [ -t 1 ] && command -v sudo >/dev/null 2>&1; then info "$PREFIX is not writable; using sudo (you may be prompted) ..." move_app "$APP_PATH" "$DEST" sudo || die "failed to install to $DEST with sudo." else USER_APPS="$HOME/Applications" info "$PREFIX is not writable and no TTY is available for sudo." info "Falling back to a per-user install in $USER_APPS." mkdir -p "$USER_APPS" || die "could not create $USER_APPS." DEST="$USER_APPS/$APP_NAME" move_app "$APP_PATH" "$DEST" || die "failed to install to $DEST." fi else die "install prefix '$PREFIX' is not a writable directory." fi # ── Post-install output (the UX) ────────────────────────────────────────────── info "" info "✓ evoglyph $INSTALL_VERSION installed to:" info " $DEST" info "" if [ "$NO_OPEN" -eq 0 ]; then info "Launch it:" info " open -a evoglyph" info "" fi info "Before evoglyph can work, grant two permissions macOS will not let any" info "installer set for you (open these, then toggle evoglyph on):" info "" info " • Microphone" info " x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone" info " • Accessibility" info " x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" info "" info "First launch downloads ~3 GB of on-device models — this is a one-time setup" info "and runs fully offline afterward." if command -v brew >/dev/null 2>&1; then info "" info "Prefer Homebrew (gives you 'brew upgrade' / 'brew uninstall')?" info " brew install --cask evoglyph/tap/evoglyph" fi info "" info "Docs: https://evoglyph.com/docs/install.html"