08 · Uninstall 卸载系统
卸载不是“按应用名全盘搜索”。它是一次身份归因:哪些文件有足够证据属于目标应用,哪些只能提示用户复核。
两阶段设计
卸载器先构建应用清单,再为选中应用扫描细节。清单元数据缓存于 ~/.cache/mole/uninstall_app_metadata_v1,TTL 为 7 天。Spotlight/mdls 查询使用极短超时,冷数据由并行 worker 补齐;数量较少时才允许有界 du。
缓存复用不仅看时间,还计算应用 inventory fingerprint。新增或 mtime 变化会触发刷新,已移除应用不必让整个缓存失效。这样菜单能快速出现,同时后台逐步修正元数据。
从应用身份到文件证据
.app / package receipt
│
├─ Bundle ID、变体、嵌套 helpers
├─ 精确 Library 路径与 defaults
├─ Homebrew cask / 官方卸载器
└─ 系统级残留(只报告)
│
▼
批量预览与一次确认
│
▼
`MOLE_UNINSTALL_MODE=1` + `mole_delete`应用名只是辅助证据。核心依赖 reverse-DNS Bundle ID、Info.plist、包收据与明确的路径形态。实现避免通用 vendor wildcard,也不会因为 GUI 与 CLI 同名就删除 ~/.codex、~/.claude 等独立状态。
默认 Trash,永久删除显式选择
卸载默认设置 MOLE_DELETE_MODE=trash,--permanent 才改变恢复语义。直接按名称匹配仍要求确认;--dry-run 走同一扫描与计划链。--list 是只读操作,在非 TTY 下自动输出 JSON。
批处理不是 N 次单卸载
批处理先并行扫描所有选中应用,再合并成一次预览和一次 sudo 请求。执行时每个应用独立记结果,单个失败不阻断后续应用。
它还处理“同 Bundle ID 有多个安装副本”:只要兄弟安装仍存在,就跳过共享数据、helper teardown 与可能破坏另一个副本的 brew zap。这是以应用实例和产品身份分层,而不是把 Bundle ID 当作唯一文件所有权证明。
系统级对象只给建议
扫描到 LaunchDaemon、系统扩展、后台项目等系统级残留时,卸载器会把它们从可删除数组中移除,转成 review-only。Local Network 权限、系统扩展和仍在运行的进程也只提示用户去系统设置处理。
这条边界很克制:能发现不等于获得删除授权。系统数据库与扩展注册通常需要官方卸载器或系统 API,手工删文件反而制造半卸载状态。
收尾操作
成功后可清理 defaults、ByHost 偏好、登录 helper、Dock 与 LaunchServices 注册;Homebrew autoremove 和刷新操作异步执行且不占用 TTY。所有实际路径仍通过应用保护与 mole_delete。
关键源码索引
bin/uninstall.sh:应用菜单、模式与元数据缓存。lib/uninstall/batch.sh:批量扫描、确认、执行与汇总。bin/uninstall.sh:身份归因、元数据缓存与残留处理。lib/core/pkg_receipts.sh:安装收据证据。
下一章比较两个更聚焦的空间回收器:项目产物 Purge 与安装包清理 Installer。