Skip to content

Commit 0bff459

Browse files
Make AsyncHTTPClient optional dependency (#101)
* Add a new abstraction `HTTPClientProtocol` to abstract the HTTP client operations. * Add a new `OfflineHTTPClient` type that just throws an error when trying to download a file. * Remove `httpClient` member from `Engine` and pass the HTTP client as a parameter to the queries that need it. * Separate `Query` and `CacheKey` protocols to allow for queries having properties that are not part of the cache key.
1 parent 911dad7 commit 0bff459

16 files changed

+217
-89
lines changed

Package.swift

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ let package = Package(
1515
],
1616
dependencies: [
1717
// Dependencies declare other packages that this package depends on.
18-
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.19.0"),
1918
.package(url: "https://github.com/apple/swift-system", from: "1.2.1"),
2019
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.2"),
2120
.package(url: "https://github.com/apple/swift-async-algorithms.git", exact: "1.0.0-beta.1"),
@@ -43,7 +42,7 @@ let package = Package(
4342
dependencies: [
4443
.target(name: "AsyncProcess"),
4544
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
46-
.product(name: "AsyncHTTPClient", package: "async-http-client"),
45+
.product(name: "NIOHTTP1", package: "swift-nio"),
4746
.product(name: "Logging", package: "swift-log"),
4847
.product(name: "SystemPackage", package: "swift-system"),
4948
"GeneratorEngine",
@@ -66,7 +65,6 @@ let package = Package(
6665
.target(
6766
name: "GeneratorEngine",
6867
dependencies: [
69-
.product(name: "AsyncHTTPClient", package: "async-http-client"),
7068
.product(name: "Crypto", package: "swift-crypto"),
7169
.product(name: "Logging", package: "swift-log"),
7270
.product(name: "SystemPackage", package: "swift-system"),
@@ -118,3 +116,26 @@ let package = Package(
118116
),
119117
]
120118
)
119+
120+
struct Configuration {
121+
let useAsyncHttpClient: Bool
122+
init(SWIFT_SDK_GENERATOR_DISABLE_AHC: Bool) {
123+
self.useAsyncHttpClient = !SWIFT_SDK_GENERATOR_DISABLE_AHC
124+
}
125+
}
126+
127+
let configuration = Configuration(
128+
SWIFT_SDK_GENERATOR_DISABLE_AHC: Context.environment["SWIFT_SDK_GENERATOR_DISABLE_AHC"] != nil
129+
)
130+
131+
if configuration.useAsyncHttpClient {
132+
package.dependencies.append(
133+
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.19.0")
134+
)
135+
let targetsToAppend: Set<String> = ["SwiftSDKGenerator"]
136+
for target in package.targets.filter({ targetsToAppend.contains($0.name) }) {
137+
target.dependencies.append(
138+
.product(name: "AsyncHTTPClient", package: "async-http-client")
139+
)
140+
}
141+
}

Sources/GeneratorEngine/Engine.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
import class AsyncHTTPClient.HTTPClient
1413
@_exported import Crypto
1514
import struct Logging.Logger
1615
@_exported import struct SystemPackage.FilePath
@@ -42,7 +41,6 @@ public actor Engine {
4241
private(set) var cacheMisses = 0
4342

4443
public let fileSystem: any FileSystem
45-
public let httpClient = HTTPClient()
4644
public let logger: Logger
4745
private let resultsCache: SQLiteBackedCache
4846
private var isShutDown = false
@@ -66,7 +64,6 @@ public actor Engine {
6664
public func shutDown() async throws {
6765
precondition(!self.isShutDown, "`QueryEngine/shutDown` should be called only once")
6866
try self.resultsCache.close()
69-
try await self.httpClient.shutdown()
7067

7168
self.isShutDown = true
7269
}
@@ -85,7 +82,7 @@ public actor Engine {
8582
public subscript(_ query: some Query) -> FileCacheRecord {
8683
get async throws {
8784
let hashEncoder = HashEncoder<SHA512>()
88-
try hashEncoder.encode(query)
85+
try hashEncoder.encode(query.cacheKey)
8986
let key = hashEncoder.finalize()
9087

9188
if let fileRecord = try resultsCache.get(key, as: FileCacheRecord.self) {

Sources/GeneratorEngine/Query.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,17 @@
1212

1313
import struct SystemPackage.FilePath
1414

15-
public protocol Query: CacheKey, Sendable {
15+
public protocol Query: Sendable {
16+
associatedtype Key: CacheKey
17+
var cacheKey: Key { get }
1618
func run(engine: Engine) async throws -> FilePath
1719
}
1820

21+
public protocol CachingQuery: Query, CacheKey where Self.Key == Self {}
22+
extension CachingQuery {
23+
public var cacheKey: Key { self }
24+
}
25+
1926
final class HashEncoder<Hash: HashFunction>: Encoder {
2027
enum Error: Swift.Error {
2128
case noCacheKeyConformance(Encodable.Type)

Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator+Download.swift

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import AsyncAlgorithms
14-
import AsyncHTTPClient
1514
import GeneratorEngine
1615
import RegexBuilder
1716

@@ -28,16 +27,19 @@ let byteCountFormatter = ByteCountFormatter()
2827

2928
extension SwiftSDKGenerator {
3029
func downloadArtifacts(
31-
_ client: HTTPClient, _ engine: Engine,
30+
_ client: some HTTPClientProtocol, _ engine: Engine,
3231
downloadableArtifacts: inout DownloadableArtifacts,
3332
itemsToDownload: @Sendable (DownloadableArtifacts) -> [DownloadableArtifacts.Item]
3433
) async throws {
3534
logGenerationStep("Downloading required toolchain packages...")
36-
var headRequest = HTTPClientRequest(url: downloadableArtifacts.hostLLVM.remoteURL.absoluteString)
37-
headRequest.method = .HEAD
38-
headRequest.headers = ["Accept": "*/*", "User-Agent": "Swift SDK Generator"]
39-
let isLLVMBinaryArtifactAvailable = try await client.execute(headRequest, deadline: .distantFuture)
40-
.status == .ok
35+
let hostLLVMURL = downloadableArtifacts.hostLLVM.remoteURL
36+
// Workaround an issue with github.com returning 400 instead of 404 status to HEAD requests from AHC.
37+
let isLLVMBinaryArtifactAvailable = try await type(of: client).with(http1Only: true) {
38+
try await $0.head(
39+
url: hostLLVMURL.absoluteString,
40+
headers: ["Accept": "*/*", "User-Agent": "Swift SDK Generator"]
41+
)
42+
}
4143

4244
if !isLLVMBinaryArtifactAvailable {
4345
downloadableArtifacts.useLLVMSources()
@@ -46,7 +48,7 @@ extension SwiftSDKGenerator {
4648
let results = try await withThrowingTaskGroup(of: FileCacheRecord.self) { group in
4749
for item in itemsToDownload(downloadableArtifacts) {
4850
group.addTask {
49-
try await engine[DownloadArtifactQuery(artifact: item)]
51+
try await engine[DownloadArtifactQuery(artifact: item, httpClient: client)]
5052
}
5153
}
5254

@@ -64,7 +66,7 @@ extension SwiftSDKGenerator {
6466
}
6567

6668
func downloadUbuntuPackages(
67-
_ client: HTTPClient,
69+
_ client: some HTTPClientProtocol,
6870
_ engine: Engine,
6971
requiredPackages: [String],
7072
versionsConfiguration: VersionsConfiguration,
@@ -112,7 +114,7 @@ extension SwiftSDKGenerator {
112114
let pathsConfiguration = self.pathsConfiguration
113115

114116
try await inTemporaryDirectory { fs, tmpDir in
115-
let downloadedFiles = try await self.downloadFiles(from: urls, to: tmpDir, engine)
117+
let downloadedFiles = try await self.downloadFiles(from: urls, to: tmpDir, client, engine)
116118
report(downloadedFiles: downloadedFiles)
117119

118120
for fileName in urls.map(\.lastPathComponent) {
@@ -123,11 +125,13 @@ extension SwiftSDKGenerator {
123125
try createDirectoryIfNeeded(at: pathsConfiguration.toolchainBinDirPath)
124126
}
125127

126-
func downloadFiles(from urls: [URL], to directory: FilePath, _ engine: Engine) async throws -> [(URL, UInt64)] {
128+
func downloadFiles(from urls: [URL], to directory: FilePath, _ client: some HTTPClientProtocol, _ engine: Engine) async throws -> [(URL, UInt64)] {
127129
try await withThrowingTaskGroup(of: (URL, UInt64).self) {
128130
for url in urls {
129131
$0.addTask {
130-
let downloadedFilePath = try await engine[DownloadFileQuery(remoteURL: url, localDirectory: directory)]
132+
let downloadedFilePath = try await engine[DownloadFileQuery(
133+
remoteURL: url, localDirectory: directory, httpClient: client
134+
)]
131135
let filePath = downloadedFilePath.path
132136
guard let fileSize = try FileManager.default.attributesOfItem(
133137
atPath: filePath.string
@@ -153,12 +157,12 @@ private func report(downloadedFiles: [(URL, UInt64)]) {
153157
}
154158
}
155159

156-
extension HTTPClient {
160+
extension HTTPClientProtocol {
157161
private func downloadUbuntuPackagesList(
158162
from url: String,
159163
isVerbose: Bool
160164
) async throws -> String? {
161-
guard let packages = try await get(url: url).get().body?.unzip(isVerbose: isVerbose) else {
165+
guard let packages = try await get(url: url).body?.unzip(isVerbose: isVerbose) else {
162166
throw FileOperationError.downloadFailed(url)
163167
}
164168

Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator+Entrypoint.swift

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import AsyncAlgorithms
14+
#if canImport(AsyncHTTPClient)
1415
import AsyncHTTPClient
16+
#endif
1517
import Foundation
1618
import GeneratorEngine
1719
import RegexBuilder
@@ -30,25 +32,16 @@ public extension Triple.Arch {
3032
}
3133
}
3234

33-
private func withHTTPClient(
34-
_ configuration: HTTPClient.Configuration,
35-
_ body: @Sendable (HTTPClient) async throws -> ()
36-
) async throws {
37-
let client = HTTPClient(eventLoopGroupProvider: .singleton, configuration: configuration)
38-
try await withAsyncThrowing {
39-
try await body(client)
40-
} defer: {
41-
try await client.shutdown()
42-
}
43-
}
44-
4535
extension SwiftSDKGenerator {
4636
public func run(recipe: SwiftSDKRecipe) async throws {
4737
try await withEngine(LocalFileSystem(), self.logger, cacheLocation: self.engineCachePath) { engine in
48-
var configuration = HTTPClient.Configuration(redirectConfiguration: .follow(max: 5, allowCycles: false))
49-
// Workaround an issue with github.com returning 400 instead of 404 status to HEAD requests from AHC.
50-
configuration.httpVersion = .http1Only
51-
try await withHTTPClient(configuration) { client in
38+
let httpClientType: HTTPClientProtocol.Type
39+
#if canImport(AsyncHTTPClient)
40+
httpClientType = HTTPClient.self
41+
#else
42+
httpClientType = OfflineHTTPClient.self
43+
#endif
44+
try await httpClientType.with { client in
5245
if !self.isIncremental {
5346
try await self.removeRecursively(at: pathsConfiguration.toolchainDirPath)
5447
}

Sources/SwiftSDKGenerator/Queries/CMakeBuildQuery.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import GeneratorEngine
1414
import struct SystemPackage.FilePath
1515

16-
struct CMakeBuildQuery: Query {
16+
struct CMakeBuildQuery: CachingQuery {
1717
let sourcesDirectory: FilePath
1818
/// Path to the output binary relative to the CMake build directory.
1919
let outputBinarySubpath: [FilePath.Component]

Sources/SwiftSDKGenerator/Queries/DownloadArtifactQuery.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,24 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
import class AsyncHTTPClient.FileDownloadDelegate
1413
import GeneratorEngine
1514
import struct SystemPackage.FilePath
1615

1716
struct DownloadArtifactQuery: Query {
17+
var cacheKey: some CacheKey { artifact }
1818
let artifact: DownloadableArtifacts.Item
19+
let httpClient: any HTTPClientProtocol
1920

2021
func run(engine: Engine) async throws -> FilePath {
2122
print("Downloading remote artifact not available in local cache: \(self.artifact.remoteURL)")
22-
let stream = await engine.httpClient.streamDownloadProgress(for: self.artifact)
23+
let stream = httpClient.streamDownloadProgress(
24+
from: self.artifact.remoteURL, to: self.artifact.localPath
25+
)
2326
.removeDuplicates(by: didProgressChangeSignificantly)
2427
._throttle(for: .seconds(1))
2528

26-
for try await item in stream {
27-
report(progress: item.progress, for: item.artifact)
29+
for try await progress in stream {
30+
report(progress: progress, for: artifact)
2831
}
2932
return self.artifact.localPath
3033
}
@@ -39,17 +42,17 @@ struct DownloadArtifactQuery: Query {
3942
/// larger than 1MiB. Returns `false` otherwise.
4043
@Sendable
4144
private func didProgressChangeSignificantly(
42-
previous: ArtifactDownloadProgress,
43-
current: ArtifactDownloadProgress
45+
previous: DownloadProgress,
46+
current: DownloadProgress
4447
) -> Bool {
45-
guard previous.progress.totalBytes == current.progress.totalBytes else {
48+
guard previous.totalBytes == current.totalBytes else {
4649
return true
4750
}
4851

49-
return current.progress.receivedBytes - previous.progress.receivedBytes > 1024 * 1024 * 1024
52+
return current.receivedBytes - previous.receivedBytes > 1024 * 1024 * 1024
5053
}
5154

52-
private func report(progress: FileDownloadDelegate.Progress, for artifact: DownloadableArtifacts.Item) {
55+
private func report(progress: DownloadProgress, for artifact: DownloadableArtifacts.Item) {
5356
if let total = progress.totalBytes {
5457
print("""
5558
\(artifact.remoteURL.lastPathComponent) \(

Sources/SwiftSDKGenerator/Queries/DownloadFileQuery.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,20 @@ import GeneratorEngine
1515
import struct SystemPackage.FilePath
1616

1717
struct DownloadFileQuery: Query {
18+
struct Key: CacheKey {
19+
let remoteURL: URL
20+
let localDirectory: FilePath
21+
}
22+
var cacheKey: Key {
23+
Key(remoteURL: remoteURL, localDirectory: localDirectory)
24+
}
1825
let remoteURL: URL
1926
let localDirectory: FilePath
27+
let httpClient: any HTTPClientProtocol
2028

2129
func run(engine: Engine) async throws -> FilePath {
2230
let downloadedFilePath = self.localDirectory.appending(self.remoteURL.lastPathComponent)
23-
_ = try await engine.httpClient.downloadFile(from: self.remoteURL, to: downloadedFilePath)
31+
_ = try await httpClient.downloadFile(from: self.remoteURL, to: downloadedFilePath)
2432
return downloadedFilePath
2533
}
2634
}

Sources/SwiftSDKGenerator/SwiftSDKRecipes/LinuxRecipe.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
import AsyncHTTPClient
1413
import Foundation
1514
import GeneratorEngine
1615
import struct SystemPackage.FilePath
@@ -128,7 +127,7 @@ public struct LinuxRecipe: SwiftSDKRecipe {
128127
public func makeSwiftSDK(
129128
generator: SwiftSDKGenerator,
130129
engine: Engine,
131-
httpClient client: HTTPClient
130+
httpClient client: some HTTPClientProtocol
132131
) async throws -> SwiftSDKProduct {
133132
let sdkDirPath = self.sdkDirPath(paths: generator.pathsConfiguration)
134133
if !generator.isIncremental {

Sources/SwiftSDKGenerator/SwiftSDKRecipes/SwiftSDKRecipe.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
import AsyncHTTPClient
1413
import GeneratorEngine
1514
import struct SystemPackage.FilePath
1615

@@ -34,7 +33,7 @@ public protocol SwiftSDKRecipe: Sendable {
3433
var defaultArtifactID: String { get }
3534

3635
/// The main entrypoint of the recipe to make a Swift SDK
37-
func makeSwiftSDK(generator: SwiftSDKGenerator, engine: Engine, httpClient: HTTPClient) async throws -> SwiftSDKProduct
36+
func makeSwiftSDK(generator: SwiftSDKGenerator, engine: Engine, httpClient: some HTTPClientProtocol) async throws -> SwiftSDKProduct
3837
}
3938

4039
extension SwiftSDKRecipe {

0 commit comments

Comments
 (0)