Skip to content

Commit fda2bbe

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 fda2bbe

File tree

3 files changed

+245
-29
lines changed

3 files changed

+245
-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/SemanticIndex/SemanticIndexManager.swift

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -385,15 +385,36 @@ package final actor SemanticIndexManager {
385385
let changedFiles = events.map(\.uri)
386386
await indexStoreUpToDateTracker.markOutOfDate(changedFiles)
387387

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

398419
await scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(
399420
filesToIndex: changedFiles,

Tests/SourceKitLSPTests/SwiftPMIntegrationTests.swift

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,4 +359,164 @@ final class SwiftPMIntegrationTests: XCTestCase {
359359
["Cannot convert value of type 'Int' to specified type 'String'"]
360360
)
361361
}
362+
363+
func testToolPluginWithBuild() async throws {
364+
let project = try await SwiftPMTestProject(
365+
files: [
366+
"Plugins/plugin.swift": #"""
367+
import PackagePlugin
368+
@main struct CodeGeneratorPlugin: BuildToolPlugin {
369+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
370+
let genSourcesDir = context.pluginWorkDirectoryURL.appending(path: "GeneratedSources")
371+
guard let target = target as? SourceModuleTarget else { return [] }
372+
let codeGenerator = try context.tool(named: "CodeGenerator").url
373+
let generatedFile = genSourcesDir.appending(path: "\(target.name)-generated.swift")
374+
return [.buildCommand(
375+
displayName: "Generating code for \(target.name)",
376+
executable: codeGenerator,
377+
arguments: [
378+
generatedFile.path
379+
],
380+
inputFiles: [],
381+
outputFiles: [generatedFile]
382+
)]
383+
}
384+
}
385+
"""#,
386+
387+
"Sources/CodeGenerator/CodeGenerator.swift": #"""
388+
import Foundation
389+
try "let generated = 1".write(to: URL(fileURLWithPath: CommandLine.arguments[1]), atomically: true, encoding: String.Encoding.utf8)
390+
"""#,
391+
392+
"Sources/TestLib/TestLib.swift": #"""
393+
func useGenerated() {
394+
_ = 2️⃣generated
395+
}
396+
"""#,
397+
],
398+
manifest: """
399+
// swift-tools-version: 6.0
400+
import PackageDescription
401+
let package = Package(
402+
name: "PluginTest",
403+
targets: [
404+
.executableTarget(name: "CodeGenerator"),
405+
.target(
406+
name: "TestLib",
407+
plugins: [.plugin(name: "CodeGeneratorPlugin")]
408+
),
409+
.plugin(
410+
name: "CodeGeneratorPlugin",
411+
capability: .buildTool(),
412+
dependencies: ["CodeGenerator"]
413+
),
414+
]
415+
)
416+
""",
417+
enableBackgroundIndexing: false
418+
)
419+
420+
let (uri, positions) = try project.openDocument("TestLib.swift")
421+
let result = try await project.testClient.send(
422+
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
423+
)
424+
// We cannot run plugins when not using background indexing, so we expect no result here.
425+
XCTAssertNil(result)
426+
}
427+
428+
func testToolPluginWithBackgroundIndexing() async throws {
429+
let project = try await SwiftPMTestProject(
430+
files: [
431+
"Plugins/plugin.swift": #"""
432+
import Foundation
433+
import PackagePlugin
434+
@main struct CodeGeneratorPlugin: BuildToolPlugin {
435+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
436+
let genSourcesDir = context.pluginWorkDirectoryURL.appending(path: "GeneratedSources")
437+
guard let target = target as? SourceModuleTarget else { return [] }
438+
let codeGenerator = try context.tool(named: "CodeGenerator").url
439+
let generatedFile = genSourcesDir.appending(path: "\(target.name)-generated.swift")
440+
return [.buildCommand(
441+
displayName: "Generating code for \(target.name)",
442+
executable: codeGenerator,
443+
arguments: [
444+
generatedFile.path
445+
],
446+
inputFiles: [URL(fileURLWithPath: "$TEST_DIR/Sources/TestLib/dep.txt")],
447+
outputFiles: [generatedFile]
448+
)]
449+
}
450+
}
451+
"""#,
452+
453+
"Sources/CodeGenerator/CodeGenerator.swift": #"""
454+
import Foundation
455+
let generated = try String(contentsOf: URL(fileURLWithPath: "$TEST_DIR/Sources/TestLib/dep.txt"))
456+
try generated.write(to: URL(fileURLWithPath: CommandLine.arguments[1]), atomically: true, encoding: String.Encoding.utf8)
457+
"""#,
458+
459+
"Sources/TestLib/TestLib.swift": #"""
460+
func useGenerated() {
461+
_ = 2️⃣generated
462+
}
463+
"""#,
464+
465+
"Sources/TestLib/dep.txt": "let generated = 1",
466+
],
467+
manifest: """
468+
// swift-tools-version: 6.0
469+
import PackageDescription
470+
let package = Package(
471+
name: "PluginTest",
472+
targets: [
473+
.executableTarget(name: "CodeGenerator"),
474+
.target(
475+
name: "TestLib",
476+
plugins: [.plugin(name: "CodeGeneratorPlugin")]
477+
),
478+
.plugin(
479+
name: "CodeGeneratorPlugin",
480+
capability: .buildTool(),
481+
dependencies: ["CodeGenerator"]
482+
),
483+
]
484+
)
485+
""",
486+
enableBackgroundIndexing: true
487+
)
488+
489+
let (uri, positions) = try project.openDocument("TestLib.swift")
490+
491+
// We should have run plugins and thus created generated.swift
492+
do {
493+
let result = try await project.testClient.send(
494+
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
495+
)
496+
let location = try XCTUnwrap(result?.locations?.only)
497+
XCTAssertTrue(location.uri.pseudoPath.hasSuffix("generated.swift"))
498+
XCTAssertEqual(location.range.lowerBound, Position(line: 0, utf16index: 4))
499+
XCTAssertEqual(location.range.upperBound, Position(line: 0, utf16index: 4))
500+
}
501+
502+
// Make a change to the input file of the plugin command
503+
let depUri = try project.uri(for: "dep.txt")
504+
let depUrl = try XCTUnwrap(depUri.fileURL)
505+
try "// some change\nlet generated = 1".write(
506+
to: depUrl,
507+
atomically: true,
508+
encoding: .utf8
509+
)
510+
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: depUri, type: .changed)]))
511+
try await project.testClient.send(PollIndexRequest())
512+
513+
// Expect that the position has been updated in the dependency
514+
try await repeatUntilExpectedResult {
515+
let result = try await project.testClient.send(
516+
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
517+
)
518+
let location = try XCTUnwrap(result?.locations?.only)
519+
return location.range.lowerBound == Position(line: 1, utf16index: 4)
520+
}
521+
}
362522
}

0 commit comments

Comments
 (0)