feat(cleaner): 新增 Homebrew 下载缓存清理

This commit is contained in:
ysicing committed 2026-09-27 22:10:45 +08:00
1 parent ce18018f27
commit f1e7de9b0e
11 files changed
+87 -4

No files matched your search

@@ -6,7 +6,7 @@ public enum RuleCatalog {
public static func rules() -> [CleanRule] {
[userCaches, logs] + browsers.map(browserRule) + [xcodeDerivedData, simulatorCaches,
npmCache, yarnCache, pnpmCache, bunCache,
goCache, rustCache, uvCache, xcodeArchives,
goCache, rustCache, uvCache, homebrewCache, xcodeArchives,
incompleteDownloads, installers, trash]
}
@@ -16,7 +16,7 @@ public enum RuleCatalog {
static let excludedCacheNames: Set<String> = [
"Google", "Firefox", "Microsoft Edge", "BraveSoftware", "Arc",
"com.google.Chrome", "com.microsoft.edgemac", "com.brave.Browser", "company.thebrowser.Browser",
"org.mozilla.firefox", "com.apple.Safari",
"org.mozilla.firefox", "com.apple.Safari", "Homebrew",
]
static let userCaches = CleanRule(
@@ -215,6 +215,30 @@ public enum RuleCatalog {
try childrenIfExists(of: env.home + "/.cache/uv")
}
static let homebrewCache = CleanRule(
id: "developer.homebrew", category: .developer, title: tr("Homebrew 下载缓存"),
detail: tr("超过 1 天的 Homebrew 下载文件,不影响已安装软件"), symbol: "shippingbox",
selectedByDefault: false, minimumAge: 86_400
) { env in
// 仅识别 Homebrew 已完成的内容寻址下载;同目录中的临时文件和其他缓存不属于本规则。
let cacheDirectory = URL(fileURLWithPath: env.home + "/Library/Caches/Homebrew")
let downloads = cacheDirectory.appendingPathComponent("downloads")
guard FileManager.default.fileExists(atPath: downloads.path) else { return [] }
guard try cacheDirectory.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink != true,
try downloads.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink != true else {
return []
}
return try children(of: downloads.path).filter { url in
let name = url.lastPathComponent
guard !name.hasSuffix(".incomplete"),
name.range(of: #"^[0-9a-f]{64}--.+$"#, options: .regularExpression) != nil,
let values = try? url.resourceValues(forKeys: [.isRegularFileKey, .isSymbolicLinkKey]) else {
return false
}
return values.isRegularFile == true && values.isSymbolicLink != true
}
}
static let xcodeArchives = CleanRule(
id: "developer.archives", category: .developer, title: tr("Xcode 归档"),
detail: tr("打包生成的 .xcarchive,删除后无法重新符号化旧版本崩溃日志"), symbol: "archivebox.fill",
@@ -116,6 +116,8 @@ uv 缓存 ذاكرة uv المؤقتة
Yarn 缓存 ذاكرة Yarn المؤقتة
pnpm 缓存 ذاكرة pnpm المؤقتة
Bun 缓存 ذاكرة Bun المؤقتة
Homebrew 下载缓存 ذاكرة تنزيلات Homebrew المؤقتة
超过 1 天的 Homebrew 下载文件,不影响已安装软件 ملفات تنزيل Homebrew الأقدم من يوم واحد؛ لا تتأثر البرامج المثبتة
npm 下载缓存,由 npm 自行清理 ذاكرة تنزيل npm المؤقتة، ينظفها npm بنفسه
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 ذاكرة Yarn العامة ومرآة العمل دون اتصال، ينظفها Yarn بنفسه
pnpm 内容寻址存储,仅清理未被引用的包 مخزن pnpm المعنون بالمحتوى؛ تزال الحزم غير المشار إليها فقط
1 # Copyright (C) 2026 ysicing
116 Yarn 缓存
117 pnpm 缓存
118 Bun 缓存
119 Homebrew 下载缓存
120 超过 1 天的 Homebrew 下载文件,不影响已安装软件
121 npm 下载缓存,由 npm 自行清理
122 Yarn 全局与离线镜像缓存,由 Yarn 自行清理
123 pnpm 内容寻址存储,仅清理未被引用的包
@@ -116,6 +116,8 @@ uv 缓存 uv-Cache
Yarn 缓存 Yarn-Cache
pnpm 缓存 pnpm-Cache
Bun 缓存 Bun-Cache
Homebrew 下载缓存 Homebrew-Download-Cache
超过 1 天的 Homebrew 下载文件,不影响已安装软件 Homebrew-Downloads, die älter als einen Tag sind; installierte Software bleibt erhalten
npm 下载缓存,由 npm 自行清理 npm-Download-Cache; wird von npm selbst bereinigt
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Globaler Yarn- und Offline-Mirror-Cache; wird von Yarn selbst bereinigt
pnpm 内容寻址存储,仅清理未被引用的包 Inhaltsadressierter pnpm-Speicher; nur nicht referenzierte Pakete werden entfernt
1 # Copyright (C) 2026 ysicing
116 Yarn 缓存
117 pnpm 缓存
118 Bun 缓存
119 Homebrew 下载缓存
120 超过 1 天的 Homebrew 下载文件,不影响已安装软件
121 npm 下载缓存,由 npm 自行清理
122 Yarn 全局与离线镜像缓存,由 Yarn 自行清理
123 pnpm 内容寻址存储,仅清理未被引用的包
@@ -116,6 +116,8 @@ uv 缓存 Caché de uv
Yarn 缓存 Caché de Yarn
pnpm 缓存 Caché de pnpm
Bun 缓存 Caché de Bun
Homebrew 下载缓存 Caché de descargas de Homebrew
超过 1 天的 Homebrew 下载文件,不影响已安装软件 Descargas de Homebrew de más de un día; el software instalado no se verá afectado
npm 下载缓存,由 npm 自行清理 Caché de descargas de npm, limpiada por npm
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Caché global y espejo sin conexión de Yarn, limpiadas por Yarn
pnpm 内容寻址存储,仅清理未被引用的包 Almacén direccionado por contenido de pnpm; solo se eliminan paquetes sin referencias
1 # Copyright (C) 2026 ysicing
116 Yarn 缓存
117 pnpm 缓存
118 Bun 缓存
119 Homebrew 下载缓存
120 超过 1 天的 Homebrew 下载文件,不影响已安装软件
121 npm 下载缓存,由 npm 自行清理
122 Yarn 全局与离线镜像缓存,由 Yarn 自行清理
123 pnpm 内容寻址存储,仅清理未被引用的包
@@ -116,6 +116,8 @@ uv 缓存 Cache uv
Yarn 缓存 Cache Yarn
pnpm 缓存 Cache pnpm
Bun 缓存 Cache Bun
Homebrew 下载缓存 Cache des téléchargements Homebrew
超过 1 天的 Homebrew 下载文件,不影响已安装软件 Téléchargements Homebrew datant de plus d’un jour ; les logiciels installés ne sont pas affectés
npm 下载缓存,由 npm 自行清理 Cache de téléchargement npm, nettoyé par npm
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Caches global et miroir hors ligne de Yarn, nettoyés par Yarn
pnpm 内容寻址存储,仅清理未被引用的包 Stockage adressé par contenu de pnpm ; seuls les paquets non référencés sont supprimés
1 # Copyright (C) 2026 ysicing
116 Yarn 缓存
117 pnpm 缓存
118 Bun 缓存
119 Homebrew 下载缓存
120 超过 1 天的 Homebrew 下载文件,不影响已安装软件
121 npm 下载缓存,由 npm 自行清理
122 Yarn 全局与离线镜像缓存,由 Yarn 自行清理
123 pnpm 内容寻址存储,仅清理未被引用的包
@@ -116,6 +116,8 @@ uv 缓存 uvキャッシュ
Yarn 缓存 Yarnキャッシュ
pnpm 缓存 pnpmキャッシュ
Bun 缓存 Bunキャッシュ
Homebrew 下载缓存 Homebrewダウンロードキャッシュ
超过 1 天的 Homebrew 下载文件,不影响已安装软件 1日以上前のHomebrewダウンロードファイル。インストール済みのソフトウェアには影響しません
npm 下载缓存,由 npm 自行清理 npmのダウンロードキャッシュ。npmでクリーンアップします
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Yarnのグローバルキャッシュとオフラインミラー。Yarnでクリーンアップします
pnpm 内容寻址存储,仅清理未被引用的包 pnpmのコンテンツアドレスストア。参照されていないパッケージのみ削除します
1 # Copyright (C) 2026 ysicing
116 Yarn 缓存
117 pnpm 缓存
118 Bun 缓存
119 Homebrew 下载缓存
120 超过 1 天的 Homebrew 下载文件,不影响已安装软件
121 npm 下载缓存,由 npm 自行清理
122 Yarn 全局与离线镜像缓存,由 Yarn 自行清理
123 pnpm 内容寻址存储,仅清理未被引用的包
@@ -116,6 +116,8 @@ uv 缓存 uv 캐시
Yarn 缓存 Yarn 캐시
pnpm 缓存 pnpm 캐시
Bun 缓存 Bun 캐시
Homebrew 下载缓存 Homebrew 다운로드 캐시
超过 1 天的 Homebrew 下载文件,不影响已安装软件 1일 이상 지난 Homebrew 다운로드 파일만 정리하며 설치된 소프트웨어에는 영향을 주지 않습니다
npm 下载缓存,由 npm 自行清理 npm 다운로드 캐시이며 npm이 직접 정리합니다
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Yarn 전역 및 오프라인 미러 캐시이며 Yarn이 직접 정리합니다
pnpm 内容寻址存储,仅清理未被引用的包 pnpm 콘텐츠 주소 지정 저장소이며 참조되지 않은 패키지만 정리합니다
1 # Copyright (C) 2026 ysicing
116 Yarn 缓存
117 pnpm 缓存
118 Bun 缓存
119 Homebrew 下载缓存
120 超过 1 天的 Homebrew 下载文件,不影响已安装软件
121 npm 下载缓存,由 npm 自行清理
122 Yarn 全局与离线镜像缓存,由 Yarn 自行清理
123 pnpm 内容寻址存储,仅清理未被引用的包
@@ -117,6 +117,8 @@ uv 缓存 uv 緩存
Yarn 缓存 Yarn 緩存
pnpm 缓存 pnpm 緩存
Bun 缓存 Bun 緩存
Homebrew 下载缓存 Homebrew 下載緩存
超过 1 天的 Homebrew 下载文件,不影响已安装软件 超過 1 天的 Homebrew 下載檔案,不影響已安裝軟件
npm 下载缓存,由 npm 自行清理 npm 下載緩存,由 npm 自行清理
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Yarn 全局與離線鏡像緩存,由 Yarn 自行清理
pnpm 内容寻址存储,仅清理未被引用的包 pnpm 內容尋址存儲,僅清理未被引用的包
1 # Copyright (c) 2026 GiantAccel, LLC
117 Yarn 缓存
118 pnpm 缓存
119 Bun 缓存
120 Homebrew 下载缓存
121 超过 1 天的 Homebrew 下载文件,不影响已安装软件
122 npm 下载缓存,由 npm 自行清理
123 Yarn 全局与离线镜像缓存,由 Yarn 自行清理
124 pnpm 内容寻址存储,仅清理未被引用的包
@@ -241,6 +241,8 @@ uv 缓存 uv Cache
Yarn 缓存 Yarn Cache
pnpm 缓存 pnpm Cache
Bun 缓存 Bun Cache
Homebrew 下载缓存 Homebrew Download Cache
超过 1 天的 Homebrew 下载文件,不影响已安装软件 Homebrew downloads older than one day; installed software is unaffected
npm 下载缓存,由 npm 自行清理 npm download cache; cleaned by npm
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Yarn global and offline mirror caches; cleaned by Yarn
pnpm 内容寻址存储,仅清理未被引用的包 pnpm content-addressable store; only unreferenced packages are removed
@@ -66,11 +66,11 @@ public final class CleanerController {
task = Task {
let result = await CleanEngine.clean(scans, selected: selection, preferTrash: preferTrash, environment: environment)
report = result
phase = .finished
// 清理后重新计算剩余可清理空间
let rescanned = await CleanEngine.scan(rules, environment: Self.environment())
self.scans = rescanned
lastScan = Date()
phase = .finished
}
}
@@ -122,12 +122,55 @@ struct DeveloperCacheRuleTests {
let rules = Dictionary(uniqueKeysWithValues: RuleCatalog.rules().map { ($0.id, $0) })
for id in ["developer.npm", "developer.yarn", "developer.pnpm", "developer.bun",
"developer.go", "developer.rust", "developer.uv"] {
"developer.go", "developer.rust", "developer.uv", "developer.homebrew"] {
#expect(rules[id]?.category == .developer)
#expect(rules[id]?.selectedByDefault == false)
}
}
@Test func homebrewRuleOnlyCleansCompletedOlderDownloads() async throws {
let home = try FakeHome()
defer { home.remove() }
let hash = String(repeating: "a", count: 64)
let oldDownload = try home.file("Library/Caches/Homebrew/downloads/\(hash)--wget-1.0.tar.gz", age: 172_800)
let recentDownload = try home.file("Library/Caches/Homebrew/downloads/\(hash)--curl-1.0.tar.gz", age: 60)
let incomplete = try home.file("Library/Caches/Homebrew/downloads/\(hash)--git-1.0.tar.gz.incomplete", age: 172_800)
let unrelated = try home.file("Library/Caches/Homebrew/downloads/notes.txt", age: 172_800)
let backup = try home.file("Library/Caches/Homebrew/Backup/important.txt", age: 172_800)
let environment = home.environment()
let generic = try RuleCatalog.userCaches.locate(environment)
#expect(!generic.contains { $0.lastPathComponent == "Homebrew" })
let scans = await CleanEngine.scan([RuleCatalog.homebrewCache], environment: environment)
let scan = try #require(scans.first)
#expect(scan.items.map { $0.url.resolvingSymlinksInPath() } == [oldDownload.resolvingSymlinksInPath()])
let report = await CleanEngine.clean(scans, selected: [RuleCatalog.homebrewCache.id],
preferTrash: false, environment: environment, log: nil)
#expect(report.removedCount == 1)
#expect(!FileManager.default.fileExists(atPath: oldDownload.path))
for retained in [recentDownload, incomplete, unrelated, backup] {
#expect(FileManager.default.fileExists(atPath: retained.path))
}
}
@Test func homebrewRuleRejectsLinkedDownloadDirectory() async throws {
let home = try FakeHome()
defer { home.remove() }
let hash = String(repeating: "b", count: 64)
let backup = try home.file("Library/Caches/Homebrew/Backup/\(hash)--archive.tar.gz", age: 172_800)
let downloads = home.root.appendingPathComponent("Library/Caches/Homebrew/downloads")
try FileManager.default.createSymbolicLink(at: downloads, withDestinationURL: backup.deletingLastPathComponent())
let environment = home.environment()
#expect(try RuleCatalog.homebrewCache.locate(environment).isEmpty)
let scans = await CleanEngine.scan([RuleCatalog.homebrewCache], environment: environment)
#expect(scans.first?.items.isEmpty == true)
#expect(FileManager.default.fileExists(atPath: backup.path))
}
@Test func packageManagerCachesUseTheirBrandLogos() {
let rules = Dictionary(uniqueKeysWithValues: RuleCatalog.rules().map { ($0.id, $0) })
let expected = [