Fix binlog replay with MSBuild 18.x and normalize obsolete constructor path by mscottford · Pull Request #329 · Buildalyzer/Buildalyzer · GitHub
Skip to content

Fix binlog replay with MSBuild 18.x and normalize obsolete constructor path - #329

Open
mscottford wants to merge 1 commit into
Buildalyzer:mainfrom
mscottford:fix/binlog-reader-and-path-normalization
Open

mscottford wants to merge 1 commit into
Buildalyzer:mainfrom
mscottford:fix/binlog-reader-and-path-normalization

Conversation

@mscottford

Copy link
Copy Markdown

Summary

Three related fixes for reading binlogs produced by the .NET 10 SDK (MSBuild 18.x):

  • AnalyzerManager.Analyze: replace StructuredLogger.BinLogReader with Microsoft.Build.Logging.BinaryLogReplayEventSource. The official MSBuild reader handles all 18.x event types (including AssemblyLoadBuildEventArgs), whereas BinLogReader from MSBuild.StructuredLogger may stop replaying mid-stream when it encounters unrecognised event types, leaving _evalulationResults empty so ProjectStarted falls back to null propertiesAndItems and produces no results.

  • EventProcessor.ProjectStarted: treat an empty e.Properties collection the same as null. BinLogReader creates ProjectStartedEventArgs with Properties set to a non-null but empty collection for binlog format 14+ events, so the existing e.Properties is null check always fell through to the old-format path, using the empty collection instead of the populated evaluation-phase results. This left ProjectReferences empty after a binlog round-trip.

  • AnalyzerManager obsolete string constructor: wrap the path in Path.GetFullPath() before passing it to IOPath.Parse(), restoring the behaviour of the previous NormalizePath implementation that was lost when IOPath was introduced.

Regression tests are included for the first two fixes.

Test plan

  • SdkProjectWithProjectReferenceGetsReferencesFromBinaryLog — builds a project with a project reference, replays the binlog, asserts ProjectReferences are populated
  • SdkProjectWithProjectReferenceGetsReferencesFromChildSideBinaryLog — same but using child-side /bl: logging
  • Existing integration test suite passes

🤖 Generated with Claude Code

…r path

Three related fixes for reading binlogs produced by the .NET 10 SDK:

1. AnalyzerManager.Analyze: replace StructuredLogger.BinLogReader with
   Microsoft.Build.Logging.BinaryLogReplayEventSource. The official MSBuild
   reader handles all 18.x event types (AssemblyLoadBuildEventArgs etc.),
   whereas BinLogReader from MSBuild.StructuredLogger may stop replaying
   mid-stream when it encounters event types it does not recognise, leaving
   _evalulationResults empty so ProjectStarted falls back to null
   propertiesAndItems and produces no results.

2. EventProcessor.ProjectStarted: treat an empty e.Properties collection
   the same as null. BinLogReader creates ProjectStartedEventArgs with
   Properties set to a non-null but empty collection for binlog format 14+
   events, so the existing `e.Properties is null` check always fell through
   to the old-format path, using the empty collection instead of the
   populated evaluation-phase results.

3. AnalyzerManager obsolete string constructor: wrap the path in
   Path.GetFullPath() before passing it to IOPath.Parse(), restoring the
   behaviour of the previous NormalizePath implementation that was removed
   when IOPath was introduced.

Regression tests added for fixes 1 and 2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mscottford added a commit to mscottford/Buildalyzer that referenced this pull request Apr 17, 2026
Fix binlog replay with MSBuild 18.x and normalize obsolete constructor path.
See Buildalyzer#329.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For readablilty, I suggest you create an file
Extensions/System.Collections.IEnumerable.cs, containing te following:

namespace System.Collections;

internal static class EnumerableExtensions
{
    extension(IEnumerable? enumerable)
    {
        public bool HasNone
            => enumerable?.GetEnumerator().MoveNext() is not true;
    }
}

Than you don't need the explanation at all, als e.Proparties.HasNone, tells the story.

Note, that <LangVersion>14</LangVersion> has to be added, which is a good thing anyway.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some consideration: Lets first merge #331 what does part the changes you want, and then follow with the rest.

/// </summary>
[Test]
public void SdkProjectWithProjectReferenceGetsReferencesFromBinaryLog(
[ValueSource(nameof(Preferences))] EnvironmentPreference preference)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use TestTools.Context.ForProject

@Corniel Corniel left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice improvements, and good spots! I'd like some (ecstatic) changes, but nothing to dramatic.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Buildalyzer’s binary log replay against newer MSBuild (18.x) binlogs and restores expected path normalization behavior, with integration regression tests to prevent regressions when analyzing from binlogs.

Changes:

  • Switch binlog replay in AnalyzerManager.Analyze to Microsoft.Build.Logging.BinaryLogReplayEventSource for MSBuild 18.x event compatibility.
  • In EventProcessor.ProjectStarted, fall back to evaluation-phase properties/items when ProjectStartedEventArgs.Properties is null or empty.
  • Normalize the obsolete AnalyzerManager(string) constructor path via Path.GetFullPath() before IOPath.Parse(), and add regression integration tests for binlog replay.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
tests/Buildalyzer.Tests/Integration/SimpleProjectsFixture.cs Adds regression integration tests validating ProjectReferences survive binlog replay (host-side and child-side logging).
src/Buildalyzer/Logging/EventProcessor.cs Treats empty ProjectStartedEventArgs.Properties like null to correctly use evaluation results for newer binlog formats.
src/Buildalyzer/AnalyzerManager.cs Uses BinaryLogReplayEventSource for replay and restores full-path normalization in the obsolete string constructor.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +398 to +399
string binLogPath = Path.ChangeExtension(Path.GetTempFileName(), ".binlog");
analyzer.AddBinaryLogger(binLogPath);

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path.GetTempFileName() creates a real temp file on disk. Using Path.ChangeExtension(...) means the original .tmp file is left behind and never deleted. Prefer generating a unique path under Path.GetTempPath() (e.g., with a GUID) to avoid leaking temp files, or delete the original temp file as well.

Copilot uses AI. Check for mistakes.
// Given
StringWriter log = new StringWriter();
IProjectAnalyzer analyzer = GetProjectAnalyzer(@"SdkNetCore2ProjectWithReference\SdkNetCore2ProjectWithReference.csproj", log);
string binLogPath = Path.ChangeExtension(Path.GetTempFileName(), ".binlog");

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same temp-file issue as above: Path.GetTempFileName() creates a .tmp file that is not deleted when you switch extensions. Consider generating a .binlog path without creating the .tmp file, or ensure the original temp file is removed.

Suggested change
string binLogPath = Path.ChangeExtension(Path.GetTempFileName(), ".binlog");
string binLogPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".binlog");

Copilot uses AI. Check for mistakes.
Preference = preference
};
// Child-side binary logging: the build process itself writes the binlog
options.Arguments.Add($"/bl:{binLogPath}");

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bl: paths containing spaces need quoting. Since EnvironmentOptions.Arguments are concatenated via string.Join(" ", ...) without automatic quoting, an unquoted temp path like C:\Users\First Last\... can make this test flaky. Consider using /bl:"{binLogPath}" (or equivalent) so MSBuild receives the full path as one argument.

Suggested change
options.Arguments.Add($"/bl:{binLogPath}");
options.Arguments.Add($"/bl:\"{binLogPath}\"");

Copilot uses AI. Check for mistakes.
Comment on lines +103 to 109

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnalyzerManager.Analyze no longer uses MSBuild.StructuredLogger, but this file still has extern alias StructuredLogger; and using StructuredLogger::... at the top. These are now unused and can produce warnings/confusion; consider removing the alias/using (and any related package aliasing) now that replay uses Microsoft.Build.Logging.BinaryLogReplayEventSource.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants