Skip to content

Commit a8ded58

Browse files
authored
[SR-12851] System-wide cache of SwiftPM dependencies (#2985)
* implemented caching * fixed packages with a path ending in “/” * fetching a repo creates an alias for the fetched repository under a new RepositorySpecifier * added purge-cache and --cache-path options * temporarily fix tests until FileSystem has locks * added documentation and some refactoring * remove unnecessary extension * remove unnecessary import * use file lock provided by FileSystem * added -skip-cache flag * using copy instead of fetch * using repository provider instead of repository manager to fetch the cached repo * added fetchDetail to delegate methods * remove func setURL(remote: String, url: String) throws * fixed unit tests by making RepositoryProvider aware of copy * fix newlines * fix lock * extract function initalizeCacheIfNeeded * update comments * remove duplicate code * remove newlines * re-add old comments and move repositoryPath back down * move isCache to the top * converted FetchDetails into a struct * use FileSystem.swiftPMCacheDirectory * add symlink to cache directory in .swiftpm * add some comments * create .swiftpm directory * don't cache local packages * added default implementations * automatically infer update parameter for fetch * deprecated old delegate methods * add Basics to SourceControl * use a more reliable way to determine if a git repository exists * open repository before fetching * added tests for repository manager * rename cacheLocalPackages
1 parent 05803d6 commit a8ded58

File tree

14 files changed

+364
-49
lines changed

14 files changed

+364
-49
lines changed

Sources/Commands/Options.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ public struct SwiftToolOptions: ParsableArguments {
136136
@Option(help: "Specify build/cache directory")
137137
var buildPath: AbsolutePath?
138138

139+
@Option(help: "Specify the global repository cache directory")
140+
var cachePath: AbsolutePath?
141+
142+
@Flag(help: "Skip the cache when fetching")
143+
var skipCache: Bool = false
144+
139145
/// The custom working directory that the tool should operate in (deprecated).
140146
@Option(name: [.long, .customShort("C")])
141147
var chdir: AbsolutePath?

Sources/Commands/SwiftPackageTool.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public struct SwiftPackageTool: ParsableCommand {
3333
version: Versioning.currentVersion.completeDisplayString,
3434
subcommands: [
3535
Clean.self,
36+
PurgeCache.self,
3637
Reset.self,
3738
Update.self,
3839
Describe.self,
@@ -83,6 +84,18 @@ extension SwiftPackageTool {
8384
try swiftTool.getActiveWorkspace().clean(with: swiftTool.diagnostics)
8485
}
8586
}
87+
88+
struct PurgeCache: SwiftCommand {
89+
static let configuration = CommandConfiguration(
90+
abstract: "Purge the global repository cache.")
91+
92+
@OptionGroup()
93+
var swiftOptions: SwiftToolOptions
94+
95+
func run(_ swiftTool: SwiftTool) throws {
96+
try swiftTool.getActiveWorkspace().purgeCache(with: swiftTool.diagnostics)
97+
}
98+
}
8699

87100
struct Reset: SwiftCommand {
88101
static let configuration = CommandConfiguration(

Sources/Commands/SwiftTool.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import SPMBuildCore
2525
import Build
2626
import XCBuildSupport
2727
import Workspace
28+
import Basics
2829

2930
typealias Diagnostic = TSCBasic.Diagnostic
3031

@@ -59,13 +60,18 @@ private class ToolWorkspaceDelegate: WorkspaceDelegate {
5960
self.diagnostics = diagnostics
6061
}
6162

62-
func fetchingWillBegin(repository: String) {
63+
func fetchingWillBegin(repository: String, fetchDetails: RepositoryManager.FetchDetails?) {
6364
stdoutStream <<< "Fetching \(repository)"
65+
if let fetchDetails = fetchDetails {
66+
if fetchDetails.fromCache {
67+
stdoutStream <<< " from cache"
68+
}
69+
}
6470
stdoutStream <<< "\n"
6571
stdoutStream.flush()
6672
}
6773

68-
func fetchingDidFinish(repository: String, diagnostic: Diagnostic?) {
74+
func fetchingDidFinish(repository: String, fetchDetails: RepositoryManager.FetchDetails?, diagnostic: Diagnostic?) {
6975
}
7076

7177
func repositoryWillUpdate(_ repository: String) {
@@ -456,6 +462,8 @@ public class SwiftTool {
456462
let isVerbose = options.verbosity != 0
457463
let delegate = ToolWorkspaceDelegate(self.stdoutStream, isVerbose: isVerbose, diagnostics: diagnostics)
458464
let provider = GitRepositoryProvider(processSet: processSet)
465+
let defaultCachePath = localFileSystem.swiftPMCacheDirectory.appending(component: "repositories")
466+
let cachePath = !options.skipCache ? options.cachePath ?? defaultCachePath : nil
459467
let workspace = Workspace(
460468
dataPath: buildPath,
461469
editablesPath: try editablesPath(),
@@ -468,7 +476,8 @@ public class SwiftTool {
468476
netrcFilePath: try resolvedNetrcFilePath(),
469477
isResolverPrefetchingEnabled: options.shouldEnableResolverPrefetching,
470478
skipUpdate: options.skipDependencyUpdate,
471-
enableResolverTrace: options.enableResolverTrace
479+
enableResolverTrace: options.enableResolverTrace,
480+
cachePath: cachePath
472481
)
473482
_workspace = workspace
474483
return workspace

Sources/SPMTestSupport/InMemoryGitRepository.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,14 @@ public final class InMemoryGitRepositoryProvider: RepositoryProvider {
335335
// Note: These methods use force unwrap (instead of throwing) to honor their preconditions.
336336

337337
public func fetch(repository: RepositorySpecifier, to path: AbsolutePath) throws {
338-
fetchedMap[path] = specifierMap[RepositorySpecifier(url: repository.url.spm_dropGitSuffix())]!.copy()
338+
let repo = specifierMap[RepositorySpecifier(url: repository.url.spm_dropGitSuffix())]!
339+
fetchedMap[path] = repo.copy()
340+
add(specifier: RepositorySpecifier(url: path.asURL.absoluteString), repository: repo)
341+
}
342+
343+
public func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
344+
let repo = fetchedMap[sourcePath]!
345+
fetchedMap[destinationPath] = repo.copy()
339346
}
340347

341348
public func open(repository: RepositorySpecifier, at path: AbsolutePath) throws -> Repository {

Sources/SPMTestSupport/MockWorkspace.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import PackageModel
1515
import SourceControl
1616
import TSCBasic
1717
import Workspace
18+
import Basics
1819

1920
public final class MockWorkspace {
2021
let sandbox: AbsolutePath
@@ -171,7 +172,8 @@ public final class MockWorkspace {
171172
checksumAlgorithm: self.checksumAlgorithm,
172173
isResolverPrefetchingEnabled: true,
173174
enablePubgrubResolver: self.enablePubGrub,
174-
skipUpdate: self.skipUpdate
175+
skipUpdate: self.skipUpdate,
176+
cachePath: localFileSystem.swiftPMCacheDirectory.appending(component: "repositories")
175177
)
176178
return self._workspace!
177179
}
@@ -552,11 +554,11 @@ public final class MockWorkspaceDelegate: WorkspaceDelegate {
552554
self.events.append("Everything is already up-to-date")
553555
}
554556

555-
public func fetchingWillBegin(repository: String) {
557+
public func fetchingWillBegin(repository: String, fetchDetails: RepositoryManager.FetchDetails?) {
556558
self.events.append("fetching repo: \(repository)")
557559
}
558560

559-
public func fetchingDidFinish(repository: String, diagnostic: Diagnostic?) {
561+
public func fetchingDidFinish(repository: String, fetchDetails: RepositoryManager.FetchDetails?, diagnostic: Diagnostic?) {
560562
self.events.append("finished fetching repo: \(repository)")
561563
}
562564

Sources/SourceControl/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ add_library(SourceControl
1212
RepositoryManager.swift)
1313

1414
target_link_libraries(SourceControl PUBLIC
15+
Basics
1516
TSCBasic
1617
TSCUtility
1718
Basics)

Sources/SourceControl/GitRepository.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,14 @@ public class GitRepositoryProvider: RepositoryProvider {
5252
// NOTE: We intentionally do not create a shallow clone here; the
5353
// expected cost of iterative updates on a full clone is less than on a
5454
// shallow clone.
55-
5655
precondition(!localFileSystem.exists(path))
57-
5856
// FIXME: Ideally we should pass `--progress` here and report status regularly. We currently don't have callbacks for that.
5957
try callGit("clone", "--mirror", repository.url, path.pathString,
60-
failureMessage: "Failed to clone repository \(repository.url)", repository: repository)
58+
failureMessage: "Failed to clone repository \(repository.url)", repository: repository)
59+
}
60+
61+
public func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
62+
try localFileSystem.copy(from: sourcePath, to: destinationPath)
6163
}
6264

6365
public func open(repository: RepositorySpecifier, at path: AbsolutePath) -> Repository {

Sources/SourceControl/Repository.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public struct RepositorySpecifier: Hashable, Codable {
3434

3535
/// Returns the cleaned basename for the specifier.
3636
public var basename: String {
37-
var basename = url.components(separatedBy: "/").last!
37+
var basename = url.components(separatedBy: "/").last(where: { !$0.isEmpty }) ?? ""
3838
if basename.hasSuffix(".git") {
3939
basename = String(basename.dropLast(4))
4040
}
@@ -71,7 +71,7 @@ public protocol RepositoryProvider {
7171
///
7272
/// - Parameters:
7373
/// - repository: The specifier of the repository to fetch.
74-
///
74+
/// - path: The destiantion path for the fetch.
7575
/// - Throws: If there is any error fetching the repository.
7676
func fetch(repository: RepositorySpecifier, to path: AbsolutePath) throws
7777

@@ -118,12 +118,23 @@ public protocol RepositoryProvider {
118118
/// - path: The location of the repository on disk, at which the repository
119119
/// has previously been created via `cloneCheckout`.
120120
func openCheckout(at path: AbsolutePath) throws -> WorkingCheckout
121+
122+
/// Copies the repository at path `from` to path `to`.
123+
/// - Parameters:
124+
/// - sourcePath: the source path.
125+
/// - destinationPath: the destination path.
126+
func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws
121127
}
122128

123129
extension RepositoryProvider {
124130
public func checkoutExists(at path: AbsolutePath) throws -> Bool {
125131
fatalError("Unimplemented")
126132
}
133+
134+
public func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
135+
fatalError("Unimplemented")
136+
}
137+
127138
}
128139

129140
/// Abstract repository operations.

0 commit comments

Comments
 (0)