-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix dependency resolution edge cases issues #3985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1216,41 +1216,51 @@ internal final class PubGrubPackageContainer { | |
|
||
// Since the pinned version is most likely to succeed, we don't compute bounds for its | ||
// incompatibilities. | ||
return try Array(constraints.map { (constraint: PackageContainerConstraint) -> [Incompatibility] in | ||
guard case .versionSet(let vs) = constraint.requirement else { | ||
return try constraints.flatMap { constraint -> [Incompatibility] in | ||
// We only have version-based requirements at this point. | ||
guard case .versionSet(let constraintRequirement) = constraint.requirement else { | ||
throw InternalError("Unexpected unversioned requirement: \(constraint)") | ||
} | ||
return try constraint.nodes().map { dependencyNode in | ||
var terms: OrderedSet<Term> = [] | ||
// the package version requirement | ||
terms.append(Term(node, .exact(version))) | ||
terms.append(Term(not: dependencyNode, vs)) | ||
// the dependency's version requirement | ||
terms.append(Term(not: dependencyNode, constraintRequirement)) | ||
return try Incompatibility(terms, root: root, cause: .dependency(node: node)) | ||
} | ||
}.joined()) | ||
} | ||
} | ||
|
||
let (lowerBounds, upperBounds) = try self.computeBounds(for: node, | ||
constraints: constraints, | ||
startingWith: version) | ||
|
||
return try constraints.flatMap { constraint in | ||
return try constraint.nodes().map { constraintNode in | ||
var terms: OrderedSet<Term> = [] | ||
let lowerBound = lowerBounds[constraintNode] ?? "0.0.0" | ||
let upperBound = upperBounds[constraintNode] ?? Version(version.major + 1, 0, 0) | ||
assert(lowerBound < upperBound) | ||
let (lowerBounds, upperBounds) = try self.computeBounds( | ||
for: node, | ||
constraints: constraints, | ||
startingWith: version | ||
) | ||
|
||
// We only have version-based requirements at this point. | ||
guard case .versionSet(let vs) = constraint.requirement else { | ||
throw InternalError("Unexpected unversioned requirement: \(constraint)") | ||
return try constraints.flatMap { constraint -> [Incompatibility] in | ||
// We only have version-based requirements at this point. | ||
guard case .versionSet(let constraintRequirement) = constraint.requirement else { | ||
throw InternalError("Unexpected unversioned requirement: \(constraint)") | ||
} | ||
return try constraint.nodes().compactMap { constraintNode in | ||
// cycle | ||
guard node != constraintNode else { | ||
return nil | ||
} | ||
|
||
let requirement: VersionSetSpecifier = .range(lowerBound ..< upperBound) | ||
terms.append(Term(node, requirement)) | ||
terms.append(Term(not: constraintNode, vs)) | ||
|
||
// Make a record for this dependency so we don't have to recompute the bounds when the selected version falls within the bounds. | ||
self.emittedIncompatibilities[constraintNode] = requirement.union(emittedIncompatibilities[constraintNode] ?? .empty) | ||
if let lowerBound = lowerBounds[constraintNode], let upperBound = upperBounds[constraintNode] { | ||
assert(lowerBound < upperBound) | ||
let requirement: VersionSetSpecifier = .range(lowerBound ..< upperBound) | ||
self.emittedIncompatibilities[constraintNode] = requirement.union(emittedIncompatibilities[constraintNode] ?? .empty) | ||
} | ||
|
||
var terms: OrderedSet<Term> = [] | ||
// the package version requirement | ||
terms.append(Term(node, .exact(version))) | ||
// the dependency's version requirement | ||
terms.append(Term(not: constraintNode, constraintRequirement)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is the heart of the change, there a couple of things going on here that seem wrong:
the change suggested here is to always set the @SDGGiesbrecht very interested in your 👀 on this, as you are probably more familiar with this code / logic than I am. I actually hope I am wrong and this is not as problematic as I think it is :D There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have much knowledge here, but I would not be surprised if some of the tests are conceptually wrong and just verifying the implemented behavior. So I wouldn't be concerned about having to change the tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Taking a closer look now. I will get back to you. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe both your observations are correct, @tomerd, and same goes for your solutions. Whatever situation used to hit the defaults will now result in nothing recorded into
I would be curious to know how the defaults were reachable in the first place, and if it could be done from the wild or only by mocked tests that bypass earlier checks.
Yes, that code appears to have been embarrassingly invalid. I think it snuck by everyone for so long because without the proper incompatibility here, it would just fail to record a short‐circuit opportunity and merely be slower, and because the nonsensical extra incompatibility would usually end up so far afield that it would be completely disjoint from the actual problem space. |
||
|
||
return try Incompatibility(terms, root: root, cause: .dependency(node: node)) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -778,7 +778,7 @@ extension Workspace { | |
observabilityScope: ObservabilityScope | ||
) throws -> [(PackageReference, Workspace.PackageStateChange)]? { | ||
// Create cache directories. | ||
createCacheDirectories(observabilityScope: observabilityScope) | ||
self.createCacheDirectories(observabilityScope: observabilityScope) | ||
|
||
// FIXME: this should not block | ||
// Load the root manifests and currently checked out manifests. | ||
|
@@ -793,7 +793,9 @@ extension Workspace { | |
guard let pinsStore = observabilityScope.trap({ try self.pinsStore.load() }) else { return nil } | ||
|
||
// Ensure we don't have any error at this point. | ||
guard !observabilityScope.errorsReported else { return nil } | ||
guard !observabilityScope.errorsReported else { | ||
return nil | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. formatting only |
||
|
||
// Add unversioned constraints for edited packages. | ||
var updateConstraints = currentManifests.editedPackagesConstraints() | ||
|
@@ -824,17 +826,28 @@ extension Workspace { | |
// Reset the active resolver. | ||
self.activeResolver = nil | ||
|
||
guard !observabilityScope.errorsReported else { return nil } | ||
guard !observabilityScope.errorsReported else { | ||
return nil | ||
} | ||
|
||
if dryRun { | ||
return observabilityScope.trap { return try computePackageStateChanges(root: graphRoot, resolvedDependencies: updateResults, updateBranches: true, observabilityScope: observabilityScope) } | ||
return observabilityScope.trap { | ||
return try self.computePackageStateChanges(root: graphRoot, resolvedDependencies: updateResults, updateBranches: true, observabilityScope: observabilityScope) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. formatting only |
||
} | ||
|
||
// Update the checkouts based on new dependency resolution. | ||
let packageStateChanges = self.updateDependenciesCheckouts(root: graphRoot, updateResults: updateResults, updateBranches: true, observabilityScope: observabilityScope) | ||
|
||
// Load the updated manifests. | ||
let updatedDependencyManifests = try self.loadDependencyManifests(root: graphRoot, observabilityScope: observabilityScope) | ||
// If we have missing packages, something is fundamentally wrong with the resolution of the graph | ||
let stillMissingPackages = updatedDependencyManifests.computePackages().missing | ||
guard stillMissingPackages.isEmpty else { | ||
let missing = stillMissingPackages.map{ $0.description } | ||
observabilityScope.emit(error: "exhausted attempts to resolve the dependencies graph, with '\(missing.joined(separator: "', '"))' unresolved.") | ||
return nil | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added validation as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a great improvement too — the silent failures cause so much confusion. |
||
|
||
// Update the resolved file. | ||
self.saveResolvedFile( | ||
|
@@ -1715,7 +1728,7 @@ extension Workspace { | |
let dependenciesToLoad = dependenciesRequired.map{ $0.createPackageRef() }.filter { !loadedManifests.keys.contains($0.identity) } | ||
let dependenciesManifests = try temp_await { self.loadManagedManifests(for: dependenciesToLoad, observabilityScope: observabilityScope, completion: $0) } | ||
dependenciesManifests.forEach { loadedManifests[$0.key] = $0.value } | ||
return pair.item.dependenciesRequired(for: pair.key.productFilter).compactMap { dependency in | ||
return dependenciesRequired.compactMap { dependency in | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small cleanup as |
||
loadedManifests[dependency.identity].flatMap { | ||
// we also compare the location as this function may attempt to load | ||
// dependencies that have the same identity but from a different location | ||
|
@@ -2698,7 +2711,7 @@ extension Workspace { | |
) -> [(PackageReference, PackageStateChange)] { | ||
// Get the update package states from resolved results. | ||
guard let packageStateChanges = observabilityScope.trap({ | ||
try computePackageStateChanges(root: root, resolvedDependencies: updateResults, updateBranches: updateBranches, observabilityScope: observabilityScope) | ||
try self.computePackageStateChanges(root: root, resolvedDependencies: updateResults, updateBranches: updateBranches, observabilityScope: observabilityScope) | ||
}) else { | ||
return [] | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bit of style cleanup, uses flatMap instead of creating an array and then joining it. no functional change.