-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[WIP] Expose a SwiftPM BSP interface based on Swift Build #9129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
public func foo() { | ||
{}() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// swift-tools-version:6.0 | ||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "Foo", | ||
products: [ | ||
.library(name: "Foo", targets: ["Foo"]), | ||
], | ||
targets: [ | ||
.target(name: "Foo", path: "./"), | ||
] | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// swift-tools-version:5.2 | ||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "Foo", | ||
products: [ | ||
.library(name: "Foo", targets: ["Foo"]), | ||
], | ||
targets: [ | ||
.target(name: "Foo", path: "./"), | ||
] | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift open source project | ||
// | ||
// Copyright (c) 2025 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See http://swift.org/LICENSE.txt for license information | ||
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import ArgumentParser | ||
import TSCBasic | ||
import SWBBuildServerProtocol | ||
import CoreCommands | ||
import Foundation | ||
import PackageGraph | ||
import SwiftPMBuildServer | ||
import SPMBuildCore | ||
import SwiftBuildSupport | ||
|
||
struct BuildServer: AsyncSwiftCommand { | ||
static let configuration = CommandConfiguration( | ||
commandName: "build-server", | ||
abstract: "Launch a build server for Swift Packages" | ||
) | ||
|
||
@OptionGroup(visibility: .hidden) | ||
var globalOptions: GlobalOptions | ||
|
||
func run(_ swiftCommandState: SwiftCommandState) async throws { | ||
// Dup stdout and redirect the fd to stderr so that a careless print() | ||
// will not break our connection stream. | ||
let realStdout = dup(STDOUT_FILENO) | ||
if realStdout == -1 { | ||
fatalError("failed to dup stdout: \(strerror(errno)!)") | ||
} | ||
if dup2(STDERR_FILENO, STDOUT_FILENO) == -1 { | ||
fatalError("failed to redirect stdout -> stderr: \(strerror(errno)!)") | ||
} | ||
|
||
let realStdoutHandle = FileHandle(fileDescriptor: realStdout, closeOnDealloc: false) | ||
|
||
let clientConnection = JSONRPCConnection( | ||
name: "client", | ||
protocol: bspRegistry, | ||
inFD: FileHandle.standardInput, | ||
outFD: realStdoutHandle, | ||
inputMirrorFile: nil, | ||
outputMirrorFile: nil | ||
) | ||
|
||
guard let buildSystem = try await swiftCommandState.createBuildSystem() as? SwiftBuildSystem else { | ||
print("Build server requires --build-system swiftbuild") | ||
Self.exit() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should exit with whatever error SwiftArgumentParser exits with when validate() fails |
||
} | ||
|
||
guard let packagePath = try swiftCommandState.getWorkspaceRoot().packages.first else { | ||
throw StringError("unknown package") | ||
} | ||
|
||
let server = try await SwiftPMBuildServer(packageRoot: packagePath, buildSystem: buildSystem, workspace: swiftCommandState.getActiveWorkspace(), connectionToClient: clientConnection, exitHandler: {_ in Self.exit() }) | ||
clientConnection.start( | ||
receiveHandler: server, | ||
closeHandler: { | ||
Self.exit() | ||
} | ||
) | ||
|
||
// Park the main function by sleeping for 10 years. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cleaner solution? await withCheckedContinuation { continuation in
clientConnection.start(
receiveHandler: server,
closeHandler: {
continuation.resume()
}
)
} |
||
while true { | ||
try? await Task.sleep(for: .seconds(60 * 60 * 24 * 365 * 10)) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,7 +109,9 @@ func withSession( | |
} | ||
} | ||
|
||
private final class PlanningOperationDelegate: SWBPlanningOperationDelegate, Sendable { | ||
package final class SwiftBuildSystemPlanningOperationDelegate: SWBPlanningOperationDelegate, SWBIndexingDelegate, Sendable { | ||
package init() {} | ||
|
||
public func provisioningTaskInputs( | ||
targetGUID: String, | ||
provisioningSourceData: SWBProvisioningTaskInputsSourceData | ||
|
@@ -194,7 +196,7 @@ public struct PluginConfiguration { | |
} | ||
|
||
public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { | ||
private let buildParameters: BuildParameters | ||
package let buildParameters: BuildParameters | ||
private let packageGraphLoader: () async throws -> ModulesGraph | ||
private let packageManagerResourcesDirectory: Basics.AbsolutePath? | ||
private let logLevel: Basics.Diagnostic.Severity | ||
|
@@ -349,7 +351,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { | |
) | ||
} | ||
|
||
try await writePIF(buildParameters: buildParameters) | ||
try await writePIF() | ||
|
||
return try await startSWBuildOperation( | ||
pifTargetName: subset.pifTargetName, | ||
|
@@ -545,7 +547,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { | |
|
||
let operation = try await session.createBuildOperation( | ||
request: request, | ||
delegate: PlanningOperationDelegate() | ||
delegate: SwiftBuildSystemPlanningOperationDelegate() | ||
) | ||
|
||
var buildState = BuildState() | ||
|
@@ -631,7 +633,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { | |
) | ||
} | ||
|
||
private func makeBuildParameters(session: SWBBuildServiceSession, genSymbolGraph: Bool) async throws -> SwiftBuild.SWBBuildParameters { | ||
package func makeBuildParameters(session: SWBBuildServiceSession, genSymbolGraph: Bool) async throws -> SwiftBuild.SWBBuildParameters { | ||
// Generate the run destination parameters. | ||
let runDestination = makeRunDestination() | ||
|
||
|
@@ -767,8 +769,8 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { | |
buildProductsPath: ddPathPrefix + "/Products", | ||
buildIntermediatesPath: ddPathPrefix + "/Intermediates.noindex", | ||
pchPath: ddPathPrefix + "/PCH", | ||
indexRegularBuildProductsPath: nil, | ||
indexRegularBuildIntermediatesPath: nil, | ||
indexRegularBuildProductsPath: ddPathPrefix + "/Index/Products", | ||
indexRegularBuildIntermediatesPath: ddPathPrefix + "/Index/Intermediates.noindex", | ||
Comment on lines
+772
to
+773
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name is a little confusing here, but FWIW this is actually meant to be swapped for the index arena 😅 - the build products/intermediate paths would be the index arena and the "index regular" paths the regular build. It's also only used for adding an overlay to fallback to the build, which we don't necessarily need. |
||
indexPCHPath: ddPathPrefix, | ||
indexDataStoreFolderPath: ddPathPrefix, | ||
indexEnableDataStore: request.parameters.arenaInfo?.indexEnableDataStore ?? false | ||
|
@@ -902,7 +904,9 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { | |
} | ||
} | ||
|
||
public func writePIF(buildParameters: BuildParameters) async throws { | ||
public func writePIF() async throws { | ||
pifBuilder = .init() | ||
packageGraph = .init() | ||
let pifBuilder = try await getPIFBuilder() | ||
let pif = try await pifBuilder.generatePIF( | ||
printPIFManifestGraphviz: buildParameters.printPIFManifestGraphviz, | ||
|
@@ -912,6 +916,27 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { | |
try self.fileSystem.writeIfChanged(path: buildParameters.pifManifest, string: pif) | ||
} | ||
|
||
package struct LongLivedBuildServiceSession { | ||
package var session: SWBBuildServiceSession | ||
package var diagnostics: [SwiftBuildMessage.DiagnosticInfo] | ||
package var teardownHandler: () async throws -> Void | ||
} | ||
|
||
package func createLongLivedSession(name: String) async throws -> LongLivedBuildServiceSession { | ||
let service = try await SWBBuildService(connectionMode: .inProcessStatic(swiftbuildServiceEntryPoint)) | ||
do { | ||
let (session, diagnostics) = try await createSession(service: service, name: name, toolchainPath: buildParameters.toolchain.toolchainDir, packageManagerResourcesDirectory: packageManagerResourcesDirectory) | ||
let teardownHandler = { | ||
try await session.close() | ||
await service.close() | ||
} | ||
return LongLivedBuildServiceSession(session: session, diagnostics: diagnostics, teardownHandler: teardownHandler) | ||
} catch { | ||
await service.close() | ||
throw error | ||
} | ||
} | ||
|
||
public func cancel(deadline: DispatchTime) throws {} | ||
|
||
/// Returns the package graph using the graph loader closure. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift.org open source project | ||
// | ||
// Copyright (c) 2014 - 2021 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 canImport(Glibc) | ||
import Glibc | ||
#elseif canImport(Musl) | ||
import Musl | ||
#elseif canImport(Android) | ||
import Android | ||
#endif | ||
|
||
#if canImport(Glibc) || canImport(Musl) || canImport(Android) | ||
// This is a lazily initialised global variable that when read for the first time, will ignore SIGPIPE. | ||
private let globallyIgnoredSIGPIPE: Bool = { | ||
/* no F_SETNOSIGPIPE on Linux :( */ | ||
_ = signal(SIGPIPE, SIG_IGN) | ||
return true | ||
}() | ||
#endif | ||
|
||
/// We receive a `SIGPIPE` if we write to a pipe that points to a crashed process. This in particular happens if the | ||
/// target of a `JSONRPCConnection` has crashed and we try to send it a message or if swift-format crashes and we try | ||
/// to send the source file to it. | ||
/// | ||
/// On Darwin, `DispatchIO` ignores `SIGPIPE` for the pipes handled by it and swift-tools-support-core offers | ||
/// `LocalFileOutputByteStream.disableSigpipe`, but that features is not available on Linux. | ||
/// | ||
/// Instead, globally ignore `SIGPIPE` on Linux to prevent us from crashing if the `JSONRPCConnection`'s target crashes. | ||
/// | ||
/// On Darwin platforms and on Windows this is a no-op. | ||
package func globallyDisableSigpipeIfNeeded() { | ||
#if !canImport(Darwin) && !os(Windows) | ||
let haveWeIgnoredSIGPIEThisIsHereToTriggerIgnoringIt = globallyIgnoredSIGPIPE | ||
guard haveWeIgnoredSIGPIEThisIsHereToTriggerIgnoringIt else { | ||
fatalError("globallyIgnoredSIGPIPE should always be true") | ||
} | ||
#endif | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We depend on Swift System, right? I suggest using FileDescriptor.duplicate cover API and not going straight down to POSIX.
IIRC there's also some oddities with the Windows versions of dup which System should be able to help smooth over.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, we’re doing this in SourceKit-LSP and it hasn’t caused any issues so far.