Skip to content

Commit e1612d5

Browse files
authored
Merge pull request #1018 from ahoppen/ahoppen/toolchain-registry
Migrate `ToolchainRegistry` to be an actor
2 parents e8e4fb2 + 6f342e2 commit e1612d5

17 files changed

+279
-324
lines changed

Sources/LSPTestSupport/Assertions.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,19 @@ public func assertEqual<T: Equatable>(
6969
XCTAssertEqual(expression1, expression2, message(), file: file, line: line)
7070
}
7171

72+
/// Same as `XCTAssertTrue` but doesn't take autoclosures and thus `expression` can contain `await`.
73+
public func assertTrue(
74+
_ expression: Bool,
75+
_ message: @autoclosure () -> String = "",
76+
file: StaticString = #filePath,
77+
line: UInt = #line
78+
) {
79+
XCTAssertTrue(expression, message(), file: file, line: line)
80+
}
81+
7282
/// Same as `XCTAssertNil` but doesn't take autoclosures and thus `expression`
7383
/// can contain `await`.
74-
public func assertNil<T: Equatable>(
84+
public func assertNil<T>(
7585
_ expression: T?,
7686
_ message: @autoclosure () -> String = "",
7787
file: StaticString = #filePath,
@@ -82,7 +92,7 @@ public func assertNil<T: Equatable>(
8292

8393
/// Same as `XCTAssertNotNil` but doesn't take autoclosures and thus `expression`
8494
/// can contain `await`.
85-
public func assertNotNil<T: Equatable>(
95+
public func assertNotNil<T>(
8696
_ expression: T?,
8797
_ message: @autoclosure () -> String = "",
8898
file: StaticString = #filePath,

Sources/SKCore/ToolchainRegistry.swift

Lines changed: 80 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,23 @@ import var TSCBasic.localFileSystem
2626
/// Most users will use the `shared` ToolchainRegistry, although it's possible to create more. A
2727
/// ToolchainRegistry is usually initialized by performing a search of predetermined paths,
2828
/// e.g. `ToolchainRegistry(searchPaths: ToolchainRegistry.defaultSearchPaths)`.
29-
public final class ToolchainRegistry {
29+
public final actor ToolchainRegistry {
3030

31-
/// The toolchains, in the order they were registered. **Must be accessed on `queue`**.
32-
var _toolchains: [Toolchain] = []
31+
/// The toolchains, in the order they were registered.
32+
public private(set) var toolchains: [Toolchain] = []
3333

34-
/// The toolchains indexed by their identifier. **Must be accessed on `queue`**.
34+
/// The toolchains indexed by their identifier.
35+
///
3536
/// Multiple toolchains may exist for the XcodeDefault toolchain identifier.
36-
var toolchainsByIdentifier: [String: [Toolchain]] = [:]
37+
private var toolchainsByIdentifier: [String: [Toolchain]] = [:]
3738

38-
/// The toolchains indexed by their path. **Must be accessed on `queue`**.
39+
/// The toolchains indexed by their path.
40+
///
3941
/// Note: Not all toolchains have a path.
40-
var toolchainsByPath: [AbsolutePath: Toolchain] = [:]
41-
42-
/// The default toolchain. **Must be accessed on `queue`**.
43-
var _default: Toolchain? = nil
42+
private var toolchainsByPath: [AbsolutePath: Toolchain] = [:]
4443

45-
/// Mutex for registering and accessing toolchains.
46-
var queue: DispatchQueue = DispatchQueue(label: "toolchain-registry-queue")
44+
/// The default toolchain.
45+
private var _default: Toolchain? = nil
4746

4847
/// The currently selected toolchain identifier on Darwin.
4948
public lazy var darwinToolchainOverride: String? = {
@@ -54,19 +53,21 @@ public final class ToolchainRegistry {
5453
}()
5554

5655
/// Creates an empty toolchain registry.
57-
public init() {}
58-
}
56+
private init() {}
57+
58+
/// A toolchain registry used for testing that scans for toolchains based on environment variables and Xcode
59+
/// installations but not next to the `sourcekit-lsp` binary because there is no `sourcekit-lsp` binary during
60+
/// testing.
61+
@_spi(Testing)
62+
public static var forTesting: ToolchainRegistry {
63+
get async {
64+
await ToolchainRegistry(localFileSystem)
65+
}
66+
}
5967

60-
extension ToolchainRegistry {
61-
62-
/// The global toolchain registry, initially populated by scanning for toolchains.
63-
///
64-
/// Scans for toolchains in:
65-
/// * env SOURCEKIT_TOOLCHAIN_PATH <-- will override default toolchain
66-
/// * (Darwin) The currently selected Xcode
67-
/// * (Darwin) [~]/Library/Developer/Toolchains
68-
/// * env SOURCEKIT_PATH, PATH
69-
public static var shared: ToolchainRegistry = ToolchainRegistry(localFileSystem)
68+
/// A toolchain registry that doesn't contain any toolchains.
69+
@_spi(Testing)
70+
public static var empty: ToolchainRegistry { ToolchainRegistry() }
7071

7172
/// Creates a toolchain registry populated by scanning for toolchains according to the given paths
7273
/// and variables.
@@ -83,11 +84,10 @@ extension ToolchainRegistry {
8384
/// let tr = ToolchainRegistry()
8485
/// tr.scanForToolchains()
8586
/// ```
86-
public convenience init(
87+
public init(
8788
installPath: AbsolutePath? = nil,
8889
_ fileSystem: FileSystem
89-
) {
90-
self.init()
90+
) async {
9191
scanForToolchains(installPath: installPath, fileSystem)
9292
}
9393
}
@@ -103,65 +103,63 @@ extension ToolchainRegistry {
103103
/// The default toolchain must be only of the registered toolchains.
104104
public var `default`: Toolchain? {
105105
get {
106-
return queue.sync {
107-
if _default == nil {
108-
if let tc = toolchainsByIdentifier[darwinToolchainIdentifier]?.first {
109-
_default = tc
110-
} else {
111-
_default = _toolchains.first
112-
}
106+
if _default == nil {
107+
if let tc = toolchainsByIdentifier[darwinToolchainIdentifier]?.first {
108+
_default = tc
109+
} else {
110+
_default = toolchains.first
113111
}
114-
return _default
115112
}
113+
return _default
116114
}
117115

118116
set {
119-
queue.sync {
120-
guard let toolchain = newValue else {
121-
_default = nil
122-
return
123-
}
124-
precondition(
125-
_toolchains.contains { $0 === toolchain },
126-
"default toolchain must be registered first"
127-
)
128-
_default = toolchain
117+
guard let toolchain = newValue else {
118+
_default = nil
119+
return
129120
}
121+
precondition(
122+
toolchains.contains { $0 === toolchain },
123+
"default toolchain must be registered first"
124+
)
125+
_default = toolchain
130126
}
131127
}
132128

133129
/// The standard default toolchain identifier on Darwin.
130+
@_spi(Testing)
134131
public static let darwinDefaultToolchainIdentifier: String = "com.apple.dt.toolchain.XcodeDefault"
135132

136133
/// The current toolchain identifier on Darwin, which is either specified byt the `TOOLCHAINS`
137134
/// environment variable, or defaults to `darwinDefaultToolchainIdentifier`.
138135
///
139136
/// The value of `default.identifier` may be different if the default toolchain has been
140137
/// explicitly overridden in code, or if there is no toolchain with this identifier.
138+
@_spi(Testing)
141139
public var darwinToolchainIdentifier: String {
142140
return darwinToolchainOverride ?? ToolchainRegistry.darwinDefaultToolchainIdentifier
143141
}
142+
}
144143

145-
/// All toolchains, in the order they were added.
146-
public var toolchains: [Toolchain] {
147-
return queue.sync { _toolchains }
148-
}
149-
144+
/// Inspecting internal state for testing purposes.
145+
extension ToolchainRegistry {
146+
@_spi(Testing)
150147
public func toolchains(identifier: String) -> [Toolchain] {
151-
return queue.sync { toolchainsByIdentifier[identifier] ?? [] }
148+
return toolchainsByIdentifier[identifier] ?? []
152149
}
153150

151+
@_spi(Testing)
154152
public func toolchain(identifier: String) -> Toolchain? {
155153
return toolchains(identifier: identifier).first
156154
}
157155

156+
@_spi(Testing)
158157
public func toolchain(path: AbsolutePath) -> Toolchain? {
159-
return queue.sync { toolchainsByPath[path] }
158+
return toolchainsByPath[path]
160159
}
161160
}
162161

163162
extension ToolchainRegistry {
164-
165163
public enum Error: Swift.Error {
166164

167165
/// There is already a toolchain with the given identifier.
@@ -178,11 +176,8 @@ extension ToolchainRegistry {
178176
///
179177
/// - parameter toolchain: The new toolchain to register.
180178
/// - throws: If `toolchain.identifier` has already been seen.
179+
@_spi(Testing)
181180
public func registerToolchain(_ toolchain: Toolchain) throws {
182-
try queue.sync { try _registerToolchain(toolchain) }
183-
}
184-
185-
func _registerToolchain(_ toolchain: Toolchain) throws {
186181
// Non-XcodeDefault toolchain: disallow all duplicates.
187182
if toolchain.identifier != ToolchainRegistry.darwinDefaultToolchainIdentifier {
188183
guard toolchainsByIdentifier[toolchain.identifier] == nil else {
@@ -198,10 +193,8 @@ extension ToolchainRegistry {
198193
toolchainsByPath[path] = toolchain
199194
}
200195

201-
var toolchains = toolchainsByIdentifier[toolchain.identifier] ?? []
196+
toolchainsByIdentifier[toolchain.identifier, default: []].append(toolchain)
202197
toolchains.append(toolchain)
203-
toolchainsByIdentifier[toolchain.identifier] = toolchains
204-
_toolchains.append(toolchain)
205198
}
206199

207200
/// Register the toolchain at the given path.
@@ -214,16 +207,11 @@ extension ToolchainRegistry {
214207
_ path: AbsolutePath,
215208
_ fileSystem: FileSystem = localFileSystem
216209
) throws -> Toolchain {
217-
return try queue.sync { try _registerToolchain(path, fileSystem) }
218-
}
219-
220-
func _registerToolchain(_ path: AbsolutePath, _ fileSystem: FileSystem) throws -> Toolchain {
221-
if let toolchain = Toolchain(path, fileSystem) {
222-
try _registerToolchain(toolchain)
223-
return toolchain
224-
} else {
210+
guard let toolchain = Toolchain(path, fileSystem) else {
225211
throw Error.invalidToolchain
226212
}
213+
try registerToolchain(toolchain)
214+
return toolchain
227215
}
228216
}
229217

@@ -237,6 +225,7 @@ extension ToolchainRegistry {
237225
/// * (Darwin) The currently selected Xcode
238226
/// * (Darwin) [~]/Library/Developer/Toolchains
239227
/// * env SOURCEKIT_PATH, PATH (or Path)
228+
@_spi(Testing)
240229
public func scanForToolchains(
241230
installPath: AbsolutePath? = nil,
242231
environmentVariables: [String] = ["SOURCEKIT_TOOLCHAIN_PATH"],
@@ -251,18 +240,20 @@ extension ToolchainRegistry {
251240
AbsolutePath(validating: "/Library/Developer/Toolchains"),
252241
]
253242

254-
queue.sync {
255-
_scanForToolchains(environmentVariables: environmentVariables, setDefault: true, fileSystem)
256-
if let installPath = installPath,
257-
let toolchain = try? _registerToolchain(installPath, fileSystem),
258-
_default == nil
259-
{
260-
_default = toolchain
261-
}
262-
xcodes.forEach { _scanForToolchains(xcode: $0, fileSystem) }
263-
xctoolchainSearchPaths.forEach { _scanForToolchains(xctoolchainSearchPath: $0, fileSystem) }
264-
_scanForToolchains(pathVariables: pathVariables, fileSystem)
243+
scanForToolchains(environmentVariables: environmentVariables, setDefault: true, fileSystem)
244+
if let installPath = installPath,
245+
let toolchain = try? registerToolchain(installPath, fileSystem),
246+
_default == nil
247+
{
248+
_default = toolchain
249+
}
250+
for xcode in xcodes {
251+
scanForToolchains(xcode: xcode, fileSystem)
252+
}
253+
for xctoolchainSearchPath in xctoolchainSearchPaths {
254+
scanForToolchains(xctoolchainSearchPath: xctoolchainSearchPath, fileSystem)
265255
}
256+
scanForToolchains(pathVariables: pathVariables, fileSystem)
266257
}
267258

268259
/// Scan for toolchains in the paths given by `environmentVariables` and possibly override the
@@ -271,30 +262,17 @@ extension ToolchainRegistry {
271262
/// - parameters:
272263
/// - environmentVariables: A list of environment variable names to search for toolchain paths.
273264
/// - setDefault: If true, the first toolchain found will be set as the default.
265+
@_spi(Testing)
274266
public func scanForToolchains(
275267
environmentVariables: [String],
276268
setDefault: Bool,
277269
_ fileSystem: FileSystem = localFileSystem
278-
) {
279-
queue.sync {
280-
_scanForToolchains(
281-
environmentVariables: environmentVariables,
282-
setDefault: setDefault,
283-
fileSystem
284-
)
285-
}
286-
}
287-
288-
func _scanForToolchains(
289-
environmentVariables: [String],
290-
setDefault: Bool,
291-
_ fileSystem: FileSystem
292270
) {
293271
var shouldSetDefault = setDefault
294272
for envVar in environmentVariables {
295273
if let pathStr = ProcessEnv.vars[envVar],
296274
let path = try? AbsolutePath(validating: pathStr),
297-
let toolchain = try? _registerToolchain(path, fileSystem),
275+
let toolchain = try? registerToolchain(path, fileSystem),
298276
shouldSetDefault
299277
{
300278
shouldSetDefault = false
@@ -308,58 +286,48 @@ extension ToolchainRegistry {
308286
/// - parameters:
309287
/// - pathVariables: A list of PATH-like environment variable names to search.
310288
/// - setDefault: If true, the first toolchain found will be set as the default.
311-
public
312-
func scanForToolchains(pathVariables: [String], _ fileSystem: FileSystem = localFileSystem)
313-
{
314-
queue.sync { _scanForToolchains(pathVariables: pathVariables, fileSystem) }
315-
}
316-
317-
func _scanForToolchains(pathVariables: [String], _ fileSystem: FileSystem) {
289+
@_spi(Testing)
290+
public func scanForToolchains(pathVariables: [String], _ fileSystem: FileSystem = localFileSystem) {
318291
pathVariables.lazy.flatMap { envVar in
319292
getEnvSearchPaths(pathString: ProcessEnv.vars[envVar], currentWorkingDirectory: nil)
320293
}
321294
.forEach { path in
322-
_ = try? _registerToolchain(path, fileSystem)
295+
_ = try? registerToolchain(path, fileSystem)
323296
}
324297
}
325298

326299
/// Scan for toolchains in the given Xcode, which should be given as a path to either the
327300
/// application (e.g. "Xcode.app") or the application's Developer directory.
328301
///
329302
/// - parameter xcode: The path to Xcode.app or Xcode.app/Contents/Developer.
303+
@_spi(Testing)
330304
public func scanForToolchains(xcode: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) {
331-
queue.sync { _scanForToolchains(xcode: xcode, fileSystem) }
332-
}
333-
334-
func _scanForToolchains(xcode: AbsolutePath, _ fileSystem: FileSystem) {
335305
var path = xcode
336306
if path.extension == "app" {
337307
path = path.appending(components: "Contents", "Developer")
338308
}
339-
_scanForToolchains(xctoolchainSearchPath: path.appending(component: "Toolchains"), fileSystem)
309+
scanForToolchains(xctoolchainSearchPath: path.appending(component: "Toolchains"), fileSystem)
340310
}
341311

342312
/// Scan for `xctoolchain` directories in the given search path.
343313
///
344314
/// - parameter toolchains: Directory containing xctoolchains, e.g. /Library/Developer/Toolchains
315+
@_spi(Testing)
345316
public func scanForToolchains(
346317
xctoolchainSearchPath searchPath: AbsolutePath,
347318
_ fileSystem: FileSystem = localFileSystem
348319
) {
349-
queue.sync { _scanForToolchains(xctoolchainSearchPath: searchPath, fileSystem) }
350-
}
351-
352-
func _scanForToolchains(xctoolchainSearchPath searchPath: AbsolutePath, _ fileSystem: FileSystem) {
353320
guard let direntries = try? fileSystem.getDirectoryContents(searchPath) else { return }
354321
for name in direntries {
355322
let path = searchPath.appending(component: name)
356323
if path.extension == "xctoolchain" {
357-
_ = try? _registerToolchain(path, fileSystem)
324+
_ = try? registerToolchain(path, fileSystem)
358325
}
359326
}
360327
}
361328

362329
/// The path of the current Xcode.app/Contents/Developer.
330+
@_spi(Testing)
363331
public static var currentXcodeDeveloperPath: AbsolutePath? {
364332
guard let str = try? Process.checkNonZeroExit(args: "/usr/bin/xcode-select", "-p") else { return nil }
365333
return try? AbsolutePath(validating: str.trimmingCharacters(in: .whitespacesAndNewlines))

0 commit comments

Comments
 (0)