Skip to content

Commit e1f3ce2

Browse files
authored
Add experimental flag to log Swift Build task backtraces (#9352)
Task backtraces are a Swift Build feature for debugging unexpected builds where task(s) re-run unexpectedly. Add a (currently experimental) flag to turn them on and log them.
1 parent 9057cae commit e1f3ce2

File tree

6 files changed

+127
-3
lines changed

6 files changed

+127
-3
lines changed

Sources/CoreCommands/Options.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,10 @@ public struct BuildOptions: ParsableArguments {
587587
@Flag(inversion: .prefixedNo, help: .hidden)
588588
public var omitFramePointers: Bool? = nil
589589

590+
// Whether to enable task backtrace logging.
591+
@Flag(name: .customLong("experimental-task-backtraces"), help: .hidden)
592+
public var enableTaskBacktraces: Bool = false
593+
590594
// Build dynamic library targets as frameworks (only available for Darwin targets and only when using the 'swiftbuild' build-system (currently used for tests).
591595
@Flag(name: .customLong("experimental-build-dylibs-as-frameworks"), help: .hidden )
592596
public var shouldBuildDylibsAsFrameworks: Bool = false

Sources/CoreCommands/SwiftCommandState.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,22 @@ public final class SwiftCommandState {
460460
if !options.build._deprecated_manifestFlags.isEmpty {
461461
observabilityScope.emit(warning: "'-Xmanifest' option is deprecated; use '-Xbuild-tools-swiftc' instead")
462462
}
463+
464+
if options.build.enableTaskBacktraces {
465+
// Task backtraces require at least verbose output to be logged
466+
if !options.logging.verbose && !options.logging.veryVerbose {
467+
observabilityScope.emit(
468+
warning: "'--experimental-task-backtraces' requires '--verbose' or '--very-verbose'"
469+
)
470+
}
471+
472+
// Task backtraces are only supported by the swiftbuild build system
473+
if options.build.buildSystem != .swiftbuild {
474+
observabilityScope.emit(
475+
warning: "'--experimental-task-backtraces' is only supported when using '--build-system swiftbuild'"
476+
)
477+
}
478+
}
463479
}
464480

465481
func waitForObservabilityEvents(timeout: DispatchTime) {
@@ -957,7 +973,8 @@ public final class SwiftCommandState {
957973
),
958974
outputParameters: .init(
959975
isColorized: self.options.logging.colorDiagnostics,
960-
isVerbose: self.logLevel <= .info
976+
isVerbose: self.logLevel <= .info,
977+
enableTaskBacktraces: self.options.build.enableTaskBacktraces
961978
),
962979
testingParameters: .init(
963980
forceTestDiscovery: self.options.build.enableTestDiscovery,

Sources/SPMBuildCore/BuildParameters/BuildParameters+Output.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@ extension BuildParameters {
1515
public struct Output: Encodable {
1616
public init(
1717
isColorized: Bool = false,
18-
isVerbose: Bool = false
18+
isVerbose: Bool = false,
19+
enableTaskBacktraces: Bool = false
1920
) {
2021
self.isColorized = isColorized
2122
self.isVerbose = isVerbose
23+
self.enableTaskBacktraces = enableTaskBacktraces
2224
}
2325

2426
public var isColorized: Bool
2527

2628
public var isVerbose: Bool
29+
30+
public var enableTaskBacktraces: Bool
2731
}
2832
}

Sources/SwiftBuildSupport/SwiftBuildSystem.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
585585
struct BuildState {
586586
private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:]
587587
private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:]
588+
var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames()
588589

589590
mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws {
590591
if activeTasks[task.taskID] != nil {
@@ -697,9 +698,22 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
697698
try? Basics.AbsolutePath(validating: $0.pathString)
698699
})
699700
}
701+
if self.buildParameters.outputParameters.enableTaskBacktraces {
702+
if let id = SWBBuildOperationBacktraceFrame.Identifier(taskSignatureData: Data(startedInfo.taskSignature.utf8)),
703+
let backtrace = SWBTaskBacktrace(from: id, collectedFrames: buildState.collectedBacktraceFrames) {
704+
let formattedBacktrace = backtrace.renderTextualRepresentation()
705+
if !formattedBacktrace.isEmpty {
706+
self.observabilityScope.emit(info: "Task backtrace:\n\(formattedBacktrace)")
707+
}
708+
}
709+
}
700710
case .targetStarted(let info):
701711
try buildState.started(target: info)
702-
case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .backtraceFrame, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate:
712+
case .backtraceFrame(let info):
713+
if self.buildParameters.outputParameters.enableTaskBacktraces {
714+
buildState.collectedBacktraceFrames.add(frame: info)
715+
}
716+
case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate:
703717
break
704718
case .buildDiagnostic, .targetDiagnostic, .taskDiagnostic:
705719
break // deprecated
@@ -1002,6 +1016,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
10021016
request.useDryRun = false
10031017
request.hideShellScriptEnvironment = true
10041018
request.showNonLoggedProgress = true
1019+
request.recordBuildBacktraces = buildParameters.outputParameters.enableTaskBacktraces
10051020

10061021
// Override the arena. We need to apply the arena info to both the request-global build
10071022
// parameters as well as the target-specific build parameters, since they may have been

Sources/_InternalTestSupport/SwiftTesting+Tags.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ extension Tag.Feature {
5151
@Tag public static var TestDiscovery: Tag
5252
@Tag public static var Traits: Tag
5353
@Tag public static var TargetSettings: Tag
54+
@Tag public static var TaskBacktraces: Tag
5455
@Tag public static var Version: Tag
5556
}
5657

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Basics
14+
import SPMBuildCore
15+
import Testing
16+
import _InternalTestSupport
17+
18+
@Suite
19+
struct TaskBacktraceTests {
20+
@Test(
21+
.tags(.TestSize.large, .Feature.TaskBacktraces)
22+
)
23+
func taskBacktraces() async throws {
24+
try await fixture(name: "Miscellaneous/Simple") { fixturePath in
25+
let (stdout, _) = try await executeSwiftBuild(
26+
fixturePath,
27+
extraArgs: ["--experimental-task-backtraces", "--verbose"],
28+
buildSystem: .swiftbuild
29+
)
30+
#expect(stdout.contains("Build complete!"))
31+
32+
// Wait to ensure file timestamps are different on filesystems with low precision
33+
try await Task.sleep(for: .milliseconds(250))
34+
35+
try localFileSystem.writeFileContents(
36+
fixturePath.appending(components: "Foo.swift"),
37+
bytes: "public func bar() {}"
38+
)
39+
40+
let (incrementalStdout, incrementalStderr) = try await executeSwiftBuild(
41+
fixturePath,
42+
extraArgs: ["--experimental-task-backtraces", "--verbose"],
43+
buildSystem: .swiftbuild
44+
)
45+
// Add a basic check that we produce backtrace output. The specifc formatting is tested by Swift Build.
46+
#expect(incrementalStderr.contains("Task backtrace:"))
47+
#expect(incrementalStderr.split(separator: "\n").contains(where: {
48+
$0.contains("Foo.swift' changed")
49+
}))
50+
#expect(incrementalStdout.contains("Build complete!"))
51+
}
52+
}
53+
54+
@Test(
55+
.tags(.TestSize.large, .Feature.TaskBacktraces)
56+
)
57+
func taskBacktracesWarnsWithoutVerboseOutput() async throws {
58+
try await fixture(name: "Miscellaneous/Simple") { fixturePath in
59+
let (_, stderr) = try await executeSwiftBuild(
60+
fixturePath,
61+
extraArgs: ["--experimental-task-backtraces"],
62+
buildSystem: .swiftbuild,
63+
throwIfCommandFails: false
64+
)
65+
#expect(stderr.contains("'--experimental-task-backtraces' requires '--verbose' or '--very-verbose'"))
66+
}
67+
}
68+
69+
@Test(
70+
.tags(.TestSize.large, .Feature.TaskBacktraces)
71+
)
72+
func taskBacktracesWarnsWithNonSwiftBuildSystem() async throws {
73+
try await fixture(name: "Miscellaneous/Simple") { fixturePath in
74+
let (_, stderr) = try await executeSwiftBuild(
75+
fixturePath,
76+
extraArgs: ["--experimental-task-backtraces", "--verbose"],
77+
buildSystem: .native,
78+
throwIfCommandFails: false
79+
)
80+
#expect(stderr.contains("'--experimental-task-backtraces' is only supported when using '--build-system swiftbuild'"))
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)