diff --git a/Sources/CoreCommands/Options.swift b/Sources/CoreCommands/Options.swift index 6421ccf4c52..ab0db1df264 100644 --- a/Sources/CoreCommands/Options.swift +++ b/Sources/CoreCommands/Options.swift @@ -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 diff --git a/Sources/CoreCommands/SwiftCommandState.swift b/Sources/CoreCommands/SwiftCommandState.swift index c927160d89a..ceacbf39b79 100644 --- a/Sources/CoreCommands/SwiftCommandState.swift +++ b/Sources/CoreCommands/SwiftCommandState.swift @@ -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) { @@ -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, diff --git a/Sources/SPMBuildCore/BuildParameters/BuildParameters+Output.swift b/Sources/SPMBuildCore/BuildParameters/BuildParameters+Output.swift index 3f4ec706a16..a542c1f6074 100644 --- a/Sources/SPMBuildCore/BuildParameters/BuildParameters+Output.swift +++ b/Sources/SPMBuildCore/BuildParameters/BuildParameters+Output.swift @@ -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 } } diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index a0d710c29bc..ab9cdd044f4 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -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 { @@ -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 @@ -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 diff --git a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift index 9873fdf111d..1a9a2ffd677 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift @@ -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 } diff --git a/Tests/FunctionalTests/TaskBacktracesTests.swift b/Tests/FunctionalTests/TaskBacktracesTests.swift new file mode 100644 index 00000000000..c75a0c88dd6 --- /dev/null +++ b/Tests/FunctionalTests/TaskBacktracesTests.swift @@ -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'")) + } + } +}