From 5465e62342901dcfff97ad7da3ab301b48f089a0 Mon Sep 17 00:00:00 2001 From: Boris Buegling Date: Fri, 25 Jul 2025 13:32:53 -0700 Subject: [PATCH 1/4] Centralize dependency diagnostics in `ValidateDependencies` action 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 --- Sources/SWBCore/Dependencies.swift | 33 +++++++- Sources/SWBCore/PlannedTaskAction.swift | 1 + .../SpecImplementations/RegisterSpecs.swift | 1 + .../SpecImplementations/Tools/CCompiler.swift | 23 +++-- .../Tools/SwiftCompiler.swift | 49 +++++++++-- .../Tools/ValidateDependencies.swift | 64 ++++++++++++++ Sources/SWBCore/TaskGeneration.swift | 2 + .../SourcesTaskProducer.swift | 17 ++++ .../TaskProducers/TaskProducer.swift | 2 + .../BuildDescriptionManager.swift | 4 + .../BuiltinTaskActionsExtension.swift | 3 +- .../TaskActions/ClangCompileTaskAction.swift | 17 ++-- .../TaskActions/SwiftDriverTaskAction.swift | 17 ++-- .../ValidateDependenciesTaskAction.swift | 84 +++++++++++++++++++ .../CapturingTaskGenerationDelegate.swift | 4 + .../SWBTestSupport/DummyCommandProducer.swift | 2 + .../TaskPlanningTestSupport.swift | 4 + .../DependencyValidationTests.swift | 23 ++--- .../CommandLineSpecPerfTests.swift | 4 + 19 files changed, 316 insertions(+), 38 deletions(-) create mode 100644 Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift create mode 100644 Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift diff --git a/Sources/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift index 0429c616..8ef18cb9 100644 --- a/Sources/SWBCore/Dependencies.swift +++ b/Sources/SWBCore/Dependencies.swift @@ -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? @@ -235,3 +235,34 @@ 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 enum Payload: Hashable, Sendable, Codable { + case clangDependencies(files: [String]) + case swiftDependencies(imports: [Import]) + case unsupported + } + + public let payload: Payload + + public init(files: [Path]?) { + if let files { + self.payload = .clangDependencies(files: files.map { $0.str }) + } else { + self.payload = .unsupported + } + } + + public init(imports: [(ModuleDependency, importLocations: [SWBUtil.Diagnostic.Location])]?) { + if let imports { + self.payload = .swiftDependencies(imports: imports.map { Import(dependency: $0.0, importLocations: $0.importLocations) }) + } else { + self.payload = .unsupported + } + } +} diff --git a/Sources/SWBCore/PlannedTaskAction.swift b/Sources/SWBCore/PlannedTaskAction.swift index 3518ddc3..c81f93ee 100644 --- a/Sources/SWBCore/PlannedTaskAction.swift +++ b/Sources/SWBCore/PlannedTaskAction.swift @@ -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 { diff --git a/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift b/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift index 4bf7277f..db2699f4 100644 --- a/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift +++ b/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift @@ -126,6 +126,7 @@ public struct BuiltinSpecsExtension: SpecificationsExtension { RegisterExecutionPolicyExceptionToolSpec.self, SwiftHeaderToolSpec.self, TAPIMergeToolSpec.self, + ValidateDependenciesSpec.self, ValidateDevelopmentAssets.self, ConstructStubExecutorFileListToolSpec.self, GateSpec.self, diff --git a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift index 28863ecb..460489d1 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift @@ -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 { @@ -448,10 +449,11 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd self.fileNameMapPath = fileNameMapPath self.moduleDependenciesContext = moduleDependenciesContext self.traceFilePath = traceFilePath + self.dependencyValidationOutputPath = dependencyValidationOutputPath } public func serialize(to serializer: T) { - serializer.serializeAggregate(8) { + serializer.serializeAggregate(9) { serializer.serialize(serializedDiagnosticsPath) serializer.serialize(indexingPayload) serializer.serialize(explicitModulesPayload) @@ -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() @@ -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() } } @@ -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", @@ -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. @@ -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) } @@ -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 { diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift index 0bfa6c0b..95f4c265 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift @@ -273,6 +273,29 @@ public struct SwiftSourceFileIndexingInfo: SourceFileIndexingInfo { } } +public struct SwiftDependencyValidationPayload: Serializable, Encodable, Sendable { + public let dependencyValidationOutputPath: Path + public let moduleDependenciesContext: ModuleDependenciesContext + + public init(dependencyValidationOutputPath: Path, moduleDependenciesContext: ModuleDependenciesContext) { + self.dependencyValidationOutputPath = dependencyValidationOutputPath + self.moduleDependenciesContext = moduleDependenciesContext + } + + public func serialize(to serializer: T) { + serializer.serializeAggregate(2) { + serializer.serialize(dependencyValidationOutputPath) + serializer.serialize(moduleDependenciesContext) + } + } + + public init(from deserializer: any Deserializer) throws { + try deserializer.beginAggregate(2) + self.dependencyValidationOutputPath = try deserializer.deserialize() + self.moduleDependenciesContext = try deserializer.deserialize() + } +} + /// The minimal data we need to serialize to reconstruct `generatePreviewInfo` public struct SwiftPreviewPayload: Serializable, Encodable, Sendable { public let architecture: String @@ -446,9 +469,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 @@ -463,7 +486,7 @@ public struct SwiftTaskPayload: ParentTaskPayload { case nil: self.previewStyle = nil } - self.moduleDependenciesContext = moduleDependenciesContext + self.dependencyValidationPayload = dependencyValidationPayload } public func serialize(to serializer: T) { @@ -475,7 +498,7 @@ public struct SwiftTaskPayload: ParentTaskPayload { serializer.serialize(numExpectedCompileSubtasks) serializer.serialize(driverPayload) serializer.serialize(previewStyle) - serializer.serialize(moduleDependenciesContext) + serializer.serialize(dependencyValidationPayload) } } @@ -488,7 +511,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() } } @@ -2283,6 +2306,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. @@ -2312,7 +2349,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. diff --git a/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift new file mode 100644 index 00000000..20015480 --- /dev/null +++ b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// 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 { + public let moduleDependenciesContext: ModuleDependenciesContext + + public init(moduleDependenciesContext: ModuleDependenciesContext) { + self.moduleDependenciesContext = moduleDependenciesContext + } + + public func serialize(to serializer: T) { + serializer.serializeAggregate(1) { + serializer.serialize(moduleDependenciesContext) + } + } + + public init(from deserializer: any Deserializer) throws { + try deserializer.beginAggregate(1) + self.moduleDependenciesContext = try deserializer.deserialize() + } +} diff --git a/Sources/SWBCore/TaskGeneration.swift b/Sources/SWBCore/TaskGeneration.swift index 82b871d7..744ea2ac 100644 --- a/Sources/SWBCore/TaskGeneration.swift +++ b/Sources/SWBCore/TaskGeneration.swift @@ -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 } diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift index 5987aa71..1c847b6a 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift @@ -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. @@ -903,6 +904,8 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase case "swiftmodule": dsymutilInputNodes.append(object) break + case "dependencies": + dependencyDataFiles.append(MakePlannedPathNode(object.path)) default: break } @@ -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 } diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift index 82fd7837..2c481238 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift @@ -282,6 +282,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution let validateProductSpec: ValidateProductToolSpec let processXCFrameworkLibrarySpec: ProcessXCFrameworkLibrarySpec public let processSDKImportsSpec: ProcessSDKImportsSpec + public let validateDependenciesSpec: ValidateDependenciesSpec public let writeFileSpec: WriteFileSpec private let _documentationCompilerSpec: Result var documentationCompilerSpec: CommandLineToolSpec? { return specForResult(_documentationCompilerSpec) } @@ -404,6 +405,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution self.validateProductSpec = workspaceContext.core.specRegistry.getSpec("com.apple.build-tools.platform.validate", domain: domain) as! ValidateProductToolSpec self.processXCFrameworkLibrarySpec = workspaceContext.core.specRegistry.getSpec(ProcessXCFrameworkLibrarySpec.identifier, domain: domain) as! ProcessXCFrameworkLibrarySpec self.processSDKImportsSpec = workspaceContext.core.specRegistry.getSpec(ProcessSDKImportsSpec.identifier, domain: domain) as! ProcessSDKImportsSpec + self.validateDependenciesSpec = workspaceContext.core.specRegistry.getSpec(ValidateDependenciesSpec.identifier, domain: domain) as! ValidateDependenciesSpec self.writeFileSpec = workspaceContext.core.specRegistry.getSpec("com.apple.build-tools.write-file", domain: domain) as! WriteFileSpec self._documentationCompilerSpec = Result { try workspaceContext.core.specRegistry.getSpec("com.apple.compilers.documentation", domain: domain) as CommandLineToolSpec } self._tapiSymbolExtractorSpec = Result { try workspaceContext.core.specRegistry.getSpec("com.apple.compilers.documentation.objc-symbol-extract", domain: domain) as TAPISymbolExtractor } diff --git a/Sources/SWBTaskExecution/BuildDescriptionManager.swift b/Sources/SWBTaskExecution/BuildDescriptionManager.swift index c6a57ead..667e0e44 100644 --- a/Sources/SWBTaskExecution/BuildDescriptionManager.swift +++ b/Sources/SWBTaskExecution/BuildDescriptionManager.swift @@ -891,6 +891,10 @@ extension BuildSystemTaskPlanningDelegate: TaskActionCreationDelegate { func createProcessSDKImportsTaskAction() -> any PlannedTaskAction { return ProcessSDKImportsTaskAction() } + + func createValidateDependenciesTaskAction() -> any PlannedTaskAction { + return ValidateDependenciesTaskAction() + } } fileprivate extension BuildDescription { diff --git a/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift b/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift index 6474dd83..fa3b298b 100644 --- a/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift +++ b/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift @@ -51,7 +51,8 @@ public struct BuiltinTaskActionsExtension: TaskActionExtension { 36: ConstructStubExecutorInputFileListTaskAction.self, 37: ConcatenateTaskAction.self, 38: GenericCachingTaskAction.self, - 39: ProcessSDKImportsTaskAction.self + 39: ProcessSDKImportsTaskAction.self, + 40: ValidateDependenciesTaskAction.self, ] } } diff --git a/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift index d72d198e..0f6a1e01 100644 --- a/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift @@ -251,12 +251,15 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA // Check if verifying dependencies from trace data is enabled. let traceFilePath: Path? let moduleDependenciesContext: ModuleDependenciesContext? + let dependencyValidationOutputPath: Path? if let payload = task.payload as? ClangTaskPayload { traceFilePath = payload.traceFilePath moduleDependenciesContext = payload.moduleDependenciesContext + dependencyValidationOutputPath = payload.dependencyValidationOutputPath } else { traceFilePath = nil moduleDependenciesContext = nil + dependencyValidationOutputPath = nil } if let traceFilePath { // Remove the trace output file if it already exists. @@ -335,13 +338,15 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA } else { files = nil } - let diagnostics = moduleDependenciesContext.makeDiagnostics(files: files) - for diagnostic in diagnostics { - outputDelegate.emit(diagnostic) - } - if diagnostics.contains(where: { $0.behavior == .error }) { - return .failed + if let dependencyValidationOutputPath { + let validationInfo = DependencyValidationInfo(files: files) + _ = try executionDelegate.fs.writeIfChanged( + dependencyValidationOutputPath, + contents: ByteString( + JSONEncoder(outputFormatting: .sortedKeys).encode(validationInfo) + ) + ) } } diff --git a/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift index 6b6b5a1a..df14fc6a 100644 --- a/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift @@ -96,17 +96,16 @@ final public class SwiftDriverTaskAction: TaskAction, BuildValueValidatingTaskAc } if driverPayload.explicitModulesEnabled, - let moduleDependenciesContext = payload.moduleDependenciesContext + let dependencyValidationPayload = payload.dependencyValidationPayload { let imports = try await dependencyGraph.mainModuleImportModuleDependencies(for: driverPayload.uniqueID) - let diagnostics = moduleDependenciesContext.makeDiagnostics(imports: imports) - for diagnostic in diagnostics { - outputDelegate.emit(diagnostic) - } - - if (diagnostics.contains { $0.behavior == .error }) { - return .failed - } + let validationInfo = DependencyValidationInfo(imports: imports) + _ = try executionDelegate.fs.writeIfChanged( + dependencyValidationPayload.dependencyValidationOutputPath, + contents: ByteString( + JSONEncoder(outputFormatting: .sortedKeys).encode(validationInfo) + ) + ) } if driverPayload.reportRequiredTargetDependencies != .no && driverPayload.explicitModulesEnabled, let target = task.forTarget { diff --git a/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift new file mode 100644 index 00000000..313262ab --- /dev/null +++ b/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import Foundation +public import SWBCore +import SWBUtil + +public final class ValidateDependenciesTaskAction: TaskAction { + public override class var toolIdentifier: String { + return "validate-dependencies" + } + + public override func performTaskAction(_ task: any ExecutableTask, dynamicExecutionDelegate: any DynamicTaskExecutionDelegate, executionDelegate: any TaskExecutionDelegate, clientDelegate: any TaskExecutionClientDelegate, outputDelegate: any TaskOutputDelegate) async -> CommandResult { + let commandLine = Array(task.commandLineAsStrings) + guard commandLine.count >= 1, commandLine[0] == "builtin-validate-dependencies" else { + outputDelegate.emitError("unexpected arguments: \(commandLine)") + return .failed + } + + guard let context = (task.payload as? ValidateDependenciesPayload)?.moduleDependenciesContext else { + if let payload = task.payload { + outputDelegate.emitError("invalid task payload: \(payload)") + } else { + outputDelegate.emitError("empty task payload") + } + return .failed + } + + do { + var allFiles = Set() + var allImports = Set() + var unsupported = false + + for inputPath in task.inputPaths { + let inputData = try Data(contentsOf: URL(fileURLWithPath: inputPath.str)) + let info = try JSONDecoder().decode(DependencyValidationInfo.self, from: inputData) + + switch info.payload { + case .clangDependencies(let files): + files.forEach { + allFiles.insert($0) + } + case .swiftDependencies(let imports): + imports.forEach { + allImports.insert($0) + } + case .unsupported: + unsupported = true + } + } + + var diagnostics: [Diagnostic] = [] + + if unsupported { + diagnostics.append(contentsOf: context.makeDiagnostics(files: nil)) + } else { + diagnostics.append(contentsOf: context.makeDiagnostics(files: allFiles.map { Path($0) })) + diagnostics.append(contentsOf: context.makeDiagnostics(imports: allImports.map { ($0.dependency, $0.importLocations) })) + } + + for diagnostic in diagnostics { + outputDelegate.emit(diagnostic) + } + + if diagnostics.contains(where: { $0.behavior == .error }) { + return .failed + } + } catch { + outputDelegate.emitError("\(error)") + return .failed + } + + return .succeeded + } +} diff --git a/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift b/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift index b4689254..cc8c6eee 100644 --- a/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift +++ b/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift @@ -239,4 +239,8 @@ extension CapturingTaskGenerationDelegate: TaskActionCreationDelegate { package func createProcessSDKImportsTaskAction() -> any PlannedTaskAction { return ProcessSDKImportsTaskAction() } + + package func createValidateDependenciesTaskAction() -> any PlannedTaskAction { + return ValidateDependenciesTaskAction() + } } diff --git a/Sources/SWBTestSupport/DummyCommandProducer.swift b/Sources/SWBTestSupport/DummyCommandProducer.swift index a208163b..9d00f0b5 100644 --- a/Sources/SWBTestSupport/DummyCommandProducer.swift +++ b/Sources/SWBTestSupport/DummyCommandProducer.swift @@ -104,6 +104,7 @@ package struct MockCommandProducer: CommandProducer, Sendable { self.mkdirSpec = try getSpec("com.apple.tools.mkdir") as MkdirToolSpec self.swiftCompilerSpec = try getSpec() as SwiftCompilerSpec self.processSDKImportsSpec = try getSpec(ProcessSDKImportsSpec.identifier) as ProcessSDKImportsSpec + self.validateDependenciesSpec = try getSpec(ValidateDependenciesSpec.identifier) as ValidateDependenciesSpec } package let specDataCaches = Registry() @@ -140,6 +141,7 @@ package struct MockCommandProducer: CommandProducer, Sendable { package let mkdirSpec: MkdirToolSpec package let swiftCompilerSpec: SwiftCompilerSpec package let processSDKImportsSpec: ProcessSDKImportsSpec + package let validateDependenciesSpec: ValidateDependenciesSpec package var defaultWorkingDirectory: Path { return Path("/tmp") diff --git a/Sources/SWBTestSupport/TaskPlanningTestSupport.swift b/Sources/SWBTestSupport/TaskPlanningTestSupport.swift index f615bdc7..0fdacfea 100644 --- a/Sources/SWBTestSupport/TaskPlanningTestSupport.swift +++ b/Sources/SWBTestSupport/TaskPlanningTestSupport.swift @@ -467,6 +467,10 @@ extension TestTaskPlanningDelegate: TaskActionCreationDelegate { package func createProcessSDKImportsTaskAction() -> any PlannedTaskAction { return ProcessSDKImportsTaskAction() } + + package func createValidateDependenciesTaskAction() -> any PlannedTaskAction { + return ValidateProductTaskAction() + } } package final class CancellingTaskPlanningDelegate: TestTaskPlanningDelegate, @unchecked Sendable { diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift index 85204c76..afc7b46a 100644 --- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift +++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift @@ -471,7 +471,8 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { groupTree: TestGroup( "Sources", path: "Sources", children: [ - TestFile("CoreFoo.m") + TestFile("CoreFoo.m"), + TestFile("CoreBar.m"), ]), buildConfigurations: [ TestBuildConfiguration( @@ -493,7 +494,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { TestStandardTarget( "CoreFoo", type: .framework, buildPhases: [ - TestSourcesBuildPhase(["CoreFoo.m"]), + TestSourcesBuildPhase(["CoreFoo.m", "CoreBar.m"]), TestFrameworksBuildPhase() ]) ]) @@ -504,14 +505,16 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { let SRCROOT = testWorkspace.sourceRoot.join("aProject") // Write the source files. - try await tester.fs.writeFileContents(SRCROOT.join("Sources/CoreFoo.m")) { contents in - contents <<< """ - #include - #include - #include - - void f0(void) { }; - """ + for stem in ["Foo", "Bar"] { + try await tester.fs.writeFileContents(SRCROOT.join("Sources/Core\(stem).m")) { contents in + contents <<< """ + #include + #include + #include + + void f\(stem)(void) { }; + """ + } } // Expect complaint about undeclared dependency diff --git a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift index d85ccb2c..b7669be0 100644 --- a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift +++ b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift @@ -233,6 +233,10 @@ extension CapturingTaskGenerationDelegate: TaskActionCreationDelegate { func createProcessSDKImportsTaskAction() -> any PlannedTaskAction { return ProcessSDKImportsTaskAction() } + + func createValidateDependenciesTaskAction() -> any PlannedTaskAction { + return ValidateProductTaskAction() + } } extension CapturingTaskGenerationDelegate: CoreClientDelegate { From edd97879bd7ffa05915a46503f9d9de3a256bd4a Mon Sep 17 00:00:00 2001 From: Boris Buegling Date: Mon, 28 Jul 2025 11:07:42 -0700 Subject: [PATCH 2/4] Skip `validateModuleDependenciesSwift` on Linux and Windows for now The toolchains in CI are currently breaking this test. --- Tests/SWBBuildSystemTests/DependencyValidationTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift index afc7b46a..d1aedee3 100644 --- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift +++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift @@ -327,7 +327,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { } } - @Test(.requireSDKs(.host)) + @Test(.requireSDKs(.host), .skipHostOS(.windows, "toolchain too old"), .skipHostOS(.linux, "toolchain too old")) func validateModuleDependenciesSwift() async throws { try await withTemporaryDirectory { tmpDir in let testWorkspace = try await TestWorkspace( From f3d7444f61ba7bb5c655917d5fce20b3a3c59ca5 Mon Sep 17 00:00:00 2001 From: Boris Buegling Date: Mon, 28 Jul 2025 11:41:25 -0700 Subject: [PATCH 3/4] Update cmake configs --- Sources/SWBCore/CMakeLists.txt | 1 + Sources/SWBTaskExecution/CMakeLists.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/Sources/SWBCore/CMakeLists.txt b/Sources/SWBCore/CMakeLists.txt index 3833e042..93ceda77 100644 --- a/Sources/SWBCore/CMakeLists.txt +++ b/Sources/SWBCore/CMakeLists.txt @@ -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 diff --git a/Sources/SWBTaskExecution/CMakeLists.txt b/Sources/SWBTaskExecution/CMakeLists.txt index 6c7cba03..4a766605 100644 --- a/Sources/SWBTaskExecution/CMakeLists.txt +++ b/Sources/SWBTaskExecution/CMakeLists.txt @@ -70,6 +70,7 @@ add_library(SWBTaskExecution TaskActions/SwiftDriverTaskAction.swift TaskActions/SwiftHeaderToolTaskAction.swift TaskActions/TaskAction.swift + TaskActions/ValidateDependenciesTaskAction.swift TaskActions/ValidateDevelopmentAssetsTaskAction.swift TaskActions/ValidateProductTaskAction.swift TaskResult.swift From 5f3e28fddfe708cab3272ac99e1c040c37dff435 Mon Sep 17 00:00:00 2001 From: Boris Buegling Date: Wed, 30 Jul 2025 11:03:34 -0700 Subject: [PATCH 4/4] Address feedback --- Sources/SWBCore/Dependencies.swift | 21 +++++++------------ .../Tools/SwiftCompiler.swift | 15 +------------ .../Tools/ValidateDependencies.swift | 13 +----------- .../TaskActions/ClangCompileTaskAction.swift | 10 ++++----- .../TaskActions/SwiftDriverTaskAction.swift | 9 ++++++-- 5 files changed, 21 insertions(+), 47 deletions(-) diff --git a/Sources/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift index 8ef18cb9..64824303 100644 --- a/Sources/SWBCore/Dependencies.swift +++ b/Sources/SWBCore/Dependencies.swift @@ -240,6 +240,11 @@ 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 { @@ -250,19 +255,7 @@ public struct DependencyValidationInfo: Hashable, Sendable, Codable { public let payload: Payload - public init(files: [Path]?) { - if let files { - self.payload = .clangDependencies(files: files.map { $0.str }) - } else { - self.payload = .unsupported - } - } - - public init(imports: [(ModuleDependency, importLocations: [SWBUtil.Diagnostic.Location])]?) { - if let imports { - self.payload = .swiftDependencies(imports: imports.map { Import(dependency: $0.0, importLocations: $0.importLocations) }) - } else { - self.payload = .unsupported - } + public init(payload: Payload) { + self.payload = payload } } diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift index 95f4c265..827eaac7 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift @@ -273,7 +273,7 @@ public struct SwiftSourceFileIndexingInfo: SourceFileIndexingInfo { } } -public struct SwiftDependencyValidationPayload: Serializable, Encodable, Sendable { +public struct SwiftDependencyValidationPayload: SerializableCodable, Encodable, Sendable { public let dependencyValidationOutputPath: Path public let moduleDependenciesContext: ModuleDependenciesContext @@ -281,19 +281,6 @@ public struct SwiftDependencyValidationPayload: Serializable, Encodable, Sendabl self.dependencyValidationOutputPath = dependencyValidationOutputPath self.moduleDependenciesContext = moduleDependenciesContext } - - public func serialize(to serializer: T) { - serializer.serializeAggregate(2) { - serializer.serialize(dependencyValidationOutputPath) - serializer.serialize(moduleDependenciesContext) - } - } - - public init(from deserializer: any Deserializer) throws { - try deserializer.beginAggregate(2) - self.dependencyValidationOutputPath = try deserializer.deserialize() - self.moduleDependenciesContext = try deserializer.deserialize() - } } /// The minimal data we need to serialize to reconstruct `generatePreviewInfo` diff --git a/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift index 20015480..0fb14969 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift @@ -44,21 +44,10 @@ public final class ValidateDependenciesSpec: CommandLineToolSpec, SpecImplementa } } -public struct ValidateDependenciesPayload: TaskPayload, Sendable { +public struct ValidateDependenciesPayload: TaskPayload, Sendable, SerializableCodable { public let moduleDependenciesContext: ModuleDependenciesContext public init(moduleDependenciesContext: ModuleDependenciesContext) { self.moduleDependenciesContext = moduleDependenciesContext } - - public func serialize(to serializer: T) { - serializer.serializeAggregate(1) { - serializer.serialize(moduleDependenciesContext) - } - } - - public init(from deserializer: any Deserializer) throws { - try deserializer.beginAggregate(1) - self.moduleDependenciesContext = try deserializer.deserialize() - } } diff --git a/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift index 0f6a1e01..88062d09 100644 --- a/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift @@ -325,22 +325,22 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA } } - if let moduleDependenciesContext, lastResult == .succeeded { + if lastResult == .succeeded { // Verify the dependencies from the trace data. - let files: [Path]? + let payload: DependencyValidationInfo.Payload if let traceFilePath { let fs = executionDelegate.fs let traceData = try JSONDecoder().decode(Array.self, from: Data(fs.read(traceFilePath))) var allFiles = Set() traceData.forEach { allFiles.formUnion(Set($0.includes)) } - files = Array(allFiles) + payload = .clangDependencies(files: allFiles.map { $0.str }) } else { - files = nil + payload = .unsupported } if let dependencyValidationOutputPath { - let validationInfo = DependencyValidationInfo(files: files) + let validationInfo = DependencyValidationInfo(payload: payload) _ = try executionDelegate.fs.writeIfChanged( dependencyValidationOutputPath, contents: ByteString( diff --git a/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift index df14fc6a..082945bd 100644 --- a/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift @@ -98,8 +98,13 @@ final public class SwiftDriverTaskAction: TaskAction, BuildValueValidatingTaskAc if driverPayload.explicitModulesEnabled, let dependencyValidationPayload = payload.dependencyValidationPayload { - let imports = try await dependencyGraph.mainModuleImportModuleDependencies(for: driverPayload.uniqueID) - let validationInfo = DependencyValidationInfo(imports: imports) + let payload: DependencyValidationInfo.Payload + if let imports = try await dependencyGraph.mainModuleImportModuleDependencies(for: driverPayload.uniqueID) { + payload = .swiftDependencies(imports: imports.map { .init(dependency: $0.0, importLocations: $0.importLocations) }) + } else { + payload = .unsupported + } + let validationInfo = DependencyValidationInfo(payload: payload) _ = try executionDelegate.fs.writeIfChanged( dependencyValidationPayload.dependencyValidationOutputPath, contents: ByteString(