Skip to content

Commit 4d93848

Browse files
authored
Merge pull request #1249 from ahoppen/configured-targets
Introduce a notion of `ConfiguredTargets` into the build system
2 parents 907e35c + d4dd578 commit 4d93848

11 files changed

+182
-57
lines changed

Sources/SKCore/BuildServerBuildSystem.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,14 +263,22 @@ extension BuildServerBuildSystem: BuildSystem {
263263
///
264264
/// Returns `nil` if no build settings have been received from the build
265265
/// server yet or if no build settings are available for this file.
266-
public func buildSettings(for document: DocumentURI, language: Language) async -> FileBuildSettings? {
266+
public func buildSettings(
267+
for document: DocumentURI,
268+
in target: ConfiguredTarget,
269+
language: Language
270+
) async -> FileBuildSettings? {
267271
return buildSettings[document]
268272
}
269273

270274
public func defaultLanguage(for document: DocumentURI) async -> Language? {
271275
return nil
272276
}
273277

278+
public func configuredTargets(for document: DocumentURI) async -> [ConfiguredTarget] {
279+
return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")]
280+
}
281+
274282
public func registerForChangeNotifications(for uri: DocumentURI) {
275283
let request = RegisterForChanges(uri: uri, action: .register)
276284
_ = self.buildServer?.send(request) { result in

Sources/SKCore/BuildSystem.swift

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,27 @@ public struct SourceFileInfo: Sendable {
4343
}
4444
}
4545

46+
/// A target / run destination combination. For example, a configured target can represent building the target
47+
/// `MyLibrary` for iOS.
48+
public struct ConfiguredTarget: Hashable, Sendable {
49+
/// An opaque string that represents the target.
50+
///
51+
/// The target's ID should be generated by the build system that handles the target and only interpreted by that
52+
/// build system.
53+
public let targetID: String
54+
55+
/// An opaque string that represents the run destination.
56+
///
57+
/// The run destination's ID should be generated by the build system that handles the target and only interpreted by
58+
/// that build system.
59+
public let runDestinationID: String
60+
61+
public init(targetID: String, runDestinationID: String) {
62+
self.targetID = targetID
63+
self.runDestinationID = runDestinationID
64+
}
65+
}
66+
4667
/// Provider of FileBuildSettings and other build-related information.
4768
///
4869
/// The primary role of the build system is to answer queries for
@@ -53,7 +74,6 @@ public struct SourceFileInfo: Sendable {
5374
/// For example, a SwiftPMWorkspace provides compiler arguments for the files
5475
/// contained in a SwiftPM package root directory.
5576
public protocol BuildSystem: AnyObject, Sendable {
56-
5777
/// The root of the project that this build system manages. For example, for SwiftPM packages, this is the folder
5878
/// containing Package.swift. For compilation databases it is the root folder based on which the compilation database
5979
/// was found.
@@ -85,7 +105,14 @@ public protocol BuildSystem: AnyObject, Sendable {
85105
///
86106
/// Returns `nil` if the build system can't provide build settings for this
87107
/// file or if it hasn't computed build settings for the file yet.
88-
func buildSettings(for document: DocumentURI, language: Language) async throws -> FileBuildSettings?
108+
func buildSettings(
109+
for document: DocumentURI,
110+
in target: ConfiguredTarget,
111+
language: Language
112+
) async throws -> FileBuildSettings?
113+
114+
/// Return the list of targets and run destinations that the given document can be built for.
115+
func configuredTargets(for document: DocumentURI) async -> [ConfiguredTarget]
89116

90117
/// If the build system has knowledge about the language that this document should be compiled in, return it.
91118
///

Sources/SKCore/BuildSystemManager.swift

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,22 +122,53 @@ extension BuildSystemManager {
122122
}
123123
}
124124

125+
/// Returns the `ConfiguredTarget` that should be used for semantic functionality of the given document.
126+
public func canonicalConfiguredTarget(for document: DocumentURI) async -> ConfiguredTarget? {
127+
// Sort the configured targets to deterministically pick the same `ConfiguredTarget` every time.
128+
// We could allow the user to specify a preference of one target over another. For now this is not necessary because
129+
// no build system currently returns multiple targets for a source file.
130+
return await buildSystem?.configuredTargets(for: document)
131+
.sorted { ($0.targetID, $0.runDestinationID) < ($1.targetID, $1.runDestinationID) }
132+
.first
133+
}
134+
135+
/// Returns the build settings for `document` from `buildSystem`.
136+
///
137+
/// Implementation detail of `buildSettings(for:language:)`.
138+
private func buildSettingsFromPrimaryBuildSystem(
139+
for document: DocumentURI,
140+
language: Language
141+
) async throws -> FileBuildSettings? {
142+
guard let buildSystem else {
143+
return nil
144+
}
145+
guard let target = await canonicalConfiguredTarget(for: document) else {
146+
logger.error("Failed to get target for \(document.forLogging)")
147+
return nil
148+
}
149+
// FIXME: (async) We should only wait `fallbackSettingsTimeout` for build
150+
// settings and return fallback afterwards. I am not sure yet, how best to
151+
// implement that with Swift concurrency.
152+
// For now, this should be fine because all build systems return
153+
// very quickly from `settings(for:language:)`.
154+
guard let settings = try await buildSystem.buildSettings(for: document, in: target, language: language) else {
155+
return nil
156+
}
157+
return settings
158+
}
159+
125160
private func buildSettings(
126161
for document: DocumentURI,
127162
language: Language
128163
) async -> FileBuildSettings? {
129164
do {
130-
// FIXME: (async) We should only wait `fallbackSettingsTimeout` for build
131-
// settings and return fallback afterwards. I am not sure yet, how best to
132-
// implement that with Swift concurrency.
133-
// For now, this should be fine because all build systems return
134-
// very quickly from `settings(for:language:)`.
135-
if let settings = try await buildSystem?.buildSettings(for: document, language: language) {
136-
return settings
165+
if let buildSettings = try await buildSettingsFromPrimaryBuildSystem(for: document, language: language) {
166+
return buildSettings
137167
}
138168
} catch {
139169
logger.error("Getting build settings failed: \(error.forLogging)")
140170
}
171+
141172
guard var settings = fallbackBuildSystem?.buildSettings(for: document, language: language) else {
142173
return nil
143174
}

Sources/SKCore/CompilationDatabaseBuildSystem.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,17 @@ public actor CompilationDatabaseBuildSystem {
9393
}
9494

9595
extension CompilationDatabaseBuildSystem: BuildSystem {
96-
9796
public var indexDatabasePath: AbsolutePath? {
9897
indexStorePath?.parentDirectory.appending(component: "IndexDatabase")
9998
}
10099

101100
public var indexPrefixMappings: [PathPrefixMapping] { return [] }
102101

103-
public func buildSettings(for document: DocumentURI, language: Language) async -> FileBuildSettings? {
102+
public func buildSettings(
103+
for document: DocumentURI,
104+
in buildTarget: ConfiguredTarget,
105+
language: Language
106+
) async -> FileBuildSettings? {
104107
guard let url = document.fileURL else {
105108
// We can't determine build settings for non-file URIs.
106109
return nil
@@ -118,6 +121,10 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
118121
return nil
119122
}
120123

124+
public func configuredTargets(for document: DocumentURI) async -> [ConfiguredTarget] {
125+
return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")]
126+
}
127+
121128
public func registerForChangeNotifications(for uri: DocumentURI) async {
122129
self.watchedFiles.insert(uri)
123130
}

Sources/SKSupport/Process+WaitUntilExitWithCancellation.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import Foundation
1515
import class TSCBasic.Process
1616
import struct TSCBasic.ProcessResult
1717

18-
public extension Process {
18+
extension Process {
1919
/// Wait for the process to exit. If the task gets cancelled, during this time, send a `SIGINT` to the process.
20-
func waitUntilExitSendingSigIntOnTaskCancellation() async throws -> ProcessResult {
20+
@discardableResult
21+
public func waitUntilExitSendingSigIntOnTaskCancellation() async throws -> ProcessResult {
2122
return try await withTaskCancellationHandler {
2223
try await waitUntilExit()
2324
} onCancel: {

Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@ import struct Basics.AbsolutePath
2929
import struct Basics.IdentifiableSet
3030
import struct Basics.TSCAbsolutePath
3131
import struct Foundation.URL
32+
import struct TSCBasic.AbsolutePath
3233
import protocol TSCBasic.FileSystem
34+
import class TSCBasic.Process
3335
import var TSCBasic.localFileSystem
3436
import func TSCBasic.resolveSymlinks
3537

38+
typealias AbsolutePath = Basics.AbsolutePath
39+
3640
#if canImport(SPMBuildCore)
3741
import SPMBuildCore
3842
#endif
@@ -92,9 +96,11 @@ public actor SwiftPMBuildSystem {
9296
let workspace: Workspace
9397
public let buildParameters: BuildParameters
9498
let fileSystem: FileSystem
99+
private let toolchainRegistry: ToolchainRegistry
95100

96101
var fileToTarget: [AbsolutePath: SwiftBuildTarget] = [:]
97102
var sourceDirToTarget: [AbsolutePath: SwiftBuildTarget] = [:]
103+
var targets: [SwiftBuildTarget] = []
98104

99105
/// The URIs for which the delegate has registered for change notifications,
100106
/// mapped to the language the delegate specified when registering for change notifications.
@@ -130,6 +136,7 @@ public actor SwiftPMBuildSystem {
130136
) async throws {
131137
self.workspacePath = workspacePath
132138
self.fileSystem = fileSystem
139+
self.toolchainRegistry = toolchainRegistry
133140

134141
guard let packageRoot = findPackageDirectory(containing: workspacePath, fileSystem) else {
135142
throw Error.noManifest(workspacePath: workspacePath)
@@ -265,6 +272,8 @@ extension SwiftPMBuildSystem {
265272
/// with only some properties modified.
266273
self.modulesGraph = modulesGraph
267274

275+
self.targets = try buildDescription.allTargetsInTopologicalOrder(in: modulesGraph)
276+
268277
self.fileToTarget = [AbsolutePath: SwiftBuildTarget](
269278
modulesGraph.allTargets.flatMap { target in
270279
return target.sources.paths.compactMap {
@@ -320,43 +329,72 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
320329

321330
public var indexPrefixMappings: [PathPrefixMapping] { return [] }
322331

323-
public func buildSettings(for uri: DocumentURI, language: Language) throws -> FileBuildSettings? {
324-
// SwiftPMBuildSystem doesn't respect the langue specified by the editor.
325-
return try buildSettings(for: uri)
326-
}
327-
328-
private func buildSettings(for uri: DocumentURI) throws -> FileBuildSettings? {
329-
guard let url = uri.fileURL else {
332+
public func buildSettings(
333+
for uri: DocumentURI,
334+
in configuredTarget: ConfiguredTarget,
335+
language: Language
336+
) throws -> FileBuildSettings? {
337+
guard let url = uri.fileURL, let path = try? AbsolutePath(validating: url.path) else {
330338
// We can't determine build settings for non-file URIs.
331339
return nil
332340
}
333-
guard let path = try? AbsolutePath(validating: url.path) else {
334-
return nil
335-
}
336341

337-
if let buildTarget = try buildTarget(for: path) {
338-
return FileBuildSettings(
339-
compilerArguments: try buildTarget.compileArguments(for: path.asURL),
340-
workingDirectory: workspacePath.pathString
341-
)
342+
if configuredTarget.targetID == "" {
343+
return try settings(forPackageManifest: path)
342344
}
343345

344-
if path.basename == "Package.swift" {
345-
return try settings(forPackageManifest: path)
346+
let buildTargets = self.targets.filter({ $0.name == configuredTarget.targetID })
347+
if buildTargets.count > 1 {
348+
logger.error("Found multiple targets with name \(configuredTarget.targetID). Picking the first one")
349+
}
350+
guard let buildTarget = buildTargets.first else {
351+
if buildTargets.isEmpty {
352+
logger.error("Did not find target with name \(configuredTarget.targetID)")
353+
}
354+
return nil
346355
}
347356

348-
if path.extension == "h" {
349-
return try settings(forHeader: path)
357+
if url.pathExtension == "h", let substituteFile = buildTarget.sources.first {
358+
return FileBuildSettings(
359+
compilerArguments: try buildTarget.compileArguments(for: substituteFile),
360+
workingDirectory: workspacePath.pathString
361+
).patching(newFile: try resolveSymlinks(path).pathString, originalFile: substituteFile.absoluteString)
350362
}
351363

352-
return nil
364+
return FileBuildSettings(
365+
compilerArguments: try buildTarget.compileArguments(for: url),
366+
workingDirectory: workspacePath.pathString
367+
)
353368
}
354369

355370
public func defaultLanguage(for document: DocumentURI) async -> Language? {
356371
// TODO (indexing): Query The SwiftPM build system for the document's language
357372
return nil
358373
}
359374

375+
public func configuredTargets(for uri: DocumentURI) -> [ConfiguredTarget] {
376+
guard let url = uri.fileURL, let path = try? AbsolutePath(validating: url.path) else {
377+
// We can't determine targets for non-file URIs.
378+
return []
379+
}
380+
381+
if let target = try? buildTarget(for: path) {
382+
return [ConfiguredTarget(targetID: target.name, runDestinationID: "dummy")]
383+
}
384+
385+
if path.basename == "Package.swift" {
386+
// We use an empty target name to represent the package manifest since an empty target name is not valid for any
387+
// user-defined target.
388+
return [ConfiguredTarget(targetID: "", runDestinationID: "dummy")]
389+
}
390+
391+
if url.pathExtension == "h", let target = try? target(forHeader: path) {
392+
return [target]
393+
}
394+
395+
return []
396+
}
397+
360398
public func registerForChangeNotifications(for uri: DocumentURI) async {
361399
self.watchedFiles.insert(uri)
362400
}
@@ -443,10 +481,10 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
443481
}
444482

445483
public func fileHandlingCapability(for uri: DocumentURI) -> FileHandlingCapability {
446-
if (try? buildSettings(for: uri)) != nil {
447-
return .handled
484+
if configuredTargets(for: uri).isEmpty {
485+
return .unhandled
448486
}
449-
return .unhandled
487+
return .handled
450488
}
451489

452490
public func sourceFiles() -> [SourceFileInfo] {
@@ -491,25 +529,13 @@ extension SwiftPMBuildSystem {
491529
return canonicalPath == path ? nil : impl(canonicalPath)
492530
}
493531

494-
/// Retrieve settings for a given header file.
495-
///
496-
/// This finds the target the header belongs to based on its location in the file system, retrieves the build settings
497-
/// for any file within that target and generates compiler arguments by replacing that picked file with the header
498-
/// file.
499-
/// This is safe because all files within one target have the same build settings except for reference to the file
500-
/// itself, which we are replacing.
501-
private func settings(forHeader path: AbsolutePath) throws -> FileBuildSettings? {
502-
func impl(_ path: AbsolutePath) throws -> FileBuildSettings? {
532+
/// This finds the target the header belongs to based on its location in the file system.
533+
private func target(forHeader path: AbsolutePath) throws -> ConfiguredTarget? {
534+
func impl(_ path: AbsolutePath) throws -> ConfiguredTarget? {
503535
var dir = path.parentDirectory
504536
while !dir.isRoot {
505537
if let buildTarget = sourceDirToTarget[dir] {
506-
if let sourceFile = buildTarget.sources.first {
507-
return FileBuildSettings(
508-
compilerArguments: try buildTarget.compileArguments(for: sourceFile),
509-
workingDirectory: workspacePath.pathString
510-
).patching(newFile: path.pathString, originalFile: sourceFile.absoluteString)
511-
}
512-
return nil
538+
return ConfiguredTarget(targetID: buildTarget.name, runDestinationID: "dummy")
513539
}
514540
dir = dir.parentDirectory
515541
}

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2471,7 +2471,11 @@ fileprivate extension CheckedIndex {
24712471
/// If the USR has an ambiguous definition, the most important role of this function is to deterministically return
24722472
/// the same result every time.
24732473
func primaryDefinitionOrDeclarationOccurrence(ofUSR usr: String) -> SymbolOccurrence? {
2474-
return definitionOrDeclarationOccurrences(ofUSR: usr).sorted().first
2474+
let result = definitionOrDeclarationOccurrences(ofUSR: usr).sorted().first
2475+
if result == nil {
2476+
logger.error("Failed to find definition of \(usr) in index")
2477+
}
2478+
return result
24752479
}
24762480
}
24772481

Tests/SKCoreTests/BuildSystemManagerTests.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,14 +445,18 @@ class ManualBuildSystem: BuildSystem {
445445
self.delegate = delegate
446446
}
447447

448-
func buildSettings(for uri: DocumentURI, language: Language) -> FileBuildSettings? {
448+
func buildSettings(for uri: DocumentURI, in buildTarget: ConfiguredTarget, language: Language) -> FileBuildSettings? {
449449
return map[uri]
450450
}
451451

452452
public func defaultLanguage(for document: DocumentURI) async -> Language? {
453453
return nil
454454
}
455455

456+
public func configuredTargets(for document: DocumentURI) async -> [ConfiguredTarget] {
457+
return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")]
458+
}
459+
456460
func registerForChangeNotifications(for uri: DocumentURI) async {
457461
}
458462

0 commit comments

Comments
 (0)