Skip to content

Commit f525da5

Browse files
committed
Use the new SwiftPM API to load the build plan
We previously skipped building/running tool plugins here, which meant that the compiler arguments for a target also missed any generated sources. Use the new `BuildDescription.load` API from SwiftPM to address this. Resolves rdar://102242345.
1 parent d5978d5 commit f525da5

File tree

4 files changed

+359
-29
lines changed

4 files changed

+359
-29
lines changed

Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ package import BuildServerProtocol
3333
package import Foundation
3434
package import LanguageServerProtocol
3535
package import SKOptions
36-
package import SourceKitLSPAPI
36+
@preconcurrency package import SourceKitLSPAPI
3737
package import ToolchainRegistry
3838
package import class ToolchainRegistry.Toolchain
3939
#else
4040
import BuildServerProtocol
4141
import Foundation
4242
import LanguageServerProtocol
4343
import SKOptions
44-
import SourceKitLSPAPI
44+
@preconcurrency import SourceKitLSPAPI
4545
import ToolchainRegistry
4646
import class ToolchainRegistry.Toolchain
4747
#endif
@@ -131,6 +131,8 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
131131
private let toolchain: Toolchain
132132
private let swiftPMWorkspace: Workspace
133133

134+
private let pluginConfiguration: PluginConfiguration
135+
134136
/// A `ObservabilitySystem` from `SwiftPM` that logs.
135137
private let observabilitySystem: ObservabilitySystem
136138

@@ -170,13 +172,10 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
170172
) async throws {
171173
self.projectRoot = projectRoot
172174
self.options = options
173-
self.fileWatchers =
174-
try ["Package.swift", "Package@swift*.swift", "Package.resolved"].map {
175-
FileSystemWatcher(globPattern: try projectRoot.appendingPathComponent($0).filePath, kind: [.change])
176-
}
177-
+ FileRuleDescription.builtinRules.flatMap({ $0.fileTypes }).map { fileExtension in
178-
FileSystemWatcher(globPattern: "**/*.\(fileExtension)", kind: [.create, .change, .delete])
179-
}
175+
// We could theoretically dynamically register all known files when we get back the build graph, but that seems
176+
// more errorprone than just watching everything and then filtering when we need to (eg. in
177+
// `SemanticIndexManager.filesDidChange`).
178+
self.fileWatchers = [FileSystemWatcher(globPattern: "**/*", kind: [.create, .change, .delete])]
180179
let toolchain = await toolchainRegistry.preferredToolchain(containing: [
181180
\.clang, \.clangd, \.sourcekitd, \.swift, \.swiftc,
182181
])
@@ -291,6 +290,19 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
291290
prepareForIndexing: options.backgroundPreparationModeOrDefault.toSwiftPMPreparation
292291
)
293292

293+
let pluginScriptRunner = DefaultPluginScriptRunner(
294+
fileSystem: localFileSystem,
295+
cacheDir: location.pluginWorkingDirectory.appending("cache"),
296+
toolchain: hostSwiftPMToolchain,
297+
extraPluginSwiftCFlags: [],
298+
enableSandbox: !(options.swiftPMOrDefault.disableSandbox ?? false)
299+
)
300+
self.pluginConfiguration = PluginConfiguration(
301+
scriptRunner: pluginScriptRunner,
302+
workDirectory: location.pluginWorkingDirectory,
303+
disableSandbox: options.swiftPMOrDefault.disableSandbox ?? false
304+
)
305+
294306
packageLoadingQueue.async {
295307
await orLog("Initial package loading") {
296308
// Schedule an initial generation of the build graph. Once the build graph is loaded, the build system will send
@@ -334,24 +346,47 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
334346

335347
signposter.emitEvent("Finished loading modules graph", id: signpostID)
336348

337-
let plan = try await BuildPlan(
338-
destinationBuildParameters: destinationBuildParameters,
339-
toolsBuildParameters: toolsBuildParameters,
340-
graph: modulesGraph,
341-
disableSandbox: options.swiftPMOrDefault.disableSandbox ?? false,
342-
fileSystem: localFileSystem,
343-
observabilityScope: observabilitySystem.topScope.makeChildScope(description: "Create SwiftPM build plan")
344-
)
349+
// We have a whole separate arena if we're performing background indexing. This allows us to also build and run
350+
// plugins, without having to worry about messing up any regular build state.
351+
let buildDescription: SourceKitLSPAPI.BuildDescription
352+
if isForIndexBuild {
353+
let loaded = try await BuildDescription.load(
354+
destinationBuildParameters: destinationBuildParameters,
355+
toolsBuildParameters: toolsBuildParameters,
356+
packageGraph: modulesGraph,
357+
pluginConfiguration: pluginConfiguration,
358+
disableSandbox: options.swiftPMOrDefault.disableSandbox ?? false,
359+
scratchDirectory: swiftPMWorkspace.location.scratchDirectory.asURL,
360+
fileSystem: localFileSystem,
361+
observabilityScope: observabilitySystem.topScope.makeChildScope(description: "Create SwiftPM build description")
362+
)
363+
if !loaded.errors.isEmpty {
364+
logger.error("Loading SwiftPM description had errors: \(loaded.errors)")
365+
}
345366

346-
signposter.emitEvent("Finished generating build plan", id: signpostID)
367+
signposter.emitEvent("Finished generating build description", id: signpostID)
347368

348-
let buildDescription = BuildDescription(buildPlan: plan)
349-
self.buildDescription = buildDescription
369+
buildDescription = loaded.description
370+
} else {
371+
let plan = try await BuildPlan(
372+
destinationBuildParameters: destinationBuildParameters,
373+
toolsBuildParameters: toolsBuildParameters,
374+
graph: modulesGraph,
375+
disableSandbox: options.swiftPMOrDefault.disableSandbox ?? false,
376+
fileSystem: localFileSystem,
377+
observabilityScope: observabilitySystem.topScope.makeChildScope(description: "Create SwiftPM build plan")
378+
)
379+
380+
signposter.emitEvent("Finished generating build plan", id: signpostID)
381+
382+
buildDescription = BuildDescription(buildPlan: plan)
383+
}
350384

351385
/// Make sure to execute any throwing statements before setting any
352386
/// properties because otherwise we might end up in an inconsistent state
353387
/// with only some properties modified.
354388

389+
self.buildDescription = buildDescription
355390
self.swiftPMTargets = [:]
356391
self.targetDependencies = [:]
357392

Sources/SKTestSupport/SkipUnless.swift

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,85 @@ package actor SkipUnless {
270270
}
271271
}
272272
}
273+
274+
package static func canLoadPluginsBuiltByToolchain(
275+
file: StaticString = #filePath,
276+
line: UInt = #line
277+
) async throws {
278+
return try await shared.skipUnlessSupported(file: file, line: line) {
279+
let project = try await SwiftPMTestProject(
280+
files: [
281+
"Plugins/plugin.swift": #"""
282+
import Foundation
283+
import PackagePlugin
284+
@main struct CodeGeneratorPlugin: BuildToolPlugin {
285+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
286+
let genSourcesDir = context.pluginWorkDirectoryURL.appending(path: "GeneratedSources")
287+
guard let target = target as? SourceModuleTarget else { return [] }
288+
let codeGenerator = try context.tool(named: "CodeGenerator").url
289+
let generatedFile = genSourcesDir.appending(path: "\(target.name)-generated.swift")
290+
return [.buildCommand(
291+
displayName: "Generating code for \(target.name)",
292+
executable: codeGenerator,
293+
arguments: [
294+
generatedFile.path
295+
],
296+
inputFiles: [],
297+
outputFiles: [generatedFile]
298+
)]
299+
}
300+
}
301+
"""#,
302+
303+
"Sources/CodeGenerator/CodeGenerator.swift": #"""
304+
import Foundation
305+
try "let foo = 1".write(
306+
to: URL(fileURLWithPath: CommandLine.arguments[1]),
307+
atomically: true,
308+
encoding: String.Encoding.utf8
309+
)
310+
"""#,
311+
312+
"Sources/TestLib/TestLib.swift": #"""
313+
func useGenerated() {
314+
_ = 1️⃣foo
315+
}
316+
"""#,
317+
],
318+
manifest: """
319+
// swift-tools-version: 6.0
320+
import PackageDescription
321+
let package = Package(
322+
name: "PluginTest",
323+
targets: [
324+
.executableTarget(name: "CodeGenerator"),
325+
.target(
326+
name: "TestLib",
327+
plugins: [.plugin(name: "CodeGeneratorPlugin")]
328+
),
329+
.plugin(
330+
name: "CodeGeneratorPlugin",
331+
capability: .buildTool(),
332+
dependencies: ["CodeGenerator"]
333+
),
334+
]
335+
)
336+
""",
337+
enableBackgroundIndexing: true
338+
)
339+
340+
let (uri, positions) = try project.openDocument("TestLib.swift")
341+
342+
let result = try await project.testClient.send(
343+
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
344+
)
345+
346+
if result?.locations?.only == nil {
347+
return .featureUnsupported(skipMessage: "Skipping because plugin protocols do not match.")
348+
}
349+
return .featureSupported
350+
}
351+
}
273352
}
274353

275354
// MARK: - Parsing Swift compiler version

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import LanguageServerProtocolExtensions
14+
1315
#if compiler(>=6)
1416
package import BuildServerProtocol
1517
package import BuildSystemIntegration
@@ -385,15 +387,28 @@ package final actor SemanticIndexManager {
385387
let changedFiles = events.map(\.uri)
386388
await indexStoreUpToDateTracker.markOutOfDate(changedFiles)
387389

388-
let targets = await changedFiles.asyncMap { await buildSystemManager.targets(for: $0) }.flatMap { $0 }
389-
let dependentTargets = await buildSystemManager.targets(dependingOn: Set(targets))
390-
logger.info(
391-
"""
392-
Marking targets as out-of-date: \
393-
\(String(dependentTargets.map(\.uri.stringValue).joined(separator: ", ")))
394-
"""
395-
)
396-
await preparationUpToDateTracker.markOutOfDate(dependentTargets)
390+
// Preparation tracking should be per file. For now consider any non-known-language change as having to re-prepare
391+
// the target itself so that we re-prepare potential input files to plugins.
392+
// https://github.com/swiftlang/sourcekit-lsp/issues/1975
393+
var outOfDateTargets = Set<BuildTargetIdentifier>()
394+
for file in changedFiles {
395+
let changedTargets = await buildSystemManager.targets(for: file)
396+
if Language(inferredFromFileExtension: file) == nil {
397+
outOfDateTargets.formUnion(changedTargets)
398+
}
399+
400+
let dependentTargets = await buildSystemManager.targets(dependingOn: changedTargets)
401+
outOfDateTargets.formUnion(dependentTargets)
402+
}
403+
if !outOfDateTargets.isEmpty {
404+
logger.info(
405+
"""
406+
Marking dependent targets as out-of-date: \
407+
\(String(outOfDateTargets.map(\.uri.stringValue).joined(separator: ", ")))
408+
"""
409+
)
410+
await preparationUpToDateTracker.markOutOfDate(outOfDateTargets)
411+
}
397412

398413
await scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(
399414
filesToIndex: changedFiles,

0 commit comments

Comments
 (0)