@@ -100,6 +100,11 @@ package final class GlobalProductPlan: GlobalTargetInfoProvider
100100 /// Maps each target to its artifactbundles (direct and transitive).
101101 package private( set) var artifactBundlesByTarget : [ ConfiguredTarget : [ ArtifactBundleInfo ] ]
102102
103+ /// For each consumer target, the names of Clang modules in its transitive deps that should
104+ /// be treated as IPI — a dep with `SKIP_INSTALL=YES` and a `MODULEMAP_FILE` resolving to a
105+ /// path under any `SRCROOT` in the closure.
106+ package private( set) var ipiClangModuleNamesByTarget : [ ConfiguredTarget : [ String ] ]
107+
103108 /// All targets in the product plan.
104109 /// - remark: This property is preferred over the `TargetBuildGraph` in the `BuildPlanRequest` as it performs additional computations for Swift packages.
105110 package private( set) var allTargets : [ ConfiguredTarget ] = [ ]
@@ -241,6 +246,7 @@ package final class GlobalProductPlan: GlobalTargetInfoProvider
241246 // Perform post-processing analysis of the build graph
242247 self . clientsOfBundlesByTarget = Self . computeBundleClients ( buildGraph: planRequest. buildGraph, buildRequestContext: planRequest. buildRequestContext)
243248 self . artifactBundlesByTarget = await Self . computeArtifactBundleInfo ( buildGraph: planRequest. buildGraph, provisioningInputs: planRequest. provisioningInputs, buildRequest: planRequest. buildRequest, buildRequestContext: planRequest. buildRequestContext, workspaceContext: planRequest. workspaceContext, getLinkageGraph: getLinkageGraph, metadataCache: self . artifactBundleMetadataCache, delegate: delegate)
249+ self . ipiClangModuleNamesByTarget = Self . computeIPIClangInfo ( buildGraph: planRequest. buildGraph, buildRequestContext: planRequest. buildRequestContext, workspaceContext: planRequest. workspaceContext)
244250 let directlyLinkedDependenciesByTarget : [ ConfiguredTarget : OrderedSet < LinkedDependency > ]
245251 ( self . impartedBuildPropertiesByTarget, directlyLinkedDependenciesByTarget) = await Self . computeImpartedBuildProperties ( planRequest: planRequest, getLinkageGraph: getLinkageGraph, delegate: delegate)
246252 self . mergeableTargetsToMergingTargets = Self . computeMergeableLibraries ( buildGraph: planRequest. buildGraph, provisioningInputs: planRequest. provisioningInputs, buildRequestContext: planRequest. buildRequestContext)
@@ -606,6 +612,128 @@ package final class GlobalProductPlan: GlobalTargetInfoProvider
606612 return ( impartedBuildPropertiesByTarget, directlyLinkedDependenciesByTarget)
607613 }
608614
615+ /// Compute, per consumer target, the Clang module names to treat as IPI.
616+ /// A target-produced Clang module qualifies when the target has `SKIP_INSTALL=YES` and a
617+ /// `MODULEMAP_FILE` whose resolved path lies under some `SRCROOT` in the consumer's
618+ /// transitive dependency closure (including the consumer's own `SRCROOT`).
619+ private static func computeIPIClangInfo( buildGraph: TargetBuildGraph , buildRequestContext: BuildRequestContext , workspaceContext: WorkspaceContext ) -> [ ConfiguredTarget : [ String ] ] {
620+ // Target-local info, computed once per target. None of these depend on which
621+ // consumer pulls the target into its closure, no need to be recomputed per consumer
622+ struct TargetIPIInfo {
623+ let resolvedSrcroot : Path ? // realpath-resolved SRCROOT, for building consumer srcroot sets
624+ let producesClangModule : Bool
625+ let skipInstall : Bool
626+ let resolvedModulemapDir : Path ? // realpath-resolved dir of MODULEMAP_FILE, nil if none
627+ let moduleNames : [ String ] // populated only for targets that could qualify
628+ }
629+ // Precomputing the expensive per-target info (computeModuleInfo, realpath, macro evaluations)
630+ var ipiInfoByTarget : [ ConfiguredTarget : TargetIPIInfo ] = [ : ]
631+ for target in buildGraph. allTargets {
632+ let settings = buildRequestContext. getCachedSettings ( target. parameters, target: target. target)
633+ let scope = settings. globalScope
634+ let srcroot = scope. evaluate ( BuiltinMacros . SRCROOT)
635+ let resolvedSrcroot : Path ? = srcroot. isEmpty ? nil : ( ( try ? localFS. realpath ( srcroot) ) ?? srcroot)
636+ // Must produce a Clang module
637+ let definesModule = scope. evaluate ( BuiltinMacros . DEFINES_MODULE)
638+ let modulemapFile = scope. evaluate ( BuiltinMacros . MODULEMAP_FILE)
639+ let modulemapContents = scope. evaluate ( BuiltinMacros . MODULEMAP_FILE_CONTENTS)
640+ let producesClangModule = definesModule || !modulemapFile. isEmpty || !modulemapContents. isEmpty
641+ let skipInstall = scope. evaluate ( BuiltinMacros . SKIP_INSTALL)
642+ let resolvedModulemapDir : Path ? = {
643+ guard !modulemapFile. isEmpty else { return nil }
644+ let modulemapPath = Path ( modulemapFile) . isAbsolute
645+ ? Path ( modulemapFile)
646+ : srcroot. join ( modulemapFile)
647+ // Resolve symlinks via the parent directory — the modulemap file itself may not
648+ // exist yet during planning, but its parent directory should.
649+ return ( try ? localFS. realpath ( modulemapPath. dirname) ) ?? modulemapPath. dirname
650+ } ( )
651+ // Pre-filtering, run name computation for targets that could qualify as IPI.
652+ var moduleNames : [ String ] = [ ]
653+ if producesClangModule && ( skipInstall || resolvedModulemapDir != nil ) {
654+ // Prefer the centralized Clang module name(s) from `ModuleInfo`, which is the single
655+ // source of truth and will inherit any future modulemap parsing.
656+ let moduleInfo = computeModuleInfo ( workspaceContext: workspaceContext, target: target. target, settings: settings, diagnosticHandler: { _, _, _, _ in } )
657+ let knownNames = moduleInfo? . knownClangModuleNames ?? [ ]
658+ if !knownNames. isEmpty {
659+ moduleNames = knownNames
660+ } else {
661+ // Fall back to $(PRODUCT_MODULE_NAME) when the module name isn't known.
662+ // The case for hand-authored modulemaps, which `computeModuleInfo` doesn't yet parse.
663+ // NOTE: this can be wrong — a hand-authored MODULEMAP_FILE / MODULEMAP_FILE_CONTENTS
664+ // may declare a differently-named module (or several), so the emitted name might
665+ // not match the actual module.
666+ // Once `computeModuleInfo` parses modulemaps this fallback (and the inaccuracy) goes away.
667+ let name = scope. evaluate ( BuiltinMacros . PRODUCT_MODULE_NAME)
668+ if !name. isEmpty {
669+ moduleNames = [ name]
670+ }
671+ }
672+ }
673+ ipiInfoByTarget [ target] = TargetIPIInfo (
674+ resolvedSrcroot: resolvedSrcroot,
675+ producesClangModule: producesClangModule,
676+ skipInstall: skipInstall,
677+ resolvedModulemapDir: resolvedModulemapDir,
678+ moduleNames: moduleNames)
679+ }
680+ // Aggregate each target's closure by folding over `buildGraph.allTargets`, which the
681+ // resolver provides in dependency-first topological order: by the time we reach a target,
682+ // each of its dependencies already holds its full aggregate, so we just union those in.
683+ var srcrootsByTarget : [ ConfiguredTarget : Set < Path > ] = [ : ]
684+ var skipNamesByTarget : [ ConfiguredTarget : Set < String > ] = [ : ]
685+ var candidatesByTarget : [ ConfiguredTarget : [ Path : Set < String > ] ] = [ : ]
686+ for target in buildGraph. allTargets {
687+ guard let info = ipiInfoByTarget [ target] else { continue }
688+ // The target's own contribution.
689+ var srcroots : Set < Path > = [ ]
690+ if let root = info. resolvedSrcroot {
691+ srcroots. insert ( root)
692+ }
693+ var skipNames : Set < String > = [ ]
694+ var candidates : [ Path : Set < String > ] = [ : ]
695+ if info. producesClangModule {
696+ if info. skipInstall {
697+ skipNames. formUnion ( info. moduleNames)
698+ }
699+ if let dir = info. resolvedModulemapDir {
700+ candidates [ dir, default: [ ] ] . formUnion ( info. moduleNames)
701+ }
702+ }
703+ // Fold in each dependency's already-computed aggregate.
704+ for dependency in buildGraph. dependencies ( of: target) {
705+ if let depSrcroots = srcrootsByTarget [ dependency] {
706+ srcroots. formUnion ( depSrcroots)
707+ }
708+ if let depSkipNames = skipNamesByTarget [ dependency] {
709+ skipNames. formUnion ( depSkipNames)
710+ }
711+ if let depCandidates = candidatesByTarget [ dependency] {
712+ for (dir, names) in depCandidates {
713+ candidates [ dir, default: [ ] ] . formUnion ( names)
714+ }
715+ }
716+ }
717+ srcrootsByTarget [ target] = srcroots
718+ skipNamesByTarget [ target] = skipNames
719+ candidatesByTarget [ target] = candidates
720+ }
721+
722+ // Apply the consumer-specific modulemap qualification against each consumer's folded srcroots.
723+ var namesByTarget : [ ConfiguredTarget : [ String ] ] = [ : ]
724+ for configuredTarget in buildGraph. allTargets {
725+ let srcroots = srcrootsByTarget [ configuredTarget] ?? [ ]
726+ var names = skipNamesByTarget [ configuredTarget] ?? [ ]
727+ for (dir, candidateNames) in candidatesByTarget [ configuredTarget] ?? [ : ] {
728+ if srcroots. contains ( where: { $0. isAncestorOrEqual ( of: dir) } ) {
729+ names. formUnion ( candidateNames)
730+ }
731+ }
732+ namesByTarget [ configuredTarget] = names. sorted ( )
733+ }
734+ return namesByTarget
735+ }
736+
609737 /// Determine which target produced each product in the build.
610738 private static func computeProducingTargetsForProducts( buildGraph: TargetBuildGraph , provisioningInputs: [ ConfiguredTarget : ProvisioningTaskInputs ] , buildRequestContext: BuildRequestContext ) -> [ Path : ConfiguredTarget ] {
611739 var productPathsToProducingTargets = [ Path: ConfiguredTarget] ( )
0 commit comments