Skip to content

Commit 129d3a4

Browse files
committed
Move Swift compiler version parsing from SkipUnless.swift to Toolchain
This allows us to inspect the Swift version and fix up compiler arguments from SwiftPM to account for an older version of SwiftPM being invoked using `swift build` than the one that’s bundled in-process with SourceKit-LSP.
1 parent 715796f commit 129d3a4

File tree

2 files changed

+80
-62
lines changed

2 files changed

+80
-62
lines changed

Sources/SKCore/Toolchain.swift

+79
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,51 @@
1212

1313
import LSPLogging
1414
import LanguageServerProtocol
15+
import RegexBuilder
1516
import SKSupport
1617

1718
import enum PackageLoading.Platform
1819
import struct TSCBasic.AbsolutePath
1920
import protocol TSCBasic.FileSystem
21+
import class TSCBasic.Process
2022
import var TSCBasic.localFileSystem
2123

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+
2260
/// A Toolchain is a collection of related compilers and libraries meant to be used together to
2361
/// build and edit source code.
2462
///
@@ -63,6 +101,47 @@ public final class Toolchain: Sendable {
63101
/// The path to the indexstore library if available.
64102
public let libIndexStore: AbsolutePath?
65103

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+
66145
public init(
67146
identifier: String,
68147
displayName: String,

Sources/SKTestSupport/SkipUnless.swift

+1-62
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,7 @@ public actor SkipUnless {
6565
// Never skip tests in CI. Toolchain should be up-to-date
6666
checkResult = .featureSupported
6767
} else {
68-
guard let swiftc = await ToolchainRegistry.forTesting.default?.swiftc else {
69-
throw SwiftVersionParsingError.failedToFindSwiftc
70-
}
71-
72-
let toolchainSwiftVersion = try await getSwiftVersion(swiftc)
68+
let toolchainSwiftVersion = try await unwrap(ToolchainRegistry.forTesting.default).swiftVersion
7369
let requiredSwiftVersion = SwiftVersion(swiftVersion.major, swiftVersion.minor)
7470
if toolchainSwiftVersion < requiredSwiftVersion {
7571
checkResult = .featureUnsupported(
@@ -297,60 +293,3 @@ fileprivate extension String {
297293
}
298294
}
299295
}
300-
301-
/// A Swift version consisting of the major and minor component.
302-
fileprivate struct SwiftVersion: Comparable, CustomStringConvertible {
303-
let major: Int
304-
let minor: Int
305-
306-
static func < (lhs: SwiftVersion, rhs: SwiftVersion) -> Bool {
307-
return (lhs.major, lhs.minor) < (rhs.major, rhs.minor)
308-
}
309-
310-
init(_ major: Int, _ minor: Int) {
311-
self.major = major
312-
self.minor = minor
313-
}
314-
315-
var description: String {
316-
return "\(major).\(minor)"
317-
}
318-
}
319-
320-
fileprivate enum SwiftVersionParsingError: Error, CustomStringConvertible {
321-
case failedToFindSwiftc
322-
case failedToParseOutput(output: String?)
323-
324-
var description: String {
325-
switch self {
326-
case .failedToFindSwiftc:
327-
return "Default toolchain does not contain a swiftc executable"
328-
case .failedToParseOutput(let output):
329-
return """
330-
Failed to parse Swift version. Output of swift --version:
331-
\(output ?? "<empty>")
332-
"""
333-
}
334-
}
335-
}
336-
337-
/// Return the major and minor version of Swift for a `swiftc` compiler at `swiftcPath`.
338-
private func getSwiftVersion(_ swiftcPath: AbsolutePath) async throws -> SwiftVersion {
339-
let process = Process(args: swiftcPath.pathString, "--version")
340-
try process.launch()
341-
let result = try await process.waitUntilExit()
342-
let output = String(bytes: try result.output.get(), encoding: .utf8)
343-
let regex = Regex {
344-
"Swift version "
345-
Capture { OneOrMore(.digit) }
346-
"."
347-
Capture { OneOrMore(.digit) }
348-
}
349-
guard let match = output?.firstMatch(of: regex) else {
350-
throw SwiftVersionParsingError.failedToParseOutput(output: output)
351-
}
352-
guard let major = Int(match.1), let minor = Int(match.2) else {
353-
throw SwiftVersionParsingError.failedToParseOutput(output: output)
354-
}
355-
return SwiftVersion(major, minor)
356-
}

0 commit comments

Comments
 (0)