Skip to content

Commit e38c81d

Browse files
authored
cache binary artifact globally (#7101)
motivation: like other dependencies, binary artifacts are good candidates for user level caching such that they do not need to be re-downloaded changes: * update BinaryArtifactsManager to take cache path and use the cache to store binary artifacts when downloading them * update test infra to enable/disable artifacts caching * update workspaace call sites * update workspace delegate to indicate when using cached binary artifact * update and add tests rdar://111774147
1 parent 8e318dc commit e38c81d

File tree

9 files changed

+374
-56
lines changed

9 files changed

+374
-56
lines changed

Sources/Commands/PackageTools/ComputeChecksum.swift

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ struct ComputeChecksum: SwiftCommand {
3333
authorizationProvider: swiftTool.getAuthorizationProvider(),
3434
hostToolchain: swiftTool.getHostToolchain(),
3535
checksumAlgorithm: SHA256(),
36+
cachePath: .none,
3637
customHTTPClient: .none,
3738
customArchiver: .none,
3839
delegate: .none

Sources/Commands/ToolWorkspaceDelegate.swift

+14-6
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class ToolWorkspaceDelegate: WorkspaceDelegate {
8181
}
8282
}
8383

84-
self.outputHandler("Fetched \(packageLocation ?? package.description) (\(duration.descriptionInSeconds))", false)
84+
self.outputHandler("Fetched \(packageLocation ?? package.description) from cache (\(duration.descriptionInSeconds))", false)
8585
}
8686

8787
func fetchingPackage(package: PackageIdentity, packageLocation: String?, progress: Int64, total: Int64?) {
@@ -135,12 +135,16 @@ class ToolWorkspaceDelegate: WorkspaceDelegate {
135135
self.outputHandler("Computed \(location) at \(version) (\(duration.descriptionInSeconds))", false)
136136
}
137137

138-
func willDownloadBinaryArtifact(from url: String) {
139-
self.outputHandler("Downloading binary artifact \(url)", false)
138+
func willDownloadBinaryArtifact(from url: String, fromCache: Bool) {
139+
if fromCache {
140+
self.outputHandler("Fetching binary artifact \(url) from cache", false)
141+
} else {
142+
self.outputHandler("Downloading binary artifact \(url)", false)
143+
}
140144
}
141145

142-
func didDownloadBinaryArtifact(from url: String, result: Result<AbsolutePath, Error>, duration: DispatchTimeInterval) {
143-
guard case .success = result, !self.observabilityScope.errorsReported else {
146+
func didDownloadBinaryArtifact(from url: String, result: Result<(path: AbsolutePath, fromCache: Bool), Error>, duration: DispatchTimeInterval) {
147+
guard case .success(let fetchDetails) = result, !self.observabilityScope.errorsReported else {
144148
return
145149
}
146150

@@ -155,7 +159,11 @@ class ToolWorkspaceDelegate: WorkspaceDelegate {
155159
}
156160
}
157161

158-
self.outputHandler("Downloaded \(url) (\(duration.descriptionInSeconds))", false)
162+
if fetchDetails.fromCache {
163+
self.outputHandler("Fetched \(url) from cache (\(duration.descriptionInSeconds))", false)
164+
} else {
165+
self.outputHandler("Downloaded \(url) (\(duration.descriptionInSeconds))", false)
166+
}
159167
}
160168

161169
func downloadingBinaryArtifact(from url: String, bytesDownloaded: Int64, totalBytesToDownload: Int64?) {

Sources/SPMTestSupport/MockWorkspace.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -893,11 +893,11 @@ public final class MockWorkspaceDelegate: WorkspaceDelegate {
893893
// noop
894894
}
895895

896-
public func willDownloadBinaryArtifact(from url: String) {
896+
public func willDownloadBinaryArtifact(from url: String, fromCache: Bool) {
897897
self.append("downloading binary artifact package: \(url)")
898898
}
899899

900-
public func didDownloadBinaryArtifact(from url: String, result: Result<AbsolutePath, Error>, duration: DispatchTimeInterval) {
900+
public func didDownloadBinaryArtifact(from url: String, result: Result<(path: AbsolutePath, fromCache: Bool), Error>, duration: DispatchTimeInterval) {
901901
self.append("finished downloading binary artifact package: \(url)")
902902
}
903903

Sources/SourceControl/RepositoryManager.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ public class RepositoryManager: Cancellable {
441441
}
442442

443443
/// Sets up the cache directories if they don't already exist.
444-
public func initializeCacheIfNeeded(cachePath: AbsolutePath) throws {
444+
private func initializeCacheIfNeeded(cachePath: AbsolutePath) throws {
445445
// Create the supplied cache directory.
446446
if !self.fileSystem.exists(cachePath) {
447447
try self.fileSystem.createDirectory(cachePath, recursive: true)

Sources/Workspace/Workspace+BinaryArtifacts.swift

+127-32
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,16 @@ extension Workspace {
2626
public struct CustomBinaryArtifactsManager {
2727
let httpClient: LegacyHTTPClient?
2828
let archiver: Archiver?
29+
let useCache: Bool?
2930

30-
public init(httpClient: LegacyHTTPClient? = .none, archiver: Archiver? = .none) {
31+
public init(
32+
httpClient: LegacyHTTPClient? = .none,
33+
archiver: Archiver? = .none,
34+
useCache: Bool? = .none
35+
) {
3136
self.httpClient = httpClient
3237
self.archiver = archiver
38+
self.useCache = useCache
3339
}
3440
}
3541

@@ -43,13 +49,15 @@ extension Workspace {
4349
private let httpClient: LegacyHTTPClient
4450
private let archiver: Archiver
4551
private let checksumAlgorithm: HashAlgorithm
52+
private let cachePath: AbsolutePath?
4653
private let delegate: Delegate?
4754

4855
public init(
4956
fileSystem: FileSystem,
5057
authorizationProvider: AuthorizationProvider?,
5158
hostToolchain: UserToolchain,
5259
checksumAlgorithm: HashAlgorithm,
60+
cachePath: AbsolutePath?,
5361
customHTTPClient: LegacyHTTPClient?,
5462
customArchiver: Archiver?,
5563
delegate: Delegate?
@@ -60,6 +68,7 @@ extension Workspace {
6068
self.checksumAlgorithm = checksumAlgorithm
6169
self.httpClient = customHTTPClient ?? LegacyHTTPClient()
6270
self.archiver = customArchiver ?? ZipArchiver(fileSystem: fileSystem)
71+
self.cachePath = cachePath
6372
self.delegate = delegate
6473
}
6574

@@ -126,7 +135,7 @@ extension Workspace {
126135
return (local: localArtifacts, remote: remoteArtifacts)
127136
}
128137

129-
func download(
138+
func fetch(
130139
_ artifacts: [RemoteArtifact],
131140
artifactsDirectory: AbsolutePath,
132141
observabilityScope: ObservabilityScope
@@ -229,37 +238,24 @@ extension Workspace {
229238
}
230239

231240
group.enter()
232-
var headers = HTTPClientHeaders()
233-
headers.add(name: "Accept", value: "application/octet-stream")
234-
var request = LegacyHTTPClient.Request.download(
235-
url: artifact.url,
236-
headers: headers,
237-
fileSystem: self.fileSystem,
238-
destination: archivePath
239-
)
240-
request.options.authorizationProvider = self.authorizationProvider?.httpAuthorizationHeader(for:)
241-
request.options.retryStrategy = .exponentialBackoff(maxAttempts: 3, baseDelay: .milliseconds(50))
242-
request.options.validResponseCodes = [200]
243-
244-
let downloadStart: DispatchTime = .now()
245-
self.delegate?.willDownloadBinaryArtifact(from: artifact.url.absoluteString)
246-
observabilityScope.emit(debug: "downloading \(artifact.url) to \(archivePath)")
247-
self.httpClient.execute(
248-
request,
241+
let fetchStart: DispatchTime = .now()
242+
self.fetch(
243+
artifact: artifact,
244+
destination: archivePath,
245+
observabilityScope: observabilityScope,
249246
progress: { bytesDownloaded, totalBytesToDownload in
250247
self.delegate?.downloadingBinaryArtifact(
251248
from: artifact.url.absoluteString,
252249
bytesDownloaded: bytesDownloaded,
253250
totalBytesToDownload: totalBytesToDownload
254251
)
255252
},
256-
completion: { downloadResult in
253+
completion: { fetchResult in
257254
defer { group.leave() }
258255

259-
// TODO: Use the same extraction logic for both remote and local archived artifacts.
260-
switch downloadResult {
261-
case .success:
262-
256+
switch fetchResult {
257+
case .success(let cached):
258+
// TODO: Use the same extraction logic for both remote and local archived artifacts.
263259
group.enter()
264260
observabilityScope.emit(debug: "validating \(archivePath)")
265261
self.archiver.validate(path: archivePath, completion: { validationResult in
@@ -381,8 +377,8 @@ extension Workspace {
381377
)
382378
self.delegate?.didDownloadBinaryArtifact(
383379
from: artifact.url.absoluteString,
384-
result: .success(artifactPath),
385-
duration: downloadStart.distance(to: .now())
380+
result: .success((path: artifactPath, fromCache: cached)),
381+
duration: fetchStart.distance(to: .now())
386382
)
387383
case .failure(let error):
388384
observabilityScope.emit(.remoteArtifactFailedExtraction(
@@ -393,7 +389,7 @@ extension Workspace {
393389
self.delegate?.didDownloadBinaryArtifact(
394390
from: artifact.url.absoluteString,
395391
result: .failure(error),
396-
duration: downloadStart.distance(to: .now())
392+
duration: fetchStart.distance(to: .now())
397393
)
398394
}
399395

@@ -409,7 +405,7 @@ extension Workspace {
409405
self.delegate?.didDownloadBinaryArtifact(
410406
from: artifact.url.absoluteString,
411407
result: .failure(error),
412-
duration: downloadStart.distance(to: .now())
408+
duration: fetchStart.distance(to: .now())
413409
)
414410
}
415411
})
@@ -423,7 +419,7 @@ extension Workspace {
423419
self.delegate?.didDownloadBinaryArtifact(
424420
from: artifact.url.absoluteString,
425421
result: .failure(error),
426-
duration: downloadStart.distance(to: .now())
422+
duration: fetchStart.distance(to: .now())
427423
)
428424
}
429425
}
@@ -563,17 +559,116 @@ extension Workspace {
563559
try cancellableArchiver.cancel(deadline: deadline)
564560
}
565561
}
562+
563+
private func fetch(
564+
artifact: RemoteArtifact,
565+
destination: AbsolutePath,
566+
observabilityScope: ObservabilityScope,
567+
progress: @escaping (Int64, Optional<Int64>) -> Void,
568+
completion: @escaping (Result<Bool, Error>) -> Void
569+
) {
570+
// not using cache, download directly
571+
guard let cachePath = self.cachePath else {
572+
self.delegate?.willDownloadBinaryArtifact(from: artifact.url.absoluteString, fromCache: false)
573+
return self.download(
574+
artifact: artifact,
575+
destination: destination,
576+
observabilityScope: observabilityScope,
577+
progress: progress,
578+
completion: { result in
579+
// not fetched from cache
580+
completion(result.map{ _ in false })
581+
}
582+
)
583+
}
584+
585+
// initialize cache if necessary
586+
do {
587+
if !self.fileSystem.exists(cachePath) {
588+
try self.fileSystem.createDirectory(cachePath, recursive: true)
589+
}
590+
} catch {
591+
return completion(.failure(error))
592+
}
593+
594+
595+
// try to fetch from cache, or download and cache
596+
// / FIXME: use better escaping of URL
597+
let cacheKey = artifact.url.absoluteString.spm_mangledToC99ExtendedIdentifier()
598+
let cachedArtifactPath = cachePath.appending(cacheKey)
599+
600+
if self.fileSystem.exists(cachedArtifactPath) {
601+
observabilityScope.emit(debug: "copying cached binary artifact for \(artifact.url) from \(cachedArtifactPath)")
602+
self.delegate?.willDownloadBinaryArtifact(from: artifact.url.absoluteString, fromCache: true)
603+
return completion(
604+
Result.init(catching: {
605+
// copy from cache to destination
606+
try self.fileSystem.copy(from: cachedArtifactPath, to: destination)
607+
return true // fetched from cache
608+
})
609+
)
610+
}
611+
612+
// download to the cache
613+
observabilityScope.emit(debug: "downloading binary artifact for \(artifact.url) to cached at \(cachedArtifactPath)")
614+
self.download(
615+
artifact: artifact,
616+
destination: cachedArtifactPath,
617+
observabilityScope: observabilityScope,
618+
progress: progress,
619+
completion: { result in
620+
self.delegate?.willDownloadBinaryArtifact(from: artifact.url.absoluteString, fromCache: false)
621+
completion(result.flatMap {
622+
Result.init(catching: {
623+
// copy from cache to destination
624+
try self.fileSystem.copy(from: cachedArtifactPath, to: destination)
625+
return false // not fetched from cache
626+
})
627+
})
628+
}
629+
)
630+
}
631+
632+
private func download(
633+
artifact: RemoteArtifact,
634+
destination: AbsolutePath,
635+
observabilityScope: ObservabilityScope,
636+
progress: @escaping (Int64, Optional<Int64>) -> Void,
637+
completion: @escaping (Result<Void, Error>) -> Void
638+
) {
639+
observabilityScope.emit(debug: "downloading \(artifact.url) to \(destination)")
640+
641+
var headers = HTTPClientHeaders()
642+
headers.add(name: "Accept", value: "application/octet-stream")
643+
var request = LegacyHTTPClient.Request.download(
644+
url: artifact.url,
645+
headers: headers,
646+
fileSystem: self.fileSystem,
647+
destination: destination
648+
)
649+
request.options.authorizationProvider = self.authorizationProvider?.httpAuthorizationHeader(for:)
650+
request.options.retryStrategy = .exponentialBackoff(maxAttempts: 3, baseDelay: .milliseconds(50))
651+
request.options.validResponseCodes = [200]
652+
653+
self.httpClient.execute(
654+
request,
655+
progress: progress,
656+
completion: { result in
657+
completion(result.map{ _ in Void() })
658+
}
659+
)
660+
}
566661
}
567662
}
568663

569664
/// Delegate to notify clients about actions being performed by BinaryArtifactsDownloadsManage.
570665
public protocol BinaryArtifactsManagerDelegate {
571666
/// The workspace has started downloading a binary artifact.
572-
func willDownloadBinaryArtifact(from url: String)
667+
func willDownloadBinaryArtifact(from url: String, fromCache: Bool)
573668
/// The workspace has finished downloading a binary artifact.
574669
func didDownloadBinaryArtifact(
575670
from url: String,
576-
result: Result<AbsolutePath, Error>,
671+
result: Result<(path: AbsolutePath, fromCache: Bool), Error>,
577672
duration: DispatchTimeInterval
578673
)
579674
/// The workspace is downloading a binary artifact.
@@ -833,7 +928,7 @@ extension Workspace {
833928
}
834929

835930
// Download the artifacts
836-
let downloadedArtifacts = try self.binaryArtifactsManager.download(
931+
let downloadedArtifacts = try self.binaryArtifactsManager.fetch(
837932
artifactsToDownload,
838933
artifactsDirectory: self.location.artifactsDirectory,
839934
observabilityScope: observabilityScope

Sources/Workspace/Workspace+Configuration.swift

+5
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ extension Workspace {
145145
self.sharedCacheDirectory.map { $0.appending(components: "registry", "downloads") }
146146
}
147147

148+
/// Path to the shared repositories cache.
149+
public var sharedBinaryArtifactsCacheDirectory: AbsolutePath? {
150+
self.sharedCacheDirectory.map { $0.appending("artifacts") }
151+
}
152+
148153
/// Create a new workspace location.
149154
///
150155
/// - Parameters:

Sources/Workspace/Workspace+Delegation.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,11 @@ public protocol WorkspaceDelegate: AnyObject {
126126
func resolvedFileChanged()
127127

128128
/// The workspace has started downloading a binary artifact.
129-
func willDownloadBinaryArtifact(from url: String)
129+
func willDownloadBinaryArtifact(from url: String, fromCache: Bool)
130130
/// The workspace has finished downloading a binary artifact.
131131
func didDownloadBinaryArtifact(
132132
from url: String,
133-
result: Result<AbsolutePath, Error>,
133+
result: Result<(path: AbsolutePath, fromCache: Bool), Error>,
134134
duration: DispatchTimeInterval
135135
)
136136
/// The workspace is downloading a binary artifact.
@@ -399,13 +399,13 @@ struct WorkspaceBinaryArtifactsManagerDelegate: Workspace.BinaryArtifactsManager
399399
self.workspaceDelegate = workspaceDelegate
400400
}
401401

402-
func willDownloadBinaryArtifact(from url: String) {
403-
self.workspaceDelegate?.willDownloadBinaryArtifact(from: url)
402+
func willDownloadBinaryArtifact(from url: String, fromCache: Bool) {
403+
self.workspaceDelegate?.willDownloadBinaryArtifact(from: url, fromCache: fromCache)
404404
}
405405

406406
func didDownloadBinaryArtifact(
407407
from url: String,
408-
result: Result<AbsolutePath, Error>,
408+
result: Result<(path: AbsolutePath, fromCache: Bool), Error>,
409409
duration: DispatchTimeInterval
410410
) {
411411
self.workspaceDelegate?.didDownloadBinaryArtifact(from: url, result: result, duration: duration)

Sources/Workspace/Workspace.swift

+1
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ public class Workspace {
528528
authorizationProvider: authorizationProvider,
529529
hostToolchain: hostToolchain,
530530
checksumAlgorithm: checksumAlgorithm,
531+
cachePath: customBinaryArtifactsManager?.useCache == false || !configuration.sharedDependenciesCacheEnabled ? .none : location.sharedBinaryArtifactsCacheDirectory,
531532
customHTTPClient: customBinaryArtifactsManager?.httpClient,
532533
customArchiver: customBinaryArtifactsManager?.archiver,
533534
delegate: delegate.map(WorkspaceBinaryArtifactsManagerDelegate.init(workspaceDelegate:))

0 commit comments

Comments
 (0)