Skip to content

Commit f8a807e

Browse files
authored
Cache lld binaries to avoid redundant rebuilds (#35)
This proceeds with converting more of the generation steps to a query-based architecture. Now `lld` doesn't need to be rebuilt if its source directory and build options don't change. This is implemented with a new `CMakeBuildQuery`, which takes source directory, and build options as arguments, and returns a freshly-built binary that can be cached by the generator engine. Also renamed `DownloadQuery` to `DownloadArtifactQuery` and moved it to a separate file.
1 parent 1ef53e8 commit f8a807e

10 files changed

+169
-113
lines changed

Sources/GeneratorEngine/Cache/CacheKeyProtocol.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ public protocol CacheKeyProtocol {
2424

2525
extension Bool: CacheKeyProtocol {
2626
public func hash(with hashFunction: inout some HashFunction) {
27-
"Swift.Bool".hash(with: &hashFunction)
27+
String(reflecting: Self.self).hash(with: &hashFunction)
2828
hashFunction.update(data: self ? [1] : [0])
2929
}
3030
}
3131

3232
extension Int: CacheKeyProtocol {
3333
public func hash(with hashFunction: inout some HashFunction) {
34-
"Swift.Int".hash(with: &hashFunction)
34+
String(reflecting: Self.self).hash(with: &hashFunction)
3535
withUnsafeBytes(of: self) {
3636
hashFunction.update(bufferPointer: $0)
3737
}
@@ -40,7 +40,7 @@ extension Int: CacheKeyProtocol {
4040

4141
extension String: CacheKeyProtocol {
4242
public func hash(with hashFunction: inout some HashFunction) {
43-
var t = "Swift.String"
43+
var t = String(reflecting: Self.self)
4444
t.withUTF8 {
4545
hashFunction.update(bufferPointer: .init($0))
4646
}
@@ -53,21 +53,28 @@ extension String: CacheKeyProtocol {
5353

5454
extension FilePath: CacheKeyProtocol {
5555
public func hash(with hashFunction: inout some HashFunction) {
56-
"SystemPackage.FilePath".hash(with: &hashFunction)
56+
String(reflecting: Self.self).hash(with: &hashFunction)
57+
self.string.hash(with: &hashFunction)
58+
}
59+
}
60+
61+
extension FilePath.Component: CacheKeyProtocol {
62+
public func hash(with hashFunction: inout some HashFunction) {
63+
String(reflecting: Self.self).hash(with: &hashFunction)
5764
self.string.hash(with: &hashFunction)
5865
}
5966
}
6067

6168
extension URL: CacheKeyProtocol {
6269
public func hash(with hashFunction: inout some HashFunction) {
63-
"Foundation.URL".hash(with: &hashFunction)
70+
String(reflecting: Self.self).hash(with: &hashFunction)
6471
self.description.hash(with: &hashFunction)
6572
}
6673
}
6774

6875
extension Optional: CacheKeyProtocol where Wrapped: CacheKeyProtocol {
6976
public func hash(with hashFunction: inout some HashFunction) {
70-
"Swift.Optional".hash(with: &hashFunction)
77+
String(reflecting: Self.self).hash(with: &hashFunction)
7178
if let self {
7279
true.hash(with: &hashFunction)
7380
self.hash(with: &hashFunction)
@@ -77,5 +84,14 @@ extension Optional: CacheKeyProtocol where Wrapped: CacheKeyProtocol {
7784
}
7885
}
7986

87+
extension Array: CacheKeyProtocol where Element: CacheKeyProtocol {
88+
public func hash(with hashFunction: inout some HashFunction) {
89+
String(reflecting: Self.self).hash(with: &hashFunction)
90+
for element in self {
91+
element.hash(with: &hashFunction)
92+
}
93+
}
94+
}
95+
8096
@attached(extension, conformances: CacheKeyProtocol, names: named(hash(with:)))
8197
public macro CacheKey() = #externalMacro(module: "Macros", type: "CacheKeyMacro")

Sources/GeneratorEngine/Cache/FileCacheRecord.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
import struct SystemPackage.FilePath
1414

1515
public struct FileCacheRecord: Sendable {
16-
let path: FilePath
17-
let hash: String
16+
public let path: FilePath
17+
public let hash: String
1818
}
1919

2020
extension FileCacheRecord: Codable {

Sources/GeneratorEngine/Cache/SQLite.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import SystemPackage
1515
import SystemSQLite
1616

1717
extension FilePath: @unchecked Sendable {}
18+
extension FilePath.Component: @unchecked Sendable {}
1819

1920
/// A minimal SQLite wrapper.
2021
public final class SQLite {

Sources/SwiftSDKGenerator/Artifacts/DownloadableArtifacts.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ struct DownloadableArtifacts: Sendable {
107107
isPrebuilt: true
108108
)
109109

110-
let targetArtifactsOS = ArtifactOS(targetTriple.os, versions)
111110
self.targetSwift = .init(
112111
remoteURL: versions.swiftDownloadURL(),
113112
localPath: paths.artifactsCachePath

Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator+Build.swift

Lines changed: 0 additions & 24 deletions
This file was deleted.

Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator+Download.swift

Lines changed: 7 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,7 @@ import struct SystemPackage.FilePath
2323
private let ubuntuAMD64Mirror = "http://gb.archive.ubuntu.com/ubuntu"
2424
private let ubuntuARM64Mirror = "http://ports.ubuntu.com/ubuntu-ports"
2525

26-
private let byteCountFormatter = ByteCountFormatter()
27-
28-
@Query
29-
struct DownloadQuery {
30-
let artifact: DownloadableArtifacts.Item
31-
32-
func run(engine: Engine) async throws -> FilePath {
33-
print("Downloading remote artifact not available in local cache: \(self.artifact.remoteURL)")
34-
let stream = await engine.httpClient.streamDownloadProgress(for: self.artifact)
35-
.removeDuplicates(by: didProgressChangeSignificantly)
36-
.throttle(for: .seconds(1))
37-
38-
for try await item in stream {
39-
report(progress: item.progress, for: item.artifact)
40-
}
41-
return self.artifact.localPath
42-
}
43-
}
26+
let byteCountFormatter = ByteCountFormatter()
4427

4528
extension SwiftSDKGenerator {
4629
func downloadArtifacts(_ client: HTTPClient) async throws {
@@ -57,9 +40,8 @@ extension SwiftSDKGenerator {
5740

5841
let results = try await withThrowingTaskGroup(of: FileCacheRecord.self) { group in
5942
for item in self.downloadableArtifacts.allItems {
60-
print(item.remoteURL)
6143
group.addTask {
62-
try await self.engine[DownloadQuery(artifact: item)]
44+
try await self.engine[DownloadArtifactQuery(artifact: item)]
6345
}
6446
}
6547

@@ -69,6 +51,11 @@ extension SwiftSDKGenerator {
6951
}
7052
return result
7153
}
54+
55+
print("Using downloaded artifacts in these locations:")
56+
for path in results.map(\.path) {
57+
print(path)
58+
}
7259
}
7360

7461
func downloadUbuntuPackages(_ client: HTTPClient, requiredPackages: [String]) async throws {
@@ -211,40 +198,3 @@ extension HTTPClient {
211198
return result
212199
}
213200
}
214-
215-
/// Checks whether two given progress value are different enough from each other. Used for filtering out progress
216-
/// values in async streams with `removeDuplicates` operator.
217-
/// - Parameters:
218-
/// - previous: Preceding progress value in the stream.
219-
/// - current: Currently processed progress value in the stream.
220-
/// - Returns: `true` if `totalBytes` value is different by any amount or if `receivedBytes` is different by amount
221-
/// larger than 1MiB. Returns `false` otherwise.
222-
@Sendable
223-
private func didProgressChangeSignificantly(
224-
previous: ArtifactDownloadProgress,
225-
current: ArtifactDownloadProgress
226-
) -> Bool {
227-
guard previous.progress.totalBytes == current.progress.totalBytes else {
228-
return true
229-
}
230-
231-
return current.progress.receivedBytes - previous.progress.receivedBytes > 1024 * 1024 * 1024
232-
}
233-
234-
private func report(progress: FileDownloadDelegate.Progress, for artifact: DownloadableArtifacts.Item) {
235-
if let total = progress.totalBytes {
236-
print("""
237-
\(artifact.remoteURL.lastPathComponent) \(
238-
byteCountFormatter
239-
.string(fromByteCount: Int64(progress.receivedBytes))
240-
)/\(
241-
byteCountFormatter
242-
.string(fromByteCount: Int64(total))
243-
)
244-
""")
245-
} else {
246-
print(
247-
"\(artifact.remoteURL.lastPathComponent) \(byteCountFormatter.string(fromByteCount: Int64(progress.receivedBytes)))"
248-
)
249-
}
250-
}

Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator+Entrypoint.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public extension SwiftSDKGenerator {
6767
try await self.unpackTargetSwiftPackage()
6868
}
6969

70-
try await self.unpackLLDLinker()
70+
try await self.prepareLLDLinker()
7171

7272
try self.fixAbsoluteSymlinks()
7373

Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator+Unpack.swift

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import struct SystemPackage.FilePath
14+
1315
private let unusedDarwinPlatforms = [
1416
"watchsimulator",
1517
"iphonesimulator",
@@ -67,36 +69,43 @@ extension SwiftSDKGenerator {
6769
}
6870
}
6971

70-
func unpackLLDLinker() async throws {
72+
func prepareLLDLinker() async throws {
7173
logGenerationStep("Unpacking and copying `lld` linker...")
7274
let downloadableArtifacts = self.downloadableArtifacts
7375
let pathsConfiguration = self.pathsConfiguration
7476
let targetOS = self.targetTriple.os
7577

76-
try await inTemporaryDirectory { fileSystem, tmpDir in
77-
let llvmArtifact = downloadableArtifacts.hostLLVM
78-
try await fileSystem.untar(
79-
file: llvmArtifact.localPath,
80-
into: tmpDir,
81-
stripComponents: 1
82-
)
78+
let llvmArtifact = downloadableArtifacts.hostLLVM
8379

84-
let unpackedLLDPath = if llvmArtifact.isPrebuilt {
85-
tmpDir.appending("bin/lld")
86-
} else {
87-
try await self.buildLLD(llvmSourcesDirectory: tmpDir)
88-
}
80+
let untarDestination = pathsConfiguration.artifactsCachePath.appending(
81+
FilePath.Component(llvmArtifact.localPath.stem!)!.stem
82+
)
83+
try self.createDirectoryIfNeeded(at: untarDestination)
84+
try await self.untar(
85+
file: llvmArtifact.localPath,
86+
into: untarDestination,
87+
stripComponents: 1
88+
)
8989

90-
let toolchainLLDPath = switch targetOS {
91-
case .linux:
92-
pathsConfiguration.toolchainBinDirPath.appending("ld.lld")
93-
case .wasi:
94-
pathsConfiguration.toolchainBinDirPath.appending("wasm-ld")
95-
default:
96-
fatalError()
97-
}
90+
let unpackedLLDPath = if llvmArtifact.isPrebuilt {
91+
untarDestination.appending("bin/lld")
92+
} else {
93+
try await self.engine[CMakeBuildQuery(
94+
sourcesDirectory: untarDestination,
95+
outputBinarySubpath: ["bin", "lld"],
96+
options: "-DLLVM_ENABLE_PROJECTS=lld -DLLVM_TARGETS_TO_BUILD=\(self.targetTriple.cpu.llvmTargetConventionName)"
97+
)].path
98+
}
9899

99-
try await fileSystem.copy(from: unpackedLLDPath, to: toolchainLLDPath)
100+
let toolchainLLDPath = switch targetOS {
101+
case .linux:
102+
pathsConfiguration.toolchainBinDirPath.appending("ld.lld")
103+
case .wasi:
104+
pathsConfiguration.toolchainBinDirPath.appending("wasm-ld")
105+
default:
106+
fatalError()
100107
}
108+
109+
try self.copy(from: unpackedLLDPath, to: toolchainLLDPath)
101110
}
102111
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2022-2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import GeneratorEngine
14+
import struct SystemPackage.FilePath
15+
16+
@Query
17+
struct CMakeBuildQuery {
18+
let sourcesDirectory: FilePath
19+
/// Path to the output binary relative to the CMake build directory.
20+
let outputBinarySubpath: [FilePath.Component]
21+
let options: String
22+
23+
func run(engine: Engine) async throws -> FilePath {
24+
try await Shell.run(
25+
"""
26+
cmake -B build -G Ninja -S llvm -DCMAKE_BUILD_TYPE=Release \(self.options)
27+
""",
28+
currentDirectory: self.sourcesDirectory
29+
)
30+
31+
let buildDirectory = self.sourcesDirectory.appending("build")
32+
try await Shell.run("ninja", currentDirectory: buildDirectory)
33+
34+
return self.outputBinarySubpath.reduce(into: buildDirectory) { $0.append($1) }
35+
}
36+
}

0 commit comments

Comments
 (0)