Skip to content

Commit 49bed82

Browse files
authored
introduce archive index files (#3302)
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 introduces 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 1e792cb commit 49bed82

File tree

7 files changed

+822
-52
lines changed

7 files changed

+822
-52
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

Sources/SPMTestSupport/MockHTTPClient.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@
99
*/
1010

1111
import Basics
12-
import Foundation
1312
import TSCBasic
1413
import TSCUtility
1514

1615
extension HTTPClient {
1716
public static func mock(fileSystem: FileSystem) -> HTTPClient {
1817
let handler: HTTPClient.Handler = { request, _, completion in
19-
2018
switch request.kind {
2119
case.generic:
2220
completion(.success(.okay(body: request.url.absoluteString)))
@@ -33,6 +31,6 @@ extension HTTPClient {
3331
}
3432
}
3533
}
36-
return HTTPClient(handler: handler)
34+
return HTTPClient(handler: handler)
3735
}
3836
}

Sources/SPMTestSupport/MockHashAlgorithm.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,25 @@
66

77
See http://swift.org/LICENSE.txt for license information
88
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9-
*/
9+
*/
1010

11+
import Basics
1112
import TSCBasic
1213

1314
public class MockHashAlgorithm: HashAlgorithm {
1415
public typealias Hash = (ByteString) -> ByteString
1516

16-
public private(set) var hashes: [ByteString] = []
17+
public private(set) var hashes = ThreadSafeArrayStore<ByteString>()
1718
private var hashFunction: Hash!
1819

1920
public init(hash: Hash? = nil) {
20-
hashFunction = hash ?? { hash in
21+
self.hashFunction = hash ?? { hash in
2122
self.hashes.append(hash)
2223
return ByteString(hash.contents.reversed())
2324
}
2425
}
2526

2627
public func hash(_ bytes: ByteString) -> ByteString {
27-
return hashFunction(bytes)
28+
return self.hashFunction(bytes)
2829
}
2930
}

Sources/SPMTestSupport/MockWorkspace.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ public final class MockWorkspace {
3434
let skipUpdate: Bool
3535
let enablePubGrub: Bool
3636

37-
3837
public init(
3938
sandbox: AbsolutePath,
4039
fs: FileSystem,

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: 108 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ extension Workspace {
771771

772772
return diagnostics.wrap {
773773
let contents = try fileSystem.readFileContents(path)
774-
return checksumAlgorithm.hash(contents).hexadecimalRepresentation
774+
return self.checksumAlgorithm.hash(contents).hexadecimalRepresentation
775775
} ?? ""
776776
}
777777
}
@@ -1546,7 +1546,7 @@ extension Workspace {
15461546
targetName: target.name,
15471547
path: absolutePath)
15481548
)
1549-
} else if let url = target.url, let checksum = target.checksum {
1549+
} else if let url = target.url.flatMap(URL.init(string:)), let checksum = target.checksum {
15501550
remoteArtifacts.append(
15511551
.init(
15521552
packageRef: packageRef,
@@ -1566,23 +1566,78 @@ extension Workspace {
15661566
private func download(_ artifacts: [RemoteArtifact], diagnostics: DiagnosticsEngine) throws -> [ManagedArtifact] {
15671567
let group = DispatchGroup()
15681568
let tempDiagnostics = DiagnosticsEngine()
1569+
let result = ThreadSafeArrayStore<ManagedArtifact>()
15691570

15701571
var authProvider: AuthorizationProviding? = nil
1571-
#if os(macOS)
1572-
// Netrc feature currently only supported on macOS
1572+
#if os(macOS) // Netrc feature currently only supported on macOS
15731573
authProvider = try? Netrc.load(fromFileAtPath: netrcFilePath).get()
15741574
#endif
15751575

1576-
var didDownloadAnyArtifact = false
1577-
let result = ThreadSafeArrayStore<ManagedArtifact>()
1576+
// zip files to download
1577+
// stored in a thread-safe way as we may fetch more from "ari" files
1578+
let zipArtifacts = ThreadSafeArrayStore<RemoteArtifact>(artifacts.filter { $0.url.pathExtension.lowercased() == "zip" })
1579+
1580+
// fetch and parse "ari" files, if any
1581+
let indexFiles = artifacts.filter { $0.url.pathExtension.lowercased() == "ari" }
1582+
if !indexFiles.isEmpty {
1583+
let hostToolchain = try UserToolchain(destination: .hostDestination())
1584+
let jsonDecoder = JSONDecoder.makeWithDefaults()
1585+
for indexFile in indexFiles {
1586+
group.enter()
1587+
var request = HTTPClient.Request(method: .get, url: indexFile.url)
1588+
request.options.validResponseCodes = [200]
1589+
request.options.authorizationProvider = authProvider?.authorization(for:)
1590+
self.httpClient.execute(request) { result in
1591+
defer { group.leave() }
15781592

1579-
for artifact in artifacts {
1580-
group.enter()
1581-
defer { group.leave() }
1593+
do {
1594+
switch result {
1595+
case .failure(let error):
1596+
throw error
1597+
case .success(let response):
1598+
guard let body = response.body else {
1599+
throw StringError("Body is empty")
1600+
}
1601+
// FIXME: would be nice if checksumAlgorithm.hash took Data directly
1602+
let bodyChecksum = self.checksumAlgorithm.hash(ByteString(body)).hexadecimalRepresentation
1603+
guard bodyChecksum == indexFile.checksum else {
1604+
throw StringError("checksum of downloaded artifact of binary target '\(indexFile.targetName)' (\(bodyChecksum)) does not match checksum specified by the manifest (\(indexFile.checksum ))")
1605+
}
1606+
let metadata = try jsonDecoder.decode(ArchiveIndexFile.self, from: body)
1607+
// FIXME: this filter needs to become more sophisticated
1608+
guard let supportedArchive = metadata.archives.first(where: { $0.fileName.lowercased().hasSuffix(".zip") && $0.supportedTriples.contains(hostToolchain.triple) }) else {
1609+
throw StringError("No supported archive was found for '\(hostToolchain.triple.tripleString)'")
1610+
}
1611+
// add relevant archive
1612+
zipArtifacts.append(
1613+
RemoteArtifact(
1614+
packageRef: indexFile.packageRef,
1615+
targetName: indexFile.targetName,
1616+
url: indexFile.url.deletingLastPathComponent().appendingPathComponent(supportedArchive.fileName),
1617+
checksum: supportedArchive.checksum)
1618+
)
1619+
}
1620+
} catch {
1621+
tempDiagnostics.emit(.error("failed retrieving '\(indexFile.url)': \(error)"))
1622+
}
1623+
}
1624+
}
15821625

1583-
guard let parsedURL = URL(string: artifact.url) else {
1584-
throw StringError("invalid url \(artifact.url)")
1626+
// wait for all "ari" files to be processed
1627+
group.wait()
1628+
1629+
// no reason to continue if we already ran into issues
1630+
if tempDiagnostics.hasErrors {
1631+
// collect all diagnostics
1632+
diagnostics.append(contentsOf: tempDiagnostics)
1633+
throw Diagnostics.fatalError
15851634
}
1635+
}
1636+
1637+
// finally download zip files, if any
1638+
for artifact in (zipArtifacts.map{ $0 }) {
1639+
group.enter()
1640+
defer { group.leave() }
15861641

15871642
let parentDirectory = self.artifactsPath.appending(component: artifact.packageRef.name)
15881643

@@ -1593,20 +1648,17 @@ extension Workspace {
15931648
continue
15941649
}
15951650

1596-
let archivePath = parentDirectory.appending(component: parsedURL.lastPathComponent)
1597-
1598-
didDownloadAnyArtifact = true
1651+
let archivePath = parentDirectory.appending(component: artifact.url.lastPathComponent)
15991652

16001653
group.enter()
1601-
1602-
var request = HTTPClient.Request.download(url: parsedURL, fileSystem: self.fileSystem, destination: archivePath)
1654+
var request = HTTPClient.Request.download(url: artifact.url, fileSystem: self.fileSystem, destination: archivePath)
16031655
request.options.authorizationProvider = authProvider?.authorization(for:)
16041656
request.options.validResponseCodes = [200]
16051657
self.httpClient.execute(
16061658
request,
16071659
progress: { bytesDownloaded, totalBytesToDownload in
16081660
self.delegate?.downloadingBinaryArtifact(
1609-
from: artifact.url,
1661+
from: artifact.url.absoluteString,
16101662
bytesDownloaded: bytesDownloaded,
16111663
totalBytesToDownload: totalBytesToDownload)
16121664
},
@@ -1642,33 +1694,32 @@ extension Workspace {
16421694
.remote(
16431695
packageRef: artifact.packageRef,
16441696
targetName: artifact.targetName,
1645-
url: artifact.url,
1697+
url: artifact.url.absoluteString,
16461698
checksum: artifact.checksum,
16471699
path: artifactPath
16481700
)
16491701
)
16501702
case .failure(let error):
16511703
let reason = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
1652-
tempDiagnostics.emit(.artifactFailedExtraction(targetName: artifact.targetName, reason: reason))
1704+
tempDiagnostics.emit(.artifactFailedExtraction(artifactURL: artifact.url, targetName: artifact.targetName, reason: reason))
16531705
}
16541706

16551707
tempDiagnostics.wrap { try self.fileSystem.removeFileTree(archivePath) }
16561708
})
16571709
case .failure(let error):
1658-
tempDiagnostics.emit(.artifactFailedDownload(targetName: artifact.targetName, reason: "\(error)"))
1710+
tempDiagnostics.emit(.artifactFailedDownload(artifactURL: artifact.url, targetName: artifact.targetName, reason: "\(error)"))
16591711
}
16601712
})
16611713
}
16621714

16631715
group.wait()
16641716

1665-
if didDownloadAnyArtifact {
1717+
if zipArtifacts.count > 0 {
16661718
delegate?.didDownloadBinaryArtifacts()
16671719
}
16681720

1669-
for diagnostic in tempDiagnostics.diagnostics {
1670-
diagnostics.emit(diagnostic.message, location: diagnostic.location)
1671-
}
1721+
// collect all diagnostics
1722+
diagnostics.append(contentsOf: tempDiagnostics)
16721723

16731724
return result.map{ $0 }
16741725
}
@@ -2565,10 +2616,34 @@ public final class LoadableResult<Value> {
25652616
private struct RemoteArtifact {
25662617
let packageRef: PackageReference
25672618
let targetName: String
2568-
let url: String
2619+
let url: Foundation.URL
25692620
let checksum: String
25702621
}
25712622

2623+
private struct ArchiveIndexFile: Decodable {
2624+
let schemaVersion: String
2625+
let archives: [Archive]
2626+
2627+
struct Archive: Decodable {
2628+
let fileName: String
2629+
let checksum: String
2630+
let supportedTriples: [Triple]
2631+
2632+
enum CodingKeys: String, CodingKey {
2633+
case fileName
2634+
case checksum
2635+
case supportedTriples
2636+
}
2637+
2638+
public init(from decoder: Decoder) throws {
2639+
let container = try decoder.container(keyedBy: CodingKeys.self)
2640+
self.fileName = try container.decode(String.self, forKey: .fileName)
2641+
self.checksum = try container.decode(String.self, forKey: .checksum)
2642+
self.supportedTriples = try container.decode([String].self, forKey: .supportedTriples).map(Triple.init)
2643+
}
2644+
}
2645+
}
2646+
25722647
private extension ManagedArtifact {
25732648
var originURL: String? {
25742649
switch self.source {
@@ -2596,3 +2671,11 @@ private extension PackageDependencyDescription {
25962671
}
25972672
}
25982673
}
2674+
2675+
private extension DiagnosticsEngine {
2676+
func append(contentsOf other: DiagnosticsEngine) {
2677+
for diagnostic in other.diagnostics {
2678+
self.emit(diagnostic.message, location: diagnostic.location)
2679+
}
2680+
}
2681+
}

0 commit comments

Comments
 (0)