diff --git a/src/Build.UnitTests/BackEnd/BuildRequestConfiguration_Tests.cs b/src/Build.UnitTests/BackEnd/BuildRequestConfiguration_Tests.cs
index c35fc6a2b3f..940b37782ce 100644
--- a/src/Build.UnitTests/BackEnd/BuildRequestConfiguration_Tests.cs
+++ b/src/Build.UnitTests/BackEnd/BuildRequestConfiguration_Tests.cs
@@ -576,5 +576,112 @@ private void TestSkipIsolationConstraints(string glob, string referencePath, boo
configuration.ShouldSkipIsolationConstraintsForReference(referencePath).ShouldBe(expectedOutput);
}
+
+ [Fact]
+ public void TestProjectEvaluationIdPreservedAcrossTranslation()
+ {
+ string projectBody = """
+
+
+
+ """.Cleanup();
+
+ using var collection = new ProjectCollection();
+ using ProjectFromString projectFromString = new(
+ projectBody,
+ new Dictionary(),
+ ObjectModelHelpers.MSBuildDefaultToolsVersion,
+ collection);
+ Project project = projectFromString.Project;
+ project.FullPath = "foo";
+ ProjectInstance instance = project.CreateProjectInstance();
+
+ BuildRequestConfiguration configuration = new(
+ new BuildRequestData(instance, [], null, BuildRequestDataFlags.None, propertiesToTransfer: []), "2.0")
+ {
+ ConfigurationId = 1,
+ };
+
+ // The evaluation ID should be set from the project instance.
+ int expectedEvalId = instance.EvaluationId;
+ configuration.ProjectEvaluationId.ShouldBe(expectedEvalId);
+ expectedEvalId.ShouldNotBe(BuildEventContext.InvalidEvaluationId);
+
+ ((ITranslatable)configuration).Translate(TranslationHelpers.GetWriteTranslator());
+ INodePacket packet = BuildRequestConfiguration.FactoryForDeserialization(TranslationHelpers.GetReadTranslator());
+
+ BuildRequestConfiguration deserializedConfig = packet as BuildRequestConfiguration;
+ deserializedConfig.ShouldNotBeNull();
+ deserializedConfig.ProjectEvaluationId.ShouldBe(expectedEvalId);
+ }
+
+ [Fact]
+ public void TestProjectEvaluationIdPreservedInShallowClone()
+ {
+ string projectBody = """
+
+
+
+ """.Cleanup();
+
+ using var collection = new ProjectCollection();
+ using ProjectFromString projectFromString = new(
+ projectBody,
+ new Dictionary(),
+ ObjectModelHelpers.MSBuildDefaultToolsVersion,
+ collection);
+ Project project = projectFromString.Project;
+ project.FullPath = "foo";
+ ProjectInstance instance = project.CreateProjectInstance();
+
+ BuildRequestConfiguration original = new(new BuildRequestData(instance, [], null), "2.0")
+ {
+ ConfigurationId = 1,
+ };
+
+ int expectedEvalId = instance.EvaluationId;
+ original.ProjectEvaluationId.ShouldBe(expectedEvalId);
+
+ BuildRequestConfiguration clone = original.ShallowCloneWithNewId(2);
+ clone.ProjectEvaluationId.ShouldBe(expectedEvalId);
+ }
+
+
+ [Fact]
+ public void TestProjectEvaluationIdPreservedAcrossTranslateForFutureUse()
+ {
+ string projectBody = """
+
+
+
+ """.Cleanup();
+
+ using var collection = new ProjectCollection();
+ using ProjectFromString projectFromString = new(
+ projectBody,
+ new Dictionary(),
+ ObjectModelHelpers.MSBuildDefaultToolsVersion,
+ collection);
+ Project project = projectFromString.Project;
+ project.FullPath = "foo";
+ ProjectInstance instance = project.CreateProjectInstance();
+
+ BuildRequestConfiguration configuration = new(new BuildRequestData(instance, [], null), "2.0")
+ {
+ ConfigurationId = 1,
+ };
+
+ int expectedEvalId = instance.EvaluationId;
+ configuration.ProjectEvaluationId.ShouldBe(expectedEvalId);
+
+ // TranslateForFutureUse uses a different serialization path.
+ configuration.TranslateForFutureUse(TranslationHelpers.GetWriteTranslator());
+ ITranslator reader = TranslationHelpers.GetReadTranslator();
+
+ BuildRequestConfiguration deserialized = new();
+ deserialized.TranslateForFutureUse(reader);
+
+ deserialized.ProjectEvaluationId.ShouldBe(expectedEvalId);
+ }
}
}
diff --git a/src/Build.UnitTests/BackEnd/BuildResult_Tests.cs b/src/Build.UnitTests/BackEnd/BuildResult_Tests.cs
index 1b94ac3357f..be5d2c61db8 100644
--- a/src/Build.UnitTests/BackEnd/BuildResult_Tests.cs
+++ b/src/Build.UnitTests/BackEnd/BuildResult_Tests.cs
@@ -10,6 +10,7 @@
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Unittest;
+using Shouldly;
using Xunit;
using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem;
@@ -343,6 +344,22 @@ public void TestTranslation()
Assert.True(TranslationHelpers.CompareCollections(result["omega"].Items, deserializedResult["omega"].Items, TaskItemComparer.Instance));
}
+ [Fact]
+ public void TestTranslationPreservesEvaluationId()
+ {
+ BuildRequest request = new(1, 1, 2, ["Build"], null, new BuildEventContext(1, 1, 2, 3, 4, 5), null);
+ BuildResult result = new(request, new BuildAbortedException())
+ {
+ EvaluationId = 42,
+ };
+
+ ((ITranslatable)result).Translate(TranslationHelpers.GetWriteTranslator());
+ INodePacket packet = BuildResult.FactoryForDeserialization(TranslationHelpers.GetReadTranslator());
+ BuildResult deserializedResult = (packet as BuildResult)!;
+
+ deserializedResult.EvaluationId.ShouldBe(42);
+ }
+
private BuildRequest CreateNewBuildRequest(int configurationId, string[] targets)
{
return new BuildRequest(1 /* submissionId */, _nodeRequestId++, configurationId, targets, null, BuildEventContext.Invalid, null);
diff --git a/src/Build.UnitTests/TerminalLogger_Tests.cs b/src/Build.UnitTests/TerminalLogger_Tests.cs
index 68928d9b783..e6739296e99 100644
--- a/src/Build.UnitTests/TerminalLogger_Tests.cs
+++ b/src/Build.UnitTests/TerminalLogger_Tests.cs
@@ -31,7 +31,7 @@ internal sealed class MockBuildEventSink(int nodeNumber) : IBuildEventSink, IEve
public bool HaveLoggedBuildFinishedEvent { get; set; }
void IBuildEventSink.Consume(BuildEventArgs buildEvent, int sinkId) => (this as IBuildEventSink).Consume(buildEvent);
-
+
void IBuildEventSink.Consume(BuildEventArgs buildEvent)
{
// map the incoming build event to the appropriate event handler
@@ -169,7 +169,7 @@ public TerminalLogger_Tests(ITestOutputHelper outputHelper)
{
_outputHelper = outputHelper;
_mockTerminal = new Terminal(_outputWriter);
-
+
_terminallogger = new TerminalLogger(_mockTerminal);
_terminallogger.Initialize(_centralNodeEventSource, _nodeCount);
_terminallogger._createStopwatch = () => new MockStopwatch();
@@ -916,11 +916,11 @@ public void TestTerminalLoggerTogetherWithOtherLoggers()
string logFileWithoutTL = env.ExpectFile(".binlog").Path;
// Execute MSBuild with binary, file and terminal loggers
- RunnerUtilities.ExecMSBuild($"{projectFile.Path} /bl:{logFileWithTL} -flp:logfile={Path.Combine(logFolder.Path, "logFileWithTL.log")};verbosity=diagnostic -tl:on", out bool success, outputHelper: _outputHelper);
+ RunnerUtilities.ExecMSBuild($"{projectFile.Path} /bl:{logFileWithTL} -flp:logfile={Path.Combine(logFolder.Path, "logFileWithTL.log")};verbosity=diagnostic -tl:on", out bool success, outputHelper: _outputHelper);
success.ShouldBeTrue();
// Execute MSBuild with binary and file loggers
- RunnerUtilities.ExecMSBuild($"{projectFile.Path} /bl:{logFileWithoutTL} -flp:logfile={Path.Combine(logFolder.Path, "logFileWithoutTL.log")};verbosity=diagnostic", out success, outputHelper: _outputHelper);
+ RunnerUtilities.ExecMSBuild($"{projectFile.Path} /bl:{logFileWithoutTL} -flp:logfile={Path.Combine(logFolder.Path, "logFileWithoutTL.log")};verbosity=diagnostic", out success, outputHelper: _outputHelper);
success.ShouldBeTrue();
// Read the binary log and replay into mockLogger
@@ -1029,7 +1029,7 @@ public void ReplayBinaryLogWithFewerNodesThanOriginalBuild()
{
// Create multiple projects that will build in parallel
TransientTestFolder logFolder = env.CreateFolder(createFolder: true);
-
+
// Create three simple projects
TransientTestFile project1 = env.CreateFile(logFolder, "project1.proj", @"
@@ -1037,21 +1037,21 @@ public void ReplayBinaryLogWithFewerNodesThanOriginalBuild()
");
-
+
TransientTestFile project2 = env.CreateFile(logFolder, "project2.proj", @"
");
-
+
TransientTestFile project3 = env.CreateFile(logFolder, "project3.proj", @"
");
-
+
// Create a solution file that builds all projects in parallel
string solutionContents = $@"
@@ -1136,5 +1136,30 @@ public async Task DisplayNodesRestoresStatusAfterMSBuildTaskYields_TestProject(b
await Verify(_outputWriter.ToString(), _settings).UniqueForOSPlatform().UseParameters(runOnCentralNode);
}
+
+ [Fact]
+ public void MetaprojProjectStartedDoesNotCrash()
+ {
+#if DEBUG
+ // Metaproj files (generated for solution multi-targeting builds) are never evaluated,
+ // so they have no matching ProjectEvaluationFinished event. TerminalLogger should
+ // handle ProjectStarted for metaproj files without hitting the Debug.Assert that
+ // checks for prior evaluation info. In Release mode this test is a no-op because
+ // Debug.Assert is compiled out.
+ string metaprojFile = NativeMethods.IsUnixLike ? "/src/solution.sln.metaproj" : @"C:\src\solution.sln.metaproj";
+
+ BuildEventContext buildContext = MakeBuildEventContext(evalId: -1, projectContextId: 10);
+
+ _centralNodeEventSource.InvokeBuildStarted(MakeBuildStartedEventArgs());
+
+ Should.NotThrow(() =>
+ {
+ _centralNodeEventSource.InvokeProjectStarted(MakeProjectStartedEventArgs(metaprojFile, "Build", buildEventContext: buildContext));
+ _centralNodeEventSource.InvokeProjectFinished(MakeProjectFinishedEventArgs(metaprojFile, true, buildEventContext: buildContext));
+ });
+
+ _centralNodeEventSource.InvokeBuildFinished(MakeBuildFinishedEventArgs(true));
+#endif
+ }
}
}
diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs
index 1cfaff635a1..3a871f1cca4 100644
--- a/src/Build/BackEnd/BuildManager/BuildManager.cs
+++ b/src/Build/BackEnd/BuildManager/BuildManager.cs
@@ -2689,6 +2689,13 @@ private void HandleResult(int node, BuildResult result)
configuration.ProjectTargets ??= result.ProjectTargets;
}
+ // Update the evaluation ID if it's valid - this propagates the eval ID
+ // from worker nodes to the central node for cached result scenarios.
+ if (result.EvaluationId != BuildEventContext.InvalidEvaluationId)
+ {
+ configuration.ProjectEvaluationId = result.EvaluationId;
+ }
+
// Only report results to the project cache services if it's the result for a build submission.
// Note that graph builds create a submission for each node in the graph, so each node in the graph will be
// handled here. This intentionally mirrors the behavior for cache requests, as it doesn't make sense to
diff --git a/src/Build/BackEnd/Components/Logging/NodeLoggingContext.cs b/src/Build/BackEnd/Components/Logging/NodeLoggingContext.cs
index e03c8ed13e7..efe2d213ddd 100644
--- a/src/Build/BackEnd/Components/Logging/NodeLoggingContext.cs
+++ b/src/Build/BackEnd/Components/Logging/NodeLoggingContext.cs
@@ -79,10 +79,8 @@ internal ProjectLoggingContext LogProjectStarted(BuildRequest request, BuildRequ
{
ErrorUtilities.VerifyThrow(this.IsValid, "Build not started.");
- // If we can retrieve the evaluationId from the project, do so. Don't if it's not available or
- // if we'd have to retrieve it from the cache in order to access it.
- // Order is important here because the Project getter will throw if IsCached.
- int evaluationId = (configuration != null && !configuration.IsCached && configuration.Project != null) ? configuration.Project.EvaluationId : BuildEventContext.InvalidEvaluationId;
+ // Use the persisted ProjectEvaluationId which remains available even when the project is cached.
+ int evaluationId = configuration?.ProjectEvaluationId ?? BuildEventContext.InvalidEvaluationId;
return new ProjectLoggingContext(this, request, configuration.ProjectFullPath, configuration.ToolsVersion, evaluationId);
}
diff --git a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs
index 5f337c99148..07e6b206528 100644
--- a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs
+++ b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs
@@ -867,6 +867,8 @@ private async Task RequestThreadProc(bool setThreadParameters)
{
ErrorUtilities.VerifyThrow(result == null, "Result already set when exception was thrown.");
result = new BuildResult(_requestEntry.Request, thrownException);
+ // Populate the evaluation ID from the configuration for sending to the central node.
+ result.EvaluationId = _requestEntry.RequestConfiguration.ProjectEvaluationId;
}
ReportResultAndCleanUp(result);
@@ -1021,7 +1023,10 @@ private BuildResult[] GetResultsForContinuation(FullyQualifiedBuildRequest[] req
results = new Dictionary();
for (int i = 0; i < requests.Length; i++)
{
- results[i] = new BuildResult(new BuildRequest(), new BuildAbortedException());
+ var abortResult = new BuildResult(new BuildRequest(), new BuildAbortedException());
+ // Populate the evaluation ID from the configuration for sending to the central node.
+ abortResult.EvaluationId = _requestEntry.RequestConfiguration.ProjectEvaluationId;
+ results[i] = abortResult;
}
}
@@ -1244,6 +1249,9 @@ private async Task BuildProject()
BuildResult result = await _targetBuilder.BuildTargets(_projectLoggingContext, _requestEntry, this,
allTargets, _requestEntry.RequestConfiguration.BaseLookup, _cancellationTokenSource.Token);
+ // Populate the evaluation ID from the configuration for sending to the central node.
+ result.EvaluationId = _requestEntry.RequestConfiguration.ProjectEvaluationId;
+
UpdateStatisticsPostBuild();
result = _requestEntry.Request.ProxyTargets == null
@@ -1334,7 +1342,7 @@ private void UpdateStatisticsPostBuild()
bool isFromNuget, isMetaprojTarget, isCustom;
- if (IsMetaprojTargetPath(projectTargetInstance.Value.FullPath))
+ if (FileUtilities.IsMetaprojectFilename(projectTargetInstance.Value.FullPath))
{
isMetaprojTarget = true;
isFromNuget = false;
@@ -1388,8 +1396,6 @@ void CollectTasksStats(TaskRegistry taskRegistry)
}
}
- private static bool IsMetaprojTargetPath(string targetPath) => targetPath.EndsWith(".metaproj", StringComparison.OrdinalIgnoreCase);
-
///
/// Saves the current operating environment (working directory and environment variables)
/// from the request's to the configuration for later restoration.
diff --git a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs
index 2d40523d648..8d1dc211da6 100644
--- a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs
+++ b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs
@@ -140,6 +140,11 @@ internal class BuildRequestConfiguration : IEquatable
///
private string _savedCurrentDirectory;
+ ///
+ /// Saves the evaluation ID for the project so that it's accessible even when the underlying Project becomes cached.
+ ///
+ private int _projectEvaluationId = BuildEventContext.InvalidEvaluationId;
+
#endregion
///
@@ -186,6 +191,7 @@ internal BuildRequestConfiguration(int configId, BuildRequestData data, string d
_projectInitialTargets = data.ProjectInstance.InitialTargets;
_projectDefaultTargets = data.ProjectInstance.DefaultTargets;
_projectTargets = GetProjectTargets(data.ProjectInstance.Targets);
+ _projectEvaluationId = data.ProjectInstance.EvaluationId;
if (data.PropertiesToTransfer != null)
{
_transferredProperties = new List();
@@ -223,6 +229,7 @@ internal BuildRequestConfiguration(int configId, ProjectInstance instance)
_projectInitialTargets = instance.InitialTargets;
_projectDefaultTargets = instance.DefaultTargets;
_projectTargets = GetProjectTargets(instance.Targets);
+ _projectEvaluationId = instance.EvaluationId;
IsCacheable = false;
}
@@ -247,6 +254,7 @@ private BuildRequestConfiguration(int configId, BuildRequestConfiguration other)
IsCacheable = other.IsCacheable;
_configId = configId;
RequestedTargets = other.RequestedTargets;
+ _projectEvaluationId = other._projectEvaluationId;
}
///
@@ -289,6 +297,15 @@ internal BuildRequestConfiguration()
///
public bool IsCached { get; private set; }
+ ///
+ /// The evaluation ID for this project, persisted so it remains available even when the project is cached.
+ ///
+ public int ProjectEvaluationId
+ {
+ get => _projectEvaluationId;
+ internal set => _projectEvaluationId = value;
+ }
+
///
/// Flag indicating if this configuration represents a traversal project. Traversal projects
/// are projects which typically do little or no work themselves, but have references to other
@@ -424,6 +441,7 @@ private void SetProjectBasedState(ProjectInstance project)
_projectInitialTargets = null;
_projectTargets = null;
+ _projectEvaluationId = _project.EvaluationId;
ProjectDefaultTargets = _project.DefaultTargets;
ProjectInitialTargets = _project.InitialTargets;
ProjectTargets = GetProjectTargets(_project.Targets);
@@ -941,6 +959,7 @@ public void Translate(ITranslator translator)
translator.Translate(ref _resultsNodeId);
translator.Translate(ref _savedCurrentDirectory);
translator.TranslateDictionary(ref _savedEnvironmentVariables, CommunicationsUtilities.EnvironmentVariableComparer);
+ translator.Translate(ref _projectEvaluationId);
// if the entire state is translated, then the transferred state represents the full evaluation data
if (translator.Mode == TranslationDirection.ReadFromStream && _transferredState?.TranslateEntireState == true)
@@ -959,6 +978,7 @@ internal void TranslateForFutureUse(ITranslator translator)
translator.Translate(ref _projectInitialTargets);
translator.Translate(ref _projectTargets);
translator.TranslateDictionary(ref _globalProperties, ProjectPropertyInstance.FactoryForDeserialization);
+ translator.Translate(ref _projectEvaluationId);
}
///
diff --git a/src/Build/BackEnd/Shared/BuildResult.cs b/src/Build/BackEnd/Shared/BuildResult.cs
index 87f110cb8d8..e657cfc2e2c 100644
--- a/src/Build/BackEnd/Shared/BuildResult.cs
+++ b/src/Build/BackEnd/Shared/BuildResult.cs
@@ -86,7 +86,7 @@ public class BuildResult : BuildResultBase, INodePacket, IBuildResults
///
/// Allows to serialize and deserialize different versions of the build result.
///
- private int _version = Traits.Instance.EscapeHatches.DoNotVersionBuildResult ? 0 : 1;
+ private int _version = Traits.Instance.EscapeHatches.DoNotVersionBuildResult ? 0 : 2;
///
/// The request caused a circular dependency in scheduling.
@@ -145,6 +145,11 @@ public class BuildResult : BuildResultBase, INodePacket, IBuildResults
///
private BuildRequestDataFlags _buildRequestDataFlags;
+ ///
+ /// The evaluation ID of the project used for this build.
+ ///
+ private int _evaluationId = BuildEventContext.InvalidEvaluationId;
+
private string? _schedulerInducedError;
private HashSet? _projectTargets;
@@ -426,6 +431,17 @@ public ProjectInstance? ProjectStateAfterBuild
///
public BuildRequestDataFlags? BuildRequestDataFlags => (_version > 0) ? _buildRequestDataFlags : null;
+ ///
+ /// The evaluation ID of the project used for this build.
+ ///
+ internal int EvaluationId
+ {
+ [DebuggerStepThrough]
+ get => _evaluationId;
+ [DebuggerStepThrough]
+ set => _evaluationId = value;
+ }
+
///
/// Returns the node packet type.
///
@@ -695,6 +711,12 @@ void ITranslatable.Translate(ITranslator translator)
{
translator.TranslateEnum(ref _buildRequestDataFlags, (int)_buildRequestDataFlags);
}
+
+ // Starting version 2 the _evaluationId field is present.
+ if (_version >= 2)
+ {
+ translator.Translate(ref _evaluationId);
+ }
}
///
diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckBuildEventHandler.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckBuildEventHandler.cs
index 0c9dcb341d1..bd1d71a89f5 100644
--- a/src/Build/BuildCheck/Infrastructure/BuildCheckBuildEventHandler.cs
+++ b/src/Build/BuildCheck/Infrastructure/BuildCheckBuildEventHandler.cs
@@ -70,7 +70,7 @@ private void HandleBuildSubmissionStartedEvent(BuildSubmissionStartedEventArgs e
private void HandleProjectEvaluationFinishedEvent(ProjectEvaluationFinishedEventArgs eventArgs)
{
- if (!IsMetaProjFile(eventArgs.ProjectFile))
+ if (!FileUtilities.IsMetaprojectFilename(eventArgs.ProjectFile))
{
_buildCheckManager.ProcessEvaluationFinishedEventArgs(
_checkContextFactory.CreateCheckContext(eventArgs.BuildEventContext!),
@@ -82,7 +82,7 @@ private void HandleProjectEvaluationFinishedEvent(ProjectEvaluationFinishedEvent
private void HandleProjectEvaluationStartedEvent(ProjectEvaluationStartedEventArgs eventArgs)
{
- if (!IsMetaProjFile(eventArgs.ProjectFile))
+ if (!FileUtilities.IsMetaprojectFilename(eventArgs.ProjectFile))
{
var checkContext = _checkContextFactory.CreateCheckContext(eventArgs.BuildEventContext!);
_buildCheckManager.ProjectFirstEncountered(
@@ -145,8 +145,6 @@ private void HandleProjectImportedEvent(ProjectImportedEventArgs eventArgs)
_checkContextFactory.CreateCheckContext(GetBuildEventContext(eventArgs)),
eventArgs);
- private bool IsMetaProjFile(string? projectFile) => projectFile?.EndsWith(".metaproj", StringComparison.OrdinalIgnoreCase) == true;
-
private readonly BuildCheckTracingData _tracingData = new BuildCheckTracingData();
private void HandleBuildFinishedEvent(BuildFinishedEventArgs eventArgs)
diff --git a/src/Build/Logging/TerminalLogger/TerminalLogger.cs b/src/Build/Logging/TerminalLogger/TerminalLogger.cs
index d7903825030..950bafb7025 100644
--- a/src/Build/Logging/TerminalLogger/TerminalLogger.cs
+++ b/src/Build/Logging/TerminalLogger/TerminalLogger.cs
@@ -725,12 +725,18 @@ private void ProjectStarted(object sender, ProjectStartedEventArgs e)
EvalContext evalContext = new(e.BuildEventContext);
string? targetFramework = null;
string? runtimeIdentifier = null;
+
if (_projectEvaluations.TryGetValue(evalContext, out EvalProjectInfo evalInfo))
{
targetFramework = evalInfo.TargetFramework;
runtimeIdentifier = evalInfo.RuntimeIdentifier;
}
- System.Diagnostics.Debug.Assert(evalInfo != default, "EvalProjectInfo should have been captured before ProjectStarted");
+
+ // Per-project metaproj files (e.g. MyProject.csproj.metaproj) are constructed
+ // directly without evaluation, so they won't have a matching ProjectEvaluationFinished event.
+ System.Diagnostics.Debug.Assert(
+ evalInfo != default || FileUtilities.IsMetaprojectFilename(e.ProjectFile),
+ "EvalProjectInfo should have been captured before ProjectStarted");
TerminalProjectInfo projectInfo = new(c, evalInfo, _createStopwatch?.Invoke());
_projects[c] = projectInfo;
diff --git a/src/Framework/FileUtilities.cs b/src/Framework/FileUtilities.cs
index c5bd6fcf376..b3e48a7ad1a 100644
--- a/src/Framework/FileUtilities.cs
+++ b/src/Framework/FileUtilities.cs
@@ -1340,7 +1340,7 @@ internal static bool IsDspFilename(string filename)
///
/// Returns true if the specified filename is a metaproject file (.metaproj), otherwise false.
///
- internal static bool IsMetaprojectFilename(string filename)
+ internal static bool IsMetaprojectFilename(string? filename)
{
return HasExtension(filename, ".metaproj");
}
@@ -1350,14 +1350,14 @@ internal static bool IsBinaryLogFilename(string filename)
return HasExtension(filename, ".binlog");
}
- private static bool HasExtension(string filename, string extension)
+ private static bool HasExtension(string? filename, string extension)
{
if (String.IsNullOrEmpty(filename))
{
return false;
}
- return filename.EndsWith(extension, PathComparison);
+ return filename!.EndsWith(extension, PathComparison);
}
///