Skip to content

Commit e79908c

Browse files
authored
Use product IDs to allow duplicate product names in package graph (#5792)
Use product IDs (package id + product name) to allow duplicate product names in a package graph Resolves rdar://94744134
1 parent 1610eda commit e79908c

File tree

11 files changed

+702
-160
lines changed

11 files changed

+702
-160
lines changed

Sources/PackageGraph/ModuleAliasTracker.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class ModuleAliasTracker {
2525
if let aliasList = productRef.moduleAliases {
2626
// Track aliases for this product
2727
try addAliases(aliasList,
28-
productID: productRef.ID,
28+
productID: productRef.identity,
2929
productName: productRef.name,
3030
originPackage: productPkgID,
3131
consumingPackage: package)
@@ -76,20 +76,20 @@ class ModuleAliasTracker {
7676
allTargetDeps.append(contentsOf: targetDeps)
7777
for dep in allTargetDeps {
7878
if case let .product(depRef, _) = dep {
79-
parentToChildProducts[product.ID, default: []].append(depRef.ID)
79+
parentToChildProducts[product.identity, default: []].append(depRef.identity)
8080
}
8181
}
8282

8383
var allTargetsInProduct = targetDeps.compactMap{$0.target}
8484
allTargetsInProduct.append(contentsOf: product.targets)
85-
idToProductToAllTargets[package, default: [:]][product.ID] = allTargetsInProduct
86-
productToDirectTargets[product.ID] = product.targets
87-
productToAllTargets[product.ID] = allTargetsInProduct
85+
idToProductToAllTargets[package, default: [:]][product.identity] = allTargetsInProduct
86+
productToDirectTargets[product.identity] = product.targets
87+
productToAllTargets[product.identity] = allTargetsInProduct
8888
}
8989

9090
func validateAndApplyAliases(product: Product,
9191
package: PackageIdentity) throws {
92-
guard let targets = idToProductToAllTargets[package]?[product.ID] else { return }
92+
guard let targets = idToProductToAllTargets[package]?[product.identity] else { return }
9393
let targetsWithAliases = targets.filter{ $0.moduleAliases != nil }
9494
for target in targetsWithAliases {
9595
if target.sources.containsNonSwiftFiles {
@@ -376,7 +376,7 @@ class ModuleAliasTracker {
376376
}
377377

378378
private func dependencyProductTargets(of targets: [Target]) -> [Target] {
379-
let result = targets.map{$0.dependencies.compactMap{$0.product?.ID}}.flatMap{$0}.compactMap{productToAllTargets[$0]}.flatMap{$0}
379+
let result = targets.map{$0.dependencies.compactMap{$0.product?.identity}}.flatMap{$0}.compactMap{productToAllTargets[$0]}.flatMap{$0}
380380
return result
381381
}
382382
}
@@ -400,7 +400,7 @@ extension Target {
400400
func dependsOn(productID: String) -> Bool {
401401
return dependencies.contains { dep in
402402
if case let .product(prodRef, _) = dep {
403-
return prodRef.ID == productID
403+
return prodRef.identity == productID
404404
}
405405
return false
406406
}

Sources/PackageGraph/PackageGraph+Loading.swift

Lines changed: 62 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -384,69 +384,8 @@ private func createResolvedPackages(
384384
}
385385
}
386386

387-
// Find duplicate products in the package graph.
388-
let productList = packageBuilders.flatMap({ $0.products }).map({ $0.product })
389-
390-
if moduleAliasingUsed {
391-
// FIXME: If moduleAliasingUsed, we want to allow duplicate product names
392-
// from different packages as often times the product name of a package is
393-
// same as its target name which might have a conflict with the name of the
394-
// product itself or its target from another package.
395-
// The following is a workaround; eventually we want to allow duplicate product
396-
// names even when module aliasing is not used, which is a cleaner solution.
397-
// Ref rdar://94744134.
398-
399-
// We first divide the products by type which determines whether to use
400-
// the product ID (unique, fully qualified name) or name to look up duplicates.
401-
402-
// There are no shared dirs/files created for automatic library products, so look
403-
// up duplicates with the ID property for those products.
404-
let autoLibProducts = productList
405-
.filter{ $0.isDefaultLibrary }
406-
.spm_findDuplicateElements(by: \.ID)
407-
.map({ $0[0] })
408-
409-
// Building other products, i.e. static libs, dylibs, executables, result in
410-
// shared dirs/files, e.g. Foo.product (dir), libFoo.dylib, etc., so we want
411-
// to keep the original product names for those. Thus, use the name property
412-
// to look up duplicates.
413-
let otherProducts = productList
414-
.filter{ !$0.isDefaultLibrary }
415-
.spm_findDuplicateElements(by: \.name)
416-
.map({ $0[0] })
417-
418-
let allProducts = autoLibProducts + otherProducts
419-
// Emit diagnostics for duplicate products.
420-
for dupProduct in allProducts {
421-
let packages = packageBuilders
422-
.filter({ $0.products.contains(where: { $0.product.isDefaultLibrary ? $0.product.ID == dupProduct.ID : $0.product.name == dupProduct.name }) })
423-
.map{ $0.package.identity.description }
424-
.sorted()
425-
observabilityScope.emit(PackageGraphError.duplicateProduct(product: dupProduct.name, packages: packages))
426-
}
427-
// Remove the duplicate products from the builders.
428-
let autoLibProductIDs = autoLibProducts.map{ $0.ID }
429-
let otherProductNames = otherProducts.map{ $0.name }
430-
for packageBuilder in packageBuilders {
431-
packageBuilder.products = packageBuilder.products.filter { $0.product.isDefaultLibrary ? !autoLibProductIDs.contains($0.product.ID) : !otherProductNames.contains($0.product.name) }
432-
}
433-
} else {
434-
let duplicateProducts = productList
435-
.spm_findDuplicateElements(by: \.name)
436-
.map({ $0[0] })
437-
// Emit diagnostics for duplicate products.
438-
for dupProduct in duplicateProducts {
439-
let packages = packageBuilders
440-
.filter({ $0.products.contains(where: { $0.product.name == dupProduct.name }) })
441-
.map{ $0.package.identity.description }
442-
.sorted()
443-
observabilityScope.emit(PackageGraphError.duplicateProduct(product: dupProduct.name, packages: packages))
444-
}
445-
// Remove the duplicate products from the builders.
446-
for packageBuilder in packageBuilders {
447-
packageBuilder.products = packageBuilder.products.filter { !duplicateProducts.map{$0.name}.contains($0.product.name) }
448-
}
449-
}
387+
let dupProductsChecker = DuplicateProductsChecker(packageBuilders: packageBuilders)
388+
try dupProductsChecker.run(lookupByProductIDs: moduleAliasingUsed, observabilityScope: observabilityScope)
450389

451390
// The set of all target names.
452391
var allTargetNames = Set<String>()
@@ -473,14 +412,19 @@ private func createResolvedPackages(
473412
return false
474413
})
475414

415+
let lookupByProductIDs = packageBuilder.package.manifest.disambiguateByProductIDs || moduleAliasingUsed
416+
476417
// Get all the products from dependencies of this package.
477418
let productDependencies = packageBuilder.dependencies
478419
.flatMap({ (dependency: ResolvedPackageBuilder) -> [ResolvedProductBuilder] in
479420
// Filter out synthesized products such as tests and implicit executables.
480-
let explicit = Set(dependency.package.manifest.products.lazy.map({ $0.name }))
481-
return dependency.products.filter({ explicit.contains($0.product.name) })
421+
// Check if a dependency product is explicitly declared as a product in its package manifest
422+
let manifestProducts = dependency.package.manifest.products.lazy.map { $0.name }
423+
let explicitProducts = dependency.package.products.filter { manifestProducts.contains($0.name) }
424+
let explicitIdsOrNames = Set(explicitProducts.lazy.map({ lookupByProductIDs ? $0.identity : $0.name }))
425+
return dependency.products.filter({ lookupByProductIDs ? explicitIdsOrNames.contains($0.product.identity) : explicitIdsOrNames.contains($0.product.name) })
482426
})
483-
let productDependencyMap = moduleAliasingUsed ? productDependencies.spm_createDictionary({ ($0.product.ID, $0) }) : productDependencies.spm_createDictionary({ ($0.product.name, $0) })
427+
let productDependencyMap = lookupByProductIDs ? productDependencies.spm_createDictionary({ ($0.product.identity, $0) }) : productDependencies.spm_createDictionary({ ($0.product.name, $0) })
484428

485429
// Establish dependencies in each target.
486430
for targetBuilder in packageBuilder.targets {
@@ -494,7 +438,7 @@ private func createResolvedPackages(
494438
for case .product(let productRef, let conditions) in targetBuilder.target.dependencies {
495439
// Find the product in this package's dependency products.
496440
// Look it up by ID if module aliasing is used, otherwise by name.
497-
let product = moduleAliasingUsed ? productDependencyMap[productRef.ID] : productDependencyMap[productRef.name]
441+
let product = lookupByProductIDs ? productDependencyMap[productRef.identity] : productDependencyMap[productRef.name]
498442
guard let product = product else {
499443
// Only emit a diagnostic if there are no other diagnostics.
500444
// This avoids flooding the diagnostics with product not
@@ -503,7 +447,7 @@ private func createResolvedPackages(
503447
if !observabilityScope.errorsReportedInAnyScope {
504448
// Emit error if a product (not target) declared in the package is also a productRef (dependency)
505449
let declProductsAsDependency = package.products.filter { product in
506-
product.name == productRef.name
450+
lookupByProductIDs ? product.identity == productRef.identity : product.name == productRef.name
507451
}.map {$0.targets}.flatMap{$0}.filter { t in
508452
t.name != productRef.name
509453
}
@@ -570,6 +514,56 @@ fileprivate extension Product {
570514
}
571515
}
572516

517+
private class DuplicateProductsChecker {
518+
var packageIDToBuilder = [String: ResolvedPackageBuilder]()
519+
var checkedPkgIDs = [String]()
520+
521+
init(packageBuilders: [ResolvedPackageBuilder]) {
522+
for packageBuilder in packageBuilders {
523+
let pkgID = packageBuilder.package.identity.description.lowercased()
524+
packageIDToBuilder[pkgID] = packageBuilder
525+
}
526+
}
527+
528+
func run(lookupByProductIDs: Bool = false, observabilityScope: ObservabilityScope) throws {
529+
var productToPkgMap = [String: [String]]()
530+
for (_, pkgBuilder) in packageIDToBuilder {
531+
let useProductIDs = pkgBuilder.package.manifest.disambiguateByProductIDs || lookupByProductIDs
532+
let depProductRefs = pkgBuilder.package.targets.map{$0.dependencies}.flatMap{$0}.compactMap{$0.product}
533+
for depRef in depProductRefs {
534+
if let depPkg = depRef.package?.lowercased() {
535+
checkedPkgIDs.append(depPkg)
536+
let depProductIDs = packageIDToBuilder[depPkg]?.package.products.filter { $0.identity == depRef.identity }.map { useProductIDs && $0.isDefaultLibrary ? $0.identity : $0.name } ?? []
537+
for depID in depProductIDs {
538+
productToPkgMap[depID, default: []].append(depPkg)
539+
}
540+
} else {
541+
let depPkgs = pkgBuilder.dependencies.filter{$0.products.contains{$0.product.name == depRef.name}}.map{$0.package.identity.description.lowercased()}
542+
productToPkgMap[depRef.name, default: []].append(contentsOf: depPkgs)
543+
checkedPkgIDs.append(contentsOf: depPkgs)
544+
}
545+
}
546+
for (depIDOrName, depPkgs) in productToPkgMap.filter({Set($0.value).count > 1}) {
547+
let name = depIDOrName.components(separatedBy: "_").dropFirst().joined(separator: "_")
548+
throw PackageGraphError.duplicateProduct(product: name.isEmpty ? depIDOrName : name, packages: depPkgs.sorted())
549+
}
550+
}
551+
552+
let uncheckedPkgs = packageIDToBuilder.filter{!checkedPkgIDs.contains($0.key)}
553+
for (pkgID, pkgBuilder) in uncheckedPkgs {
554+
let productIDOrNames = pkgBuilder.products.map { pkgBuilder.package.manifest.disambiguateByProductIDs && $0.product.isDefaultLibrary ? $0.product.identity : $0.product.name }
555+
for productIDOrName in productIDOrNames {
556+
productToPkgMap[productIDOrName, default: []].append(pkgID)
557+
}
558+
}
559+
560+
let duplicates = productToPkgMap.filter({Set($0.value).count > 1})
561+
for (productName, pkgs) in duplicates {
562+
throw PackageGraphError.duplicateProduct(product: productName, packages: pkgs.sorted())
563+
}
564+
}
565+
}
566+
573567
private func computePlatforms(
574568
package: Package,
575569
usingXCTest: Bool,

Sources/PackageLoading/Diagnostics.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ extension Basics.Diagnostic {
4040
return .warning("ignoring duplicate product '\(product.name)'\(typeString)")
4141
}
4242

43+
static func duplicateProduct(name: String, package: String) -> Self {
44+
return .warning("ignoring duplicate product '\(name)' from package '\(package)'")
45+
}
46+
4347
static func duplicateTargetDependency(dependency: String, target: String, package: String) -> Self {
4448
.warning("invalid duplicate target dependency declaration '\(dependency)' in target '\(target)' from package '\(package)'")
4549
}

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -765,8 +765,23 @@ public final class PackageBuilder {
765765
}
766766

767767
// Check for duplicate target dependencies
768-
dependencies.filter{$0.product?.moduleAliases == nil}.spm_findDuplicateElements(by: \.nameAndType).map(\.[0].name).forEach {
769-
self.observabilityScope.emit(.duplicateTargetDependency(dependency: $0, target: potentialModule.name, package: self.identity.description))
768+
if self.manifest.disambiguateByProductIDs {
769+
let dupProductIDs = dependencies.compactMap{$0.product?.identity}.spm_findDuplicates()
770+
for dupProductID in dupProductIDs {
771+
let comps = dupProductID.components(separatedBy: "_")
772+
let pkg = comps.first ?? ""
773+
let name = comps.dropFirst().joined(separator: "_")
774+
let dupProductName = name.isEmpty ? dupProductID : name
775+
self.observabilityScope.emit(.duplicateProduct(name: dupProductName, package: pkg))
776+
}
777+
let dupTargetNames = dependencies.compactMap{$0.target?.name}.spm_findDuplicates()
778+
for dupTargetName in dupTargetNames {
779+
self.observabilityScope.emit(.duplicateTargetDependency(dependency: dupTargetName, target: potentialModule.name, package: self.identity.description))
780+
}
781+
} else {
782+
dependencies.filter{$0.product?.moduleAliases == nil}.spm_findDuplicateElements(by: \.nameAndType).map(\.[0].name).forEach {
783+
self.observabilityScope.emit(.duplicateTargetDependency(dependency: $0, target: potentialModule.name, package: self.identity.description))
784+
}
770785
}
771786

772787
// Create the build setting assignment table for this target.

Sources/PackageModel/PackageModel.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ extension Package.Error: CustomStringConvertible {
122122
}
123123
}
124124

125+
extension Manifest {
126+
public var disambiguateByProductIDs: Bool {
127+
return self.toolsVersion >= .vNext
128+
}
129+
}
130+
125131
extension ObservabilityMetadata {
126132
public static func packageMetadata(identity: PackageIdentity, kind: PackageReference.Kind) -> Self {
127133
var metadata = ObservabilityMetadata()

Sources/PackageModel/Product.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public class Product: Codable {
2020
public let name: String
2121

2222
/// Fully qualified name for this product: package ID + name of this product
23-
public let ID: String
23+
public let identity: String
2424

2525
/// The type of product to create.
2626
public let type: ProductType
@@ -54,7 +54,7 @@ public class Product: Codable {
5454
}
5555
self.name = name
5656
self.type = type
57-
self.ID = package.description.lowercased() + "_" + name
57+
self.identity = package.description.lowercased() + "_" + name
5858
self._targets = .init(wrappedValue: targets)
5959
self.testEntryPointPath = testEntryPointPath
6060
}

Sources/PackageModel/Target.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public class Target: PolymorphicCodableProtocol {
5050
public let moduleAliases: [String: String]?
5151

5252
/// Fully qualified name for this product dependency: package ID + name of the product
53-
public var ID: String {
53+
public var identity: String {
5454
if let pkg = package {
5555
return pkg.lowercased() + "_" + name
5656
}

0 commit comments

Comments
 (0)