diff --git a/lib/uninstall/brew.sh b/lib/uninstall/brew.sh index 7ffc168..d526e05 100644 --- a/lib/uninstall/brew.sh +++ b/lib/uninstall/brew.sh @@ -15,49 +15,18 @@ readonly MOLE_BREW_UNINSTALL_LOADED=1 # Returns: Absolute resolved path, or empty string on failure resolve_path() { local p="$1" + [[ -e "$p" ]] || return 1 - # Prefer realpath if available (GNU coreutils) - if command -v realpath > /dev/null 2>&1; then - realpath "$p" 2> /dev/null && return 0 - fi - - # macOS fallback: use python3 (almost always available) - if command -v python3 > /dev/null 2>&1; then - python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$p" 2> /dev/null && return 0 - fi - - # Last resort: perl (available on macOS) - if command -v perl > /dev/null 2>&1; then - perl -MCwd -e 'print Cwd::realpath($ARGV[0])' "$p" 2> /dev/null && return 0 - fi - - # Final fallback: if symlink, try to make readlink output absolute - if [[ -L "$p" ]]; then - local target - target=$(readlink "$p" 2>/dev/null) || return 1 - # If target is relative, prepend the directory of the symlink - if [[ "$target" != /* ]]; then - local dir - dir=$(cd -P "$(dirname "$p")" 2>/dev/null && pwd) || return 1 - target="$dir/$target" - fi - # Normalize by resolving the directory component - local target_dir target_base - target_dir=$(cd -P "$(dirname "$target")" 2>/dev/null && pwd) || { - echo "$target" - return 0 - } - target_base=$(basename "$target") - echo "$target_dir/$target_base" + # macOS 12.3+ and Linux have realpath + if realpath "$p" 2>/dev/null; then return 0 fi - # Not a symlink, return as-is if it exists - if [[ -e "$p" ]]; then - echo "$p" - return 0 - fi - return 1 + # Fallback: use cd -P to resolve directory, then append basename + local dir base + dir=$(cd -P "$(dirname "$p")" 2>/dev/null && pwd) || return 1 + base=$(basename "$p") + echo "$dir/$base" } # Check if Homebrew is installed and accessible @@ -112,81 +81,54 @@ _detect_cask_via_caskroom_search() { [[ -z "$app_bundle_name" ]] && return 1 local -a tokens=() - local -a uniq=() - local room match token t u seen + local room match token for room in "/opt/homebrew/Caskroom" "/usr/local/Caskroom"; do [[ -d "$room" ]] || continue while IFS= read -r match; do [[ -n "$match" ]] || continue token=$(_extract_cask_token_from_path "$match" 2>/dev/null) || continue - [[ -n "$token" ]] || continue - tokens+=("$token") + [[ -n "$token" ]] && tokens+=("$token") done < <(find "$room" -maxdepth 3 -name "$app_bundle_name" 2>/dev/null) done - # Deduplicate tokens - for t in "${tokens[@]+"${tokens[@]}"}"; do - seen=false - for u in "${uniq[@]+"${uniq[@]}"}"; do - [[ "$u" == "$t" ]] && { seen=true; break; } - done - [[ "$seen" == "false" ]] && uniq+=("$t") - done + # Deduplicate and check count + local -a uniq + IFS=$'\n' read -r -d '' -a uniq < <(printf '%s\n' "${tokens[@]}" | sort -u && printf '\0') || true # Only succeed if exactly one unique token found and it's installed - if ((${#uniq[@]} == 1)); then - local candidate="${uniq[0]}" - HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2>/dev/null | grep -qxF "$candidate" || return 1 - echo "$candidate" + if ((${#uniq[@]} == 1)) && [[ -n "${uniq[0]}" ]]; then + HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2>/dev/null | grep -qxF "${uniq[0]}" || return 1 + echo "${uniq[0]}" return 0 fi return 1 } -# Stage 3: Check if app_path is a direct symlink to Caskroom (simpler readlink check) -# Redundant with stage 1 in most cases, but kept as fallback +# Stage 3: Check if app_path is a direct symlink to Caskroom _detect_cask_via_symlink_check() { local app_path="$1" [[ -L "$app_path" ]] || return 1 local target target=$(readlink "$app_path" 2>/dev/null) || return 1 - - for room in "/opt/homebrew/Caskroom" "/usr/local/Caskroom"; do - if [[ "$target" == "$room/"* ]]; then - local relative="${target#"$room"/}" - local token="${relative%%/*}" - if [[ -n "$token" && "$token" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then - echo "$token" - return 0 - fi - fi - done - - return 1 + _extract_cask_token_from_path "$target" } -# Stage 4: Query brew list --cask and verify with brew info -# Slowest but catches edge cases where app was moved/renamed +# Stage 4: Query brew list --cask and verify with brew info (slowest fallback) _detect_cask_via_brew_list() { local app_path="$1" local app_bundle_name="$2" + local app_name_lower + app_name_lower=$(echo "${app_bundle_name%.app}" | LC_ALL=C tr '[:upper:]' '[:lower:]') - local app_name_only="${app_bundle_name%.app}" local cask_name - cask_name=$(HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2> /dev/null | grep -Fix "$(echo "$app_name_only" | LC_ALL=C tr '[:upper:]' '[:lower:]')" || echo "") + cask_name=$(HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2>/dev/null | grep -Fix "$app_name_lower") || return 1 - if [[ -n "$cask_name" ]]; then - # Verify this cask actually owns this app path - if HOMEBREW_NO_ENV_HINTS=1 brew info --cask "$cask_name" 2> /dev/null | grep -qF "$app_path"; then - echo "$cask_name" - return 0 - fi - fi - - return 1 + # Verify this cask actually owns this app path + HOMEBREW_NO_ENV_HINTS=1 brew info --cask "$cask_name" 2>/dev/null | grep -qF "$app_path" || return 1 + echo "$cask_name" } # Get Homebrew cask name for an app @@ -228,54 +170,24 @@ brew_uninstall_cask() { debug_log "Attempting brew uninstall --cask $cask_name" - # Suppress hints, auto-update, and ensure non-interactive - export HOMEBREW_NO_ENV_HINTS=1 - export HOMEBREW_NO_AUTO_UPDATE=1 - export NONINTERACTIVE=1 - - # Run uninstall with timeout (cask uninstalls can hang on prompts) - local output - local uninstall_succeeded=false - if output=$(run_with_timeout 120 brew uninstall --cask "$cask_name" 2>&1); then - debug_log "brew uninstall --cask $cask_name completed successfully" - uninstall_succeeded=true - else - local exit_code=$? - debug_log "brew uninstall --cask $cask_name exited with code $exit_code: $output" + # Run uninstall with timeout (suppress hints/auto-update) + local uninstall_ok=false + if HOMEBREW_NO_ENV_HINTS=1 HOMEBREW_NO_AUTO_UPDATE=1 NONINTERACTIVE=1 \ + run_with_timeout 120 brew uninstall --cask "$cask_name" 2>&1; then + uninstall_ok=true fi - # Check current state - local cask_still_installed=false - if HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2>/dev/null | grep -qxF "$cask_name"; then - cask_still_installed=true - fi + # Verify removal + local cask_gone=true app_gone=true + HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2>/dev/null | grep -qxF "$cask_name" && cask_gone=false + [[ -n "$app_path" && -e "$app_path" ]] && app_gone=false - local app_still_exists=false - if [[ -n "$app_path" && -e "$app_path" ]]; then - app_still_exists=true - fi - - # Success cases: - # 1. Uninstall succeeded and cask/app are gone - # 2. Uninstall failed but cask wasn't installed anyway (idempotent) - if [[ "$uninstall_succeeded" == "true" ]]; then - if [[ "$cask_still_installed" == "true" ]]; then - debug_log "Cask '$cask_name' still in brew list after successful uninstall" - return 1 - fi - if [[ "$app_still_exists" == "true" ]]; then - debug_log "App still exists at '$app_path' after brew uninstall" - return 1 - fi + # Success: uninstall worked and both are gone, or already uninstalled + if $cask_gone && $app_gone; then debug_log "Successfully uninstalled cask '$cask_name'" return 0 - else - # Uninstall command failed - only succeed if already fully uninstalled - if [[ "$cask_still_installed" == "false" && "$app_still_exists" == "false" ]]; then - debug_log "Cask '$cask_name' was already uninstalled" - return 0 - fi - debug_log "brew uninstall failed and cask/app still present" - return 1 fi + + debug_log "brew uninstall failed: cask_gone=$cask_gone app_gone=$app_gone" + return 1 }