-
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
fix dependency resolution edge cases issues #3985
Conversation
@swift-ci please smoke test |
motivation: dependency resolution could be incorrect in some edge cases when a pre-release requirement is specified, and not report the issue to the user changes: * depedency resolustion should not assume 0.0.0 is the correct minimal requirment, instead it should use the specified version * fix dependency term computation which used incorrect version requirment for the the parent node * add validation at the end of package udpate API to make sure no missing packages exist after resolution, and fail the operation if such exists. this mirrors the behavior of package resolve API
04ccc12
to
6c445b5
Compare
// 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 comment
The 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:
-
requirement
was used for two different purposes: to record the dependency version (self.emittedIncompatibilities[constraintNode] = requirement.union(emittedIncompatibilities[constraintNode] ?? .empty)
) but also as the resolved version fornode
(terms.append(Term(node, requirement))
), which iiuc does not really make sense as it mixed the dependency (constraintNode
) version with the package (node
) version. -
requirement
is looked up on the bounds computed for the dependencies usingcomputeBounds
withconstraintNode
as the key which is correct, however if not found, defaults to 0.0.0 < version(major+1, 0, 0). from what I can see, the defaulting is wrong in two ways: it does not work for pre-release versions since it strips the pre-release extensions (e.g..exact("1.0.0-alpha.10")
becomes.range(0.0.0 ..< 2.0.0)
which is causing downstream issue as we are using range checks later in the graph resolution and "1.0.0-alpha.10" is not included in 0.0.0 ..< 2.0.0 based on our version inclusion rules (which is the bug I was chasing and got me here). maybe slightly worse, the default is based onversion
which is the version ofnode
and has nothing to do withconstraintNode
so we end up recording (inself.emittedIncompatibilities
) version ranges that are potentially wrong
the change suggested here is to always set the node
term to the exact version we are computing for which works for pre-release and non-pre-release. it does however break a bunch of tests that recorded the 0.0.0 < version(major+1, 0, 0) behavior, but maybe that is okay since they are testing for wrong behavior?
@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 comment
The 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 comment
The 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 comment
The 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 emittedIncompatibilities
. At first that concerned me, but after tracking where that leads, it feels safe, even if a little odd. The only two times emittedIncompatibilities
is read from are:
- To skip processing a node that has already been handled. Such nodes will be re‐processed now, but I don’t think that would be a problem.
- To not trust a pin if we have already encountered anything odd. This too only prevents a shortcut, as the whole graph will be traversed either way, just on the one hand not necessarily trying the most probable solution first when obvious, or on the other hand not preloading alternatives when overestimating the probability that a solution will pan out.
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.
I actually hope I am wrong and this is not as problematic as I think it is :D
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
formatting only
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 comment
The reason will be displayed to describe this comment to others. Learn more.
formatting only
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 comment
The reason will be displayed to describe this comment to others. Learn more.
added validation as package update
could fail silently without this check
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.
This is a great improvement too — the silent failures cause so much confusion.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
small cleanup as dependenciesRequired
is already defined above as pair.item.dependenciesRequired(for: pair.key.productFilter)
@@ -496,7 +496,7 @@ final class PubgrubTests: XCTestCase { | |||
]) | |||
} | |||
|
|||
func testCycle1() { | |||
func _testCycle1() { |
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.
this test crashes now on a precondition and will be fixed together with the other failing tests if this change is good
return try Incompatibility(terms, root: root, cause: .dependency(node: node)) | ||
} | ||
}.joined()) | ||
} |
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.
@swift-ci please smoke test |
@swift-ci please test package compatibility |
@swift-ci please smoke test macOS |
motivation: dependency resolution could be incorrect in some edge cases when a pre-release requirement is specified, and not report the issue to the user changes: * depedency resolustion should not assume 0.0.0 is the correct minimal requirment, instead it should use the specified version * fix dependency term computation which used incorrect version requirement for the the parent node * add validation at the end of package udpate API to make sure no missing packages exist after resolution, and fail the operation if such exists. this mirrors the behavior of package resolve API * adjust tests
motivation: dependency resolution could be incorrect in some edge cases when a pre-release requirement is specified, and not report the issue to the user changes: * depedency resolustion should not assume 0.0.0 is the correct minimal requirment, instead it should use the specified version * fix dependency term computation which used incorrect version requirement for the the parent node * add validation at the end of package udpate API to make sure no missing packages exist after resolution, and fail the operation if such exists. this mirrors the behavior of package resolve API * adjust tests
motivation: improve dependency resolution performance changes: * add informational diagnostics on manifest cache hit/miss * disable expensive optimization in pubgrub resolver that has no functional value given changes from swiftlang#3985 * adjust resolver workspace delgate to be less noisy given the difference in resolution callbacks
motivation: improve dependency resolution performance changes: * add informational diagnostics on manifest cache hit/miss * disable expensive optimization in pubgrub resolver that has no functional value given changes from swiftlang#3985 * adjust resolver workspace delgate to be less noisy given the difference in resolution callbacks
motivation: improve dependency resolution performance changes: * add informational diagnostics on manifest cache hit/miss * disable expensive optimization in pubgrub resolver that has no functional value given changes from swiftlang#3985 * adjust resolver workspace delgate to be less noisy given the difference in resolution callbacks
motivation: improve dependency resolution performance changes: * add informational diagnostics on manifest cache hit/miss * disable expensive optimization in pubgrub resolver that has no functional value given changes from #3985 * adjust resolver workspace delgate to be less noisy given the difference in resolution callbacks
motivation: improve dependency resolution performance changes: * add informational diagnostics on manifest cache hit/miss * disable expensive optimization in pubgrub resolver that has no functional value given changes from swiftlang#3985 * adjust resolver workspace delgate to be less noisy given the difference in resolution callbacks
motivation: improve dependency resolution performance changes: * add informational diagnostics on manifest cache hit/miss * disable expensive optimization in pubgrub resolver that has no functional value given changes from #3985 * adjust resolver workspace delgate to be less noisy given the difference in resolution callbacks
motivation: dependency resolution could be incorrect in some edge cases when a pre-release requirement is specified, and not report the issue to the user
changes: