Skip to content

Commit 1f48ed4

Browse files
authored
Allow purge-cache to run without Package.swift (#9247)
This change refactors cache purging to work in any directory, not just package directories. Previously, purge-cache required a Package.swift file and initialized a full workspace. Now it creates minimal cache managers directly and purges them independently. Also add some logging to print the paths being purged. Logs are only shown with `--verbose` or `--very-verbose`. Issue: #9235
1 parent cda31ed commit 1f48ed4

File tree

8 files changed

+149
-11
lines changed

8 files changed

+149
-11
lines changed

Sources/Commands/PackageCommands/ResetCommands.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ extension SwiftPackageCommand {
3535
var globalOptions: GlobalOptions
3636

3737
func run(_ swiftCommandState: SwiftCommandState) async throws {
38-
try await swiftCommandState.getActiveWorkspace().purgeCache(observabilityScope: swiftCommandState.observabilityScope)
38+
try await swiftCommandState.purgeCaches(observabilityScope: swiftCommandState.observabilityScope)
3939
}
4040
}
4141

Sources/CoreCommands/SwiftCommandState.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ import Basics
1616
import Dispatch
1717
import class Foundation.NSLock
1818
import class Foundation.ProcessInfo
19+
import PackageFingerprint
1920
import PackageGraph
2021
import PackageLoading
2122
@_spi(SwiftPMInternal)
2223
import PackageModel
24+
import PackageRegistry
25+
import PackageSigning
26+
import SourceControl
2327
import SPMBuildCore
2428
import Workspace
2529

@@ -50,6 +54,7 @@ import class TSCBasic.FileLock
5054
import enum TSCBasic.JSON
5155
import protocol TSCBasic.OutputByteStream
5256
import enum TSCBasic.ProcessEnv
57+
import struct TSCBasic.SHA256
5358
import enum TSCBasic.ProcessLockError
5459
import var TSCBasic.stderrStream
5560
import class TSCBasic.TerminalController
@@ -534,6 +539,55 @@ public final class SwiftCommandState {
534539
return workspace
535540
}
536541

542+
/// Purges all global caches without requiring workspace initialization.
543+
/// This method creates minimal cache managers directly and calls their purgeCache methods.
544+
public func purgeCaches(observabilityScope: ObservabilityScope) async throws {
545+
// Create repository manager for repository cache
546+
let repositoryManager = RepositoryManager(
547+
fileSystem: self.fileSystem,
548+
path: self.scratchDirectory.appending("repositories"),
549+
provider: GitRepositoryProvider(),
550+
cachePath: self.sharedCacheDirectory.appending("repositories"),
551+
initializationWarningHandler: { observabilityScope.emit(warning: $0) },
552+
delegate: nil
553+
)
554+
555+
// Create manifest loader for manifest cache
556+
let manifestLoader = ManifestLoader(
557+
toolchain: try self.getHostToolchain(),
558+
cacheDir: Workspace.DefaultLocations.manifestsDirectory(at: self.sharedCacheDirectory),
559+
importRestrictions: nil,
560+
delegate: nil,
561+
pruneDependencies: false
562+
)
563+
564+
// Create registry downloads manager for registry cache
565+
let registryClient = RegistryClient(
566+
configuration: .init(),
567+
fingerprintStorage: nil,
568+
fingerprintCheckingMode: .strict,
569+
skipSignatureValidation: false,
570+
signingEntityStorage: nil,
571+
signingEntityCheckingMode: .strict,
572+
authorizationProvider: nil,
573+
delegate: nil,
574+
checksumAlgorithm: SHA256()
575+
)
576+
577+
let registryDownloadsManager = RegistryDownloadsManager(
578+
fileSystem: self.fileSystem,
579+
path: self.scratchDirectory.appending(components: "registry", "downloads"),
580+
cachePath: self.sharedCacheDirectory.appending(components: "registry", "downloads"),
581+
registryClient: registryClient,
582+
delegate: nil
583+
)
584+
585+
// Purge all caches
586+
repositoryManager.purgeCache(observabilityScope: observabilityScope)
587+
registryDownloadsManager.purgeCache(observabilityScope: observabilityScope)
588+
await manifestLoader.purgeCache(observabilityScope: observabilityScope)
589+
}
590+
537591
public func getRootPackageInformation(_ enableAllTraits: Bool = false) async throws -> (dependencies: [PackageIdentity: [PackageIdentity]], targets: [PackageIdentity: [String]]) {
538592
let workspace = try self.getActiveWorkspace(enableAllTraits: enableAllTraits)
539593
let root = try self.getWorkspaceRoot()

Sources/PackageLoading/ManifestLoader.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,8 @@ public final class ManifestLoader: ManifestLoaderProtocol {
999999
return
10001000
}
10011001

1002+
observabilityScope.emit(info: "Purging manifest cache at '\(manifestCacheDBPath)'")
1003+
10021004
do {
10031005
try localFileSystem.removeFileTree(manifestCacheDBPath)
10041006
} catch {

Sources/PackageRegistry/RegistryDownloadsManager.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ public class RegistryDownloadsManager: AsyncCancellable {
275275
return
276276
}
277277

278+
observabilityScope.emit(info: "Purging registry cache at '\(cachePath)'")
279+
278280
do {
279281
try self.fileSystem.withLock(on: cachePath, type: .exclusive) {
280282
let cachedPackages = try self.fileSystem.getDirectoryContents(cachePath)

Sources/SourceControl/RepositoryManager.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,8 @@ public class RepositoryManager: Cancellable {
470470
return
471471
}
472472

473+
observabilityScope.emit(info: "Purging repository cache at '\(cachePath)'")
474+
473475
do {
474476
try self.fileSystem.withLock(on: cachePath, type: .exclusive) {
475477
let cachedRepositories = try self.fileSystem.getDirectoryContents(cachePath)

Sources/Workspace/Workspace.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -907,16 +907,6 @@ extension Workspace {
907907
}
908908
}
909909

910-
/// Cleans the build artifacts from workspace data.
911-
///
912-
/// - Parameters:
913-
/// - observabilityScope: The observability scope that reports errors, warnings, etc
914-
public func purgeCache(observabilityScope: ObservabilityScope) async {
915-
self.repositoryManager.purgeCache(observabilityScope: observabilityScope)
916-
self.registryDownloadsManager.purgeCache(observabilityScope: observabilityScope)
917-
await self.manifestLoader.purgeCache(observabilityScope: observabilityScope)
918-
}
919-
920910
/// Resets the entire workspace by removing the data directory.
921911
///
922912
/// - Parameters:

Sources/_InternalTestSupport/SwiftTesting+Tags.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ extension Tag.Feature.Command.Package {
6868
@Tag public static var Migrate: Tag
6969
@Tag public static var Plugin: Tag
7070
@Tag public static var Reset: Tag
71+
@Tag public static var PurgeCache: Tag
7172
@Tag public static var Resolve: Tag
7273
@Tag public static var ShowDependencies: Tag
7374
@Tag public static var ShowExecutables: Tag

Tests/CommandsTests/PackageCommandTests.swift

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2935,6 +2935,93 @@ struct PackageCommandTests {
29352935
}
29362936
}
29372937

2938+
@Test(
2939+
.tags(.Feature.Command.Package.PurgeCache),
2940+
arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms),
2941+
)
2942+
func purgeCacheWithoutPackage(
2943+
data: BuildData,
2944+
) async throws {
2945+
// Create a temporary directory without Package.swift
2946+
try await fixture(name: "Miscellaneous") { fixturePath in
2947+
let tempDir = fixturePath.appending("empty-dir-for-purge-test")
2948+
try localFileSystem.createDirectory(tempDir, recursive: true)
2949+
2950+
// Use a unique temporary cache directory to avoid conflicts with parallel tests
2951+
try await withTemporaryDirectory(removeTreeOnDeinit: true) { cacheDir in
2952+
let result = try await executeSwiftPackage(
2953+
tempDir,
2954+
configuration: data.config,
2955+
extraArgs: ["purge-cache", "--cache-path", cacheDir.pathString],
2956+
buildSystem: data.buildSystem
2957+
)
2958+
2959+
#expect(!result.stderr.contains("Could not find Package.swift"))
2960+
}
2961+
}
2962+
}
2963+
2964+
@Test(
2965+
.tags(.Feature.Command.Package.PurgeCache),
2966+
arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms),
2967+
)
2968+
func purgeCacheInPackageDirectory(
2969+
data: BuildData,
2970+
) async throws {
2971+
// Test that purge-cache works in a package directory and successfully purges caches
2972+
try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in
2973+
let packageRoot = fixturePath.appending("Bar")
2974+
2975+
// Use a unique temporary cache directory for this test
2976+
try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in
2977+
let cacheDir = tempDir.appending("test-cache")
2978+
let cacheArgs = ["--cache-path", cacheDir.pathString]
2979+
2980+
// Resolve dependencies to populate cache
2981+
// Note: This fixture uses local dependencies, so only manifest cache will be populated
2982+
try await executeSwiftPackage(
2983+
packageRoot,
2984+
configuration: data.config,
2985+
extraArgs: ["resolve"] + cacheArgs,
2986+
buildSystem: data.buildSystem
2987+
)
2988+
2989+
// Verify manifest cache was populated
2990+
let manifestsCache = cacheDir.appending(components: "manifests")
2991+
expectDirectoryExists(at: manifestsCache)
2992+
2993+
// Check for manifest.db file (main database file)
2994+
let manifestDB = manifestsCache.appending("manifest.db")
2995+
let hasManifestDB = localFileSystem.exists(manifestDB)
2996+
2997+
// Check for SQLite auxiliary files that might exist
2998+
let manifestDBWAL = manifestsCache.appending("manifest.db-wal")
2999+
let manifestDBSHM = manifestsCache.appending("manifest.db-shm")
3000+
let hasAuxFiles = localFileSystem.exists(manifestDBWAL) || localFileSystem.exists(manifestDBSHM)
3001+
3002+
// At least one manifest database file should exist
3003+
#expect(hasManifestDB || hasAuxFiles, "Manifest cache should be populated after resolve")
3004+
3005+
// Run purge-cache
3006+
let result = try await executeSwiftPackage(
3007+
packageRoot,
3008+
configuration: data.config,
3009+
extraArgs: ["purge-cache"] + cacheArgs,
3010+
buildSystem: data.buildSystem
3011+
)
3012+
3013+
// Verify command succeeded
3014+
#expect(!result.stderr.contains("Could not find Package.swift"))
3015+
3016+
// Verify manifest.db was removed (the purge implementation removes this file)
3017+
expectFileDoesNotExists(at: manifestDB, "manifest.db should be removed after purge")
3018+
3019+
// Note: SQLite auxiliary files (WAL/SHM) may or may not be removed depending on SQLite state
3020+
// The important check is that the main database file is removed
3021+
}
3022+
}
3023+
}
3024+
29383025
@Test(
29393026
.tags(
29403027
.Feature.Command.Package.Resolve,

0 commit comments

Comments
 (0)