Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Fixtures/Miscellaneous/VersionSpecificManifest/Foo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
public func foo() {
{}()
}
12 changes: 12 additions & 0 deletions Fixtures/Miscellaneous/VersionSpecificManifest/Package.swift
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: "./"),
]
)
26 changes: 26 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,23 @@ let package = Package(
]
),

// MARK: BSP
.target(
name: "SwiftPMBuildServer",
dependencies: [
"Basics",
"Build",
"PackageGraph",
"PackageLoading",
"PackageModel",
"SPMBuildCore",
"SourceControl",
"SourceKitLSPAPI",
"SwiftBuildSupport",
.product(name: "SWBBuildServerProtocol", package: "swift-build"),
] + swiftTSCBasicsDeps
),

// MARK: Commands

.target(
Expand Down Expand Up @@ -613,6 +630,7 @@ let package = Package(
"XCBuildSupport",
"SwiftBuildSupport",
"SwiftFixIt",
"SwiftPMBuildServer",
] + swiftSyntaxDependencies(["SwiftIDEUtils", "SwiftRefactor"]),
exclude: ["CMakeLists.txt", "README.md"],
swiftSettings: swift6CompatibleExperimentalFeatures + [
Expand Down Expand Up @@ -1053,6 +1071,14 @@ if ProcessInfo.processInfo.environment["SWIFTCI_DISABLE_SDK_DEPENDENT_TESTS"] ==
"dummy-swiftc",
]
),
.testTarget(
name: "SwiftPMBuildServerTests",
dependencies: [
"SwiftPMBuildServer",
"_InternalTestSupport",
.product(name: "SWBBuildServerProtocol", package: "swift-build"),
]
),
])
}

Expand Down
76 changes: 76 additions & 0 deletions Sources/Commands/PackageCommands/BuildServer.swift
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)
Copy link
Contributor

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.

Copy link
Member

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.

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()
Copy link
Contributor

Choose a reason for hiding this comment

The 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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Up @@ -39,6 +39,7 @@ public struct SwiftPackageCommand: AsyncParsableCommand {
AddTargetDependency.self,
AddSetting.self,
AuditBinaryArtifact.self,
BuildServer.self,
Clean.self,
PurgeCache.self,
Reset.self,
Expand Down
41 changes: 33 additions & 8 deletions Sources/SwiftBuildSupport/SwiftBuildSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down
47 changes: 47 additions & 0 deletions Sources/SwiftPMBuildServer/DisableSigpipe.swift
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
}
Loading