mirror of
https://github.com/tw93/Mole.git
synced 2026-02-05 22:33:02 +00:00
- Add broken preferences detection using plutil -lint validation - Add broken login items cleanup (LaunchAgents pointing to missing files) - Add universal binary slimming (opt-in via MOLE_SLIM_BINARIES=true) - Protect Spotify cache if >500MB (likely contains offline music) - Warn when browsers are running before cache cleanup - All cleanup stats properly counted in final summary
303 lines
10 KiB
Bash
303 lines
10 KiB
Bash
#!/bin/bash
|
|
# Maintenance Cleanup Module
|
|
# Universal binary slimming, broken preferences, broken login items
|
|
|
|
set -euo pipefail
|
|
|
|
# ============================================================================
|
|
# Universal Binary Slimming
|
|
# Remove unused architecture code from universal binaries
|
|
# ============================================================================
|
|
|
|
# Slim universal binaries to current architecture only
|
|
# Only processes apps in /Applications, skips signed/notarized apps
|
|
# Env: DRY_RUN
|
|
# Globals: files_cleaned, total_size_cleaned, total_items (modified)
|
|
clean_universal_binaries() {
|
|
# Only run on Apple Silicon (most benefit)
|
|
if [[ "$(uname -m)" != "arm64" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Check if lipo is available
|
|
if ! command -v lipo > /dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
local current_arch="arm64"
|
|
local remove_arch="x86_64"
|
|
local total_saved_kb=0
|
|
local apps_slimmed=0
|
|
local max_apps=50 # Limit to prevent long runs
|
|
|
|
if [[ -t 1 ]]; then
|
|
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning universal binaries..."
|
|
fi
|
|
|
|
local app_count=0
|
|
while IFS= read -r app_path; do
|
|
[[ -d "$app_path" ]] || continue
|
|
|
|
((app_count++))
|
|
if [[ $app_count -gt $max_apps ]]; then
|
|
break
|
|
fi
|
|
|
|
local binary_path="$app_path/Contents/MacOS"
|
|
[[ -d "$binary_path" ]] || continue
|
|
|
|
# Get the main executable
|
|
local info_plist="$app_path/Contents/Info.plist"
|
|
[[ -f "$info_plist" ]] || continue
|
|
|
|
local exec_name
|
|
exec_name=$(defaults read "$info_plist" CFBundleExecutable 2> /dev/null || echo "")
|
|
[[ -z "$exec_name" ]] && continue
|
|
|
|
local exec_path="$binary_path/$exec_name"
|
|
[[ -f "$exec_path" ]] || continue
|
|
|
|
# Check if it's a universal binary with both architectures
|
|
local archs
|
|
archs=$(lipo -archs "$exec_path" 2> /dev/null || echo "")
|
|
if [[ "$archs" != *"$current_arch"* ]] || [[ "$archs" != *"$remove_arch"* ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Skip if app is code signed (removing arch breaks signature)
|
|
if codesign -v "$app_path" 2> /dev/null; then
|
|
continue
|
|
fi
|
|
|
|
# Calculate size before
|
|
local size_before
|
|
size_before=$(du -sk "$exec_path" 2> /dev/null | awk '{print $1}' || echo "0")
|
|
|
|
if [[ "$DRY_RUN" != "true" ]]; then
|
|
# Create backup and slim
|
|
local backup_path="${exec_path}.universal.bak"
|
|
if cp "$exec_path" "$backup_path" 2> /dev/null; then
|
|
if lipo "$backup_path" -remove "$remove_arch" -output "$exec_path" 2> /dev/null; then
|
|
rm -f "$backup_path"
|
|
local size_after
|
|
size_after=$(du -sk "$exec_path" 2> /dev/null | awk '{print $1}' || echo "0")
|
|
local saved=$((size_before - size_after))
|
|
if [[ $saved -gt 0 ]]; then
|
|
((total_saved_kb += saved))
|
|
((apps_slimmed++))
|
|
fi
|
|
else
|
|
# Restore backup on failure
|
|
mv "$backup_path" "$exec_path" 2> /dev/null || true
|
|
fi
|
|
fi
|
|
else
|
|
# Dry run: estimate savings (roughly 40-50% of binary size)
|
|
local estimated_save=$((size_before / 2))
|
|
((total_saved_kb += estimated_save))
|
|
((apps_slimmed++))
|
|
fi
|
|
done < <(find /Applications -maxdepth 2 -type d -name "*.app" 2> /dev/null || true)
|
|
|
|
if [[ -t 1 ]]; then
|
|
stop_inline_spinner
|
|
fi
|
|
|
|
if [[ $apps_slimmed -gt 0 && $total_saved_kb -gt 1024 ]]; then
|
|
local saved_human
|
|
saved_human=$(bytes_to_human "$((total_saved_kb * 1024))")
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
echo -e " ${YELLOW}→${NC} Universal binaries: $apps_slimmed apps ${YELLOW}(~$saved_human dry)${NC}"
|
|
else
|
|
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Slimmed $apps_slimmed apps ${GREEN}($saved_human)${NC}"
|
|
fi
|
|
# Update global statistics
|
|
((files_cleaned += apps_slimmed))
|
|
((total_size_cleaned += total_saved_kb))
|
|
((total_items++))
|
|
note_activity
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Broken Preferences Detection and Cleanup
|
|
# Find and remove corrupted .plist files
|
|
# ============================================================================
|
|
|
|
# Clean broken preference files
|
|
# Uses plutil -lint to validate plist files
|
|
# Env: DRY_RUN
|
|
# Globals: files_cleaned, total_size_cleaned, total_items (modified)
|
|
clean_broken_preferences() {
|
|
local prefs_dir="$HOME/Library/Preferences"
|
|
[[ -d "$prefs_dir" ]] || return 0
|
|
|
|
local broken_count=0
|
|
local total_size_kb=0
|
|
|
|
if [[ -t 1 ]]; then
|
|
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking preference files..."
|
|
fi
|
|
|
|
# Check main preferences directory
|
|
while IFS= read -r plist_file; do
|
|
[[ -f "$plist_file" ]] || continue
|
|
|
|
# Skip system preferences
|
|
local filename
|
|
filename=$(basename "$plist_file")
|
|
case "$filename" in
|
|
com.apple.* | .GlobalPreferences* | loginwindow.plist)
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
# Validate plist using plutil
|
|
if ! plutil -lint "$plist_file" > /dev/null 2>&1; then
|
|
local size_kb
|
|
size_kb=$(du -sk "$plist_file" 2> /dev/null | awk '{print $1}' || echo "0")
|
|
|
|
if [[ "$DRY_RUN" != "true" ]]; then
|
|
rm -f "$plist_file" 2> /dev/null || true
|
|
fi
|
|
|
|
((broken_count++))
|
|
((total_size_kb += size_kb))
|
|
fi
|
|
done < <(find "$prefs_dir" -maxdepth 1 -name "*.plist" -type f 2> /dev/null || true)
|
|
|
|
# Check ByHost preferences
|
|
local byhost_dir="$prefs_dir/ByHost"
|
|
if [[ -d "$byhost_dir" ]]; then
|
|
while IFS= read -r plist_file; do
|
|
[[ -f "$plist_file" ]] || continue
|
|
|
|
local filename
|
|
filename=$(basename "$plist_file")
|
|
case "$filename" in
|
|
com.apple.* | .GlobalPreferences*)
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
if ! plutil -lint "$plist_file" > /dev/null 2>&1; then
|
|
local size_kb
|
|
size_kb=$(du -sk "$plist_file" 2> /dev/null | awk '{print $1}' || echo "0")
|
|
|
|
if [[ "$DRY_RUN" != "true" ]]; then
|
|
rm -f "$plist_file" 2> /dev/null || true
|
|
fi
|
|
|
|
((broken_count++))
|
|
((total_size_kb += size_kb))
|
|
fi
|
|
done < <(find "$byhost_dir" -name "*.plist" -type f 2> /dev/null || true)
|
|
fi
|
|
|
|
if [[ -t 1 ]]; then
|
|
stop_inline_spinner
|
|
fi
|
|
|
|
if [[ $broken_count -gt 0 ]]; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
echo -e " ${YELLOW}→${NC} Broken preferences: $broken_count files ${YELLOW}(dry)${NC}"
|
|
else
|
|
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $broken_count broken preference files"
|
|
fi
|
|
# Update global statistics
|
|
((files_cleaned += broken_count))
|
|
((total_size_cleaned += total_size_kb))
|
|
((total_items++))
|
|
note_activity
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Broken Login Items Cleanup
|
|
# Find and remove login items pointing to non-existent files
|
|
# ============================================================================
|
|
|
|
# Clean broken login items (LaunchAgents pointing to missing executables)
|
|
# Env: DRY_RUN
|
|
# Globals: files_cleaned, total_items (modified)
|
|
clean_broken_login_items() {
|
|
local launch_agents_dir="$HOME/Library/LaunchAgents"
|
|
[[ -d "$launch_agents_dir" ]] || return 0
|
|
|
|
local broken_count=0
|
|
local total_size_kb=0
|
|
|
|
if [[ -t 1 ]]; then
|
|
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking login items..."
|
|
fi
|
|
|
|
while IFS= read -r plist_file; do
|
|
[[ -f "$plist_file" ]] || continue
|
|
|
|
# Skip system items
|
|
local filename
|
|
filename=$(basename "$plist_file")
|
|
case "$filename" in
|
|
com.apple.*)
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
# Extract Program or ProgramArguments[0] from plist
|
|
local program=""
|
|
program=$(defaults read "$plist_file" Program 2> /dev/null || echo "")
|
|
|
|
if [[ -z "$program" ]]; then
|
|
# Try ProgramArguments array
|
|
program=$(defaults read "$plist_file" ProgramArguments 2> /dev/null | head -2 | tail -1 | sed 's/^[[:space:]]*"//' | sed 's/".*$//' || echo "")
|
|
fi
|
|
|
|
# Skip if no program found or program exists
|
|
[[ -z "$program" ]] && continue
|
|
[[ -e "$program" ]] && continue
|
|
|
|
# Program doesn't exist - this is a broken login item
|
|
local size_kb
|
|
size_kb=$(du -sk "$plist_file" 2> /dev/null | awk '{print $1}' || echo "0")
|
|
|
|
if [[ "$DRY_RUN" != "true" ]]; then
|
|
# Unload first if loaded
|
|
launchctl unload "$plist_file" 2> /dev/null || true
|
|
rm -f "$plist_file" 2> /dev/null || true
|
|
fi
|
|
|
|
((broken_count++))
|
|
((total_size_kb += size_kb))
|
|
done < <(find "$launch_agents_dir" -name "*.plist" -type f 2> /dev/null || true)
|
|
|
|
if [[ -t 1 ]]; then
|
|
stop_inline_spinner
|
|
fi
|
|
|
|
if [[ $broken_count -gt 0 ]]; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
echo -e " ${YELLOW}→${NC} Broken login items: $broken_count ${YELLOW}(dry)${NC}"
|
|
else
|
|
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $broken_count broken login items"
|
|
fi
|
|
# Update global statistics
|
|
((files_cleaned += broken_count))
|
|
((total_size_cleaned += total_size_kb))
|
|
((total_items++))
|
|
note_activity
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main maintenance cleanup function
|
|
# ============================================================================
|
|
|
|
clean_maintenance() {
|
|
clean_broken_preferences
|
|
clean_broken_login_items
|
|
# Universal binary slimming is risky, only run if explicitly enabled
|
|
if [[ "${MOLE_SLIM_BINARIES:-false}" == "true" ]]; then
|
|
clean_universal_binaries
|
|
fi
|
|
}
|