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
|
//===----------------------------------------------------------------------===//
//
// 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 LanguageServerProtocolExtensions
import SwiftExtensions
import TSCExtensions
#if compiler(>=6)
package import Foundation
package import class TSCBasic.Process
package import enum TSCBasic.ProcessEnv
package import struct TSCBasic.ProcessEnvironmentKey
package import func TSCBasic.getEnvSearchPaths
#else
import Foundation
import struct TSCBasic.AbsolutePath
import class TSCBasic.Process
import enum TSCBasic.ProcessEnv
import struct TSCBasic.ProcessEnvironmentKey
import func TSCBasic.getEnvSearchPaths
#endif
/// 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)`.
package 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.
package 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: [URL: Toolchain]
/// The currently selected toolchain identifier on Darwin.
package let darwinToolchainOverride: String?
/// Create a toolchain registry with a pre-defined list of toolchains.
///
/// For testing purposes only.
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: [URL: 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.
package static var forTesting: ToolchainRegistry {
ToolchainRegistry()
}
/// 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`
package init(
installPath: URL? = nil,
environmentVariables: [ProcessEnvironmentKey] = ["SOURCEKIT_TOOLCHAIN_PATH"],
xcodes: [URL] = [_currentXcodeDeveloperPath].compactMap({ $0 }),
libraryDirectories: [URL] = FileManager.default.urls(for: .libraryDirectory, in: .allDomainsMask),
pathEnvironmentVariables: [ProcessEnvironmentKey] = ["SOURCEKIT_PATH", "PATH"],
darwinToolchainOverride: String? = ProcessEnv.block["TOOLCHAINS"]
) {
// The paths at which we have found toolchains
var toolchainPaths: [(path: URL, reason: ToolchainRegisterReason)] = []
// Scan for toolchains in the paths given by `environmentVariables`.
for envVar in environmentVariables {
if let pathStr = ProcessEnv.block[envVar] {
toolchainPaths.append((URL(fileURLWithPath: pathStr), .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
var toolchainSearchPaths =
xcodes.map {
if $0.pathExtension == "app" {
return $0.appendingPathComponent("Contents").appendingPathComponent("Developer").appendingPathComponent(
"Toolchains"
)
} else {
return $0.appendingPathComponent("Toolchains")
}
}
toolchainSearchPaths += libraryDirectories.compactMap {
$0.appendingPathComponent("Developer").appendingPathComponent("Toolchains")
}
for xctoolchainSearchPath in toolchainSearchPaths {
let entries =
(try? FileManager.default.contentsOfDirectory(at: xctoolchainSearchPath, includingPropertiesForKeys: nil)) ?? []
for entry in entries {
if entry.pathExtension == "xctoolchain" {
toolchainPaths.append((entry, .xcode))
}
}
}
// Scan for toolchains by the given PATH-like environment variables.
for envVar: ProcessEnvironmentKey in pathEnvironmentVariables {
for path in getEnvSearchPaths(pathString: ProcessEnv.block[envVar], currentWorkingDirectory: nil) {
toolchainPaths.append((path.asURL, .pathEnvironmentVariable))
}
}
let toolchainsAndReasons = toolchainPaths.compactMap {
if let toolchain = Toolchain($0.path) {
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.
package 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.
package 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.
package var darwinToolchainIdentifier: String {
return darwinToolchainOverride ?? ToolchainRegistry.darwinDefaultToolchainIdentifier
}
/// Returns the preferred toolchain that contains all the tools at the given key paths.
package func preferredToolchain(containing requiredTools: [KeyPath<Toolchain, URL?>]) -> 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 {
package func toolchains(withIdentifier identifier: String) -> [Toolchain] {
return toolchainsByIdentifier[identifier] ?? []
}
package func toolchain(withPath path: URL) -> Toolchain? {
return toolchainsByPath[path]
}
}
extension ToolchainRegistry {
/// The path of the current Xcode.app/Contents/Developer.
package static var _currentXcodeDeveloperPath: URL? {
guard let str = try? Process.checkNonZeroExit(args: "/usr/bin/xcode-select", "-p") else { return nil }
return URL(fileURLWithPath: str.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
|