Skip to content

Commit 942bb0a

Browse files
authored
Merge pull request #1366 from ahoppen/fix-modules-include-directory
Support running tests that require building with a Swift 5.10 toolchain
2 parents 83f8287 + 48df054 commit 942bb0a

12 files changed

+105
-96
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/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift

+22-3
Original file line numberDiff line numberDiff line change
@@ -358,11 +358,30 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
358358

359359
public var indexPrefixMappings: [PathPrefixMapping] { return [] }
360360

361+
/// Return the compiler arguments for the given source file within a target, making any necessary adjustments to
362+
/// account for differences in the SwiftPM versions being linked into SwiftPM and being installed in the toolchain.
363+
private func compilerArguments(for file: URL, in buildTarget: any SwiftBuildTarget) async throws -> [String] {
364+
let compileArguments = try buildTarget.compileArguments(for: file)
365+
366+
// Fix up compiler arguments that point to a `/Modules` subdirectory if the Swift version in the toolchain is less
367+
// than 6.0 because it places the modules one level higher up.
368+
let toolchainVersion = await orLog("Getting Swift version") { try await toolchainRegistry.default?.swiftVersion }
369+
guard let toolchainVersion, toolchainVersion < SwiftVersion(6, 0) else {
370+
return compileArguments
371+
}
372+
return compileArguments.map { argument in
373+
if argument.hasSuffix("/Modules"), argument.contains(self.workspace.location.scratchDirectory.pathString) {
374+
return String(argument.dropLast(8))
375+
}
376+
return argument
377+
}
378+
}
379+
361380
public func buildSettings(
362381
for uri: DocumentURI,
363382
in configuredTarget: ConfiguredTarget,
364383
language: Language
365-
) throws -> FileBuildSettings? {
384+
) async throws -> FileBuildSettings? {
366385
guard let url = uri.fileURL, let path = try? AbsolutePath(validating: url.path) else {
367386
// We can't determine build settings for non-file URIs.
368387
return nil
@@ -388,13 +407,13 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
388407
// getting its compiler arguments and then patching up the compiler arguments by replacing the substitute file
389408
// with the `.cpp` file.
390409
return FileBuildSettings(
391-
compilerArguments: try buildTarget.compileArguments(for: substituteFile),
410+
compilerArguments: try await compilerArguments(for: substituteFile, in: buildTarget),
392411
workingDirectory: workspacePath.pathString
393412
).patching(newFile: try resolveSymlinks(path).pathString, originalFile: substituteFile.absoluteString)
394413
}
395414

396415
return FileBuildSettings(
397-
compilerArguments: try buildTarget.compileArguments(for: url),
416+
compilerArguments: try await compilerArguments(for: url, in: buildTarget),
398417
workingDirectory: workspacePath.pathString
399418
)
400419
}

Sources/SKTestSupport/SkipUnless.swift

+1-66
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(
@@ -174,10 +170,6 @@ public actor SkipUnless {
174170

175171
/// SwiftPM moved the location where it stores Swift modules to a subdirectory in
176172
/// https://github.com/apple/swift-package-manager/pull/7103.
177-
///
178-
/// sourcekit-lsp uses the built-in SwiftPM to synthesize compiler arguments and cross-module tests fail if the host
179-
/// toolchain’s SwiftPM stores the Swift modules on the top level but we synthesize compiler arguments expecting the
180-
/// modules to be in a `Modules` subdirectory.
181173
public static func swiftpmStoresModulesInSubdirectory(
182174
file: StaticString = #filePath,
183175
line: UInt = #line
@@ -297,60 +289,3 @@ fileprivate extension String {
297289
}
298290
}
299291
}
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-
}

Tests/SKSwiftPMWorkspaceTests/SwiftPMBuildSystemTests.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ import struct PackageModel.BuildFlags
2828
#endif
2929

3030
fileprivate extension SwiftPMBuildSystem {
31-
func buildSettings(for uri: DocumentURI, language: Language) throws -> FileBuildSettings? {
31+
func buildSettings(for uri: DocumentURI, language: Language) async throws -> FileBuildSettings? {
3232
guard let target = self.configuredTargets(for: uri).only else {
3333
return nil
3434
}
35-
return try buildSettings(for: uri, in: target, language: language)
35+
return try await buildSettings(for: uri, in: target, language: language)
3636
}
3737
}
3838

@@ -116,6 +116,7 @@ final class SwiftPMBuildSystemTests: XCTestCase {
116116
}
117117

118118
func testBasicSwiftArgs() async throws {
119+
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
119120
let fs = localFileSystem
120121
try await withTestScratchDir { tempDir in
121122
try fs.createFiles(

Tests/SourceKitLSPTests/BackgroundIndexingTests.swift

-6
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ final class BackgroundIndexingTests: XCTestCase {
104104
}
105105

106106
func testBackgroundIndexingOfMultiModuleProject() async throws {
107-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
108107
let project = try await SwiftPMTestProject(
109108
files: [
110109
"LibA/MyFile.swift": """
@@ -212,7 +211,6 @@ final class BackgroundIndexingTests: XCTestCase {
212211
}
213212

214213
func testBackgroundIndexingOfPackageDependency() async throws {
215-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
216214
let dependencyContents = """
217215
public func 1️⃣doSomething() {}
218216
"""
@@ -541,7 +539,6 @@ final class BackgroundIndexingTests: XCTestCase {
541539
}
542540

543541
func testPrepareTargetAfterEditToDependency() async throws {
544-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
545542
var serverOptions = SourceKitLSPServer.Options.testDefault
546543
let expectedPreparationTracker = ExpectedIndexTaskTracker(expectedPreparations: [
547544
[
@@ -652,7 +649,6 @@ final class BackgroundIndexingTests: XCTestCase {
652649
let libBStartedPreparation = self.expectation(description: "LibB started preparing")
653650
let libDPreparedForEditing = self.expectation(description: "LibD prepared for editing")
654651

655-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
656652
var serverOptions = SourceKitLSPServer.Options.testDefault
657653
let expectedPreparationTracker = ExpectedIndexTaskTracker(expectedPreparations: [
658654
// Preparation of targets during the initial of the target
@@ -750,8 +746,6 @@ final class BackgroundIndexingTests: XCTestCase {
750746
}
751747

752748
func testIndexingHappensInParallel() async throws {
753-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
754-
755749
let fileAIndexingStarted = self.expectation(description: "FileA indexing started")
756750
let fileBIndexingStarted = self.expectation(description: "FileB indexing started")
757751

Tests/SourceKitLSPTests/DefinitionTests.swift

-1
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,6 @@ class DefinitionTests: XCTestCase {
440440

441441
func testDependentModuleGotBuilt() async throws {
442442
try SkipUnless.longTestsEnabled()
443-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
444443
let project = try await SwiftPMTestProject(
445444
files: [
446445
"LibA/LibA.swift": """

Tests/SourceKitLSPTests/DependencyTrackingTests.swift

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import XCTest
1616

1717
final class DependencyTrackingTests: XCTestCase {
1818
func testDependenciesUpdatedSwift() async throws {
19-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
2019
let project = try await SwiftPMTestProject(
2120
files: [
2221
"LibA/LibA.swift": """

Tests/SourceKitLSPTests/IndexTests.swift

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import XCTest
1616

1717
final class IndexTests: XCTestCase {
1818
func testIndexSwiftModules() async throws {
19-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
2019
let project = try await SwiftPMTestProject(
2120
files: [
2221
"LibA/LibA.swift": """

Tests/SourceKitLSPTests/PublishDiagnosticsTests.swift

-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,6 @@ final class PublishDiagnosticsTests: XCTestCase {
154154

155155
func testDiagnosticUpdatedAfterDependentModuleIsBuilt() async throws {
156156
try SkipUnless.longTestsEnabled()
157-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
158157

159158
let project = try await SwiftPMTestProject(
160159
files: [

Tests/SourceKitLSPTests/PullDiagnosticsTests.swift

-3
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,6 @@ final class PullDiagnosticsTests: XCTestCase {
185185

186186
func testDiagnosticUpdatedAfterDependentModuleIsBuilt() async throws {
187187
try SkipUnless.longTestsEnabled()
188-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
189188

190189
let project = try await SwiftPMTestProject(
191190
files: [
@@ -251,8 +250,6 @@ final class PullDiagnosticsTests: XCTestCase {
251250
}
252251

253252
func testDiagnosticsWaitForDocumentToBePrepared() async throws {
254-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
255-
256253
nonisolated(unsafe) var diagnosticRequestSent = AtomicBool(initialValue: false)
257254
var serverOptions = SourceKitLSPServer.Options.testDefault
258255
serverOptions.indexTestHooks.preparationTaskDidStart = { @Sendable taskDescription in

Tests/SourceKitLSPTests/SwiftInterfaceTests.swift

-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ final class SwiftInterfaceTests: XCTestCase {
5555
}
5656

5757
func testOpenInterface() async throws {
58-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
5958
let project = try await SwiftPMTestProject(
6059
files: [
6160
"MyLibrary/MyLibrary.swift": """
@@ -152,7 +151,6 @@ final class SwiftInterfaceTests: XCTestCase {
152151
}
153152

154153
func testSwiftInterfaceAcrossModules() async throws {
155-
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
156154
let project = try await SwiftPMTestProject(
157155
files: [
158156
"MyLibrary/MyLibrary.swift": """

0 commit comments

Comments
 (0)