Skip to content

Commit 6b18ebf

Browse files
authored
improve dependency resolution diagnostics when resolving non-versioned dependencies (#4030)
motivation: when resolving graphs with non-versioned dependencies (such as branch dependencies), the dependencies get "flattened" onto the root node which looses the original association with the dependency's parent yielding incorrect diagnostics changes: * more accuretly track the underlying dependency when computing root dependencies from non-versioned depedendencie * peek into the "real" cause when producing dependency based diagnostics for the root node * only use synthesized root when resolving multi-root-packages, otherwise use the top-level package as the root * add tests rdar://87486528
1 parent df5cce6 commit 6b18ebf

File tree

4 files changed

+643
-107
lines changed

4 files changed

+643
-107
lines changed

Sources/Basics/Dictionary+Extensions.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,13 @@ extension Dictionary {
2020
return value
2121
}
2222
}
23+
24+
extension OrderedDictionary {
25+
public subscript(key: Key, `default` `default`: Value) -> Value {
26+
set {
27+
self[key] = newValue
28+
} get {
29+
self[key] ?? `default`
30+
}
31+
}
32+
}

Sources/PackageGraph/DependencyResolutionNode.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public enum DependencyResolutionNode {
3939
/// They derive from the manifest.
4040
///
4141
/// Tools versions before 5.2 do not know which products belong to which packages, so each product is required from every dependency.
42-
/// Since a non‐existant product ends up with only its implicit dependency on its own package,
42+
/// Since a non‐existent product ends up with only its implicit dependency on its own package,
4343
/// only whichever package contains the product will end up adding additional constraints.
4444
/// See `ProductFilter` and `Manifest.register(...)`.
4545
case product(String, package: PackageReference)
@@ -52,7 +52,7 @@ public enum DependencyResolutionNode {
5252
///
5353
/// - zero or more dependencies on each external product node required to build any of its targets (vended or not).
5454
/// - zero or more dependencies directly on external empty package nodes.
55-
/// This special case occurs when a dependecy is declared but not used.
55+
/// This special case occurs when a dependency is declared but not used.
5656
/// It is a warning condition, and builds do not actually need these dependencies.
5757
/// However, forcing the graph to resolve and fetch them anyway allows the diagnostics passes access
5858
/// to the information needed in order to provide actionable suggestions to help the user stitch up the dependency declarations properly.

Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -132,33 +132,38 @@ public struct PubgrubDependencyResolver {
132132

133133
/// Execute the resolution algorithm to find a valid assignment of versions.
134134
public func solve(constraints: [Constraint]) -> Result<[DependencyResolver.Binding], Error> {
135-
let root = DependencyResolutionNode.root(package: .root(
136-
identity: .plain("<synthesized-root>"),
137-
path: .root
138-
))
135+
// the graph resolution root
136+
let root: DependencyResolutionNode
137+
if constraints.count == 1, let constraint = constraints.first, constraint.package.kind.isRoot {
138+
// root level package, use it as our resolution root
139+
root = .root(package: constraint.package)
140+
} else {
141+
// more complex setup requires a synthesized root
142+
root = .root(package: .root(
143+
identity: .plain("<synthesized-root>"),
144+
path: .root
145+
))
146+
}
139147

140148
do {
141149
// strips state
142150
return .success(try self.solve(root: root, constraints: constraints).bindings)
143151
} catch {
144-
var error = error
145-
146152
// If version solving failing, build the user-facing diagnostic.
147153
if let pubGrubError = error as? PubgrubError, let rootCause = pubGrubError.rootCause, let incompatibilities = pubGrubError.incompatibilities {
148-
var builder = DiagnosticReportBuilder(
149-
root: root,
150-
incompatibilities: incompatibilities,
151-
provider: self.provider
152-
)
153-
154154
do {
155+
var builder = DiagnosticReportBuilder(
156+
root: root,
157+
incompatibilities: incompatibilities,
158+
provider: self.provider
159+
)
155160
let diagnostic = try builder.makeErrorReport(for: rootCause)
156-
error = PubgrubError.unresolvable(diagnostic)
161+
return.failure(PubgrubError.unresolvable(diagnostic))
157162
} catch {
158163
// failed to construct the report, will report the original error
164+
return .failure(error)
159165
}
160166
}
161-
162167
return .failure(error)
163168
}
164169
}
@@ -262,13 +267,13 @@ public struct PubgrubDependencyResolver {
262267
var overriddenPackages: [PackageReference: (version: BoundVersion, products: ProductFilter)] = [:]
263268

264269
// The list of version-based references reachable via local and branch-based references.
265-
// These are added as top-level incompatibilities since they always need to be statisfied.
270+
// These are added as top-level incompatibilities since they always need to be satisfied.
266271
// Some of these might be overridden as we discover local and branch-based references.
267-
var versionBasedDependencies: [DependencyResolutionNode: [VersionBasedConstraint]] = [:]
272+
var versionBasedDependencies = OrderedDictionary<DependencyResolutionNode, [VersionBasedConstraint]>()
268273

269274
// Process unversioned constraints in first phase. We go through all of the unversioned packages
270275
// and collect them and their dependencies. This gives us the complete list of unversioned
271-
// packages in the graph since unversioned packages can only be refered by other
276+
// packages in the graph since unversioned packages can only be referred by other
272277
// unversioned packages.
273278
while let constraint = constraints.first(where: { $0.requirement == .unversioned }) {
274279
constraints.remove(constraint)
@@ -370,7 +375,7 @@ public struct PubgrubDependencyResolver {
370375
case .versionSet(let req):
371376
for node in dependency.nodes() {
372377
let versionedBasedConstraint = VersionBasedConstraint(node: node, req: req)
373-
versionBasedDependencies[node, default: []].append(versionedBasedConstraint)
378+
versionBasedDependencies[.root(package: constraint.package), default: []].append(versionedBasedConstraint)
374379
}
375380
case .revision:
376381
constraints.append(dependency)
@@ -386,13 +391,11 @@ public struct PubgrubDependencyResolver {
386391

387392
// At this point, we should be left with only version-based requirements in our constraints
388393
// list. Add them to our version-based dependency list.
389-
for dependency in constraints {
390-
switch dependency.requirement {
394+
for constraint in constraints {
395+
switch constraint.requirement {
391396
case .versionSet(let req):
392-
for node in dependency.nodes() {
397+
for node in constraint.nodes() {
393398
let versionedBasedConstraint = VersionBasedConstraint(node: node, req: req)
394-
// FIXME: It would be better to record where this constraint came from, instead of just
395-
// using root.
396399
versionBasedDependencies[root, default: []].append(versionedBasedConstraint)
397400
}
398401
case .revision, .unversioned:
@@ -401,11 +404,11 @@ public struct PubgrubDependencyResolver {
401404
}
402405

403406
// Finally, compute the root incompatibilities (which will be all version-based).
407+
// note versionBasedDependencies may point to the root package dependencies, or the dependencies of root's non-versioned dependencies
404408
var rootIncompatibilities: [Incompatibility] = []
405409
for (node, constraints) in versionBasedDependencies {
406410
for constraint in constraints {
407411
if overriddenPackages.keys.contains(constraint.node.package) { continue }
408-
409412
let incompat = try Incompatibility(
410413
Term(root, .exact("1.0.0")),
411414
Term(not: constraint.node, constraint.requirement),
@@ -850,14 +853,20 @@ private struct DiagnosticReportBuilder {
850853

851854
private func description(for incompatibility: Incompatibility) throws -> String {
852855
switch incompatibility.cause {
853-
case .dependency(node: _):
856+
case .dependency(let causeNode):
854857
assert(incompatibility.terms.count == 2)
855858
let depender = incompatibility.terms.first!
856859
let dependee = incompatibility.terms.last!
857860
assert(depender.isPositive)
858861
assert(!dependee.isPositive)
859862

860-
let dependerDesc = try description(for: depender, normalizeRange: true)
863+
let dependerDesc: String
864+
// when depender is the root node, the causeNode may be different as it may represent root's indirect dependencies (e.g. dependencies of root's unversioned dependencies)
865+
if depender.node == self.rootNode, causeNode != self.rootNode {
866+
dependerDesc = causeNode.nameForDiagnostics
867+
} else {
868+
dependerDesc = try description(for: depender, normalizeRange: true)
869+
}
861870
let dependeeDesc = try description(for: dependee)
862871
return "\(dependerDesc) depends on \(dependeeDesc)"
863872
case .noAvailableVersion:
@@ -871,6 +880,8 @@ private struct DiagnosticReportBuilder {
871880
let term = incompatibility.terms.first!
872881
assert(term.isPositive)
873882
return "\(term.node.nameForDiagnostics) is \(term.requirement)"
883+
case .conflict where incompatibility.terms.count == 1 && incompatibility.terms.first?.node == self.rootNode:
884+
return "dependencies could not be resolved"
874885
case .conflict:
875886
break
876887
case .versionBasedDependencyContainsUnversionedDependency(let versionedDependency, let unversionedDependency):
@@ -880,10 +891,6 @@ private struct DiagnosticReportBuilder {
880891
return "\(try description(for: term, normalizeRange: true)) contains incompatible tools version (\(version))"
881892
}
882893

883-
if isFailure(incompatibility) {
884-
return "dependencies could not be resolved"
885-
}
886-
887894
let terms = incompatibility.terms
888895
if terms.count == 1 {
889896
let term = terms.first!
@@ -957,11 +964,6 @@ private struct DiagnosticReportBuilder {
957964
return !lineNumbers.keys.contains(complex)
958965
}
959966

960-
// FIXME: This is duplicated and wrong.
961-
private func isFailure(_ incompatibility: Incompatibility) -> Bool {
962-
return incompatibility.terms.count == 1 && incompatibility.terms.first?.node.package.identity == .plain("<synthesized-root>")
963-
}
964-
965967
private func description(for term: Term, normalizeRange: Bool = false) throws -> String {
966968
let name = term.node.nameForDiagnostics
967969

@@ -1393,6 +1395,7 @@ private extension PackageRequirement {
13931395

13941396
private extension DependencyResolutionNode {
13951397
var nameForDiagnostics: String {
1396-
return "'\(package.identity)'"
1398+
return "'\(self.package.identity)'"
13971399
}
13981400
}
1401+

0 commit comments

Comments
 (0)