@@ -428,6 +428,15 @@ extension Toolchain {
428428
429429/// The ToolchainRegistry manages the set of registered toolchains.
430430public 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}
0 commit comments