Skip to content

Commit 71ab073

Browse files
committed
Add a new BuildDescription.load API for use in SourceKit-LSP
Build tool plugins need to be built and run as part of the build plan generation. Rather than relying on clients to do so, add a new API that does both. This does unfortunately expose much more of the SwiftPM API than previously, but given it is all required to a generate a `BuildPlan`, this was really already the case. Long term this module should be replaced by a long-running BSP server that can prepare and provide build settings in the one process. Resolves rdar://102242345.
1 parent 71f9ecc commit 71ab073

File tree

8 files changed

+163
-42
lines changed

8 files changed

+163
-42
lines changed

Package.swift

+4
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,11 @@ let package = Package(
169169
.target(
170170
name: "SourceKitLSPAPI",
171171
dependencies: [
172+
"Basics",
172173
"Build",
174+
"PackageGraph",
175+
"PackageLoading",
176+
"PackageModel",
173177
"SPMBuildCore",
174178
],
175179
exclude: ["CMakeLists.txt"],

Sources/Build/BuildOperation.swift

+15-11
Original file line numberDiff line numberDiff line change
@@ -309,12 +309,12 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
309309
}
310310
}
311311
// We need to perform actual planning if we reach here.
312-
return try await self.plan(subset: subset).description
312+
return try await self.generateDescription(subset: subset).description
313313
}
314314
}
315315

316316
public func getBuildManifest() async throws -> LLBuildManifest {
317-
try await self.plan().manifest
317+
try await self.generateDescription().manifest
318318
}
319319

320320
/// Cancel the active build operation.
@@ -647,9 +647,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
647647
}
648648
}
649649

650-
/// Create the build plan and return the build description.
651-
private func plan(subset: BuildSubset? = nil) async throws -> (description: BuildDescription, manifest: LLBuildManifest) {
652-
// Load the package graph.
650+
package func generatePlan() async throws -> BuildPlan {
653651
let graph = try await getPackageGraph()
654652

655653
let pluginTools: [ResolvedModule.ID: [String: PluginTool]]
@@ -672,7 +670,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
672670
}
673671

674672
// Create the build plan based on the modules graph and any information from plugins.
675-
let plan = try await BuildPlan(
673+
return try await BuildPlan(
676674
destinationBuildParameters: self.config.destinationBuildParameters,
677675
toolsBuildParameters: self.config.buildParameters(for: .host),
678676
graph: graph,
@@ -684,23 +682,29 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
684682
fileSystem: self.fileSystem,
685683
observabilityScope: self.observabilityScope
686684
)
685+
686+
}
687+
688+
/// Create the build plan and return the build description.
689+
private func generateDescription(subset: BuildSubset? = nil) async throws -> (description: BuildDescription, manifest: LLBuildManifest) {
690+
let plan = try await generatePlan()
687691
self._buildPlan = plan
688692

689693
// Emit warnings about any unhandled files in authored packages. We do this after applying build tool plugins, once we know what files they handled.
690694
// rdar://113256834 This fix works for the plugins that do not have PreBuildCommands.
691695
let targetsToConsider: [ResolvedModule]
692696
if let subset = subset, let recursiveDependencies = try
693-
subset.recursiveDependencies(for: graph, observabilityScope: observabilityScope) {
697+
subset.recursiveDependencies(for: plan.graph, observabilityScope: observabilityScope) {
694698
targetsToConsider = recursiveDependencies
695699
} else {
696-
targetsToConsider = Array(graph.reachableModules)
700+
targetsToConsider = Array(plan.graph.reachableModules)
697701
}
698702

699703
for module in targetsToConsider {
700704
// Subtract out any that were inputs to any commands generated by plugins.
701705
if let pluginResults = plan.buildToolPluginInvocationResults[module.id] {
702706
diagnoseUnhandledFiles(
703-
modulesGraph: graph,
707+
modulesGraph: plan.graph,
704708
module: module,
705709
buildToolPluginInvocationResults: pluginResults
706710
)
@@ -851,7 +855,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
851855

852856
public func packageStructureChanged() async -> Bool {
853857
do {
854-
_ = try await self.plan()
858+
_ = try await self.generateDescription()
855859
}
856860
catch Diagnostics.fatalError {
857861
return false
@@ -947,7 +951,7 @@ extension BuildOperation {
947951
// Determine the tools to which this plugin has access, and create a name-to-path mapping from tool
948952
// names to the corresponding paths. Built tools are assumed to be in the build tools directory.
949953
let accessibleTools = try await plugin.preparePluginTools(
950-
fileSystem: fileSystem,
954+
fileSystem: config.fileSystem,
951955
environment: config.buildEnvironment(for: .host),
952956
for: hostTriple
953957
) { name, path in

Sources/CoreCommands/Options.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ package struct TraitOptions: ParsableArguments {
654654
/// The traits to enable for the package.
655655
@Option(
656656
name: .customLong("traits"),
657-
help: "Enables the passed traits of the package. Multiple traits can be specified by providing a space separated list e.g. `--traits Trait1 Trait2`. When enabling specific traits the defaults traits need to explictily enabled as well by passing `defaults` to this command."
657+
help: "Enables the passed traits of the package. Multiple traits can be specified by providing a comma separated list e.g. `--traits Trait1,Trait2`. When enabling specific traits the defaults traits need to explictily enabled as well by passing `defaults` to this command."
658658
)
659659
package var _enabledTraits: String?
660660

Sources/PackageGraph/TraitConfiguration.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public struct TraitConfiguration: Codable, Hashable {
1818
/// Enables all traits of the package.
1919
package var enableAllTraits: Bool
2020

21-
package init(
21+
public init(
2222
enabledTraits: Set<String>? = nil,
2323
enableAllTraits: Bool = false
2424
) {

Sources/SourceKitLSPAPI/BuildDescription.swift

+67-20
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,17 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
import struct Foundation.URL
14-
15-
private import struct Basics.AbsolutePath
16-
private import func Basics.resolveSymlinks
17-
18-
internal import SPMBuildCore
19-
20-
// FIXME: should import these module with `private` or `internal` access control
21-
import class Build.BuildPlan
22-
import class Build.ClangModuleBuildDescription
23-
import class Build.SwiftModuleBuildDescription
24-
import struct PackageGraph.ResolvedModule
25-
import struct PackageGraph.ModulesGraph
26-
internal import class PackageModel.UserToolchain
13+
import Foundation
14+
import TSCBasic
15+
16+
// Ideally wouldn't expose these (it defeats the purpose of this module), but we should replace this entire API with
17+
// a BSP server, so this is good enough for now (and LSP is using all these types internally anyway).
18+
import Basics
19+
import Build
20+
import PackageGraph
21+
internal import PackageLoading
22+
internal import PackageModel
23+
import SPMBuildCore
2724

2825
public enum BuildDestination {
2926
case host
@@ -90,7 +87,13 @@ private struct WrappedClangTargetBuildDescription: BuildTarget {
9087
}
9188

9289
var others: [URL] {
93-
return description.others.map(\.asURL)
90+
var others = Set(description.others)
91+
for pluginResult in description.buildToolPluginInvocationResults {
92+
for buildCommand in pluginResult.buildCommands {
93+
others.formUnion(buildCommand.inputFiles)
94+
}
95+
}
96+
return others.map(\.asURL)
9497
}
9598

9699
public var name: String {
@@ -102,7 +105,7 @@ private struct WrappedClangTargetBuildDescription: BuildTarget {
102105
}
103106

104107
public func compileArguments(for fileURL: URL) throws -> [String] {
105-
let filePath = try resolveSymlinks(try AbsolutePath(validating: fileURL.path))
108+
let filePath = try resolveSymlinks(try Basics.AbsolutePath(validating: fileURL.path))
106109
let commandLine = try description.emitCommandLine(for: filePath)
107110
// First element on the command line is the compiler itself, not an argument.
108111
return Array(commandLine.dropFirst())
@@ -143,7 +146,13 @@ private struct WrappedSwiftTargetBuildDescription: BuildTarget {
143146
}
144147

145148
var others: [URL] {
146-
return description.others.map(\.asURL)
149+
var others = Set(description.others)
150+
for pluginResult in description.buildToolPluginInvocationResults {
151+
for buildCommand in pluginResult.buildCommands {
152+
others.formUnion(buildCommand.inputFiles)
153+
}
154+
}
155+
return others.map(\.asURL)
147156
}
148157

149158
func compileArguments(for fileURL: URL) throws -> [String] {
@@ -160,14 +169,52 @@ public struct BuildDescription {
160169

161170
/// The inputs of the build plan so we don't need to re-compute them on every call to
162171
/// `fileAffectsSwiftOrClangBuildSettings`.
163-
private let inputs: [BuildPlan.Input]
172+
private let inputs: [Build.BuildPlan.Input]
164173

165-
// FIXME: should not use `BuildPlan` in the public interface
174+
/// Wrap an already constructed build plan.
166175
public init(buildPlan: Build.BuildPlan) {
167176
self.buildPlan = buildPlan
168177
self.inputs = buildPlan.inputs
169178
}
170179

180+
/// Construct a build description, compiling build tool plugins and generating their output when necessary.
181+
public static func load(
182+
destinationBuildParameters: BuildParameters,
183+
toolsBuildParameters: BuildParameters,
184+
packageGraph: ModulesGraph,
185+
pluginConfiguration: PluginConfiguration,
186+
traitConfiguration: TraitConfiguration,
187+
disableSandbox: Bool,
188+
scratchDirectory: URL,
189+
fileSystem: any FileSystem,
190+
observabilityScope: ObservabilityScope
191+
) async throws -> (description: BuildDescription, errors: String) {
192+
let bufferedOutput = BufferedOutputByteStream()
193+
let threadSafeOutput = ThreadSafeOutputByteStream(bufferedOutput)
194+
195+
// This is quite an abuse of `BuildOperation`, building plugins should really be refactored out of it. Though
196+
// even better would be to have a BSP server that handles both preparing and getting settings.
197+
// https://github.com/swiftlang/swift-package-manager/issues/8287
198+
let operation = BuildOperation(
199+
productsBuildParameters: destinationBuildParameters,
200+
toolsBuildParameters: toolsBuildParameters,
201+
cacheBuildManifest: true,
202+
packageGraphLoader: { packageGraph },
203+
pluginConfiguration: pluginConfiguration,
204+
scratchDirectory: try Basics.AbsolutePath(validating: scratchDirectory.path),
205+
traitConfiguration: traitConfiguration,
206+
additionalFileRules: FileRuleDescription.swiftpmFileTypes,
207+
pkgConfigDirectories: [],
208+
outputStream: threadSafeOutput,
209+
logLevel: .error,
210+
fileSystem: fileSystem,
211+
observabilityScope: observabilityScope
212+
)
213+
214+
let plan = try await operation.generatePlan()
215+
return (BuildDescription(buildPlan: plan), bufferedOutput.bytes.description)
216+
}
217+
171218
func getBuildTarget(
172219
for module: ResolvedModule,
173220
destination: BuildParameters.Destination
@@ -219,7 +266,7 @@ public struct BuildDescription {
219266
/// Returns `true` if the file at the given path might influence build settings for a `swiftc` or `clang` invocation
220267
/// generated by SwiftPM.
221268
public func fileAffectsSwiftOrClangBuildSettings(_ url: URL) -> Bool {
222-
guard let filePath = try? AbsolutePath(validating: url.path) else {
269+
guard let filePath = try? Basics.AbsolutePath(validating: url.path) else {
223270
return false
224271
}
225272

Sources/SourceKitLSPAPI/CMakeLists.txt

+5
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ add_library(SourceKitLSPAPI STATIC
1010
BuildDescription.swift
1111
PluginTargetBuildDescription.swift)
1212
target_link_libraries(SourceKitLSPAPI PUBLIC
13+
Basics
1314
Build
15+
PackageGraph
1416
SPMBuildCore)
17+
target_link_libraries(SourceKitLSPAPI PRIVATE
18+
PackageLoading
19+
PackageModel)
1520

1621
# NOTE(compnerd) workaround for CMake not setting up include flags yet
1722
set_target_properties(SourceKitLSPAPI PROPERTIES

Sources/SourceKitLSPAPI/PluginTargetBuildDescription.swift

+5-8
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,12 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
private import Basics
13+
import Foundation
1414

15-
import struct Foundation.URL
16-
17-
import struct PackageGraph.ResolvedModule
18-
19-
private import class PackageLoading.ManifestLoader
20-
internal import struct PackageModel.ToolsVersion
21-
internal import protocol PackageModel.Toolchain
15+
import Basics
16+
import PackageGraph
17+
internal import PackageLoading
18+
internal import PackageModel
2219

2320
struct PluginTargetBuildDescription: BuildTarget {
2421
private let target: ResolvedModule

Tests/SourceKitLSPAPITests/SourceKitLSPAPITests.swift

+65-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import PackageGraph
1818

1919
import PackageModel
2020
@testable import SourceKitLSPAPI
21-
import struct SPMBuildCore.BuildParameters
21+
import SPMBuildCore
2222
import _InternalTestSupport
2323
import XCTest
2424

@@ -255,6 +255,70 @@ final class SourceKitLSPAPITests: XCTestCase {
255255
]
256256
)
257257
}
258+
259+
func testLoadPackage() async throws {
260+
let fs = InMemoryFileSystem(emptyFiles:
261+
"/Pkg/Sources/lib/lib.swift"
262+
)
263+
264+
let observability = ObservabilitySystem.makeForTesting()
265+
let graph = try loadModulesGraph(
266+
fileSystem: fs,
267+
manifests: [
268+
Manifest.createRootManifest(
269+
displayName: "Pkg",
270+
path: "/Pkg",
271+
toolsVersion: .v5_10,
272+
targets: [
273+
TargetDescription(
274+
name: "lib",
275+
dependencies: []
276+
)
277+
]),
278+
],
279+
observabilityScope: observability.topScope
280+
)
281+
XCTAssertNoDiagnostics(observability.diagnostics)
282+
283+
let destinationBuildParameters = mockBuildParameters(destination: .target)
284+
try await withTemporaryDirectory { tmpDir in
285+
let pluginConfiguration = PluginConfiguration(
286+
scriptRunner: DefaultPluginScriptRunner(
287+
fileSystem: fs,
288+
cacheDir: tmpDir.appending("cache"),
289+
toolchain: try UserToolchain.default
290+
),
291+
workDirectory: tmpDir.appending("work"),
292+
disableSandbox: false
293+
)
294+
let scratchDirectory = tmpDir.appending(".build")
295+
296+
let loaded = try await BuildDescription.load(
297+
destinationBuildParameters: destinationBuildParameters,
298+
toolsBuildParameters: mockBuildParameters(destination: .host),
299+
packageGraph: graph,
300+
pluginConfiguration: pluginConfiguration,
301+
traitConfiguration: TraitConfiguration(),
302+
disableSandbox: false,
303+
scratchDirectory: scratchDirectory.asURL,
304+
fileSystem: fs,
305+
observabilityScope: observability.topScope
306+
)
307+
308+
try loaded.description.checkArguments(
309+
for: "lib",
310+
graph: graph,
311+
partialArguments: [
312+
"-module-name", "lib",
313+
"-package-name", "pkg",
314+
"-emit-dependencies",
315+
"-emit-module",
316+
"-emit-module-path", "/path/to/build/\(destinationBuildParameters.triple)/debug/Modules/lib.swiftmodule"
317+
],
318+
isPartOfRootPackage: true
319+
)
320+
}
321+
}
258322
}
259323

260324
extension SourceKitLSPAPI.BuildDescription {

0 commit comments

Comments
 (0)