Skip to content

Commit cbe748d

Browse files
authored
Module aliasing: allow multiple root packages (#4199)
Module aliasing: allow multiple root packages Add module alias tracker API Resolves rdar://88518683
1 parent e3432e9 commit cbe748d

File tree

3 files changed

+616
-90
lines changed

3 files changed

+616
-90
lines changed

Sources/PackageGraph/PackageGraph+Loading.swift

Lines changed: 163 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -197,24 +197,24 @@ private func checkAllDependenciesAreUsed(_ rootPackages: [ResolvedPackage], obse
197197

198198
extension Package {
199199
// Add module aliases specified for applicable targets
200-
fileprivate func setModuleAliasesForTargets(with moduleAliasMap: [String: String]) {
200+
fileprivate func setModuleAliasesForTargets(with moduleAliasMap: [String: [ModuleAliasModel]]) {
201201
// Set module aliases for each target's dependencies
202-
for (entryName, entryAlias) in moduleAliasMap {
203-
for target in self.targets {
204-
// First add dependency module aliases for this target
205-
if entryName != target.name {
206-
target.addModuleAlias(for: entryName, as: entryAlias)
202+
for target in self.targets {
203+
let aliasesForTarget = moduleAliasMap.filter {$0.key == target.name}.values.flatMap{$0}
204+
for entry in aliasesForTarget {
205+
if entry.name != target.name {
206+
target.addModuleAlias(for: entry.name, as: entry.alias)
207207
}
208208
}
209209
}
210210

211211
// This loop should run after the loop above as it may rename the target
212212
// as an alias if specified
213-
for (entryName, entryAlias) in moduleAliasMap {
214-
for target in self.targets {
215-
// Then set this target to be aliased if specified
216-
if entryName == target.name {
217-
target.addModuleAlias(for: target.name, as: entryAlias)
213+
for target in self.targets {
214+
let aliasesForTarget = moduleAliasMap.filter {$0.key == target.name}.values.flatMap{$0}
215+
for entry in aliasesForTarget {
216+
if entry.name == target.name {
217+
target.addModuleAlias(for: entry.name, as: entry.alias)
218218
}
219219
}
220220
}
@@ -264,8 +264,9 @@ private func createResolvedPackages(
264264
return ($0.package.identity, $0)
265265
}
266266

267-
// Gather all module aliases specified for targets in all dependent packages
268-
let packageAliases = gatherModuleAliases(from: packageBuilders, for: rootManifests.first?.key, with: packagesByIdentity, onError: observabilityScope)
267+
// Gather and resolve module aliases specified for targets in all dependent packages
268+
let packageAliases = resolveModuleAliases(with: packageBuilders,
269+
onError: observabilityScope)
269270

270271
// Scan and validate the dependencies
271272
for packageBuilder in packageBuilders {
@@ -522,11 +523,10 @@ private func createResolvedPackages(
522523
return try packageBuilders.map{ try $0.construct() }
523524
}
524525

525-
// Create a map between a package and module aliases specified for the targets in the package.
526-
private func gatherModuleAliases(from packageBuilders: [ResolvedPackageBuilder],
527-
for rootPkgID: PackageIdentity?,
528-
with packagesByIdentity: [PackageIdentity: ResolvedPackageBuilder],
529-
onError observabilityScope: ObservabilityScope) -> [PackageIdentity: [String: String]]? {
526+
// Track and override module aliases specified for targets in a package graph
527+
private func resolveModuleAliases(with packageBuilders: [ResolvedPackageBuilder],
528+
onError observabilityScope: ObservabilityScope) -> [PackageIdentity: [String: [ModuleAliasModel]]]? {
529+
530530
// If there are no aliases, return early
531531
let depsWithAliases = packageBuilders.map { $0.package.targets.map { $0.dependencies.filter { dep in
532532
if case let .product(prodRef, _) = dep {
@@ -536,79 +536,165 @@ private func gatherModuleAliases(from packageBuilders: [ResolvedPackageBuilder],
536536
}}}.flatMap{$0}.flatMap{$0}
537537

538538
guard !depsWithAliases.isEmpty else { return nil }
539-
540-
var result = [PackageIdentity: [String: String]]()
541539

542-
// There could be multiple root packages but the common cases involve
543-
// just one root package; handling multiple roots is tracked rdar://88518683
544-
if let rootPkg = rootPkgID {
545-
var pkgStack = [PackageIdentity]()
546-
walkPkgTreeAndGetModuleAliases(for: rootPkg, with: packagesByIdentity, onError: observabilityScope, using: &pkgStack, output: &result)
540+
let aliasTracker = ModuleAliasTracker()
541+
for packageBuilder in packageBuilders {
542+
for target in packageBuilder.package.targets {
543+
for dep in target.dependencies {
544+
if case let .product(prodRef, _) = dep,
545+
let prodPkg = prodRef.package {
546+
let prodPkgID = PackageIdentity.plain(prodPkg)
547+
// Track package ID dependency chain
548+
aliasTracker.addPackageIDChain(parent: packageBuilder.package.identity, child: prodPkgID)
549+
550+
if let aliasList = prodRef.moduleAliases {
551+
for (depName, depAlias) in aliasList {
552+
if let existingAlias = aliasTracker.alias(of: depName, in: prodPkgID) {
553+
// Error if there are multiple aliases specified for this product dependency
554+
observabilityScope.emit(PackageGraphError.multipleModuleAliases(target: depName, product: prodRef.name, package: prodPkg, aliases: [existingAlias, depAlias]))
555+
return nil
556+
}
557+
// Track aliases for this product
558+
aliasTracker.addAlias(depAlias,
559+
of: depName,
560+
for: prodRef.name,
561+
from: PackageIdentity.plain(prodPkg),
562+
in: packageBuilder.package.identity)
563+
}
564+
}
565+
}
566+
}
567+
}
547568
}
548-
return result
569+
570+
// Track targets that need module aliases for each package
571+
for packageBuilder in packageBuilders {
572+
for prod in packageBuilder.package.products {
573+
var list = prod.targets.map{$0.dependencies}.flatMap{$0}.compactMap{$0.target?.name}
574+
list.append(contentsOf: prod.targets.map{$0.name})
575+
aliasTracker.addAliasesForTargets(list, for: prod.name, in: packageBuilder.package.identity)
576+
}
577+
}
578+
579+
// Override module aliases upstream if needed
580+
aliasTracker.propagateAliases()
581+
582+
return aliasTracker.idTargetToAliases
549583
}
550584

551-
// Walk a package dependency tree and set the module aliases for targets in each package.
552-
// If multiple aliases are specified in upstream packages, aliases specified most downstream
553-
// will be used.
554-
private func walkPkgTreeAndGetModuleAliases(for pkgID: PackageIdentity,
555-
with packagesByIdentity: [PackageIdentity: ResolvedPackageBuilder],
556-
onError observabilityScope: ObservabilityScope,
557-
using pkgStack: inout [PackageIdentity],
558-
output result: inout [PackageIdentity: [String: String]]) {
559-
// Get the builder first
560-
if let builder = packagesByIdentity[pkgID] {
561-
builder.package.targets.forEach { target in
562-
target.dependencies.forEach { dep in
563-
// Check if a dependency for this target has module aliases specified
564-
if case let .product(prodRef, _) = dep {
565-
if let prodPkg = prodRef.package {
566-
if let prodModuleAliases = prodRef.moduleAliases {
567-
for (depName, depAlias) in prodModuleAliases {
568-
let prodPkgID = PackageIdentity.plain(prodPkg)
569-
if let existingAlias = result[prodPkgID, default: [:]][depName] {
570-
// error if there are multiple aliases for
571-
// a dependency target for a product
572-
observabilityScope.emit(PackageGraphError.multipleModuleAliases(target: depName, product: prodRef.name, package: prodPkg, aliases: [existingAlias, depAlias]))
573-
return
574-
}
585+
// This class helps track module aliases in a package graph and override
586+
// upstream alises if needed
587+
private class ModuleAliasTracker {
588+
var aliasMap = [PackageIdentity: [String: [ModuleAliasModel]]]()
589+
var idTargetToAliases = [PackageIdentity: [String: [ModuleAliasModel]]]()
590+
var parentToChildIDs = [PackageIdentity: [PackageIdentity]]()
591+
var childToParentID = [PackageIdentity: PackageIdentity]()
592+
593+
init() {}
594+
595+
func addAlias(_ alias: String,
596+
of targetName: String,
597+
for product: String,
598+
from originPackage: PackageIdentity,
599+
in parentPackage: PackageIdentity) {
600+
let model = ModuleAliasModel(name: targetName, alias: alias, originPackage: originPackage, parentPackage: parentPackage)
601+
aliasMap[originPackage, default: [:]][product, default: []].append(model)
602+
}
575603

576-
// Add the specified alias and the dependency package to a map
577-
result[prodPkgID, default: [:]][depName] = depAlias
604+
func addPackageIDChain(parent: PackageIdentity,
605+
child: PackageIdentity) {
606+
if parentToChildIDs[parent]?.contains(child) ?? false {
607+
// Already added
608+
} else {
609+
parentToChildIDs[parent, default: []].append(child)
610+
// Used to track the top-most level package
611+
childToParentID[child] = parent
612+
}
613+
}
614+
615+
func addAliasesForTargets(_ targets: [String],
616+
for product: String,
617+
in package: PackageIdentity) {
618+
619+
let aliases = aliasMap[package]?[product]
620+
for targetName in targets {
621+
if idTargetToAliases[package]?[targetName] == nil {
622+
idTargetToAliases[package, default: [:]][targetName] = []
623+
}
624+
625+
if let aliases = aliases {
626+
idTargetToAliases[package]?[targetName]?.append(contentsOf: aliases)
627+
}
628+
}
629+
}
630+
631+
func alias(of targetName: String,
632+
in originPackage: PackageIdentity) -> String? {
633+
if let aliasDict = aliasMap[originPackage] {
634+
let models = aliasDict.values.flatMap{$0}.filter { $0.name == targetName }
635+
// this func only checks if there's any existing alias so
636+
// just return the first alias value
637+
return models.first?.alias
638+
}
639+
return nil
640+
}
641+
642+
func propagateAliases() {
643+
// First get the root package ID
644+
var pkgID = childToParentID.first?.key
645+
var rootPkg = pkgID
646+
while pkgID != nil {
647+
rootPkg = pkgID
648+
// pkgID is not nil here so can be force unwrapped
649+
pkgID = childToParentID[pkgID!]
650+
}
651+
652+
guard let rootPkg = rootPkg else { return }
653+
propagate(from: rootPkg)
654+
}
655+
656+
func propagate(from cur: PackageIdentity) {
657+
guard let children = parentToChildIDs[cur] else { return }
658+
for child in children {
659+
if let parentMap = idTargetToAliases[cur],
660+
let childMap = idTargetToAliases[child] {
661+
for (parentTarget, parentAliases) in parentMap {
662+
for parentModel in parentAliases {
663+
for (childTarget, childAliases) in childMap {
664+
if !parentMap.keys.contains(childTarget),
665+
childTarget == parentModel.name {
666+
if childAliases.isEmpty {
667+
idTargetToAliases[child]?[childTarget]?.append(parentModel)
668+
} else {
669+
for childModel in childAliases {
670+
childModel.alias = parentModel.alias
671+
}
672+
}
578673
}
579674
}
580675
}
581676
}
582677
}
583-
584-
// If multiple aliases are specified in the package chain,
585-
// use the ones specified most downstream to override
586-
// upstream targets
587-
for pkgInChain in pkgStack {
588-
if let entry = result[pkgInChain],
589-
let aliasToOverride = entry[target.name] {
590-
result[pkgID, default: [:]][target.name] = aliasToOverride
591-
break
592-
}
593-
}
594-
}
595-
// Add pkgID to a stack used to keep track of multiple
596-
// aliases specified in the package chain. Need to add
597-
// pkgID here, otherwise need pkgID != pkgInChain check
598-
// in the for loop above
599-
pkgStack.append(pkgID)
600-
601-
// Recursively (depth-first) walk the package dependency tree
602-
for pkgDep in builder.package.manifest.dependencies {
603-
walkPkgTreeAndGetModuleAliases(for: pkgDep.identity, with: packagesByIdentity, onError: observabilityScope, using: &pkgStack, output: &result)
604-
// Last added package has been looked up, so pop here
605-
if !pkgStack.isEmpty {
606-
pkgStack.removeLast()
607-
}
678+
propagate(from: child)
608679
}
609680
}
610681
}
611682

683+
// Used to keep track of module alias info for each package
684+
private class ModuleAliasModel {
685+
let name: String
686+
var alias: String
687+
let originPackage: PackageIdentity
688+
let parentPackage: PackageIdentity
689+
690+
init(name: String, alias: String, originPackage: PackageIdentity, parentPackage: PackageIdentity) {
691+
self.name = name
692+
self.alias = alias
693+
self.originPackage = originPackage
694+
self.parentPackage = parentPackage
695+
}
696+
}
697+
612698
/// A generic builder for `Resolved` models.
613699
private class ResolvedBuilder<T> {
614700
/// The constructed object, available after the first call to `construct()`.

Sources/PackageModel/Target.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ public class Target: PolymorphicCodableProtocol {
143143
self.c99name = alias.spm_mangledToC99ExtendedIdentifier()
144144
}
145145
}
146-
147146
/// The default localization for resources.
148147
public let defaultLocalization: String?
149148

0 commit comments

Comments
 (0)