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