Skip to content

Commit f6d3d4f

Browse files
authored
Centralize dependency diagnostics in ValidateDependencies action (#675)
Instead of emitting diagnostics directly, individual tasks emit .dependencies files which are read by a per-target action which aggregates them. This avoids duplication of diagnostics across tasks. rdar://156174696
1 parent 60d63bc commit f6d3d4f

21 files changed

+297
-43
lines changed

Sources/SWBCore/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ add_library(SWBCore
161161
SpecImplementations/Tools/TiffUtilTool.swift
162162
SpecImplementations/Tools/TouchTool.swift
163163
SpecImplementations/Tools/UnifdefTool.swift
164+
SpecImplementations/Tools/ValidateDependencies.swift
164165
SpecImplementations/Tools/ValidateDevelopmentAssets.swift
165166
SpecImplementations/Tools/ValidateEmbeddedBinaryTool.swift
166167
SpecImplementations/Tools/ValidateProductTool.swift

Sources/SWBCore/Dependencies.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public struct ModuleDependency: Hashable, Sendable, SerializableCodable {
6363
}
6464

6565
public struct ModuleDependenciesContext: Sendable, SerializableCodable {
66-
var validate: BooleanWarningLevel
66+
public var validate: BooleanWarningLevel
6767
var moduleDependencies: [ModuleDependency]
6868
var fixItContext: FixItContext?
6969

@@ -235,3 +235,27 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
235235
}
236236
}
237237
}
238+
239+
public struct DependencyValidationInfo: Hashable, Sendable, Codable {
240+
public struct Import: Hashable, Sendable, Codable {
241+
public let dependency: ModuleDependency
242+
public let importLocations: [Diagnostic.Location]
243+
244+
public init(dependency: ModuleDependency, importLocations: [Diagnostic.Location]) {
245+
self.dependency = dependency
246+
self.importLocations = importLocations
247+
}
248+
}
249+
250+
public enum Payload: Hashable, Sendable, Codable {
251+
case clangDependencies(files: [String])
252+
case swiftDependencies(imports: [Import])
253+
case unsupported
254+
}
255+
256+
public let payload: Payload
257+
258+
public init(payload: Payload) {
259+
self.payload = payload
260+
}
261+
}

Sources/SWBCore/PlannedTaskAction.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ public protocol TaskActionCreationDelegate
346346
func createSignatureCollectionTaskAction() -> any PlannedTaskAction
347347
func createClangModuleVerifierInputGeneratorTaskAction() -> any PlannedTaskAction
348348
func createProcessSDKImportsTaskAction() -> any PlannedTaskAction
349+
func createValidateDependenciesTaskAction() -> any PlannedTaskAction
349350
}
350351

351352
extension TaskActionCreationDelegate {

Sources/SWBCore/SpecImplementations/RegisterSpecs.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ public struct BuiltinSpecsExtension: SpecificationsExtension {
126126
RegisterExecutionPolicyExceptionToolSpec.self,
127127
SwiftHeaderToolSpec.self,
128128
TAPIMergeToolSpec.self,
129+
ValidateDependenciesSpec.self,
129130
ValidateDevelopmentAssets.self,
130131
ConstructStubExecutorFileListToolSpec.self,
131132
GateSpec.self,

Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -434,8 +434,9 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
434434

435435
public let moduleDependenciesContext: ModuleDependenciesContext?
436436
public let traceFilePath: Path?
437+
public let dependencyValidationOutputPath: Path?
437438

438-
fileprivate init(serializedDiagnosticsPath: Path?, indexingPayload: ClangIndexingPayload?, explicitModulesPayload: ClangExplicitModulesPayload? = nil, outputObjectFilePath: Path? = nil, fileNameMapPath: Path? = nil, developerPathString: String? = nil, moduleDependenciesContext: ModuleDependenciesContext? = nil, traceFilePath: Path? = nil) {
439+
fileprivate init(serializedDiagnosticsPath: Path?, indexingPayload: ClangIndexingPayload?, explicitModulesPayload: ClangExplicitModulesPayload? = nil, outputObjectFilePath: Path? = nil, fileNameMapPath: Path? = nil, developerPathString: String? = nil, moduleDependenciesContext: ModuleDependenciesContext? = nil, traceFilePath: Path? = nil, dependencyValidationOutputPath: Path? = nil) {
439440
if let developerPathString, explicitModulesPayload == nil {
440441
self.dependencyInfoEditPayload = .init(removablePaths: [], removableBasenames: [], developerPath: Path(developerPathString))
441442
} else {
@@ -448,10 +449,11 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
448449
self.fileNameMapPath = fileNameMapPath
449450
self.moduleDependenciesContext = moduleDependenciesContext
450451
self.traceFilePath = traceFilePath
452+
self.dependencyValidationOutputPath = dependencyValidationOutputPath
451453
}
452454

453455
public func serialize<T: Serializer>(to serializer: T) {
454-
serializer.serializeAggregate(8) {
456+
serializer.serializeAggregate(9) {
455457
serializer.serialize(serializedDiagnosticsPath)
456458
serializer.serialize(indexingPayload)
457459
serializer.serialize(explicitModulesPayload)
@@ -460,11 +462,12 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
460462
serializer.serialize(dependencyInfoEditPayload)
461463
serializer.serialize(moduleDependenciesContext)
462464
serializer.serialize(traceFilePath)
465+
serializer.serialize(dependencyValidationOutputPath)
463466
}
464467
}
465468

466469
public init(from deserializer: any Deserializer) throws {
467-
try deserializer.beginAggregate(8)
470+
try deserializer.beginAggregate(9)
468471
self.serializedDiagnosticsPath = try deserializer.deserialize()
469472
self.indexingPayload = try deserializer.deserialize()
470473
self.explicitModulesPayload = try deserializer.deserialize()
@@ -473,6 +476,7 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
473476
self.dependencyInfoEditPayload = try deserializer.deserialize()
474477
self.moduleDependenciesContext = try deserializer.deserialize()
475478
self.traceFilePath = try deserializer.deserialize()
479+
self.dependencyValidationOutputPath = try deserializer.deserialize()
476480
}
477481
}
478482

@@ -1165,10 +1169,14 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
11651169
dependencyData = nil
11661170
}
11671171

1172+
let extraOutputs: [any PlannedNode]
11681173
let moduleDependenciesContext = cbc.producer.moduleDependenciesContext
1174+
let dependencyValidationOutputPath: Path?
11691175
let traceFilePath: Path?
11701176
if clangInfo?.hasFeature("print-headers-direct-per-file") ?? false,
11711177
(moduleDependenciesContext?.validate ?? .defaultValue) != .no {
1178+
dependencyValidationOutputPath = Path(outputNode.path.str + ".dependencies")
1179+
11721180
let file = Path(outputNode.path.str + ".trace.json")
11731181
commandLine += [
11741182
"-Xclang", "-header-include-file",
@@ -1177,8 +1185,12 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
11771185
"-Xclang", "-header-include-format=json"
11781186
]
11791187
traceFilePath = file
1188+
1189+
extraOutputs = [MakePlannedPathNode(dependencyValidationOutputPath!), MakePlannedPathNode(traceFilePath!)]
11801190
} else {
1191+
dependencyValidationOutputPath = nil
11811192
traceFilePath = nil
1193+
extraOutputs = []
11821194
}
11831195

11841196
// Add the diagnostics serialization flag. We currently place the diagnostics file right next to the output object file.
@@ -1293,7 +1305,8 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
12931305
fileNameMapPath: verifierPayload?.fileNameMapPath,
12941306
developerPathString: recordSystemHeaderDepsOutsideSysroot ? cbc.scope.evaluate(BuiltinMacros.DEVELOPER_DIR).str : nil,
12951307
moduleDependenciesContext: moduleDependenciesContext,
1296-
traceFilePath: traceFilePath
1308+
traceFilePath: traceFilePath,
1309+
dependencyValidationOutputPath: dependencyValidationOutputPath
12971310
)
12981311

12991312
var inputNodes: [any PlannedNode] = inputDeps.map { delegate.createNode($0) }
@@ -1357,7 +1370,7 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
13571370
}
13581371

13591372
// Finally, create the task.
1360-
delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo, additionalSignatureData: additionalSignatureData, commandLine: commandLine, additionalOutput: additionalOutput, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: inputNodes + extraInputs, outputs: [outputNode], action: action ?? delegate.taskActionCreationDelegate.createDeferredExecutionTaskActionIfRequested(userPreferences: cbc.producer.userPreferences), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilationForIndexableSourceFile], usesExecutionInputs: usesExecutionInputs, showEnvironment: true, priority: .preferred)
1373+
delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo, additionalSignatureData: additionalSignatureData, commandLine: commandLine, additionalOutput: additionalOutput, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: inputNodes + extraInputs, outputs: [outputNode] + extraOutputs, action: action ?? delegate.taskActionCreationDelegate.createDeferredExecutionTaskActionIfRequested(userPreferences: cbc.producer.userPreferences), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilationForIndexableSourceFile], usesExecutionInputs: usesExecutionInputs, showEnvironment: true, priority: .preferred)
13611374

13621375
// If the object file verifier is enabled and we are building with explicit modules, also create a job to produce adjacent objects using implicit modules, then compare the results.
13631376
if cbc.scope.evaluate(BuiltinMacros.CLANG_ENABLE_EXPLICIT_MODULES_OBJECT_FILE_VERIFIER) && action != nil {

Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,16 @@ public struct SwiftSourceFileIndexingInfo: SourceFileIndexingInfo {
273273
}
274274
}
275275

276+
public struct SwiftDependencyValidationPayload: SerializableCodable, Encodable, Sendable {
277+
public let dependencyValidationOutputPath: Path
278+
public let moduleDependenciesContext: ModuleDependenciesContext
279+
280+
public init(dependencyValidationOutputPath: Path, moduleDependenciesContext: ModuleDependenciesContext) {
281+
self.dependencyValidationOutputPath = dependencyValidationOutputPath
282+
self.moduleDependenciesContext = moduleDependenciesContext
283+
}
284+
}
285+
276286
/// The minimal data we need to serialize to reconstruct `generatePreviewInfo`
277287
public struct SwiftPreviewPayload: Serializable, Encodable, Sendable {
278288
public let architecture: String
@@ -446,9 +456,9 @@ public struct SwiftTaskPayload: ParentTaskPayload {
446456
/// The preview build style in effect (dynamic replacement or XOJIT), if any.
447457
public let previewStyle: PreviewStyleMessagePayload?
448458

449-
public let moduleDependenciesContext: ModuleDependenciesContext?
459+
public let dependencyValidationPayload: SwiftDependencyValidationPayload?
450460

451-
init(moduleName: String, indexingPayload: SwiftIndexingPayload, previewPayload: SwiftPreviewPayload?, localizationPayload: SwiftLocalizationPayload?, numExpectedCompileSubtasks: Int, driverPayload: SwiftDriverPayload?, previewStyle: PreviewStyle?, moduleDependenciesContext: ModuleDependenciesContext?) {
461+
init(moduleName: String, indexingPayload: SwiftIndexingPayload, previewPayload: SwiftPreviewPayload?, localizationPayload: SwiftLocalizationPayload?, numExpectedCompileSubtasks: Int, driverPayload: SwiftDriverPayload?, previewStyle: PreviewStyle?, dependencyValidationPayload: SwiftDependencyValidationPayload?) {
452462
self.moduleName = moduleName
453463
self.indexingPayload = indexingPayload
454464
self.previewPayload = previewPayload
@@ -463,7 +473,7 @@ public struct SwiftTaskPayload: ParentTaskPayload {
463473
case nil:
464474
self.previewStyle = nil
465475
}
466-
self.moduleDependenciesContext = moduleDependenciesContext
476+
self.dependencyValidationPayload = dependencyValidationPayload
467477
}
468478

469479
public func serialize<T: Serializer>(to serializer: T) {
@@ -475,7 +485,7 @@ public struct SwiftTaskPayload: ParentTaskPayload {
475485
serializer.serialize(numExpectedCompileSubtasks)
476486
serializer.serialize(driverPayload)
477487
serializer.serialize(previewStyle)
478-
serializer.serialize(moduleDependenciesContext)
488+
serializer.serialize(dependencyValidationPayload)
479489
}
480490
}
481491

@@ -488,7 +498,7 @@ public struct SwiftTaskPayload: ParentTaskPayload {
488498
self.numExpectedCompileSubtasks = try deserializer.deserialize()
489499
self.driverPayload = try deserializer.deserialize()
490500
self.previewStyle = try deserializer.deserialize()
491-
self.moduleDependenciesContext = try deserializer.deserialize()
501+
self.dependencyValidationPayload = try deserializer.deserialize()
492502
}
493503
}
494504

@@ -2283,6 +2293,20 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
22832293
moduleOutputPaths.append(moduleWrapOutput)
22842294
}
22852295

2296+
let dependencyValidationPayload: SwiftDependencyValidationPayload?
2297+
if let context = cbc.producer.moduleDependenciesContext, let outputPath = objectOutputPaths.first, context.validate != .no {
2298+
let primarySwiftBaseName = cbc.scope.evaluate(BuiltinMacros.TARGET_NAME) + compilationMode.moduleBaseNameSuffix + "-primary"
2299+
let dependencyValidationOutputPath = outputPath.dirname.join(primarySwiftBaseName + ".dependencies")
2300+
extraOutputPaths.append(dependencyValidationOutputPath)
2301+
2302+
dependencyValidationPayload = .init(
2303+
dependencyValidationOutputPath: dependencyValidationOutputPath,
2304+
moduleDependenciesContext: context
2305+
)
2306+
} else {
2307+
dependencyValidationPayload = nil
2308+
}
2309+
22862310
// The rule info.
22872311
//
22882312
// NOTE: If this changes, be sure to update the log parser to extract the variant and arch properly.
@@ -2312,7 +2336,7 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
23122336
numExpectedCompileSubtasks: isUsingWholeModuleOptimization ? 1 : cbc.inputs.count,
23132337
driverPayload: await driverPayload(uniqueID: String(args.hashValue), scope: cbc.scope, delegate: delegate, compilationMode: compilationMode, isUsingWholeModuleOptimization: isUsingWholeModuleOptimization, args: args, tempDirPath: objectFileDir, explicitModulesTempDirPath: Path(cbc.scope.evaluate(BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH)), variant: variant, arch: arch + compilationMode.moduleBaseNameSuffix, commandLine: ["builtin-SwiftDriver", "--"] + args, ruleInfo: ruleInfo(compilationMode.ruleNameIntegratedDriver, targetName), casOptions: casOptions, linkerResponseFilePath: moduleLinkerArgsPath),
23142338
previewStyle: cbc.scope.previewStyle,
2315-
moduleDependenciesContext: cbc.producer.moduleDependenciesContext
2339+
dependencyValidationPayload: dependencyValidationPayload
23162340
)
23172341

23182342
// Finally, assemble the input and output paths and create the Swift compiler command.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
public import SWBUtil
14+
import SWBMacro
15+
16+
public final class ValidateDependenciesSpec: CommandLineToolSpec, SpecImplementationType, @unchecked Sendable {
17+
public static let identifier = "com.apple.tools.validate-dependencies"
18+
19+
public static func construct(registry: SpecRegistry, proxy: SpecProxy) -> Spec {
20+
return ValidateDependenciesSpec(registry: registry)
21+
}
22+
23+
public init(registry: SpecRegistry) {
24+
let proxy = SpecProxy(identifier: Self.identifier, domain: "", path: Path(""), type: Self.self, classType: nil, basedOn: nil, data: ["ExecDescription": PropertyListItem("Validate dependencies")], localizedStrings: nil)
25+
super.init(createSpecParser(for: proxy, registry: registry), nil, isGeneric: false)
26+
}
27+
28+
required init(_ parser: SpecParser, _ basedOnSpec: Spec?) {
29+
super.init(parser, basedOnSpec, isGeneric: false)
30+
}
31+
32+
override public func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async {
33+
fatalError("unexpected direct invocation")
34+
}
35+
36+
public override var payloadType: (any TaskPayload.Type)? { return ValidateDependenciesPayload.self }
37+
38+
public func createTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, dependencyInfos: [PlannedPathNode], payload: ValidateDependenciesPayload) async {
39+
guard let configuredTarget = cbc.producer.configuredTarget else {
40+
return
41+
}
42+
let output = delegate.createVirtualNode("ValidateDependencies \(configuredTarget.guid)")
43+
delegate.createTask(type: self, payload: payload, ruleInfo: ["ValidateDependencies"], commandLine: ["builtin-validate-dependencies"] + dependencyInfos.map { $0.path.str }, environment: EnvironmentBindings(), workingDirectory: cbc.producer.defaultWorkingDirectory, inputs: dependencyInfos + cbc.commandOrderingInputs, outputs: [output], action: delegate.taskActionCreationDelegate.createValidateDependenciesTaskAction(), preparesForIndexing: false, enableSandboxing: false)
44+
}
45+
}
46+
47+
public struct ValidateDependenciesPayload: TaskPayload, Sendable, SerializableCodable {
48+
public let moduleDependenciesContext: ModuleDependenciesContext
49+
50+
public init(moduleDependenciesContext: ModuleDependenciesContext) {
51+
self.moduleDependenciesContext = moduleDependenciesContext
52+
}
53+
}

Sources/SWBCore/TaskGeneration.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ public protocol CommandProducer: PlatformBuildContext, SpecLookupContext, Refere
189189

190190
var processSDKImportsSpec: ProcessSDKImportsSpec { get }
191191

192+
var validateDependenciesSpec: ValidateDependenciesSpec { get }
193+
192194
/// The default working directory to use for a task, if it doesn't have a stronger preference.
193195
var defaultWorkingDirectory: Path { get }
194196

Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,7 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
758758
}
759759

760760
var tasks: [any PlannedTask] = []
761+
var dependencyDataFiles: [PlannedPathNode] = []
761762

762763
// Generate any auxiliary files whose content is not per-arch or per-variant.
763764
// For the index build arena it is important to avoid adding this because it forces creation of the Swift module due to the generated ObjC header being an input dependency. This is unnecessary work since we don't need to generate the Swift module of the target to be able to successfully create a compiler AST for the Swift files of the target.
@@ -903,6 +904,8 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
903904
case "swiftmodule":
904905
dsymutilInputNodes.append(object)
905906
break
907+
case "dependencies":
908+
dependencyDataFiles.append(MakePlannedPathNode(object.path))
906909
default:
907910
break
908911
}
@@ -1594,6 +1597,20 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
15941597
tasks = tasks.filter { $0.inputs.contains(where: { $0.path.isValidLocalizedContent(scope) || $0.path.fileExtension == "xcstrings" }) }
15951598
}
15961599

1600+
// Create a task to validate dependencies if that feature is enabled.
1601+
if let moduleDependenciesContext = context.moduleDependenciesContext, moduleDependenciesContext.validate != .no {
1602+
var validateDepsTasks = [any PlannedTask]()
1603+
await appendGeneratedTasks(&validateDepsTasks, usePhasedOrdering: true) { delegate in
1604+
await context.validateDependenciesSpec.createTasks(
1605+
CommandBuildContext(producer: context, scope: scope, inputs: []),
1606+
delegate,
1607+
dependencyInfos: dependencyDataFiles,
1608+
payload: .init(moduleDependenciesContext: moduleDependenciesContext)
1609+
)
1610+
}
1611+
tasks.append(contentsOf: validateDepsTasks)
1612+
}
1613+
15971614
return tasks
15981615
}
15991616

0 commit comments

Comments
 (0)