From 60b27cb39a34233fcc620f7b3986520b710d07ff Mon Sep 17 00:00:00 2001 From: Owen Voorhees Date: Mon, 2 Jun 2025 17:34:27 -0700 Subject: [PATCH] Improve printed descriptions of task backtraces - Clarify frame descriptions based on feedback - expose public API for rendering a backtrace as text rdar://152194638 --- Sources/SWBBuildSystem/BuildOperation.swift | 14 +- .../SWBBuildOperationBacktraceFrame.swift | 11 ++ .../TaskBacktraces.swift | 8 ++ .../BuildBacktraceTests.swift | 123 ++++++++++++++++-- 4 files changed, 139 insertions(+), 17 deletions(-) diff --git a/Sources/SWBBuildSystem/BuildOperation.swift b/Sources/SWBBuildSystem/BuildOperation.swift index 9efc58ee..dc91a5f6 100644 --- a/Sources/SWBBuildSystem/BuildOperation.swift +++ b/Sources/SWBBuildSystem/BuildOperation.swift @@ -2304,9 +2304,9 @@ internal final class OperationSystemAdaptor: SWBLLBuild.BuildSystemDelegate, Act private func inputNounPhraseForBuildKey(_ inputKey: BuildKey) -> String { switch inputKey { case is BuildKey.Command, is BuildKey.CustomTask: - return "the producer" + return "the task producing" case is BuildKey.DirectoryContents, is BuildKey.FilteredDirectoryContents, is BuildKey.DirectoryTreeSignature, is BuildKey.Node: - return "an input" + return "an input of" case is BuildKey.Target, is BuildKey.Stat: return "" default: @@ -2343,15 +2343,15 @@ internal final class OperationSystemAdaptor: SWBLLBuild.BuildSystemDelegate, Act previousFrameID = nil case .signatureChanged: category = .ruleSignatureChanged - description = "signature of \(descriptionForBuildKey(rule)) changed" + description = "arguments, environment, or working directory of \(descriptionForBuildKey(rule)) changed" previousFrameID = nil case .invalidValue: category = .ruleHadInvalidValue previousFrameID = nil if let command = rule as? BuildKey.Command, let task = lookupTask(TaskIdentifier(rawValue: command.name)), task.alwaysExecuteTask { - description = "\(descriptionForBuildKey(rule)) is configured to run in every incremental build" + description = "\(descriptionForBuildKey(rule)) was configured to run in every incremental build" } else if rule is BuildKey.Command || rule is BuildKey.CustomTask { - description = "\(descriptionForBuildKey(rule)) did not have up-to-date outputs" + description = "outputs of \(descriptionForBuildKey(rule)) were missing or modified" } else { description = "\(descriptionForBuildKey(rule)) changed" } @@ -2361,7 +2361,7 @@ internal final class OperationSystemAdaptor: SWBLLBuild.BuildSystemDelegate, Act if isTriggerNode(rule), let mutatedNodeDescription = descriptionOfInputMutatedByBuildKey(inputRule) { description = "\(descriptionForBuildKey(inputRule)) mutated \(mutatedNodeDescription)" } else { - description = "\(inputNounPhraseForBuildKey(inputRule)) of \(descriptionForBuildKey(rule)) \(rebuiltVerbPhraseForBuildKey(inputRule))" + description = "\(inputNounPhraseForBuildKey(inputRule)) \(descriptionForBuildKey(rule)) \(rebuiltVerbPhraseForBuildKey(inputRule))" } previousFrameID = previousFrameIdentifier } else { @@ -2370,7 +2370,7 @@ internal final class OperationSystemAdaptor: SWBLLBuild.BuildSystemDelegate, Act } case .forced: category = .ruleForced - description = "\(descriptionForBuildKey(rule)) was forced to run" + description = "\(descriptionForBuildKey(rule)) was forced to run to break a cycle in the build graph" previousFrameID = nil @unknown default: category = .none diff --git a/Sources/SwiftBuild/SWBBuildOperationBacktraceFrame.swift b/Sources/SwiftBuild/SWBBuildOperationBacktraceFrame.swift index 7bb1fce4..cb0109d4 100644 --- a/Sources/SwiftBuild/SWBBuildOperationBacktraceFrame.swift +++ b/Sources/SwiftBuild/SWBBuildOperationBacktraceFrame.swift @@ -193,4 +193,15 @@ public struct SWBTaskBacktrace { } self.frames = frames } + + public func renderTextualRepresentation() -> String { + var textualBacktrace: String = "" + for (frameNumber, frame) in frames.enumerated() { + guard frame.category.isUserFacing else { + continue + } + textualBacktrace += "#\(frameNumber): \(frame.description)\n" + } + return textualBacktrace + } } diff --git a/Sources/SwiftBuildTestSupport/TaskBacktraces.swift b/Sources/SwiftBuildTestSupport/TaskBacktraces.swift index 423937e6..e0f196fa 100644 --- a/Sources/SwiftBuildTestSupport/TaskBacktraces.swift +++ b/Sources/SwiftBuildTestSupport/TaskBacktraces.swift @@ -80,6 +80,14 @@ extension BuildOperationTester.BuildResults { } } } + + package func checkTextualBacktrace(_ task: Task, _ expected: String, sourceLocation: SourceLocation = #_sourceLocation) { + if let frameID = getBacktraceID(task, sourceLocation: sourceLocation), let backtrace = reconstructBacktrace(for: frameID) { + #expect(backtrace.renderTextualRepresentation() == expected, sourceLocation: sourceLocation) + } else { + // already recorded an issue + } + } } extension BuildOperationTester { diff --git a/Tests/SWBBuildSystemTests/BuildBacktraceTests.swift b/Tests/SWBBuildSystemTests/BuildBacktraceTests.swift index a6cbc12e..1e4dd699 100644 --- a/Tests/SWBBuildSystemTests/BuildBacktraceTests.swift +++ b/Tests/SWBBuildSystemTests/BuildBacktraceTests.swift @@ -121,11 +121,11 @@ fileprivate struct BuildBacktraceTests: CoreBasedTests { results.checkTask(.matchTargetName("TargetBar"), .matchRuleType("Ld")) { task in results.checkBacktrace(task, [ "", - "", + "", "", - "", + "", "", - "", + "", "", "" ]) @@ -139,8 +139,8 @@ fileprivate struct BuildBacktraceTests: CoreBasedTests { results.checkTask(.matchTargetName("TargetFoo"), .matchRuleType("CompileC")) { task in results.checkBacktrace(task, [ "", - "", - "" + "", + "" ]) } if tester.fs.fileSystemMode == .checksumOnly { @@ -155,10 +155,10 @@ fileprivate struct BuildBacktraceTests: CoreBasedTests { results.checkTask(.matchTargetName("TargetBar"), .matchRuleType("Ld")) { task in results.checkBacktrace(task, [ "", - "", + "", "", - "", - "" + "", + "" ]) } } @@ -320,7 +320,7 @@ fileprivate struct BuildBacktraceTests: CoreBasedTests { results.checkNoDiagnostics() results.checkTask(.matchTargetName("TargetFoo"), .matchRuleType("CompileC")) { task in results.checkBacktrace(task, [ - "", + "", ]) } } @@ -374,7 +374,7 @@ fileprivate struct BuildBacktraceTests: CoreBasedTests { results.checkNoDiagnostics() results.checkTask(.matchTargetName("TargetFoo"), .matchRuleType("PhaseScriptExecution")) { task in results.checkBacktrace(task, [ - "" + "" ]) } } @@ -471,4 +471,107 @@ fileprivate struct BuildBacktraceTests: CoreBasedTests { } } } + + @Test(.requireSDKs(.macOS)) + func backtraceTextRendering() async throws { + try await withTemporaryDirectory { tmpDirPath async throws -> Void in + let testWorkspace = TestWorkspace( + "Test", + sourceRoot: tmpDirPath.join("Test"), + projects: [ + TestProject( + "aProject", + groupTree: TestGroup( + "Sources", + path: "Sources", + children: [ + TestFile("foo.c"), + TestFile("bar.c"), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)", + ]) + ], + targets: [ + TestStandardTarget( + "TargetFoo", + type: .framework, + buildPhases: [ + TestSourcesBuildPhase([ + "foo.c", + ]), + ]), + TestStandardTarget( + "TargetBar", + type: .framework, + buildPhases: [ + TestSourcesBuildPhase([ + "bar.c", + ]), + TestFrameworksBuildPhase([ + "TargetFoo.framework" + ]) + ], dependencies: ["TargetFoo"]), + ]) + ]) + + let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false, fileSystem: localFS) + let parameters = BuildParameters(configuration: "Debug") + let buildRequest = BuildRequest(parameters: parameters, buildTargets: tester.workspace.projects[0].targets.map({ BuildRequest.BuildTargetInfo(parameters: parameters, target: $0) }), dependencyScope: .workspace, continueBuildingAfterErrors: true, useParallelTargets: true, useImplicitDependencies: false, useDryRun: false) + let SRCROOT = testWorkspace.sourceRoot.join("aProject") + + // Create the source files. + try await tester.fs.writeFileContents(SRCROOT.join("Sources/foo.c")) { file in + file <<< + """ + int foo(void) { + return 1; + } + """ + } + try await tester.fs.writeFileContents(SRCROOT.join("Sources/bar.c")) { file in + file <<< + """ + int bar(void) { + return 2; + } + """ + } + + try await tester.checkBuild(runDestination: .macOS, buildRequest: buildRequest, persistent: true) { results in + results.checkNoDiagnostics() + } + + try await tester.checkNullBuild(runDestination: .macOS, buildRequest: buildRequest, persistent: true) + + try await tester.fs.writeFileContents(SRCROOT.join("Sources/foo.c")) { file in + file <<< + """ + int foo2(void) { + return 42; + } + """ + } + + try await tester.checkBuild(runDestination: .macOS, buildRequest: buildRequest, persistent: true) { results in + results.checkNoDiagnostics() + results.checkTask(.matchTargetName("TargetBar"), .matchRuleType("Ld")) { task in + results.checkTextualBacktrace(task, """ + #0: an input of 'Link TargetBar (x86_64)' changed + #1: the task producing file '\(SRCROOT.str)/build/EagerLinkingTBDs/Debug/TargetFoo.framework/Versions/A/TargetFoo.tbd' ran + #2: an input of 'Generate TBD TargetFoo' changed + #3: the task producing file '\(SRCROOT.str)/build/Debug/TargetFoo.framework/Versions/A/TargetFoo' ran + #4: an input of 'Link TargetFoo (x86_64)' changed + #5: the task producing file '\(SRCROOT.str)/build/aProject.build/Debug/TargetFoo.build/Objects-normal/x86_64/foo.o' ran + #6: an input of 'Compile foo.c (x86_64)' changed + #7: file '\(SRCROOT.str)/Sources/foo.c' changed + + """) + } + } + } + } }