Skip to content
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
2 changes: 2 additions & 0 deletions Sources/SWBCore/Settings/BuiltinMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Serializer>(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()
}
}

Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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),
Expand All @@ -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))
}
}
}
}
90 changes: 90 additions & 0 deletions Tests/SWBBuildSystemTests/APIDigesterBuildOperationTests.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
}
Loading