Files
sourcekit-lsp/Tests/BuildServerIntegrationTests/SwiftPMBuildServerTests.swift
T
Bri Peticca da0c9a9ad9 Fix spm API usage for appropriate trait configuration
The `WorkspaceConfiguration.default` was being used to populate
the `Workspace`; there was an extra step here needed to assure that
we are propagating the trait configuration to the workspace.

The added test assures that non-default traits that are enabled are
indeed processed as enabled.
2026-05-27 12:57:16 -04:00

1751 lines
60 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 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
//
//===----------------------------------------------------------------------===//
#if !NO_SWIFTPM_DEPENDENCY
@_spi(SourceKitLSP) import BuildServerProtocol
@_spi(Testing) import BuildServerIntegration
@_spi(SourceKitLSP) import LanguageServerProtocol
@_spi(SourceKitLSP) import LanguageServerProtocolExtensions
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions
@_spi(SourceKitLSP) import LanguageServerProtocolTransport
import PackageModel
import SKLogging
import SKOptions
import SKTestSupport
import SourceKitLSP
@preconcurrency import SPMBuildCore
import SwiftExtensions
import TSCBasic
import TSCExtensions
import ToolchainRegistry
import Foundation
import Testing
import struct Basics.AbsolutePath
import struct Basics.Triple
import struct Basics.UniversalArchiver
private var hostTriple: Triple {
get async throws {
let toolchain = try #require(
await ToolchainRegistry.forTesting.preferredToolchain(containing: [
\.clang, \.clangd, \.sourcekitd, \.swift, \.swiftc,
])
)
let destinationToolchainBinDir = try #require(toolchain.swiftc?.deletingLastPathComponent())
let hostSDK = try SwiftSDK.hostSwiftSDK(Basics.AbsolutePath(validating: destinationToolchainBinDir.filePath))
let hostSwiftPMToolchain = try UserToolchain(swiftSDK: hostSDK)
return hostSwiftPMToolchain.targetTriple
}
}
fileprivate extension SourceKitLSPOptions {
static var forTestingExperimentalSwiftPMBuildServer: Self {
SourceKitLSPOptions(swiftPM: SwiftPMOptions(buildSystem: .swiftbuild))
}
}
let swiftPMHasExperimentalBuildServer: Bool = {
@Sendable func impl() async throws -> Bool {
if ProcessEnv.block["SWIFTCI_USE_LOCAL_DEPS"] != nil {
// In general, don't skip tests in CI. Toolchain should be up-to-date.
return false
}
let swift = try await unwrap(ToolchainRegistry.forTesting.default?.swift).deletingLastPathComponent()
.appending(component: "swift")
let output = try await Process.run(
arguments: [
try swift.filePath,
"package",
"experimental-build-server",
"--help",
],
// "swift package experimental-build-server --help" times out without a working directory. Set a nonsensical
// working directory to make it finish faster.
workingDirectory: TSCBasic.AbsolutePath(validating: testScratchDir().filePath)
)
return try output.utf8stderrOutput().contains("Usage: swift package experimental-build-server")
}
// We need to decide whether to run the experimental build server tests synchronously. If more tests start relying on
// this, we need to find a better solution.
nonisolated(unsafe) var result: Result<Bool, any Error>!
let sema = WrappedSemaphore(name: "swiftPMHasExperimentalBuildServer")
let task = Task { @Sendable in
do {
result = .success(try await impl())
} catch {
result = .failure(error)
}
sema.signal()
}
try! sema.waitOrThrow()
return try! result.get()
}()
@Suite(.serialized, .configureLogging)
struct SwiftPMBuildServerTests {
@Test
func testNoPackage() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": ""
]
)
let packageRoot = tempDir.appending(component: "pkg")
let buildServerSpec = SwiftPMBuildServer.searchForConfig(in: packageRoot, options: try await .testDefault())
#expect(buildServerSpec == nil)
}
}
@Test
func testNoToolchain() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
await expectThrowsError(
try await SwiftPMBuildServer(
projectRoot: packageRoot,
toolchainRegistry: ToolchainRegistry(toolchains: []),
options: SourceKitLSPOptions(),
connectionToSourceKitLSP: LocalConnection(receiverName: "dummy"),
testHooks: SwiftPMTestHooks()
)
)
}
}
@Test
func testRelativeScratchPath() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let options = SourceKitLSPOptions(
swiftPM: .init(
scratchPath: "non_default_relative_build_path"
),
backgroundIndexing: false
)
let swiftpmBuildServer = try await SwiftPMBuildServer(
projectRoot: packageRoot,
toolchainRegistry: .forTesting,
options: options,
connectionToSourceKitLSP: LocalConnection(receiverName: "dummy"),
testHooks: SwiftPMTestHooks()
)
let dataPath = await swiftpmBuildServer.destinationBuildParameters.dataPath
let expectedScratchPath = packageRoot.appending(component: try #require(options.swiftPMOrDefault.scratchPath))
#expect(dataPath.asURL.isDescendant(of: expectedScratchPath))
}
}
@Test(
arguments: swiftPMHasExperimentalBuildServer
? [SourceKitLSPOptions(), .forTestingExperimentalSwiftPMBuildServer] : [SourceKitLSPOptions()]
)
func testBasicSwiftArgs(options: SourceKitLSPOptions) async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:5.8
import PackageDescription
let package = Package(
name: "a",
platforms: [.macOS(.v13)],
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: options,
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "lib", "a.swift")
let build = try await buildPath(root: packageRoot, platform: hostTriple.platformBuildPathComponent)
_ = try #require(await buildServerManager.initializationData?.indexDatabasePath)
_ = try #require(await buildServerManager.initializationData?.indexStorePath)
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain("-module-name", "lib", arguments: arguments)
expectArgumentsContain("-parse-as-library", arguments: arguments)
expectArgumentsContain("-target", arguments: arguments) // Only one!
#if os(macOS)
let versionString = "13.0" // matches the explicit platforms: [.macOS(.v13)] above
if options.swiftPMOrDefault.buildSystem == .swiftbuild {
expectArgumentsContain(
"-target",
// Account for differences in macOS naming canonicalization
try await hostTriple.tripleString(forPlatformVersion: versionString).replacing("macosx", with: "macos"),
arguments: arguments
)
} else {
expectArgumentsContain(
"-target",
try await hostTriple.tripleString(forPlatformVersion: versionString),
arguments: arguments
)
}
expectArgumentsContain(
"-sdk",
arguments: arguments,
allowMultiple: options.swiftPMOrDefault.buildSystem == .swiftbuild
)
expectArgumentsContain("-F", arguments: arguments, allowMultiple: true)
#else
expectArgumentsContain("-target", try await hostTriple.tripleString, arguments: arguments)
#endif
if options.swiftPMOrDefault.buildSystem != .swiftbuild {
// Swift Build and the native build system setup search paths differently. We deliberately avoid testing implementation details of Swift Build here.
expectArgumentsContain("-I", try build.appending(component: "Modules").filePath, arguments: arguments)
}
expectArgumentsContain(try aswift.filePath, arguments: arguments)
}
}
@Test
func testCompilerArgumentsForFileThatContainsPlusCharacterURLEncoded() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Sources/lib/a+something.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aPlusSomething =
packageRoot
.appending(components: "Sources", "lib", "a+something.swift")
_ = try #require(await buildServerManager.initializationData?.indexStorePath)
// Simulate an LSP client that percent-encodes `+` as `%2B` in file URIs.
let urlWithPlusEscaped = try #require(
URL(string: aPlusSomething.absoluteString.replacing("+", with: "%2B"))
)
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(urlWithPlusEscaped),
language: .swift,
fallbackAfterTimeout: false
)
)
.compilerArguments
// Check that we have both source files in the compiler arguments, which means that we didn't compute the compiler
// arguments for a+something.swift using substitute arguments from a.swift.
#expect(
try arguments.contains(aPlusSomething.filePath),
"Compiler arguments do not contain a+something.swift: \(arguments)"
)
#expect(
try arguments.contains(
packageRoot.appending(components: "Sources", "lib", "a.swift")
.filePath
),
"Compiler arguments do not contain a.swift: \(arguments)"
)
}
}
@Test
func testBuildSetup() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let options = SourceKitLSPOptions.SwiftPMOptions(
configuration: .release,
scratchPath: try packageRoot.appending(component: "non_default_build_path").filePath,
cCompilerFlags: ["-m32"],
swiftCompilerFlags: ["-typecheck"]
)
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(swiftPM: options),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "lib", "a.swift")
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain("-typecheck", arguments: arguments)
expectArgumentsContain("-Xcc", "-m32", arguments: arguments)
expectArgumentsContain("-O", arguments: arguments)
}
}
@Test
func testManifestArgs() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let source = try packageRoot.appending(component: "Package.swift").realpath
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(source),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain("-swift-version", "4.2", arguments: arguments)
expectArgumentsContain(try source.filePath, arguments: arguments)
}
}
@Test
func testMultiFileSwift() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Sources/lib/b.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "lib", "a.swift")
let bswift =
packageRoot
.appending(components: "Sources", "lib", "b.swift")
let argumentsA = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain(try aswift.filePath, arguments: argumentsA)
expectArgumentsContain(try bswift.filePath, arguments: argumentsA)
let argumentsB = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain(try aswift.filePath, arguments: argumentsB)
expectArgumentsContain(try bswift.filePath, arguments: argumentsB)
}
}
@Test
func testMultiTargetSwift() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/libA/a.swift": "",
"pkg/Sources/libB/b.swift": "",
"pkg/Sources/libC/include/libC.h": "",
"pkg/Sources/libC/libC.c": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [
.target(name: "libA", dependencies: ["libB", "libC"]),
.target(name: "libB"),
.target(name: "libC"),
]
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "libA", "a.swift")
let bswift =
packageRoot
.appending(components: "Sources", "libB", "b.swift")
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain(try aswift.filePath, arguments: arguments)
expectArgumentsDoNotContain(try bswift.filePath, arguments: arguments)
expectArgumentsContain(
"-Xcc",
"-I",
"-Xcc",
try packageRoot
.appending(components: "Sources", "libC", "include")
.filePath,
arguments: arguments
)
let argumentsB = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(bswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain(try bswift.filePath, arguments: argumentsB)
expectArgumentsDoNotContain(try aswift.filePath, arguments: argumentsB)
expectArgumentsDoNotContain(
"-I",
try packageRoot
.appending(components: "Sources", "libC", "include")
.filePath,
arguments: argumentsB
)
}
}
@Test
func testUnknownFile() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/libA/a.swift": "",
"pkg/Sources/libB/b.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "libA")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "libA", "a.swift")
let bswift =
packageRoot
.appending(components: "Sources", "libB", "b.swift")
_ = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
)
#expect(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(bswift),
language: .swift,
fallbackAfterTimeout: false
)?.isFallback == true
)
#expect(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(URL(string: "https://www.apple.com")!),
language: .swift,
fallbackAfterTimeout: false
)?.isFallback == true
)
}
}
@Test
func testBasicCXXArgs() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.cpp": "",
"pkg/Sources/lib/b.cpp": "",
"pkg/Sources/lib/include/a.h": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")],
cxxLanguageStandard: .cxx14
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let acxx =
packageRoot
.appending(components: "Sources", "lib", "a.cpp")
let bcxx =
packageRoot
.appending(components: "Sources", "lib", "b.cpp")
let header =
packageRoot
.appending(components: "Sources", "lib", "include", "a.h")
let build = buildPath(root: packageRoot, platform: try await hostTriple.platformBuildPathComponent)
_ = try #require(await buildServerManager.initializationData?.indexStorePath)
for file in [acxx, header] {
let args = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(file),
language: .cpp,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain("-std=c++14", arguments: args)
expectArgumentsDoNotContain("-arch", arguments: args)
expectArgumentsContain("-target", arguments: args) // Only one!
#if os(macOS)
let versionString = PackageModel.Platform.macOS.oldestSupportedVersion.versionString
expectArgumentsContain(
"-target",
try await hostTriple.tripleString(forPlatformVersion: versionString),
arguments: args
)
expectArgumentsContain("-isysroot", arguments: args)
expectArgumentsContain("-F", arguments: args, allowMultiple: true)
#else
expectArgumentsContain("-target", try await hostTriple.tripleString, arguments: args)
#endif
expectArgumentsContain(
"-I",
try packageRoot
.appending(components: "Sources", "lib", "include")
.filePath,
arguments: args
)
expectArgumentsDoNotContain("-I", try build.filePath, arguments: args)
expectArgumentsDoNotContain(try bcxx.filePath, arguments: args)
URL(fileURLWithPath: try build.appending(components: "lib.build", "a.cpp.o").filePath)
.withUnsafeFileSystemRepresentation {
expectArgumentsContain("-o", String(cString: $0!), arguments: args)
}
}
}
}
@Test
func testDeploymentTargetSwift() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:5.0
import PackageDescription
let package = Package(name: "a",
platforms: [.macOS(.v10_13)],
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "lib", "a.swift")
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain("-target", arguments: arguments) // Only one!
#if os(macOS)
try await expectArgumentsContain(
"-target",
hostTriple.tripleString(forPlatformVersion: "10.13"),
arguments: arguments
)
#else
expectArgumentsContain("-target", try await hostTriple.tripleString, arguments: arguments)
#endif
}
}
@Test
func testSymlinkInWorkspaceSwift() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg_real/Sources/lib/a.swift": "",
"pkg_real/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
try FileManager.default.createSymbolicLink(
at: URL(fileURLWithPath: packageRoot.filePath),
withDestinationURL: URL(fileURLWithPath: tempDir.appending(component: "pkg_real").filePath)
)
let buildServerSpec = try #require(
SwiftPMBuildServer.searchForConfig(in: packageRoot, options: await .testDefault())
)
let buildServerManager = await BuildServerManager(
buildServerSpec: buildServerSpec,
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswiftSymlink =
packageRoot
.appending(components: "Sources", "lib", "a.swift")
let aswiftReal = try aswiftSymlink.realpath
let manifest = packageRoot.appending(component: "Package.swift")
let argumentsFromSymlink = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswiftSymlink),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
// We opened the project from a symlink. The realpath isn't part of the project and we should thus not receive
// build settings for it.
#expect(
try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswiftReal),
language: .swift,
fallbackAfterTimeout: false
)
).isFallback
)
expectArgumentsContain(try aswiftSymlink.filePath, arguments: argumentsFromSymlink)
expectArgumentsDoNotContain(try aswiftReal.filePath, arguments: argumentsFromSymlink)
let argsManifest = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(manifest),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
expectArgumentsContain(try manifest.filePath, arguments: argsManifest)
expectArgumentsDoNotContain(try manifest.realpath.filePath, arguments: argsManifest)
}
}
@Test
func testSymlinkInWorkspaceCXX() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg_real/Sources/lib/a.cpp": "",
"pkg_real/Sources/lib/b.cpp": "",
"pkg_real/Sources/lib/include/a.h": "",
"pkg_real/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")],
cxxLanguageStandard: .cxx14
)
""",
]
)
let acpp = ["Sources", "lib", "a.cpp"]
let ah = ["Sources", "lib", "include", "a.h"]
let realRoot = tempDir.appending(component: "pkg_real")
let symlinkRoot = tempDir.appending(component: "pkg")
try FileManager.default.createSymbolicLink(
at: URL(fileURLWithPath: symlinkRoot.filePath),
withDestinationURL: URL(fileURLWithPath: tempDir.appending(component: "pkg_real").filePath)
)
let buildServerSpec = try #require(
SwiftPMBuildServer.searchForConfig(in: symlinkRoot, options: await .testDefault())
)
let buildServerManager = await BuildServerManager(
buildServerSpec: buildServerSpec,
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
for file in [acpp, ah] {
let args = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(symlinkRoot.appending(components: file)),
language: .cpp,
fallbackAfterTimeout: false
)?
.compilerArguments
)
expectArgumentsDoNotContain(try realRoot.appending(components: file).filePath, arguments: args)
expectArgumentsContain(try symlinkRoot.appending(components: file).filePath, arguments: args)
}
}
}
@Test
func testSwiftDerivedSources() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Sources/lib/a.txt": "",
"pkg/Package.swift": """
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib", resources: [.copy("a.txt")])]
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Sources", "lib", "a.swift")
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
)
.compilerArguments
expectArgumentsContain(try aswift.filePath, arguments: arguments)
_ = try #require(
arguments.firstIndex(where: {
$0.hasSuffix(".swift") && $0.contains("DerivedSources")
}),
"missing resource_bundle_accessor.swift from \(arguments)"
)
}
}
@Test
func testNestedInvalidPackageSwift() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/Package.swift": "// not a valid package",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let workspaceRoot =
tempDir
.appending(components: "pkg", "Sources", "lib")
let buildServerSpec = SwiftPMBuildServer.searchForConfig(in: workspaceRoot, options: try await .testDefault())
#expect(buildServerSpec == nil)
}
}
@Test
func testPluginArgs() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Plugins/MyPlugin/a.swift": "",
"pkg/Sources/lib/lib.swift": "",
"pkg/Package.swift": """
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "a",
targets: [
.target(name: "lib"),
.plugin(name: "MyPlugin", capability: .buildTool)
]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let aswift =
packageRoot
.appending(components: "Plugins", "MyPlugin", "a.swift")
_ = try #require(await buildServerManager.initializationData?.indexStorePath)
let arguments = try #require(
await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(aswift),
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
// Plugins get compiled with the same compiler arguments as the package manifest
expectArgumentsContain("-package-description-version", "5.7.0", arguments: arguments)
expectArgumentsContain(try aswift.filePath, arguments: arguments)
}
}
@Test
func testPackageWithDependencyWithoutResolving() async throws {
// This package has a dependency but we haven't run `swift package resolve`. We don't want to resolve packages from
// SourceKit-LSP because it has side-effects to the build directory.
// But even without the dependency checked out, we should be able to create a SwiftPMBuildServer and retrieve the
// existing source files.
let project = try await SwiftPMTestProject(
files: [
"Tests/PackageTests/PackageTests.swift": """
import Testing
1️⃣@Test func topLevelTestPassing() {}2️⃣
"""
],
manifest: """
let package = Package(
name: "MyLibrary",
dependencies: [.package(url: "https://github.com/swiftlang/swift-testing.git", branch: "main")],
targets: [
.testTarget(name: "PackageTests", dependencies: [.product(name: "Testing", package: "swift-testing")]),
]
)
"""
)
let tests = try await project.testClient.send(WorkspaceTestsRequest())
#expect(
tests == [
TestItem(
id: "PackageTests.topLevelTestPassing()",
label: "topLevelTestPassing()",
disabled: false,
style: "swift-testing",
location: try project.location(from: "1️⃣", to: "2️⃣", in: "PackageTests.swift"),
children: [],
tags: []
)
]
)
}
@Test
func testPackageLoadingWorkDoneProgress() async throws {
let didReceiveWorkDoneProgressNotification = WrappedSemaphore(name: "work done progress received")
let project = try await SwiftPMTestProject(
files: [
"MyLibrary/Test.swift": ""
],
capabilities: ClientCapabilities(window: WindowClientCapabilities(workDoneProgress: true)),
hooks: Hooks(
buildServerHooks: BuildServerHooks(
swiftPMTestHooks: SwiftPMTestHooks(reloadPackageDidStart: {
didReceiveWorkDoneProgressNotification.waitOrXCTFail()
})
)
),
pollIndex: false,
preInitialization: { testClient in
testClient.handleMultipleRequests { (request: CreateWorkDoneProgressRequest) in
return VoidResponse()
}
}
)
let begin = try await project.testClient.nextNotification(ofType: WorkDoneProgress.self)
#expect(begin.value == .begin(WorkDoneProgressBegin(title: "SourceKit-LSP: Reloading Package")))
didReceiveWorkDoneProgressNotification.signal()
let end = try await project.testClient.nextNotification(ofType: WorkDoneProgress.self)
#expect(end.token == begin.token)
#expect(end.value == .end(WorkDoneProgressEnd()))
}
@Test
func testBuildSettingsForVersionSpecificPackageManifest() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
"pkg/Package@swift-5.8.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let versionSpecificManifestURL = packageRoot.appending(component: "Package@swift-5.8.swift")
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let settings = await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(versionSpecificManifestURL),
language: .swift,
fallbackAfterTimeout: false
)
let compilerArgs = try #require(settings?.compilerArguments)
#expect(compilerArgs.contains("-package-description-version"))
#expect(compilerArgs.contains(try versionSpecificManifestURL.filePath))
}
}
@Test
func testBuildSettingsForInvalidManifest() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:5.1
import PackageDescription
""",
]
)
let packageRoot = try tempDir.appending(component: "pkg").realpath
let manifestURL = packageRoot.appending(component: "Package.swift")
let buildServerManager = await BuildServerManager(
buildServerSpec: .swiftpmSpec(for: packageRoot),
toolchainRegistry: .forTesting,
options: SourceKitLSPOptions(),
connectionToClient: DummyBuildServerManagerConnectionToClient(),
buildServerHooks: BuildServerHooks(),
createMainFilesProvider: { _, _ in nil }
)
await buildServerManager.waitForUpToDateBuildGraph()
let settings = await buildServerManager.buildSettingsInferredFromMainFile(
for: DocumentURI(manifestURL),
language: .swift,
fallbackAfterTimeout: false
)
let compilerArgs = try #require(settings?.compilerArguments)
expectArgumentsContain("-package-description-version", "5.1.0", arguments: compilerArgs)
#expect(compilerArgs.contains(try manifestURL.filePath))
}
}
@Test(
.enabled(if: Platform.current != .windows, "Toolsets are not working on Windows, see swift-package-manager#9438.")
)
func testToolsets() async throws {
let project = try await SwiftPMTestProject(
files: [
"Foo/foo.swift": """
import Bar
func foo() {
bar()
}
""",
"Bar/bar.swift": """
#if BAR
public func bar() {}
#endif
""",
"/toolset.json": """
{
"schemaVersion": "1.0",
"swiftCompiler": {
"extraCLIOptions": [
"-DBAR"
]
}
}
""",
"/.sourcekit-lsp/config.json": """
{
"swiftPM": {
"toolsets": ["toolset.json"]
}
}
""",
],
manifest: """
let package = Package(
name: "MyLibrary",
platforms: [.macOS(.v13)],
targets: [
.target(name: "Foo", dependencies: ["Bar"]),
.target(name: "Bar"),
]
)
""",
options: .testDefault(experimentalFeatures: [.sourceKitOptionsRequest]),
enableBackgroundIndexing: true,
)
let (uri, _) = try project.openDocument("foo.swift")
let options = try await project.testClient.send(
SourceKitOptionsRequest(
textDocument: TextDocumentIdentifier(uri),
prepareTarget: false,
allowFallbackSettings: false
)
)
#expect(options.compilerArguments.contains("-DBAR"))
let diagnostics = try #require(
await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
).fullReport?.items
)
#expect(diagnostics.isEmpty)
}
// MARK: - Package reload filtering
/// Creates a minimal package containing a zip-based binary target and returns the server ready
/// for reload-filtering tests.
///
/// Using a zip file is important: SwiftPM extracts zipped binary targets into the scratch
/// directory (`.build/index-build/artifacts/` by default), which makes those extracted paths
/// part of `buildDescription.inputs`. That is exactly the condition under which file-change
/// events in `.build/` would trigger a spurious package reload without the
/// `isInScratchDirectory` fix.
///
/// - Returns: A tuple of the running server and the project root URL.
private func makeServerWithBinaryTargetAndWaitForInitialLoad(
in tempDir: URL,
options: SourceKitLSPOptions = SourceKitLSPOptions(),
reloadPackageDidStart: (@Sendable () async -> Void)? = nil
) async throws -> (server: SwiftPMBuildServer, projectRoot: URL) {
let artifactBundleName = "MyBinaryTool.artifactbundle"
let zipName = "\(artifactBundleName).zip"
try FileManager.default.createFiles(
root: tempDir,
files: [
// Artifact bundle is staged outside of pkg/ so it doesn't pollute the package directory.
// ZipArchiver.compress will zip it into pkg/ below.
"\(artifactBundleName)/info.json": """
{
"schemaVersion": "1.0",
"artifacts": {
"MyBinaryTool": {
"type": "executable",
"version": "1.0.0",
"variants": []
}
}
}
""",
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "pkg",
targets: [
.target(name: "lib"),
.binaryTarget(name: "MyBinaryTool", path: "\(zipName)")
]
)
""",
]
)
let pkgDir = tempDir.appending(component: "pkg")
try await UniversalArchiver(localFileSystem).compress(
directory: Basics.AbsolutePath(validating: tempDir.appending(component: artifactBundleName).filePath),
to: Basics.AbsolutePath(validating: pkgDir.appending(component: zipName).filePath)
)
try FileManager.default.removeItem(at: tempDir.appending(component: artifactBundleName))
let projectRoot = try pkgDir.realpath
let server = try await SwiftPMBuildServer(
projectRoot: projectRoot,
toolchainRegistry: .forTesting,
options: options,
connectionToSourceKitLSP: LocalConnection(receiverName: "dummy"),
testHooks: SwiftPMTestHooks(reloadPackageDidStart: reloadPackageDidStart)
)
_ = await server.waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest())
return (server, projectRoot)
}
/// Verifies that file-change events for paths inside the SwiftPM scratch directory do not
/// trigger a package reload.
///
/// When a package contains a zip-based binary target, SwiftPM extracts the artifact into
/// `<scratch>/artifacts/<pkg>/<target>/` on every package load. Before the
/// `isInScratchDirectory` fix those extracted paths were registered in
/// `buildDescription.inputs`, so the resulting file-created/-deleted events passed
/// `fileAffectsSwiftOrClangBuildSettings`, triggering another reload — an infinite loop.
@Test
func testBinaryTargetArtifactEventsDoNotTriggerPackageReload() async throws {
try await withTestScratchDir { tempDir in
let packageInitialized = ThreadSafeBox<Bool>(initialValue: false)
let unexpectedReloadStarted = ThreadSafeBox<Bool>(initialValue: false)
let (server, projectRoot) = try await makeServerWithBinaryTargetAndWaitForInitialLoad(
in: tempDir,
reloadPackageDidStart: {
if packageInitialized.value {
unexpectedReloadStarted.value = true
}
}
)
packageInitialized.value = true
// SwiftPM extracts the artifact bundle to:
// <scratch>/artifacts/<package-identity>/<target-name>/<artifact-name>/
// With the default options, scratch = .build/index-build/.
let extractedInfoJSON = projectRoot.appending(
components: ".build",
"index-build",
"artifacts",
"pkg",
"MyBinaryTool",
"MyBinaryTool.artifactbundle",
"info.json"
)
#expect(FileManager.default.fileExists(atPath: extractedInfoJSON.path))
await server.didChangeWatchedFiles(
notification: OnWatchedFilesDidChangeNotification(
changes: [
FileEvent(uri: DocumentURI(extractedInfoJSON), type: .deleted),
FileEvent(uri: DocumentURI(extractedInfoJSON), type: .created),
]
)
)
_ = await server.waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest())
#expect(!unexpectedReloadStarted.value)
}
}
/// Same scenario with a custom scratch path configured outside of `.build/`.
///
/// When a custom scratch path is used, SourceKit-LSP's artifact extraction goes to
/// `<custom-scratch>/artifacts/`, which the first `isInScratchDirectory` check covers.
/// The second check (default `.build/` directory) additionally suppresses events from
/// whatever the regular `swift build` command writes to `.build/`.
@Test
func testBinaryTargetArtifactEventsDoNotTriggerPackageReloadWithCustomScratchPath()
async throws
{
try await withTestScratchDir { tempDir in
let customScratch = tempDir.appending(component: "custom-scratch")
let packageInitialized = ThreadSafeBox<Bool>(initialValue: false)
let unexpectedReloadStarted = ThreadSafeBox<Bool>(initialValue: false)
let (server, projectRoot) = try await makeServerWithBinaryTargetAndWaitForInitialLoad(
in: tempDir,
options: SourceKitLSPOptions(swiftPM: .init(scratchPath: try customScratch.filePath)),
reloadPackageDidStart: {
if packageInitialized.value {
unexpectedReloadStarted.value = true
}
}
)
packageInitialized.value = true
// With a custom scratch path, SwiftPM extracts to <custom-scratch>/artifacts/.
// Simulate the delete-and-re-expand cycle for those paths.
let extractedInfoJSON = customScratch.appending(
components: "artifacts",
"pkg",
"MyBinaryTool",
"MyBinaryTool.artifactbundle",
"info.json"
)
#expect(FileManager.default.fileExists(atPath: extractedInfoJSON.path))
await server.didChangeWatchedFiles(
notification: OnWatchedFilesDidChangeNotification(
changes: [
FileEvent(uri: DocumentURI(extractedInfoJSON), type: .deleted),
FileEvent(uri: DocumentURI(extractedInfoJSON), type: .created),
]
)
)
// Also verify that the default .build/ directory is filtered even when a custom
// scratch path is configured (second isInScratchDirectory check).
let defaultBuildArtifact = projectRoot.appending(
components: ".build",
"artifacts",
"pkg",
"MyBinaryTool",
"MyBinaryTool.artifactbundle",
"info.json"
)
await server.didChangeWatchedFiles(
notification: OnWatchedFilesDidChangeNotification(
changes: [FileEvent(uri: DocumentURI(defaultBuildArtifact), type: .created)]
)
)
_ = await server.waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest())
#expect(!unexpectedReloadStarted.value)
}
}
/// Verifies that the build system is correctly inferred as 'native' from the `.buildSystem_debug` file.
@Test(
arguments: [SwiftPMBuildSystem.native, .swiftbuild],
[SKOptions.BuildConfiguration.debug, .release]
)
func testBuildSystemInferenceFromFile(
buildSystem: SwiftPMBuildSystem,
buildConfiguration: SKOptions.BuildConfiguration,
) async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
"pkg/.build/.buildSystem_\(buildConfiguration)": "\(buildSystem)",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let options = SourceKitLSPOptions(swiftPM: .init(configuration: buildConfiguration))
let spec = try #require(
SwiftPMBuildServer.searchForConfig(in: packageRoot, options: options)
)
if case .swiftPM(let inferredBuildSystem) = spec.kind {
#expect(
inferredBuildSystem == buildSystem,
"Expected \(buildSystem) but got \(String(describing: inferredBuildSystem))"
)
} else {
Issue.record("Expected swiftPM build server kind")
}
}
}
/// Verifies that the build system inference falls back to heuristics when the file doesn't exist.
@Test
func testBuildSystemInferenceFallbackToHeuristics() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let buildDir = packageRoot.appending(component: ".build")
try FileManager.default.createDirectory(at: buildDir, withIntermediateDirectories: true)
// Create 'debug' directory to trigger native heuristic
let debugDir = buildDir.appending(component: "debug")
try FileManager.default.createDirectory(at: debugDir, withIntermediateDirectories: true)
let spec = try #require(
SwiftPMBuildServer.searchForConfig(in: packageRoot, options: SourceKitLSPOptions())
)
if case .swiftPM(let inferredBuildSystem) = spec.kind {
#expect(
inferredBuildSystem == .native,
"Expected .native from heuristic but got \(String(describing: inferredBuildSystem))"
)
} else {
Issue.record("Expected swiftPM build server kind")
}
}
}
/// Verifies that when both build system outputs exist, swiftbuild is preferred.
@Test
func testBuildSystemInferencePreferSwiftBuildWhenBothExist() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let buildDir = packageRoot.appending(component: ".build")
try FileManager.default.createDirectory(at: buildDir, withIntermediateDirectories: true)
// Create both 'debug' and 'out' directories
let debugDir = buildDir.appending(component: "debug")
let outDir = buildDir.appending(component: "out")
try FileManager.default.createDirectory(at: debugDir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: outDir, withIntermediateDirectories: true)
let spec = try #require(
SwiftPMBuildServer.searchForConfig(in: packageRoot, options: SourceKitLSPOptions())
)
if case .swiftPM(let inferredBuildSystem) = spec.kind {
#expect(
inferredBuildSystem == .swiftbuild,
"Expected .swiftbuild when both outputs exist but got \(String(describing: inferredBuildSystem))"
)
} else {
Issue.record("Expected swiftPM build server kind")
}
}
}
/// Verifies that the correct `.buildSystem_{config}` file is read based on the configuration when both exist.
@Test
func testBuildSystemInferenceUsesCorrectConfigurationFile() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
"pkg/.build/.buildSystem_debug": "native",
"pkg/.build/.buildSystem_release": "swiftbuild",
]
)
let packageRoot = tempDir.appending(component: "pkg")
// Test with debug configuration (default)
let debugSpec = try #require(
SwiftPMBuildServer.searchForConfig(in: packageRoot, options: SourceKitLSPOptions())
)
if case .swiftPM(let inferredBuildSystem) = debugSpec.kind {
#expect(
inferredBuildSystem == .native,
"Expected .native for debug config but got \(String(describing: inferredBuildSystem))"
)
} else {
Issue.record("Expected swiftPM build server kind for debug")
}
// Test with release configuration
let releaseOptions = SourceKitLSPOptions(swiftPM: .init(configuration: .release))
let releaseSpec = try #require(
SwiftPMBuildServer.searchForConfig(in: packageRoot, options: releaseOptions)
)
if case .swiftPM(let inferredBuildSystem) = releaseSpec.kind {
#expect(
inferredBuildSystem == .swiftbuild,
"Expected .swiftbuild for release config but got \(String(describing: inferredBuildSystem))"
)
} else {
Issue.record("Expected swiftPM build server kind for release")
}
}
}
/// Verifies that invalid content in the `.buildSystem_{config}` file results in nil inference.
@Test
func testBuildSystemInferenceInvalidContent() async throws {
try await withTestScratchDir { tempDir in
try FileManager.default.createFiles(
root: tempDir,
files: [
"pkg/Sources/lib/a.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "a",
targets: [.target(name: "lib")]
)
""",
"pkg/.build/.buildSystem_debug": "invalid_build_system",
]
)
let packageRoot = tempDir.appending(component: "pkg")
let spec = try #require(
SwiftPMBuildServer.searchForConfig(in: packageRoot, options: SourceKitLSPOptions())
)
if case .swiftPM(let inferredBuildSystem) = spec.kind {
#expect(
inferredBuildSystem == nil,
"Expected nil for invalid build system but got \(String(describing: inferredBuildSystem))"
)
} else {
Issue.record("Expected swiftPM build server kind")
}
}
}
@Test
func testPackagePlugin() async throws {
let testProject = try await SwiftPMTestProject(
files: [
"Test.swift": """
#if NonDefaultTrait
#warning("Trait enabled")
#endif
"""
],
manifest: """
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "MyLibrary",
traits: [
.default(enabledTraits: []),
"NonDefaultTrait",
],
targets: [.target(name: "MyLibrary")]
)
""",
options: SourceKitLSPOptions(swiftPM: .init(traits: ["NonDefaultTrait"])),
enableBackgroundIndexing: true
)
let (uri, _) = try testProject.openDocument("Test.swift")
let diagnostics = try await testProject.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
)
#expect(diagnostics.fullReport?.items.map(\.message) == ["Trait enabled"])
}
}
private func expectArgumentsDoNotContain(
_ pattern: String...,
arguments: [String],
sourceLocation: SourceLocation = #_sourceLocation
) {
if let index = arguments.firstRange(of: pattern)?.startIndex {
Issue.record(
"not-pattern \(pattern) unexpectedly found at \(index) in arguments \(arguments)",
sourceLocation: sourceLocation
)
return
}
}
private func expectArgumentsContain(
_ pattern: String...,
arguments: [String],
allowMultiple: Bool = false,
sourceLocation: SourceLocation = #_sourceLocation
) {
guard let index = arguments.firstRange(of: pattern)?.startIndex else {
Issue.record("pattern \(pattern) not found in arguments \(arguments)", sourceLocation: sourceLocation)
return
}
if !allowMultiple, let index2 = arguments[(index + 1)...].firstRange(of: pattern)?.startIndex {
Issue.record(
"pattern \(pattern) found twice (\(index), \(index2)) in \(arguments)",
sourceLocation: sourceLocation
)
}
}
private func buildPath(
root: URL,
options: SourceKitLSPOptions.SwiftPMOptions = SourceKitLSPOptions.SwiftPMOptions(),
platform: String
) -> URL {
let buildPath =
if let scratchPath = options.scratchPath {
URL(fileURLWithPath: scratchPath)
} else {
root.appending(components: ".build", "index-build")
}
return buildPath.appending(components: platform, "\(options.configuration ?? .debug)")
}
fileprivate extension URL {
func appending(components: [String]) -> URL {
var result = self
for component in components {
result.appendPathComponent(component)
}
return result
}
}
fileprivate extension BuildServerSpec {
static func swiftpmSpec(for packageRoot: URL) -> BuildServerSpec {
return BuildServerSpec(
kind: .swiftPM(inferredBuildSystem: .native),
projectRoot: packageRoot,
configPath: packageRoot.appending(component: "Package.swift")
)
}
}
#endif