Skip to content

Commit 757df3b

Browse files
authored
[5.7] Allow duplicate product names when module aliasing is used (#5586)
* Allow duplicate automatic library product names when module aliasing is used. (#5578) This change is for automatic library products only. No product names are altered. Add an ID property to Product and ProductReference: combine package ID and product name to make it fully qualified. Pass packageID to Product/Reference initializers. Use product IDs to look up duplicates if the product is an automatic library type. Use product IDs to compute aliasing related logic in ModuleAliasTracker. Resolves rdar://89836609. * Reorganize files for module aliasing (#5529) Resolves rdar://93611579
1 parent 01c8363 commit 757df3b

File tree

10 files changed

+3112
-2611
lines changed

10 files changed

+3112
-2611
lines changed

Sources/PackageGraph/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ add_library(PackageGraph
1313
DependencyResolver.swift
1414
Diagnostics.swift
1515
GraphLoadingNode.swift
16+
ModuleAliasTracker.swift
1617
PackageContainer.swift
1718
PackageGraph.swift
1819
PackageGraph+Loading.swift
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import PackageModel
2+
3+
// This class helps track module aliases in a package graph and override
4+
// upstream alises if needed
5+
class ModuleAliasTracker {
6+
var aliasMap = [PackageIdentity: [String: [ModuleAliasModel]]]()
7+
var idToProductToAllTargets = [PackageIdentity: [String: [Target]]]()
8+
var productToDirectTargets = [String: [Target]]()
9+
var productToAllTargets = [String: [Target]]()
10+
var parentToChildProducts = [String: [String]]()
11+
var childToParentProducts = [String: [String]]()
12+
var parentToChildIDs = [PackageIdentity: [PackageIdentity]]()
13+
var childToParentID = [PackageIdentity: PackageIdentity]()
14+
15+
init() {}
16+
func addTargetAliases(targets: [Target], package: PackageIdentity) throws {
17+
let targetDependencies = targets.map{$0.dependencies}.flatMap{$0}
18+
for dep in targetDependencies {
19+
if case let .product(productRef, _) = dep,
20+
let productPkg = productRef.package {
21+
let productPkgID = PackageIdentity.plain(productPkg)
22+
// Track dependency package ID chain
23+
addPackageIDChain(parent: package, child: productPkgID)
24+
if let aliasList = productRef.moduleAliases {
25+
// Track aliases for this product
26+
try addAliases(aliasList,
27+
productID: productRef.ID,
28+
productName: productRef.name,
29+
originPackage: productPkgID,
30+
consumingPackage: package)
31+
}
32+
}
33+
}
34+
}
35+
36+
func addAliases(_ aliases: [String: String],
37+
productID: String,
38+
productName: String,
39+
originPackage: PackageIdentity,
40+
consumingPackage: PackageIdentity) throws {
41+
if let aliasDict = aliasMap[originPackage] {
42+
let existingAliases = aliasDict.values.flatMap{$0}.filter { aliases.keys.contains($0.name) }
43+
for existingAlias in existingAliases {
44+
if let newAlias = aliases[existingAlias.name], newAlias != existingAlias.alias {
45+
// Error if there are multiple different aliases specified for
46+
// targets in this product
47+
throw PackageGraphError.multipleModuleAliases(target: existingAlias.name, product: productName, package: originPackage.description, aliases: existingAliases.map{$0.alias} + [newAlias])
48+
}
49+
}
50+
}
51+
52+
for (originalName, newName) in aliases {
53+
let model = ModuleAliasModel(name: originalName, alias: newName, originPackage: originPackage, consumingPackage: consumingPackage)
54+
aliasMap[originPackage, default: [:]][productID, default: []].append(model)
55+
}
56+
}
57+
58+
func addPackageIDChain(parent: PackageIdentity,
59+
child: PackageIdentity) {
60+
if parentToChildIDs[parent]?.contains(child) ?? false {
61+
// Already added
62+
} else {
63+
parentToChildIDs[parent, default: []].append(child)
64+
// Used to track the top-most level package
65+
childToParentID[child] = parent
66+
}
67+
}
68+
69+
// This func should be called once per product
70+
func trackTargetsPerProduct(product: Product,
71+
package: PackageIdentity) {
72+
let targetDeps = product.targets.map{$0.dependencies}.flatMap{$0}
73+
var allTargetDeps = product.targets.map{$0.dependentTargets().map{$0.dependencies}}.flatMap{$0}.flatMap{$0}
74+
allTargetDeps.append(contentsOf: targetDeps)
75+
for dep in allTargetDeps {
76+
if case let .product(depRef, _) = dep {
77+
childToParentProducts[depRef.ID, default: []].append(product.ID)
78+
parentToChildProducts[product.ID, default: []].append(depRef.ID)
79+
}
80+
}
81+
82+
var allTargetsInProduct = targetDeps.compactMap{$0.target}
83+
allTargetsInProduct.append(contentsOf: product.targets)
84+
idToProductToAllTargets[package, default: [:]][product.ID] = allTargetsInProduct
85+
productToDirectTargets[product.ID] = product.targets
86+
productToAllTargets[product.ID] = allTargetsInProduct
87+
}
88+
89+
func validateAndApplyAliases(product: Product,
90+
package: PackageIdentity) throws {
91+
guard let targets = idToProductToAllTargets[package]?[product.ID] else { return }
92+
let targetsWithAliases = targets.filter{ $0.moduleAliases != nil }
93+
for target in targetsWithAliases {
94+
if target.sources.containsNonSwiftFiles {
95+
throw PackageGraphError.invalidSourcesForModuleAliasing(target: target.name, product: product.name, package: package.description)
96+
}
97+
target.applyAlias()
98+
}
99+
}
100+
101+
func propagateAliases() {
102+
// First get the root package ID
103+
var pkgID = childToParentID.first?.key
104+
var rootPkg = pkgID
105+
while pkgID != nil {
106+
rootPkg = pkgID
107+
// pkgID is not nil here so can be force unwrapped
108+
pkgID = childToParentID[pkgID!]
109+
}
110+
guard let rootPkg = rootPkg else { return }
111+
// Propagate and override upstream aliases if needed
112+
var aliasBuffer = [String: ModuleAliasModel]()
113+
propagate(package: rootPkg, aliasBuffer: &aliasBuffer)
114+
// Now merge overriden upstream aliases and add them to
115+
// downstream targets
116+
if let productToAllTargets = idToProductToAllTargets[rootPkg] {
117+
for productID in productToAllTargets.keys {
118+
mergeAliases(productID: productID)
119+
}
120+
}
121+
}
122+
123+
// Traverse upstream and override aliases for the same targets if needed
124+
func propagate(package: PackageIdentity, aliasBuffer: inout [String: ModuleAliasModel]) {
125+
if let curProductToTargetAliases = aliasMap[package] {
126+
let curAliasModels = curProductToTargetAliases.map {$0.value}.filter{!$0.isEmpty}.flatMap{$0}
127+
for aliasModel in curAliasModels {
128+
// A buffer is used to track the most downstream aliases
129+
// (hence the nil check here) to allow overriding upstream
130+
// aliases for targets; if the downstream aliases are applied
131+
// to upstream targets, then they get removed
132+
if aliasBuffer[aliasModel.name] == nil {
133+
// Add a target name as a key. The buffer only tracks
134+
// a target that needs to be renamed, not the depending
135+
// targets which might have multiple target dependencies
136+
// with their aliases, so add a single alias model as value.
137+
aliasBuffer[aliasModel.name] = aliasModel
138+
}
139+
}
140+
}
141+
if let curProductToTargets = idToProductToAllTargets[package] {
142+
// Check if targets for the products in this package have
143+
// aliases tracked by the buffer
144+
let curProductToTargetsToAlias = curProductToTargets.filter { $0.value.contains { aliasBuffer[$0.name] != nil } }
145+
if !curProductToTargetsToAlias.isEmpty {
146+
var usedKeys = Set<String>()
147+
for (curProductName, targetsForCurProduct) in curProductToTargets {
148+
if let targetListToAlias = curProductToTargetsToAlias[curProductName] {
149+
for targetToAlias in targetListToAlias {
150+
if let aliasModel = aliasBuffer[targetToAlias.name] {
151+
var didAlias = false
152+
for curTarget in targetsForCurProduct {
153+
// Check if curTarget is relevant for aliasing
154+
let canAlias = curTarget.name == aliasModel.name || curTarget.dependencies.contains { $0.name == aliasModel.name }
155+
if canAlias {
156+
curTarget.addModuleAlias(for: aliasModel.name, as: aliasModel.alias)
157+
didAlias = true
158+
}
159+
}
160+
if didAlias {
161+
usedKeys.insert(targetToAlias.name)
162+
}
163+
}
164+
}
165+
}
166+
}
167+
for used in usedKeys {
168+
// Remove an entry for a used alias
169+
aliasBuffer.removeValue(forKey: used)
170+
}
171+
}
172+
}
173+
guard let children = parentToChildIDs[package] else { return }
174+
for childID in children {
175+
propagate(package: childID, aliasBuffer: &aliasBuffer)
176+
}
177+
}
178+
179+
// Merge overriden upstream aliases and add them to downstream targets
180+
func mergeAliases(productID: String) {
181+
guard let childProducts = parentToChildProducts[productID] else { return }
182+
for child in childProducts {
183+
mergeAliases(productID: child)
184+
// Filter out targets in the current product with names that are
185+
// aliased with different values in the child products since they
186+
// should either not be aliased or their existing aliases if any
187+
// should not be overridden.
188+
let allTargetNamesInCurProduct = productToAllTargets[productID]?.compactMap{$0.name} ?? []
189+
let childTargetsAliases = productToDirectTargets[child]?.compactMap{$0.moduleAliases}.flatMap{$0}.filter{ !allTargetNamesInCurProduct.contains($0.key) }
190+
191+
if let childTargetsAliases = childTargetsAliases,
192+
let directTargets = productToDirectTargets[productID] {
193+
// Keep track of all targets in this product that directly
194+
// or indirectly depend on the child product
195+
let directRelevantTargets = directTargets.filter {$0.dependsOn(productID: child)}
196+
var relevantTargets = directTargets.map{$0.dependentTargets()}.flatMap{$0}.filter {$0.dependsOn(productID: child)}
197+
relevantTargets.append(contentsOf: directTargets)
198+
relevantTargets.append(contentsOf: directRelevantTargets)
199+
let relevantTargetSet = Set(relevantTargets)
200+
201+
// Used to compare with aliases defined in other child products
202+
// and detect a conflict if any.
203+
let allTargetsInOtherChildProducts = childProducts.filter{$0 != child }.compactMap{productToAllTargets[$0]}.flatMap{$0}
204+
let allTargetNamesInChildProduct = productToAllTargets[child]?.map{$0.name} ?? []
205+
for curTarget in relevantTargetSet {
206+
for (nameToBeAliased, aliasInChild) in childTargetsAliases {
207+
// If there are targets in other child products that
208+
// have the same name that's being aliased here, but
209+
// targets in this child product don't, we need to use
210+
// alias values of those targets as they take a precedence
211+
let otherAliasesInChildProducts = allTargetsInOtherChildProducts.filter{$0.name == nameToBeAliased}.compactMap{$0.moduleAliases}.flatMap{$0}.filter{$0.key == nameToBeAliased}
212+
if !otherAliasesInChildProducts.isEmpty,
213+
!allTargetNamesInChildProduct.contains(curTarget.name) {
214+
for (aliasKey, aliasValue) in otherAliasesInChildProducts {
215+
// Reset the old alias value with this aliasValue
216+
if curTarget.moduleAliases?[aliasKey] != aliasValue {
217+
curTarget.addModuleAlias(for: aliasKey, as: aliasValue)
218+
}
219+
}
220+
} else {
221+
// If there are no aliases or conflicting aliases
222+
// for the same key defined in other child products,
223+
// those aliases should be removed from this target.
224+
let hasConflict = allTargetsInOtherChildProducts.contains{ otherTarget in
225+
if let otherAlias = otherTarget.moduleAliases?[nameToBeAliased] {
226+
return otherAlias != aliasInChild
227+
} else {
228+
return otherTarget.name == nameToBeAliased
229+
}
230+
}
231+
if hasConflict {
232+
// If there are aliases, remove as aliasing should
233+
// not be applied
234+
curTarget.removeModuleAlias(for: nameToBeAliased)
235+
} else if curTarget.moduleAliases?[nameToBeAliased] == nil {
236+
// Otherwise add the alias if none exists
237+
curTarget.addModuleAlias(for: nameToBeAliased, as: aliasInChild)
238+
}
239+
}
240+
}
241+
}
242+
}
243+
}
244+
}
245+
}
246+
247+
// Used to keep track of module alias info for each package
248+
class ModuleAliasModel {
249+
let name: String
250+
var alias: String
251+
let originPackage: PackageIdentity
252+
let consumingPackage: PackageIdentity
253+
254+
init(name: String, alias: String, originPackage: PackageIdentity, consumingPackage: PackageIdentity) {
255+
self.name = name
256+
self.alias = alias
257+
self.originPackage = originPackage
258+
self.consumingPackage = consumingPackage
259+
}
260+
}
261+
262+
extension Target {
263+
func dependsOn(productID: String) -> Bool {
264+
return dependencies.contains { dep in
265+
if case let .product(prodRef, _) = dep {
266+
return prodRef.ID == productID
267+
}
268+
return false
269+
}
270+
}
271+
272+
func dependentTargets() -> [Target] {
273+
return dependencies.compactMap{$0.target}
274+
}
275+
}
276+

0 commit comments

Comments
 (0)