From d3e1c2e79078cb215f19cd18d90332affb51ce04 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 5 Aug 2025 17:58:38 -0500 Subject: [PATCH 1/6] Detect and use RIDs in project build reporting --- .../NodeStatus_SizeChange_Tests.cs | 2 +- .../NodeStatus_Transition_Tests.cs | 14 +- .../Logging/TerminalLogger/TerminalLogger.cs | 259 ++++++++++++------ .../TerminalLogger/TerminalNodeStatus.cs | 35 ++- .../TerminalLogger/TerminalNodesFrame.cs | 8 +- .../TerminalLogger/TerminalProjectInfo.cs | 43 ++- src/Build/Resources/Strings.resx | 26 ++ src/Build/Resources/xlf/Strings.cs.xlf | 28 ++ src/Build/Resources/xlf/Strings.de.xlf | 28 ++ src/Build/Resources/xlf/Strings.es.xlf | 28 ++ src/Build/Resources/xlf/Strings.fr.xlf | 28 ++ src/Build/Resources/xlf/Strings.it.xlf | 28 ++ src/Build/Resources/xlf/Strings.ja.xlf | 28 ++ src/Build/Resources/xlf/Strings.ko.xlf | 28 ++ src/Build/Resources/xlf/Strings.pl.xlf | 28 ++ src/Build/Resources/xlf/Strings.pt-BR.xlf | 28 ++ src/Build/Resources/xlf/Strings.ru.xlf | 28 ++ src/Build/Resources/xlf/Strings.tr.xlf | 28 ++ src/Build/Resources/xlf/Strings.zh-Hans.xlf | 28 ++ src/Build/Resources/xlf/Strings.zh-Hant.xlf | 28 ++ 20 files changed, 631 insertions(+), 120 deletions(-) diff --git a/src/Build.UnitTests/NodeStatus_SizeChange_Tests.cs b/src/Build.UnitTests/NodeStatus_SizeChange_Tests.cs index bbe391ce360..9c697ba98bb 100644 --- a/src/Build.UnitTests/NodeStatus_SizeChange_Tests.cs +++ b/src/Build.UnitTests/NodeStatus_SizeChange_Tests.cs @@ -17,7 +17,7 @@ namespace Microsoft.Build.CommandLine.UnitTests; [UsesVerify] public class NodeStatus_SizeChange_Tests : IDisposable { - private readonly TerminalNodeStatus _status = new("Namespace.Project", "TargetFramework", "Target", new MockStopwatch()); + private readonly TerminalNodeStatus _status = new("Namespace.Project", "TargetFramework", null, "Target", new MockStopwatch()); private CultureInfo _currentCulture; public NodeStatus_SizeChange_Tests() diff --git a/src/Build.UnitTests/NodeStatus_Transition_Tests.cs b/src/Build.UnitTests/NodeStatus_Transition_Tests.cs index 62cc3557215..2a720f90819 100644 --- a/src/Build.UnitTests/NodeStatus_Transition_Tests.cs +++ b/src/Build.UnitTests/NodeStatus_Transition_Tests.cs @@ -29,7 +29,7 @@ public void NodeStatusTargetThrowsForInputWithAnsi() { #if DEBUG // This is testing a Debug.Assert, which won't throw in Release mode. - Func newNodeStatus = () => new TerminalNodeStatus("project", "tfm", AnsiCodes.Colorize("colorized target", TerminalColor.Green), new MockStopwatch()); + Func newNodeStatus = () => new TerminalNodeStatus("project", "tfm", "rid", AnsiCodes.Colorize("colorized target", TerminalColor.Green), new MockStopwatch()); newNodeStatus.ShouldThrow().Message.ShouldContain("Target should not contain any escape codes, if you want to colorize target use the other constructor."); #endif } @@ -39,10 +39,10 @@ public async Task NodeTargetChanges() { var rendered = Animate( [ - new("Namespace.Project", "TargetFramework", "Build", new MockStopwatch()) + new("Namespace.Project", "TargetFramework", null, "Build", new MockStopwatch()) ], [ - new("Namespace.Project", "TargetFramework", "Testing", new MockStopwatch()) + new("Namespace.Project", "TargetFramework", null, "Testing", new MockStopwatch()) ]); await VerifyReplay(rendered); @@ -54,7 +54,7 @@ public async Task NodeTargetUpdatesTime() // This test look like there is no change between the frames, but we ask the stopwatch for time they will increase the number. // We need this because animations check that NodeStatus reference is the same. // And we cannot use MockStopwatch because we don't know when to call Tick on them, and if we do it right away, the time will update in "both" nodes. - TerminalNodeStatus node = new("Namespace.Project", "TargetFramework", "Build", new TickingStopwatch()); + TerminalNodeStatus node = new("Namespace.Project", "TargetFramework", null, "Build", new TickingStopwatch()); var rendered = Animate( [ node, @@ -71,10 +71,10 @@ public async Task NodeTargetChangesToColoredTarget() { var rendered = Animate( [ - new("Namespace.Project", "TargetFramework", "Testing", new MockStopwatch()) + new("Namespace.Project", "TargetFramework", null, "Testing", new MockStopwatch()) ], [ - new("Namespace.Project", "TargetFramework", TerminalColor.Red, "failed", "MyTestName1", new MockStopwatch()) + new("Namespace.Project", "TargetFramework", null, TerminalColor.Red, "failed", "MyTestName1", new MockStopwatch()) ]); await VerifyReplay(rendered); @@ -86,7 +86,7 @@ public async Task NodeWithColoredTargetUpdatesTime() // This test look like there is no change between the frames, but we ask the stopwatch for time they will increase the number. // We need this because animations check that NodeStatus reference is the same. // And we cannot use MockStopwatch because we don't know when to call Tick on them, and if we do it right away, the time will update in "both" nodes. - TerminalNodeStatus node = new("Namespace.Project", "TargetFramework", TerminalColor.Green, "passed", "MyTestName1", new TickingStopwatch()); + TerminalNodeStatus node = new("Namespace.Project", "TargetFramework", null, TerminalColor.Green, "passed", "MyTestName1", new TickingStopwatch()); var rendered = Animate( [ node, diff --git a/src/Build/Logging/TerminalLogger/TerminalLogger.cs b/src/Build/Logging/TerminalLogger/TerminalLogger.cs index f84d5d66ffb..4d9593ae623 100644 --- a/src/Build/Logging/TerminalLogger/TerminalLogger.cs +++ b/src/Build/Logging/TerminalLogger/TerminalLogger.cs @@ -49,7 +49,19 @@ internal record struct ProjectContext(int Id) { public ProjectContext(BuildEventContext context) : this(context.ProjectContextId) - { } + { + } + } + + /// + /// A wrapper over the evaluation context ID passed to us in logger events. + /// + internal record struct EvalContext(int Id) + { + public EvalContext(BuildEventContext context) + : this(context.EvaluationId) + { + } } private readonly record struct TestSummary(int Total, int Passed, int Skipped, int Failed); @@ -64,6 +76,7 @@ public ProjectContext(BuildEventContext context) internal const string TripleIndentation = $"{Indentation}{Indentation}{Indentation}"; internal const TerminalColor TargetFrameworkColor = TerminalColor.Cyan; + internal const TerminalColor RuntimeIdentifierColor = TerminalColor.Magenta; internal Func? CreateStopwatch = null; @@ -90,6 +103,8 @@ public ProjectContext(BuildEventContext context) /// private readonly Dictionary _projects = new(); + private readonly Dictionary _evals = new(); + /// /// Tracks the work currently being done by build nodes. Null means the node is not doing any work worth reporting. /// @@ -596,9 +611,16 @@ private void RenderBuildSummary() private void StatusEventRaised(object sender, BuildStatusEventArgs e) { - if (e is BuildCanceledEventArgs buildCanceledEventArgs) + switch (e) { - RenderImmediateMessage(e.Message!); + case BuildCanceledEventArgs cancelEvent: + RenderImmediateMessage(cancelEvent.Message!); + break; + case ProjectEvaluationStartedEventArgs _evalStart: + break; + case ProjectEvaluationFinishedEventArgs evalFinish: + CaptureEvalContext(evalFinish); + break; } } @@ -607,28 +629,34 @@ private void StatusEventRaised(object sender, BuildStatusEventArgs e) /// private void ProjectStarted(object sender, ProjectStartedEventArgs e) { - var buildEventContext = e.BuildEventContext; - if (buildEventContext is null) + if (e.BuildEventContext is null) { return; } - ProjectContext c = new ProjectContext(buildEventContext); + ProjectContext c = new(e.BuildEventContext); if (_restoreContext is null) { - if (e.GlobalProperties?.TryGetValue("TargetFramework", out string? targetFramework) != true) + EvalContext evalContext = new(e.BuildEventContext); + string? targetFramework = null; + string? runtimeIdentifier = null; + if (_evals.TryGetValue(evalContext, out EvalProjectInfo evalInfo)) { - targetFramework = null; + targetFramework = evalInfo.TargetFramework; + runtimeIdentifier = evalInfo.RuntimeIdentifier; } - _projects[c] = new(e.ProjectFile!, targetFramework, CreateStopwatch?.Invoke()); + System.Diagnostics.Debug.Assert(evalInfo != default, "EvalProjectInfo should have been captured before ProjectStarted"); + + TerminalProjectInfo projectInfo = new(c, evalInfo, CreateStopwatch?.Invoke()); + _projects[c] = projectInfo; // First ever restore in the build is starting. if (e.TargetNames == "Restore" && !_restoreFinished) { _restoreContext = c; - int nodeIndex = NodeIndexForContext(buildEventContext); - _nodes[nodeIndex] = new TerminalNodeStatus(e.ProjectFile!, null, "Restore", _projects[c].Stopwatch); + int nodeIndex = NodeIndexForContext(e.BuildEventContext); + _nodes[nodeIndex] = new TerminalNodeStatus(e.ProjectFile!, targetFramework, runtimeIdentifier, "Restore", _projects[c].Stopwatch); } } } @@ -712,65 +740,10 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e) Terminal.Write(projectFinishedHeader); // Print the output path as a link if we have it. - if (outputPath is not null) + if (outputPath is { } outputPathSpan) { - ReadOnlySpan outputPathSpan = outputPath.Value.Span; - ReadOnlySpan url = outputPathSpan; - try - { - // If possible, make the link point to the containing directory of the output. - url = Path.GetDirectoryName(url); - } - catch - { - // Ignore any GetDirectoryName exceptions. - } - - // Generates file:// schema url string which is better handled by various Terminal clients than raw folder name. - string urlString = url.ToString(); - if (Uri.TryCreate(urlString, UriKind.Absolute, out Uri? uri)) - { - // url.ToString() un-escapes the URL which is needed for our case file:// - // but not valid for http:// - urlString = uri.ToString(); - } - - // now we compute the path to show the user for this project. - // some options: - // * the raw, full output path from the MSBuild logic (OutputPath property) - // * the output path relative to the initial working directory, if it is under it - // * the output path relative to the source root, if it is under it - - // full path fallback - var projectDisplayPathSpan = outputPathSpan; - var workingDirectorySpan = _initialWorkingDirectory.AsSpan(); - // under working dir case - if (outputPathSpan.StartsWith(workingDirectorySpan, FileUtilities.PathComparison)) - { - if (outputPathSpan.Length > workingDirectorySpan.Length - && (outputPathSpan[workingDirectorySpan.Length] == Path.DirectorySeparatorChar - || outputPathSpan[workingDirectorySpan.Length] == Path.AltDirectorySeparatorChar)) - { - projectDisplayPathSpan = outputPathSpan.Slice(workingDirectorySpan.Length + 1); - } - } - // under source root case - else if (project.SourceRoot is { Span: var sourceRootSpan } ) - { - var relativePathFromWorkingDirToSourceRoot = Path.GetRelativePath(workingDirectorySpan.ToString(), sourceRootSpan.ToString()).AsSpan(); - if (outputPathSpan.StartsWith(sourceRootSpan, FileUtilities.PathComparison)) - { - if (outputPathSpan.Length > sourceRootSpan.Length - // offsets are -1 here compared to above for ***reasons*** - && (outputPathSpan[sourceRootSpan.Length - 1] == Path.DirectorySeparatorChar - || outputPathSpan[sourceRootSpan.Length - 1] == Path.AltDirectorySeparatorChar)) - { - projectDisplayPathSpan = Path.Combine(relativePathFromWorkingDirToSourceRoot.ToString(), outputPathSpan.Slice(sourceRootSpan.Length).ToString()).AsSpan(); - } - } - } - - Terminal.WriteLine(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("ProjectFinished_OutputPath", CreateLink(uri, projectDisplayPathSpan.ToString()))); + (var projectDisplayPath, var urlLink) = DetermineOutputPathToRender(outputPathSpan, _initialWorkingDirectory.AsMemory(), project.SourceRoot); + Terminal.WriteLine(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("ProjectFinished_OutputPath", CreateLink(urlLink, projectDisplayPath.ToString()))); } else { @@ -799,6 +772,95 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e) } } } + + private void CaptureEvalContext(ProjectEvaluationFinishedEventArgs evalFinish) + { + var buildEventContext = evalFinish.BuildEventContext; + if (buildEventContext is null) + { + return; + } + + EvalContext c = new(buildEventContext); + + if (!_evals.TryGetValue(c, out EvalProjectInfo _)) + { + string? tfm = null; + string? rid = null; + foreach (var property in evalFinish.EnumerateProperties()) + { + if (tfm is not null && rid is not null) + { + // We already have both properties, no need to continue. + break; + } + switch (property.Name) + { + case "TargetFramework": + tfm = property.Value; + break; + case "RuntimeIdentifier": + rid = property.Value; + break; + } + } + var evalInfo = new EvalProjectInfo(c, evalFinish.ProjectFile!, tfm, rid); + _evals[c] = evalInfo; + } + } + + private static (string outputPathToRender, Uri? linkToAssign) DetermineOutputPathToRender(ReadOnlyMemory outputPath, ReadOnlyMemory workingDir, ReadOnlyMemory? sourceRoot) + { + ReadOnlySpan outputPathSpan = outputPath.Span; + + // Generates file:// schema url string which is better handled by various Terminal clients than raw folder name. +#if NET + Uri.TryCreate(new(Path.GetDirectoryName(outputPathSpan)), UriKind.Absolute, out Uri? uri); +#else + Uri.TryCreate(Path.GetDirectoryName(outputPathSpan.ToString()), UriKind.Absolute, out Uri? uri); +#endif + + // now we compute the path to show the user for this project. + // some options: + // * the raw, full output path from the MSBuild logic (OutputPath property) + // * the output path relative to the initial working directory, if it is under it + // * the output path relative to the source root, if it is under it + + // full path fallback + var projectDisplayPathSpan = outputPathSpan; + var workingDirectorySpan = workingDir.Span; + // under working dir case + if (outputPathSpan.StartsWith(workingDirectorySpan, FileUtilities.PathComparison)) + { + if (outputPathSpan.Length > workingDirectorySpan.Length + && (outputPathSpan[workingDirectorySpan.Length] == Path.DirectorySeparatorChar + || outputPathSpan[workingDirectorySpan.Length] == Path.AltDirectorySeparatorChar)) + { + projectDisplayPathSpan = outputPathSpan.Slice(workingDirectorySpan.Length + 1); + } + } + // under source root case + else if (sourceRoot is { Span: var sourceRootSpan }) + { + var relativePathFromWorkingDirToSourceRoot = Path.GetRelativePath(workingDirectorySpan.ToString(), sourceRootSpan.ToString()).AsSpan(); + if (outputPathSpan.StartsWith(sourceRootSpan, FileUtilities.PathComparison)) + { + if (outputPathSpan.Length > sourceRootSpan.Length + // offsets are -1 here compared to above for ***reasons*** + && (outputPathSpan[sourceRootSpan.Length - 1] == Path.DirectorySeparatorChar + || outputPathSpan[sourceRootSpan.Length - 1] == Path.AltDirectorySeparatorChar)) + { + + projectDisplayPathSpan = Path.Combine(relativePathFromWorkingDirToSourceRoot.ToString(), outputPathSpan.Slice(sourceRootSpan.Length).ToString()).AsSpan(); + } + } + } +#if NET + return (new(projectDisplayPathSpan), uri); +#else + return (projectDisplayPathSpan.ToString(), uri); +#endif + } private static string? CreateLink(Uri? uri, string? linkText) => (uri, linkText) switch @@ -810,31 +872,52 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e) private static string GetProjectFinishedHeader(TerminalProjectInfo project, string buildResult, string duration) { - string projectFile = project.File is not null ? - Path.GetFileNameWithoutExtension(project.File) : + string projectFile = project.ProjectFile is not null ? + Path.GetFileNameWithoutExtension(project.ProjectFile) : string.Empty; - if (string.IsNullOrEmpty(project.TargetFramework)) + return (project.TargetFramework, project.RuntimeIdentifier, project.IsTestProject) switch { - string resourceName = project.IsTestProject ? "TestProjectFinished_NoTF" : "ProjectFinished_NoTF"; - - return ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword(resourceName, + (string tfm, null, true) => ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TestProjectFinished_WithTF", Indentation, projectFile, + AnsiCodes.Colorize(tfm, TargetFrameworkColor), buildResult, - duration); - } - else - { - string resourceName = project.IsTestProject ? "TestProjectFinished_WithTF" : "ProjectFinished_WithTF"; - - return ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword(resourceName, + duration), + (string tfm, null, false) => ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("ProjectFinished_WithTF", Indentation, projectFile, - AnsiCodes.Colorize(project.TargetFramework, TargetFrameworkColor), + AnsiCodes.Colorize(tfm, TargetFrameworkColor), buildResult, - duration); - } + duration), + (null, string rid, true) => ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TestProjectFinished_WithTF", + Indentation, + projectFile, + AnsiCodes.Colorize(rid, RuntimeIdentifierColor), + buildResult, + duration), + (null, string rid, false) => ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("ProjectFinished_WithTF", + Indentation, + projectFile, + AnsiCodes.Colorize(rid, RuntimeIdentifierColor), + buildResult, + duration), + (string tfm, string rid, true) => ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TestProjectFinished_WithTFAndRID", + Indentation, + projectFile, + AnsiCodes.Colorize(tfm, TargetFrameworkColor), + AnsiCodes.Colorize(rid, RuntimeIdentifierColor), + buildResult, + duration), + (string tfm, string rid, false) => ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("ProjectFinished_WithTFAndRID", + Indentation, + projectFile, + AnsiCodes.Colorize(tfm, TargetFrameworkColor), + AnsiCodes.Colorize(rid, RuntimeIdentifierColor), + buildResult, + duration), + (null, null, _) => "" // this is a weird pattern - what should we do? + }; } /// @@ -869,7 +952,7 @@ private void TargetStarted(object sender, TargetStartedEventArgs e) project.IsTestProject = true; } - TerminalNodeStatus nodeStatus = new(projectFile, project.TargetFramework, targetName, project.Stopwatch); + TerminalNodeStatus nodeStatus = new(projectFile, project.TargetFramework, project.RuntimeIdentifier, targetName, project.Stopwatch); UpdateNodeStatus(buildEventContext, nodeStatus); } } @@ -1008,7 +1091,7 @@ private void MessageRaised(object sender, BuildMessageEventArgs e) var indicator = extendedMessage.ExtendedMetadata!["localizedResult"]!; var displayName = extendedMessage.ExtendedMetadata!["displayName"]!; - var status = new TerminalNodeStatus(node.Project, node.TargetFramework, TerminalColor.Green, indicator, displayName, project.Stopwatch); + var status = new TerminalNodeStatus(node.Project, node.TargetFramework, node.RuntimeIdentifier, TerminalColor.Green, indicator, displayName, project.Stopwatch); UpdateNodeStatus(buildEventContext, status); break; } @@ -1018,7 +1101,7 @@ private void MessageRaised(object sender, BuildMessageEventArgs e) var indicator = extendedMessage.ExtendedMetadata!["localizedResult"]!; var displayName = extendedMessage.ExtendedMetadata!["displayName"]!; - var status = new TerminalNodeStatus(node.Project, node.TargetFramework, TerminalColor.Yellow, indicator, displayName, project.Stopwatch); + var status = new TerminalNodeStatus(node.Project, node.TargetFramework, node.RuntimeIdentifier, TerminalColor.Yellow, indicator, displayName, project.Stopwatch); UpdateNodeStatus(buildEventContext, status); break; } diff --git a/src/Build/Logging/TerminalLogger/TerminalNodeStatus.cs b/src/Build/Logging/TerminalLogger/TerminalNodeStatus.cs index 22eb0157257..dd14f8586ee 100644 --- a/src/Build/Logging/TerminalLogger/TerminalNodeStatus.cs +++ b/src/Build/Logging/TerminalLogger/TerminalNodeStatus.cs @@ -15,6 +15,7 @@ internal class TerminalNodeStatus { public string Project { get; } public string? TargetFramework { get; } + public string? RuntimeIdentifier { get; } public TerminalColor TargetPrefixColor { get; } = TerminalColor.Default; public string? TargetPrefix { get; } public string Target { get; } @@ -25,9 +26,10 @@ internal class TerminalNodeStatus /// /// The project that is written on left side. /// Target framework that is colorized and written on left side after project. + /// Runtime identifier that is colorized and written on left side after target framework. /// The currently running work, usually the currently running target. Written on right. /// Duration of the current step. Written on right after target. - public TerminalNodeStatus(string project, string? targetFramework, string target, StopwatchAbstraction stopwatch) + public TerminalNodeStatus(string project, string? targetFramework, string? runtimeIdentifier, string target, StopwatchAbstraction stopwatch) { #if DEBUG if (target.Contains("\x1B")) @@ -37,6 +39,7 @@ public TerminalNodeStatus(string project, string? targetFramework, string target #endif Project = project; TargetFramework = targetFramework; + RuntimeIdentifier = runtimeIdentifier; Target = target; Stopwatch = stopwatch; } @@ -46,12 +49,13 @@ public TerminalNodeStatus(string project, string? targetFramework, string target /// /// The project that is written on left side. /// Target framework that is colorized and written on left side after project. + /// Runtime identifier that is colorized and written on left side after target framework. /// Color for the status of the currently running work written on right. /// Colorized status for the currently running work, written on right, before target, and separated by 1 space from it. /// The currently running work, usually the currently runnig target. Written on right. /// Duration of the current step. Written on right after target. - public TerminalNodeStatus(string project, string? targetFramework, TerminalColor targetPrefixColor, string targetPrefix, string target, StopwatchAbstraction stopwatch) - : this(project, targetFramework, target, stopwatch) + public TerminalNodeStatus(string project, string? targetFramework, string? runtimeIdentifier, TerminalColor targetPrefixColor, string targetPrefix, string target, StopwatchAbstraction stopwatch) + : this(project, targetFramework, runtimeIdentifier, target, stopwatch) { TargetPrefixColor = targetPrefixColor; TargetPrefix = targetPrefix; @@ -60,18 +64,23 @@ public TerminalNodeStatus(string project, string? targetFramework, TerminalColor /// /// Equality is based on the project, target framework, and target, but NOT the elapsed time. /// - public override bool Equals(object? obj) => - obj is TerminalNodeStatus status && - Project == status.Project && - TargetFramework == status.TargetFramework && - Target == status.Target && - TargetPrefixColor == status.TargetPrefixColor && - TargetPrefix == status.TargetPrefix; + public virtual bool Equals(TerminalNodeStatus? other) => + other is not null && + Project == other.Project && + TargetFramework == other.TargetFramework && + RuntimeIdentifier == other.RuntimeIdentifier && + Target == other.Target && + TargetPrefixColor == other.TargetPrefixColor && + TargetPrefix == other.TargetPrefix; public override string ToString() => - string.IsNullOrEmpty(TargetFramework) ? - $"{TerminalLogger.Indentation}{Project} {Target} ({Stopwatch.ElapsedSeconds:F1}s)" : - $"{TerminalLogger.Indentation}{Project} {AnsiCodes.Colorize(TargetFramework, TerminalLogger.TargetFrameworkColor)} {Target} ({Stopwatch.ElapsedSeconds:F1}s)"; + (TargetFramework, RuntimeIdentifier) switch + { + (null, null) => $"{TerminalLogger.Indentation}{Project} {Target} ({Stopwatch.ElapsedSeconds:F1}s)", + (null, _) => $"{TerminalLogger.Indentation}{Project} {AnsiCodes.Colorize(RuntimeIdentifier, TerminalLogger.RuntimeIdentifierColor)} {Target} ({Stopwatch.ElapsedSeconds:F1}s)", + (_, null) => $"{TerminalLogger.Indentation}{Project} {AnsiCodes.Colorize(TargetFramework, TerminalLogger.TargetFrameworkColor)} {Target} ({Stopwatch.ElapsedSeconds:F1}s)", + _ => $"{TerminalLogger.Indentation}{Project} {AnsiCodes.Colorize(TargetFramework, TerminalLogger.TargetFrameworkColor)} {AnsiCodes.Colorize(RuntimeIdentifier, TerminalLogger.RuntimeIdentifierColor)} {Target} ({Stopwatch.ElapsedSeconds:F1}s)" + }; public override int GetHashCode() { diff --git a/src/Build/Logging/TerminalLogger/TerminalNodesFrame.cs b/src/Build/Logging/TerminalLogger/TerminalNodesFrame.cs index 93f4f2dee9b..1ea8fc94c1b 100644 --- a/src/Build/Logging/TerminalLogger/TerminalNodesFrame.cs +++ b/src/Build/Logging/TerminalLogger/TerminalNodesFrame.cs @@ -51,6 +51,7 @@ internal ReadOnlySpan RenderNodeStatus(int i) string project = status.Project; string? targetFramework = status.TargetFramework; + string? runtimeIdentifier = status.RuntimeIdentifier; string target = status.Target; string? targetPrefix = status.TargetPrefix; TerminalColor targetPrefixColor = status.TargetPrefixColor; @@ -60,7 +61,7 @@ internal ReadOnlySpan RenderNodeStatus(int i) ? targetPrefix!.Length + 1 + target.Length : target.Length; - int renderedWidth = Length(durationString, project, targetFramework, targetWithoutAnsiLength); + int renderedWidth = Length(durationString, project, targetFramework, runtimeIdentifier, targetWithoutAnsiLength); if (renderedWidth > Width) { @@ -82,12 +83,13 @@ internal ReadOnlySpan RenderNodeStatus(int i) } var renderedTarget = !string.IsNullOrWhiteSpace(targetPrefix) ? $"{AnsiCodes.Colorize(targetPrefix, targetPrefixColor)} {target}" : target; - return $"{TerminalLogger.Indentation}{project}{(targetFramework is null ? string.Empty : " ")}{AnsiCodes.Colorize(targetFramework, TerminalLogger.TargetFrameworkColor)} {AnsiCodes.SetCursorHorizontal(MaxColumn)}{AnsiCodes.MoveCursorBackward(targetWithoutAnsiLength + durationString.Length + 1)}{renderedTarget} {durationString}".AsSpan(); + return $"{TerminalLogger.Indentation}{project}{(targetFramework is null ? string.Empty : " ")}{AnsiCodes.Colorize(targetFramework, TerminalLogger.TargetFrameworkColor)} {AnsiCodes.Colorize(runtimeIdentifier, TerminalLogger.RuntimeIdentifierColor)} {AnsiCodes.SetCursorHorizontal(MaxColumn)}{AnsiCodes.MoveCursorBackward(targetWithoutAnsiLength + durationString.Length + 1)}{renderedTarget} {durationString}".AsSpan(); - static int Length(string durationString, string project, string? targetFramework, int targetWithoutAnsiLength) => + static int Length(string durationString, string project, string? targetFramework, string? runtimeIdentifier, int targetWithoutAnsiLength) => TerminalLogger.Indentation.Length + project.Length + 1 + (targetFramework?.Length ?? -1) + 1 + + (runtimeIdentifier?.Length ?? -1) + 1 + targetWithoutAnsiLength + 1 + durationString.Length; } diff --git a/src/Build/Logging/TerminalLogger/TerminalProjectInfo.cs b/src/Build/Logging/TerminalLogger/TerminalProjectInfo.cs index 54c6c5cfb7f..b69c2c515e9 100644 --- a/src/Build/Logging/TerminalLogger/TerminalProjectInfo.cs +++ b/src/Build/Logging/TerminalLogger/TerminalProjectInfo.cs @@ -7,6 +7,18 @@ namespace Microsoft.Build.Logging; +/// +/// A struct containing relevant evaluation-time data that may not be knowable just from ProjectStart events. +/// +/// +/// +/// +/// +internal record struct EvalProjectInfo(TerminalLogger.EvalContext context, string? ProjectFile, string? TargetFramework, string? RuntimeIdentifier) +{ + public readonly int Id => context.Id; +} + /// /// Represents a project being built. /// @@ -15,15 +27,15 @@ internal sealed class TerminalProjectInfo private List? _buildMessages; /// - /// Initialized a new with the given . + /// Initialized a new with the given . /// - /// The full path to the project file. - /// The target framework of the project or null if not multi-targeting. + /// The ProjectContext of this project execution. + /// A subset of the interesting eval-time data for this running project /// A stopwatch to time the build of the project. - public TerminalProjectInfo(string projectFile, string? targetFramework, StopwatchAbstraction? stopwatch) + public TerminalProjectInfo(TerminalLogger.ProjectContext context, EvalProjectInfo evalInfo, StopwatchAbstraction? stopwatch) { - File = projectFile; - TargetFramework = targetFramework; + _evalInfo = evalInfo; + _context = context; if (stopwatch is not null) { @@ -36,7 +48,15 @@ public TerminalProjectInfo(string projectFile, string? targetFramework, Stopwatc } } - public string File { get; } + /// + /// The int value of the ProjectContext id of this project execution. + /// + public int Id => _context.Id; + + /// + /// The full path to the project file. + /// + public string? ProjectFile => _evalInfo.ProjectFile; /// /// A stopwatch to time the build of the project. @@ -56,7 +76,14 @@ public TerminalProjectInfo(string projectFile, string? targetFramework, Stopwatc /// /// The target framework of the project or null if not multi-targeting. /// - public string? TargetFramework { get; } + public string? TargetFramework => _evalInfo.TargetFramework; + + /// + /// The runtime identifier of the project or null if platform-agnostic. + /// + public string? RuntimeIdentifier => _evalInfo.RuntimeIdentifier; + private readonly TerminalLogger.ProjectContext _context; + private readonly EvalProjectInfo _evalInfo; /// /// True when the project has run target with name "_TestRunStart" defined in . diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 6c511815b91..83dd0ac7d07 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -2309,6 +2309,19 @@ Utilization: {0} Average Utilization: {1:###.0} 's' should reflect the localized abbreviation for seconds + + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds + + failed with {0} error(s) @@ -2368,6 +2381,19 @@ Utilization: {0} Average Utilization: {1:###.0} 's' should reflect the localized abbreviation for seconds + + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds + + → {0} diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index 16ffd9066c8..e7001cbe918 100644 --- a/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/Build/Resources/xlf/Strings.cs.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Chyby: {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds diff --git a/src/Build/Resources/xlf/Strings.de.xlf b/src/Build/Resources/xlf/Strings.de.xlf index 2b92c1bee01..8e6d5a369b2 100644 --- a/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/Build/Resources/xlf/Strings.de.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Fehler: {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds diff --git a/src/Build/Resources/xlf/Strings.es.xlf b/src/Build/Resources/xlf/Strings.es.xlf index 95e81438c58..6631f253e84 100644 --- a/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/Build/Resources/xlf/Strings.es.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Errores: {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds diff --git a/src/Build/Resources/xlf/Strings.fr.xlf b/src/Build/Resources/xlf/Strings.fr.xlf index 7af7d66e1c0..1aa16dbf401 100644 --- a/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/Build/Resources/xlf/Strings.fr.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Erreurs : {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds diff --git a/src/Build/Resources/xlf/Strings.it.xlf b/src/Build/Resources/xlf/Strings.it.xlf index dfd9f31ade2..8bc4b37165a 100644 --- a/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/Build/Resources/xlf/Strings.it.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Errori: {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds diff --git a/src/Build/Resources/xlf/Strings.ja.xlf b/src/Build/Resources/xlf/Strings.ja.xlf index eee19dd00a6..64722645e1e 100644 --- a/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/Build/Resources/xlf/Strings.ja.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Errors: {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf index 313f7824028..3a10b61b3ff 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Errors: {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds diff --git a/src/Build/Resources/xlf/Strings.pl.xlf b/src/Build/Resources/xlf/Strings.pl.xlf index 3c79fc52ee2..7b2b6b45198 100644 --- a/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/Build/Resources/xlf/Strings.pl.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Błędy: {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds diff --git a/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/Build/Resources/xlf/Strings.pt-BR.xlf index d01d09dda0f..496264fd045 100644 --- a/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Erros: {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds diff --git a/src/Build/Resources/xlf/Strings.ru.xlf b/src/Build/Resources/xlf/Strings.ru.xlf index 054e526735b..f9d8b31f463 100644 --- a/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/Build/Resources/xlf/Strings.ru.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Errors: {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds diff --git a/src/Build/Resources/xlf/Strings.tr.xlf b/src/Build/Resources/xlf/Strings.tr.xlf index 4cd901a09d7..3ef1693eb1a 100644 --- a/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/Build/Resources/xlf/Strings.tr.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Hatalar: {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds diff --git a/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/Build/Resources/xlf/Strings.zh-Hans.xlf index 505b7a8735f..5a528af65b6 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Errors: {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds diff --git a/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/Build/Resources/xlf/Strings.zh-Hant.xlf index 1f787746f47..efb74f8e393 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -776,6 +776,20 @@ {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} {2} {3} {4} ({5}s) + {0}{1} {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds @@ -1011,6 +1025,20 @@ Errors: {3} {3}: BuildResult_{X} {4}: duration in seconds with 1 decimal point 's' should reflect the localized abbreviation for seconds + + + + {0}{1} test {2} {3} {4} ({5}s) + {0}{1} test {2} {3} {4} ({5}s) + + Project finished summary including target framework and runtime identifier information. + {0}: indentation - few spaces to visually indent row + {1}: project name + {2}: target framework + {3}: runtime identifier (RID) + {4}: BuildResult_{X} + {5}: duration in seconds with 1 decimal point + 's' should reflect the localized abbreviation for seconds From 934920d26e2819656be38346c2067ef48850b653 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 5 Aug 2025 20:28:46 -0500 Subject: [PATCH 2/6] Fix test failures Fix rendering project finished status when there is no TF or RID data. Fix an errant extra space when there is no RID on the project status summary. Update tests to fire Project Eval events so new invariants are upheld. --- src/Build.UnitTests/TerminalLogger_Tests.cs | 164 +++++++++++------- .../Logging/TerminalLogger/TerminalLogger.cs | 11 +- .../TerminalLogger/TerminalNodesFrame.cs | 20 ++- 3 files changed, 129 insertions(+), 66 deletions(-) diff --git a/src/Build.UnitTests/TerminalLogger_Tests.cs b/src/Build.UnitTests/TerminalLogger_Tests.cs index 52defb8b171..97989705f27 100644 --- a/src/Build.UnitTests/TerminalLogger_Tests.cs +++ b/src/Build.UnitTests/TerminalLogger_Tests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Build.CommandLine.UnitTests; @@ -170,117 +171,136 @@ public void Dispose() #region Event args helpers - private BuildEventContext MakeBuildEventContext() + /// + /// Helper function to create a BuildEventContext keyed to specific scenarios. + /// When you want to refer to the same eval properties, use the same evalId. + /// When you want to refer to the same project, use the same projectContextId. + /// By default, nodeId, evalId, projectContextId, and targetId are all set to 1. + /// + private BuildEventContext MakeBuildEventContext(int evalId = 1, int projectContextId = 1) { - return new BuildEventContext(1, 1, 1, 1); + return new BuildEventContext( + submissionId: -1, + nodeId: 1, + evaluationId: evalId, + projectInstanceId: -1, + projectContextId: projectContextId, + targetId: 1, + taskId: 1); } - private BuildStartedEventArgs MakeBuildStartedEventArgs() + private BuildStartedEventArgs MakeBuildStartedEventArgs(BuildEventContext? buildEventContext = null) { - return new BuildStartedEventArgs(null, null, _buildStartTime); + return new BuildStartedEventArgs(null, null, _buildStartTime) + { + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), + }; } - private BuildFinishedEventArgs MakeBuildFinishedEventArgs(bool succeeded) + private BuildFinishedEventArgs MakeBuildFinishedEventArgs(bool succeeded, BuildEventContext? buildEventContext = null) { - return new BuildFinishedEventArgs(null, null, succeeded, _buildFinishTime); + return new BuildFinishedEventArgs(null, null, succeeded, _buildFinishTime) + { + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), + }; } - private ProjectStartedEventArgs MakeProjectStartedEventArgs(string projectFile, string targetNames = "Build") + private ProjectStartedEventArgs MakeProjectStartedEventArgs(string projectFile, string targetNames = "Build", BuildEventContext? buildEventContext = null) { return new ProjectStartedEventArgs("", "", projectFile, targetNames, new Dictionary(), new List()) { - BuildEventContext = MakeBuildEventContext(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), }; } - private ProjectFinishedEventArgs MakeProjectFinishedEventArgs(string projectFile, bool succeeded) + private ProjectFinishedEventArgs MakeProjectFinishedEventArgs(string projectFile, bool succeeded, BuildEventContext? buildEventContext = null) { return new ProjectFinishedEventArgs(null, null, projectFile, succeeded) { - BuildEventContext = MakeBuildEventContext(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), }; } - private TargetStartedEventArgs MakeTargetStartedEventArgs(string projectFile, string targetName) + private TargetStartedEventArgs MakeTargetStartedEventArgs(string projectFile, string targetName, BuildEventContext? buildEventContext = null) { return new TargetStartedEventArgs("", "", targetName, projectFile, targetFile: projectFile, String.Empty, TargetBuiltReason.None, _targetStartTime) { - BuildEventContext = MakeBuildEventContext(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), }; } - private TargetFinishedEventArgs MakeTargetFinishedEventArgs(string projectFile, string targetName, bool succeeded) + private TargetFinishedEventArgs MakeTargetFinishedEventArgs(string projectFile, string targetName, bool succeeded, BuildEventContext? buildEventContext = null) { return new TargetFinishedEventArgs("", "", targetName, projectFile, targetFile: projectFile, succeeded) { - BuildEventContext = MakeBuildEventContext(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), }; } - private TaskStartedEventArgs MakeTaskStartedEventArgs(string projectFile, string taskName) + private TaskStartedEventArgs MakeTaskStartedEventArgs(string projectFile, string taskName, BuildEventContext? buildEventContext = null) { return new TaskStartedEventArgs("", "", projectFile, taskFile: projectFile, taskName) { - BuildEventContext = MakeBuildEventContext(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), }; } - private TaskFinishedEventArgs MakeTaskFinishedEventArgs(string projectFile, string taskName, bool succeeded) + private TaskFinishedEventArgs MakeTaskFinishedEventArgs(string projectFile, string taskName, bool succeeded, BuildEventContext? buildEventContext = null) { return new TaskFinishedEventArgs("", "", projectFile, taskFile: projectFile, taskName, succeeded) { - BuildEventContext = MakeBuildEventContext(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), }; } - private BuildWarningEventArgs MakeCopyRetryWarning(int retryCount) + private BuildWarningEventArgs MakeCopyRetryWarning(int retryCount, BuildEventContext? buildEventContext = null) { return new BuildWarningEventArgs("", "MSB3026", "directory/file", 1, 2, 3, 4, $"MSB3026: Could not copy \"sourcePath\" to \"destinationPath\". Beginning retry {retryCount} in x ms.", null, null) { - BuildEventContext = MakeBuildEventContext(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), }; } - private BuildMessageEventArgs MakeMessageEventArgs(string message, MessageImportance importance, string? code = null, string? keyword = "keyword") + private BuildMessageEventArgs MakeMessageEventArgs(string message, MessageImportance importance, string? code = null, string? keyword = "keyword", BuildEventContext? buildEventContext = null) { return new BuildMessageEventArgs(message: message, helpKeyword: keyword, senderName: null, importance: importance, eventTimestamp: DateTime.UtcNow, lineNumber: 0, columnNumber: 0, endLineNumber: 0, endColumnNumber: 0, code: code, subcategory: null, file: null) { - BuildEventContext = MakeBuildEventContext(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), }; } - private BuildMessageEventArgs MakeTaskCommandLineEventArgs(string message, MessageImportance importance) + private BuildMessageEventArgs MakeTaskCommandLineEventArgs(string message, MessageImportance importance, BuildEventContext? buildEventContext = null) { return new TaskCommandLineEventArgs(message, "Task", importance) { - BuildEventContext = MakeBuildEventContext(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), }; } - private BuildMessageEventArgs MakeExtendedMessageEventArgs(string message, MessageImportance importance, string extendedType, Dictionary? extendedMetadata) + private BuildMessageEventArgs MakeExtendedMessageEventArgs(string message, MessageImportance importance, string extendedType, Dictionary? extendedMetadata, BuildEventContext? buildEventContext = null) { return new ExtendedBuildMessageEventArgs(extendedType, message, "keyword", null, importance, _messageTime) { - BuildEventContext = MakeBuildEventContext(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), ExtendedMetadata = extendedMetadata }; } - private BuildErrorEventArgs MakeErrorEventArgs(string error, string? link = null, string? keyword = null) + private BuildErrorEventArgs MakeErrorEventArgs(string error, string? link = null, string? keyword = null, BuildEventContext? buildEventContext = null) { return new BuildErrorEventArgs(subcategory: null, code: "AA0000", file: "directory/file", lineNumber: 1, columnNumber: 2, endLineNumber: 3, endColumnNumber: 4, message: error, helpKeyword: keyword, helpLink: link, senderName: null, eventTimestamp: DateTime.UtcNow) { - BuildEventContext = MakeBuildEventContext(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), }; } - private BuildWarningEventArgs MakeWarningEventArgs(string warning, string? link = null, string? keyword = null) + private BuildWarningEventArgs MakeWarningEventArgs(string warning, string? link = null, string? keyword = null, BuildEventContext? buildEventContext = null) { return new BuildWarningEventArgs(subcategory: null, code: "AA0000", file: "directory/file", lineNumber: 1, columnNumber: 2, endLineNumber: 3, endColumnNumber: 4, message: warning, helpKeyword: keyword, helpLink: link, senderName: null, eventTimestamp: DateTime.UtcNow) { - BuildEventContext = MakeBuildEventContext(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), }; } @@ -293,6 +313,8 @@ private void InvokeLoggerCallbacksForSimpleProject(bool succeeded, Action additi projectFile ??= _projectFile; BuildStarted?.Invoke(_eventSender, MakeBuildStartedEventArgs()); + StatusEventRaised?.Invoke(_eventSender, MakeProjectEvalFinishedArgs(projectFile)); + ProjectStarted?.Invoke(_eventSender, MakeProjectStartedEventArgs(projectFile)); TargetStarted?.Invoke(_eventSender, MakeTargetStartedEventArgs(projectFile, "Build")); @@ -307,9 +329,21 @@ private void InvokeLoggerCallbacksForSimpleProject(bool succeeded, Action additi BuildFinished?.Invoke(_eventSender, MakeBuildFinishedEventArgs(succeeded)); } + private ProjectEvaluationFinishedEventArgs MakeProjectEvalFinishedArgs(string projectFile, List<(string, string)>? properties = null, List<(string, string)>? items = null, BuildEventContext? buildEventContext = null) + { + return new ProjectEvaluationFinishedEventArgs + { + ProjectFile = projectFile, + Properties = properties?.ToDictionary(k => k.Item1, v => v.Item2) ?? new Dictionary(), + Items = items?.Select(kvp => new DictionaryEntry(kvp.Item1, kvp.Item2)).ToList() ?? new List(), + BuildEventContext = buildEventContext ?? MakeBuildEventContext(), + }; + } + private void InvokeLoggerCallbacksForTestProject(bool succeeded, Action additionalCallbacks) { BuildStarted?.Invoke(_eventSender, MakeBuildStartedEventArgs()); + StatusEventRaised?.Invoke(_eventSender, MakeProjectEvalFinishedArgs(_projectFile)); ProjectStarted?.Invoke(_eventSender, MakeProjectStartedEventArgs(_projectFile)); TargetStarted?.Invoke(_eventSender, MakeTargetStartedEventArgs(_projectFile, "_TestRunStart")); @@ -328,26 +362,29 @@ private void InvokeLoggerCallbacksForTestProject(bool succeeded, Action addition private void InvokeLoggerCallbacksForTwoProjects(bool succeeded, Action additionalCallbacks, Action additionalCallbacks2) { BuildStarted?.Invoke(_eventSender, MakeBuildStartedEventArgs()); - - ProjectStarted?.Invoke(_eventSender, MakeProjectStartedEventArgs(_projectFile)); - TargetStarted?.Invoke(_eventSender, MakeTargetStartedEventArgs(_projectFile, "Build1")); - TaskStarted?.Invoke(_eventSender, MakeTaskStartedEventArgs(_projectFile, "Task1")); + var p1BuildContext = MakeBuildEventContext(evalId: 1, projectContextId: 1); + StatusEventRaised?.Invoke(_eventSender, MakeProjectEvalFinishedArgs(_projectFile, buildEventContext: p1BuildContext)); + ProjectStarted?.Invoke(_eventSender, MakeProjectStartedEventArgs(_projectFile, buildEventContext: p1BuildContext)); + TargetStarted?.Invoke(_eventSender, MakeTargetStartedEventArgs(_projectFile, "Build1", buildEventContext: p1BuildContext)); + TaskStarted?.Invoke(_eventSender, MakeTaskStartedEventArgs(_projectFile, "Task1", buildEventContext: p1BuildContext)); additionalCallbacks(); - TaskFinished?.Invoke(_eventSender, MakeTaskFinishedEventArgs(_projectFile, "Task1", succeeded)); - TargetFinished?.Invoke(_eventSender, MakeTargetFinishedEventArgs(_projectFile, "Build1", succeeded)); - ProjectFinished?.Invoke(_eventSender, MakeProjectFinishedEventArgs(_projectFile, succeeded)); + TaskFinished?.Invoke(_eventSender, MakeTaskFinishedEventArgs(_projectFile, "Task1", succeeded, buildEventContext: p1BuildContext)); + TargetFinished?.Invoke(_eventSender, MakeTargetFinishedEventArgs(_projectFile, "Build1", succeeded, buildEventContext: p1BuildContext)); + ProjectFinished?.Invoke(_eventSender, MakeProjectFinishedEventArgs(_projectFile, succeeded, buildEventContext: p1BuildContext)); - ProjectStarted?.Invoke(_eventSender, MakeProjectStartedEventArgs(_projectFile2)); - TargetStarted?.Invoke(_eventSender, MakeTargetStartedEventArgs(_projectFile2, "Build2")); - TaskStarted?.Invoke(_eventSender, MakeTaskStartedEventArgs(_projectFile2, "Task2")); + var p2BuildContext = MakeBuildEventContext(evalId: 2, projectContextId: 2); + StatusEventRaised?.Invoke(_eventSender, MakeProjectEvalFinishedArgs(_projectFile2, buildEventContext: p2BuildContext)); + ProjectStarted?.Invoke(_eventSender, MakeProjectStartedEventArgs(_projectFile2, buildEventContext: p2BuildContext)); + TargetStarted?.Invoke(_eventSender, MakeTargetStartedEventArgs(_projectFile2, "Build2", buildEventContext: p2BuildContext)); + TaskStarted?.Invoke(_eventSender, MakeTaskStartedEventArgs(_projectFile2, "Task2", buildEventContext: p2BuildContext)); additionalCallbacks2(); - TaskFinished?.Invoke(_eventSender, MakeTaskFinishedEventArgs(_projectFile2, "Task2", succeeded)); - TargetFinished?.Invoke(_eventSender, MakeTargetFinishedEventArgs(_projectFile2, "Build2", succeeded)); - ProjectFinished?.Invoke(_eventSender, MakeProjectFinishedEventArgs(_projectFile2, succeeded)); + TaskFinished?.Invoke(_eventSender, MakeTaskFinishedEventArgs(_projectFile2, "Task2", succeeded, buildEventContext: p2BuildContext)); + TargetFinished?.Invoke(_eventSender, MakeTargetFinishedEventArgs(_projectFile2, "Build2", succeeded, buildEventContext: p2BuildContext)); + ProjectFinished?.Invoke(_eventSender, MakeProjectFinishedEventArgs(_projectFile2, succeeded, buildEventContext: p2BuildContext)); BuildFinished?.Invoke(_eventSender, MakeBuildFinishedEventArgs(succeeded)); } @@ -510,17 +547,19 @@ public Task PrintBuildSummary_2Projects_FailedWithErrorsAndWarnings() succeeded: false, () => { - WarningRaised?.Invoke(_eventSender, MakeWarningEventArgs("Warning1!")); - WarningRaised?.Invoke(_eventSender, MakeWarningEventArgs("Warning2!")); - ErrorRaised?.Invoke(_eventSender, MakeErrorEventArgs("Error1!")); - ErrorRaised?.Invoke(_eventSender, MakeErrorEventArgs("Error2!")); + var p1Context = MakeBuildEventContext(evalId: 1, projectContextId: 1); + WarningRaised?.Invoke(_eventSender, MakeWarningEventArgs("Warning1!", buildEventContext: p1Context)); + WarningRaised?.Invoke(_eventSender, MakeWarningEventArgs("Warning2!", buildEventContext: p1Context)); + ErrorRaised?.Invoke(_eventSender, MakeErrorEventArgs("Error1!", buildEventContext: p1Context)); + ErrorRaised?.Invoke(_eventSender, MakeErrorEventArgs("Error2!", buildEventContext: p1Context)); }, () => { - WarningRaised?.Invoke(_eventSender, MakeWarningEventArgs("Warning3!")); - WarningRaised?.Invoke(_eventSender, MakeWarningEventArgs("Warning4!")); - ErrorRaised?.Invoke(_eventSender, MakeErrorEventArgs("Error3!")); - ErrorRaised?.Invoke(_eventSender, MakeErrorEventArgs("Error4!")); + var p2Context = MakeBuildEventContext(evalId: 2, projectContextId: 2); + WarningRaised?.Invoke(_eventSender, MakeWarningEventArgs("Warning3!", buildEventContext: p2Context)); + WarningRaised?.Invoke(_eventSender, MakeWarningEventArgs("Warning4!", buildEventContext: p2Context)); + ErrorRaised?.Invoke(_eventSender, MakeErrorEventArgs("Error3!", buildEventContext: p2Context)); + ErrorRaised?.Invoke(_eventSender, MakeErrorEventArgs("Error4!", buildEventContext: p2Context)); }); return Verify(_outputWriter.ToString(), _settings).UniqueForOSPlatform(); @@ -752,25 +791,22 @@ public void DisplayNodesOverwritesTime() public async Task DisplayNodesOverwritesWithNewTargetFramework() { BuildStarted?.Invoke(_eventSender, MakeBuildStartedEventArgs()); + StatusEventRaised?.Invoke(_eventSender, MakeProjectEvalFinishedArgs(_projectFile, properties: [("TargetFramework", "tfName")])); - ProjectStartedEventArgs pse = MakeProjectStartedEventArgs(_projectFile, "Build"); - pse.GlobalProperties = new Dictionary() { ["TargetFramework"] = "tfName" }; - - ProjectStarted?.Invoke(_eventSender, pse); - + ProjectStarted?.Invoke(_eventSender, MakeProjectStartedEventArgs(_projectFile, "Build")); TargetStarted?.Invoke(_eventSender, MakeTargetStartedEventArgs(_projectFile, "Build")); TaskStarted?.Invoke(_eventSender, MakeTaskStartedEventArgs(_projectFile, "Task")); _terminallogger.DisplayNodes(); - // This is a bit fast and loose with the events that would be fired - // in a real "stop building that TF for the project and start building - // a new TF of the same project" situation, but it's enough now. - ProjectStartedEventArgs pse2 = MakeProjectStartedEventArgs(_projectFile, "Build"); - pse2.GlobalProperties = new Dictionary() { ["TargetFramework"] = "tf2" }; + // force the current node to stop building and 'yield' + TaskStarted?.Invoke(_eventSender, MakeTaskStartedEventArgs(_projectFile, "MSBuild")); - ProjectStarted?.Invoke(_eventSender, pse2); - TargetStarted?.Invoke(_eventSender, MakeTargetStartedEventArgs(_projectFile, "Build")); + // now create a new project with a different target framework that runs on the same node + var buildContext2 = MakeBuildEventContext(evalId: 2, projectContextId: 2); + StatusEventRaised?.Invoke(_eventSender, MakeProjectEvalFinishedArgs(_projectFile, properties: [("TargetFramework", "tf2")], buildEventContext: buildContext2)); + ProjectStarted?.Invoke(_eventSender, MakeProjectStartedEventArgs(_projectFile, "Build", buildEventContext: buildContext2)); + TargetStarted?.Invoke(_eventSender, MakeTargetStartedEventArgs(_projectFile, "Build", buildEventContext: buildContext2)); _terminallogger.DisplayNodes(); @@ -865,7 +901,7 @@ public async Task PrintWarningLinks() await Verify(_outputWriter.ToString(), _settings).UniqueForOSPlatform(); } - + [Fact] public async Task PrintErrorLinks() { diff --git a/src/Build/Logging/TerminalLogger/TerminalLogger.cs b/src/Build/Logging/TerminalLogger/TerminalLogger.cs index 4d9593ae623..24eac40cb55 100644 --- a/src/Build/Logging/TerminalLogger/TerminalLogger.cs +++ b/src/Build/Logging/TerminalLogger/TerminalLogger.cs @@ -916,7 +916,16 @@ private static string GetProjectFinishedHeader(TerminalProjectInfo project, stri AnsiCodes.Colorize(rid, RuntimeIdentifierColor), buildResult, duration), - (null, null, _) => "" // this is a weird pattern - what should we do? + (null, null, true) => ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TestProjectFinished_NoTF", + Indentation, + projectFile, + buildResult, + duration), + (null, null, false) => ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("ProjectFinished_NoTF", + Indentation, + projectFile, + buildResult, + duration), }; } diff --git a/src/Build/Logging/TerminalLogger/TerminalNodesFrame.cs b/src/Build/Logging/TerminalLogger/TerminalNodesFrame.cs index 1ea8fc94c1b..74232e078d2 100644 --- a/src/Build/Logging/TerminalLogger/TerminalNodesFrame.cs +++ b/src/Build/Logging/TerminalLogger/TerminalNodesFrame.cs @@ -3,6 +3,7 @@ using System; using System.Text; +using Microsoft.Build.Framework; using Microsoft.Build.Framework.Logging; using Microsoft.Build.Shared; @@ -83,7 +84,24 @@ internal ReadOnlySpan RenderNodeStatus(int i) } var renderedTarget = !string.IsNullOrWhiteSpace(targetPrefix) ? $"{AnsiCodes.Colorize(targetPrefix, targetPrefixColor)} {target}" : target; - return $"{TerminalLogger.Indentation}{project}{(targetFramework is null ? string.Empty : " ")}{AnsiCodes.Colorize(targetFramework, TerminalLogger.TargetFrameworkColor)} {AnsiCodes.Colorize(runtimeIdentifier, TerminalLogger.RuntimeIdentifierColor)} {AnsiCodes.SetCursorHorizontal(MaxColumn)}{AnsiCodes.MoveCursorBackward(targetWithoutAnsiLength + durationString.Length + 1)}{renderedTarget} {durationString}".AsSpan(); + var builder = StringBuilderCache.Acquire(renderedWidth); + builder.Append(TerminalLogger.Indentation).Append(project); + if (!string.IsNullOrWhiteSpace(targetFramework)) + { + builder.Append(' ').Append(AnsiCodes.Colorize(targetFramework, TerminalLogger.TargetFrameworkColor)); + } + if (!string.IsNullOrWhiteSpace(runtimeIdentifier)) + { + builder.Append(' ').Append(AnsiCodes.Colorize(runtimeIdentifier, TerminalLogger.RuntimeIdentifierColor)); + } + builder.Append(' ').Append(AnsiCodes.SetCursorHorizontal(MaxColumn)) + .Append(AnsiCodes.MoveCursorBackward(targetWithoutAnsiLength + durationString.Length + 1)) + .Append(renderedTarget) + .Append(' ') + .Append(durationString); + var span = builder.ToString().AsSpan(); + StringBuilderCache.Release(builder); + return span; static int Length(string durationString, string project, string? targetFramework, string? runtimeIdentifier, int targetWithoutAnsiLength) => TerminalLogger.Indentation.Length + From e58af7e07db2c8a4c1497f89f3629502d92ecb2e Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 6 Aug 2025 12:18:12 -0500 Subject: [PATCH 3/6] Add some tests covering RID output --- ...ortsRuntimeIdentifier.Windows.verified.txt | 5 ++ ...kAndRuntimeIdentifier.Windows.verified.txt | 5 ++ src/Build.UnitTests/TerminalLogger_Tests.cs | 48 +++++++++++++++---- 3 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Windows.verified.txt create mode 100644 src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Windows.verified.txt diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Windows.verified.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Windows.verified.txt new file mode 100644 index 00000000000..e641abd07e8 --- /dev/null +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Windows.verified.txt @@ -0,0 +1,5 @@ +]9;4;3;\ project win-x64 succeeded (0.2s) → ]8;;file:///C:/src\C:\src\project.dll]8;;\ +[?25l +[?25h +Build succeeded in 5.0s +]9;4;0;\ \ No newline at end of file diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Windows.verified.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Windows.verified.txt new file mode 100644 index 00000000000..9b473ace8ee --- /dev/null +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Windows.verified.txt @@ -0,0 +1,5 @@ +]9;4;3;\ project net10.0 win-x64 succeeded (0.2s) → ]8;;file:///C:/src\C:\src\project.dll]8;;\ +[?25l +[?25h +Build succeeded in 5.0s +]9;4;0;\ \ No newline at end of file diff --git a/src/Build.UnitTests/TerminalLogger_Tests.cs b/src/Build.UnitTests/TerminalLogger_Tests.cs index 97989705f27..59d8590970a 100644 --- a/src/Build.UnitTests/TerminalLogger_Tests.cs +++ b/src/Build.UnitTests/TerminalLogger_Tests.cs @@ -271,6 +271,16 @@ private BuildMessageEventArgs MakeMessageEventArgs(string message, MessageImport }; } + private BuildMessageEventArgs MakeBuildOutputEventArgs(string projectFilePath, BuildEventContext? buildEventContext = null) + { + var projectName = Path.GetFileNameWithoutExtension(projectFilePath); + var outputPath = Path.ChangeExtension(projectFilePath, "dll"); + var messageString = $"{projectName} -> {outputPath}"; + var args = MakeMessageEventArgs(messageString, MessageImportance.High, buildEventContext: buildEventContext); + args.ProjectFile = projectFilePath; + return args; + } + private BuildMessageEventArgs MakeTaskCommandLineEventArgs(string message, MessageImportance importance, BuildEventContext? buildEventContext = null) { return new TaskCommandLineEventArgs(message, "Task", importance) @@ -308,19 +318,19 @@ private BuildWarningEventArgs MakeWarningEventArgs(string warning, string? link #region Build summary tests - private void InvokeLoggerCallbacksForSimpleProject(bool succeeded, Action additionalCallbacks, string? projectFile = null) + private void InvokeLoggerCallbacksForSimpleProject(bool succeeded, Action? additionalCallbacks = null, string? projectFile = null, List<(string, string)>? properties = null) { projectFile ??= _projectFile; BuildStarted?.Invoke(_eventSender, MakeBuildStartedEventArgs()); - StatusEventRaised?.Invoke(_eventSender, MakeProjectEvalFinishedArgs(projectFile)); + StatusEventRaised?.Invoke(_eventSender, MakeProjectEvalFinishedArgs(projectFile, properties: properties)); ProjectStarted?.Invoke(_eventSender, MakeProjectStartedEventArgs(projectFile)); TargetStarted?.Invoke(_eventSender, MakeTargetStartedEventArgs(projectFile, "Build")); TaskStarted?.Invoke(_eventSender, MakeTaskStartedEventArgs(projectFile, "Task")); - additionalCallbacks(); + additionalCallbacks?.Invoke(); TaskFinished?.Invoke(_eventSender, MakeTaskFinishedEventArgs(projectFile, "Task", succeeded)); TargetFinished?.Invoke(_eventSender, MakeTargetFinishedEventArgs(projectFile, "Build", succeeded)); @@ -569,11 +579,7 @@ public Task PrintBuildSummary_2Projects_FailedWithErrorsAndWarnings() public Task PrintProjectOutputDirectoryLink() { // Send message in order to set project output path - BuildMessageEventArgs e = MakeMessageEventArgs( - $"㐇𠁠𪨰𫠊𫦠𮚮⿕ -> {_projectFileWithNonAnsiSymbols.Replace("proj", "dll")}", - MessageImportance.High); - e.ProjectFile = _projectFileWithNonAnsiSymbols; - + BuildMessageEventArgs e = MakeBuildOutputEventArgs(_projectFileWithNonAnsiSymbols); InvokeLoggerCallbacksForSimpleProject(succeeded: true, () => { MessageRaised?.Invoke(_eventSender, e); @@ -804,7 +810,7 @@ public async Task DisplayNodesOverwritesWithNewTargetFramework() // now create a new project with a different target framework that runs on the same node var buildContext2 = MakeBuildEventContext(evalId: 2, projectContextId: 2); - StatusEventRaised?.Invoke(_eventSender, MakeProjectEvalFinishedArgs(_projectFile, properties: [("TargetFramework", "tf2")], buildEventContext: buildContext2)); + StatusEventRaised?.Invoke(_eventSender, MakeProjectEvalFinishedArgs(_projectFile, properties: [("TargetFramework", "tf2")], buildEventContext: buildContext2)); ProjectStarted?.Invoke(_eventSender, MakeProjectStartedEventArgs(_projectFile, "Build", buildEventContext: buildContext2)); TargetStarted?.Invoke(_eventSender, MakeTargetStartedEventArgs(_projectFile, "Build", buildEventContext: buildContext2)); @@ -915,5 +921,29 @@ public async Task PrintErrorLinks() await Verify(_outputWriter.ToString(), _settings).UniqueForOSPlatform(); } + + [Fact] + public async Task ProjectFinishedReportsRuntimeIdentifier() + { + // this project will report a RID and so will show a RID in the build output + var buildOutputEvent = MakeBuildOutputEventArgs(_projectFile); + InvokeLoggerCallbacksForSimpleProject(succeeded: true, properties: [("RuntimeIdentifier", "win-x64")], additionalCallbacks: () => + { + MessageRaised?.Invoke(_eventSender, buildOutputEvent); + }); + await Verify(_outputWriter.ToString(), _settings).UniqueForOSPlatform(); + } + + [Fact] + public async Task ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier() + { + // this project will report a TFM and a RID and so will show a both in the output + var buildOutputEvent = MakeBuildOutputEventArgs(_projectFile); + InvokeLoggerCallbacksForSimpleProject(succeeded: true, properties: [("TargetFramework", "net10.0"), ("RuntimeIdentifier", "win-x64")], additionalCallbacks: () => + { + MessageRaised?.Invoke(_eventSender, buildOutputEvent); + }); + await Verify(_outputWriter.ToString(), _settings).UniqueForOSPlatform(); + } } } From dfa6c4eed2cdbd1a224c4d5cf237ced3f58dcd36 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 6 Aug 2025 12:48:39 -0500 Subject: [PATCH 4/6] add macos test variations --- ...s.ProjectFinishedReportsRuntimeIdentifier.OSX.received.txt | 4 ++++ ...eportsTargetFrameworkAndRuntimeIdentifier.OSX.received.txt | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.OSX.received.txt create mode 100644 src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.received.txt diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.OSX.received.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.OSX.received.txt new file mode 100644 index 00000000000..7515e312cdf --- /dev/null +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.OSX.received.txt @@ -0,0 +1,4 @@ + project win-x64 succeeded (0.2s) → ]8;;file:///src\/src/project.dll]8;;\ +[?25l +[?25h +Build succeeded in 5.0s diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.received.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.received.txt new file mode 100644 index 00000000000..e4caad3cc16 --- /dev/null +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.received.txt @@ -0,0 +1,4 @@ + project net10.0 win-x64 succeeded (0.2s) → ]8;;file:///src\/src/project.dll]8;;\ +[?25l +[?25h +Build succeeded in 5.0s From 4bb1fa523070645d721270c5dffb2e5e0e237516 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 6 Aug 2025 12:53:08 -0500 Subject: [PATCH 5/6] Add Linux test snapshots too --- ...rojectFinishedReportsRuntimeIdentifier.Linux.received.txt | 5 +++++ ...rtsTargetFrameworkAndRuntimeIdentifier.Linux.received.txt | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.received.txt create mode 100644 src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.received.txt diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.received.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.received.txt new file mode 100644 index 00000000000..fc7d56605ad --- /dev/null +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.received.txt @@ -0,0 +1,5 @@ +]9;4;3;\ project win-x64 succeeded (0.2s) → ]8;;file:///src\/src/project.dll]8;;\ +[?25l +[?25h +Build succeeded in 5.0s +]9;4;0;\ \ No newline at end of file diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.received.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.received.txt new file mode 100644 index 00000000000..5d32134141c --- /dev/null +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.received.txt @@ -0,0 +1,5 @@ +]9;4;3;\ project net10.0 win-x64 succeeded (0.2s) → ]8;;file:///src\/src/project.dll]8;;\ +[?25l +[?25h +Build succeeded in 5.0s +]9;4;0;\ \ No newline at end of file From 4e30717aadedb88912c5e7a5c6f140a22478b578 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 6 Aug 2025 13:12:44 -0500 Subject: [PATCH 6/6] Use the correct file names for the snapshot files --- ...ts.ProjectFinishedReportsRuntimeIdentifier.Linux.verified.txt} | 0 ...ests.ProjectFinishedReportsRuntimeIdentifier.OSX.verified.txt} | 0 ...ReportsTargetFrameworkAndRuntimeIdentifier.Linux.verified.txt} | 0 ...edReportsTargetFrameworkAndRuntimeIdentifier.OSX.verified.txt} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/Build.UnitTests/Snapshots/{TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.received.txt => TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.verified.txt} (100%) rename src/Build.UnitTests/Snapshots/{TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.OSX.received.txt => TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.OSX.verified.txt} (100%) rename src/Build.UnitTests/Snapshots/{TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.received.txt => TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.verified.txt} (100%) rename src/Build.UnitTests/Snapshots/{TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.received.txt => TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.verified.txt} (100%) diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.received.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.verified.txt similarity index 100% rename from src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.received.txt rename to src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.verified.txt diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.OSX.received.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.OSX.verified.txt similarity index 100% rename from src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.OSX.received.txt rename to src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.OSX.verified.txt diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.received.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.verified.txt similarity index 100% rename from src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.received.txt rename to src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.verified.txt diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.received.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.verified.txt similarity index 100% rename from src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.received.txt rename to src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.verified.txt