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/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift index 0429c616..64824303 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,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 + } +} 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..827eaac7 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift @@ -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 @@ -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 @@ -463,7 +473,7 @@ public struct SwiftTaskPayload: ParentTaskPayload { case nil: self.previewStyle = nil } - self.moduleDependenciesContext = moduleDependenciesContext + self.dependencyValidationPayload = dependencyValidationPayload } public func serialize(to serializer: T) { @@ -475,7 +485,7 @@ public struct SwiftTaskPayload: ParentTaskPayload { serializer.serialize(numExpectedCompileSubtasks) serializer.serialize(driverPayload) serializer.serialize(previewStyle) - serializer.serialize(moduleDependenciesContext) + serializer.serialize(dependencyValidationPayload) } } @@ -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() } } @@ -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. @@ -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. diff --git a/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift new file mode 100644 index 00000000..0fb14969 --- /dev/null +++ b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift @@ -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 + } +} 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/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 diff --git a/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift index d72d198e..88062d09 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. @@ -322,26 +325,28 @@ 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 - } - let diagnostics = moduleDependenciesContext.makeDiagnostics(files: files) - for diagnostic in diagnostics { - outputDelegate.emit(diagnostic) + payload = .unsupported } - if diagnostics.contains(where: { $0.behavior == .error }) { - return .failed + if let dependencyValidationOutputPath { + let validationInfo = DependencyValidationInfo(payload: payload) + _ = 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..082945bd 100644 --- a/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift @@ -96,17 +96,21 @@ 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 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( + 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..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( @@ -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 {