Skip to content

Commit 1cdff33

Browse files
committed
Add PackageRegistry target
Add PackageRegistryTests target
1 parent 27d2b45 commit 1cdff33

File tree

6 files changed

+508
-1
lines changed

6 files changed

+508
-1
lines changed

Package.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,16 @@ let package = Package(
133133
name: "LLBuildManifest",
134134
dependencies: ["SwiftToolsSupport-auto", "Basics"]),
135135

136+
.target(
137+
/** Package registry support */
138+
name: "PackageRegistry",
139+
dependencies: ["SwiftToolsSupport-auto", "Basics", "PackageLoading", "PackageModel"]),
140+
136141
.target(
137142
/** Source control operations */
138143
name: "SourceControl",
139144
dependencies: ["SwiftToolsSupport-auto", "Basics"]),
145+
140146
.target(
141147
/** Shim for llbuild library */
142148
name: "SPMLLBuild",
@@ -158,7 +164,7 @@ let package = Package(
158164
.target(
159165
/** Data structures and support for complete package graphs */
160166
name: "PackageGraph",
161-
dependencies: ["SwiftToolsSupport-auto", "Basics", "PackageLoading", "PackageModel", "SourceControl"]),
167+
dependencies: ["SwiftToolsSupport-auto", "Basics", "PackageLoading", "PackageModel", "PackageRegistry", "SourceControl"]),
162168

163169
// MARK: Package Collections
164170

@@ -300,6 +306,9 @@ let package = Package(
300306
.testTarget(
301307
name: "PackageCollectionsTests",
302308
dependencies: ["PackageCollections", "SPMTestSupport"]),
309+
.testTarget(
310+
name: "PackageRegistryTests",
311+
dependencies: ["SPMTestSupport", "PackageRegistry"]),
303312
.testTarget(
304313
name: "SourceControlTests",
305314
dependencies: ["SourceControl", "SPMTestSupport"]),

Sources/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ add_subdirectory(PackageGraph)
1919
add_subdirectory(PackageLoading)
2020
add_subdirectory(PackageModel)
2121
add_subdirectory(PackagePlugin)
22+
add_subdirectory(PackageRegistry)
2223
add_subdirectory(SPMBuildCore)
2324
add_subdirectory(SPMLLBuild)
2425
add_subdirectory(SourceControl)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# This source file is part of the Swift.org open source project
2+
#
3+
# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
4+
# Licensed under Apache License v2.0 with Runtime Library Exception
5+
#
6+
# See http://swift.org/LICENSE.txt for license information
7+
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors
8+
9+
add_library(PackageRegistry
10+
RegistryManager.swift)
11+
target_link_libraries(PackageRegistry PUBLIC
12+
TSCBasic
13+
PackageLoading
14+
PackageModel
15+
TSCUtility)
16+
# NOTE(compnerd) workaround for CMake not setting up include flags yet
17+
set_target_properties(PackageRegistry PROPERTIES
18+
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
19+
20+
if(USE_CMAKE_INSTALL)
21+
install(TARGETS PackageRegistry
22+
ARCHIVE DESTINATION lib
23+
LIBRARY DESTINATION lib
24+
RUNTIME DESTINATION bin)
25+
endif()
26+
set_property(GLOBAL APPEND PROPERTY SwiftPM_EXPORTS PackageRegistry)
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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 PackageLoading
13+
import PackageModel
14+
15+
import TSCBasic
16+
import TSCUtility
17+
18+
import struct Foundation.URL
19+
import struct Foundation.URLComponents
20+
import struct Foundation.URLQueryItem
21+
22+
import Dispatch
23+
24+
public enum RegistryError: Error {
25+
case invalidPackage(PackageReference)
26+
case invalidOperation
27+
case invalidResponse
28+
case invalidURL
29+
case invalidChecksum(expected: String, actual: String)
30+
}
31+
32+
public final class RegistryManager {
33+
internal static var archiverFactory: (FileSystem) -> Archiver = { fileSystem in
34+
return ZipArchiver(fileSystem: fileSystem)
35+
}
36+
37+
internal static var clientFactory: (DiagnosticsEngine?) -> HTTPClientProtocol = { diagnosticEngine in
38+
var configuration = HTTPClientConfiguration()
39+
return HTTPClient(configuration: configuration, handler: nil, diagnosticsEngine: diagnosticEngine)
40+
}
41+
42+
private static var cache = ThreadSafeKeyValueStore<URL, RegistryManager>()
43+
44+
private let registryBaseURL: Foundation.URL
45+
private let client: HTTPClientProtocol
46+
private let identityResolver: IdentityResolver
47+
private let diagnosticEngine: DiagnosticsEngine?
48+
49+
public init(registryBaseURL: Foundation.URL,
50+
identityResolver: IdentityResolver,
51+
diagnosticEngine: DiagnosticsEngine? = nil)
52+
{
53+
self.registryBaseURL = registryBaseURL
54+
self.client = Self.clientFactory(diagnosticEngine)
55+
self.identityResolver = identityResolver
56+
self.diagnosticEngine = diagnosticEngine
57+
}
58+
59+
public func fetchVersions(
60+
of package: PackageReference,
61+
on queue: DispatchQueue,
62+
completion: @escaping (Result<[Version], Error>) -> Void
63+
) {
64+
guard case let (scope, name)? = package.scopeAndName else {
65+
return completion(.failure(RegistryError.invalidPackage(package)))
66+
}
67+
68+
var components = URLComponents(url: registryBaseURL, resolvingAgainstBaseURL: true)!
69+
components.path += "/\(scope)/\(name)"
70+
71+
guard let url = components.url else {
72+
return completion(.failure(RegistryError.invalidURL))
73+
}
74+
75+
let request = HTTPClient.Request(
76+
method: .get,
77+
url: url,
78+
headers: [
79+
"Accept": "application/vnd.swift.registry.v1+json"
80+
]
81+
)
82+
83+
client.execute(request, progress: nil) { result in
84+
completion(result.tryMap { response in
85+
if response.statusCode == 200,
86+
response.headers.get("Content-Version").first == "1",
87+
response.headers.get("Content-Type").first?.hasPrefix("application/json") == true,
88+
let data = response.body,
89+
case .dictionary(let payload) = try? JSON(data: data),
90+
case .dictionary(let releases) = payload["releases"]
91+
{
92+
let versions = releases.filter { (try? $0.value.getJSON("problem")) == nil }
93+
.compactMap { Version(string: $0.key) }
94+
.sorted(by: >)
95+
return versions
96+
} else {
97+
throw RegistryError.invalidResponse
98+
}
99+
})
100+
}
101+
}
102+
103+
public func fetchManifest(
104+
for version: Version,
105+
of package: PackageReference,
106+
using manifestLoader: ManifestLoaderProtocol,
107+
toolsVersion: ToolsVersion = .currentToolsVersion,
108+
swiftLanguageVersion: SwiftLanguageVersion? = nil,
109+
on queue: DispatchQueue,
110+
completion: @escaping (Result<Manifest, Error>) -> Void
111+
) {
112+
guard case let (scope, name)? = package.scopeAndName else {
113+
return completion(.failure(RegistryError.invalidPackage(package)))
114+
}
115+
116+
var components = URLComponents(url: registryBaseURL, resolvingAgainstBaseURL: true)!
117+
components.path += "/\(scope)/\(name)/\(version)/Package.swift"
118+
if let swiftLanguageVersion = swiftLanguageVersion {
119+
components.queryItems = [
120+
URLQueryItem(name: "swift-version", value: swiftLanguageVersion.rawValue)
121+
]
122+
}
123+
124+
guard let url = components.url else {
125+
return completion(.failure(RegistryError.invalidURL))
126+
}
127+
128+
let request = HTTPClient.Request(
129+
method: .get,
130+
url: url,
131+
headers: [
132+
"Accept": "application/vnd.swift.registry.v1+swift"
133+
]
134+
)
135+
136+
client.execute(request, progress: nil) { result in
137+
do {
138+
if case .failure(let error) = result {
139+
throw error
140+
} else if case .success(let response) = result,
141+
response.statusCode == 200,
142+
response.headers.get("Content-Version").first == "1",
143+
response.headers.get("Content-Type").first?.hasPrefix("text/x-swift") == true,
144+
let data = response.body
145+
{
146+
let fs = InMemoryFileSystem()
147+
148+
let filename: String
149+
if let swiftLanguageVersion = swiftLanguageVersion {
150+
filename = Manifest.basename + "@swift-\(swiftLanguageVersion).swift"
151+
} else {
152+
filename = Manifest.basename + ".swift"
153+
}
154+
155+
try fs.writeFileContents(.root.appending(component: filename), bytes: ByteString(data))
156+
manifestLoader.load(at: .root, packageIdentity: package.identity, packageKind: .remote, packageLocation: package.location, version: version, revision: nil, toolsVersion: .currentToolsVersion, identityResolver: self.identityResolver, fileSystem: fs, diagnostics: self.diagnosticEngine, on: .sharedConcurrent, completion: completion)
157+
} else {
158+
throw RegistryError.invalidResponse
159+
}
160+
} catch {
161+
queue.async {
162+
completion(.failure(error))
163+
}
164+
}
165+
}
166+
}
167+
168+
public func downloadSourceArchive(
169+
for version: Version,
170+
of package: PackageReference,
171+
into fileSystem: FileSystem,
172+
at destinationPath: AbsolutePath,
173+
expectedChecksum: ByteString? = nil,
174+
on queue: DispatchQueue,
175+
completion: @escaping (Result<Void, Error>) -> Void
176+
) {
177+
guard case let (scope, name)? = package.scopeAndName else {
178+
return completion(.failure(RegistryError.invalidPackage(package)))
179+
}
180+
181+
var components = URLComponents(url: registryBaseURL, resolvingAgainstBaseURL: true)!
182+
components.path += "/\(scope)/\(name)/\(version).zip"
183+
184+
guard let url = components.url else {
185+
return completion(.failure(RegistryError.invalidURL))
186+
}
187+
188+
let request = HTTPClient.Request(
189+
method: .get,
190+
url: url,
191+
headers: [
192+
"Accept": "application/vnd.swift.registry.v1+zip"
193+
]
194+
)
195+
196+
client.execute(request, progress: nil) { result in
197+
switch result {
198+
case .success(let response):
199+
if response.statusCode == 200,
200+
response.headers.get("Content-Version").first == "1",
201+
response.headers.get("Content-Type").first?.hasPrefix("application/zip") == true,
202+
let digest = response.headers.get("Digest").first,
203+
let data = response.body
204+
{
205+
do {
206+
let contents = ByteString(data)
207+
let advertisedChecksum = digest.spm_dropPrefix("sha-256=")
208+
let actualChecksum = SHA256().hash(contents).hexadecimalRepresentation
209+
210+
guard (expectedChecksum?.hexadecimalRepresentation ?? actualChecksum) == actualChecksum,
211+
advertisedChecksum == actualChecksum
212+
else {
213+
throw RegistryError.invalidChecksum(
214+
expected: expectedChecksum?.hexadecimalRepresentation ?? advertisedChecksum,
215+
actual: actualChecksum
216+
)
217+
}
218+
219+
let archivePath = destinationPath.withExtension("zip")
220+
try fileSystem.writeFileContents(archivePath, bytes: contents)
221+
222+
try fileSystem.createDirectory(destinationPath, recursive: true)
223+
224+
let archiver = Self.archiverFactory(fileSystem)
225+
// TODO: Bail if archive contains relative paths or overlapping files
226+
archiver.extract(from: archivePath, to: destinationPath) { result in
227+
completion(result)
228+
try? fileSystem.removeFileTree(archivePath)
229+
}
230+
} catch {
231+
try? fileSystem.removeFileTree(destinationPath)
232+
completion(.failure(error))
233+
}
234+
} else {
235+
completion(.failure(RegistryError.invalidResponse))
236+
}
237+
case .failure(let error):
238+
completion(.failure(error))
239+
}
240+
}
241+
}
242+
}
243+
244+
private extension String {
245+
/// Drops the given suffix from the string, if present.
246+
func spm_dropPrefix(_ prefix: String) -> String {
247+
if hasPrefix(prefix) {
248+
return String(dropFirst(prefix.count))
249+
}
250+
return self
251+
}
252+
}
253+
254+
private extension AbsolutePath {
255+
func withExtension(_ extension: String) -> AbsolutePath {
256+
guard !self.isRoot else { return self }
257+
let `extension` = `extension`.spm_dropPrefix(".")
258+
return AbsolutePath(self, RelativePath("..")).appending(component: "\(basename).\(`extension`)")
259+
}
260+
}
261+
262+
// FIXME: Implement in PackageIdentity
263+
private extension PackageReference {
264+
var scopeAndName: (String, String)? {
265+
let components = identity.description.split(separator: ".", maxSplits: 1, omittingEmptySubsequences: true)
266+
guard let scope = components.first,
267+
let name = components.last,
268+
components.count == 2
269+
else { return nil }
270+
271+
return (String(scope), String(name))
272+
}
273+
}

Sources/SPMTestSupport/misc.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,5 @@ public func loadPackageGraph(
249249
createREPLProduct: createREPLProduct
250250
)
251251
}
252+
253+
public let emptyZipFile = ByteString([0x80, 0x75, 0x05, 0x06] + [UInt8](repeating: 0x00, count: 18))

0 commit comments

Comments
 (0)