Skip to content

Commit 06351c5

Browse files
Fix PubGrub Container (#3860)
Spun out of #3838. This contains only the pieces related to PubGrubPackageContainer and the bug in its caching. The relevant code branches are traversed and tested under both resolution modes. (But I think the context that flushes out the problems never occurs naturally under legacy resolution.)
1 parent 098b8d4 commit 06351c5

File tree

3 files changed

+187
-44
lines changed

3 files changed

+187
-44
lines changed

Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,7 +1028,7 @@ private struct DiagnosticReportBuilder {
10281028

10291029
/// A container for an individual package. This enhances PackageContainer to add PubGrub specific
10301030
/// logic which is mostly related to computing incompatibilities at a particular version.
1031-
private final class PubGrubPackageContainer {
1031+
internal final class PubGrubPackageContainer {
10321032
/// The underlying package container.
10331033
let underlying: PackageContainer
10341034

@@ -1037,10 +1037,10 @@ private final class PubGrubPackageContainer {
10371037

10381038
/// The map of dependencies to version set that indicates the versions that have had their
10391039
/// incompatibilities emitted.
1040-
private var emittedIncompatibilities = ThreadSafeKeyValueStore<PackageReference, VersionSetSpecifier>()
1040+
private var emittedIncompatibilities = ThreadSafeKeyValueStore<DependencyResolutionNode, VersionSetSpecifier>()
10411041

10421042
/// Whether we've emitted the incompatibilities for the pinned versions.
1043-
private var emittedPinnedVersionIncompatibilities = ThreadSafeBox(false)
1043+
private var emittedPinnedVersionIncompatibilities = ThreadSafeKeyValueStore<DependencyResolutionNode, Bool>()
10441044

10451045
init(underlying: PackageContainer, pinsMap: PinsStore.PinsMap) {
10461046
self.underlying = underlying
@@ -1191,20 +1191,28 @@ private final class PubGrubPackageContainer {
11911191

11921192
// Skip if we already emitted incompatibilities for this dependency such that the selected
11931193
// falls within the previously computed bounds.
1194-
if emittedIncompatibilities[dep.package]?.contains(version) != true {
1195-
constraints.append(dep)
1194+
if emittedIncompatibilities[node]?.contains(version) != true {
1195+
for node in dep.nodes() {
1196+
constraints.append(
1197+
PackageContainerConstraint(
1198+
package: node.package,
1199+
requirement: dep.requirement,
1200+
products: node.productFilter
1201+
)
1202+
)
1203+
}
11961204
}
11971205
}
11981206

11991207
// Emit the dependencies at the pinned version if we haven't emitted anything else yet.
12001208
if version == pinnedVersion, emittedIncompatibilities.isEmpty {
12011209
// We don't need to emit anything if we already emitted the incompatibilities at the
12021210
// pinned version.
1203-
if self.emittedPinnedVersionIncompatibilities.get() ?? false {
1211+
if self.emittedPinnedVersionIncompatibilities[node] ?? false {
12041212
return []
12051213
}
12061214

1207-
self.emittedPinnedVersionIncompatibilities.put(true)
1215+
self.emittedPinnedVersionIncompatibilities[node] = true
12081216

12091217
// Since the pinned version is most likely to succeed, we don't compute bounds for its
12101218
// incompatibilities.
@@ -1223,30 +1231,29 @@ private final class PubGrubPackageContainer {
12231231

12241232
let (lowerBounds, upperBounds) = try self.computeBounds(for: node,
12251233
constraints: constraints,
1226-
startingWith: version,
1227-
products: node.productFilter)
1228-
1229-
return try constraints.map { constraint in
1230-
var terms: OrderedSet<Term> = []
1231-
let lowerBound = lowerBounds[constraint.package] ?? "0.0.0"
1232-
let upperBound = upperBounds[constraint.package] ?? Version(version.major + 1, 0, 0)
1233-
assert(lowerBound < upperBound)
1234-
1235-
// We only have version-based requirements at this point.
1236-
guard case .versionSet(let vs) = constraint.requirement else {
1237-
throw InternalError("Unexpected unversioned requirement: \(constraint)")
1238-
}
1234+
startingWith: version)
1235+
1236+
return try constraints.flatMap { constraint in
1237+
return try constraint.nodes().map { constraintNode in
1238+
var terms: OrderedSet<Term> = []
1239+
let lowerBound = lowerBounds[constraintNode] ?? "0.0.0"
1240+
let upperBound = upperBounds[constraintNode] ?? Version(version.major + 1, 0, 0)
1241+
assert(lowerBound < upperBound)
1242+
1243+
// We only have version-based requirements at this point.
1244+
guard case .versionSet(let vs) = constraint.requirement else {
1245+
throw InternalError("Unexpected unversioned requirement: \(constraint)")
1246+
}
12391247

1240-
for constraintNode in constraint.nodes() {
12411248
let requirement: VersionSetSpecifier = .range(lowerBound ..< upperBound)
12421249
terms.append(Term(node, requirement))
12431250
terms.append(Term(not: constraintNode, vs))
12441251

12451252
// Make a record for this dependency so we don't have to recompute the bounds when the selected version falls within the bounds.
1246-
self.emittedIncompatibilities[constraint.package] = requirement.union(emittedIncompatibilities[constraint.package] ?? .empty)
1247-
}
1253+
self.emittedIncompatibilities[constraintNode] = requirement.union(emittedIncompatibilities[constraintNode] ?? .empty)
12481254

1249-
return try Incompatibility(terms, root: root, cause: .dependency(node: node))
1255+
return try Incompatibility(terms, root: root, cause: .dependency(node: node))
1256+
}
12501257
}
12511258
}
12521259

@@ -1259,9 +1266,8 @@ private final class PubGrubPackageContainer {
12591266
private func computeBounds(
12601267
for node: DependencyResolutionNode,
12611268
constraints: [PackageContainerConstraint],
1262-
startingWith firstVersion: Version,
1263-
products: ProductFilter
1264-
) throws -> (lowerBounds: [PackageReference: Version], upperBounds: [PackageReference: Version]) {
1269+
startingWith firstVersion: Version
1270+
) throws -> (lowerBounds: [DependencyResolutionNode: Version], upperBounds: [DependencyResolutionNode: Version]) {
12651271
let preloadCount = 3
12661272

12671273
// nothing to do
@@ -1274,15 +1280,15 @@ private final class PubGrubPackageContainer {
12741280
for version in versions {
12751281
DispatchQueue.sharedConcurrent.async(group: sync) {
12761282
if self.underlying.isToolsVersionCompatible(at: version) {
1277-
_ = try? self.underlying.getDependencies(at: version, productFilter: products)
1283+
_ = try? self.underlying.getDependencies(at: version, productFilter: node.productFilter)
12781284
}
12791285
}
12801286
}
12811287
sync.wait()
12821288
}
12831289

1284-
func compute(_ versions: [Version], upperBound: Bool) -> [PackageReference: Version] {
1285-
var result: [PackageReference: Version] = [:]
1290+
func compute(_ versions: [Version], upperBound: Bool) -> [DependencyResolutionNode: Version] {
1291+
var result: [DependencyResolutionNode: Version] = [:]
12861292
var previousVersion = firstVersion
12871293

12881294
for (index, version) in versions.enumerated() {
@@ -1299,17 +1305,19 @@ private final class PubGrubPackageContainer {
12991305
let bound = upperBound ? version : previousVersion
13001306

13011307
let isToolsVersionCompatible = self.underlying.isToolsVersionCompatible(at: version)
1302-
for constraint in constraints where !result.keys.contains(constraint.package) {
1303-
// If we hit a version which doesn't have a compatible tools version then that's the boundary.
1304-
// Record the bound if the tools version isn't compatible at the current version.
1305-
if !isToolsVersionCompatible {
1306-
result[constraint.package] = bound
1307-
} else {
1308-
// Get the dependencies at this version.
1309-
if let currentDependencies = try? self.underlying.getDependencies(at: version, productFilter: products) {
1310-
// Record this version as the bound for our list of dependencies, if appropriate.
1311-
if currentDependencies.first(where: { $0.package == constraint.package }) != constraint {
1312-
result[constraint.package] = bound
1308+
for constraint in constraints {
1309+
for constraintNode in constraint.nodes() where !result.keys.contains(constraintNode) {
1310+
// If we hit a version which doesn't have a compatible tools version then that's the boundary.
1311+
// Record the bound if the tools version isn't compatible at the current version.
1312+
if !isToolsVersionCompatible {
1313+
result[constraintNode] = bound
1314+
} else {
1315+
// Get the dependencies at this version.
1316+
if let currentDependencies = try? self.underlying.getDependencies(at: version, productFilter: constraintNode.productFilter) {
1317+
// Record this version as the bound for our list of dependencies, if appropriate.
1318+
if currentDependencies.first(where: { $0.package == constraint.package }) != constraint {
1319+
result[constraintNode] = bound
1320+
}
13131321
}
13141322
}
13151323
}
@@ -1334,12 +1342,12 @@ private final class PubGrubPackageContainer {
13341342

13351343
let sync = DispatchGroup()
13361344

1337-
var upperBounds = [PackageReference: Version]()
1345+
var upperBounds = [DependencyResolutionNode: Version]()
13381346
DispatchQueue.sharedConcurrent.async(group: sync) {
13391347
upperBounds = compute(Array(versions.dropFirst(idx + 1)), upperBound: true)
13401348
}
13411349

1342-
var lowerBounds = [PackageReference: Version]()
1350+
var lowerBounds = [DependencyResolutionNode: Version]()
13431351
DispatchQueue.sharedConcurrent.async(group: sync) {
13441352
lowerBounds = compute(Array(versions.dropLast(versions.count - idx).reversed()), upperBound: false)
13451353
}

Sources/SPMTestSupport/MockPackageContainer.swift

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public class MockPackageContainer: PackageContainer {
2626
public let package: PackageReference
2727

2828
let dependencies: [String: [Dependency]]
29+
let filteredMode: Bool
30+
let filteredDependencies: [ProductFilter: [Dependency]]
2931

3032
public var unversionedDeps: [MockPackageContainer.Constraint] = []
3133

@@ -47,7 +49,13 @@ public class MockPackageContainer: PackageContainer {
4749
}
4850

4951
public func getDependencies(at revision: String, productFilter: ProductFilter) -> [MockPackageContainer.Constraint] {
50-
return dependencies[revision]!.map { value in
52+
let dependencies: [Dependency]
53+
if filteredMode {
54+
dependencies = filteredDependencies[productFilter]!
55+
} else {
56+
dependencies = self.dependencies[revision]!
57+
}
58+
return dependencies.map { value in
5159
let (package, requirement) = value
5260
return MockPackageContainer.Constraint(package: package, requirement: requirement, products: productFilter)
5361
}
@@ -97,6 +105,29 @@ public class MockPackageContainer: PackageContainer {
97105
self.package = package
98106
self._versions = dependencies.keys.compactMap(Version.init(_:)).sorted()
99107
self.dependencies = dependencies
108+
self.filteredMode = false
109+
self.filteredDependencies = [:]
110+
}
111+
112+
public init(
113+
name: String,
114+
dependenciesByProductFilter: [ProductFilter: [(container: String, versionRequirement: VersionSetSpecifier)]]
115+
) {
116+
var dependencies: [ProductFilter: [Dependency]] = [:]
117+
for (filter, deps) in dependenciesByProductFilter {
118+
dependencies[filter] = deps.map {
119+
let path = AbsolutePath("/\($0.container)")
120+
let ref = PackageReference.localSourceControl(identity: .init(path: path), path: path)
121+
return (ref, .versionSet($0.versionRequirement))
122+
}
123+
}
124+
let path = AbsolutePath("/\(name)")
125+
let ref = PackageReference.localSourceControl(identity: .init(path: path), path: path)
126+
self.package = ref
127+
self._versions = [Version(1, 0, 0)]
128+
self.dependencies = [:]
129+
self.filteredMode = true
130+
self.filteredDependencies = dependencies
100131
}
101132
}
102133

Tests/PackageGraphTests/PubgrubTests.swift

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,6 +1160,110 @@ final class PubgrubTests: XCTestCase {
11601160
XCTAssertMatch(delegate.events, ["didResolve 'bar' at '2.0.1'"])
11611161
XCTAssertMatch(delegate.events, ["solved: 'bar' at '2.0.1', 'foo' at '1.1.0'"])
11621162
}
1163+
1164+
func testPubGrubPackageContainerCacheParameterization() {
1165+
let container = PubGrubPackageContainer(
1166+
underlying: MockPackageContainer(
1167+
name: "Package",
1168+
dependenciesByProductFilter: [
1169+
.specific(["FilterA"]): [(
1170+
container: "DependencyA",
1171+
versionRequirement: .exact(Version(1, 0, 0))
1172+
)],
1173+
.specific(["FilterB"]): [(
1174+
container: "DependencyB",
1175+
versionRequirement: .exact(Version(1, 0, 0))
1176+
)]
1177+
]),
1178+
pinsMap: PinsStore.PinsMap()
1179+
)
1180+
let rootLocation = AbsolutePath("/Root")
1181+
let otherLocation = AbsolutePath("/Other")
1182+
let dependencyALocation = AbsolutePath("/DependencyA")
1183+
let dependencyBLocation = AbsolutePath("/DependencyB")
1184+
let root = PackageReference(identity: PackageIdentity(path: rootLocation), kind: .fileSystem(rootLocation))
1185+
let other = PackageReference.localSourceControl(identity: .init(path: otherLocation), path: otherLocation)
1186+
let dependencyA = PackageReference.localSourceControl(identity: .init(path: dependencyALocation), path: dependencyALocation)
1187+
let dependencyB = PackageReference.localSourceControl(identity: .init(path: dependencyBLocation), path: dependencyBLocation)
1188+
XCTAssertEqual(
1189+
try container.incompatibilites(
1190+
at: Version(1, 0, 0),
1191+
node: .product(
1192+
"FilterA",
1193+
package: other
1194+
),
1195+
overriddenPackages: [:],
1196+
root: .root(package: root)
1197+
),
1198+
[
1199+
Incompatibility(
1200+
terms: [
1201+
Term(
1202+
node: .product("FilterA", package: other), requirement: .range(Version(0, 0, 0)..<Version(2, 0, 0)),
1203+
isPositive: true
1204+
),
1205+
Term(
1206+
node: .product("FilterA", package: dependencyA), requirement: .exact(Version(1, 0, 0)),
1207+
isPositive: false
1208+
)
1209+
],
1210+
cause: .dependency(node: .product("FilterA", package: other))
1211+
),
1212+
Incompatibility(
1213+
terms: [
1214+
Term(
1215+
node: .product("FilterA", package: other), requirement: .range(Version(0, 0, 0)..<Version(2, 0, 0)),
1216+
isPositive: true
1217+
),
1218+
Term(
1219+
node: .empty(package: other), requirement: .exact(Version(1, 0, 0)),
1220+
isPositive: false
1221+
),
1222+
],
1223+
cause: .dependency(node: .product("FilterA", package: other))
1224+
)
1225+
]
1226+
)
1227+
XCTAssertEqual(
1228+
try container.incompatibilites(
1229+
at: Version(1, 0, 0),
1230+
node: .product(
1231+
"FilterB",
1232+
package: other
1233+
),
1234+
overriddenPackages: [:],
1235+
root: .root(package: root)
1236+
),
1237+
[
1238+
Incompatibility(
1239+
terms: [
1240+
Term(
1241+
node: .product("FilterB", package: other), requirement: .range(Version(0, 0, 0)..<Version(2, 0, 0)),
1242+
isPositive: true
1243+
),
1244+
Term(
1245+
node: .product("FilterB", package: dependencyB), requirement: .exact(Version(1, 0, 0)),
1246+
isPositive: false
1247+
)
1248+
],
1249+
cause: .dependency(node: .product("FilterB", package: other))
1250+
),
1251+
Incompatibility(
1252+
terms: [
1253+
Term(
1254+
node: .product("FilterB", package: other), requirement: .range(Version(0, 0, 0)..<Version(2, 0, 0)),
1255+
isPositive: true
1256+
),
1257+
Term(
1258+
node: .empty(package: other), requirement: .exact(Version(1, 0, 0)),
1259+
isPositive: false
1260+
),
1261+
],
1262+
cause: .dependency(node: .product("FilterB", package: other))
1263+
)
1264+
]
1265+
)
1266+
}
11631267
}
11641268

11651269
final class PubGrubTestsBasicGraphs: XCTestCase {

0 commit comments

Comments
 (0)