Skip to content

Commit 3dc16db

Browse files
authored
Add basic QueryEngine scaffolding w/ Encodable hashing (#7347)
### Motivation: Caching of computations in SwiftPM is ad-hoc and mostly relies on in-memory `ThreadSafeKeyValueStore`, which utilizes locking and makes it harder to add `Sendable` conformances on types that have these stores as properties. To allow consistent and persistent caching when SwiftPM processes are relaunched, we can use a SQLite-backed async-first caching engine. ### Modifications: This change ports [most of the current `GeneratorEngine` implementation](https://github.com/apple/swift-sdk-generator/blob/main/Sources/GeneratorEngine/Engine.swift) from the Swift SDK Generator repository to the SwiftPM code base as `QueryEngine`. Since SwiftNIO is not supported on Windows, references to `AsyncHTTPClient` have been removed. Additionally, we can't use macros in the SwiftPM code base either, thus the `Query` protocol has to conform to `Encodable` instead of using macro-generated conformances. We don't have a consistent hashing implementation for `Encodable` yet, and a temporary stub for it is marked with `fatalError` for now. ### Result: NFC, new `QueryEngine` module is not used anywhere yet.
1 parent 382fc2e commit 3dc16db

11 files changed

+596
-36
lines changed

Package.swift

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,34 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import PackageDescription
1615
import class Foundation.ProcessInfo
16+
import PackageDescription
1717

1818
// When building the toolchain on the CI for ELF platforms, remove the CI's
1919
// stdlib absolute runpath and add ELF's $ORIGIN relative paths before installing.
20-
let swiftpmLinkSettings : [LinkerSetting]
21-
let packageLibraryLinkSettings : [LinkerSetting]
20+
let swiftpmLinkSettings: [LinkerSetting]
21+
let packageLibraryLinkSettings: [LinkerSetting]
2222
if let resourceDirPath = ProcessInfo.processInfo.environment["SWIFTCI_INSTALL_RPATH_OS"] {
23-
swiftpmLinkSettings = [ .unsafeFlags(["-no-toolchain-stdlib-rpath", "-Xlinker", "-rpath", "-Xlinker", "$ORIGIN/../lib/swift/\(resourceDirPath)"]) ]
24-
packageLibraryLinkSettings = [ .unsafeFlags(["-no-toolchain-stdlib-rpath", "-Xlinker", "-rpath", "-Xlinker", "$ORIGIN/../../\(resourceDirPath)"]) ]
23+
swiftpmLinkSettings = [.unsafeFlags([
24+
"-no-toolchain-stdlib-rpath",
25+
"-Xlinker", "-rpath",
26+
"-Xlinker", "$ORIGIN/../lib/swift/\(resourceDirPath)",
27+
])]
28+
packageLibraryLinkSettings = [.unsafeFlags([
29+
"-no-toolchain-stdlib-rpath",
30+
"-Xlinker", "-rpath",
31+
"-Xlinker", "$ORIGIN/../../\(resourceDirPath)",
32+
])]
2533
} else {
26-
swiftpmLinkSettings = []
27-
packageLibraryLinkSettings = []
34+
swiftpmLinkSettings = []
35+
packageLibraryLinkSettings = []
2836
}
2937

3038
/** SwiftPMDataModel is the subset of SwiftPM product that includes just its data model.
31-
This allows some clients (such as IDEs) that use SwiftPM's data model but not its build system
32-
to not have to depend on SwiftDriver, SwiftLLBuild, etc. We should probably have better names here,
33-
though that could break some clients.
34-
*/
39+
This allows some clients (such as IDEs) that use SwiftPM's data model but not its build system
40+
to not have to depend on SwiftDriver, SwiftLLBuild, etc. We should probably have better names here,
41+
though that could break some clients.
42+
*/
3543
let swiftPMDataModelProduct = (
3644
name: "SwiftPMDataModel",
3745
targets: [
@@ -51,7 +59,7 @@ let swiftPMDataModelProduct = (
5159
command line tools, while `libSwiftPMDataModel` includes only the data model.
5260

5361
NOTE: This API is *unstable* and may change at any time.
54-
*/
62+
*/
5563
let swiftPMProduct = (
5664
name: "SwiftPM",
5765
targets: swiftPMDataModelProduct.targets + [
@@ -69,8 +77,8 @@ let systemSQLitePkgConfig: String? = "sqlite3"
6977
#endif
7078

7179
/** An array of products which have two versions listed: one dynamically linked, the other with the
72-
automatic linking type with `-auto` suffix appended to product's name.
73-
*/
80+
automatic linking type with `-auto` suffix appended to product's name.
81+
*/
7482
let autoProducts = [swiftPMProduct, swiftPMDataModelProduct]
7583

7684

@@ -90,11 +98,11 @@ let package = Package(
9098
name: "SwiftPM",
9199
platforms: [
92100
.macOS(.v13),
93-
.iOS(.v16)
101+
.iOS(.v16),
94102
],
95103
products:
96-
autoProducts.flatMap {
97-
[
104+
autoProducts.flatMap {
105+
[
98106
.library(
99107
name: $0.name,
100108
type: .dynamic,
@@ -103,9 +111,9 @@ let package = Package(
103111
.library(
104112
name: "\($0.name)-auto",
105113
targets: $0.targets
106-
)
107-
]
108-
} + [
114+
),
115+
]
116+
} + [
109117
.library(
110118
name: "XCBuildSupport",
111119
targets: ["XCBuildSupport"]
@@ -166,7 +174,7 @@ let package = Package(
166174
name: "SourceKitLSPAPI",
167175
dependencies: [
168176
"Build",
169-
"SPMBuildCore"
177+
"SPMBuildCore",
170178
],
171179
exclude: ["CMakeLists.txt"],
172180
swiftSettings: [.enableExperimentalFeature("AccessLevelOnImport")]
@@ -213,7 +221,7 @@ let package = Package(
213221
name: "SourceControl",
214222
dependencies: [
215223
"Basics",
216-
"PackageModel"
224+
"PackageModel",
217225
],
218226
exclude: ["CMakeLists.txt"]
219227
),
@@ -255,7 +263,7 @@ let package = Package(
255263
dependencies: [
256264
"Basics",
257265
"PackageLoading",
258-
"PackageModel"
266+
"PackageModel",
259267
],
260268
exclude: ["CMakeLists.txt", "README.md"]
261269
),
@@ -267,7 +275,7 @@ let package = Package(
267275
name: "PackageCollectionsModel",
268276
dependencies: [],
269277
exclude: [
270-
"Formats/v1.md"
278+
"Formats/v1.md",
271279
]
272280
),
273281

@@ -301,7 +309,7 @@ let package = Package(
301309
],
302310
exclude: ["CMakeLists.txt"]
303311
),
304-
312+
305313
.target(
306314
name: "PackageSigning",
307315
dependencies: [
@@ -320,7 +328,7 @@ let package = Package(
320328
name: "SPMBuildCore",
321329
dependencies: [
322330
"Basics",
323-
"PackageGraph"
331+
"PackageGraph",
324332
],
325333
exclude: ["CMakeLists.txt"]
326334
),
@@ -460,6 +468,14 @@ let package = Package(
460468
]
461469
),
462470

471+
.target(
472+
name: "QueryEngine",
473+
dependencies: [
474+
"Basics",
475+
.product(name: "Crypto", package: "swift-crypto"),
476+
]
477+
),
478+
463479
.executableTarget(
464480
/** The main executable provided by SwiftPM */
465481
name: "swift-package",
@@ -562,7 +578,8 @@ let package = Package(
562578
.target(
563579
/** Test for thread-santizer. */
564580
name: "tsan_utils",
565-
dependencies: []),
581+
dependencies: []
582+
),
566583

567584
// MARK: SwiftPM tests
568585

@@ -664,7 +681,7 @@ let package = Package(
664681
name: "XCBuildSupportTests",
665682
dependencies: ["XCBuildSupport", "SPMTestSupport"],
666683
exclude: ["Inputs/Foo.pc"]
667-
)
684+
),
668685
],
669686
swiftLanguageVersions: [.v5]
670687
)
@@ -682,15 +699,16 @@ package.targets.append(contentsOf: [
682699
),
683700
])
684701

685-
// rdar://101868275 "error: cannot find 'XCTAssertEqual' in scope" can affect almost any functional test, so we flat out disable them all until we know what is going on
702+
// rdar://101868275 "error: cannot find 'XCTAssertEqual' in scope" can affect almost any functional test, so we flat out
703+
// disable them all until we know what is going on
686704
if ProcessInfo.processInfo.environment["SWIFTCI_DISABLE_SDK_DEPENDENT_TESTS"] == nil {
687705
package.targets.append(contentsOf: [
688706
.testTarget(
689707
name: "FunctionalTests",
690708
dependencies: [
691709
"swift-package-manager",
692710
"PackageModel",
693-
"SPMTestSupport"
711+
"SPMTestSupport",
694712
]
695713
),
696714

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2023-2024 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+
/// Runs a cleanup closure (`deferred`) after a given `work` closure,
14+
/// making sure `deferred` is run also when `work` throws an error.
15+
/// - Parameters:
16+
/// - work: The work that should be performed. Will always be executed.
17+
/// - deferred: The cleanup that needs to be done in any case.
18+
/// - Throws: Any error thrown by `deferred` or `work` (in that order).
19+
/// - Returns: The result of `work`.
20+
/// - Note: If `work` **and** `deferred` throw an error,
21+
/// the one thrown by `deferred` is thrown from this function.
22+
/// - SeeAlso: ``withAsyncThrowing(do:defer:)``
23+
public func withThrowing<T>(
24+
do work: () throws -> T,
25+
defer deferred: () throws -> Void
26+
) throws -> T {
27+
do {
28+
let result = try work()
29+
try deferred()
30+
return result
31+
} catch {
32+
try deferred()
33+
throw error
34+
}
35+
}
36+
37+
/// Runs an async cleanup closure (`deferred`) after a given async `work` closure,
38+
/// making sure `deferred` is run also when `work` throws an error.
39+
/// - Parameters:
40+
/// - work: The work that should be performed. Will always be executed.
41+
/// - deferred: The cleanup that needs to be done in any case.
42+
/// - Throws: Any error thrown by `deferred` or `work` (in that order).
43+
/// - Returns: The result of `work`.
44+
/// - Note: If `work` **and** `deferred` throw an error,
45+
/// the one thrown by `deferred` is thrown from this function.
46+
/// - SeeAlso: ``withThrowing(do:defer:)``
47+
public func withAsyncThrowing<T: Sendable>(
48+
do work: @Sendable () async throws -> T,
49+
defer deferred: @Sendable () async throws -> Void
50+
) async throws -> T {
51+
do {
52+
let result = try await work()
53+
try await deferred()
54+
return result
55+
} catch {
56+
try await deferred()
57+
throw error
58+
}
59+
}

Sources/Basics/SQLiteBackedCache.swift

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ public final class SQLiteBackedCache<Value: Codable>: Closable {
8484
}
8585
}
8686

87-
public func put(
88-
key: Key,
87+
private func put(
88+
rawKey key: SQLite.SQLiteValue,
8989
value: Value,
9090
replace: Bool = false,
9191
observabilityScope: ObservabilityScope? = nil
@@ -95,7 +95,7 @@ public final class SQLiteBackedCache<Value: Codable>: Closable {
9595
try self.executeStatement(query) { statement in
9696
let data = try self.jsonEncoder.encode(value)
9797
let bindings: [SQLite.SQLiteValue] = [
98-
.string(key),
98+
key,
9999
.blob(data),
100100
]
101101
try statement.bind(bindings)
@@ -107,17 +107,39 @@ public final class SQLiteBackedCache<Value: Codable>: Closable {
107107
}
108108
observabilityScope?
109109
.emit(
110-
warning: "truncating \(self.tableName) cache database since it reached max size of \(self.configuration.maxSizeInBytes ?? 0) bytes"
110+
warning: """
111+
truncating \(self.tableName) cache database since it reached max size of \(
112+
self.configuration.maxSizeInBytes ?? 0
113+
) bytes
114+
"""
111115
)
112116
try self.executeStatement("DELETE FROM \(self.tableName);") { statement in
113117
try statement.step()
114118
}
115-
try self.put(key: key, value: value, replace: replace, observabilityScope: observabilityScope)
119+
try self.put(rawKey: key, value: value, replace: replace, observabilityScope: observabilityScope)
116120
} catch {
117121
throw error
118122
}
119123
}
120124

125+
public func put(
126+
blobKey key: some Sequence<UInt8>,
127+
value: Value,
128+
replace: Bool = false,
129+
observabilityScope: ObservabilityScope? = nil
130+
) throws {
131+
try self.put(rawKey: .blob(Data(key)), value: value, observabilityScope: observabilityScope)
132+
}
133+
134+
public func put(
135+
key: Key,
136+
value: Value,
137+
replace: Bool = false,
138+
observabilityScope: ObservabilityScope? = nil
139+
) throws {
140+
try self.put(rawKey: .string(key), value: value, replace: replace, observabilityScope: observabilityScope)
141+
}
142+
121143
public func get(key: Key) throws -> Value? {
122144
let query = "SELECT value FROM \(self.tableName) WHERE key = ? LIMIT 1;"
123145
return try self.executeStatement(query) { statement -> Value? in
@@ -129,6 +151,17 @@ public final class SQLiteBackedCache<Value: Codable>: Closable {
129151
}
130152
}
131153

154+
public func get(blobKey key: some Sequence<UInt8>) throws -> Value? {
155+
let query = "SELECT value FROM \(self.tableName) WHERE key = ? LIMIT 1;"
156+
return try self.executeStatement(query) { statement -> Value? in
157+
try statement.bind([.blob(Data(key))])
158+
let data = try statement.step()?.blob(at: 0)
159+
return try data.flatMap {
160+
try self.jsonDecoder.decode(Value.self, from: $0)
161+
}
162+
}
163+
}
164+
132165
public func remove(key: Key) throws {
133166
let query = "DELETE FROM \(self.tableName) WHERE key = ?;"
134167
try self.executeStatement(query) { statement in
@@ -143,7 +176,7 @@ public final class SQLiteBackedCache<Value: Codable>: Closable {
143176
let result: Result<T, Error>
144177
let statement = try db.prepare(query: query)
145178
do {
146-
result = .success(try body(statement))
179+
result = try .success(body(statement))
147180
} catch {
148181
result = .failure(error)
149182
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2023-2024 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 protocol _Concurrency.Actor
14+
import protocol Crypto.HashFunction
15+
import struct SystemPackage.Errno
16+
import struct SystemPackage.FilePath
17+
18+
public protocol AsyncFileSystem: Actor {
19+
func withOpenReadableFile<T>(_ path: FilePath, _ body: (OpenReadableFile) async throws -> T) async throws -> T
20+
func withOpenWritableFile<T>(_ path: FilePath, _ body: (OpenWritableFile) async throws -> T) async throws -> T
21+
}
22+
23+
enum FileSystemError: Error {
24+
case fileDoesNotExist(FilePath)
25+
case bufferLimitExceeded(FilePath)
26+
case systemError(FilePath, Errno)
27+
}
28+
29+
extension Error {
30+
func attach(path: FilePath) -> any Error {
31+
if let error = self as? Errno {
32+
FileSystemError.systemError(path, error)
33+
} else {
34+
self
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)