Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions DevCleaner/Base/CmdLine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ public final class CmdLine {
return .derivedData
case "docs-cache":
return .documentationCache
case "user-data-cache":
return .userDataCache
case "old-logs":
return .logs
case "old-documentation":
Expand Down Expand Up @@ -169,8 +171,8 @@ public final class CmdLine {
let argsParser = ArgumentsParser(toolName: "dev-cleaner", description: "Reclaims storage that Xcode stores in caches and old files")
argsParser.addOption(name: "info", description: "Show all items available to clean.")
argsParser.addOptionWithValue(name: "clean",
description: "Perform cleaning of given items. Available options: all, device-support, archives, derived-data, docs-cache, old-logs, old-documentation. If you want to clean all, pass \"all\" or nothing",
possibleValues: ["all","device-support","archives","derived-data","docs-cache","old-logs","old-documentation"])
description: "Perform cleaning of given items. Available options: all, device-support, archives, derived-data, docs-cache, user-data-cache, old-logs, old-documentation. If you want to clean all, pass \"all\" or nothing",
possibleValues: ["all","device-support","archives","derived-data","docs-cache","user-data-cache","old-logs","old-documentation"])
argsParser.addOption(name: "--help", description: "Prints this message")

do {
Expand Down
26 changes: 26 additions & 0 deletions DevCleaner/Model/Entries/XcodeFileEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,32 @@ open class XcodeFileEntry: NSObject {
return self.size
}

/// Reuses already computed child sizes and only recalculates child entries whose size is unknown.
@discardableResult
public func recalculateSizeUsingCachedChildrenIfNeeded() -> Size? {
var result: Int64 = 0

// calculate sizes of children, preferring already computed values
for item in self.items {
if let size = item.recalculateSizeIfNeeded(), let sizeInBytes = size.numberOfBytes {
result += sizeInBytes
}
}

// calculate own size
let fileManager = FileManager.default
for pathUrl in self.paths {
if let dirSize = try? fileManager.allocatedSizeOfDirectory(atUrl: pathUrl) {
result += dirSize
} else if let fileSize = try? fileManager.allocatedSizeOfFile(at: pathUrl) {
result += fileSize
}
}

self.size = .value(result)
return self.size
}

@discardableResult
public func recalculateSizeIfNeeded() -> Size? {
guard case .value(let size) = self.size else {
Expand Down
55 changes: 53 additions & 2 deletions DevCleaner/Model/XcodeFiles.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public protocol XcodeFilesDeleteDelegate: AnyObject {
final public class XcodeFiles {
// MARK: Types
public enum Location: Int, CaseIterable {
case deviceSupport, archives, derivedData, documentationCache, logs, oldDocumentation
case deviceSupport, archives, derivedData, documentationCache, userDataCache, logs, oldDocumentation
}

public enum State {
Expand Down Expand Up @@ -98,6 +98,7 @@ final public class XcodeFiles {
.archives: XcodeFileEntry(label: "Archives", tooltipText: "Archived apps. Delete only if you're sure you don't need them.", tooltip: true, selected: false),
.derivedData: XcodeFileEntry(label: "Derived Data", tooltipText: "Cached project data and symbol index.", tooltip: true, selected: false),
.documentationCache: XcodeFileEntry(label: "Documentation Cache", tooltipText: "Documentation cache for each version of Xcode.", tooltip: true, selected: false),
.userDataCache: XcodeFileEntry(label: "UserData Cache", tooltipText: "Regenerable caches stored in Xcode/UserData. This excludes editor preferences and other persistent settings.", tooltip: true, selected: false),
.logs: XcodeFileEntry(label: "Old Logs", tooltipText: "Old device logs and crash databases. Usually, only the most recent ones are needed as they are duplicates of earlier logs.", tooltip: true, selected: false),
.oldDocumentation: OldDocumentationFileEntry(selected: false)
]
Expand Down Expand Up @@ -453,6 +454,9 @@ final public class XcodeFiles {
case .documentationCache:
entry.addChildren(items: self.scanDocumentationCacheLocations())

case .userDataCache:
entry.addChildren(items: self.scanUserDataCacheLocations())

case .logs:
entry.addChildren(items: self.scanLogsLocations())

Expand All @@ -462,7 +466,7 @@ final public class XcodeFiles {
}

// check for those files sizes
entry.recalculateSizeIfNeeded()
entry.recalculateSizeUsingCachedChildrenIfNeeded()

// check for selections
entry.recalculateSelection()
Expand Down Expand Up @@ -685,6 +689,53 @@ final public class XcodeFiles {
return entries
}

private func scanUserDataCacheLocations() -> [XcodeFileEntry] {
typealias CacheLocation = (label: String, relativePath: String, tooltip: String)
let safeCacheLocations: [CacheLocation] = [
(
label: "SwiftUI Previews",
relativePath: "Xcode/UserData/Previews",
tooltip: "Preview simulator data recreated by Xcode when previews run again."
),
(
label: "IB Support Simulator Devices",
relativePath: "Xcode/UserData/IB Support/Simulator Devices",
tooltip: "Interface Builder preview simulator data recreated by Xcode when needed."
),
(
label: "Capabilities Cache",
relativePath: "Xcode/UserData/Capabilities",
tooltip: "Downloaded capabilities metadata cache recreated by Xcode when needed."
)
]

var entries = [XcodeFileEntry]()
for cacheLocation in safeCacheLocations {
let path = self.userDeveloperFolderUrl.appendingPathComponent(cacheLocation.relativePath)
guard FileManager.default.fileExists(atPath: path.path) else {
continue
}

let entry = XcodeFileEntry(label: cacheLocation.label,
extraInfo: path.path,
tooltipText: cacheLocation.tooltip,
icon: .system(name: NSImage.folderName),
tooltip: true,
selected: false)
entry.addPath(path: path)
_ = entry.recalculateSize()
entries.append(entry)
}

return entries.sorted { lhs, rhs in
if lhs.size == rhs.size {
return lhs.label.localizedStandardCompare(rhs.label) == .orderedAscending
} else {
return lhs.size > rhs.size
}
}
}

private func scanLogsLocations() -> [XcodeFileEntry] {
struct LogEntry {
let path: URL
Expand Down
9 changes: 7 additions & 2 deletions DevCleaner/Resources/Manual/manual.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ <h4>Documentation Cache</h4>
amount of space may be wasted due to that.</p>
<p>Removing all cache is not harmful but remember that your most current Xcode will regenerate it anyway if it'll have a chance. It also could slow down access to documentation after cleaning them.</p>

<h4>UserData Cache</h4>
<p>Xcode also stores regenerable cache data in <code>~/Library/Developer/Xcode/UserData</code>, including SwiftUI previews, Interface Builder
preview simulator devices and downloaded capabilities metadata.</p>
<p>DevCleaner only targets explicit cache folders in this location and does not remove editor preferences or other persistent user settings.</p>

<h4>Old Simulator & Device Logs</h4>
<p>Each version of Xcode appears to have a new version of database that holds logs from devices & simulators. Those databases are migrated with content of the previous ones. If you use Xcode for a long time, it would accumulate to significant amount of unused data.</p>

Expand Down Expand Up @@ -134,8 +139,8 @@ <h4>Usage</h4>

info
Show all items available to clean.
clean <all,device-support,archives,derived-data,docs-cache,old-logs,old-documentation>
Perform cleaning of given items. Available options: all,device-support,archives,derived-data,docs-cache,previews,old-logs,old-documentation. If you want to clean all, pass "all" or nothing
clean <all,device-support,archives,derived-data,docs-cache,user-data-cache,old-logs,old-documentation>
Perform cleaning of given items. Available options: all,device-support,archives,derived-data,docs-cache,user-data-cache,old-logs,old-documentation. If you want to clean all, pass "all" or nothing
--help
Prints this message
</pre>
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ we would use them again.
Modern Xcodes are caching documentation that is accessed via the web. Unfortunately older caches are not cleaned up by Xcode and significant
amount of space may be wasted due to that.

### UserData Cache

Xcode also keeps some regenerable data in `~/Library/Developer/Xcode/UserData`, including SwiftUI previews, Interface Builder preview simulator
devices and downloaded capabilities metadata. DevCleaner only targets explicit cache folders here and does not remove editor preferences or other
persistent user settings.

### Old Simulator & Device Logs

Old device logs & crashes databases, only most recent ones are needed. It seems that new versions of Xcodes migrates old logs database, but keeping older ones on disk.
Expand Down