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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/Commands/PackageCommands/ResetCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ extension SwiftPackageCommand {
var globalOptions: GlobalOptions

func run(_ swiftCommandState: SwiftCommandState) async throws {
try await swiftCommandState.getActiveWorkspace().purgeCache(observabilityScope: swiftCommandState.observabilityScope)
try await swiftCommandState.purgeCaches(observabilityScope: swiftCommandState.observabilityScope)
}
}

Expand Down
54 changes: 54 additions & 0 deletions Sources/CoreCommands/SwiftCommandState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ import Basics
import Dispatch
import class Foundation.NSLock
import class Foundation.ProcessInfo
import PackageFingerprint
import PackageGraph
import PackageLoading
@_spi(SwiftPMInternal)
import PackageModel
import PackageRegistry
import PackageSigning
import SourceControl
import SPMBuildCore
import Workspace

Expand Down Expand Up @@ -50,6 +54,7 @@ import class TSCBasic.FileLock
import enum TSCBasic.JSON
import protocol TSCBasic.OutputByteStream
import enum TSCBasic.ProcessEnv
import struct TSCBasic.SHA256
import enum TSCBasic.ProcessLockError
import var TSCBasic.stderrStream
import class TSCBasic.TerminalController
Expand Down Expand Up @@ -534,6 +539,55 @@ public final class SwiftCommandState {
return workspace
}

/// Purges all global caches without requiring workspace initialization.
/// This method creates minimal cache managers directly and calls their purgeCache methods.
public func purgeCaches(observabilityScope: ObservabilityScope) async throws {
// Create repository manager for repository cache
let repositoryManager = RepositoryManager(
fileSystem: self.fileSystem,
path: self.scratchDirectory.appending("repositories"),
provider: GitRepositoryProvider(),
cachePath: self.sharedCacheDirectory.appending("repositories"),
initializationWarningHandler: { observabilityScope.emit(warning: $0) },
delegate: nil
)

// Create manifest loader for manifest cache
let manifestLoader = ManifestLoader(
toolchain: try self.getHostToolchain(),
cacheDir: Workspace.DefaultLocations.manifestsDirectory(at: self.sharedCacheDirectory),
importRestrictions: nil,
delegate: nil,
pruneDependencies: false
)

// Create registry downloads manager for registry cache
let registryClient = RegistryClient(
configuration: .init(),
fingerprintStorage: nil,
fingerprintCheckingMode: .strict,
skipSignatureValidation: false,
signingEntityStorage: nil,
signingEntityCheckingMode: .strict,
authorizationProvider: nil,
delegate: nil,
checksumAlgorithm: SHA256()
)

let registryDownloadsManager = RegistryDownloadsManager(
fileSystem: self.fileSystem,
path: self.scratchDirectory.appending(components: "registry", "downloads"),
cachePath: self.sharedCacheDirectory.appending(components: "registry", "downloads"),
registryClient: registryClient,
delegate: nil
)

// Purge all caches
repositoryManager.purgeCache(observabilityScope: observabilityScope)
registryDownloadsManager.purgeCache(observabilityScope: observabilityScope)
await manifestLoader.purgeCache(observabilityScope: observabilityScope)
}

public func getRootPackageInformation(_ enableAllTraits: Bool = false) async throws -> (dependencies: [PackageIdentity: [PackageIdentity]], targets: [PackageIdentity: [String]]) {
let workspace = try self.getActiveWorkspace(enableAllTraits: enableAllTraits)
let root = try self.getWorkspaceRoot()
Expand Down
2 changes: 2 additions & 0 deletions Sources/PackageLoading/ManifestLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,8 @@ public final class ManifestLoader: ManifestLoaderProtocol {
return
}

observabilityScope.emit(info: "Purging manifest cache at '\(manifestCacheDBPath)'")

do {
try localFileSystem.removeFileTree(manifestCacheDBPath)
} catch {
Expand Down
2 changes: 2 additions & 0 deletions Sources/PackageRegistry/RegistryDownloadsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ public class RegistryDownloadsManager: AsyncCancellable {
return
}

observabilityScope.emit(info: "Purging registry cache at '\(cachePath)'")

do {
try self.fileSystem.withLock(on: cachePath, type: .exclusive) {
let cachedPackages = try self.fileSystem.getDirectoryContents(cachePath)
Expand Down
2 changes: 2 additions & 0 deletions Sources/SourceControl/RepositoryManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,8 @@ public class RepositoryManager: Cancellable {
return
}

observabilityScope.emit(info: "Purging repository cache at '\(cachePath)'")

do {
try self.fileSystem.withLock(on: cachePath, type: .exclusive) {
let cachedRepositories = try self.fileSystem.getDirectoryContents(cachePath)
Expand Down
10 changes: 0 additions & 10 deletions Sources/Workspace/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -907,16 +907,6 @@ extension Workspace {
}
}

/// Cleans the build artifacts from workspace data.
///
/// - Parameters:
/// - observabilityScope: The observability scope that reports errors, warnings, etc
public func purgeCache(observabilityScope: ObservabilityScope) async {
self.repositoryManager.purgeCache(observabilityScope: observabilityScope)
self.registryDownloadsManager.purgeCache(observabilityScope: observabilityScope)
await self.manifestLoader.purgeCache(observabilityScope: observabilityScope)
}

/// Resets the entire workspace by removing the data directory.
///
/// - Parameters:
Expand Down
1 change: 1 addition & 0 deletions Sources/_InternalTestSupport/SwiftTesting+Tags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ extension Tag.Feature.Command.Package {
@Tag public static var Migrate: Tag
@Tag public static var Plugin: Tag
@Tag public static var Reset: Tag
@Tag public static var PurgeCache: Tag
@Tag public static var Resolve: Tag
@Tag public static var ShowDependencies: Tag
@Tag public static var ShowExecutables: Tag
Expand Down
87 changes: 87 additions & 0 deletions Tests/CommandsTests/PackageCommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2935,6 +2935,93 @@ struct PackageCommandTests {
}
}

@Test(
.tags(.Feature.Command.Package.PurgeCache),
arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms),
)
func purgeCacheWithoutPackage(
data: BuildData,
) async throws {
// Create a temporary directory without Package.swift
try await fixture(name: "Miscellaneous") { fixturePath in
let tempDir = fixturePath.appending("empty-dir-for-purge-test")
try localFileSystem.createDirectory(tempDir, recursive: true)

// Use a unique temporary cache directory to avoid conflicts with parallel tests
try await withTemporaryDirectory(removeTreeOnDeinit: true) { cacheDir in
let result = try await executeSwiftPackage(
tempDir,
configuration: data.config,
extraArgs: ["purge-cache", "--cache-path", cacheDir.pathString],
buildSystem: data.buildSystem
)

#expect(!result.stderr.contains("Could not find Package.swift"))
}
}
}

@Test(
.tags(.Feature.Command.Package.PurgeCache),
arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms),
)
func purgeCacheInPackageDirectory(
data: BuildData,
) async throws {
// Test that purge-cache works in a package directory and successfully purges caches
try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in
let packageRoot = fixturePath.appending("Bar")

// Use a unique temporary cache directory for this test
try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in
let cacheDir = tempDir.appending("test-cache")
let cacheArgs = ["--cache-path", cacheDir.pathString]

// Resolve dependencies to populate cache
// Note: This fixture uses local dependencies, so only manifest cache will be populated
try await executeSwiftPackage(
packageRoot,
configuration: data.config,
extraArgs: ["resolve"] + cacheArgs,
buildSystem: data.buildSystem
)

// Verify manifest cache was populated
let manifestsCache = cacheDir.appending(components: "manifests")
expectDirectoryExists(at: manifestsCache)

// Check for manifest.db file (main database file)
let manifestDB = manifestsCache.appending("manifest.db")
let hasManifestDB = localFileSystem.exists(manifestDB)

// Check for SQLite auxiliary files that might exist
let manifestDBWAL = manifestsCache.appending("manifest.db-wal")
let manifestDBSHM = manifestsCache.appending("manifest.db-shm")
let hasAuxFiles = localFileSystem.exists(manifestDBWAL) || localFileSystem.exists(manifestDBSHM)

// At least one manifest database file should exist
#expect(hasManifestDB || hasAuxFiles, "Manifest cache should be populated after resolve")

// Run purge-cache
let result = try await executeSwiftPackage(
packageRoot,
configuration: data.config,
extraArgs: ["purge-cache"] + cacheArgs,
buildSystem: data.buildSystem
)

// Verify command succeeded
#expect(!result.stderr.contains("Could not find Package.swift"))

// Verify manifest.db was removed (the purge implementation removes this file)
expectFileDoesNotExists(at: manifestDB, "manifest.db should be removed after purge")

// Note: SQLite auxiliary files (WAL/SHM) may or may not be removed depending on SQLite state
// The important check is that the main database file is removed
}
}
}

@Test(
.tags(
.Feature.Command.Package.Resolve,
Expand Down