Skip to content

Commit 3b2bf99

Browse files
committed
introduce archive index files
motivation: to support the package extensions feature, we need to support binary dependencies with tools that can be distributed across different platforms. in previous PR we intriduces the archive file, this PR adds the archive index files which helps control the size of the archive files by splitting them across key platforms changes: * introduce new binary dependency file type named "Archive Index" with the extension "ari" * the structure of the file is a flat list of archive files, their checksums and the target-triples they support * add code to Workspace pre-downloading the binaryTarget "zip" files to first fetch "ari" files, parse them and add any relevant "zip" files to the *host* tripple to the list of candidate downloads * add and adjust tests
1 parent 32c49a3 commit 3b2bf99

File tree

6 files changed

+760
-27
lines changed

6 files changed

+760
-27
lines changed

Sources/Basics/ConcurrencyHelpers.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ public final class ThreadSafeArrayStore<Value> {
109109
}
110110
}
111111

112+
@discardableResult
113+
public func append(contentsOf items: [Value]) -> Int {
114+
self.lock.withLock {
115+
self.underlying.append(contentsOf: items)
116+
return self.underlying.count
117+
}
118+
}
119+
112120
public var count: Int {
113121
self.lock.withLock {
114122
self.underlying.count
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Basics
12+
import TSCBasic
13+
import TSCUtility
14+
import Foundation
15+
16+
extension HTTPClient {
17+
public static func mock() -> HTTPClient {
18+
let handler: HTTPClient.Handler = { request, _, completion in
19+
completion(.success(.init(statusCode: 200, body: Data(request.url.absoluteString.utf8))))
20+
}
21+
return HTTPClient(handler: handler)
22+
}
23+
}

Sources/SPMTestSupport/MockWorkspace.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Basics
2020
public final class MockWorkspace {
2121
let sandbox: AbsolutePath
2222
let fs: FileSystem
23+
public let httpClient: HTTPClient
2324
public let downloader: MockDownloader
2425
public let archiver: MockArchiver
2526
public let checksumAlgorithm: MockHashAlgorithm
@@ -34,10 +35,10 @@ public final class MockWorkspace {
3435
let skipUpdate: Bool
3536
let enablePubGrub: Bool
3637

37-
3838
public init(
3939
sandbox: AbsolutePath,
4040
fs: FileSystem,
41+
httpClient: HTTPClient = HTTPClient.mock(),
4142
downloader: MockDownloader? = nil,
4243
archiver: MockArchiver = MockArchiver(),
4344
checksumAlgorithm: MockHashAlgorithm = MockHashAlgorithm(),
@@ -50,6 +51,7 @@ public final class MockWorkspace {
5051
) throws {
5152
self.sandbox = sandbox
5253
self.fs = fs
54+
self.httpClient = httpClient
5355
self.downloader = downloader ?? MockDownloader(fileSystem: fs)
5456
self.archiver = archiver
5557
self.checksumAlgorithm = checksumAlgorithm
@@ -172,6 +174,7 @@ public final class MockWorkspace {
172174
fileSystem: self.fs,
173175
repositoryProvider: self.repoProvider,
174176
identityResolver: self.identityResolver,
177+
httpClient: self.httpClient,
175178
downloader: self.downloader,
176179
archiver: self.archiver,
177180
checksumAlgorithm: self.checksumAlgorithm,

Sources/Workspace/Diagnostics.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,12 @@ extension Diagnostic.Message {
141141
.error("checksum of downloaded artifact of binary target '\(targetName)' (\(actualChecksum)) does not match checksum specified by the manifest (\(expectedChecksum))")
142142
}
143143

144-
static func artifactFailedDownload(targetName: String, reason: String) -> Diagnostic.Message {
145-
.error("artifact of binary target '\(targetName)' failed download: \(reason)")
144+
static func artifactFailedDownload(artifactURL: Foundation.URL, targetName: String, reason: String) -> Diagnostic.Message {
145+
.error("failed downloading '\(artifactURL.absoluteString)' which is required by binary target '\(targetName)': \(reason)")
146146
}
147147

148-
static func artifactFailedExtraction(targetName: String, reason: String) -> Diagnostic.Message {
149-
.error("artifact of binary target '\(targetName)' failed extraction: \(reason)")
148+
static func artifactFailedExtraction(artifactURL: Foundation.URL, targetName: String, reason: String) -> Diagnostic.Message {
149+
.error("failed extracting '\(artifactURL.absoluteString)' which is required by binary target '\(targetName)': \(reason)")
150150
}
151151

152152
static func artifactNotFound(targetName: String, artifactName: String) -> Diagnostic.Message {

Sources/Workspace/Workspace.swift

Lines changed: 111 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,11 @@ public class Workspace {
211211
/// The package container provider.
212212
fileprivate let containerProvider: RepositoryPackageContainerProvider
213213

214+
/// The http client used for downloading binary artifacts.
215+
fileprivate let httpClient: HTTPClient
216+
214217
/// The downloader used for downloading binary artifacts.
218+
// FIXME: consolidate HttpClient and Downloader as they provide same functionality
215219
fileprivate let downloader: Downloader
216220

217221
fileprivate let netrcFilePath: AbsolutePath?
@@ -267,6 +271,7 @@ public class Workspace {
267271
fileSystem: FileSystem = localFileSystem,
268272
repositoryProvider: RepositoryProvider = GitRepositoryProvider(),
269273
identityResolver: IdentityResolver? = nil,
274+
httpClient: HTTPClient? = nil,
270275
downloader: Downloader = FoundationDownloader(),
271276
netrcFilePath: AbsolutePath? = nil,
272277
archiver: Archiver = ZipArchiver(),
@@ -286,6 +291,7 @@ public class Workspace {
286291
self.currentToolsVersion = currentToolsVersion
287292
self.toolsVersionLoader = toolsVersionLoader
288293
self.downloader = downloader
294+
self.httpClient = httpClient ?? HTTPClient()
289295
self.netrcFilePath = netrcFilePath
290296
self.archiver = archiver
291297

@@ -771,7 +777,7 @@ extension Workspace {
771777

772778
return diagnostics.wrap {
773779
let contents = try fileSystem.readFileContents(path)
774-
return checksumAlgorithm.hash(contents).hexadecimalRepresentation
780+
return self.checksumAlgorithm.hash(contents).hexadecimalRepresentation
775781
} ?? ""
776782
}
777783
}
@@ -1546,7 +1552,7 @@ extension Workspace {
15461552
targetName: target.name,
15471553
path: absolutePath)
15481554
)
1549-
} else if let url = target.url, let checksum = target.checksum {
1555+
} else if let url = target.url.flatMap(URL.init(string:)), let checksum = target.checksum {
15501556
remoteArtifacts.append(
15511557
.init(
15521558
packageRef: packageRef,
@@ -1566,6 +1572,7 @@ extension Workspace {
15661572
private func download(_ artifacts: [RemoteArtifact], diagnostics: DiagnosticsEngine) throws -> [ManagedArtifact] {
15671573
let group = DispatchGroup()
15681574
let tempDiagnostics = DiagnosticsEngine()
1575+
let result = ThreadSafeArrayStore<ManagedArtifact>()
15691576

15701577
var authProvider: AuthorizationProviding? = nil
15711578
var didDownloadAnyArtifact = false
@@ -1577,15 +1584,70 @@ extension Workspace {
15771584
}
15781585
#endif
15791586

1580-
let result = ThreadSafeArrayStore<ManagedArtifact>()
1587+
// zip files to download
1588+
// stored in a thread-safe way as we may fetch more from "ari" files
1589+
let zipArtifacts = ThreadSafeArrayStore<RemoteArtifact>(artifacts.filter { $0.url.pathExtension.lowercased() == "zip" })
15811590

1582-
for artifact in artifacts {
1591+
// fetch and parse "ari" files, if any
1592+
let hostToolchain = try UserToolchain(destination: .hostDestination())
1593+
let jsonDecoder = JSONDecoder.makeWithDefaults()
1594+
let indexFiles = artifacts.filter { $0.url.pathExtension.lowercased() == "ari" }
1595+
for indexFile in indexFiles {
15831596
group.enter()
1584-
defer { group.leave() }
1597+
var request = HTTPClient.Request(method: .get, url: indexFile.url)
1598+
request.options.validResponseCodes = [200]
1599+
request.options.authorizationProvider = authProvider?.authorization(for:)
1600+
self.httpClient.execute(request) { result in
1601+
defer { group.leave() }
15851602

1586-
guard let parsedURL = URL(string: artifact.url) else {
1587-
throw StringError("invalid url \(artifact.url)")
1603+
do {
1604+
switch result {
1605+
case .failure(let error):
1606+
throw error
1607+
case .success(let response):
1608+
guard let body = response.body else {
1609+
throw StringError("Body is empty")
1610+
}
1611+
// FIXME: would be nice if checksumAlgorithm.hash took Data directly
1612+
let bodyChecksum = self.checksumAlgorithm.hash(ByteString(body)).hexadecimalRepresentation
1613+
guard bodyChecksum == indexFile.checksum else {
1614+
throw StringError("checksum of downloaded artifact of binary target '\(indexFile.targetName)' (\(bodyChecksum)) does not match checksum specified by the manifest (\(indexFile.checksum ))")
1615+
}
1616+
let metadata = try jsonDecoder.decode(ArchiveIndexFile.self, from: body)
1617+
// FIXME: this filter needs to become more sophisticated
1618+
guard let supportedArchive = metadata.archives.first(where: { $0.fileName.lowercased().hasSuffix(".zip") && $0.supportedTriples.contains(hostToolchain.triple) }) else {
1619+
throw StringError("No supported archive was found for '\(hostToolchain.triple)'")
1620+
}
1621+
// add relevant archive
1622+
zipArtifacts.append(
1623+
RemoteArtifact(
1624+
packageRef: indexFile.packageRef,
1625+
targetName: indexFile.targetName,
1626+
url: indexFile.url.deletingLastPathComponent().appendingPathComponent(supportedArchive.fileName),
1627+
checksum: supportedArchive.checksum)
1628+
)
1629+
}
1630+
} catch {
1631+
tempDiagnostics.emit(.error("failed retrieving '\(indexFile.url)': \(error)"))
1632+
}
15881633
}
1634+
}
1635+
1636+
// wait for all "ari" files to be processed
1637+
group.wait()
1638+
1639+
// no reason to continue if we already ran into issues
1640+
if tempDiagnostics.hasErrors {
1641+
// collect all diagnostics
1642+
diagnostics.append(contentsOf: tempDiagnostics)
1643+
throw Diagnostics.fatalError
1644+
}
1645+
1646+
// finally download zip files, if any
1647+
didDownloadAnyArtifact = zipArtifacts.count > 0
1648+
for artifact in (zipArtifacts.map{ $0 }) {
1649+
group.enter()
1650+
defer { group.leave() }
15891651

15901652
let parentDirectory = self.artifactsPath.appending(component: artifact.packageRef.name)
15911653

@@ -1596,18 +1658,16 @@ extension Workspace {
15961658
continue
15971659
}
15981660

1599-
let archivePath = parentDirectory.appending(component: parsedURL.lastPathComponent)
1600-
1601-
didDownloadAnyArtifact = true
1661+
let archivePath = parentDirectory.appending(component: artifact.url.lastPathComponent)
16021662

16031663
group.enter()
1604-
downloader.downloadFile(
1605-
at: parsedURL,
1664+
self.downloader.downloadFile(
1665+
at: artifact.url,
16061666
to: archivePath,
16071667
withAuthorizationProvider: authProvider,
16081668
progress: { bytesDownloaded, totalBytesToDownload in
16091669
self.delegate?.downloadingBinaryArtifact(
1610-
from: artifact.url,
1670+
from: artifact.url.absoluteString,
16111671
bytesDownloaded: bytesDownloaded,
16121672
totalBytesToDownload: totalBytesToDownload)
16131673
},
@@ -1643,21 +1703,21 @@ extension Workspace {
16431703
.remote(
16441704
packageRef: artifact.packageRef,
16451705
targetName: artifact.targetName,
1646-
url: artifact.url,
1706+
url: artifact.url.absoluteString,
16471707
checksum: artifact.checksum,
16481708
path: artifactPath
16491709
)
16501710
)
16511711
case .failure(let error):
16521712
let reason = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
1653-
tempDiagnostics.emit(.artifactFailedExtraction(targetName: artifact.targetName, reason: reason))
1713+
tempDiagnostics.emit(.artifactFailedExtraction(artifactURL: artifact.url, targetName: artifact.targetName, reason: reason))
16541714
}
16551715

16561716
tempDiagnostics.wrap { try self.fileSystem.removeFileTree(archivePath) }
16571717
})
16581718
case .failure(let error):
16591719
let reason = error.errorDescription ?? error.localizedDescription
1660-
tempDiagnostics.emit(.artifactFailedDownload(targetName: artifact.targetName, reason: reason))
1720+
tempDiagnostics.emit(.artifactFailedDownload(artifactURL: artifact.url, targetName: artifact.targetName, reason: reason))
16611721
}
16621722
})
16631723
}
@@ -1668,9 +1728,8 @@ extension Workspace {
16681728
delegate?.didDownloadBinaryArtifacts()
16691729
}
16701730

1671-
for diagnostic in tempDiagnostics.diagnostics {
1672-
diagnostics.emit(diagnostic.message, location: diagnostic.location)
1673-
}
1731+
// collect all diagnostics
1732+
diagnostics.append(contentsOf: tempDiagnostics)
16741733

16751734
return result.map{ $0 }
16761735
}
@@ -2567,10 +2626,34 @@ public final class LoadableResult<Value> {
25672626
private struct RemoteArtifact {
25682627
let packageRef: PackageReference
25692628
let targetName: String
2570-
let url: String
2629+
let url: Foundation.URL
25712630
let checksum: String
25722631
}
25732632

2633+
private struct ArchiveIndexFile: Decodable {
2634+
let schemaVersion: String
2635+
let archives: [Archive]
2636+
2637+
struct Archive: Decodable {
2638+
let fileName: String
2639+
let checksum: String
2640+
let supportedTriples: [Triple]
2641+
2642+
enum CodingKeys: String, CodingKey {
2643+
case fileName
2644+
case checksum
2645+
case supportedTriples
2646+
}
2647+
2648+
public init(from decoder: Decoder) throws {
2649+
let container = try decoder.container(keyedBy: CodingKeys.self)
2650+
self.fileName = try container.decode(String.self, forKey: .fileName)
2651+
self.checksum = try container.decode(String.self, forKey: .checksum)
2652+
self.supportedTriples = try container.decode([String].self, forKey: .supportedTriples).map(Triple.init)
2653+
}
2654+
}
2655+
}
2656+
25742657
private extension ManagedArtifact {
25752658
var originURL: String? {
25762659
switch self.source {
@@ -2598,3 +2681,11 @@ private extension PackageDependencyDescription {
25982681
}
25992682
}
26002683
}
2684+
2685+
private extension DiagnosticsEngine {
2686+
func append(contentsOf other: DiagnosticsEngine) {
2687+
for diagnostic in other.diagnostics {
2688+
self.emit(diagnostic.message, location: diagnostic.location)
2689+
}
2690+
}
2691+
}

0 commit comments

Comments
 (0)