puremac-macos-cleaner

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PureMac macOS Cleaner

PureMac macOS清理工具

Skill by ara.so — Daily 2026 Skills collection.
PureMac is a free, native SwiftUI macOS application that cleans system junk, user caches, Xcode derived data, Homebrew caches, mail attachments, and purgeable APFS space. It is a privacy-respecting, open-source alternative to CleanMyMac X with no telemetry, no subscriptions, and no network calls.

来自ara.so的Skill——Daily 2026 Skills合集。
PureMac是一款免费的原生SwiftUI macOS应用,可清理系统垃圾、用户缓存、Xcode衍生数据、Homebrew缓存、邮件附件以及可释放的APFS空间。它是CleanMyMac X的隐私友好型开源替代方案,无遥测、无订阅、无网络请求。

Install

安装

Homebrew (recommended)

Homebrew(推荐)

bash
brew tap momenbasel/tap
brew install --cask puremac
bash
brew tap momenbasel/tap
brew install --cask puremac

Direct Download

直接下载

Download the latest
.app
from Releases, unzip, and drag to
/Applications
.
Releases下载最新的
.app
文件,解压后拖至
/Applications
目录。

Build from Source

从源码构建

bash
brew install xcodegen
git clone https://github.com/momenbasel/PureMac.git
cd PureMac
xcodegen generate
xcodebuild \
  -project PureMac.xcodeproj \
  -scheme PureMac \
  -configuration Release \
  -derivedDataPath build \
  build
open build/Build/Products/Release/PureMac.app
Requirements: macOS 13.0+, Swift 5.9, Xcode 15+.

bash
brew install xcodegen
git clone https://github.com/momenbasel/PureMac.git
cd PureMac
xcodegen generate
xcodebuild \
  -project PureMac.xcodeproj \
  -scheme PureMac \
  -configuration Release \
  -derivedDataPath build \
  build
open build/Build/Products/Release/PureMac.app
要求:macOS 13.0+、Swift 5.9、Xcode 15+。

Project Structure

项目结构

PureMac/
├── PureMac/
│   ├── App/
│   │   └── PureMacApp.swift          # App entry point
│   ├── Views/
│   │   ├── ContentView.swift         # Main window
│   │   ├── ScanView.swift            # Smart scan UI
│   │   ├── CategoryDetailView.swift  # Per-category drill-down
│   │   └── SettingsView.swift        # Schedule & preferences
│   ├── Models/
│   │   ├── CleanCategory.swift       # Category definitions
│   │   └── ScanResult.swift          # Scan result model
│   ├── Services/
│   │   ├── ScannerService.swift      # File scanning logic
│   │   ├── CleanerService.swift      # Deletion logic
│   │   ├── SchedulerService.swift    # Auto-clean scheduling
│   │   └── PurgeableService.swift    # APFS purgeable space
│   └── Utilities/
│       └── FileSizeFormatter.swift
├── project.yml                       # XcodeGen spec
└── CONTRIBUTING.md

PureMac/
├── PureMac/
│   ├── App/
│   │   └── PureMacApp.swift          # App入口
│   ├── Views/
│   │   ├── ContentView.swift         # 主窗口
│   │   ├── ScanView.swift            # 智能扫描UI
│   │   ├── CategoryDetailView.swift  # 分类详情页
│   │   └── SettingsView.swift        # 计划任务与偏好设置
│   ├── Models/
│   │   ├── CleanCategory.swift       # 分类定义
│   │   └── ScanResult.swift          # 扫描结果模型
│   ├── Services/
│   │   ├── ScannerService.swift      # 文件扫描逻辑
│   │   ├── CleanerService.swift      # 删除逻辑
│   │   ├── SchedulerService.swift    # 自动清理计划任务
│   │   └── PurgeableService.swift    # APFS可释放空间处理
│   └── Utilities/
│       └── FileSizeFormatter.swift
├── project.yml                       # XcodeGen配置文件
└── CONTRIBUTING.md

Core Concepts

核心概念

Clean Categories

清理分类

PureMac operates on named categories, each mapping to specific filesystem paths:
CategoryKey Paths
System Junk
/Library/Caches
,
/Library/Logs
,
/tmp
,
~/Library/Logs
User Cache
~/Library/Caches
, npm/pip/yarn/pnpm caches
Mail Attachments
~/Library/Mail Downloads
Trash
~/.Trash
Large & Old Files
~/Downloads
,
~/Documents
,
~/Desktop
(>100 MB or >1 year old)
Purgeable SpaceAPFS Time Machine snapshots via
tmutil
Xcode Junk
DerivedData
,
Archives
,
CoreSimulator/Caches
Homebrew Cache
~/Library/Caches/Homebrew
Large & Old Files are never auto-selected — the user must explicitly choose items before cleaning.

PureMac基于命名分类运行,每个分类对应特定的文件系统路径:
分类关键路径
System Junk(系统垃圾)
/Library/Caches
,
/Library/Logs
,
/tmp
,
~/Library/Logs
User Cache(用户缓存)
~/Library/Caches
, npm/pip/yarn/pnpm缓存
Mail Attachments(邮件附件)
~/Library/Mail Downloads
Trash(废纸篓)
~/.Trash
Large & Old Files(大文件与旧文件)
~/Downloads
,
~/Documents
,
~/Desktop
(大于100MB或超过1年)
Purgeable Space(可释放空间)通过
tmutil
处理APFS Time Machine快照
Xcode Junk(Xcode垃圾)
DerivedData
,
Archives
,
CoreSimulator/Caches
Homebrew Cache(Homebrew缓存)
~/Library/Caches/Homebrew
大文件与旧文件永远不会被自动选中——用户必须明确选择项目后才能清理。

Working with the Codebase

代码库使用指南

Adding a New Clean Category

添加新的清理分类

  1. Define the category in
    CleanCategory.swift
    :
swift
// CleanCategory.swift
enum CleanCategory: String, CaseIterable, Identifiable {
    case systemJunk       = "System Junk"
    case userCache        = "User Cache"
    case mailAttachments  = "Mail Attachments"
    case trash            = "Trash"
    case largeOldFiles    = "Large & Old Files"
    case purgeableSpace   = "Purgeable Space"
    case xcodeJunk        = "Xcode Junk"
    case homebrewCache    = "Homebrew Cache"
    // Add your new category here:
    case gradleCache      = "Gradle Cache"

    var id: String { rawValue }

    var iconName: String {
        switch self {
        case .systemJunk:      return "trash.circle"
        case .userCache:       return "internaldrive"
        case .xcodeJunk:       return "hammer"
        case .homebrewCache:   return "shippingbox"
        case .gradleCache:     return "archivebox"   // new
        default:               return "folder"
        }
    }
}
  1. Add scanning logic in
    ScannerService.swift
    :
swift
// ScannerService.swift
func scanCategory(_ category: CleanCategory) async throws -> ScanResult {
    switch category {
    case .gradleCache:
        return try await scanPaths([
            FileManager.default.homeDirectoryForCurrentUser
                .appendingPathComponent(".gradle/caches")
        ])
    // ...existing cases
    default:
        throw ScannerError.unsupportedCategory
    }
}

private func scanPaths(_ urls: [URL]) async throws -> ScanResult {
    var files: [ScannedFile] = []
    let fm = FileManager.default

    for url in urls {
        guard fm.fileExists(atPath: url.path) else { continue }
        let enumerator = fm.enumerator(
            at: url,
            includingPropertiesForKeys: [.fileSizeKey, .contentModificationDateKey],
            options: [.skipsHiddenFiles]
        )
        while let fileURL = enumerator?.nextObject() as? URL {
            let values = try fileURL.resourceValues(forKeys: [.fileSizeKey, .contentModificationDateKey])
            let size = Int64(values.fileSize ?? 0)
            let modified = values.contentModificationDate ?? Date.distantPast
            files.append(ScannedFile(url: fileURL, size: size, modifiedDate: modified))
        }
    }

    let totalBytes = files.reduce(0) { $0 + $1.size }
    return ScanResult(category: .gradleCache, files: files, totalBytes: totalBytes)
}
  1. Add cleaning logic in
    CleanerService.swift
    :
swift
// CleanerService.swift
func clean(_ result: ScanResult, selectedFiles: Set<URL>? = nil) async throws -> Int64 {
    let filesToDelete = selectedFiles.map { Array($0) } ?? result.files.map(\.url)
    var bytesFreed: Int64 = 0
    let fm = FileManager.default

    for url in filesToDelete {
        do {
            let attrs = try fm.attributesOfItem(atPath: url.path)
            let size = attrs[.size] as? Int64 ?? 0
            try fm.removeItem(at: url)
            bytesFreed += size
        } catch {
            // Log but continue — don't abort on single-file failure
            print("Failed to delete \(url.lastPathComponent): \(error.localizedDescription)")
        }
    }
    return bytesFreed
}

  1. CleanCategory.swift
    中定义分类
swift
// CleanCategory.swift
enum CleanCategory: String, CaseIterable, Identifiable {
    case systemJunk       = "System Junk"
    case userCache        = "User Cache"
    case mailAttachments  = "Mail Attachments"
    case trash            = "Trash"
    case largeOldFiles    = "Large & Old Files"
    case purgeableSpace   = "Purgeable Space"
    case xcodeJunk        = "Xcode Junk"
    case homebrewCache    = "Homebrew Cache"
    // 添加你的新分类:
    case gradleCache      = "Gradle Cache"

    var id: String { rawValue }

    var iconName: String {
        switch self {
        case .systemJunk:      return "trash.circle"
        case .userCache:       return "internaldrive"
        case .xcodeJunk:       return "hammer"
        case .homebrewCache:   return "shippingbox"
        case .gradleCache:     return "archivebox"   // 新增
        default:               return "folder"
        }
    }
}
  1. ScannerService.swift
    中添加扫描逻辑
swift
// ScannerService.swift
func scanCategory(_ category: CleanCategory) async throws -> ScanResult {
    switch category {
    case .gradleCache:
        return try await scanPaths([
            FileManager.default.homeDirectoryForCurrentUser
                .appendingPathComponent(".gradle/caches")
        ])
    // ...现有分支
    default:
        throw ScannerError.unsupportedCategory
    }
}

private func scanPaths(_ urls: [URL]) async throws -> ScanResult {
    var files: [ScannedFile] = []
    let fm = FileManager.default

    for url in urls {
        guard fm.fileExists(atPath: url.path) else { continue }
        let enumerator = fm.enumerator(
            at: url,
            includingPropertiesForKeys: [.fileSizeKey, .contentModificationDateKey],
            options: [.skipsHiddenFiles]
        )
        while let fileURL = enumerator?.nextObject() as? URL {
            let values = try fileURL.resourceValues(forKeys: [.fileSizeKey, .contentModificationDateKey])
            let size = Int64(values.fileSize ?? 0)
            let modified = values.contentModificationDate ?? Date.distantPast
            files.append(ScannedFile(url: fileURL, size: size, modifiedDate: modified))
        }
    }

    let totalBytes = files.reduce(0) { $0 + $1.size }
    return ScanResult(category: .gradleCache, files: files, totalBytes: totalBytes)
}
  1. CleanerService.swift
    中添加清理逻辑
swift
// CleanerService.swift
func clean(_ result: ScanResult, selectedFiles: Set<URL>? = nil) async throws -> Int64 {
    let filesToDelete = selectedFiles.map { Array($0) } ?? result.files.map(\.url)
    var bytesFreed: Int64 = 0
    let fm = FileManager.default

    for url in filesToDelete {
        do {
            let attrs = try fm.attributesOfItem(atPath: url.path)
            let size = attrs[.size] as? Int64 ?? 0
            try fm.removeItem(at: url)
            bytesFreed += size
        } catch {
            // 记录日志但继续执行——不因单个文件删除失败而终止
            print("Failed to delete \(url.lastPathComponent): \(error.localizedDescription)")
        }
    }
    return bytesFreed
}

Scheduled Auto-Cleaning

定时自动清理

Configure via Settings → Schedule tab. Intervals: hourly, 3h, 6h, 12h, daily, weekly, biweekly, monthly.
swift
// SchedulerService.swift — how scheduling is implemented
import UserNotifications

class SchedulerService: ObservableObject {
    @AppStorage("schedulingEnabled")    var schedulingEnabled: Bool = false
    @AppStorage("cleaningInterval")     var cleaningInterval: String = "daily"
    @AppStorage("autoCleanAfterScan")   var autoCleanAfterScan: Bool = false
    @AppStorage("autoPurgePurgeable")   var autoPurgePurgeable: Bool = false

    private var timer: Timer?

    func scheduleIfNeeded() {
        timer?.invalidate()
        guard schedulingEnabled else { return }
        let interval = intervalSeconds(for: cleaningInterval)
        timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
            Task { await self?.runScheduledClean() }
        }
    }

    private func intervalSeconds(for key: String) -> TimeInterval {
        switch key {
        case "hourly":    return 3_600
        case "3h":        return 10_800
        case "6h":        return 21_600
        case "12h":       return 43_200
        case "daily":     return 86_400
        case "weekly":    return 604_800
        case "biweekly":  return 1_209_600
        case "monthly":   return 2_592_000
        default:          return 86_400
        }
    }

    @MainActor
    private func runScheduledClean() async {
        let scanner = ScannerService()
        let cleaner = CleanerService()
        for category in CleanCategory.allCases where category != .largeOldFiles {
            if let result = try? await scanner.scanCategory(category), autoCleanAfterScan {
                _ = try? await cleaner.clean(result)
            }
        }
        if autoPurgePurgeable {
            try? await PurgeableService.shared.purge()
        }
    }
}
Enable scheduling programmatically:
swift
let scheduler = SchedulerService()
scheduler.cleaningInterval = "weekly"
scheduler.autoCleanAfterScan = true
scheduler.autoPurgePurgeable = false
scheduler.schedulingEnabled = true
scheduler.scheduleIfNeeded()

通过「设置」→「计划任务」标签页配置。时间间隔:每小时、3小时、6小时、12小时、每天、每周、每两周、每月。
swift
// SchedulerService.swift — 计划任务实现方式
import UserNotifications

class SchedulerService: ObservableObject {
    @AppStorage("schedulingEnabled")    var schedulingEnabled: Bool = false
    @AppStorage("cleaningInterval")     var cleaningInterval: String = "daily"
    @AppStorage("autoCleanAfterScan")   var autoCleanAfterScan: Bool = false
    @AppStorage("autoPurgePurgeable")   var autoPurgePurgeable: Bool = false

    private var timer: Timer?

    func scheduleIfNeeded() {
        timer?.invalidate()
        guard schedulingEnabled else { return }
        let interval = intervalSeconds(for: cleaningInterval)
        timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
            Task { await self?.runScheduledClean() }
        }
    }

    private func intervalSeconds(for key: String) -> TimeInterval {
        switch key {
        case "hourly":    return 3_600
        case "3h":        return 10_800
        case "6h":        return 21_600
        case "12h":       return 43_200
        case "daily":     return 86_400
        case "weekly":    return 604_800
        case "biweekly":  return 1_209_600
        case "monthly":   return 2_592_000
        default:          return 86_400
        }
    }

    @MainActor
    private func runScheduledClean() async {
        let scanner = ScannerService()
        let cleaner = CleanerService()
        for category in CleanCategory.allCases where category != .largeOldFiles {
            if let result = try? await scanner.scanCategory(category), autoCleanAfterScan {
                _ = try? await cleaner.clean(result)
            }
        }
        if autoPurgePurgeable {
            try? await PurgeableService.shared.purge()
        }
    }
}
以编程方式启用计划任务:
swift
let scheduler = SchedulerService()
scheduler.cleaningInterval = "weekly"
scheduler.autoCleanAfterScan = true
scheduler.autoPurgePurgeable = false
scheduler.schedulingEnabled = true
scheduler.scheduleIfNeeded()

Purgeable Space (APFS Snapshots)

可释放空间(APFS快照)

PureMac uses
tmutil
to delete local Time Machine snapshots — this is the only operation requiring elevated privileges:
swift
// PurgeableService.swift
import Foundation

class PurgeableService {
    static let shared = PurgeableService()

    func listSnapshots() async throws -> [String] {
        let output = try await shell("tmutil listlocalsnapshots /")
        return output
            .split(separator: "\n")
            .map(String.init)
            .filter { $0.hasPrefix("com.apple.TimeMachine") }
    }

    func purge() async throws {
        let snapshots = try await listSnapshots()
        for snapshot in snapshots {
            try await shell("tmutil deletelocalsnapshots \(snapshot)")
        }
    }

    @discardableResult
    private func shell(_ command: String) async throws -> String {
        try await withCheckedThrowingContinuation { continuation in
            let task = Process()
            task.launchPath = "/bin/bash"
            task.arguments = ["-c", command]
            let pipe = Pipe()
            task.standardOutput = pipe
            task.terminationHandler = { _ in
                let data = pipe.fileHandleForReading.readDataToEndOfFile()
                continuation.resume(returning: String(data: data, encoding: .utf8) ?? "")
            }
            do { try task.run() } catch { continuation.resume(throwing: error) }
        }
    }
}

PureMac使用
tmutil
删除本地Time Machine快照——这是唯一需要提升权限的操作:
swift
// PurgeableService.swift
import Foundation

class PurgeableService {
    static let shared = PurgeableService()

    func listSnapshots() async throws -> [String] {
        let output = try await shell("tmutil listlocalsnapshots /")
        return output
            .split(separator: "\n")
            .map(String.init)
            .filter { $0.hasPrefix("com.apple.TimeMachine") }
    }

    func purge() async throws {
        let snapshots = try await listSnapshots()
        for snapshot in snapshots {
            try await shell("tmutil deletelocalsnapshots \(snapshot)")
        }
    }

    @discardableResult
    private func shell(_ command: String) async throws -> String {
        try await withCheckedThrowingContinuation { continuation in
            let task = Process()
            task.launchPath = "/bin/bash"
            task.arguments = ["-c", command]
            let pipe = Pipe()
            task.standardOutput = pipe
            task.terminationHandler = { _ in
                let data = pipe.fileHandleForReading.readDataToEndOfFile()
                continuation.resume(returning: String(data: data, encoding: .utf8) ?? "")
            }
            do { try task.run() } catch { continuation.resume(throwing: error) }
        }
    }
}

Xcode Cache Paths

Xcode缓存路径

swift
// Paths cleaned by the Xcode Junk category
let home = FileManager.default.homeDirectoryForCurrentUser

let xcodePaths: [URL] = [
    home.appendingPathComponent("Library/Developer/Xcode/DerivedData"),
    home.appendingPathComponent("Library/Developer/Xcode/Archives"),
    home.appendingPathComponent("Library/Developer/CoreSimulator/Caches"),
]
Scan these and safely delete their contents without removing the directories themselves.

swift
// Xcode Junk分类清理的路径
let home = FileManager.default.homeDirectoryForCurrentUser

let xcodePaths: [URL] = [
    home.appendingPathComponent("Library/Developer/Xcode/DerivedData"),
    home.appendingPathComponent("Library/Developer/Xcode/Archives"),
    home.appendingPathComponent("Library/Developer/CoreSimulator/Caches"),
]
扫描这些路径并安全删除其内容,不会删除目录本身。

SwiftUI View Patterns

SwiftUI视图模式

Scan Progress View

扫描进度视图

swift
// Example: triggering a scan from a SwiftUI view
struct ScanView: View {
    @StateObject private var scanner = ScannerService()
    @State private var results: [ScanResult] = []
    @State private var isScanning = false

    var body: some View {
        VStack {
            if isScanning {
                ProgressView("Scanning…")
            } else {
                Button("Smart Scan") {
                    Task { await runScan() }
                }
            }
            List(results, id: \.category) { result in
                CategoryRow(result: result)
            }
        }
    }

    private func runScan() async {
        isScanning = true
        results = []
        for category in CleanCategory.allCases {
            if let result = try? await scanner.scanCategory(category) {
                results.append(result)
            }
        }
        isScanning = false
    }
}
swift
// 示例:从SwiftUI视图触发扫描
struct ScanView: View {
    @StateObject private var scanner = ScannerService()
    @State private var results: [ScanResult] = []
    @State private var isScanning = false

    var body: some View {
        VStack {
            if isScanning {
                ProgressView("Scanning…")
            } else {
                Button("Smart Scan") {
                    Task { await runScan() }
                }
            }
            List(results, id: \.category) { result in
                CategoryRow(result: result)
            }
        }
    }

    private func runScan() async {
        isScanning = true
        results = []
        for category in CleanCategory.allCases {
            if let result = try? await scanner.scanCategory(category) {
                results.append(result)
            }
        }
        isScanning = false
    }
}

File Inspector (Click-to-Inspect)

文件检查器(点击查看)

swift
// Show files before deletion — users can deselect
struct CategoryDetailView: View {
    let result: ScanResult
    @State private var selected: Set<URL> = []
    @State private var cleaned = false

    var body: some View {
        List(result.files, id: \.url, selection: $selected) { file in
            HStack {
                Image(systemName: "doc")
                Text(file.url.lastPathComponent)
                Spacer()
                Text(ByteCountFormatter.string(fromByteCount: file.size, countStyle: .file))
                    .foregroundStyle(.secondary)
            }
        }
        .toolbar {
            Button("Clean Selected") {
                Task {
                    let cleaner = CleanerService()
                    _ = try? await cleaner.clean(result, selectedFiles: selected)
                    cleaned = true
                }
            }
            .disabled(selected.isEmpty)
        }
    }
}

swift
// 删除前显示文件——用户可以取消选择
struct CategoryDetailView: View {
    let result: ScanResult
    @State private var selected: Set<URL> = []
    @State private var cleaned = false

    var body: some View {
        List(result.files, id: \.url, selection: $selected) { file in
            HStack {
                Image(systemName: "doc")
                Text(file.url.lastPathComponent)
                Spacer()
                Text(ByteCountFormatter.string(fromByteCount: file.size, countStyle: .file))
                    .foregroundStyle(.secondary)
            }
        }
        .toolbar {
            Button("Clean Selected") {
                Task {
                    let cleaner = CleanerService()
                    _ = try? await cleaner.clean(result, selectedFiles: selected)
                    cleaned = true
                }
            }
            .disabled(selected.isEmpty)
        }
    }
}

Configuration (AppStorage Keys)

配置(AppStorage键)

All preferences are stored in
UserDefaults
via
@AppStorage
:
KeyTypeDefaultDescription
schedulingEnabled
BoolfalseEnable scheduled cleaning
cleaningInterval
String"daily"Interval key (see above)
autoCleanAfterScan
BoolfalseAuto-clean after scheduled scan
autoPurgePurgeable
BoolfalseAuto-purge APFS snapshots
Read/write from anywhere:
swift
UserDefaults.standard.set(true,    forKey: "schedulingEnabled")
UserDefaults.standard.set("weekly", forKey: "cleaningInterval")

所有偏好设置通过
@AppStorage
存储在
UserDefaults
中:
类型默认值描述
schedulingEnabled
Boolfalse启用定时清理
cleaningInterval
String"daily"时间间隔键(见上文)
autoCleanAfterScan
Boolfalse定时扫描后自动清理
autoPurgePurgeable
Boolfalse自动释放APFS快照
可在任意位置读写:
swift
UserDefaults.standard.set(true,    forKey: "schedulingEnabled")
UserDefaults.standard.set("weekly", forKey: "cleaningInterval")

Building & Testing

构建与测试

bash
undefined
bash
undefined

Generate Xcode project from project.yml

从project.yml生成Xcode项目

xcodegen generate
xcodegen generate

Build Release

构建Release版本

xcodebuild
-project PureMac.xcodeproj
-scheme PureMac
-configuration Release
-derivedDataPath build
build
xcodebuild
-project PureMac.xcodeproj
-scheme PureMac
-configuration Release
-derivedDataPath build
build

Run tests

运行测试

xcodebuild test
-project PureMac.xcodeproj
-scheme PureMac
-destination 'platform=macOS'
xcodebuild test
-project PureMac.xcodeproj
-scheme PureMac
-destination 'platform=macOS'

Open built app

打开已构建的应用

open build/Build/Products/Release/PureMac.app

---
open build/Build/Products/Release/PureMac.app

---

Contributing

贡献指南

  1. Fork and clone the repo.
  2. Run
    xcodegen generate
    to create the
    .xcodeproj
    .
  3. Create a feature branch:
    git checkout -b feature/gradle-cache-cleaning
  4. Follow existing patterns in
    ScannerService
    /
    CleanerService
    .
  5. Never add network calls, analytics SDKs, or telemetry of any kind.
  6. Large & Old Files must never be auto-selected for deletion.
  7. Open a PR against
    main
    .
See CONTRIBUTING.md for full guidelines.

  1. Fork并克隆仓库。
  2. 运行
    xcodegen generate
    创建
    .xcodeproj
    文件。
  3. 创建特性分支:
    git checkout -b feature/gradle-cache-cleaning
  4. 遵循
    ScannerService
    /
    CleanerService
    中的现有模式。
  5. 禁止添加网络请求、分析SDK或任何类型的遥测功能。
  6. 大文件与旧文件永远不能被自动选中进行删除。
  7. 针对
    main
    分支提交PR。
完整指南请查看CONTRIBUTING.md

Troubleshooting

故障排除

ProblemSolution
xcodegen: command not found
brew install xcodegen
App blocked by GatekeeperThe release build is notarized; if building from source, run
xattr -cr PureMac.app
Purgeable scan returns 0 bytesNo local Time Machine snapshots exist — this is normal if TM is off
Xcode paths not foundXcode has not been used yet or DerivedData was already cleared
tmutil
requires password
Purgeable purge may prompt for admin credentials — this is expected macOS behavior
Scheduled cleaning not triggeringEnsure the app is running (it is not a background daemon); check Settings → Schedule

问题解决方案
xcodegen: command not found
brew install xcodegen
应用被Gatekeeper阻止发布版本已通过公证;如果从源码构建,运行
xattr -cr PureMac.app
可释放空间扫描返回0字节不存在本地Time Machine快照——如果TM关闭则属于正常情况
Xcode路径未找到Xcode尚未使用或DerivedData已被清除
tmutil
需要密码
释放可释放空间可能会提示管理员凭据——这是macOS的预期行为
定时清理未触发确保应用正在运行(它不是后台守护进程);检查「设置」→「计划任务」

Safety Guarantees

安全保障

  • Never deletes system-critical files or application bundles.
  • Only removes caches, logs, temp files, and user-selected items.
  • Large & Old Files require explicit user selection before deletion.
  • Purgeable operations target only APFS Time Machine snapshots — not free space.
  • All filesystem operations use
    FileManager.removeItem(at:)
    — no
    rm -rf
    shell calls for regular cleaning.
  • 绝不删除系统关键文件或应用包。
  • 仅删除缓存、日志、临时文件以及用户选择的项目。
  • 大文件与旧文件需要用户明确选择才能删除。
  • 可释放空间操作仅针对APFS Time Machine快照——不涉及可用空间。
  • 所有文件系统操作使用
    FileManager.removeItem(at:)
    ——常规清理不使用
    rm -rf
    shell命令。