|
12 | 12 |
|
13 | 13 | import LSPLogging
|
14 | 14 | import LanguageServerProtocol
|
| 15 | +import RegexBuilder |
15 | 16 | import SKSupport
|
16 | 17 |
|
17 | 18 | import enum PackageLoading.Platform
|
18 | 19 | import struct TSCBasic.AbsolutePath
|
19 | 20 | import protocol TSCBasic.FileSystem
|
| 21 | +import class TSCBasic.Process |
20 | 22 | import var TSCBasic.localFileSystem
|
21 | 23 |
|
| 24 | +/// A Swift version consisting of the major and minor component. |
| 25 | +public struct SwiftVersion: Sendable, Comparable, CustomStringConvertible { |
| 26 | + public let major: Int |
| 27 | + public let minor: Int |
| 28 | + |
| 29 | + public static func < (lhs: SwiftVersion, rhs: SwiftVersion) -> Bool { |
| 30 | + return (lhs.major, lhs.minor) < (rhs.major, rhs.minor) |
| 31 | + } |
| 32 | + |
| 33 | + public init(_ major: Int, _ minor: Int) { |
| 34 | + self.major = major |
| 35 | + self.minor = minor |
| 36 | + } |
| 37 | + |
| 38 | + public var description: String { |
| 39 | + return "\(major).\(minor)" |
| 40 | + } |
| 41 | +} |
| 42 | + |
| 43 | +fileprivate enum SwiftVersionParsingError: Error, CustomStringConvertible { |
| 44 | + case failedToFindSwiftc |
| 45 | + case failedToParseOutput(output: String?) |
| 46 | + |
| 47 | + var description: String { |
| 48 | + switch self { |
| 49 | + case .failedToFindSwiftc: |
| 50 | + return "Default toolchain does not contain a swiftc executable" |
| 51 | + case .failedToParseOutput(let output): |
| 52 | + return """ |
| 53 | + Failed to parse Swift version. Output of swift --version: |
| 54 | + \(output ?? "<empty>") |
| 55 | + """ |
| 56 | + } |
| 57 | + } |
| 58 | +} |
| 59 | + |
22 | 60 | /// A Toolchain is a collection of related compilers and libraries meant to be used together to
|
23 | 61 | /// build and edit source code.
|
24 | 62 | ///
|
@@ -63,6 +101,47 @@ public final class Toolchain: Sendable {
|
63 | 101 | /// The path to the indexstore library if available.
|
64 | 102 | public let libIndexStore: AbsolutePath?
|
65 | 103 |
|
| 104 | + private let swiftVersionTask = ThreadSafeBox<Task<SwiftVersion, any Error>?>(initialValue: nil) |
| 105 | + |
| 106 | + /// The Swift version installed in the toolchain. Throws an error if the version could not be parsed or if no Swift |
| 107 | + /// compiler is installed in the toolchain. |
| 108 | + public var swiftVersion: SwiftVersion { |
| 109 | + get async throws { |
| 110 | + let task = swiftVersionTask.withLock { task in |
| 111 | + if let task { |
| 112 | + return task |
| 113 | + } |
| 114 | + let newTask = Task { () -> SwiftVersion in |
| 115 | + guard let swiftc else { |
| 116 | + throw SwiftVersionParsingError.failedToFindSwiftc |
| 117 | + } |
| 118 | + |
| 119 | + let process = Process(args: swiftc.pathString, "--version") |
| 120 | + try process.launch() |
| 121 | + let result = try await process.waitUntilExit() |
| 122 | + let output = String(bytes: try result.output.get(), encoding: .utf8) |
| 123 | + let regex = Regex { |
| 124 | + "Swift version " |
| 125 | + Capture { OneOrMore(.digit) } |
| 126 | + "." |
| 127 | + Capture { OneOrMore(.digit) } |
| 128 | + } |
| 129 | + guard let match = output?.firstMatch(of: regex) else { |
| 130 | + throw SwiftVersionParsingError.failedToParseOutput(output: output) |
| 131 | + } |
| 132 | + guard let major = Int(match.1), let minor = Int(match.2) else { |
| 133 | + throw SwiftVersionParsingError.failedToParseOutput(output: output) |
| 134 | + } |
| 135 | + return SwiftVersion(major, minor) |
| 136 | + } |
| 137 | + task = newTask |
| 138 | + return newTask |
| 139 | + } |
| 140 | + |
| 141 | + return try await task.value |
| 142 | + } |
| 143 | + } |
| 144 | + |
66 | 145 | public init(
|
67 | 146 | identifier: String,
|
68 | 147 | displayName: String,
|
|
0 commit comments