Skip to content

Commit d4dd578

Browse files
committed
Introduce a notion of ConfiguredTargets into the build system
Instead of asking for build settings of a file, the build system manager asks for the targets of a file and then asks for the build settings of that file in a specific target. This has two advantages: - We know about targets and can prepare the targets for background indexing - Once we support build systems in which a single file can be part of multiple targets, we can have a centralized place that picks preferred targets for a file, eg. based on user configuration
1 parent a643749 commit d4dd578

File tree

9 files changed

+174
-54
lines changed

9 files changed

+174
-54
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/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@ import Workspace
2828
import struct Basics.AbsolutePath
2929
import struct Basics.TSCAbsolutePath
3030
import struct Foundation.URL
31+
import struct TSCBasic.AbsolutePath
3132
import protocol TSCBasic.FileSystem
33+
import class TSCBasic.Process
3234
import var TSCBasic.localFileSystem
3335
import func TSCBasic.resolveSymlinks
3436

37+
typealias AbsolutePath = Basics.AbsolutePath
38+
3539
#if canImport(SPMBuildCore)
3640
import SPMBuildCore
3741
#endif
@@ -91,9 +95,11 @@ public actor SwiftPMBuildSystem {
9195
let workspace: Workspace
9296
public let buildParameters: BuildParameters
9397
let fileSystem: FileSystem
98+
private let toolchainRegistry: ToolchainRegistry
9499

95100
var fileToTarget: [AbsolutePath: SwiftBuildTarget] = [:]
96101
var sourceDirToTarget: [AbsolutePath: SwiftBuildTarget] = [:]
102+
var targets: [SwiftBuildTarget] = []
97103

98104
/// The URIs for which the delegate has registered for change notifications,
99105
/// mapped to the language the delegate specified when registering for change notifications.
@@ -129,6 +135,7 @@ public actor SwiftPMBuildSystem {
129135
) async throws {
130136
self.workspacePath = workspacePath
131137
self.fileSystem = fileSystem
138+
self.toolchainRegistry = toolchainRegistry
132139

133140
guard let packageRoot = findPackageDirectory(containing: workspacePath, fileSystem) else {
134141
throw Error.noManifest(workspacePath: workspacePath)
@@ -259,6 +266,8 @@ extension SwiftPMBuildSystem {
259266
/// with only some properties modified.
260267
self.modulesGraph = modulesGraph
261268

269+
self.targets = try buildDescription.allTargetsInTopologicalOrder(in: modulesGraph)
270+
262271
self.fileToTarget = [AbsolutePath: SwiftBuildTarget](
263272
modulesGraph.allTargets.flatMap { target in
264273
return target.sources.paths.compactMap {
@@ -314,43 +323,72 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
314323

315324
public var indexPrefixMappings: [PathPrefixMapping] { return [] }
316325

317-
public func buildSettings(for uri: DocumentURI, language: Language) throws -> FileBuildSettings? {
318-
// SwiftPMBuildSystem doesn't respect the langue specified by the editor.
319-
return try buildSettings(for: uri)
320-
}
321-
322-
private func buildSettings(for uri: DocumentURI) throws -> FileBuildSettings? {
323-
guard let url = uri.fileURL else {
326+
public func buildSettings(
327+
for uri: DocumentURI,
328+
in configuredTarget: ConfiguredTarget,
329+
language: Language
330+
) throws -> FileBuildSettings? {
331+
guard let url = uri.fileURL, let path = try? AbsolutePath(validating: url.path) else {
324332
// We can't determine build settings for non-file URIs.
325333
return nil
326334
}
327-
guard let path = try? AbsolutePath(validating: url.path) else {
328-
return nil
329-
}
330335

331-
if let buildTarget = try buildTarget(for: path) {
332-
return FileBuildSettings(
333-
compilerArguments: try buildTarget.compileArguments(for: path.asURL),
334-
workingDirectory: workspacePath.pathString
335-
)
336+
if configuredTarget.targetID == "" {
337+
return try settings(forPackageManifest: path)
336338
}
337339

338-
if path.basename == "Package.swift" {
339-
return try settings(forPackageManifest: path)
340+
let buildTargets = self.targets.filter({ $0.name == configuredTarget.targetID })
341+
if buildTargets.count > 1 {
342+
logger.error("Found multiple targets with name \(configuredTarget.targetID). Picking the first one")
343+
}
344+
guard let buildTarget = buildTargets.first else {
345+
if buildTargets.isEmpty {
346+
logger.error("Did not find target with name \(configuredTarget.targetID)")
347+
}
348+
return nil
340349
}
341350

342-
if path.extension == "h" {
343-
return try settings(forHeader: path)
351+
if url.pathExtension == "h", let substituteFile = buildTarget.sources.first {
352+
return FileBuildSettings(
353+
compilerArguments: try buildTarget.compileArguments(for: substituteFile),
354+
workingDirectory: workspacePath.pathString
355+
).patching(newFile: try resolveSymlinks(path).pathString, originalFile: substituteFile.absoluteString)
344356
}
345357

346-
return nil
358+
return FileBuildSettings(
359+
compilerArguments: try buildTarget.compileArguments(for: url),
360+
workingDirectory: workspacePath.pathString
361+
)
347362
}
348363

349364
public func defaultLanguage(for document: DocumentURI) async -> Language? {
350365
// TODO (indexing): Query The SwiftPM build system for the document's language
351366
return nil
352367
}
353368

369+
public func configuredTargets(for uri: DocumentURI) -> [ConfiguredTarget] {
370+
guard let url = uri.fileURL, let path = try? AbsolutePath(validating: url.path) else {
371+
// We can't determine targets for non-file URIs.
372+
return []
373+
}
374+
375+
if let target = try? buildTarget(for: path) {
376+
return [ConfiguredTarget(targetID: target.name, runDestinationID: "dummy")]
377+
}
378+
379+
if path.basename == "Package.swift" {
380+
// We use an empty target name to represent the package manifest since an empty target name is not valid for any
381+
// user-defined target.
382+
return [ConfiguredTarget(targetID: "", runDestinationID: "dummy")]
383+
}
384+
385+
if url.pathExtension == "h", let target = try? target(forHeader: path) {
386+
return [target]
387+
}
388+
389+
return []
390+
}
391+
354392
public func registerForChangeNotifications(for uri: DocumentURI) async {
355393
self.watchedFiles.insert(uri)
356394
}
@@ -437,10 +475,10 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
437475
}
438476

439477
public func fileHandlingCapability(for uri: DocumentURI) -> FileHandlingCapability {
440-
if (try? buildSettings(for: uri)) != nil {
441-
return .handled
478+
if configuredTargets(for: uri).isEmpty {
479+
return .unhandled
442480
}
443-
return .unhandled
481+
return .handled
444482
}
445483

446484
public func sourceFiles() -> [SourceFileInfo] {
@@ -485,25 +523,13 @@ extension SwiftPMBuildSystem {
485523
return canonicalPath == path ? nil : impl(canonicalPath)
486524
}
487525

488-
/// Retrieve settings for a given header file.
489-
///
490-
/// This finds the target the header belongs to based on its location in the file system, retrieves the build settings
491-
/// for any file within that target and generates compiler arguments by replacing that picked file with the header
492-
/// file.
493-
/// This is safe because all files within one target have the same build settings except for reference to the file
494-
/// itself, which we are replacing.
495-
private func settings(forHeader path: AbsolutePath) throws -> FileBuildSettings? {
496-
func impl(_ path: AbsolutePath) throws -> FileBuildSettings? {
526+
/// This finds the target the header belongs to based on its location in the file system.
527+
private func target(forHeader path: AbsolutePath) throws -> ConfiguredTarget? {
528+
func impl(_ path: AbsolutePath) throws -> ConfiguredTarget? {
497529
var dir = path.parentDirectory
498530
while !dir.isRoot {
499531
if let buildTarget = sourceDirToTarget[dir] {
500-
if let sourceFile = buildTarget.sources.first {
501-
return FileBuildSettings(
502-
compilerArguments: try buildTarget.compileArguments(for: sourceFile),
503-
workingDirectory: workspacePath.pathString
504-
).patching(newFile: path.pathString, originalFile: sourceFile.absoluteString)
505-
}
506-
return nil
532+
return ConfiguredTarget(targetID: buildTarget.name, runDestinationID: "dummy")
507533
}
508534
dir = dir.parentDirectory
509535
}

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

Tests/SKCoreTests/CompilationDatabaseTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ final class CompilationDatabaseTests: XCTestCase {
290290
) { buildSystem in
291291
let settings = await buildSystem.buildSettings(
292292
for: DocumentURI(URL(fileURLWithPath: "/a/a.swift")),
293+
in: ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy"),
293294
language: .swift
294295
)
295296
XCTAssertNotNil(settings)

Tests/SKSwiftPMWorkspaceTests/SwiftPMBuildSystemTests.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,16 @@ import struct PackageModel.BuildFlags
2727
import SPMBuildCore
2828
#endif
2929

30-
final class SwiftPMWorkspaceTests: XCTestCase {
30+
fileprivate extension SwiftPMBuildSystem {
31+
func buildSettings(for uri: DocumentURI, language: Language) throws -> FileBuildSettings? {
32+
guard let target = self.configuredTargets(for: uri).only else {
33+
return nil
34+
}
35+
return try buildSettings(for: uri, in: target, language: language)
36+
}
37+
}
3138

39+
final class SwiftPMBuildSystemTests: XCTestCase {
3240
func testNoPackage() async throws {
3341
let fs = InMemoryFileSystem()
3442
try await withTestScratchDir { tempDir in

0 commit comments

Comments
 (0)