Skip to content

Centralize dependency diagnostics in ValidateDependencies action #675

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/SWBCore/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ add_library(SWBCore
SpecImplementations/Tools/TiffUtilTool.swift
SpecImplementations/Tools/TouchTool.swift
SpecImplementations/Tools/UnifdefTool.swift
SpecImplementations/Tools/ValidateDependencies.swift
SpecImplementations/Tools/ValidateDevelopmentAssets.swift
SpecImplementations/Tools/ValidateEmbeddedBinaryTool.swift
SpecImplementations/Tools/ValidateProductTool.swift
Expand Down
26 changes: 25 additions & 1 deletion Sources/SWBCore/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public struct ModuleDependency: Hashable, Sendable, SerializableCodable {
}

public struct ModuleDependenciesContext: Sendable, SerializableCodable {
var validate: BooleanWarningLevel
public var validate: BooleanWarningLevel
var moduleDependencies: [ModuleDependency]
var fixItContext: FixItContext?

Expand Down Expand Up @@ -235,3 +235,27 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
}
}
}

public struct DependencyValidationInfo: Hashable, Sendable, Codable {
public struct Import: Hashable, Sendable, Codable {
public let dependency: ModuleDependency
public let importLocations: [Diagnostic.Location]

public init(dependency: ModuleDependency, importLocations: [Diagnostic.Location]) {
self.dependency = dependency
self.importLocations = importLocations
}
}

public enum Payload: Hashable, Sendable, Codable {
case clangDependencies(files: [String])
case swiftDependencies(imports: [Import])
case unsupported
}

public let payload: Payload

public init(payload: Payload) {
self.payload = payload
}
}
1 change: 1 addition & 0 deletions Sources/SWBCore/PlannedTaskAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ public protocol TaskActionCreationDelegate
func createSignatureCollectionTaskAction() -> any PlannedTaskAction
func createClangModuleVerifierInputGeneratorTaskAction() -> any PlannedTaskAction
func createProcessSDKImportsTaskAction() -> any PlannedTaskAction
func createValidateDependenciesTaskAction() -> any PlannedTaskAction
}

extension TaskActionCreationDelegate {
Expand Down
1 change: 1 addition & 0 deletions Sources/SWBCore/SpecImplementations/RegisterSpecs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public struct BuiltinSpecsExtension: SpecificationsExtension {
RegisterExecutionPolicyExceptionToolSpec.self,
SwiftHeaderToolSpec.self,
TAPIMergeToolSpec.self,
ValidateDependenciesSpec.self,
ValidateDevelopmentAssets.self,
ConstructStubExecutorFileListToolSpec.self,
GateSpec.self,
Expand Down
23 changes: 18 additions & 5 deletions Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,9 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd

public let moduleDependenciesContext: ModuleDependenciesContext?
public let traceFilePath: Path?
public let dependencyValidationOutputPath: Path?

fileprivate init(serializedDiagnosticsPath: Path?, indexingPayload: ClangIndexingPayload?, explicitModulesPayload: ClangExplicitModulesPayload? = nil, outputObjectFilePath: Path? = nil, fileNameMapPath: Path? = nil, developerPathString: String? = nil, moduleDependenciesContext: ModuleDependenciesContext? = nil, traceFilePath: Path? = nil) {
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) {
if let developerPathString, explicitModulesPayload == nil {
self.dependencyInfoEditPayload = .init(removablePaths: [], removableBasenames: [], developerPath: Path(developerPathString))
} else {
Expand All @@ -448,10 +449,11 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
self.fileNameMapPath = fileNameMapPath
self.moduleDependenciesContext = moduleDependenciesContext
self.traceFilePath = traceFilePath
self.dependencyValidationOutputPath = dependencyValidationOutputPath
}

public func serialize<T: Serializer>(to serializer: T) {
serializer.serializeAggregate(8) {
serializer.serializeAggregate(9) {
serializer.serialize(serializedDiagnosticsPath)
serializer.serialize(indexingPayload)
serializer.serialize(explicitModulesPayload)
Expand All @@ -460,11 +462,12 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
serializer.serialize(dependencyInfoEditPayload)
serializer.serialize(moduleDependenciesContext)
serializer.serialize(traceFilePath)
serializer.serialize(dependencyValidationOutputPath)
}
}

public init(from deserializer: any Deserializer) throws {
try deserializer.beginAggregate(8)
try deserializer.beginAggregate(9)
self.serializedDiagnosticsPath = try deserializer.deserialize()
self.indexingPayload = try deserializer.deserialize()
self.explicitModulesPayload = try deserializer.deserialize()
Expand All @@ -473,6 +476,7 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
self.dependencyInfoEditPayload = try deserializer.deserialize()
self.moduleDependenciesContext = try deserializer.deserialize()
self.traceFilePath = try deserializer.deserialize()
self.dependencyValidationOutputPath = try deserializer.deserialize()
}
}

Expand Down Expand Up @@ -1165,10 +1169,14 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
dependencyData = nil
}

let extraOutputs: [any PlannedNode]
let moduleDependenciesContext = cbc.producer.moduleDependenciesContext
let dependencyValidationOutputPath: Path?
let traceFilePath: Path?
if clangInfo?.hasFeature("print-headers-direct-per-file") ?? false,
(moduleDependenciesContext?.validate ?? .defaultValue) != .no {
dependencyValidationOutputPath = Path(outputNode.path.str + ".dependencies")

let file = Path(outputNode.path.str + ".trace.json")
commandLine += [
"-Xclang", "-header-include-file",
Expand All @@ -1177,8 +1185,12 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
"-Xclang", "-header-include-format=json"
]
traceFilePath = file

extraOutputs = [MakePlannedPathNode(dependencyValidationOutputPath!), MakePlannedPathNode(traceFilePath!)]
} else {
dependencyValidationOutputPath = nil
traceFilePath = nil
extraOutputs = []
}

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

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

// Finally, create the task.
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)
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)

// 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.
if cbc.scope.evaluate(BuiltinMacros.CLANG_ENABLE_EXPLICIT_MODULES_OBJECT_FILE_VERIFIER) && action != nil {
Expand Down
36 changes: 30 additions & 6 deletions Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,16 @@ public struct SwiftSourceFileIndexingInfo: SourceFileIndexingInfo {
}
}

public struct SwiftDependencyValidationPayload: SerializableCodable, Encodable, Sendable {
public let dependencyValidationOutputPath: Path
public let moduleDependenciesContext: ModuleDependenciesContext

public init(dependencyValidationOutputPath: Path, moduleDependenciesContext: ModuleDependenciesContext) {
self.dependencyValidationOutputPath = dependencyValidationOutputPath
self.moduleDependenciesContext = moduleDependenciesContext
}
}

/// The minimal data we need to serialize to reconstruct `generatePreviewInfo`
public struct SwiftPreviewPayload: Serializable, Encodable, Sendable {
public let architecture: String
Expand Down Expand Up @@ -446,9 +456,9 @@ public struct SwiftTaskPayload: ParentTaskPayload {
/// The preview build style in effect (dynamic replacement or XOJIT), if any.
public let previewStyle: PreviewStyleMessagePayload?

public let moduleDependenciesContext: ModuleDependenciesContext?
public let dependencyValidationPayload: SwiftDependencyValidationPayload?

init(moduleName: String, indexingPayload: SwiftIndexingPayload, previewPayload: SwiftPreviewPayload?, localizationPayload: SwiftLocalizationPayload?, numExpectedCompileSubtasks: Int, driverPayload: SwiftDriverPayload?, previewStyle: PreviewStyle?, moduleDependenciesContext: ModuleDependenciesContext?) {
init(moduleName: String, indexingPayload: SwiftIndexingPayload, previewPayload: SwiftPreviewPayload?, localizationPayload: SwiftLocalizationPayload?, numExpectedCompileSubtasks: Int, driverPayload: SwiftDriverPayload?, previewStyle: PreviewStyle?, dependencyValidationPayload: SwiftDependencyValidationPayload?) {
self.moduleName = moduleName
self.indexingPayload = indexingPayload
self.previewPayload = previewPayload
Expand All @@ -463,7 +473,7 @@ public struct SwiftTaskPayload: ParentTaskPayload {
case nil:
self.previewStyle = nil
}
self.moduleDependenciesContext = moduleDependenciesContext
self.dependencyValidationPayload = dependencyValidationPayload
}

public func serialize<T: Serializer>(to serializer: T) {
Expand All @@ -475,7 +485,7 @@ public struct SwiftTaskPayload: ParentTaskPayload {
serializer.serialize(numExpectedCompileSubtasks)
serializer.serialize(driverPayload)
serializer.serialize(previewStyle)
serializer.serialize(moduleDependenciesContext)
serializer.serialize(dependencyValidationPayload)
}
}

Expand All @@ -488,7 +498,7 @@ public struct SwiftTaskPayload: ParentTaskPayload {
self.numExpectedCompileSubtasks = try deserializer.deserialize()
self.driverPayload = try deserializer.deserialize()
self.previewStyle = try deserializer.deserialize()
self.moduleDependenciesContext = try deserializer.deserialize()
self.dependencyValidationPayload = try deserializer.deserialize()
}
}

Expand Down Expand Up @@ -2283,6 +2293,20 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
moduleOutputPaths.append(moduleWrapOutput)
}

let dependencyValidationPayload: SwiftDependencyValidationPayload?
if let context = cbc.producer.moduleDependenciesContext, let outputPath = objectOutputPaths.first, context.validate != .no {
let primarySwiftBaseName = cbc.scope.evaluate(BuiltinMacros.TARGET_NAME) + compilationMode.moduleBaseNameSuffix + "-primary"
let dependencyValidationOutputPath = outputPath.dirname.join(primarySwiftBaseName + ".dependencies")
extraOutputPaths.append(dependencyValidationOutputPath)

dependencyValidationPayload = .init(
dependencyValidationOutputPath: dependencyValidationOutputPath,
moduleDependenciesContext: context
)
} else {
dependencyValidationPayload = nil
}

// The rule info.
//
// NOTE: If this changes, be sure to update the log parser to extract the variant and arch properly.
Expand Down Expand Up @@ -2312,7 +2336,7 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
numExpectedCompileSubtasks: isUsingWholeModuleOptimization ? 1 : cbc.inputs.count,
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),
previewStyle: cbc.scope.previewStyle,
moduleDependenciesContext: cbc.producer.moduleDependenciesContext
dependencyValidationPayload: dependencyValidationPayload
)

// Finally, assemble the input and output paths and create the Swift compiler command.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

public import SWBUtil
import SWBMacro

public final class ValidateDependenciesSpec: CommandLineToolSpec, SpecImplementationType, @unchecked Sendable {
public static let identifier = "com.apple.tools.validate-dependencies"

public static func construct(registry: SpecRegistry, proxy: SpecProxy) -> Spec {
return ValidateDependenciesSpec(registry: registry)
}

public init(registry: SpecRegistry) {
let proxy = SpecProxy(identifier: Self.identifier, domain: "", path: Path(""), type: Self.self, classType: nil, basedOn: nil, data: ["ExecDescription": PropertyListItem("Validate dependencies")], localizedStrings: nil)
super.init(createSpecParser(for: proxy, registry: registry), nil, isGeneric: false)
}

required init(_ parser: SpecParser, _ basedOnSpec: Spec?) {
super.init(parser, basedOnSpec, isGeneric: false)
}

override public func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async {
fatalError("unexpected direct invocation")
}

public override var payloadType: (any TaskPayload.Type)? { return ValidateDependenciesPayload.self }

public func createTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, dependencyInfos: [PlannedPathNode], payload: ValidateDependenciesPayload) async {
guard let configuredTarget = cbc.producer.configuredTarget else {
return
}
let output = delegate.createVirtualNode("ValidateDependencies \(configuredTarget.guid)")
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)
}
}

public struct ValidateDependenciesPayload: TaskPayload, Sendable, SerializableCodable {
public let moduleDependenciesContext: ModuleDependenciesContext

public init(moduleDependenciesContext: ModuleDependenciesContext) {
self.moduleDependenciesContext = moduleDependenciesContext
}
}
2 changes: 2 additions & 0 deletions Sources/SWBCore/TaskGeneration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ public protocol CommandProducer: PlatformBuildContext, SpecLookupContext, Refere

var processSDKImportsSpec: ProcessSDKImportsSpec { get }

var validateDependenciesSpec: ValidateDependenciesSpec { get }

/// The default working directory to use for a task, if it doesn't have a stronger preference.
var defaultWorkingDirectory: Path { get }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,7 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
}

var tasks: [any PlannedTask] = []
var dependencyDataFiles: [PlannedPathNode] = []

// Generate any auxiliary files whose content is not per-arch or per-variant.
// 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.
Expand Down Expand Up @@ -903,6 +904,8 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
case "swiftmodule":
dsymutilInputNodes.append(object)
break
case "dependencies":
dependencyDataFiles.append(MakePlannedPathNode(object.path))
default:
break
}
Expand Down Expand Up @@ -1594,6 +1597,20 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
tasks = tasks.filter { $0.inputs.contains(where: { $0.path.isValidLocalizedContent(scope) || $0.path.fileExtension == "xcstrings" }) }
}

// Create a task to validate dependencies if that feature is enabled.
if let moduleDependenciesContext = context.moduleDependenciesContext, moduleDependenciesContext.validate != .no {
var validateDepsTasks = [any PlannedTask]()
await appendGeneratedTasks(&validateDepsTasks, usePhasedOrdering: true) { delegate in
await context.validateDependenciesSpec.createTasks(
CommandBuildContext(producer: context, scope: scope, inputs: []),
delegate,
dependencyInfos: dependencyDataFiles,
payload: .init(moduleDependenciesContext: moduleDependenciesContext)
)
}
tasks.append(contentsOf: validateDepsTasks)
}

return tasks
}

Expand Down
Loading