diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index 618b3ca5..ba164dd5 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -997,6 +997,7 @@ public final class BuiltinMacros { public static let RPATH_ORIGIN = BuiltinMacros.declareStringMacro("RPATH_ORIGIN") public static let PLATFORM_USES_DSYMS = BuiltinMacros.declareBooleanMacro("PLATFORM_USES_DSYMS") public static let SWIFT_ABI_CHECKER_BASELINE_DIR = BuiltinMacros.declareStringMacro("SWIFT_ABI_CHECKER_BASELINE_DIR") + public static let SWIFT_ABI_CHECKER_DOWNGRADE_ERRORS = BuiltinMacros.declareBooleanMacro("SWIFT_ABI_CHECKER_DOWNGRADE_ERRORS") public static let SWIFT_ABI_CHECKER_EXCEPTIONS_FILE = BuiltinMacros.declareStringMacro("SWIFT_ABI_CHECKER_EXCEPTIONS_FILE") public static let SWIFT_ABI_GENERATION_TOOL_OUTPUT_DIR = BuiltinMacros.declareStringMacro("SWIFT_ABI_GENERATION_TOOL_OUTPUT_DIR") public static let SWIFT_ACCESS_NOTES_PATH = BuiltinMacros.declareStringMacro("SWIFT_ACCESS_NOTES_PATH") @@ -2171,6 +2172,7 @@ public final class BuiltinMacros { RPATH_ORIGIN, PLATFORM_USES_DSYMS, SWIFT_ABI_CHECKER_BASELINE_DIR, + SWIFT_ABI_CHECKER_DOWNGRADE_ERRORS, SWIFT_ABI_CHECKER_EXCEPTIONS_FILE, SWIFT_ABI_GENERATION_TOOL_OUTPUT_DIR, SWIFT_ACCESS_NOTES_PATH, diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftABICheckerTool.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftABICheckerTool.swift index b33ca677..4cb16d6b 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/SwiftABICheckerTool.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftABICheckerTool.swift @@ -42,17 +42,22 @@ public final class SwiftABICheckerToolSpec : GenericCommandLineToolSpec, SpecIde /// The path to the serialized diagnostic output. Every clang task must provide this path. let serializedDiagnosticsPath: Path - init(serializedDiagnosticsPath: Path) { + let downgradeErrors: Bool + + init(serializedDiagnosticsPath: Path, downgradeErrors: Bool) { self.serializedDiagnosticsPath = serializedDiagnosticsPath + self.downgradeErrors = downgradeErrors } public func serialize(to serializer: T) { - serializer.serializeAggregate(1) { + serializer.serializeAggregate(2) { serializer.serialize(serializedDiagnosticsPath) + serializer.serialize(downgradeErrors) } } public init(from deserializer: any Deserializer) throws { - try deserializer.beginAggregate(1) + try deserializer.beginAggregate(2) self.serializedDiagnosticsPath = try deserializer.deserialize() + self.downgradeErrors = try deserializer.deserialize() } } @@ -67,7 +72,12 @@ public final class SwiftABICheckerToolSpec : GenericCommandLineToolSpec, SpecIde // Override this func to ensure we can see these diagnostics in unit tests. public override func customOutputParserType(for task: any ExecutableTask) -> (any TaskOutputParser.Type)? { - return SerializedDiagnosticsOutputParser.self + let payload = task.payload! as! ABICheckerPayload + if payload.downgradeErrors { + return APIDigesterDowngradingSerializedDiagnosticsOutputParser.self + } else { + return SerializedDiagnosticsOutputParser.self + } } public func constructABICheckingTask(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, _ serializedDiagsPath: Path, _ baselinePath: Path?, _ allowlistPath: Path?) async { let toolSpecInfo: DiscoveredSwiftCompilerToolSpecInfo @@ -86,6 +96,10 @@ public final class SwiftABICheckerToolSpec : GenericCommandLineToolSpec, SpecIde if let allowlistPath { commandLine += ["-breakage-allowlist-path", allowlistPath.normalize().str] } + let downgradeErrors = cbc.scope.evaluate(BuiltinMacros.SWIFT_ABI_CHECKER_DOWNGRADE_ERRORS) + if downgradeErrors { + commandLine += ["-disable-fail-on-error"] + } let allInputs = cbc.inputs.map { delegate.createNode($0.absolutePath) } + [baselinePath, allowlistPath].compactMap { $0 }.map { delegate.createNode($0.normalize()) } // Add import search paths for searchPath in SwiftCompilerSpec.collectInputSearchPaths(cbc, toolInfo: toolSpecInfo) { @@ -95,7 +109,10 @@ public final class SwiftABICheckerToolSpec : GenericCommandLineToolSpec, SpecIde commandLine += cbc.scope.evaluate(BuiltinMacros.SWIFT_SYSTEM_INCLUDE_PATHS).flatMap { ["-I", $0] } commandLine += cbc.scope.evaluate(BuiltinMacros.SYSTEM_FRAMEWORK_SEARCH_PATHS).flatMap { ["-F", $0] } delegate.createTask(type: self, - payload: ABICheckerPayload(serializedDiagnosticsPath: serializedDiagsPath), + payload: ABICheckerPayload( + serializedDiagnosticsPath: serializedDiagsPath, + downgradeErrors: downgradeErrors + ), ruleInfo: defaultRuleInfo(cbc, delegate), commandLine: commandLine, environment: environmentFromSpec(cbc, delegate), @@ -105,3 +122,40 @@ public final class SwiftABICheckerToolSpec : GenericCommandLineToolSpec, SpecIde enableSandboxing: enableSandboxing) } } + +public final class APIDigesterDowngradingSerializedDiagnosticsOutputParser: TaskOutputParser { + private let task: any ExecutableTask + + public let workspaceContext: WorkspaceContext + public let buildRequestContext: BuildRequestContext + public let delegate: any TaskOutputParserDelegate + + required public init(for task: any ExecutableTask, workspaceContext: WorkspaceContext, buildRequestContext: BuildRequestContext, delegate: any TaskOutputParserDelegate, progressReporter: (any SubtaskProgressReporter)?) { + self.task = task + self.workspaceContext = workspaceContext + self.buildRequestContext = buildRequestContext + self.delegate = delegate + } + + public func write(bytes: ByteString) { + // Forward the unparsed bytes immediately (without line buffering). + delegate.emitOutput(bytes) + + // Disable diagnostic scraping, since we use serialized diagnostics. + } + + public func close(result: TaskResult?) { + defer { + delegate.close() + } + // Don't try to read diagnostics if the process crashed or got cancelled as they were almost certainly not written in this case. + if result.shouldSkipParsingDiagnostics { return } + + for path in task.type.serializedDiagnosticsPaths(task, workspaceContext.fs) { + let diagnostics = delegate.readSerializedDiagnostics(at: path, workingDirectory: task.workingDirectory, workspaceContext: workspaceContext) + for diagnostic in diagnostics { + delegate.diagnosticsEngine.emit(diagnostic.with(behavior: diagnostic.behavior == .error ? .warning : diagnostic.behavior)) + } + } + } +} diff --git a/Tests/SWBBuildSystemTests/APIDigesterBuildOperationTests.swift b/Tests/SWBBuildSystemTests/APIDigesterBuildOperationTests.swift new file mode 100644 index 00000000..afd19b47 --- /dev/null +++ b/Tests/SWBBuildSystemTests/APIDigesterBuildOperationTests.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// 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 Testing +import Foundation + +import SWBBuildSystem +import SWBCore +import SWBTestSupport +import SWBTaskExecution +import SWBUtil +import SWBProtocol + +@Suite +fileprivate struct APIDigesterBuildOperationTests: CoreBasedTests { + @Test(.requireSDKs(.host), .skipHostOS(.windows, "Windows toolchains are missing swift-api-digester")) + func apiDigesterDisableFailOnError() async throws { + try await withTemporaryDirectory { (tmpDir: Path) in + let testProject = try await TestProject( + "TestProject", + sourceRoot: tmpDir, + groupTree: TestGroup( + "SomeFiles", + children: [ + TestFile("foo.swift"), + ]), + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [ + "ARCHS": "$(ARCHS_STANDARD)", + "PRODUCT_NAME": "$(TARGET_NAME)", + "SDKROOT": "$(HOST_PLATFORM)", + "SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)", + "SWIFT_VERSION": swiftVersion, + "CODE_SIGNING_ALLOWED": "NO", + ]) + ], + targets: [ + TestStandardTarget( + "foo", + type: .dynamicLibrary, + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [:]) + ], + buildPhases: [ + TestSourcesBuildPhase(["foo.swift"]), + ] + ), + ]) + let core = try await getCore() + let tester = try await BuildOperationTester(core, testProject, simulated: false) + + let projectDir = tester.workspace.projects[0].sourceRoot + + try await tester.fs.writeFileContents(projectDir.join("foo.swift")) { stream in + stream <<< "public func foo() -> Int { 42 }" + } + + try await tester.checkBuild(parameters: BuildParameters(configuration: "Debug", overrides: [ + "RUN_SWIFT_ABI_GENERATION_TOOL": "YES", + "SWIFT_API_DIGESTER_MODE": "api", + "SWIFT_ABI_GENERATION_TOOL_OUTPUT_DIR": tmpDir.join("baseline").join("ABI").str, + ]), runDestination: .host) { results in + results.checkNoErrors() + } + + try await tester.fs.writeFileContents(projectDir.join("foo.swift")) { stream in + stream <<< "public func foo() -> String { \"hello, world!\" }" + } + + try await tester.checkBuild(parameters: BuildParameters(configuration: "Debug", overrides: [ + "RUN_SWIFT_ABI_CHECKER_TOOL": "YES", + "SWIFT_API_DIGESTER_MODE": "api", + "SWIFT_ABI_CHECKER_BASELINE_DIR": tmpDir.join("baseline").str, + "SWIFT_ABI_CHECKER_DOWNGRADE_ERRORS": "YES", + ]), runDestination: .host) { results in + results.checkWarning(.contains("func foo() has return type change from Swift.Int to Swift.String")) + results.checkNoDiagnostics() + } + } + } +}