Skip to content

Commit a65ecf6

Browse files
committed
Add session API for registering additional toolchains
1 parent 94711fa commit a65ecf6

File tree

7 files changed

+132
-27
lines changed

7 files changed

+132
-27
lines changed

Sources/SWBBuildService/Messages.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,13 @@ private struct SetSessionUserPreferencesMsg: MessageHandler {
302302
}
303303
}
304304

305+
private struct RegisterToolchainHandler: MessageHandler {
306+
func handle(request: Request, message: RegisterToolchainRequest) async throws -> StringResponse {
307+
let session = try request.session(for: message)
308+
return StringResponse(try await session.core.registerToolchain(at: message.path))
309+
}
310+
}
311+
305312
/// Start a PIF transfer from the client.
306313
///
307314
/// This will establish a workspace context in the relevant session by exchanging a PIF from the client to the service incrementally, only transferring subobjects as necessary.
@@ -1542,6 +1549,7 @@ public struct ServiceSessionMessageHandlers: ServiceExtension {
15421549
service.registerMessageHandler(SetSessionSystemInfoMsg.self)
15431550
service.registerMessageHandler(SetSessionUserInfoMsg.self)
15441551
service.registerMessageHandler(SetSessionUserPreferencesMsg.self)
1552+
service.registerMessageHandler(RegisterToolchainHandler.self)
15451553
service.registerMessageHandler(DeveloperPathHandler.self)
15461554
}
15471555
}

Sources/SWBCore/Core.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,10 @@ public final class Core: Sendable {
533533
specRegistry.freeze()
534534
}
535535

536+
public func registerToolchain(at toolchainPath: Path) async throws -> String {
537+
return try await toolchainRegistry.registerToolchain(at: toolchainPath, operatingSystem: hostOperatingSystem, delegate: registryDelegate, diagnoseAlreadyRegisteredToolchain: false, aliases: [])
538+
}
539+
536540
/// Dump information on the registered platforms.
537541
public func getPlatformsDump() -> String {
538542
var result = ""
@@ -615,7 +619,7 @@ public final class Core: Sendable {
615619
/// Dump information on the registered toolchains.
616620
public func getToolchainsDump() async -> String {
617621
var result = ""
618-
for (_,toolchain) in toolchainRegistry.toolchainsByIdentifier.sorted(byKey: <) {
622+
for toolchain in toolchainRegistry.toolchains.sorted(by: \.identifier) {
619623
result += "\(toolchain)\n"
620624
}
621625
return result

Sources/SWBCore/ToolchainRegistry.swift

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,15 @@ extension Toolchain {
428428

429429
/// The ToolchainRegistry manages the set of registered toolchains.
430430
public final class ToolchainRegistry: @unchecked Sendable {
431+
enum Error: Swift.Error, CustomStringConvertible {
432+
case toolchainAlreadyRegistered(String, Path)
433+
434+
var description: String {
435+
switch self {
436+
case .toolchainAlreadyRegistered(let identifier, let path): "toolchain '\(identifier)' already registered from \(path.str)"
437+
}
438+
}
439+
}
431440
@_spi(Testing) public struct SearchPath: Sendable {
432441
public var path: Path
433442
public var strict: Bool
@@ -443,11 +452,14 @@ public final class ToolchainRegistry: @unchecked Sendable {
443452
let fs: any FSProxy
444453
let hostOperatingSystem: OperatingSystem
445454

446-
/// The map of toolchains by identifier.
447-
@_spi(Testing) public private(set) var toolchainsByIdentifier = Dictionary<String, Toolchain>()
455+
struct State {
456+
/// The map of toolchains by identifier.
457+
@_spi(Testing) public fileprivate(set) var toolchainsByIdentifier = Dictionary<String, Toolchain>()
448458

449-
/// Lower-cased alias -> toolchain (alias lookup is case-insensitive)
450-
@_spi(Testing) public private(set) var toolchainsByAlias = Dictionary<String, Toolchain>()
459+
/// Lower-cased alias -> toolchain (alias lookup is case-insensitive)
460+
@_spi(Testing) public fileprivate(set) var toolchainsByAlias = Dictionary<String, Toolchain>()
461+
}
462+
private let state: SWBMutex<State> = .init(State())
451463

452464
public static let defaultToolchainIdentifier: String = "com.apple.dt.toolchain.XcodeDefault"
453465

@@ -503,40 +515,53 @@ public final class ToolchainRegistry: @unchecked Sendable {
503515
guard toolchainPath.basenameWithoutSuffix != "swift-latest" else { continue }
504516

505517
do {
506-
let toolchain = try await Toolchain(path: toolchainPath, operatingSystem: operatingSystem, aliases: aliases, fs: fs, pluginManager: delegate.pluginManager, platformRegistry: delegate.platformRegistry)
507-
try register(toolchain)
518+
_ = try await registerToolchain(at: toolchainPath, operatingSystem: operatingSystem, delegate: delegate, diagnoseAlreadyRegisteredToolchain: true, aliases: aliases)
508519
} catch let err {
509520
delegate.issue(strict: strict, toolchainPath, "failed to load toolchain: \(err)")
510521
}
511522
}
512523
}
513524

514-
private func register(_ toolchain: Toolchain) throws {
515-
if let duplicateToolchain = toolchainsByIdentifier[toolchain.identifier] {
516-
throw StubError.error("toolchain '\(toolchain.identifier)' already registered from \(duplicateToolchain.path.str)")
525+
func registerToolchain(at toolchainPath: Path, operatingSystem: OperatingSystem, delegate: any ToolchainRegistryDelegate, diagnoseAlreadyRegisteredToolchain: Bool, aliases: Set<String>) async throws -> String {
526+
do {
527+
let toolchain = try await Toolchain(path: toolchainPath, operatingSystem: operatingSystem, aliases: aliases, fs: fs, pluginManager: delegate.pluginManager, platformRegistry: delegate.platformRegistry)
528+
try register(toolchain)
529+
return toolchain.identifier
530+
} catch Error.toolchainAlreadyRegistered(let identifier, _) where !diagnoseAlreadyRegisteredToolchain {
531+
return identifier
517532
}
518-
toolchainsByIdentifier[toolchain.identifier] = toolchain
519-
520-
for alias in toolchain.aliases {
521-
guard !alias.isEmpty else { continue }
522-
assert(alias.lowercased() == alias)
533+
}
523534

524-
// When two toolchains have conflicting aliases, the highest-versioned toolchain wins (regardless of identifier)
525-
if let existingToolchain = toolchainsByAlias[alias], existingToolchain.version >= toolchain.version {
526-
continue
535+
private func register(_ toolchain: Toolchain) throws {
536+
try state.withLock { state in
537+
if let duplicateToolchain = state.toolchainsByIdentifier[toolchain.identifier] {
538+
throw Error.toolchainAlreadyRegistered(toolchain.identifier, duplicateToolchain.path)
527539
}
540+
state.toolchainsByIdentifier[toolchain.identifier] = toolchain
541+
542+
for alias in toolchain.aliases {
543+
guard !alias.isEmpty else { continue }
544+
assert(alias.lowercased() == alias)
545+
546+
// When two toolchains have conflicting aliases, the highest-versioned toolchain wins (regardless of identifier)
547+
if let existingToolchain = state.toolchainsByAlias[alias], existingToolchain.version >= toolchain.version {
548+
continue
549+
}
528550

529-
toolchainsByAlias[alias] = toolchain
551+
state.toolchainsByAlias[alias] = toolchain
552+
}
530553
}
531554
}
532555

533556
/// Look up the toolchain with the given identifier.
534557
public func lookup(_ identifier: String) -> Toolchain? {
535-
let lowercasedIdentifier = identifier.lowercased()
536-
if ["default", "xcode"].contains(lowercasedIdentifier) {
537-
return toolchainsByIdentifier[ToolchainRegistry.defaultToolchainIdentifier] ?? toolchainsByAlias[lowercasedIdentifier]
538-
} else {
539-
return toolchainsByIdentifier[identifier] ?? toolchainsByAlias[lowercasedIdentifier]
558+
state.withLock { state in
559+
let lowercasedIdentifier = identifier.lowercased()
560+
if ["default", "xcode"].contains(lowercasedIdentifier) {
561+
return state.toolchainsByIdentifier[ToolchainRegistry.defaultToolchainIdentifier] ?? state.toolchainsByAlias[lowercasedIdentifier]
562+
} else {
563+
return state.toolchainsByIdentifier[identifier] ?? state.toolchainsByAlias[lowercasedIdentifier]
564+
}
540565
}
541566
}
542567

@@ -545,6 +570,8 @@ public final class ToolchainRegistry: @unchecked Sendable {
545570
}
546571

547572
public var toolchains: Set<Toolchain> {
548-
return Set(self.toolchainsByIdentifier.values)
573+
state.withLock { state in
574+
return Set(state.toolchainsByIdentifier.values)
575+
}
549576
}
550577
}

Sources/SWBProtocol/Message.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,20 @@ public struct MacCatalystUnavailableFrameworkNamesRequest: RequestMessage, Equat
356356
}
357357
}
358358

359+
public struct RegisterToolchainRequest: SessionMessage, RequestMessage, Equatable, SerializableCodable {
360+
public typealias ResponseMessage = StringResponse
361+
362+
public static let name = "REGISTER_TOOLCHAIN_REQUEST"
363+
364+
public let sessionHandle: String
365+
public let path: Path
366+
367+
public init(sessionHandle: String, path: Path) {
368+
self.sessionHandle = sessionHandle
369+
self.path = path
370+
}
371+
}
372+
359373
public struct AppleSystemFrameworkNamesRequest: RequestMessage, Equatable, PendingSerializableCodable {
360374
public typealias ResponseMessage = StringListResponse
361375

@@ -1168,6 +1182,7 @@ public struct IPCMessage: Serializable, Sendable {
11681182
GetSpecsRequest.self,
11691183
GetStatisticsRequest.self,
11701184
GetToolchainsRequest.self,
1185+
RegisterToolchainRequest.self,
11711186
GetBuildSettingsDescriptionRequest.self,
11721187
ExecuteCommandLineToolRequest.self,
11731188

Sources/SwiftBuild/SWBBuildServiceSession.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,10 @@ public final class SWBBuildServiceSession: Sendable {
629629
public func setUserPreferences(enableDebugActivityLogs: Bool, enableBuildDebugging: Bool, enableBuildSystemCaching: Bool, activityTextShorteningLevel: Int, usePerConfigurationBuildLocations: Bool?, allowsExternalToolExecution: Bool) async throws {
630630
_ = try await service.send(request: SetSessionUserPreferencesRequest(sessionHandle: self.uid, enableDebugActivityLogs: enableDebugActivityLogs, enableBuildDebugging: enableBuildDebugging, enableBuildSystemCaching: enableBuildSystemCaching, activityTextShorteningLevel: ActivityTextShorteningLevel(rawValue: activityTextShorteningLevel) ?? .default, usePerConfigurationBuildLocations: usePerConfigurationBuildLocations, allowsExternalToolExecution: allowsExternalToolExecution))
631631
}
632+
633+
public func registerToolchain(at path: String) async throws -> String {
634+
return try await service.send(request: RegisterToolchainRequest(sessionHandle: self.uid, path: Path(path))).value
635+
}
632636
}
633637

634638
extension SWBBuildServiceSession {

Tests/SWBCoreTests/ToolchainRegistryTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ import SWBServiceCore
124124
("swift.xctoolchain", ["CFBundleIdentifier": "org.swift.3020161115a", "Aliases": ["swift"]]),
125125
("swift-latest.xctoolchain", ["CFBundleIdentifier": "org.swift.latest"]),
126126
]) { registry, warnings, errors in
127-
#expect(registry.toolchainsByIdentifier.keys.sorted(by: <) == [ToolchainRegistry.defaultToolchainIdentifier, "d", "org.swift.3020161115a"])
127+
#expect(registry.toolchains.map(\.identifier).sorted(by: <) == [ToolchainRegistry.defaultToolchainIdentifier, "d", "org.swift.3020161115a"])
128128

129129
if strict {
130130
#expect(warnings.isEmpty)
@@ -175,7 +175,7 @@ import SWBServiceCore
175175
("swift-older.xctoolchain", ["CFBundleIdentifier": "org.swift.3020161114a", "Version": "3.0.220161211141", "Aliases": ["swift"]]),
176176
], infoPlistName: "Info.plist") { registry, _, errors in
177177

178-
#expect(Set(registry.toolchainsByIdentifier.keys) == Set(["org.swift.3020161114a", "org.swift.3020161115a"] + additionalToolchains))
178+
#expect(Set(registry.toolchains.map(\.identifier)) == Set(["org.swift.3020161114a", "org.swift.3020161115a"] + additionalToolchains))
179179
#expect(errors.count == 0, "\(errors)")
180180
#expect(registry.lookup("org.swift.3020161115a")?.identifier == "org.swift.3020161115a")
181181
#expect(registry.lookup("org.swift.3020161114a")?.identifier == "org.swift.3020161114a")
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import Testing
15+
import SwiftBuild
16+
import SwiftBuildTestSupport
17+
import SWBTestSupport
18+
@_spi(Testing) import SWBUtil
19+
20+
@Suite
21+
fileprivate struct ToolchainTests {
22+
@Test
23+
func lateRegistration() async throws {
24+
try await withTemporaryDirectory { temporaryDirectory in
25+
try await withAsyncDeferrable { deferrable in
26+
let tmpDirPath = temporaryDirectory.path
27+
let testSession = try await TestSWBSession(temporaryDirectory: temporaryDirectory)
28+
await deferrable.addBlock {
29+
await #expect(throws: Never.self) {
30+
try await testSession.close()
31+
}
32+
}
33+
try localFS.createDirectory(tmpDirPath.join("Foo.xctoolchain"))
34+
try await localFS.writePlist(tmpDirPath.join("Foo.xctoolchain/Info.plist"), ["Identifier" : "org.swift.foo"])
35+
do {
36+
let identifier = try await testSession.session.registerToolchain(at: tmpDirPath.join("Foo.xctoolchain").str)
37+
#expect(identifier == "org.swift.foo")
38+
}
39+
// Late registration should be idempotent
40+
do {
41+
let identifier = try await testSession.session.registerToolchain(at: tmpDirPath.join("Foo.xctoolchain").str)
42+
#expect(identifier == "org.swift.foo")
43+
}
44+
}
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)