Skip to content

Commit 6348603

Browse files
committed
Take run destinations into account for SwiftPM build targets
We can have two targets with the same name in a SwiftPM workspace, one for a build target and one for the destination. We need to be able to tell them apart based on the run destination.
1 parent fff4dc4 commit 6348603

File tree

3 files changed

+122
-52
lines changed

3 files changed

+122
-52
lines changed

Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift

Lines changed: 68 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,26 @@ private func getDefaultToolchain(_ toolchainRegistry: ToolchainRegistry) async -
6363
return await toolchainRegistry.default
6464
}
6565

66+
fileprivate extension BuildTriple {
67+
/// A string that can be used to identify the build triple in `ConfiguredTarget.runDestinationID`.
68+
var id: String {
69+
switch self {
70+
case .tools:
71+
return "tools"
72+
case .destination:
73+
return "destination"
74+
}
75+
}
76+
}
77+
78+
fileprivate extension ConfiguredTarget {
79+
init(_ buildTarget: any SwiftBuildTarget) {
80+
self.init(targetID: buildTarget.name, runDestinationID: buildTarget.buildTriple.id)
81+
}
82+
83+
static let forPackageManifest = ConfiguredTarget(targetID: "", runDestinationID: "")
84+
}
85+
6686
/// Swift Package Manager build system and workspace support.
6787
///
6888
/// This class implements the `BuildSystem` interface to provide the build settings for a Swift
@@ -98,13 +118,13 @@ public actor SwiftPMBuildSystem {
98118
let fileSystem: FileSystem
99119
private let toolchainRegistry: ToolchainRegistry
100120

101-
var fileToTarget: [AbsolutePath: SwiftBuildTarget] = [:]
102-
var sourceDirToTarget: [AbsolutePath: SwiftBuildTarget] = [:]
121+
var fileToTargets: [AbsolutePath: [SwiftBuildTarget]] = [:]
122+
var sourceDirToTargets: [AbsolutePath: [SwiftBuildTarget]] = [:]
103123

104-
/// Maps target ids (aka. `ConfiguredTarget.targetID`) to their SwiftPM build target as well as an index in their
105-
/// topological sorting. Targets with lower index are more low level, ie. targets with higher indices depend on
106-
/// targets with lower indices.
107-
var targets: [String: (index: Int, buildTarget: SwiftBuildTarget)] = [:]
124+
/// Maps configured targets ids to their SwiftPM build target as well as an index in their topological sorting.
125+
///
126+
/// Targets with lower index are more low level, ie. targets with higher indices depend on targets with lower indices.
127+
var targets: [ConfiguredTarget: (index: Int, buildTarget: SwiftBuildTarget)] = [:]
108128

109129
/// The URIs for which the delegate has registered for change notifications,
110130
/// mapped to the language the delegate specified when registering for change notifications.
@@ -292,40 +312,34 @@ extension SwiftPMBuildSystem {
292312

293313
self.targets = Dictionary(
294314
try buildDescription.allTargetsInTopologicalOrder(in: modulesGraph).enumerated().map { (index, target) in
295-
return (key: target.name, (index, target))
315+
return (key: ConfiguredTarget(target), value: (index, target))
296316
},
297317
uniquingKeysWith: { first, second in
298318
logger.fault("Found two targets with the same name \(first.buildTarget.name)")
299319
return second
300320
}
301321
)
302322

303-
self.fileToTarget = [AbsolutePath: SwiftBuildTarget](
323+
self.fileToTargets = [AbsolutePath: [SwiftBuildTarget]](
304324
modulesGraph.allTargets.flatMap { target in
305-
return target.sources.paths.compactMap {
325+
return target.sources.paths.compactMap { (filePath) -> (key: AbsolutePath, value: [SwiftBuildTarget])? in
306326
guard let buildTarget = buildDescription.getBuildTarget(for: target, in: modulesGraph) else {
307327
return nil
308328
}
309-
return (key: $0, value: buildTarget)
329+
return (key: filePath, value: [buildTarget])
310330
}
311331
},
312-
uniquingKeysWith: { td, _ in
313-
// FIXME: is there a preferred target?
314-
return td
315-
}
332+
uniquingKeysWith: { $0 + $1 }
316333
)
317334

318-
self.sourceDirToTarget = [AbsolutePath: SwiftBuildTarget](
319-
modulesGraph.allTargets.compactMap { (target) -> (AbsolutePath, SwiftBuildTarget)? in
335+
self.sourceDirToTargets = [AbsolutePath: [SwiftBuildTarget]](
336+
modulesGraph.allTargets.compactMap { (target) -> (AbsolutePath, [SwiftBuildTarget])? in
320337
guard let buildTarget = buildDescription.getBuildTarget(for: target, in: modulesGraph) else {
321338
return nil
322339
}
323-
return (key: target.sources.root, value: buildTarget)
340+
return (key: target.sources.root, value: [buildTarget])
324341
},
325-
uniquingKeysWith: { td, _ in
326-
// FIXME: is there a preferred target?
327-
return td
328-
}
342+
uniquingKeysWith: { $0 + $1 }
329343
)
330344

331345
guard let delegate = self.delegate else {
@@ -365,11 +379,11 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
365379
return nil
366380
}
367381

368-
if configuredTarget.targetID == "" {
382+
if configuredTarget == .forPackageManifest {
369383
return try settings(forPackageManifest: path)
370384
}
371385

372-
guard let buildTarget = self.targets[configuredTarget.targetID]?.buildTarget else {
386+
guard let buildTarget = self.targets[configuredTarget]?.buildTarget else {
373387
logger.error("Did not find target with name \(configuredTarget.targetID)")
374388
return nil
375389
}
@@ -399,27 +413,29 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
399413
return []
400414
}
401415

402-
if let target = try? buildTarget(for: path) {
403-
return [ConfiguredTarget(targetID: target.name, runDestinationID: "dummy")]
416+
if let targets = orLog("Getting build targets for file", { try buildTargets(for: path) }), !targets.isEmpty {
417+
return targets.map(ConfiguredTarget.init)
404418
}
405419

406420
if path.basename == "Package.swift" {
407421
// We use an empty target name to represent the package manifest since an empty target name is not valid for any
408422
// user-defined target.
409-
return [ConfiguredTarget(targetID: "", runDestinationID: "dummy")]
423+
return [ConfiguredTarget.forPackageManifest]
410424
}
411425

412-
if url.pathExtension == "h", let target = try? target(forHeader: path) {
413-
return [target]
426+
if url.pathExtension == "h", let targets = orLog("Getting targets for header", { try targets(forHeader: path) }),
427+
!targets.isEmpty
428+
{
429+
return targets
414430
}
415431

416432
return []
417433
}
418434

419435
public func topologicalSort(of targets: [ConfiguredTarget]) -> [ConfiguredTarget]? {
420436
return targets.sorted { (lhs: ConfiguredTarget, rhs: ConfiguredTarget) -> Bool in
421-
let lhsIndex = self.targets[lhs.targetID]?.index ?? self.targets.count
422-
let rhsIndex = self.targets[lhs.targetID]?.index ?? self.targets.count
437+
let lhsIndex = self.targets[lhs]?.index ?? self.targets.count
438+
let rhsIndex = self.targets[lhs]?.index ?? self.targets.count
423439
return lhsIndex < rhsIndex
424440
}
425441
}
@@ -494,19 +510,19 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
494510
self.watchedFiles.remove(uri)
495511
}
496512

497-
/// Returns the resolved target description for the given file, if one is known.
498-
private func buildTarget(for file: AbsolutePath) throws -> SwiftBuildTarget? {
499-
if let td = fileToTarget[file] {
500-
return td
513+
/// Returns the resolved target descriptions for the given file.
514+
private func buildTargets(for file: AbsolutePath) throws -> [SwiftBuildTarget] {
515+
if let targets = fileToTargets[file] {
516+
return targets
501517
}
502518

503519
let realpath = try resolveSymlinks(file)
504-
if realpath != file, let td = fileToTarget[realpath] {
505-
fileToTarget[file] = td
506-
return td
520+
if realpath != file, let targets = fileToTargets[realpath] {
521+
fileToTargets[file] = targets
522+
return targets
507523
}
508524

509-
return nil
525+
return []
510526
}
511527

512528
/// An event is relevant if it modifies a file that matches one of the file rules used by the SwiftPM workspace.
@@ -547,11 +563,11 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
547563
guard let url = event.uri.fileURL,
548564
url.pathExtension == "swift",
549565
let absolutePath = try? AbsolutePath(validating: url.path),
550-
let target = fileToTarget[absolutePath]
566+
let targets = fileToTargets[absolutePath]
551567
else {
552568
continue
553569
}
554-
filesWithUpdatedDependencies.formUnion(target.sources.map { DocumentURI($0) })
570+
filesWithUpdatedDependencies.formUnion(targets.flatMap(\.sources).map(DocumentURI.init))
555571
}
556572

557573
// If a `.swiftmodule` file is updated, this means that we have performed a build / are
@@ -564,7 +580,7 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
564580
// All of this shouldn't be necessary once we have background preparation, in which case we know when preparation of
565581
// a target has finished.
566582
if events.contains(where: { $0.uri.fileURL?.pathExtension == "swiftmodule" }) {
567-
filesWithUpdatedDependencies.formUnion(self.fileToTarget.keys.map { DocumentURI($0.asURL) })
583+
filesWithUpdatedDependencies.formUnion(self.fileToTargets.keys.map { DocumentURI($0.asURL) })
568584
}
569585
await self.fileDependenciesUpdatedDebouncer.scheduleCall(filesWithUpdatedDependencies)
570586
}
@@ -577,12 +593,12 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
577593
}
578594

579595
public func sourceFiles() -> [SourceFileInfo] {
580-
return fileToTarget.compactMap { (path, target) -> SourceFileInfo? in
596+
return fileToTargets.compactMap { (path, targets) -> SourceFileInfo? in
581597
// We should only set mayContainTests to `true` for files from test targets
582598
// (https://github.com/apple/sourcekit-lsp/issues/1174).
583599
return SourceFileInfo(
584600
uri: DocumentURI(path.asURL),
585-
isPartOfRootProject: target.isPartOfRootPackage,
601+
isPartOfRootProject: targets.contains(where: \.isPartOfRootPackage),
586602
mayContainTests: true
587603
)
588604
}
@@ -615,25 +631,26 @@ extension SwiftPMBuildSystem {
615631
return canonicalPath == path ? nil : impl(canonicalPath)
616632
}
617633

618-
/// This finds the target the header belongs to based on its location in the file system.
619-
private func target(forHeader path: AbsolutePath) throws -> ConfiguredTarget? {
620-
func impl(_ path: AbsolutePath) throws -> ConfiguredTarget? {
634+
/// This finds the targets the header belongs to based on its location in the file system.
635+
private func targets(forHeader path: AbsolutePath) throws -> [ConfiguredTarget] {
636+
func impl(_ path: AbsolutePath) throws -> [ConfiguredTarget] {
621637
var dir = path.parentDirectory
622638
while !dir.isRoot {
623-
if let buildTarget = sourceDirToTarget[dir] {
624-
return ConfiguredTarget(targetID: buildTarget.name, runDestinationID: "dummy")
639+
if let buildTargets = sourceDirToTargets[dir], !buildTargets.isEmpty {
640+
return buildTargets.map(ConfiguredTarget.init)
625641
}
626642
dir = dir.parentDirectory
627643
}
628-
return nil
644+
return []
629645
}
630646

631-
if let result = try impl(path) {
647+
let result = try impl(path)
648+
if !result.isEmpty {
632649
return result
633650
}
634651

635652
let canonicalPath = try resolveSymlinks(path)
636-
return try canonicalPath == path ? nil : impl(canonicalPath)
653+
return try canonicalPath == path ? [] : impl(canonicalPath)
637654
}
638655
}
639656

Sources/SKTestSupport/SwiftPMTestProject.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public class SwiftPMTestProject: MultiFileTestProject {
5353
for (fileLocation, contents) in files {
5454
let directories =
5555
switch fileLocation.directories.first {
56-
case "Sources", "Tests":
56+
case "Sources", "Tests", "Plugins":
5757
fileLocation.directories
5858
case nil:
5959
["Sources", "MyLibrary"]

Tests/SourceKitLSPTests/BackgroundIndexingTests.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,4 +373,57 @@ final class BackgroundIndexingTests: XCTestCase {
373373

374374
withExtendedLifetime(project) {}
375375
}
376+
377+
func testLibraryUsedByExecutableTargetAndPackagePlugin() async throws {
378+
try await SkipUnless.swiftpmStoresModulesInSubdirectory()
379+
let project = try await SwiftPMTestProject(
380+
files: [
381+
"Lib/MyFile.swift": """
382+
public func 1️⃣foo() {}
383+
""",
384+
"MyExec/MyExec.swift": """
385+
import Lib
386+
func bar() {
387+
2️⃣foo()
388+
}
389+
""",
390+
"Plugins/MyPlugin/MyPlugin.swift": """
391+
import PackagePlugin
392+
393+
@main
394+
struct MyPlugin: CommandPlugin {
395+
func performCommand(context: PluginContext, arguments: [String]) async throws {}
396+
}
397+
""",
398+
],
399+
manifest: """
400+
// swift-tools-version: 5.7
401+
402+
import PackageDescription
403+
404+
let package = Package(
405+
name: "MyLibrary",
406+
targets: [
407+
.target(name: "Lib"),
408+
.executableTarget(name: "MyExec", dependencies: ["Lib"]),
409+
.plugin(
410+
name: "MyPlugin",
411+
capability: .command(
412+
intent: .sourceCodeFormatting(),
413+
permissions: []
414+
),
415+
dependencies: ["MyExec"]
416+
)
417+
]
418+
)
419+
""",
420+
serverOptions: backgroundIndexingOptions
421+
)
422+
423+
let (uri, positions) = try project.openDocument("MyExec.swift")
424+
let definition = try await project.testClient.send(
425+
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
426+
)
427+
XCTAssertEqual(definition, .locations([try project.location(from: "1️⃣", to: "1️⃣", in: "MyFile.swift")]))
428+
}
376429
}

0 commit comments

Comments
 (0)