File: ToolchainRegistry.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (289 lines) | stat: -rw-r--r-- 11,348 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Dispatch
import Foundation
import LanguageServerProtocol
import SKSupport

import struct TSCBasic.AbsolutePath
import protocol TSCBasic.FileSystem
import class TSCBasic.Process
import enum TSCBasic.ProcessEnv
import struct TSCBasic.ProcessEnvironmentKey
import func TSCBasic.getEnvSearchPaths
import var TSCBasic.localFileSystem

/// Set of known toolchains.
///
/// Most users will use the `shared` ToolchainRegistry, although it's possible to create more. A
/// ToolchainRegistry is usually initialized by performing a search of predetermined paths,
/// e.g. `ToolchainRegistry(searchPaths: ToolchainRegistry.defaultSearchPaths)`.
public final actor ToolchainRegistry {
  /// The reason why a toolchain got added to the registry.
  ///
  /// Used to determine the default toolchain. For example, a toolchain discoverd by the `SOURCEKIT_TOOLCHAIN_PATH`
  /// environment variable always takes precedence.
  private enum ToolchainRegisterReason: Comparable {
    /// The toolchain was found because of the `SOURCEKIT_TOOLCHAIN_PATH` environment variable (or equivalent if
    /// overridden in `ToolchainRegistry.init`).
    case sourcekitToolchainEnvironmentVariable

    /// The toolchain was found relative to the location where sourcekit-lsp is installed.
    case relativeToInstallPath

    /// The toolchain was found in an Xcode installation
    case xcode

    /// The toolchain was found relative to the `SOURCEKIT_PATH` or `PATH` environment variables.
    case pathEnvironmentVariable
  }

  /// The toolchains and the reasons why they were added to the registry.s
  private let toolchainsAndReasons: [(toolchain: Toolchain, reason: ToolchainRegisterReason)]

  /// The toolchains, in the order they were registered.
  public var toolchains: [Toolchain] {
    return toolchainsAndReasons.map(\.toolchain)
  }

  /// The toolchains indexed by their identifier.
  ///
  /// Multiple toolchains may exist for the XcodeDefault toolchain identifier.
  private let toolchainsByIdentifier: [String: [Toolchain]]

  /// The toolchains indexed by their path.
  ///
  /// Note: Not all toolchains have a path.
  private let toolchainsByPath: [AbsolutePath: Toolchain]

  /// The currently selected toolchain identifier on Darwin.
  public let darwinToolchainOverride: String?

  /// Create a toolchain registry with a pre-defined list of toolchains.
  ///
  /// For testing purposes only.
  @_spi(Testing)
  public init(toolchains: [Toolchain]) {
    self.init(
      toolchainsAndReasons: toolchains.map { ($0, .xcode) },
      darwinToolchainOverride: nil
    )
  }

  /// Creates a toolchain registry from a list of toolchains.
  ///
  /// - Parameters:
  ///   - toolchainsAndReasons: The toolchains that should be stored in the registry and why they should be added.
  ///   - darwinToolchainOverride: The contents of the `TOOLCHAINS` environment
  ///     variable, which picks the default toolchain.
  private init(
    toolchainsAndReasons toolchainsAndReasonsParam: [(toolchain: Toolchain, reason: ToolchainRegisterReason)],
    darwinToolchainOverride: String?
  ) {
    var toolchainsAndReasons: [(toolchain: Toolchain, reason: ToolchainRegisterReason)] = []
    var toolchainsByIdentifier: [String: [Toolchain]] = [:]
    var toolchainsByPath: [AbsolutePath: Toolchain] = [:]
    for (toolchain, reason) in toolchainsAndReasonsParam {
      // Non-XcodeDefault toolchain: disallow all duplicates.
      if toolchain.identifier != ToolchainRegistry.darwinDefaultToolchainIdentifier {
        guard toolchainsByIdentifier[toolchain.identifier] == nil else {
          continue
        }
      }

      // Toolchain should always be unique by path if it is present.
      if let path = toolchain.path {
        guard toolchainsByPath[path] == nil else {
          continue
        }
        toolchainsByPath[path] = toolchain
      }

      toolchainsByIdentifier[toolchain.identifier, default: []].append(toolchain)
      toolchainsAndReasons.append((toolchain, reason))
    }

    self.toolchainsAndReasons = toolchainsAndReasons
    self.toolchainsByIdentifier = toolchainsByIdentifier
    self.toolchainsByPath = toolchainsByPath

    if let darwinToolchainOverride, !darwinToolchainOverride.isEmpty, darwinToolchainOverride != "default" {
      self.darwinToolchainOverride = darwinToolchainOverride
    } else {
      self.darwinToolchainOverride = nil
    }
  }

  /// A toolchain registry used for testing that scans for toolchains based on environment variables and Xcode
  /// installations but not next to the `sourcekit-lsp` binary because there is no `sourcekit-lsp` binary during
  /// testing.
  @_spi(Testing)
  public static var forTesting: ToolchainRegistry {
    ToolchainRegistry(localFileSystem)
  }

  /// Creates a toolchain registry populated by scanning for toolchains according to the given paths
  /// and variables.
  ///
  /// If called with the default values, creates a toolchain registry that searches:
  /// * `env SOURCEKIT_TOOLCHAIN_PATH` <-- will override default toolchain
  /// * `installPath` <-- will override default toolchain
  /// * (Darwin) The currently selected Xcode
  /// * (Darwin) `[~]/Library/Developer/Toolchains`
  /// * `env SOURCEKIT_PATH, PATH`
  public init(
    installPath: AbsolutePath? = nil,
    environmentVariables: [ProcessEnvironmentKey] = ["SOURCEKIT_TOOLCHAIN_PATH"],
    xcodes: [AbsolutePath] = [_currentXcodeDeveloperPath].compactMap({ $0 }),
    darwinToolchainOverride: String? = ProcessEnv.block["TOOLCHAINS"],
    _ fileSystem: FileSystem = localFileSystem
  ) {
    // The paths at which we have found toolchains
    var toolchainPaths: [(path: AbsolutePath, reason: ToolchainRegisterReason)] = []

    // Scan for toolchains in the paths given by `environmentVariables`.
    for envVar in environmentVariables {
      if let pathStr = ProcessEnv.block[envVar], let path = try? AbsolutePath(validating: pathStr) {
        toolchainPaths.append((path, .sourcekitToolchainEnvironmentVariable))
      }
    }

    // Search for toolchains relative to the path at which sourcekit-lsp is installed.
    if let installPath = installPath {
      toolchainPaths.append((installPath, .relativeToInstallPath))
    }

    // Search for toolchains in the Xcode developer directories and global toolchain install paths
    let toolchainSearchPaths =
      xcodes.map {
        if $0.extension == "app" {
          return $0.appending(components: "Contents", "Developer", "Toolchains")
        } else {
          return $0.appending(component: "Toolchains")
        }
      } + [
        try! AbsolutePath(expandingTilde: "~/Library/Developer/Toolchains"),
        try! AbsolutePath(validating: "/Library/Developer/Toolchains"),
      ]

    for xctoolchainSearchPath in toolchainSearchPaths {
      guard let direntries = try? fileSystem.getDirectoryContents(xctoolchainSearchPath) else {
        continue
      }
      for name in direntries {
        let path = xctoolchainSearchPath.appending(component: name)
        if path.extension == "xctoolchain" {
          toolchainPaths.append((path, .xcode))
        }
      }
    }

    // Scan for toolchains by the given PATH-like environment variables.
    for envVar: ProcessEnvironmentKey in ["SOURCEKIT_PATH", "PATH", "Path"] {
      for path in getEnvSearchPaths(pathString: ProcessEnv.block[envVar], currentWorkingDirectory: nil) {
        toolchainPaths.append((path, .pathEnvironmentVariable))
      }
    }

    let toolchainsAndReasons = toolchainPaths.compactMap {
      if let toolchain = Toolchain($0.path, fileSystem) {
        return (toolchain, $0.reason)
      }
      return nil
    }
    self.init(toolchainsAndReasons: toolchainsAndReasons, darwinToolchainOverride: darwinToolchainOverride)
  }

  /// The default toolchain.
  ///
  /// On Darwin, this is typically the toolchain with the identifier `darwinToolchainIdentifier`,
  /// i.e. the default toolchain of the active Xcode. Otherwise it is the first toolchain that was
  /// registered, if any.
  ///
  /// The default toolchain must be only of the registered toolchains.
  public var `default`: Toolchain? {
    get {
      // Toolchains discovered from the `SOURCEKIT_TOOLCHAIN_PATH` environment variable or relative to sourcekit-lsp's
      // install path always take precedence over Xcode toolchains.
      if let (toolchain, reason) = toolchainsAndReasons.first, reason < .xcode {
        return toolchain
      }
      // Try finding the Xcode default toolchain.
      if let tc = toolchainsByIdentifier[darwinToolchainIdentifier]?.first {
        return tc
      }
      var result: Toolchain? = nil
      for toolchain in toolchains {
        if result == nil || toolchain.isProperSuperset(of: result!) {
          result = toolchain
        }
      }
      return result
    }
  }

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

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

  /// Returns the preferred toolchain that contains all the tools at the given key paths.
  public func preferredToolchain(containing requiredTools: [KeyPath<Toolchain, AbsolutePath?>]) -> Toolchain? {
    if let toolchain = self.default, requiredTools.allSatisfy({ toolchain[keyPath: $0] != nil }) {
      return toolchain
    }

    for toolchain in toolchains {
      if requiredTools.allSatisfy({ toolchain[keyPath: $0] != nil }) {
        return toolchain
      }
    }

    return nil
  }
}

/// Inspecting internal state for testing purposes.
extension ToolchainRegistry {
  @_spi(Testing)
  public func toolchains(identifier: String) -> [Toolchain] {
    return toolchainsByIdentifier[identifier] ?? []
  }

  @_spi(Testing)
  public func toolchain(identifier: String) -> Toolchain? {
    return toolchains(identifier: identifier).first
  }

  @_spi(Testing)
  public func toolchain(path: AbsolutePath) -> Toolchain? {
    return toolchainsByPath[path]
  }
}

extension ToolchainRegistry {
  /// The path of the current Xcode.app/Contents/Developer.
  public static var _currentXcodeDeveloperPath: AbsolutePath? {
    guard let str = try? Process.checkNonZeroExit(args: "/usr/bin/xcode-select", "-p") else { return nil }
    return try? AbsolutePath(validating: str.trimmingCharacters(in: .whitespacesAndNewlines))
  }
}