Files
repertory/repertory/Install repertory.command
Scott E. Graves 53b8c70c80
All checks were successful
Blockstorage/repertory/pipeline/head This commit looks good
BlockStorage/repertory/pipeline/head This commit looks good
only snapshot matching paths
2025-09-15 10:14:03 -05:00

422 lines
15 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# No `-e` (we tolerate benign non-zero returns); keep -Euo
set -Euo pipefail
LOG_DIR="/tmp"
LOG_FILE="${LOG_DIR}/Install-$(date +%Y%m%d-%H%M%S).log"
exec > >(tee -a "$LOG_FILE") 2>&1
echo "Logging to: $LOG_FILE"
TARGET_DIR="/Applications"
APP_NAME="repertory.app"
# Embedded at pack time (from CFBundleIdentifier prefix)
LABEL_PREFIX="__LABEL_PREFIX__"
UI_LABEL="${LABEL_PREFIX}.ui"
staged=""
backup=""
snapfile=""
skip_ui_launch=0
log() { printf "[%(%H:%M:%S)T] %s\n" -1 "$*"; }
warn() { log "WARN: $*"; }
die() {
log "ERROR: $*"
exit 2
}
here="$(cd "$(dirname "$0")" && pwd)"
# Locate source app on the DMG (supports hidden payload dirs)
src_app="${here}/${APP_NAME}"
if [[ ! -d "$src_app" ]]; then
src_app="$(/usr/bin/find "$here" -type d -name "$APP_NAME" -print -quit 2>/dev/null || true)"
fi
[[ -d "$src_app" ]] || die "Could not find ${APP_NAME} on this disk image."
app_basename="$(basename "$src_app")"
dest_app="${TARGET_DIR}/${APP_NAME}"
bundle_id_of() { /usr/bin/defaults read "$1/Contents/Info" CFBundleIdentifier 2>/dev/null || true; }
bundle_exec_of() { /usr/bin/defaults read "$1/Contents/Info" CFBundleExecutable 2>/dev/null || echo "${app_basename%.app}"; }
bundle_version_of() {
/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$1/Contents/Info.plist" 2>/dev/null ||
/usr/bin/defaults read "$1/Contents/Info" CFBundleShortVersionString 2>/dev/null || echo "(unknown)"
}
bundle_build_of() {
/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$1/Contents/Info.plist" 2>/dev/null ||
/usr/bin/defaults read "$1/Contents/Info" CFBundleVersion 2>/dev/null || echo "(unknown)"
}
# Require /Applications; prompt for sudo if needed; abort if cannot elevate
USE_SUDO=0
SUDO=""
ensure_target_writable() {
if mkdir -p "${TARGET_DIR}/.repertory_install_test.$$" 2>/dev/null; then
rmdir "${TARGET_DIR}/.repertory_install_test.$$" 2>/dev/null || true
USE_SUDO=0
SUDO=""
return 0
fi
if command -v sudo >/dev/null 2>&1; then
log "Elevating privileges to write to ${TARGET_DIR}"
sudo -v || die "Administrator privileges required to install into ${TARGET_DIR}."
USE_SUDO=1
SUDO="sudo"
return 0
fi
die "Cannot write to ${TARGET_DIR} and sudo is unavailable."
}
# ----- STRICT LABEL PREFIX GATE (fail if invalid) -----
_is_valid_label_prefix() {
local p="${1:-}"
[[ -n "$p" ]] && [[ "$p" != "__LABEL_PREFIX__" ]] && [[ "$p" =~ ^[A-Za-z0-9._-]+$ ]] && [[ "$p" == *.* ]]
}
if ! _is_valid_label_prefix "${LABEL_PREFIX:-}"; then
die "Invalid LABEL_PREFIX in installer (value: \"${LABEL_PREFIX:-}\"). Rebuild the DMG so the installer contains a valid reverse-DNS prefix."
fi
# ----- LaunchServices helpers -----
ls_prune_bundle_id() {
local bundle_id="$1" keep_path="$2"
[[ -z "$bundle_id" ]] && return 0
local search_roots=("/Applications" "$HOME/Applications" "/Volumes")
if [[ -n "${here:-}" && "$here" == /Volumes/* ]]; then search_roots+=("$here"); fi
local candidates=""
for root in "${search_roots[@]}"; do
[[ -d "$root" ]] || continue
candidates+=$'\n'"$(/usr/bin/mdfind -onlyin "$root" "kMDItemCFBundleIdentifier == '${bundle_id}'" 2>/dev/null || true)"
done
# Include backups adjacent to keep_path (quote-safe)
local parent_dir="${keep_path%/*.app}"
candidates+=$'\n'$(/bin/ls -1d "${parent_dir}/"*.bak 2>/dev/null || true)
local LSREG="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
printf "%s\n" "$candidates" | /usr/bin/awk 'NF' | /usr/bin/sort -u | while IFS= read -r p; do
[[ -z "$p" || ! -d "$p" || "$p" == "$keep_path" ]] && continue
log "Unregistering older LS entry for ${bundle_id}: $p"
"$LSREG" -u "$p" >/dev/null 2>&1 || true
done
}
# ----- FUSE unmount (no process killing here) -----
is_mounted() { /sbin/mount | /usr/bin/awk '{print $3}' | /usr/bin/grep -Fx "${1:-}" >/dev/null 2>&1; }
_list_repertory_fuse_mounts() { /sbin/mount | /usr/bin/grep -Ei 'macfuse|osxfuse' | /usr/bin/awk '{print $3}' | /usr/bin/grep -i "repertory" || true; }
_unmount_one() {
local mnt="${1:-}"
[[ -n "$mnt" ]] || return 0
/usr/sbin/diskutil unmount "$mnt" >/dev/null 2>&1 || /sbin/umount "$mnt" >/dev/null 2>&1 || true
if is_mounted "$mnt"; then
/usr/sbin/diskutil unmount force "$mnt" >/dev/null 2>&1 || /sbin/umount -f "$mnt" >/dev/null 2>&1 || true
fi
for _ in {1..20}; do
is_mounted "$mnt" || return 0
sleep 0.25
done
return 1
}
unmount_existing_repertory_volumes() {
# Hard-fail on the first unmount problem.
while IFS= read -r mnt; do
[[ -z "$mnt" ]] && continue
log "Unmounting FUSE mount: $mnt"
if ! _unmount_one "$mnt"; then
warn "Failed to unmount $mnt"
return 1
fi
done < <(_list_repertory_fuse_mounts)
sync || true
sleep 0.3
return 0
}
# ----- user LaunchAgents (by LABEL_PREFIX only) -----
get_plist_label() { /usr/bin/defaults read "$1" Label 2>/dev/null || /usr/libexec/PlistBuddy -c "Print :Label" "$1" 2>/dev/null || basename "$1" .plist; }
# Return the executable path a LaunchAgent runs (ProgramArguments[0] or Program).
# Echoes empty string if neither is present.
get_plist_exec_path() {
local plist="$1" arg0=""
# Prefer ProgramArguments[0]
arg0="$(/usr/libexec/PlistBuddy -c 'Print :ProgramArguments:0' "$plist" 2>/dev/null || true)"
if [[ -z "$arg0" ]]; then
# Fallback to Program (older style)
arg0="$(/usr/libexec/PlistBuddy -c 'Print :Program' "$plist" 2>/dev/null || true)"
fi
printf '%s\n' "$arg0"
}
snapshot_launchagents_user() {
local user_agents="$HOME/Library/LaunchAgents"
snapfile="$(/usr/bin/mktemp "/tmp/repertory_launchagents.XXXXXX")" || snapfile=""
if [[ -z "$snapfile" ]]; then
warn "Could not create temporary snapshot file; skipping LaunchAgent restart snapshot."
return 0
fi
[[ -d "$user_agents" ]] || return 0
# We collect non-UI first, then UI last, to preserve restart order later.
local tmp_nonui tmp_ui
tmp_nonui="$(/usr/bin/mktemp "/tmp/repertory_launchagents.nonui.XXXXXX")" || tmp_nonui=""
tmp_ui="$(/usr/bin/mktemp "/tmp/repertory_launchagents.ui.XXXXXX")" || tmp_ui=""
/usr/bin/find "$user_agents" -maxdepth 1 -type f -name "${LABEL_PREFIX}"'*.plist' -print 2>/dev/null |
while IFS= read -r plist; do
[[ -z "$plist" ]] && continue
# Label must match the prefix
local label
label="$(get_plist_label "$plist")"
[[ -n "$label" && "$label" == "${LABEL_PREFIX}"* ]] || continue
# Executable must point into the *installed* app path
local exec_path
exec_path="$(get_plist_exec_path "$plist")"
[[ -n "$exec_path" ]] || continue
# Normalize: only accept absolute paths under $dest_app (e.g. .../repertory.app/Contents/...)
if [[ "$exec_path" != "$dest_app/"* ]]; then
# Not one of ours; skip
continue
fi
# Defer UI label to the end
if [[ "$label" == "$UI_LABEL" ]]; then
[[ -n "$tmp_ui" ]] && printf "%s\t%s\n" "$plist" "$label" >>"$tmp_ui"
else
[[ -n "$tmp_nonui" ]] && printf "%s\t%s\n" "$plist" "$label" >>"$tmp_nonui"
fi
done
# Stitch together: non-UI first, then UI
[[ -s "$tmp_nonui" ]] && /bin/cat "$tmp_nonui" >>"$snapfile"
[[ -s "$tmp_ui" ]] && /bin/cat "$tmp_ui" >>"$snapfile"
[[ -n "$tmp_nonui" ]] && /bin/rm -f "$tmp_nonui" 2>/dev/null || true
[[ -n "$tmp_ui" ]] && /bin/rm -f "$tmp_ui" 2>/dev/null || true
log "Snapshot contains $(/usr/bin/wc -l <"$snapfile" 2>/dev/null || echo 0) LaunchAgent(s)."
}
unload_launchd_helpers_user() {
local uid user_agents
uid="$(id -u)"
user_agents="$HOME/Library/LaunchAgents"
[[ -d "$user_agents" ]] || return 0
while IFS= read -r plist; do
[[ -z "$plist" ]] && continue
local base label
base="$(basename "$plist")"
[[ "$base" == "${LABEL_PREFIX}"* ]] || continue
label="$(get_plist_label "$plist")"
[[ -n "$label" && "$label" == "${LABEL_PREFIX}"* ]] || continue
log "Booting out user label ${label} (${plist})"
/bin/launchctl bootout "gui/${uid}" "$plist" 2>/dev/null ||
/bin/launchctl bootout "gui/${uid}" "$label" 2>/dev/null ||
/bin/launchctl remove "$label" 2>/dev/null || true
done < <(/usr/bin/find "$user_agents" -maxdepth 1 -type f -name "${LABEL_PREFIX}"'*.plist' -print 2>/dev/null)
/bin/launchctl list 2>/dev/null | /usr/bin/awk -v pre="$LABEL_PREFIX" 'NF>=3 && index($3, pre)==1 {print $3}' |
while read -r lbl; do
[[ -z "$lbl" ]] && continue
log "Booting out leftover user label: $lbl"
/bin/launchctl bootout "gui/${uid}" "$lbl" 2>/dev/null || /bin/launchctl remove "$lbl" 2>/dev/null || true
done
}
restart_launchagents_from_snapshot() {
[[ -n "${snapfile:-}" && -f "${snapfile}" ]] || return 0
local uid count=0 ui_seen=0
uid="$(id -u)"
# Pass 1: restart all non-UI agents first
while IFS=$'\t' read -r plist label; do
[[ -n "$plist" && -n "$label" ]] || continue
[[ -f "$plist" ]] || continue
[[ "$label" == "$UI_LABEL" ]] && continue
log "Re-starting LaunchAgent: ${label}"
/bin/launchctl bootstrap "gui/${uid}" "$plist" 2>/dev/null || true
/bin/launchctl kickstart -k "gui/${uid}/${label}" 2>/dev/null || true
((count++)) || true
done <"$snapfile"
# Give helpers a moment to settle (e.g., automounts)
sleep 0.3
# Pass 2: restart the UI agent last (if present in the snapshot)
while IFS=$'\t' read -r plist label; do
[[ -n "$plist" && -n "$label" ]] || continue
[[ -f "$plist" ]] || continue
[[ "$label" == "$UI_LABEL" ]] || continue
log "Re-starting UI LaunchAgent last: ${label}"
/bin/launchctl bootstrap "gui/${uid}" "$plist" 2>/dev/null || true
/bin/launchctl kickstart -k "gui/${uid}/${label}" 2>/dev/null || true
ui_seen=1
((count++)) || true
done <"$snapfile"
log "Re-started ${count} LaunchAgent(s) with prefix ${LABEL_PREFIX}." || true
if ((ui_seen)); then
# If the UI label is active, skip manual open(1).
if /bin/launchctl list | /usr/bin/awk '{print $3}' | /usr/bin/grep -Fxq "$UI_LABEL"; then
log "UI LaunchAgent (${UI_LABEL}) active after restart; skipping manual UI launch."
skip_ui_launch=1
fi
fi
}
# ----- quarantine helper -----
remove_quarantine() {
local path="${1:-}"
if [[ "${USE_SUDO:-0}" == "1" ]]; then
sudo /usr/bin/xattr -dr com.apple.quarantine "$path" 2>/dev/null || true
else
/usr/bin/xattr -dr com.apple.quarantine "$path" 2>/dev/null || true
fi
}
# ----- process helpers -----
kill_repertory_processes() {
local exec_name="$1"
/usr/bin/pkill -TERM -f "$dest_app" >/dev/null 2>&1 || true
/usr/bin/pkill -TERM -x "$exec_name" >/dev/null 2>&1 || true
for _ in {1..20}; do
/usr/bin/pgrep -af "$dest_app" >/dev/null 2>&1 || /usr/bin/pgrep -x "$exec_name" >/dev/null 2>&1 || break
sleep 0.1
done
/usr/bin/pkill -KILL -f "$dest_app" >/dev/null 2>&1 || true
/usr/bin/pkill -KILL -x "$exec_name" >/dev/null 2>&1 || true
}
# ----- visibility helper -----
unhide_path() {
local path="$1"
/usr/bin/chflags -R nohidden "$path" 2>/dev/null || true
/usr/bin/xattr -d -r com.apple.FinderInfo "$path" 2>/dev/null || true
}
# ----- stage / validate / activate / post-activate -----
stage_new_app() {
staged="${dest_app}.new-$$"
log "Staging new app → $staged"
$SUDO /usr/bin/ditto "$src_app" "$staged" || die "ditto to stage failed"
remove_quarantine "$staged"
}
validate_staged_app() {
[[ -f "$staged/Contents/Info.plist" ]] || {
$SUDO /bin/rm -rf "$staged"
die "staged app missing Info.plist"
}
local exe_name_staged
exe_name_staged="$(/usr/bin/defaults read "$staged/Contents/Info" CFBundleExecutable 2>/dev/null || echo "${app_basename%.app}")"
[[ -x "$staged/Contents/MacOS/$exe_name_staged" ]] || {
$SUDO /bin/rm -rf "$staged"
die "staged app missing main executable"
}
}
activate_staged_app() {
if [[ -d "$dest_app" ]]; then
backup="${dest_app}.$(date +%Y%m%d%H%M%S).bak"
log "Moving existing app to backup: $backup"
$SUDO /bin/mv "$dest_app" "$backup" || {
$SUDO /bin/rm -rf "$staged"
die "failed to move existing app out of the way"
}
fi
log "Activating new app → $dest_app"
if ! $SUDO /bin/mv "$staged" "$dest_app"; then
warn "Activation failed; attempting rollback…"
[[ -n "$backup" && -d "$backup" ]] && $SUDO /bin/mv "$backup" "$dest_app" || true
$SUDO /bin/rm -rf "$staged" || true
die "install activation failed"
fi
}
post_activate_cleanup() {
log "Clearing quarantine on installed app…"
remove_quarantine "$dest_app"
log "Clearing hidden flags on installed app…"
unhide_path "$dest_app"
local LSREG="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
[[ -x "$LSREG" ]] && "$LSREG" -f "$dest_app" >/dev/null 2>&1 || true
local BID
BID="$(bundle_id_of "$dest_app")"
ls_prune_bundle_id "$BID" "$dest_app"
log "Installed ${app_basename}: version=$(bundle_version_of "$dest_app") build=$(bundle_build_of "$dest_app")"
}
launch_ui() {
log "Launching the new app…"
/usr/bin/open -n "$dest_app" || warn "open -n by path failed; not falling back to -b to avoid launching a stale copy."
}
remove_backup() {
[[ -n "$backup" && -d "$backup" ]] && {
log "Removing backup: $backup"
$SUDO /bin/rm -rf "$backup" || warn "Could not remove backup (safe to delete manually): $backup"
}
log "Done."
}
cleanup_staged() {
if [[ -n "${staged:-}" && -d "${staged}" ]]; then
log "Cleaning up staged folder: ${staged}"
if [[ "${USE_SUDO:-0}" == "1" ]]; then
sudo /bin/rm -rf "${staged}" 2>/dev/null || true
else
/bin/rm -rf "${staged}" 2>/dev/null || true
fi
fi
if [[ -n "${snapfile:-}" && -f "${snapfile}" ]]; then
/bin/rm -f "${snapfile}" 2>/dev/null || true
fi
}
main() {
ensure_target_writable
local exec_name
exec_name="$(bundle_exec_of "$src_app")"
# 1) Snapshot agents well restart later
snapshot_launchagents_user
# 2) Hard-fail if any FUSE unmount fails
unmount_existing_repertory_volumes || die "One or more FUSE mounts resisted unmount."
# 3) Stop user LaunchAgents (do NOT delete plists)
unload_launchd_helpers_user
# 4) Kill any remaining repertory processes
kill_repertory_processes "$exec_name"
# 5) Stage → validate → activate → post-activate
stage_new_app
validate_staged_app
activate_staged_app
post_activate_cleanup
# 6) Re-start previously-running LaunchAgents (so automount helpers come up)
restart_launchagents_from_snapshot
# 7) If UI LaunchAgent came back, skip manual launch
if ((!skip_ui_launch)); then
launch_ui
fi
# 8) Remove backup now that everything is good
remove_backup
}
trap 'rc=$?; cleanup_staged; if (( rc != 0 )); then echo "Installer failed with code $rc. See $LOG_FILE"; fi' EXIT
main "$@"