Skip to content
Open
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
4 changes: 4 additions & 0 deletions Sources/CoreCommands/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,10 @@ public struct BuildOptions: ParsableArguments {
@Flag(inversion: .prefixedNo, help: .hidden)
public var omitFramePointers: Bool? = nil

// Whether to enable task backtrace logging.
@Flag(name: .customLong("experimental-task-backtraces"), help: .hidden)
public var enableTaskBacktraces: Bool = false

// Build dynamic library targets as frameworks (only available for Darwin targets and only when using the 'swiftbuild' build-system (currently used for tests).
@Flag(name: .customLong("experimental-build-dylibs-as-frameworks"), help: .hidden )
public var shouldBuildDylibsAsFrameworks: Bool = false
Expand Down
19 changes: 18 additions & 1 deletion Sources/CoreCommands/SwiftCommandState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,22 @@ public final class SwiftCommandState {
if !options.build._deprecated_manifestFlags.isEmpty {
observabilityScope.emit(warning: "'-Xmanifest' option is deprecated; use '-Xbuild-tools-swiftc' instead")
}

if options.build.enableTaskBacktraces {
// Task backtraces require at least verbose output to be logged
if !options.logging.verbose && !options.logging.veryVerbose {
observabilityScope.emit(
warning: "'--experimental-task-backtraces' requires '--verbose' or '--very-verbose'"
)
}

// Task backtraces are only supported by the swiftbuild build system
if options.build.buildSystem != .swiftbuild {
observabilityScope.emit(
warning: "'--experimental-task-backtraces' is only supported when using '--build-system swiftbuild'"
)
}
}
}

func waitForObservabilityEvents(timeout: DispatchTime) {
Expand Down Expand Up @@ -956,7 +972,8 @@ public final class SwiftCommandState {
),
outputParameters: .init(
isColorized: self.options.logging.colorDiagnostics,
isVerbose: self.logLevel <= .info
isVerbose: self.logLevel <= .info,
enableTaskBacktraces: self.options.build.enableTaskBacktraces
),
testingParameters: .init(
forceTestDiscovery: self.options.build.enableTestDiscovery,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ extension BuildParameters {
public struct Output: Encodable {
public init(
isColorized: Bool = false,
isVerbose: Bool = false
isVerbose: Bool = false,
enableTaskBacktraces: Bool = false
) {
self.isColorized = isColorized
self.isVerbose = isVerbose
self.enableTaskBacktraces = enableTaskBacktraces
}

public var isColorized: Bool

public var isVerbose: Bool

public var enableTaskBacktraces: Bool
}
}
17 changes: 16 additions & 1 deletion Sources/SwiftBuildSupport/SwiftBuildSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
struct BuildState {
private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:]
private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:]
var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames()

mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws {
if activeTasks[task.taskID] != nil {
Expand Down Expand Up @@ -697,9 +698,22 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
try? Basics.AbsolutePath(validating: $0.pathString)
})
}
if self.buildParameters.outputParameters.enableTaskBacktraces {
if let id = SWBBuildOperationBacktraceFrame.Identifier(taskSignatureData: Data(startedInfo.taskSignature.utf8)),
let backtrace = SWBTaskBacktrace(from: id, collectedFrames: buildState.collectedBacktraceFrames) {
let formattedBacktrace = backtrace.renderTextualRepresentation()
if !formattedBacktrace.isEmpty {
self.observabilityScope.emit(info: "Task backtrace:\n\(formattedBacktrace)")
}
}
}
case .targetStarted(let info):
try buildState.started(target: info)
case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .backtraceFrame, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate:
case .backtraceFrame(let info):
if self.buildParameters.outputParameters.enableTaskBacktraces {
buildState.collectedBacktraceFrames.add(frame: info)
}
case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate:
break
case .buildDiagnostic, .targetDiagnostic, .taskDiagnostic:
break // deprecated
Expand Down Expand Up @@ -1002,6 +1016,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
request.useDryRun = false
request.hideShellScriptEnvironment = true
request.showNonLoggedProgress = true
request.recordBuildBacktraces = buildParameters.outputParameters.enableTaskBacktraces

// Override the arena. We need to apply the arena info to both the request-global build
// parameters as well as the target-specific build parameters, since they may have been
Expand Down
1 change: 1 addition & 0 deletions Sources/_InternalTestSupport/SwiftTesting+Tags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ extension Tag.Feature {
@Tag public static var TestDiscovery: Tag
@Tag public static var Traits: Tag
@Tag public static var TargetSettings: Tag
@Tag public static var TaskBacktraces: Tag
@Tag public static var Version: Tag
}

Expand Down
83 changes: 83 additions & 0 deletions Tests/FunctionalTests/TaskBacktracesTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//===----------------------------------------------------------------------===//
//
// 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 Basics
import SPMBuildCore
import Testing
import _InternalTestSupport

@Suite
struct TaskBacktraceTests {
@Test(
.tags(.TestSize.large, .Feature.TaskBacktraces)
)
func taskBacktraces() async throws {
try await fixture(name: "Miscellaneous/Simple") { fixturePath in
let (stdout, _) = try await executeSwiftBuild(
fixturePath,
extraArgs: ["--experimental-task-backtraces", "--verbose"],
buildSystem: .swiftbuild
)
#expect(stdout.contains("Build complete!"))

// Wait to ensure file timestamps are different on filesystems with low precision
try await Task.sleep(for: .milliseconds(250))

try localFileSystem.writeFileContents(
fixturePath.appending(components: "Foo.swift"),
bytes: "public func bar() {}"
)

let (incrementalStdout, incrementalStderr) = try await executeSwiftBuild(
fixturePath,
extraArgs: ["--experimental-task-backtraces", "--verbose"],
buildSystem: .swiftbuild
)
// Add a basic check that we produce backtrace output. The specifc formatting is tested by Swift Build.
#expect(incrementalStderr.contains("Task backtrace:"))
#expect(incrementalStderr.split(separator: "\n").contains(where: {
$0.contains("Foo.swift' changed")
}))
#expect(incrementalStdout.contains("Build complete!"))
}
}

@Test(
.tags(.TestSize.large, .Feature.TaskBacktraces)
)
func taskBacktracesWarnsWithoutVerboseOutput() async throws {
try await fixture(name: "Miscellaneous/Simple") { fixturePath in
let (_, stderr) = try await executeSwiftBuild(
fixturePath,
extraArgs: ["--experimental-task-backtraces"],
buildSystem: .swiftbuild,
throwIfCommandFails: false
)
#expect(stderr.contains("'--experimental-task-backtraces' requires '--verbose' or '--very-verbose'"))
}
}

@Test(
.tags(.TestSize.large, .Feature.TaskBacktraces)
)
func taskBacktracesWarnsWithNonSwiftBuildSystem() async throws {
try await fixture(name: "Miscellaneous/Simple") { fixturePath in
let (_, stderr) = try await executeSwiftBuild(
fixturePath,
extraArgs: ["--experimental-task-backtraces", "--verbose"],
buildSystem: .native,
throwIfCommandFails: false
)
#expect(stderr.contains("'--experimental-task-backtraces' is only supported when using '--build-system swiftbuild'"))
}
}
}