Skip to content

Commit 6505ec8

Browse files
committed
Make ToolchainRegistry initializer synchronous
This requires a bit of a restructuring of `ToolchainRegistry` because calling `scanForToolchain` on `self` is a suspension point. Instead, we need to implement the entire toolchain discovery in the initializer itself, which really isn’t that bad anyway. This allows us to make the `SourceKitLSP.run` function synchronous again. Fixes #1023 rdar://120976054
1 parent 6a77e3b commit 6505ec8

File tree

5 files changed

+352
-380
lines changed

5 files changed

+352
-380
lines changed

Sources/SKCore/ToolchainRegistry.swift

Lines changed: 110 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import struct TSCBasic.AbsolutePath
1818
import protocol TSCBasic.FileSystem
1919
import class TSCBasic.Process
2020
import enum TSCBasic.ProcessEnv
21+
import struct TSCBasic.ProcessEnvironmentKey
2122
import func TSCBasic.getEnvSearchPaths
2223
import var TSCBasic.localFileSystem
2324

@@ -29,63 +30,141 @@ import var TSCBasic.localFileSystem
2930
public final actor ToolchainRegistry {
3031

3132
/// The toolchains, in the order they were registered.
32-
public private(set) var toolchains: [Toolchain] = []
33+
public let toolchains: [Toolchain]
3334

3435
/// The toolchains indexed by their identifier.
3536
///
3637
/// Multiple toolchains may exist for the XcodeDefault toolchain identifier.
37-
private var toolchainsByIdentifier: [String: [Toolchain]] = [:]
38+
private let toolchainsByIdentifier: [String: [Toolchain]]
3839

3940
/// The toolchains indexed by their path.
4041
///
4142
/// Note: Not all toolchains have a path.
42-
private var toolchainsByPath: [AbsolutePath: Toolchain] = [:]
43+
private let toolchainsByPath: [AbsolutePath: Toolchain]
4344

4445
/// The currently selected toolchain identifier on Darwin.
45-
public lazy var darwinToolchainOverride: String? = {
46-
if let id = ProcessEnv.vars["TOOLCHAINS"], !id.isEmpty, id != "default" {
47-
return id
46+
public let darwinToolchainOverride: String?
47+
48+
/// Creates a toolchain registry from a list of toolchains.
49+
///
50+
/// - Parameters:
51+
/// - toolchains: The toolchains that should be stored in the registry.
52+
/// - darwinToolchainOverride: The contents of the `TOOLCHAINS` environment
53+
/// variable, which picks the default toolchain.
54+
@_spi(Testing)
55+
public init(
56+
toolchains toolchainsParam: [Toolchain],
57+
darwinToolchainOverride: String?
58+
) {
59+
var toolchains: [Toolchain] = []
60+
var toolchainsByIdentifier: [String: [Toolchain]] = [:]
61+
var toolchainsByPath: [AbsolutePath: Toolchain] = [:]
62+
for toolchain in toolchainsParam {
63+
// Non-XcodeDefault toolchain: disallow all duplicates.
64+
if toolchain.identifier != ToolchainRegistry.darwinDefaultToolchainIdentifier {
65+
guard toolchainsByIdentifier[toolchain.identifier] == nil else {
66+
continue
67+
}
68+
}
69+
70+
// Toolchain should always be unique by path if it is present.
71+
if let path = toolchain.path {
72+
guard toolchainsByPath[path] == nil else {
73+
continue
74+
}
75+
toolchainsByPath[path] = toolchain
76+
}
77+
78+
toolchainsByIdentifier[toolchain.identifier, default: []].append(toolchain)
79+
toolchains.append(toolchain)
4880
}
49-
return nil
50-
}()
5181

52-
/// Creates an empty toolchain registry.
53-
private init() {}
82+
self.toolchains = toolchains
83+
self.toolchainsByIdentifier = toolchainsByIdentifier
84+
self.toolchainsByPath = toolchainsByPath
85+
86+
if let darwinToolchainOverride, !darwinToolchainOverride.isEmpty, darwinToolchainOverride != "default" {
87+
self.darwinToolchainOverride = darwinToolchainOverride
88+
} else {
89+
self.darwinToolchainOverride = nil
90+
}
91+
}
5492

5593
/// A toolchain registry used for testing that scans for toolchains based on environment variables and Xcode
5694
/// installations but not next to the `sourcekit-lsp` binary because there is no `sourcekit-lsp` binary during
5795
/// testing.
5896
@_spi(Testing)
5997
public static var forTesting: ToolchainRegistry {
60-
get async {
61-
await ToolchainRegistry(localFileSystem)
62-
}
98+
ToolchainRegistry(localFileSystem)
6399
}
64100

65-
/// A toolchain registry that doesn't contain any toolchains.
66-
@_spi(Testing)
67-
public static var empty: ToolchainRegistry { ToolchainRegistry() }
68-
69101
/// Creates a toolchain registry populated by scanning for toolchains according to the given paths
70102
/// and variables.
71103
///
72104
/// If called with the default values, creates a toolchain registry that searches:
73-
/// * env SOURCEKIT_TOOLCHAIN_PATH <-- will override default toolchain
74-
/// * installPath <-- will override default toolchain
105+
/// * `env SOURCEKIT_TOOLCHAIN_PATH` <-- will override default toolchain
106+
/// * `installPath` <-- will override default toolchain
75107
/// * (Darwin) The currently selected Xcode
76-
/// * (Darwin) [~]/Library/Developer/Toolchains
77-
/// * env SOURCEKIT_PATH, PATH
78-
///
79-
/// This is equivalent to
80-
/// ```
81-
/// let tr = ToolchainRegistry()
82-
/// tr.scanForToolchains()
83-
/// ```
108+
/// * (Darwin) `[~]/Library/Developer/Toolchains`
109+
/// * `env SOURCEKIT_PATH, PATH`
84110
public init(
85111
installPath: AbsolutePath? = nil,
86-
_ fileSystem: FileSystem
87-
) async {
88-
scanForToolchains(installPath: installPath, fileSystem)
112+
environmentVariables: [ProcessEnvironmentKey] = ["SOURCEKIT_TOOLCHAIN_PATH"],
113+
xcodes: [AbsolutePath] = [_currentXcodeDeveloperPath].compactMap({ $0 }),
114+
darwinToolchainOverride: String? = ProcessEnv.block["TOOLCHAINS"],
115+
_ fileSystem: FileSystem = localFileSystem
116+
) {
117+
// The paths at which we have found toolchains
118+
var toolchainPaths: [AbsolutePath] = []
119+
120+
// Scan for toolchains in the paths given by `environmentVariables`.
121+
for envVar in environmentVariables {
122+
if let pathStr = ProcessEnv.block[envVar], let path = try? AbsolutePath(validating: pathStr) {
123+
toolchainPaths.append(path)
124+
}
125+
}
126+
127+
// Search for toolchains relative to the path at which sourcekit-lsp is installed.
128+
if let installPath = installPath {
129+
toolchainPaths.append(installPath)
130+
}
131+
132+
// Search for toolchains in the Xcode developer directories and global toolchain install paths
133+
let toolchainSearchPaths =
134+
xcodes.map {
135+
if $0.extension == "app" {
136+
return $0.appending(components: "Contents", "Developer", "Toolchains")
137+
} else {
138+
return $0.appending(component: "Toolchains")
139+
}
140+
} + [
141+
try! AbsolutePath(expandingTilde: "~/Library/Developer/Toolchains"),
142+
try! AbsolutePath(validating: "/Library/Developer/Toolchains"),
143+
]
144+
145+
for xctoolchainSearchPath in toolchainSearchPaths {
146+
guard let direntries = try? fileSystem.getDirectoryContents(xctoolchainSearchPath) else {
147+
continue
148+
}
149+
for name in direntries {
150+
let path = xctoolchainSearchPath.appending(component: name)
151+
if path.extension == "xctoolchain" {
152+
toolchainPaths.append(path)
153+
}
154+
}
155+
}
156+
157+
// Scan for toolchains by the given PATH-like environment variables.
158+
for envVar: ProcessEnvironmentKey in ["SOURCEKIT_PATH", "PATH", "Path"] {
159+
for path in getEnvSearchPaths(pathString: ProcessEnv.block[envVar], currentWorkingDirectory: nil) {
160+
toolchainPaths.append(path)
161+
}
162+
}
163+
164+
self.init(
165+
toolchains: toolchainPaths.compactMap { Toolchain($0, fileSystem) },
166+
darwinToolchainOverride: darwinToolchainOverride
167+
)
89168
}
90169

91170
/// The default toolchain.
@@ -102,7 +181,6 @@ public final actor ToolchainRegistry {
102181
} else {
103182
return toolchains.first
104183
}
105-
return nil
106184
}
107185
}
108186

@@ -140,164 +218,8 @@ extension ToolchainRegistry {
140218
}
141219

142220
extension ToolchainRegistry {
143-
public enum Error: Swift.Error {
144-
145-
/// There is already a toolchain with the given identifier.
146-
case duplicateToolchainIdentifier
147-
148-
/// There is already a toolchain with the given path.
149-
case duplicateToolchainPath
150-
151-
/// The toolchain does not exist, or has no tools.
152-
case invalidToolchain
153-
}
154-
155-
/// Register the given toolchain.
156-
///
157-
/// - parameter toolchain: The new toolchain to register.
158-
/// - throws: If `toolchain.identifier` has already been seen.
159-
@_spi(Testing)
160-
public func registerToolchain(_ toolchain: Toolchain) throws {
161-
// Non-XcodeDefault toolchain: disallow all duplicates.
162-
if toolchain.identifier != ToolchainRegistry.darwinDefaultToolchainIdentifier {
163-
guard toolchainsByIdentifier[toolchain.identifier] == nil else {
164-
throw Error.duplicateToolchainIdentifier
165-
}
166-
}
167-
168-
// Toolchain should always be unique by path if it is present.
169-
if let path = toolchain.path {
170-
guard toolchainsByPath[path] == nil else {
171-
throw Error.duplicateToolchainPath
172-
}
173-
toolchainsByPath[path] = toolchain
174-
}
175-
176-
toolchainsByIdentifier[toolchain.identifier, default: []].append(toolchain)
177-
toolchains.append(toolchain)
178-
}
179-
180-
/// Register the toolchain at the given path.
181-
///
182-
/// - parameter path: The path to search for a toolchain to register.
183-
/// - returns: The toolchain, if any.
184-
/// - throws: If there is no toolchain at the given `path`, or if `toolchain.identifier` has
185-
/// already been seen.
186-
public func registerToolchain(
187-
_ path: AbsolutePath,
188-
_ fileSystem: FileSystem = localFileSystem
189-
) throws {
190-
guard let toolchain = Toolchain(path, fileSystem) else {
191-
throw Error.invalidToolchain
192-
}
193-
try registerToolchain(toolchain)
194-
}
195-
}
196-
197-
extension ToolchainRegistry {
198-
199-
/// Scans for toolchains according to the given paths and variables.
200-
///
201-
/// If called with the default values, creates a toolchain registry that searches:
202-
/// * env SOURCEKIT_TOOLCHAIN_PATH <-- will override default toolchain
203-
/// * installPath <-- will override default toolchain
204-
/// * (Darwin) The currently selected Xcode
205-
/// * (Darwin) [~]/Library/Developer/Toolchains
206-
/// * env SOURCEKIT_PATH, PATH (or Path)
207-
@_spi(Testing)
208-
public func scanForToolchains(
209-
installPath: AbsolutePath? = nil,
210-
environmentVariables: [String] = ["SOURCEKIT_TOOLCHAIN_PATH"],
211-
xcodes: [AbsolutePath] = [currentXcodeDeveloperPath].compactMap({ $0 }),
212-
xctoolchainSearchPaths: [AbsolutePath]? = nil,
213-
pathVariables: [String] = ["SOURCEKIT_PATH", "PATH", "Path"],
214-
_ fileSystem: FileSystem
215-
) {
216-
let xctoolchainSearchPaths =
217-
try! xctoolchainSearchPaths ?? [
218-
AbsolutePath(expandingTilde: "~/Library/Developer/Toolchains"),
219-
AbsolutePath(validating: "/Library/Developer/Toolchains"),
220-
]
221-
222-
scanForToolchains(environmentVariables: environmentVariables, fileSystem)
223-
if let installPath = installPath {
224-
try? registerToolchain(installPath, fileSystem)
225-
}
226-
for xcode in xcodes {
227-
scanForToolchains(xcode: xcode, fileSystem)
228-
}
229-
for xctoolchainSearchPath in xctoolchainSearchPaths {
230-
scanForToolchains(xctoolchainSearchPath: xctoolchainSearchPath, fileSystem)
231-
}
232-
scanForToolchains(pathVariables: pathVariables, fileSystem)
233-
}
234-
235-
/// Scan for toolchains in the paths given by `environmentVariables` and possibly override the
236-
/// default toolchain with the first one found.
237-
///
238-
/// - parameters:
239-
/// - environmentVariables: A list of environment variable names to search for toolchain paths.
240-
@_spi(Testing)
241-
public func scanForToolchains(
242-
environmentVariables: [String],
243-
_ fileSystem: FileSystem = localFileSystem
244-
) {
245-
for envVar in environmentVariables {
246-
if let pathStr = ProcessEnv.vars[envVar],
247-
let path = try? AbsolutePath(validating: pathStr)
248-
{
249-
try? registerToolchain(path, fileSystem)
250-
}
251-
}
252-
}
253-
254-
/// Scan for toolchains by the given PATH-like environment variables.
255-
///
256-
/// - parameters:
257-
/// - pathVariables: A list of PATH-like environment variable names to search.
258-
@_spi(Testing)
259-
public func scanForToolchains(pathVariables: [String], _ fileSystem: FileSystem = localFileSystem) {
260-
pathVariables.lazy.flatMap { envVar in
261-
getEnvSearchPaths(pathString: ProcessEnv.vars[envVar], currentWorkingDirectory: nil)
262-
}
263-
.forEach { path in
264-
_ = try? registerToolchain(path, fileSystem)
265-
}
266-
}
267-
268-
/// Scan for toolchains in the given Xcode, which should be given as a path to either the
269-
/// application (e.g. "Xcode.app") or the application's Developer directory.
270-
///
271-
/// - parameter xcode: The path to Xcode.app or Xcode.app/Contents/Developer.
272-
@_spi(Testing)
273-
public func scanForToolchains(xcode: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) {
274-
var path = xcode
275-
if path.extension == "app" {
276-
path = path.appending(components: "Contents", "Developer")
277-
}
278-
scanForToolchains(xctoolchainSearchPath: path.appending(component: "Toolchains"), fileSystem)
279-
}
280-
281-
/// Scan for `xctoolchain` directories in the given search path.
282-
///
283-
/// - parameter toolchains: Directory containing xctoolchains, e.g. /Library/Developer/Toolchains
284-
@_spi(Testing)
285-
public func scanForToolchains(
286-
xctoolchainSearchPath searchPath: AbsolutePath,
287-
_ fileSystem: FileSystem = localFileSystem
288-
) {
289-
guard let direntries = try? fileSystem.getDirectoryContents(searchPath) else { return }
290-
for name in direntries {
291-
let path = searchPath.appending(component: name)
292-
if path.extension == "xctoolchain" {
293-
_ = try? registerToolchain(path, fileSystem)
294-
}
295-
}
296-
}
297-
298221
/// The path of the current Xcode.app/Contents/Developer.
299-
@_spi(Testing)
300-
public static var currentXcodeDeveloperPath: AbsolutePath? {
222+
public static var _currentXcodeDeveloperPath: AbsolutePath? {
301223
guard let str = try? Process.checkNonZeroExit(args: "/usr/bin/xcode-select", "-p") else { return nil }
302224
return try? AbsolutePath(validating: str.trimmingCharacters(in: .whitespacesAndNewlines))
303225
}

Sources/SKTestSupport/TestSourceKitLSPClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public final class TestSourceKitLSPClient: MessageHandler {
105105

106106
let clientConnection = LocalConnection()
107107
self.serverToClientConnection = clientConnection
108-
server = await SourceKitServer(
108+
server = SourceKitServer(
109109
client: clientConnection,
110110
toolchainRegistry: ToolchainRegistry.forTesting,
111111
options: serverOptions,

Sources/sourcekit-lsp/SourceKitLSP.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ struct SourceKitLSP: ParsableCommand {
219219
return serverOptions
220220
}
221221

222-
func run() async throws {
222+
func run() throws {
223223
// Dup stdout and redirect the fd to stderr so that a careless print()
224224
// will not break our connection stream.
225225
let realStdout = dup(STDOUT_FILENO)
@@ -242,7 +242,7 @@ struct SourceKitLSP: ParsableCommand {
242242

243243
let installPath = try AbsolutePath(validating: Bundle.main.bundlePath)
244244

245-
let server = await SourceKitServer(
245+
let server = SourceKitServer(
246246
client: clientConnection,
247247
toolchainRegistry: ToolchainRegistry(installPath: installPath, localFileSystem),
248248
options: mapOptions(),

0 commit comments

Comments
 (0)