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.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.verified.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.verified.txt new file mode 100644 index 00000000000..fc7d56605ad --- /dev/null +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.Linux.verified.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.ProjectFinishedReportsRuntimeIdentifier.OSX.verified.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.OSX.verified.txt new file mode 100644 index 00000000000..7515e312cdf --- /dev/null +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsRuntimeIdentifier.OSX.verified.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.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.Linux.verified.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.verified.txt new file mode 100644 index 00000000000..5d32134141c --- /dev/null +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.verified.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 diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.verified.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.verified.txt new file mode 100644 index 00000000000..e4caad3cc16 --- /dev/null +++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.verified.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 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 52defb8b171..59d8590970a 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,146 @@ 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 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) { - 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(), }; } @@ -288,17 +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, 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)); @@ -307,9 +339,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 +372,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 +557,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(); @@ -530,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); @@ -752,25 +797,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 +907,7 @@ public async Task PrintWarningLinks() await Verify(_outputWriter.ToString(), _settings).UniqueForOSPlatform(); } - + [Fact] public async Task PrintErrorLinks() { @@ -879,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(); + } } } diff --git a/src/Build/Logging/TerminalLogger/TerminalLogger.cs b/src/Build/Logging/TerminalLogger/TerminalLogger.cs index f84d5d66ffb..24eac40cb55 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,61 @@ 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, true) => ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TestProjectFinished_NoTF", + Indentation, + projectFile, + buildResult, + duration), + (null, null, false) => ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("ProjectFinished_NoTF", + Indentation, + projectFile, + buildResult, + duration), + }; } /// @@ -869,7 +961,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 +1100,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 +1110,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..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; @@ -51,6 +52,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 +62,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 +84,30 @@ 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(); - - static int Length(string durationString, string project, string? targetFramework, int targetWithoutAnsiLength) => + 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 + 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