Skip to content

Commit 34e4507

Browse files
committed
better diagnostics when potentially duplicate packages are found
motivation: with the intriduction of the regsitry, there is a higher likelyhood of running into duplicate packages changes: perform a heuristics to try and determin if the packages are duplicate and provide a better diagnostics instead of the standard "duplicate product" one
1 parent aa6f358 commit 34e4507

File tree

2 files changed

+85
-9
lines changed

2 files changed

+85
-9
lines changed

Sources/PackageGraph/PackageGraph+Loading.swift

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ private func createResolvedPackages(
505505
throw InternalError("dependency reference for \(product.packageBuilder.package.manifest.packageLocation) not found")
506506
}
507507
let referencedPackageName = referencedPackageDependency.nameForTargetDependencyResolutionOnly
508-
if productRef.name != referencedPackageName {
508+
if productRef.name != referencedPackageName {
509509
let error = PackageGraphError.productDependencyMissingPackage(
510510
productName: productRef.name,
511511
targetName: targetBuilder.target.name,
@@ -522,16 +522,80 @@ private func createResolvedPackages(
522522

523523
// If a target with similar name was encountered before, we emit a diagnostic.
524524
if foundDuplicateTarget {
525+
var duplicateTargets = [String: [Package]]()
525526
for targetName in allTargetNames.sorted() {
526-
// Find the packages this target is present in.
527527
let packages = packageBuilders
528528
.filter({ $0.targets.contains(where: { $0.target.name == targetName }) })
529-
.map{ $0.package.identity.description }
530-
.sorted()
529+
.map{ $0.package }
531530
if packages.count > 1 {
532-
observabilityScope.emit(ModuleError.duplicateModule(targetName, packages))
531+
duplicateTargets[targetName, default: []].append(contentsOf: packages)
533532
}
534533
}
534+
535+
struct Pair: Hashable {
536+
let package1: Package
537+
let package2: Package
538+
539+
static func == (lhs: Pair, rhs: Pair) -> Bool {
540+
return lhs.package1.identity == rhs.package1.identity &&
541+
lhs.package2.identity == rhs.package2.identity
542+
}
543+
544+
public func hash(into hasher: inout Hasher) {
545+
hasher.combine(self.package1.identity)
546+
hasher.combine(self.package2.identity)
547+
}
548+
}
549+
550+
var potentiallyDuplicatePackages = [Pair: [String]]()
551+
for entry in duplicateTargets {
552+
// the duplicate is across exactly two packages
553+
if entry.value.count == 2 {
554+
potentiallyDuplicatePackages[Pair(package1: entry.value[0], package2: entry.value[1]), default: []].append(entry.key)
555+
}
556+
}
557+
558+
var duplicateTargetsAddressed = [String]()
559+
for potentiallyDuplicatePackage in potentiallyDuplicatePackages {
560+
// more than three target matches, or all targets in the package match
561+
if potentiallyDuplicatePackage.value.count > 3 || potentiallyDuplicatePackage.value.sorted() == potentiallyDuplicatePackage.key.package1.targets.map({ $0.name }).sorted() {
562+
switch (potentiallyDuplicatePackage.key.package1.identity.registry, potentiallyDuplicatePackage.key.package2.identity.registry) {
563+
case (.some(let registryIdentity), .none):
564+
observabilityScope.emit(
565+
ModuleError.duplicateModulesScmAndRegistry(
566+
regsitryPackage: registryIdentity,
567+
scmPackage: potentiallyDuplicatePackage.key.package2.identity,
568+
targets: potentiallyDuplicatePackage.value
569+
)
570+
)
571+
case (.none, .some(let registryIdentity)):
572+
observabilityScope.emit(
573+
ModuleError.duplicateModulesScmAndRegistry(
574+
regsitryPackage: registryIdentity,
575+
scmPackage: potentiallyDuplicatePackage.key.package1.identity,
576+
targets: potentiallyDuplicatePackage.value
577+
)
578+
)
579+
default:
580+
observabilityScope.emit(
581+
ModuleError.duplicateModules(
582+
package: potentiallyDuplicatePackage.key.package1.identity,
583+
otherPackage: potentiallyDuplicatePackage.key.package2.identity,
584+
targets: potentiallyDuplicatePackage.value
585+
)
586+
)
587+
}
588+
duplicateTargetsAddressed += potentiallyDuplicatePackage.value
589+
}
590+
}
591+
592+
for entry in duplicateTargets.filter({ !duplicateTargetsAddressed.contains($0.key) }) {
593+
observabilityScope.emit(
594+
ModuleError.duplicateModule(
595+
targetName: entry.key,
596+
packages: entry.value.map{ $0.identity })
597+
)
598+
}
535599
}
536600

537601
return try packageBuilders.map{ try $0.construct() }

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public enum ModuleError: Swift.Error {
2727
}
2828

2929
/// Indicates two targets with the same name and their corresponding packages.
30-
case duplicateModule(String, [String])
30+
case duplicateModule(targetName: String, packages: [PackageIdentity])
3131

3232
/// The referenced target could not be found.
3333
case moduleNotFound(String, TargetDescription.TargetType, shouldSuggestRelaxedSourceDir: Bool)
@@ -79,14 +79,20 @@ public enum ModuleError: Swift.Error {
7979

8080
/// A C target has declared an embedded resource
8181
case embedInCodeNotSupported(target: String)
82+
83+
/// Indicates several targets with the same name exist in packages
84+
case duplicateModules(package: PackageIdentity, otherPackage: PackageIdentity, targets: [String])
85+
86+
/// Indicates several targets with the same name exist in a registry and scm package
87+
case duplicateModulesScmAndRegistry(regsitryPackage: PackageIdentity.RegistryIdentity, scmPackage: PackageIdentity, targets: [String])
8288
}
8389

8490
extension ModuleError: CustomStringConvertible {
8591
public var description: String {
8692
switch self {
87-
case .duplicateModule(let name, let packages):
88-
let packages = packages.joined(separator: "', '")
89-
return "multiple targets named '\(name)' in: '\(packages)'; consider using the `moduleAliases` parameter in manifest to provide unique names"
93+
case .duplicateModule(let target, let packages):
94+
let packages = packages.map{ $0.description }.sorted().joined(separator: "', '")
95+
return "multiple targets named '\(target)' in: '\(packages)'; consider using the `moduleAliases` parameter in manifest to provide unique names"
9096
case .moduleNotFound(let target, let type, let shouldSuggestRelaxedSourceDir):
9197
let folderName = (type == .test) ? "Tests" : (type == .plugin) ? "Plugins" : "Sources"
9298
var clauses = ["Source files for target \(target) should be located under '\(folderName)/\(target)'"]
@@ -134,6 +140,12 @@ extension ModuleError: CustomStringConvertible {
134140
return "plugin target '\(target)' doesn't have a 'capability' property"
135141
case .embedInCodeNotSupported(let target):
136142
return "embedding resources in code not supported for C-family language target \(target)"
143+
case .duplicateModules(let package, let otherPackage, let targets):
144+
let targets = targets.sorted().joined(separator: "', '")
145+
return "multiple identical targets '\(targets)' appear in package '\(package)' and '\(otherPackage)', this may indicate that the two packages are the same and can be de-duplicated by using mirrors. if they are not duplicate consider using the `moduleAliases` parameter in manifest to provide unique names"
146+
case .duplicateModulesScmAndRegistry(let regsitryPackage, let scmPackage, let targets):
147+
let targets = targets.sorted().joined(separator: "', '")
148+
return "multiple identical targets '\(targets)' appear in registry package '\(regsitryPackage)' and source control package '\(scmPackage)', this may indicate that the two packages are the same and can be de-duplicated by activating the automatic source-control to registry replacement, or by using mirrors. if they are not duplicate consider using the `moduleAliases` parameter in manifest to provide unique names"
137149
}
138150
}
139151
}

0 commit comments

Comments
 (0)