Skip to content

Commit 7053c2f

Browse files
authored
Merge pull request #1404 from sepy97/ipi-clang-modules
Pass IPI Clang module names to the Swift compiler
2 parents 25153aa + 20ce517 commit 7053c2f

5 files changed

Lines changed: 781 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
@@ -616,6 +616,7 @@ public struct DiscoveredSwiftCompilerToolSpecInfo: DiscoveredCommandLineToolSpec
616616
case emitLocalizedStrings = "emit-localized-strings"
617617
case libraryLevel = "library-level"
618618
case packageName = "package-name-if-supported"
619+
case ipiClangModule = "ipi-clang-module"
619620
case vfsDirectoryRemap = "vfs-directory-remap"
620621
case indexUnitOutputPath = "index-unit-output-path"
621622
case indexUnitOutputPathWithoutWarning = "no-warn-superfluous-index-unit-path"
@@ -1623,6 +1624,22 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
16231624
args += ["-library-level", libraryLevel]
16241625
}
16251626

1627+
let ipiNames = cbc.producer.ipiClangModuleNames
1628+
if !ipiNames.isEmpty && cbc.scope.evaluate(BuiltinMacros.SWIFT_ENABLE_IPI_LIBRARY_LEVEL) {
1629+
if LibSwiftDriver.supportsDriverFlag(spelled: "-ipi-clang-module") {
1630+
// Driver knows the option; let it forward to the frontend.
1631+
for name in ipiNames {
1632+
args += ["-ipi-clang-module", name]
1633+
}
1634+
} else if toolSpecInfo.toolFeatures.has(.ipiClangModule) {
1635+
// Older driver that doesn't recognise the option, but the
1636+
// frontend supports it. Pass it through directly.
1637+
for name in ipiNames {
1638+
args += ["-Xfrontend", "-ipi-clang-module", "-Xfrontend", name]
1639+
}
1640+
}
1641+
}
1642+
16261643
if toolSpecInfo.toolFeatures.has(.packageName),
16271644
let packageName = cbc.scope.evaluate(BuiltinMacros.SWIFT_PACKAGE_NAME).nilIfEmpty {
16281645
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: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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]()

Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift

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

1266+
public var ipiClangModuleNames: [String] {
1267+
guard let configuredTarget else { return [] }
1268+
return globalProductPlan.ipiClangModuleNamesByTarget[configuredTarget] ?? []
1269+
}
1270+
12661271
public var userPreferences: UserPreferences {
12671272
workspaceContext.userPreferences
12681273
}

0 commit comments

Comments
 (0)