@@ -97,6 +97,11 @@ package final class GlobalProductPlan: GlobalTargetInfoProvider
9797 /// Maps each target to its artifactbundles (direct and transitive).
9898 package private( set) var artifactBundlesByTarget : [ ConfiguredTarget : [ ArtifactBundleInfo ] ]
9999
100+ /// For each consumer target, the names of Clang modules in its transitive deps that should
101+ /// be treated as IPI — a dep with `SKIP_INSTALL=YES` and a `MODULEMAP_FILE` resolving to a
102+ /// path under any `SRCROOT` in the closure.
103+ package private( set) var ipiClangModuleNamesByTarget : [ ConfiguredTarget : [ String ] ]
104+
100105 /// All targets in the product plan.
101106 /// - remark: This property is preferred over the `TargetBuildGraph` in the `BuildPlanRequest` as it performs additional computations for Swift packages.
102107 package private( set) var allTargets : [ ConfiguredTarget ] = [ ]
@@ -219,6 +224,7 @@ package final class GlobalProductPlan: GlobalTargetInfoProvider
219224 // Perform post-processing analysis of the build graph
220225 self . clientsOfBundlesByTarget = Self . computeBundleClients ( buildGraph: planRequest. buildGraph, buildRequestContext: planRequest. buildRequestContext)
221226 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)
227+ self . ipiClangModuleNamesByTarget = Self . computeIPIClangInfo ( buildGraph: planRequest. buildGraph, buildRequestContext: planRequest. buildRequestContext, workspaceContext: planRequest. workspaceContext)
222228 let directlyLinkedDependenciesByTarget : [ ConfiguredTarget : OrderedSet < LinkedDependency > ]
223229 ( self . impartedBuildPropertiesByTarget, directlyLinkedDependenciesByTarget) = await Self . computeImpartedBuildProperties ( planRequest: planRequest, getLinkageGraph: getLinkageGraph, delegate: delegate)
224230 self . mergeableTargetsToMergingTargets = Self . computeMergeableLibraries ( buildGraph: planRequest. buildGraph, provisioningInputs: planRequest. provisioningInputs, buildRequestContext: planRequest. buildRequestContext)
@@ -586,6 +592,74 @@ package final class GlobalProductPlan: GlobalTargetInfoProvider
586592 return ( impartedBuildPropertiesByTarget, directlyLinkedDependenciesByTarget)
587593 }
588594
595+ /// Compute, per consumer target, the Clang module names to treat as IPI.
596+ /// A target-produced Clang module qualifies when the target has `SKIP_INSTALL=YES` and a
597+ /// `MODULEMAP_FILE` whose resolved path lies under some `SRCROOT` in the consumer's
598+ /// transitive dependency closure (including the consumer's own `SRCROOT`).
599+ private static func computeIPIClangInfo( buildGraph: TargetBuildGraph , buildRequestContext: BuildRequestContext , workspaceContext: WorkspaceContext ) -> [ ConfiguredTarget : [ String ] ] {
600+ var namesByTarget : [ ConfiguredTarget : [ String ] ] = [ : ]
601+ for configuredTarget in buildGraph. allTargets {
602+ let deps = transitiveClosure ( [ configuredTarget] , successors: buildGraph. dependencies ( of: ) ) . 0
603+ let allTargets = [ configuredTarget] + deps
604+ // Collect the set of project-internal roots for this consumer.
605+ var srcroots : Set < Path > = [ ]
606+ for target in allTargets {
607+ let srcroot = buildRequestContext
608+ . getCachedSettings ( target. parameters, target: target. target)
609+ . globalScope. evaluate ( BuiltinMacros . SRCROOT)
610+ if !srcroot. isEmpty {
611+ srcroots. insert ( srcroot)
612+ }
613+ }
614+ var names : Set < String > = [ ]
615+ for target in allTargets {
616+ let settings = buildRequestContext. getCachedSettings ( target. parameters, target: target. target)
617+ let scope = settings. globalScope
618+ // Must produce a Clang module.
619+ let definesModule = scope. evaluate ( BuiltinMacros . DEFINES_MODULE)
620+ let modulemapFile = scope. evaluate ( BuiltinMacros . MODULEMAP_FILE)
621+ let modulemapContents = scope. evaluate ( BuiltinMacros . MODULEMAP_FILE_CONTENTS)
622+ guard definesModule || !modulemapFile. isEmpty || !modulemapContents. isEmpty else { continue }
623+ // IPI if: SKIP_INSTALL=YES (target is never installed) OR the explicit MODULEMAP_FILE
624+ // resolves to a path under a project-internal SRCROOT (module lives in the source tree).
625+ let skipInstall = scope. evaluate ( BuiltinMacros . SKIP_INSTALL)
626+ let modulemapUnderSRCROOT : Bool = {
627+ guard !modulemapFile. isEmpty else { return false }
628+ let modulemapPath = Path ( modulemapFile) . isAbsolute
629+ ? Path ( modulemapFile)
630+ : scope. evaluate ( BuiltinMacros . SRCROOT) . join ( modulemapFile)
631+ // Resolve symlinks via the parent directory — the modulemap file itself may not
632+ // exist yet during planning, but its parent directory should.
633+ let resolvedModulemapDir = ( try ? localFS. realpath ( modulemapPath. dirname) ) ?? modulemapPath. dirname
634+ return srcroots. contains ( where: {
635+ ( ( try ? localFS. realpath ( $0) ) ?? $0) . isAncestorOrEqual ( of: resolvedModulemapDir)
636+ } )
637+ } ( )
638+ guard skipInstall || modulemapUnderSRCROOT else { continue }
639+ // Prefer the centralized Clang module name(s) from `ModuleInfo`, which is the single
640+ // source of truth and will inherit any future modulemap parsing.
641+ let moduleInfo = computeModuleInfo ( workspaceContext: workspaceContext, target: target. target, settings: settings, diagnosticHandler: { _, _, _, _ in } )
642+ let knownNames = moduleInfo? . knownClangModuleNames ?? [ ]
643+ if !knownNames. isEmpty {
644+ names. formUnion ( knownNames)
645+ } else {
646+ // Fall back to $(PRODUCT_MODULE_NAME) when the module name isn't known.
647+ // The case for hand-authored modulemaps, which `computeModuleInfo` doesn't yet parse.
648+ // NOTE: this can be wrong — a hand-authored MODULEMAP_FILE / MODULEMAP_FILE_CONTENTS
649+ // may declare a differently-named module (or several), so the emitted name might
650+ // not match the actual module.
651+ // Once `computeModuleInfo` parses modulemaps this fallback (and the inaccuracy) goes away.
652+ let name = scope. evaluate ( BuiltinMacros . PRODUCT_MODULE_NAME)
653+ if !name. isEmpty {
654+ names. insert ( name)
655+ }
656+ }
657+ }
658+ namesByTarget [ configuredTarget] = names. sorted ( )
659+ }
660+ return namesByTarget
661+ }
662+
589663 /// Determine which target produced each product in the build.
590664 private static func computeProducingTargetsForProducts( buildGraph: TargetBuildGraph , provisioningInputs: [ ConfiguredTarget : ProvisioningTaskInputs ] , buildRequestContext: BuildRequestContext ) -> [ Path : ConfiguredTarget ] {
591665 var productPathsToProducingTargets = [ Path: ConfiguredTarget] ( )
0 commit comments