Skip to content

Commit 5d9d48d

Browse files
committed
Pass IPI Clang module names to the Swift compiler
When a Swift target is compiled, swift-build now computes which Clang modules in its transitive dependency closure should be treated as IPI (project-internal) and passes their names via compiler flags. The SKIP_INSTALL condition covers target-produced modules including auto-generated modulemaps. The SRCROOT condition covers explicit modulemaps committed to a project's source tree regardless of SKIP_INSTALL. rdar://177010683
1 parent f1301bc commit 5d9d48d

5 files changed

Lines changed: 727 additions & 0 deletions

File tree

Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,7 @@ public struct DiscoveredSwiftCompilerToolSpecInfo: DiscoveredCommandLineToolSpec
612612
case emitLocalizedStrings = "emit-localized-strings"
613613
case libraryLevel = "library-level"
614614
case packageName = "package-name-if-supported"
615+
case ipiClangModule = "ipi-clang-module"
615616
case vfsDirectoryRemap = "vfs-directory-remap"
616617
case indexUnitOutputPath = "index-unit-output-path"
617618
case indexUnitOutputPathWithoutWarning = "no-warn-superfluous-index-unit-path"
@@ -1606,6 +1607,22 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
16061607
args += ["-library-level", libraryLevel]
16071608
}
16081609

1610+
let ipiNames = cbc.producer.ipiClangModuleNames
1611+
if !ipiNames.isEmpty && cbc.scope.evaluate(BuiltinMacros.SWIFT_ENABLE_IPI_LIBRARY_LEVEL) {
1612+
if LibSwiftDriver.supportsDriverFlag(spelled: "-ipi-clang-module") {
1613+
// Driver knows the option; let it forward to the frontend.
1614+
for name in ipiNames {
1615+
args += ["-ipi-clang-module", name]
1616+
}
1617+
} else if toolSpecInfo.toolFeatures.has(.ipiClangModule) {
1618+
// Older driver that doesn't recognise the option, but the
1619+
// frontend supports it. Pass it through directly.
1620+
for name in ipiNames {
1621+
args += ["-Xfrontend", "-ipi-clang-module", "-Xfrontend", name]
1622+
}
1623+
}
1624+
}
1625+
16091626
if toolSpecInfo.toolFeatures.has(.packageName),
16101627
let packageName = cbc.scope.evaluate(BuiltinMacros.SWIFT_PACKAGE_NAME).nilIfEmpty {
16111628
args += ["-package-name", packageName]

Sources/SWBCore/TaskGeneration.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ public protocol CommandProducer: PlatformBuildContext, SpecLookupContext, Refere
210210
/// The module info for the target, if available.
211211
var moduleInfo: ModuleInfo? { get }
212212

213+
/// The names of Clang modules in this target's transitive dependency closure that should
214+
/// be treated as IPI (project-internal), and thus passed to the Swift compiler via
215+
/// `-ipi-clang-module`.
216+
var ipiClangModuleNames: [String] { get }
217+
213218
/// Whether or not the build is using a VFS.
214219
var needsVFS: Bool { get }
215220

@@ -286,6 +291,11 @@ extension CommandProducer {
286291
sdkVariant?.llvmTargetTripleVendor == "apple"
287292
}
288293

294+
/// By default, no Clang modules are treated as IPI.
295+
public var ipiClangModuleNames: [String] {
296+
[]
297+
}
298+
289299
package func discoveredCommandLineToolSpecInfo<T: DiscoveredCommandLineToolSpecInfo>(_ delegate: any CoreClientTargetDiagnosticProducingDelegate, _ toolName: String, _ path: Path, _ process: @Sendable (_ contents: Data) async throws -> any DiscoveredCommandLineToolSpecInfo) async throws -> T {
290300
let info = try await discoveredCommandLineToolSpecInfo(delegate, toolName, path, process)
291301
guard let info = info as? T else {

Sources/SWBTaskConstruction/ProductPlanning/ProductPlan.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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]()

Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,6 +1256,11 @@ extension TaskProducerContext: CommandProducer {
12561256
return globalProductPlan.getModuleInfo(configuredTarget!)
12571257
}
12581258

1259+
public var ipiClangModuleNames: [String] {
1260+
guard let configuredTarget else { return [] }
1261+
return globalProductPlan.ipiClangModuleNamesByTarget[configuredTarget] ?? []
1262+
}
1263+
12591264
public var userPreferences: UserPreferences {
12601265
workspaceContext.userPreferences
12611266
}

0 commit comments

Comments
 (0)