Skip to content

Commit 00dc44a

Browse files
authored
Provide context necessary to resolve identity conflict. (#8390)
Update error messaging to be a bit more clear where the conflict of identities is coming from. ### Motivation: #8311 — when multiple packages share the same identity (for example: `gh/swiftlang/swift-driver` and `gh/apple/swift-driver` share the same identity `swift-driver`) then the error doesn't help to resolve the conflict. It is particularly confusing when the conflicting package is pulled from transitive dependencies. I wanted to offer more context to resolve these types of conflicts, particularly: - how the conflict is introduced in relation to the root package - itemize all paths for both sides of the conflict, so that users can better understand how to approach resolution, but not overwhelm the error message with too many details ### Modifications: - Rephrase error message for clarity, add a suggestion to work with maintainers of transitive dependencies. - Reconstruct the dependency chains from roots to the conflicting packages. ### Result: Before: `'swift-build': 'swift-build' dependency on 'https://github.com/swiftlang/swift-driver.git' conflicts with dependency on 'https://github.com/apple/swift-driver.git' which has the same identity 'swift-driver'.` After: `Conflicting identity for swift-driver: dependency 'github.com/swiftlang/swift-driver' and dependency 'github.com/apple/swift-driver' both point to the same package identity 'swift-driver'. The dependencies are introduced through the following chains: (A) /users/yy/pub/swift-package-manager->github.com/swiftlang/swift-build->github.com/swiftlang/swift-driver (B) /users/yy/pub/swift-package-manager->github.com/apple/swift-driver. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. ` Plus also debug logs: `debug: 'swift-build': Conflicting identity for swift-driver: chains of dependencies for https://github.com/swiftlang/swift-driver.git: [[/users/yy/pub/swift-package-manager, github.com/swiftlang/swift-build, github.com/swiftlang/swift-driver]]` `debug: 'swift-build': Conflicting identity for swift-driver: chains of dependencies for https://github.com/apple/swift-driver.git: [[/users/yy/pub/swift-package-manager, github.com/apple/swift-driver]]`
1 parent 16b57de commit 00dc44a

File tree

4 files changed

+376
-28
lines changed

4 files changed

+376
-28
lines changed

Sources/PackageGraph/ModulesGraph+Loading.swift

+75-7
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,45 @@ fileprivate extension ResolvedProduct {
336336
}
337337
}
338338

339+
/// Find all transitive dependencies between `root` and `dependency`.
340+
/// - root: A root package to start search from
341+
/// - dependency: A dependency which to find transitive dependencies for.
342+
/// - graph: List of resolved package builders representing a dependency graph.
343+
/// The function returns all possible dependency chains, each chain is a list of nodes representing transitive
344+
/// dependencies between `root` and `dependency`. A dependency chain
345+
/// "A root depends on B, which depends on C" is returned as [Root, B, C].
346+
/// If `root` doesn't actually depend on `dependency` then the function returns empty list.
347+
private func findAllTransitiveDependencies(
348+
root: CanonicalPackageLocation,
349+
dependency: CanonicalPackageLocation,
350+
graph: [ResolvedPackageBuilder]
351+
) throws -> [[CanonicalPackageLocation]] {
352+
let edges = try Dictionary(uniqueKeysWithValues: graph.map { try (
353+
$0.package.manifest.canonicalPackageLocation,
354+
Set(
355+
$0.package.manifest.dependenciesRequired(for: $0.productFilter, $0.enabledTraits)
356+
.map(\.packageRef.canonicalLocation)
357+
)
358+
) })
359+
// Use BFS to find paths between start and finish.
360+
var queue: [(CanonicalPackageLocation, [CanonicalPackageLocation])] = []
361+
var foundPaths: [[CanonicalPackageLocation]] = []
362+
queue.append((root, []))
363+
while !queue.isEmpty {
364+
let currentItem = queue.removeFirst()
365+
let current = currentItem.0
366+
let pathToCurrent = currentItem.1
367+
if current == dependency {
368+
let pathToFinish = pathToCurrent + [current]
369+
foundPaths.append(pathToFinish)
370+
}
371+
for dependency in edges[current] ?? [] {
372+
queue.append((dependency, pathToCurrent + [current]))
373+
}
374+
}
375+
return foundPaths
376+
}
377+
339378
/// Create resolved packages from the loaded packages.
340379
private func createResolvedPackages(
341380
nodes: [GraphLoadingNode],
@@ -410,9 +449,9 @@ private func createResolvedPackages(
410449
guard dependencies[resolvedPackage.package.identity] == nil else {
411450
let error = PackageGraphError.dependencyAlreadySatisfiedByIdentifier(
412451
package: package.identity.description,
413-
dependencyLocation: dependencyPackageRef.locationString,
414-
otherDependencyURL: resolvedPackage.package.manifest.packageLocation,
415-
identity: dependency.identity
452+
identity: dependency.identity,
453+
dependencyLocation: dependencyPackageRef.canonicalLocation.description,
454+
otherDependencyLocation: resolvedPackage.package.manifest.canonicalPackageLocation.description,
416455
)
417456
return packageObservabilityScope.emit(error)
418457
}
@@ -423,11 +462,40 @@ private func createResolvedPackages(
423462
if resolvedPackage.package.manifest.canonicalPackageLocation != dependencyPackageRef
424463
.canonicalLocation && !resolvedPackage.allowedToOverride
425464
{
465+
let rootPackages = packageBuilders.filter { $0.allowedToOverride == true }
466+
let dependenciesPaths = try rootPackages.map { try findAllTransitiveDependencies(
467+
root: $0.package.manifest.canonicalPackageLocation,
468+
dependency: dependencyPackageRef.canonicalLocation,
469+
graph: packageBuilders
470+
) }.filter { !$0.isEmpty }.flatMap { $0 }
471+
let otherDependenciesPaths = try rootPackages.map { try findAllTransitiveDependencies(
472+
root: $0.package.manifest.canonicalPackageLocation,
473+
dependency: resolvedPackage.package.manifest.canonicalPackageLocation,
474+
graph: packageBuilders
475+
) }.filter { !$0.isEmpty }.flatMap { $0 }
476+
packageObservabilityScope
477+
.emit(
478+
debug: (
479+
"Conflicting identity for \(dependency.identity): " +
480+
"chains of dependencies for \(dependencyPackageRef.locationString): " +
481+
"\(String(describing: dependenciesPaths))"
482+
)
483+
)
484+
packageObservabilityScope
485+
.emit(
486+
debug: (
487+
"Conflicting identity for \(dependency.identity): " +
488+
"chains of dependencies for \(resolvedPackage.package.manifest.packageLocation): " +
489+
"\(String(describing: otherDependenciesPaths))"
490+
)
491+
)
426492
let error = PackageGraphError.dependencyAlreadySatisfiedByIdentifier(
427493
package: package.identity.description,
428-
dependencyLocation: dependencyPackageRef.locationString,
429-
otherDependencyURL: resolvedPackage.package.manifest.packageLocation,
430-
identity: dependency.identity
494+
identity: dependency.identity,
495+
dependencyLocation: dependencyPackageRef.canonicalLocation.description,
496+
otherDependencyLocation: resolvedPackage.package.manifest.canonicalPackageLocation.description,
497+
dependencyPath: (dependenciesPaths.first ?? []).map(\.description),
498+
otherDependencyPath: (otherDependenciesPaths.first ?? []).map(\.description)
431499
)
432500
// 9/2021 this is currently emitting a warning only to support
433501
// backwards compatibility with older versions of SwiftPM that had too weak of a validation
@@ -439,7 +507,7 @@ private func createResolvedPackages(
439507
packageObservabilityScope
440508
.emit(
441509
warning: error
442-
.description + ". this will be escalated to an error in future versions of SwiftPM."
510+
.description + " This will be escalated to an error in future versions of SwiftPM."
443511
)
444512
} else {
445513
return packageObservabilityScope.emit(error)

Sources/PackageGraph/ModulesGraph.swift

+34-4
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,19 @@ enum PackageGraphError: Swift.Error {
3636
)
3737

3838
/// The package dependency already satisfied by a different dependency package
39+
/// - package: Package for which the dependency conflict was detected.
40+
/// - identity: Conflicting identity.
41+
/// - dependencyLocation: Dependency from the current package which triggered the conflict.
42+
/// - otherDependencyLocation: Conflicting dependency from another package.
43+
/// - dependencyPath: a dependency path as a list of locations from the root to the dependency that triggered the conflict.
44+
/// - otherDependencyPath: a dependency path as a list of locations from the root to the conflicting dependency from another package.
3945
case dependencyAlreadySatisfiedByIdentifier(
4046
package: String,
47+
identity: PackageIdentity,
4148
dependencyLocation: String,
42-
otherDependencyURL: String,
43-
identity: PackageIdentity
49+
otherDependencyLocation: String,
50+
dependencyPath: [String] = [],
51+
otherDependencyPath: [String] = []
4452
)
4553

4654
/// The package dependency already satisfied by a different dependency package
@@ -300,8 +308,30 @@ extension PackageGraphError: CustomStringConvertible {
300308
}
301309
return description
302310
}
303-
case .dependencyAlreadySatisfiedByIdentifier(let package, let dependencyURL, let otherDependencyURL, let identity):
304-
return "'\(package)' dependency on '\(dependencyURL)' conflicts with dependency on '\(otherDependencyURL)' which has the same identity '\(identity)'"
311+
case .dependencyAlreadySatisfiedByIdentifier(
312+
_,
313+
let identity,
314+
let dependencyURL,
315+
let otherDependencyURL,
316+
let dependencyPath,
317+
let otherDependencyPath
318+
):
319+
var description =
320+
"Conflicting identity for \(identity): " +
321+
"dependency '\(dependencyURL)' and dependency '\(otherDependencyURL)' " +
322+
"both point to the same package identity '\(identity)'."
323+
if !dependencyPath.isEmpty && !otherDependencyPath.isEmpty {
324+
let chainA = dependencyPath.map { String(describing: $0) }.joined(separator: "->")
325+
let chainB = otherDependencyPath.map { String(describing: $0) }.joined(separator: "->")
326+
description += (
327+
" The dependencies are introduced through the following chains: " +
328+
"(A) \(chainA) (B) \(chainB). If there are multiple chains that lead to the same dependency, " +
329+
"only the first chain is shown here. To see all chains use debug output option. " +
330+
"To resolve the conflict, coordinate with the maintainer of the package " +
331+
"that introduces the conflicting dependency."
332+
)
333+
}
334+
return description
305335

306336
case .dependencyAlreadySatisfiedByName(let package, let dependencyURL, let otherDependencyURL, let name):
307337
return "'\(package)' dependency on '\(dependencyURL)' conflicts with dependency on '\(otherDependencyURL)' which has the same explicit name '\(name)'"

Sources/PackageModel/PackageIdentity.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ struct PackageIdentityParser {
413413
/// ```
414414
/// file:///Users/mona/LinkedList → /Users/mona/LinkedList
415415
/// ```
416-
public struct CanonicalPackageLocation: Equatable, CustomStringConvertible {
416+
public struct CanonicalPackageLocation: Equatable, CustomStringConvertible, Hashable {
417417
/// A textual representation of this instance.
418418
public let description: String
419419

0 commit comments

Comments
 (0)