Skip to content

Commit 1cfb833

Browse files
committed
[Workspace] Detect changes in URLs when identity remains the same
Validates that the URL of the required dependencies are actually the ones that we currently have in the managed dependencies. This fixes a major bug where SwiftPM would not detect if fork of a package is added or removed. <rdar://problem/34302690> Detect changes in dependency URLs when the package identity remains same
1 parent 620bff7 commit 1cfb833

File tree

5 files changed

+131
-7
lines changed

5 files changed

+131
-7
lines changed

Sources/TestSupport/TestWorkspace.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,10 @@ public final class TestWorkspace {
7171
var manifests: [MockManifestLoader.Key: Manifest] = [:]
7272

7373
func create(package: TestPackage, basePath: AbsolutePath, isRoot: Bool) throws {
74-
let packagePath = basePath.appending(component: package.path ?? package.name)
74+
let packagePath = basePath.appending(RelativePath(package.path ?? package.name))
75+
7576
let sourcesDir = packagePath.appending(component: "Sources")
76-
let url = (isRoot ? packagePath : packagesDir.appending(component: package.path ?? package.name)).asString
77+
let url = (isRoot ? packagePath : packagesDir.appending(RelativePath(package.path ?? package.name))).asString
7778
let specifier = RepositorySpecifier(url: url)
7879

7980
// Create targets on disk.
@@ -161,7 +162,7 @@ public final class TestWorkspace {
161162

162163
fileprivate func convert(_ packagesDir: AbsolutePath) -> PackageGraphRootInput.PackageDependency {
163164
return PackageGraphRootInput.PackageDependency(
164-
url: packagesDir.appending(component: name).asString,
165+
url: packagesDir.appending(RelativePath(name)).asString,
165166
requirement: requirement,
166167
location: name
167168
)
@@ -445,7 +446,7 @@ public struct TestDependency {
445446
}
446447

447448
public func convert(baseURL: AbsolutePath) -> PackageDependencyDescription {
448-
return PackageDependencyDescription(url: baseURL.appending(component: name).asString, requirement: requirement)
449+
return PackageDependencyDescription(url: baseURL.appending(RelativePath(name)).asString, requirement: requirement)
449450
}
450451
}
451452

Sources/Workspace/PinsStore.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,19 @@ public final class PinsStore {
127127

128128
/// Unpin all of the currently pinnned dependencies.
129129
///
130-
/// This method does not automatically write to state file.
130+
/// This method does not automatically write to the state file.
131131
public func unpinAll() {
132132
// Reset the pins map.
133133
pinsMap = [:]
134134
}
135135

136+
/// Unpin the given package.
137+
///
138+
/// This method does not automatically write to the state file.
139+
public func unpin(_ package: PackageReference) {
140+
pinsMap[package.identity] = nil
141+
}
142+
136143
/// Creates constraints based on the pins in the store.
137144
public func createConstraints() -> [RepositoryPackageConstraint] {
138145
return pins.map({ pin in

Sources/Workspace/Workspace.swift

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,32 @@ extension Workspace {
880880
return manifestLoader.interpreterFlags(for: toolsVersion.manifestVersion)
881881
}
882882

883+
/// Checks if the managed dependency for a given package reference is still valid.
884+
func validateManagedDependency(for inputRef: PackageReference, diagnostics: DiagnosticsEngine) {
885+
// There is nothing to validate if there is no managed dependency for this package.
886+
guard let dependencyRef = managedDependencies[forIdentity: inputRef.identity]?.packageRef else {
887+
return
888+
}
889+
890+
// Ensure that the package paths are equal.
891+
if dependencyRef.path == inputRef.path {
892+
return
893+
}
894+
895+
// Otherwise, the managed dependency is out-of-sync and we need to
896+
// remove it. This usually happens when a fork is added or removed for
897+
// a dependency.
898+
diagnostics.wrap {
899+
// Remove the dependency.
900+
try self.remove(package: dependencyRef)
901+
902+
// Unpin it.
903+
let pinsStore = try self.pinsStore.load()
904+
pinsStore.unpin(dependencyRef)
905+
try pinsStore.saveState()
906+
}
907+
}
908+
883909
/// Load the manifests for the current dependency tree.
884910
///
885911
/// This will load the manifests for the root package as well as all the
@@ -905,8 +931,13 @@ extension Workspace {
905931
return DependencyManifests(root: root, dependencies: [], workspace: self)
906932
}
907933

908-
let rootDependencyManifests = root.dependencies.compactMap({
909-
return loadManifest(for: $0.createPackageRef(), diagnostics: diagnostics)
934+
let rootDependencyManifests: [Manifest] = root.dependencies.compactMap({
935+
let dependencyRef = $0.createPackageRef()
936+
937+
validateManagedDependency(for: dependencyRef, diagnostics: diagnostics)
938+
if diagnostics.hasErrors { return nil }
939+
940+
return loadManifest(for: dependencyRef, diagnostics: diagnostics)
910941
})
911942
let inputManifests = root.manifests + rootDependencyManifests
912943

@@ -917,6 +948,10 @@ extension Workspace {
917948
let dependencies = transitiveClosure(inputManifests.map({ KeyedPair($0, key: $0.name) })) { node in
918949
return node.item.dependencies.compactMap({ dependency in
919950
let ref = dependency.createPackageRef()
951+
952+
validateManagedDependency(for: ref, diagnostics: diagnostics)
953+
if diagnostics.hasErrors { return nil }
954+
920955
let manifest = loadedManifests[ref.identity] ?? loadManifest(for: ref, diagnostics: diagnostics)
921956
loadedManifests[ref.identity] = manifest
922957
return manifest.flatMap({ KeyedPair($0, key: $0.name) })

Tests/WorkspaceTests/WorkspaceTests.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2175,6 +2175,86 @@ final class WorkspaceTests: XCTestCase {
21752175
}
21762176
}
21772177

2178+
func testDependencySwitchWithSameIdentity() throws {
2179+
let sandbox = AbsolutePath("/tmp/ws/")
2180+
let fs = InMemoryFileSystem()
2181+
2182+
let workspace = try TestWorkspace(
2183+
sandbox: sandbox,
2184+
fs: fs,
2185+
roots: [
2186+
TestPackage(
2187+
name: "Root",
2188+
targets: [
2189+
TestTarget(name: "Root", dependencies: []),
2190+
],
2191+
products: [],
2192+
dependencies: []
2193+
),
2194+
],
2195+
packages: [
2196+
TestPackage(
2197+
name: "Foo",
2198+
targets: [
2199+
TestTarget(name: "Foo"),
2200+
],
2201+
products: [
2202+
TestProduct(name: "Foo", targets: ["Foo"]),
2203+
],
2204+
versions: [nil]
2205+
),
2206+
TestPackage(
2207+
name: "Foo",
2208+
path: "Nested/Foo",
2209+
targets: [
2210+
TestTarget(name: "Foo"),
2211+
],
2212+
products: [
2213+
TestProduct(name: "Foo", targets: ["Foo"]),
2214+
],
2215+
versions: [nil]
2216+
),
2217+
]
2218+
)
2219+
2220+
// Test that switching between two same local packages placed at
2221+
// different locations works correctly.
2222+
2223+
var deps: [TestWorkspace.PackageDependency] = [
2224+
.init(name: "Foo", requirement: .localPackage),
2225+
]
2226+
workspace.checkPackageGraph(roots: ["Root"], deps: deps) { (graph, diagnostics) in
2227+
PackageGraphTester(graph) { result in
2228+
result.check(roots: "Root")
2229+
result.check(packages: "Foo", "Root")
2230+
}
2231+
XCTAssertNoDiagnostics(diagnostics)
2232+
}
2233+
workspace.checkManagedDependencies() { result in
2234+
result.check(dependency: "foo", at: .local)
2235+
}
2236+
do {
2237+
let ws = workspace.createWorkspace()
2238+
let dependency = ws.managedDependencies[forIdentity: "foo"]
2239+
XCTAssertEqual(dependency?.packageRef.path, "/tmp/ws/pkgs/Foo")
2240+
}
2241+
2242+
deps = [
2243+
.init(name: "Nested/Foo", requirement: .localPackage),
2244+
]
2245+
workspace.checkPackageGraph(roots: ["Root"], deps: deps) { (_, diagnostics) in
2246+
XCTAssertNoDiagnostics(diagnostics)
2247+
}
2248+
workspace.checkManagedDependencies() { result in
2249+
result.check(dependency: "foo", at: .local)
2250+
}
2251+
do {
2252+
let ws = workspace.createWorkspace()
2253+
let dependency = ws.managedDependencies[forIdentity: "foo"]
2254+
XCTAssertEqual(dependency?.packageRef.path, "/tmp/ws/pkgs/Nested/Foo")
2255+
}
2256+
}
2257+
21782258
func testResolvedFileUpdate() throws {
21792259
let sandbox = AbsolutePath("/tmp/ws/")
21802260
let fs = InMemoryFileSystem()

Tests/WorkspaceTests/XCTestManifests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ extension WorkspaceTests {
4949
("testDeletedCheckoutDirectory", testDeletedCheckoutDirectory),
5050
("testDependencyManifestLoading", testDependencyManifestLoading),
5151
("testDependencyResolutionWithEdit", testDependencyResolutionWithEdit),
52+
("testDependencySwitchWithSameIdentity", testDependencySwitchWithSameIdentity),
5253
("testEditDependency", testEditDependency),
5354
("testGraphData", testGraphData),
5455
("testGraphRootDependencies", testGraphRootDependencies),

0 commit comments

Comments
 (0)