Skip to content

Commit ac0eb22

Browse files
committed
Improve package reloading logic when manifest is changed
Two improvements to the logic that determines if we need to reload the package on a file change: 1. Only reload a package if the modified package manifest or Package.resolved is in the root of the package folder. This is relevant if we have a multi-root workspace, in which currently a single change to a package manifest or Package.resolved causes all packages to get reloaded. 2. While I was at it, I noticed that we didn’t properly handle version-specific package manifests. Also trigger a package reload if a version-specific package manifest is modified, created or deleted.
1 parent 1ca9780 commit ac0eb22

File tree

3 files changed

+190
-12
lines changed

3 files changed

+190
-12
lines changed

Sources/BuildServerIntegration/SwiftPMBuildServer.swift

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,12 @@ package actor SwiftPMBuildServer: BuiltInBuildServer {
137137

138138
private var targetDependencies: [BuildTargetIdentifier: Set<BuildTargetIdentifier>] = [:]
139139

140-
static package func searchForConfig(in path: URL, options: SourceKitLSPOptions) -> BuildServerSpec? {
140+
/// Regular expression that matches version-specific package manifest file names such as [email protected]
141+
private static var versionSpecificPackageManifestNameRegex: Regex<(Substring, Substring, Substring?, Substring?)> {
142+
#/^Package@swift-(\d+)(?:\.(\d+))?(?:\.(\d+))?.swift$/#
143+
}
141144

145+
static package func searchForConfig(in path: URL, options: SourceKitLSPOptions) -> BuildServerSpec? {
142146
let packagePath = path.appendingPathComponent("Package.swift")
143147
if (try? String(contentsOf: packagePath, encoding: .utf8))?.contains("PackageDescription") ?? false {
144148
return BuildServerSpec(kind: .swiftPM, projectRoot: path, configPath: packagePath)
@@ -543,12 +547,11 @@ package actor SwiftPMBuildServer: BuiltInBuildServer {
543547
// (https://github.com/swiftlang/sourcekit-lsp/issues/1267)
544548
for target in request.targets {
545549
if target == .forPackageManifest {
546-
let packageManifestName = #/^Package@swift-(\d+)(?:\.(\d+))?(?:\.(\d+))?.swift$/#
547550
let versionSpecificManifests = try? FileManager.default.contentsOfDirectory(
548551
at: projectRoot,
549552
includingPropertiesForKeys: nil
550553
).compactMap { (url) -> SourceItem? in
551-
guard (try? packageManifestName.wholeMatch(in: url.lastPathComponent)) != nil else {
554+
guard (try? Self.versionSpecificPackageManifestNameRegex.wholeMatch(in: url.lastPathComponent)) != nil else {
552555
return nil
553556
}
554557
return SourceItem(
@@ -806,11 +809,28 @@ package actor SwiftPMBuildServer: BuiltInBuildServer {
806809
}
807810
}
808811

812+
private func isPackageManifestOrPackageResolved(_ url: URL) -> Bool {
813+
guard url.lastPathComponent.contains("Package") else {
814+
// Fast check to early exit for files that don't like a package manifest or Package.resolved
815+
return false
816+
}
817+
guard
818+
url.lastPathComponent == "Package.resolved" || url.lastPathComponent == "Package.swift"
819+
|| (try? Self.versionSpecificPackageManifestNameRegex.wholeMatch(in: url.lastPathComponent)) != nil
820+
else {
821+
return false
822+
}
823+
return url.deletingLastPathComponent() == self.projectRoot
824+
}
825+
809826
/// An event is relevant if it modifies a file that matches one of the file rules used by the SwiftPM workspace.
810827
private func fileEventShouldTriggerPackageReload(event: FileEvent) -> Bool {
811828
guard let fileURL = event.uri.fileURL else {
812829
return false
813830
}
831+
if isPackageManifestOrPackageResolved(fileURL) {
832+
return true
833+
}
814834
switch event.type {
815835
case .created, .deleted:
816836
guard let buildDescription else {
@@ -819,7 +839,8 @@ package actor SwiftPMBuildServer: BuiltInBuildServer {
819839

820840
return buildDescription.fileAffectsSwiftOrClangBuildSettings(fileURL)
821841
case .changed:
822-
return fileURL.lastPathComponent == "Package.swift" || fileURL.lastPathComponent == "Package.resolved"
842+
// Only modified package manifests should trigger a package reload and that's handled above.
843+
return false
823844
default: // Unknown file change type
824845
return false
825846
}

Tests/SourceKitLSPTests/BackgroundIndexingTests.swift

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,7 +1325,7 @@ final class BackgroundIndexingTests: XCTestCase {
13251325
// Updating Package.swift causes a package reload but should not cause dependencies to be updated.
13261326
project.testClient.send(
13271327
DidChangeWatchedFilesNotification(changes: [
1328-
FileEvent(uri: DocumentURI(project.scratchDirectory.appendingPathComponent("Package.resolved")), type: .changed)
1328+
FileEvent(uri: DocumentURI(project.scratchDirectory.appendingPathComponent("Package.swift")), type: .changed)
13291329
])
13301330
)
13311331
try await project.testClient.send(SynchronizeRequest(index: true))
@@ -1360,14 +1360,19 @@ final class BackgroundIndexingTests: XCTestCase {
13601360
])
13611361
)
13621362
try await project.testClient.send(SynchronizeRequest(index: true))
1363+
let dependencyCheckoutFile = try XCTUnwrap(
1364+
FileManager.default.findFiles(
1365+
named: "Dependency.swift",
1366+
in: project.scratchDirectory
1367+
.appendingPathComponent(".build")
1368+
.appendingPathComponent("index-build")
1369+
.appendingPathComponent("checkouts")
1370+
).only
1371+
)
1372+
// Check that modifying Package.resolved actually modified the dependency checkout inside the package
1373+
assertContains(try String(contentsOf: dependencyCheckoutFile, encoding: .utf8), "Do something v1.1.0")
13631374
project.testClient.send(
1364-
DidChangeWatchedFilesNotification(
1365-
changes: FileManager.default.findFiles(
1366-
named: "Dependency.swift",
1367-
in: project.scratchDirectory.appendingPathComponent(".build").appendingPathComponent("index-build")
1368-
.appendingPathComponent("checkouts")
1369-
).map { FileEvent(uri: DocumentURI($0), type: .changed) }
1370-
)
1375+
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: DocumentURI(dependencyCheckoutFile), type: .changed)])
13711376
)
13721377

13731378
try await repeatUntilExpectedResult {

Tests/SourceKitLSPTests/SwiftPMIntegrationTests.swift

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,4 +561,156 @@ final class SwiftPMIntegrationTests: XCTestCase {
561561
return location.range.lowerBound == Position(line: 3, utf16index: 4)
562562
}
563563
}
564+
565+
func testChangePackageManifestFile() async throws {
566+
let project = try await SwiftPMTestProject(
567+
files: [
568+
"Lib.swift": """
569+
#if MY_FLAG
570+
#error("MY_FLAG set")
571+
#else
572+
#error("MY_FLAG not set")
573+
#endif
574+
"""
575+
],
576+
manifest: """
577+
// swift-tools-version: 5.7
578+
import PackageDescription
579+
let package = Package(
580+
name: "MyLibrary",
581+
targets: [.target(name: "MyLibrary")]
582+
)
583+
"""
584+
)
585+
586+
let (uri, _) = try project.openDocument("Lib.swift")
587+
let initialDiagnostics = try await project.testClient.send(
588+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
589+
)
590+
XCTAssertEqual(initialDiagnostics.fullReport?.items.map(\.message), ["MY_FLAG not set"])
591+
592+
try await project.changeFileOnDisk(
593+
"Package.swift",
594+
newMarkedContents: """
595+
// swift-tools-version: 5.7
596+
import PackageDescription
597+
let package = Package(
598+
name: "MyLibrary",
599+
targets: [.target(name: "MyLibrary", swiftSettings: [.define("MY_FLAG")])]
600+
)
601+
"""
602+
)
603+
try await repeatUntilExpectedResult {
604+
let diagnosticsAfterUpdate = try await project.testClient.send(
605+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
606+
)
607+
return diagnosticsAfterUpdate.fullReport?.items.map(\.message) == ["MY_FLAG set"]
608+
}
609+
}
610+
611+
func testChangeVersionSpecificPackageManifestFile() async throws {
612+
let project = try await SwiftPMTestProject(
613+
files: [
614+
"Lib.swift": """
615+
#if MY_FLAG
616+
#error("MY_FLAG set")
617+
#elseif MY_OTHER_FLAG
618+
#error("MY_OTHER_FLAG set")
619+
#else
620+
#error("no flag set")
621+
#endif
622+
""",
623+
624+
// swift-tools-version: 6.1
625+
import PackageDescription
626+
let package = Package(
627+
name: "MyLibrary",
628+
targets: [.target(name: "MyLibrary", swiftSettings: [.define("MY_FLAG")])]
629+
)
630+
""",
631+
],
632+
manifest: """
633+
// swift-tools-version: 5.7
634+
import PackageDescription
635+
let package = Package(
636+
name: "MyLibrary",
637+
targets: [.target(name: "MyLibrary")]
638+
)
639+
"""
640+
)
641+
642+
let (uri, _) = try project.openDocument("Lib.swift")
643+
let initialDiagnostics = try await project.testClient.send(
644+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
645+
)
646+
XCTAssertEqual(initialDiagnostics.fullReport?.items.map(\.message), ["MY_FLAG set"])
647+
648+
try await project.changeFileOnDisk(
649+
650+
newMarkedContents: """
651+
// swift-tools-version: 6.1
652+
import PackageDescription
653+
let package = Package(
654+
name: "MyLibrary",
655+
targets: [.target(name: "MyLibrary", swiftSettings: [.define("MY_OTHER_FLAG")])]
656+
)
657+
"""
658+
)
659+
try await repeatUntilExpectedResult {
660+
let diagnosticsAfterUpdate = try await project.testClient.send(
661+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
662+
)
663+
return diagnosticsAfterUpdate.fullReport?.items.map(\.message) == ["MY_OTHER_FLAG set"]
664+
}
665+
}
666+
667+
func testAddVersionSpecificPackageManifestFile() async throws {
668+
let project = try await SwiftPMTestProject(
669+
files: [
670+
"Lib.swift": """
671+
#if MY_FLAG
672+
#error("MY_FLAG set")
673+
#else
674+
#error("MY_FLAG not set")
675+
#endif
676+
"""
677+
],
678+
manifest: """
679+
// swift-tools-version: 5.7
680+
import PackageDescription
681+
let package = Package(
682+
name: "MyLibrary",
683+
targets: [.target(name: "MyLibrary")]
684+
)
685+
"""
686+
)
687+
688+
let (uri, _) = try project.openDocument("Lib.swift")
689+
let initialDiagnostics = try await project.testClient.send(
690+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
691+
)
692+
XCTAssertEqual(initialDiagnostics.fullReport?.items.map(\.message), ["MY_FLAG not set"])
693+
694+
let versionSpecificManifestUrl = project.scratchDirectory.appending(component: "[email protected]")
695+
try await """
696+
// swift-tools-version: 6.1
697+
import PackageDescription
698+
let package = Package(
699+
name: "MyLibrary",
700+
targets: [.target(name: "MyLibrary", swiftSettings: [.define("MY_FLAG")])]
701+
)
702+
""".writeWithRetry(to: versionSpecificManifestUrl)
703+
704+
project.testClient.send(
705+
DidChangeWatchedFilesNotification(changes: [
706+
FileEvent(uri: DocumentURI(versionSpecificManifestUrl), type: .created)
707+
])
708+
)
709+
try await repeatUntilExpectedResult {
710+
let diagnosticsAfterUpdate = try await project.testClient.send(
711+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
712+
)
713+
return diagnosticsAfterUpdate.fullReport?.items.map(\.message) == ["MY_FLAG set"]
714+
}
715+
}
564716
}

0 commit comments

Comments
 (0)