Skip to content

Commit fd08cad

Browse files
committed
Allow opting in to using the new SwiftPM BSP when opening a package
1 parent ca9ef8c commit fd08cad

File tree

8 files changed

+197
-31
lines changed

8 files changed

+197
-31
lines changed

Documentation/Configuration File.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ The structure of the file is currently not guaranteed to be stable. Options may
3030
- `linkerFlags: string[]`: Extra arguments passed to the linker. Equivalent to SwiftPM's `-Xlinker` option.
3131
- `buildToolsSwiftCompilerFlags: string[]`: Extra arguments passed to the compiler for Swift files or plugins. Equivalent to SwiftPM's `-Xbuild-tools-swiftc` option.
3232
- `disableSandbox: boolean`: Disables running subprocesses from SwiftPM in a sandbox. Equivalent to SwiftPM's `--disable-sandbox` option. Useful when running `sourcekit-lsp` in a sandbox because nested sandboxes are not supported.
33+
- `buildSystem: "native"|"swiftbuild"`: Which SwiftPM build system should be used when opening a package.
3334
- `compilationDatabase`: Dictionary with the following keys, defining options for workspaces with a compilation database.
3435
- `searchPaths: string[]`: Additional paths to search for a compilation database, relative to a workspace root.
3536
- `fallbackBuildSystem`: Dictionary with the following keys, defining options for files that aren't managed by any build server.

Sources/BuildServerIntegration/BuildServerManager.swift

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -280,22 +280,43 @@ private extension BuildServerSpec {
280280
)
281281
}
282282
case .swiftPM:
283-
#if !NO_SWIFTPM_DEPENDENCY
284-
return await createBuiltInBuildServerAdapter(
285-
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
286-
buildServerHooks: buildServerHooks
287-
) { connectionToSourceKitLSP in
288-
try await SwiftPMBuildServer(
289-
projectRoot: projectRoot,
290-
toolchainRegistry: toolchainRegistry,
291-
options: options,
292-
connectionToSourceKitLSP: connectionToSourceKitLSP,
293-
testHooks: buildServerHooks.swiftPMTestHooks
294-
)
283+
switch options.swiftPMOrDefault.buildSystem {
284+
case .swiftbuild:
285+
let buildServer = await orLog("Creating external SwiftPM build server") {
286+
try await ExternalBuildServerAdapter(
287+
projectRoot: projectRoot,
288+
config: BuildServerConfig.forSwiftPMBuildServer(
289+
projectRoot: projectRoot,
290+
swiftPMOptions: options.swiftPMOrDefault,
291+
toolchainRegistry: toolchainRegistry
292+
),
293+
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler
294+
)
295+
}
296+
guard let buildServer else {
297+
logger.log("Failed to create external SwiftPM build server at \(projectRoot)")
298+
return nil
299+
}
300+
logger.log("Created external SwiftPM build server at \(projectRoot)")
301+
return .external(buildServer)
302+
case .native, nil:
303+
#if !NO_SWIFTPM_DEPENDENCY
304+
return await createBuiltInBuildServerAdapter(
305+
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
306+
buildServerHooks: buildServerHooks
307+
) { connectionToSourceKitLSP in
308+
try await SwiftPMBuildServer(
309+
projectRoot: projectRoot,
310+
toolchainRegistry: toolchainRegistry,
311+
options: options,
312+
connectionToSourceKitLSP: connectionToSourceKitLSP,
313+
testHooks: buildServerHooks.swiftPMTestHooks
314+
)
315+
}
316+
#else
317+
return nil
318+
#endif
295319
}
296-
#else
297-
return nil
298-
#endif
299320
case .injected(let injector):
300321
let connectionToSourceKitLSP = LocalConnection(
301322
receiverName: "BuildServerManager for \(projectRoot.lastPathComponent)",

Sources/BuildServerIntegration/ExternalBuildServerAdapter.swift

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Foundation
1919
import SKOptions
2020
import SwiftExtensions
2121
import TSCExtensions
22+
import ToolchainRegistry
2223

2324
import func TSCBasic.getEnvSearchPaths
2425
import var TSCBasic.localFileSystem
@@ -61,7 +62,7 @@ enum BuildServerNotFoundError: Error {
6162
/// BSP configuration
6263
///
6364
/// See https://build-server-protocol.github.io/docs/overview/server-discovery#the-bsp-connection-details
64-
private struct BuildServerConfig: Codable {
65+
struct BuildServerConfig: Codable {
6566
/// The name of the build tool.
6667
let name: String
6768

@@ -82,15 +83,92 @@ private struct BuildServerConfig: Codable {
8283
let fileData = try Data(contentsOf: path)
8384
return try decoder.decode(BuildServerConfig.self, from: fileData)
8485
}
86+
87+
static func forSwiftPMBuildServer(
88+
projectRoot: URL,
89+
swiftPMOptions: SourceKitLSPOptions.SwiftPMOptions,
90+
toolchainRegistry: ToolchainRegistry
91+
) async throws -> BuildServerConfig {
92+
let toolchain = await toolchainRegistry.preferredToolchain(containing: [\.swift])
93+
guard let swiftPath = try toolchain?.swift?.filePath else {
94+
throw ExecutableNotFoundError(executableName: "swift")
95+
}
96+
var args: [String] = [swiftPath, "package", "experimental-build-server"]
97+
// The build server requires use of the Swift Build backend.
98+
args.append(contentsOf: ["--build-system", "swiftbuild"])
99+
// Explicitly specify the package path.
100+
try args.append(contentsOf: ["--package-path", projectRoot.filePath])
101+
// Map LSP SwiftPM options to build server flags
102+
if let configuration = swiftPMOptions.configuration {
103+
args.append(contentsOf: ["--configuration", configuration.rawValue])
104+
}
105+
if let scratchPath = swiftPMOptions.scratchPath {
106+
args.append(contentsOf: ["--scratch-path", scratchPath])
107+
}
108+
if let swiftSDKsDirectory = swiftPMOptions.swiftSDKsDirectory {
109+
args.append(contentsOf: ["--swift-sdks-path", swiftSDKsDirectory])
110+
}
111+
if let swiftSDK = swiftPMOptions.swiftSDK {
112+
args.append(contentsOf: ["--swift-sdk", swiftSDK])
113+
}
114+
if let triple = swiftPMOptions.triple {
115+
args.append(contentsOf: ["--triple", triple])
116+
}
117+
if let toolsets = swiftPMOptions.toolsets {
118+
for toolset in toolsets {
119+
args.append(contentsOf: ["--toolset", toolset])
120+
}
121+
}
122+
if let traits = swiftPMOptions.traits {
123+
args.append(contentsOf: ["--traits", traits.joined(separator: ",")])
124+
}
125+
if let cCompilerFlags = swiftPMOptions.cCompilerFlags {
126+
for flag in cCompilerFlags {
127+
args.append(contentsOf: ["-Xcc", flag])
128+
}
129+
}
130+
if let cxxCompilerFlags = swiftPMOptions.cxxCompilerFlags {
131+
for flag in cxxCompilerFlags {
132+
args.append(contentsOf: ["-Xcxx", flag])
133+
}
134+
}
135+
if let swiftCompilerFlags = swiftPMOptions.swiftCompilerFlags {
136+
for flag in swiftCompilerFlags {
137+
args.append(contentsOf: ["-Xswiftc", flag])
138+
}
139+
}
140+
if let linkerFlags = swiftPMOptions.linkerFlags {
141+
for flag in linkerFlags {
142+
args.append(contentsOf: ["-Xlinker", flag])
143+
}
144+
}
145+
if let buildToolsSwiftCompilerFlags = swiftPMOptions.buildToolsSwiftCompilerFlags {
146+
for flag in buildToolsSwiftCompilerFlags {
147+
args.append(contentsOf: ["-Xbuild-tools-swiftc", flag])
148+
}
149+
}
150+
if swiftPMOptions.disableSandbox == true {
151+
args.append("--disable-sandbox")
152+
}
153+
// The skipPlugins option isn't currently respected because the underlying build server does not support it.
154+
// We may want to reconsider this in the future, or remove the option entirely.
155+
return BuildServerConfig(
156+
name: "SwiftPM Build Server",
157+
version: "",
158+
bspVersion: "2.2.0",
159+
languages: [Language.c, .cpp, .objective_c, .objective_cpp, .swift].map(\.rawValue),
160+
argv: args
161+
)
162+
}
85163
}
86164

87165
/// Launches a subprocess that is a BSP server and manages the process's lifetime.
88166
actor ExternalBuildServerAdapter {
89167
/// The root folder of the project. Used to resolve relative server paths.
90168
private let projectRoot: URL
91169

92-
/// The file that specifies the configuration for this build server.
93-
private let configPath: URL
170+
/// The configuration for this build server.
171+
private let serverConfig: BuildServerConfig
94172

95173
/// The `BuildServerManager` that handles messages from the BSP server to SourceKit-LSP.
96174
var messagesToSourceKitLSPHandler: any MessageHandler
@@ -123,15 +201,28 @@ actor ExternalBuildServerAdapter {
123201

124202
init(
125203
projectRoot: URL,
126-
configPath: URL,
204+
config: BuildServerConfig,
127205
messagesToSourceKitLSPHandler: any MessageHandler
128206
) async throws {
129207
self.projectRoot = projectRoot
130-
self.configPath = configPath
208+
self.serverConfig = config
131209
self.messagesToSourceKitLSPHandler = messagesToSourceKitLSPHandler
132210
self.connectionToBuildServer = try await self.createConnectionToBspServer()
133211
}
134212

213+
init(
214+
projectRoot: URL,
215+
configPath: URL,
216+
messagesToSourceKitLSPHandler: any MessageHandler
217+
) async throws {
218+
let serverConfig = try BuildServerConfig.load(from: configPath)
219+
try await self.init(
220+
projectRoot: projectRoot,
221+
config: serverConfig,
222+
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler
223+
)
224+
}
225+
135226
/// Change the handler that handles messages from the build server.
136227
///
137228
/// The intended use of this is to intercept messages from the build server by `LegacyBuildServer`.
@@ -165,7 +256,6 @@ actor ExternalBuildServerAdapter {
165256

166257
/// Create a new JSONRPCConnection to the build server.
167258
private func createConnectionToBspServer() async throws -> JSONRPCConnection {
168-
let serverConfig = try BuildServerConfig.load(from: configPath)
169259
var serverPath = URL(fileURLWithPath: serverConfig.argv[0], relativeTo: projectRoot.ensuringCorrectTrailingSlash)
170260
var serverArgs = Array(serverConfig.argv[1...])
171261

Sources/SKOptions/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ add_library(SKOptions STATIC
33
BuildConfiguration.swift
44
ExperimentalFeatures.swift
55
SourceKitLSPOptions.swift
6+
SwiftPMBuildSystem.swift
67
WorkspaceType.swift)
78
set_target_properties(SKOptions PROPERTIES
89
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})

Sources/SKOptions/SourceKitLSPOptions.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
7676
/// background indexing.
7777
public var skipPlugins: Bool?
7878

79+
/// Which SwiftPM build system should be used when opening a package.
80+
public var buildSystem: SwiftPMBuildSystem?
81+
7982
public init(
8083
configuration: BuildConfiguration? = nil,
8184
scratchPath: String? = nil,
@@ -90,7 +93,8 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
9093
linkerFlags: [String]? = nil,
9194
buildToolsSwiftCompilerFlags: [String]? = nil,
9295
disableSandbox: Bool? = nil,
93-
skipPlugins: Bool? = nil
96+
skipPlugins: Bool? = nil,
97+
buildSystem: SwiftPMBuildSystem? = nil
9498
) {
9599
self.configuration = configuration
96100
self.scratchPath = scratchPath
@@ -105,6 +109,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
105109
self.linkerFlags = linkerFlags
106110
self.buildToolsSwiftCompilerFlags = buildToolsSwiftCompilerFlags
107111
self.disableSandbox = disableSandbox
112+
self.buildSystem = buildSystem
108113
}
109114

110115
static func merging(base: SwiftPMOptions, override: SwiftPMOptions?) -> SwiftPMOptions {
@@ -122,7 +127,8 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
122127
linkerFlags: override?.linkerFlags ?? base.linkerFlags,
123128
buildToolsSwiftCompilerFlags: override?.buildToolsSwiftCompilerFlags ?? base.buildToolsSwiftCompilerFlags,
124129
disableSandbox: override?.disableSandbox ?? base.disableSandbox,
125-
skipPlugins: override?.skipPlugins ?? base.skipPlugins
130+
skipPlugins: override?.skipPlugins ?? base.skipPlugins,
131+
buildSystem: override?.buildSystem ?? base.buildSystem
126132
)
127133
}
128134
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
public enum SwiftPMBuildSystem: String, Codable, Sendable {
14+
case native
15+
case swiftbuild
16+
}

Tests/BuildServerIntegrationTests/SwiftPMBuildServerTests.swift

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ private var hostTriple: Triple {
4747
}
4848
}
4949

50+
fileprivate extension SourceKitLSPOptions {
51+
static var forTestingExperimentalSwiftPMBuildServer: Self {
52+
SourceKitLSPOptions(swiftPM: SwiftPMOptions(buildSystem: .swiftbuild))
53+
}
54+
}
55+
5056
@Suite(.serialized, .configureLogging)
5157
struct SwiftPMBuildServerTests {
5258
@Test
@@ -132,8 +138,8 @@ struct SwiftPMBuildServerTests {
132138
}
133139
}
134140

135-
@Test
136-
func testBasicSwiftArgs() async throws {
141+
@Test(arguments: [SourceKitLSPOptions(), .forTestingExperimentalSwiftPMBuildServer])
142+
func testBasicSwiftArgs(options: SourceKitLSPOptions) async throws {
137143
try await withTestScratchDir { tempDir in
138144
try FileManager.default.createFiles(
139145
root: tempDir,
@@ -153,7 +159,7 @@ struct SwiftPMBuildServerTests {
153159
let buildServerManager = await BuildServerManager(
154160
buildServerSpec: .swiftpmSpec(for: packageRoot),
155161
toolchainRegistry: .forTesting,
156-
options: SourceKitLSPOptions(),
162+
options: options,
157163
connectionToClient: DummyBuildServerManagerConnectionToClient(),
158164
buildServerHooks: BuildServerHooks(),
159165
createMainFilesProvider: { _, _ in nil }
@@ -181,18 +187,34 @@ struct SwiftPMBuildServerTests {
181187
expectArgumentsContain("-target", arguments: arguments) // Only one!
182188
#if os(macOS)
183189
let versionString = PackageModel.Platform.macOS.oldestSupportedVersion.versionString
190+
if options.swiftPMOrDefault.buildSystem == .swiftbuild {
191+
expectArgumentsContain(
192+
"-target",
193+
// Account for differences in macOS naming canonicalization
194+
try await hostTriple.tripleString(forPlatformVersion: versionString).replacing("macosx", with: "macos"),
195+
arguments: arguments
196+
)
197+
} else {
198+
expectArgumentsContain(
199+
"-target",
200+
try await hostTriple.tripleString(forPlatformVersion: versionString),
201+
arguments: arguments
202+
)
203+
}
184204
expectArgumentsContain(
185-
"-target",
186-
try await hostTriple.tripleString(forPlatformVersion: versionString),
187-
arguments: arguments
205+
"-sdk",
206+
arguments: arguments,
207+
allowMultiple: options.swiftPMOrDefault.buildSystem == .swiftbuild
188208
)
189-
expectArgumentsContain("-sdk", arguments: arguments)
190209
expectArgumentsContain("-F", arguments: arguments, allowMultiple: true)
191210
#else
192211
expectArgumentsContain("-target", try await hostTriple.tripleString, arguments: arguments)
193212
#endif
194213

195-
expectArgumentsContain("-I", try build.appending(component: "Modules").filePath, arguments: arguments)
214+
if options.swiftPMOrDefault.buildSystem != .swiftbuild {
215+
// Swift Build and the native build system setup search paths differently. We deliberately avoid testing implementation details of Swift Build here.
216+
expectArgumentsContain("-I", try build.appending(component: "Modules").filePath, arguments: arguments)
217+
}
196218

197219
expectArgumentsContain(try aswift.filePath, arguments: arguments)
198220
}

config.schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,15 @@
205205
"description" : "Options for SwiftPM workspaces.",
206206
"markdownDescription" : "Options for SwiftPM workspaces.",
207207
"properties" : {
208+
"buildSystem" : {
209+
"description" : "Which SwiftPM build system should be used when opening a package.",
210+
"enum" : [
211+
"native",
212+
"swiftbuild"
213+
],
214+
"markdownDescription" : "Which SwiftPM build system should be used when opening a package.",
215+
"type" : "string"
216+
},
208217
"buildToolsSwiftCompilerFlags" : {
209218
"description" : "Extra arguments passed to the compiler for Swift files or plugins. Equivalent to SwiftPM's `-Xbuild-tools-swiftc` option.",
210219
"items" : {

0 commit comments

Comments
 (0)