290 lines · 9.0 KB · Shell
RawDownload
#!/usr/bin/env bashset -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"INSTALL_ROOT="${LOCAL_STUDIO_INSTALL_ROOT:-/Applications}"ROLLBACK_ROOT="${LOCAL_STUDIO_ROLLBACK_ROOT:-$HOME/Library/Application Support/Local Studio Installer/Rollbacks}"LSREGISTER="${LOCAL_STUDIO_LSREGISTER:-/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister}"PLIST_BUDDY="${LOCAL_STUDIO_PLIST_BUDDY:-/usr/libexec/PlistBuddy}"SKIP_RUNTIME_CLEANUP="${LOCAL_STUDIO_SKIP_RUNTIME_CLEANUP:-0}"RELEASE_DMG_URL="${LOCAL_STUDIO_RELEASE_DMG_URL:-https://github.com/sybil-solutions/local-studio/releases/latest/download/Local-Studio-arm64.dmg}"RELEASE_TEMP=""RELEASE_MOUNT="" channel="stable"keep_backup=1mode="install" for arg in "$@"; do  case "$arg" in    stable|dev) channel="$arg" ;;    --no-backup) keep_backup=0 ;;    --migrate-rollbacks) mode="migrate" ;;    *) echo "error: unknown argument $arg" >&2; exit 2 ;;  esacdone if [[ "$INSTALL_ROOT" != /* || "$INSTALL_ROOT" == "/" ]]; then  echo "error: install root must be an absolute directory below /" >&2  exit 2fi if [[ "$ROLLBACK_ROOT" != /* || "$ROLLBACK_ROOT" == "/" || "$ROLLBACK_ROOT" == "$INSTALL_ROOT" || "$ROLLBACK_ROOT/" == "$INSTALL_ROOT/"* ]]; then  echo "error: rollback root must be an absolute directory outside the install root" >&2  exit 2fi if [[ "$channel" == "dev" ]]; then  APP_NAME="Local Studio Dev"  APP_ID="org.local.studio.desktop.dev"  BUILT="${LOCAL_STUDIO_BUILT_APP:-$REPO_ROOT/frontend/dist-desktop-dev/mac-arm64/$APP_NAME.app}"else  APP_NAME="Local Studio"  APP_ID="org.local.studio.desktop"  BUILT="${LOCAL_STUDIO_BUILT_APP:-}"fi TARGET="$INSTALL_ROOT/$APP_NAME.app"ROLLBACK="$ROLLBACK_ROOT/$APP_NAME.zip"STAGED="$INSTALL_ROOT/.local-studio-installing-$APP_ID-$$"REPLACED="$INSTALL_ROOT/.local-studio-replaced-$APP_ID-$$" rollback_for_id() {  case "$1" in    org.local.studio.desktop) printf '%s/Local Studio.zip\n' "$ROLLBACK_ROOT" ;;    org.local.studio.desktop.dev) printf '%s/Local Studio Dev.zip\n' "$ROLLBACK_ROOT" ;;    *) return 1 ;;  esac} canonical_for_id() {  case "$1" in    org.local.studio.desktop) printf '%s/Local Studio.app\n' "$INSTALL_ROOT" ;;    org.local.studio.desktop.dev) printf '%s/Local Studio Dev.app\n' "$INSTALL_ROOT" ;;    *) return 1 ;;  esac} bundle_id() {  "$PLIST_BUDDY" -c 'Print :CFBundleIdentifier' "$1/Contents/Info.plist" 2>/dev/null} archive_bundle() {  local source="$1"  local destination="$2"  local temporary="$destination.tmp.$$"   mkdir -p "$(dirname "$destination")"  rm -f "$temporary"  if ! ditto -c -k --sequesterRsrc --keepParent "$source/Contents" "$temporary"; then    rm -f "$temporary"    return 1  fi  if ! archive_is_valid "$temporary"; then    rm -f "$temporary"    echo "error: rollback archive is incomplete" >&2    return 1  fi  mv -f "$temporary" "$destination"} archive_is_valid() {  local archive="$1"  [[ -f "$archive" ]] || return 1  unzip -tqq "$archive" || return 1  unzip -Z1 "$archive" | awk '$0 == "Contents/Info.plist" || ($0 ~ /^Local Studio( Dev)?\.app/ && $0 ~ /\/Contents\/Info\.plist$/) { found = 1 } END { exit found ? 0 : 1 }'} legacy_bundles() {  [[ -d "$INSTALL_ROOT" ]] || return 0  find "$INSTALL_ROOT" -mindepth 1 -maxdepth 1 -type d -iname '*Local Studio*' -print0} unregister_bundle_tree() {  local root="$1"  local nested  [[ -x "$LSREGISTER" ]] || return 0  while IFS= read -r -d '' nested; do    "$LSREGISTER" -u "$nested" >/dev/null 2>&1 || true  done < <(find "$root" -type d -name '*.app' -print0 2>/dev/null)  "$LSREGISTER" -u "$root" >/dev/null 2>&1 || true} prune_stale_launch_services() {  local registered  [[ -x "$LSREGISTER" ]] || return 0  while IFS= read -r registered; do    case "$registered" in      "$INSTALL_ROOT/Local Studio.app"|"$INSTALL_ROOT/Local Studio.app/"*|"$INSTALL_ROOT/Local Studio Dev.app"|"$INSTALL_ROOT/Local Studio Dev.app/"*) continue ;;      "$INSTALL_ROOT/Local Studio"*) ;;      *) continue ;;    esac    [[ ! -e "$registered" ]] || continue    "$LSREGISTER" -u "$registered" >/dev/null 2>&1 || true  done < <("$LSREGISTER" -dump 2>/dev/null | sed -nE 's/^[[:space:]]*path:[[:space:]]*(.*) \(0x[[:xdigit:]]+\)$/\1/p')  "$LSREGISTER" -gc >/dev/null 2>&1 || true} migrate_legacy_bundles() {  local skip_id="${1:-}"  local candidate id canonical archive   while IFS= read -r -d '' candidate; do    id="$(bundle_id "$candidate" || true)"    [[ "$id" == "org.local.studio.desktop" || "$id" == "org.local.studio.desktop.dev" ]] || continue    canonical="$(canonical_for_id "$id")"    [[ "$candidate" != "$canonical" ]] || continue    archive="$(rollback_for_id "$id")"     if [[ "$id" != "$skip_id" ]] && ! archive_is_valid "$archive"; then      echo "==> archiving legacy rollback $candidate -> $archive"      archive_bundle "$candidate" "$archive"    fi     echo "==> removing discoverable legacy bundle $candidate"    unregister_bundle_tree "$candidate"    rm -rf "$candidate"  done < <(legacy_bundles)   prune_stale_launch_services} cleanup_temporary_paths() {  rm -rf "$STAGED"  if [[ "${SWAP_VERIFIED:-0}" == "0" && -d "$REPLACED" ]]; then    rm -rf "$TARGET"    mv "$REPLACED" "$TARGET"  elif [[ "${SWAP_VERIFIED:-0}" == "0" && "${TARGET_INSTALLED:-0}" == "1" ]]; then    rm -rf "$TARGET"  fi  cleanup_release_source} cleanup_release_source() {  if [[ -n "$RELEASE_MOUNT" ]]; then    hdiutil detach "$RELEASE_MOUNT" -quiet || hdiutil detach "$RELEASE_MOUNT" -force -quiet || true  fi  [[ -z "$RELEASE_TEMP" ]] || rm -rf "$RELEASE_TEMP"  RELEASE_MOUNT=""  RELEASE_TEMP=""} if [[ "$mode" == "migrate" ]]; then  migrate_legacy_bundles  echo "==> done. rollback archives: $ROLLBACK_ROOT"  exit 0fi HAD_TARGET=0SWAP_VERIFIED=0TARGET_INSTALLED=0trap cleanup_temporary_paths EXIT if [[ "$channel" == "stable" && -z "$BUILT" ]]; then  RELEASE_TEMP="$(mktemp -d "${TMPDIR:-/tmp}/local-studio-release.XXXXXX")"  RELEASE_MOUNT="$RELEASE_TEMP/mount"  release_dmg="$RELEASE_TEMP/Local-Studio-arm64.dmg"  mkdir -p "$RELEASE_MOUNT"  echo "==> downloading latest stable release"  curl --fail --location --silent --show-error "$RELEASE_DMG_URL" --output "$release_dmg"  xcrun stapler validate "$release_dmg"  spctl --assess --type open --context context:primary-signature "$release_dmg"  hdiutil attach -readonly -nobrowse -mountpoint "$RELEASE_MOUNT" "$release_dmg" >/dev/null  BUILT="$RELEASE_MOUNT/$APP_NAME.app"fi if [[ ! -d "$BUILT" ]]; then  echo "error: no built bundle at $BUILT" >&2  hint="desktop:dist"  [[ "$channel" == "dev" ]] && hint="desktop:dist:dev"  echo "       run: npm --prefix frontend run $hint" >&2  exit 1fi if [[ "$(bundle_id "$BUILT" || true)" != "$APP_ID" ]]; then  echo "error: built bundle identifier does not match $APP_ID" >&2  exit 1fi if [[ ! -x "$BUILT/Contents/MacOS/$APP_NAME" ]]; then  echo "error: built bundle has no executable" >&2  exit 1fi if [[ "$BUILT" != /* ]]; then  echo "error: built bundle path must be absolute" >&2  exit 2fi codesign --verify --deep --strict "$BUILT"if [[ "$channel" == "stable" ]]; then  spctl --assess --type execute "$BUILT"fimkdir -p "$INSTALL_ROOT" "$ROLLBACK_ROOT"rm -rf "$STAGED" "$REPLACED" ditto "$BUILT" "$STAGED"codesign --verify --deep --strict "$STAGED"cleanup_release_source if [[ -d "$TARGET" && "$keep_backup" == "1" ]]; then  echo "==> archiving current install -> $ROLLBACK"  archive_bundle "$TARGET" "$ROLLBACK"elif [[ "$keep_backup" == "0" ]]; then  rm -f "$ROLLBACK"fi if [[ "$SKIP_RUNTIME_CLEANUP" != "1" ]]; then  echo "==> quitting $APP_NAME"  osascript -e "tell application \"$APP_NAME\" to quit" >/dev/null 2>&1 || true  for _ in 1 2 3 4 5 6 7 8 9 10; do    pgrep -f "$APP_NAME.app/Contents/MacOS/$APP_NAME" >/dev/null || break    sleep 0.5  done  pkill -f "$APP_NAME.app/Contents/MacOS/$APP_NAME" >/dev/null 2>&1 || true   while IFS= read -r volume; do    [[ -n "$volume" ]] || continue    echo "==> ejecting stale disk image $volume"    hdiutil detach "$volume" -quiet || hdiutil detach "$volume" -force -quiet || true  done < <(find /Volumes -mindepth 1 -maxdepth 1 -type d -name "$APP_NAME*" -print 2>/dev/null || true)   for port in 3000 8081; do    pid="$(lsof -nP -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null | head -1 || true)"    [[ -n "$pid" ]] || continue    echo "==> stopping stale server on :$port (pid $pid)"    kill "$pid" 2>/dev/null || true  donefi if [[ -d "$TARGET" ]]; then  HAD_TARGET=1  mv "$TARGET" "$REPLACED"fi mv "$STAGED" "$TARGET"TARGET_INSTALLED=1codesign --verify --deep --strict "$TARGET"if [[ "$channel" == "stable" ]]; then  spctl --assess --type execute "$TARGET"fiSWAP_VERIFIED=1rm -rf "$REPLACED" if [[ "$keep_backup" == "1" ]]; then  migrate_legacy_bundleselse  migrate_legacy_bundles "$APP_ID"fi if [[ -x "$LSREGISTER" ]]; then  "$LSREGISTER" -f "$TARGET" >/dev/null 2>&1 || true  "$LSREGISTER" -gc >/dev/null 2>&1 || truefi trap - EXITecho "==> installed $TARGET"if [[ -f "$ROLLBACK" ]]; then  echo "==> rollback archive: $ROLLBACK"fiecho "    launch with: open \"$TARGET\""