Skip to content

Commit 05cf77e

Browse files
committed
Add a BSP implementation for packages to be adopted by sourcekit-lsp
1 parent 2096cff commit 05cf77e

File tree

13 files changed

+779
-14
lines changed

13 files changed

+779
-14
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public func foo() {
2+
{}()
3+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// swift-tools-version:6.0
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "Foo",
6+
products: [
7+
.library(name: "Foo", targets: ["Foo"]),
8+
],
9+
targets: [
10+
.target(name: "Foo", path: "./"),
11+
]
12+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// swift-tools-version:5.2
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "Foo",
6+
products: [
7+
.library(name: "Foo", targets: ["Foo"]),
8+
],
9+
targets: [
10+
.target(name: "Foo", path: "./"),
11+
]
12+
)

Package.swift

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,14 @@ let swiftDriverDeps: [Target.Dependency]
105105
let swiftTSCBasicsDeps: [Target.Dependency]
106106
let swiftToolsCoreSupportAutoDeps: [Target.Dependency]
107107
let swiftTSCTestSupportDeps: [Target.Dependency]
108+
let swiftToolsProtocolsDeps: [Target.Dependency]
108109

109110
if shouldUseSwiftBuildFramework {
110111
swiftDriverDeps = []
111112
swiftTSCBasicsDeps = []
112113
swiftToolsCoreSupportAutoDeps = []
113114
swiftTSCTestSupportDeps = []
115+
swiftToolsProtocolsDeps = []
114116
} else {
115117
swiftDriverDeps = [
116118
.product(name: "SwiftDriver", package: "swift-driver")
@@ -124,6 +126,11 @@ if shouldUseSwiftBuildFramework {
124126
swiftTSCTestSupportDeps = [
125127
.product(name: "TSCTestSupport", package: "swift-tools-support-core"),
126128
]
129+
swiftToolsProtocolsDeps = [
130+
.product(name: "BuildServerProtocol", package: "swift-tools-protocols", condition: .when(platforms: [.macOS, .linux, .windows, .android, .openbsd, .custom("freebsd")])),
131+
.product(name: "LanguageServerProtocol", package: "swift-tools-protocols", condition: .when(platforms: [.macOS, .linux, .windows, .android, .openbsd, .custom("freebsd")])),
132+
.product(name: "LanguageServerProtocolTransport", package: "swift-tools-protocols", condition: .when(platforms: [.macOS, .linux, .windows, .android, .openbsd, .custom("freebsd")])),
133+
]
127134
}
128135
let package = Package(
129136
name: "SwiftPM",
@@ -547,6 +554,23 @@ let package = Package(
547554
]
548555
),
549556

557+
// MARK: BSP
558+
.target(
559+
name: "SwiftPMBuildServer",
560+
dependencies: [
561+
"Basics",
562+
"Build",
563+
"PackageGraph",
564+
"PackageLoading",
565+
"PackageModel",
566+
"SPMBuildCore",
567+
"SourceControl",
568+
"SourceKitLSPAPI",
569+
"SwiftBuildSupport",
570+
"Workspace"
571+
] + swiftTSCBasicsDeps + swiftToolsProtocolsDeps
572+
),
573+
550574
// MARK: Commands
551575

552576
.target(
@@ -575,6 +599,7 @@ let package = Package(
575599
dependencies: [
576600
.product(name: "ArgumentParser", package: "swift-argument-parser"),
577601
.product(name: "OrderedCollections", package: "swift-collections"),
602+
.product(name: "SystemPackage", package: "swift-system"),
578603
"Basics",
579604
"BinarySymbols",
580605
"Build",
@@ -585,6 +610,7 @@ let package = Package(
585610
"XCBuildSupport",
586611
"SwiftBuildSupport",
587612
"SwiftFixIt",
613+
"SwiftPMBuildServer",
588614
] + swiftSyntaxDependencies(["SwiftIDEUtils", "SwiftRefactor"]),
589615
exclude: ["CMakeLists.txt", "README.md"],
590616
swiftSettings: swift6CompatibleExperimentalFeatures + [
@@ -1055,6 +1081,13 @@ if ProcessInfo.processInfo.environment["SWIFTCI_DISABLE_SDK_DEPENDENT_TESTS"] ==
10551081
"dummy-swiftc",
10561082
]
10571083
),
1084+
.testTarget(
1085+
name: "SwiftPMBuildServerTests",
1086+
dependencies: [
1087+
"SwiftPMBuildServer",
1088+
"_InternalTestSupport",
1089+
] + swiftToolsProtocolsDeps
1090+
),
10581091
])
10591092
}
10601093

@@ -1151,7 +1184,7 @@ if ProcessInfo.processInfo.environment["ENABLE_APPLE_PRODUCT_TYPES"] == "1" {
11511184

11521185
if !shouldUseSwiftBuildFramework {
11531186

1154-
let swiftbuildsupport: Target = package.targets.first(where: { $0.name == "SwiftBuildSupport" } )!
1187+
let swiftbuildsupport: Target = package.targets.first(where: { ["SwiftBuildSupport", "SwiftPMBuildServer", "SwiftPMBuildServerTests"].contains($0.name) } )!
11551188
swiftbuildsupport.dependencies += [
11561189
.product(name: "SwiftBuild", package: "swift-build"),
11571190
]
@@ -1164,10 +1197,12 @@ if !shouldUseSwiftBuildFramework {
11641197
if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
11651198
package.dependencies += [
11661199
.package(url: "https://github.com/swiftlang/swift-build.git", branch: relatedDependenciesBranch),
1200+
.package(url: "https://github.com/swiftlang/swift-tools-protocols.git", .upToNextMinor(from: "0.0.9")),
11671201
]
11681202
} else {
11691203
package.dependencies += [
11701204
.package(path: "../swift-build"),
1205+
.package(path: "../swift-tools-protocols"),
11711206
]
11721207
}
11731208
}

Sources/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ add_subdirectory(swift-sdk)
4040
add_subdirectory(swift-package)
4141
add_subdirectory(swift-run)
4242
add_subdirectory(swift-test)
43+
add_subdirectory(SwiftPMBuildServer)
4344
add_subdirectory(SwiftSDKCommand)
4445
add_subdirectory(Workspace)
4546
add_subdirectory(XCBuildSupport)

Sources/Commands/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ add_library(Commands
1515
PackageCommands/APIDiff.swift
1616
PackageCommands/ArchiveSource.swift
1717
PackageCommands/AuditBinaryArtifact.swift
18+
PackageCommands/BuildServer.swift
1819
PackageCommands/CompletionCommand.swift
1920
PackageCommands/ComputeChecksum.swift
2021
PackageCommands/Config.swift
@@ -61,6 +62,7 @@ add_library(Commands
6162
target_link_libraries(Commands PUBLIC
6263
SwiftCollections::OrderedCollections
6364
SwiftSyntax::SwiftRefactor
65+
SwiftSystem::SystemPackage
6466
ArgumentParser
6567
Basics
6668
BinarySymbols
@@ -70,6 +72,7 @@ target_link_libraries(Commands PUBLIC
7072
PackageGraph
7173
SourceControl
7274
SwiftFixIt
75+
SwiftPMBuildServer
7376
TSCBasic
7477
TSCUtility
7578
Workspace
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
#if canImport(LanguageServerProtocolTransport)
13+
import ArgumentParser
14+
import TSCBasic
15+
import SwiftBuild
16+
import BuildServerProtocol
17+
import LanguageServerProtocol
18+
import LanguageServerProtocolTransport
19+
import CoreCommands
20+
import Foundation
21+
import PackageGraph
22+
import SwiftPMBuildServer
23+
import SPMBuildCore
24+
import SwiftBuildSupport
25+
import SystemPackage
26+
27+
struct BuildServer: AsyncSwiftCommand {
28+
static let configuration = CommandConfiguration(
29+
commandName: "experimental-build-server",
30+
abstract: "Launch a build server for Swift Packages",
31+
shouldDisplay: false
32+
)
33+
34+
@OptionGroup(visibility: .hidden)
35+
var globalOptions: GlobalOptions
36+
37+
func run(_ swiftCommandState: SwiftCommandState) async throws {
38+
// Dup stdout and redirect the fd to stderr so that a careless print()
39+
// will not break our connection stream.
40+
let realStdout = try FileDescriptor.standardOutput.duplicate()
41+
_ = try FileDescriptor.standardError.duplicate(as: FileDescriptor.standardOutput)
42+
43+
let realStdoutHandle = FileHandle(fileDescriptor: realStdout.rawValue, closeOnDealloc: false)
44+
45+
let clientConnection = JSONRPCConnection(
46+
name: "client",
47+
protocol: MessageRegistry.bspProtocol,
48+
inFD: FileHandle.standardInput,
49+
outFD: realStdoutHandle,
50+
inputMirrorFile: nil,
51+
outputMirrorFile: nil
52+
)
53+
54+
guard let buildSystem = try await swiftCommandState.createBuildSystem() as? SwiftBuildSystem else {
55+
throw ArgumentParser.ValidationError("Build server requires --build-system swiftbuild")
56+
}
57+
58+
guard let packagePath = try swiftCommandState.getWorkspaceRoot().packages.first else {
59+
throw ArgumentParser.ValidationError("unknown package")
60+
}
61+
62+
let server = try await SwiftPMBuildServer(
63+
packageRoot: packagePath,
64+
buildSystem: buildSystem,
65+
workspace: swiftCommandState.getActiveWorkspace(),
66+
connectionToClient: clientConnection,
67+
exitHandler: {_ in clientConnection.close() }
68+
)
69+
await withCheckedContinuation {continuation in
70+
clientConnection.start(receiveHandler: server, closeHandler: { continuation.resume() })
71+
}
72+
}
73+
}
74+
#endif

Sources/Commands/PackageCommands/SwiftPackageCommand.swift

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,9 @@ import enum TSCUtility.Diagnostics
2626

2727
/// swift-package tool namespace
2828
public struct SwiftPackageCommand: AsyncParsableCommand {
29-
public static var configuration = CommandConfiguration(
30-
commandName: "package",
31-
_superCommandName: "swift",
32-
abstract: "Perform operations on Swift packages.",
33-
discussion: "SEE ALSO: swift build, swift run, swift test \n(Run this command without --help to see possible dynamic plugin commands.)",
34-
version: SwiftVersion.current.completeDisplayString,
35-
subcommands: [
29+
30+
private static var subcommands: [any ParsableCommand.Type] = {
31+
var subcommands: [any ParsableCommand.Type] = [
3632
AddDependency.self,
3733
AddProduct.self,
3834
AddTarget.self,
@@ -75,7 +71,22 @@ public struct SwiftPackageCommand: AsyncParsableCommand {
7571

7672
DefaultCommand.self,
7773
]
78-
+ (ProcessInfo.processInfo.environment["SWIFTPM_ENABLE_SNIPPETS"] == "1" ? [Learn.self] : []),
74+
if ProcessInfo.processInfo.environment["SWIFTPM_ENABLE_SNIPPETS"] == "1" {
75+
subcommands.append(Learn.self)
76+
}
77+
#if canImport(LanguageServerProtocol)
78+
subcommands.append(BuildServer.self)
79+
#endif
80+
return subcommands
81+
}()
82+
83+
public static var configuration = CommandConfiguration(
84+
commandName: "package",
85+
_superCommandName: "swift",
86+
abstract: "Perform operations on Swift packages.",
87+
discussion: "SEE ALSO: swift build, swift run, swift test \n(Run this command without --help to see possible dynamic plugin commands.)",
88+
version: SwiftVersion.current.completeDisplayString,
89+
subcommands: Self.subcommands,
7990
defaultSubcommand: DefaultCommand.self,
8091
helpNames: []
8192
)

Sources/SwiftBuildSupport/SwiftBuildSystem.swift

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ func withSession(
111111
}
112112
}
113113

114-
private final class PlanningOperationDelegate: SWBPlanningOperationDelegate, Sendable {
114+
package final class SwiftBuildSystemPlanningOperationDelegate: SWBPlanningOperationDelegate, SWBIndexingDelegate, Sendable {
115+
package init() {}
116+
115117
public func provisioningTaskInputs(
116118
targetGUID: String,
117119
provisioningSourceData: SWBProvisioningTaskInputsSourceData
@@ -175,7 +177,7 @@ private final class PlanningOperationDelegate: SWBPlanningOperationDelegate, Sen
175177
}
176178

177179
public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
178-
private let buildParameters: BuildParameters
180+
package let buildParameters: BuildParameters
179181
private let packageGraphLoader: () async throws -> ModulesGraph
180182
private let packageManagerResourcesDirectory: Basics.AbsolutePath?
181183
private let logLevel: Basics.Diagnostic.Severity
@@ -341,7 +343,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
341343
return result
342344
}
343345

344-
try await writePIF(buildParameters: buildParameters)
346+
try await writePIF(buildParameters: self.buildParameters)
345347

346348
return try await startSWBuildOperation(
347349
pifTargetName: subset.pifTargetName,
@@ -726,7 +728,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
726728

727729
let operation = try await session.createBuildOperation(
728730
request: request,
729-
delegate: PlanningOperationDelegate(),
731+
delegate: SwiftBuildSystemPlanningOperationDelegate(),
730732
retainBuildDescription: true
731733
)
732734

@@ -1171,18 +1173,43 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
11711173
}
11721174

11731175
public func generatePIF(preserveStructure: Bool) async throws -> String {
1174-
return try await getPIFBuilder().generatePIF(
1176+
pifBuilder = .init()
1177+
packageGraph = .init()
1178+
let pifBuilder = try await getPIFBuilder()
1179+
let pif = try await pifBuilder.generatePIF(
11751180
preservePIFModelStructure: preserveStructure,
11761181
printPIFManifestGraphviz: buildParameters.printPIFManifestGraphviz,
11771182
buildParameters: buildParameters
11781183
)
1184+
return pif
11791185
}
11801186

11811187
public func writePIF(buildParameters: BuildParameters) async throws {
11821188
let pif = try await generatePIF(preserveStructure: false)
11831189
try self.fileSystem.writeIfChanged(path: buildParameters.pifManifest, string: pif)
11841190
}
11851191

1192+
package struct LongLivedBuildServiceSession {
1193+
package var session: SWBBuildServiceSession
1194+
package var diagnostics: [SwiftBuildMessage.DiagnosticInfo]
1195+
package var teardownHandler: () async throws -> Void
1196+
}
1197+
1198+
package func createLongLivedSession(name: String) async throws -> LongLivedBuildServiceSession {
1199+
let service = try await SWBBuildService(connectionMode: .inProcessStatic(swiftbuildServiceEntryPoint))
1200+
do {
1201+
let (session, diagnostics) = try await createSession(service: service, name: name, toolchainPath: buildParameters.toolchain.toolchainDir, packageManagerResourcesDirectory: packageManagerResourcesDirectory)
1202+
let teardownHandler = {
1203+
try await session.close()
1204+
await service.close()
1205+
}
1206+
return LongLivedBuildServiceSession(session: session, diagnostics: diagnostics, teardownHandler: teardownHandler)
1207+
} catch {
1208+
await service.close()
1209+
throw error
1210+
}
1211+
}
1212+
11861213
public func cancel(deadline: DispatchTime) throws {}
11871214

11881215
/// Returns the package graph using the graph loader closure.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# This source file is part of the Swift open source project
2+
#
3+
# Copyright (c) 2025 Apple Inc. and the Swift project authors
4+
# Licensed under Apache License v2.0 with Runtime Library Exception
5+
#
6+
# See http://swift.org/LICENSE.txt for license information
7+
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors
8+
9+
add_library(SwiftPMBuildServer STATIC
10+
DisableSigpipe.swift
11+
SwiftPMBuildServer.swift)
12+
target_link_libraries(SwiftPMBuildServer PUBLIC
13+
Basics
14+
Build
15+
PackageGraph
16+
PackageLoading
17+
PackageModel
18+
SPMBuildCore
19+
SourceControl
20+
SourceKitLSPAPI
21+
SwiftBuildSupport
22+
Workspace
23+
24+
SwiftBuild::SwiftBuild
25+
26+
SwiftToolsProtocols::ToolsProtocolsSwiftExtensions
27+
SwiftToolsProtocols::BuildServerProtocol
28+
SwiftToolsProtocols::LanguageServerProtocol
29+
SwiftToolsProtocols::LanguageServerProtocolTransport
30+
31+
TSCBasic)
32+
33+
# NOTE(compnerd) workaround for CMake not setting up include flags yet
34+
set_target_properties(SwiftPMBuildServer PROPERTIES
35+
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})

0 commit comments

Comments
 (0)