From 776ec14bc022f451a99fc7ecf8c20964cd109215 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Jun 2025 20:23:30 +0000 Subject: [PATCH 01/18] Implement internally-incremental GenerateDepsFile and GenerateRuntimeConfigurationFiles Co-authored-by: marcpopMSFT <12663534+marcpopMSFT@users.noreply.github.com> --- .../GivenAGenerateDepsFile.cs | 83 +++++++++++++++++++ ...GivenAGenerateRuntimeConfigurationFiles.cs | 54 ++++++++++++ .../GenerateDepsFile.cs | 32 ++++++- .../GenerateRuntimeConfigurationFiles.cs | 29 ++++++- 4 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateDepsFile.cs diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateDepsFile.cs new file mode 100644 index 000000000000..b3ce2c8f877f --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateDepsFile.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.NET.TestFramework; +using Xunit; + +namespace Microsoft.NET.Build.Tasks.UnitTests +{ + public class GivenAGenerateDepsFile + { + private readonly string _depsFilePath; + + public GivenAGenerateDepsFile() + { + string testTempDir = Path.Combine(Path.GetTempPath(), "dotnetSdkTests"); + Directory.CreateDirectory(testTempDir); + _depsFilePath = Path.Combine(testTempDir, nameof(GivenAGenerateDepsFile) + ".deps.json"); + if (File.Exists(_depsFilePath)) + { + File.Delete(_depsFilePath); + } + } + + [Fact] + public void ItDoesNotOverwriteFileWithSameContent() + { + var task = new TestableGenerateDepsFile + { + BuildEngine = new MockNeverCacheBuildEngine4(), + ProjectPath = "TestProject.csproj", + DepsFilePath = _depsFilePath, + TargetFramework = "net8.0", + AssemblyName = "TestProject", + AssemblyExtension = ".dll", + AssemblyVersion = "1.0.0.0", + IncludeMainProject = true, + CompileReferences = new MockTaskItem[0], + ResolvedNuGetFiles = new MockTaskItem[0], + ResolvedRuntimeTargetsFiles = new MockTaskItem[0], + RuntimeGraphPath = "" + }; + + // Execute task first time + task.PublicExecuteCore(); + var firstWriteTime = File.GetLastWriteTimeUtc(_depsFilePath); + + // Wait a bit to ensure timestamp would change if file is rewritten + Thread.Sleep(100); + + // Execute task again with same configuration + var task2 = new TestableGenerateDepsFile + { + BuildEngine = new MockNeverCacheBuildEngine4(), + ProjectPath = "TestProject.csproj", + DepsFilePath = _depsFilePath, + TargetFramework = "net8.0", + AssemblyName = "TestProject", + AssemblyExtension = ".dll", + AssemblyVersion = "1.0.0.0", + IncludeMainProject = true, + CompileReferences = new MockTaskItem[0], + ResolvedNuGetFiles = new MockTaskItem[0], + ResolvedRuntimeTargetsFiles = new MockTaskItem[0], + RuntimeGraphPath = "" + }; + + task2.PublicExecuteCore(); + var secondWriteTime = File.GetLastWriteTimeUtc(_depsFilePath); + + // File should not have been rewritten when content is the same + secondWriteTime.Should().Be(firstWriteTime, "file should not be rewritten when content is unchanged"); + } + + private class TestableGenerateDepsFile : GenerateDepsFile + { + public void PublicExecuteCore() + { + base.ExecuteCore(); + } + } + } +} \ No newline at end of file diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRuntimeConfigurationFiles.cs index 6da7349172c5..f90b407c7533 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRuntimeConfigurationFiles.cs @@ -218,6 +218,60 @@ public void GivenTargetMonikerItGeneratesShortName() }}"); } + [Fact] + public void ItDoesNotOverwriteFileWithSameContent() + { + var task = new TestableGenerateRuntimeConfigurationFiles + { + BuildEngine = new MockNeverCacheBuildEngine4(), + TargetFrameworkMoniker = $".NETCoreApp,Version=v{ToolsetInfo.CurrentTargetFrameworkVersion}", + RuntimeConfigPath = _runtimeConfigPath, + RuntimeFrameworks = new[] + { + new MockTaskItem( + "Microsoft.NETCore.App", + new Dictionary + { + {"FrameworkName", "Microsoft.NETCore.App"}, {"Version", $"{ToolsetInfo.CurrentTargetFrameworkVersion}.0"} + } + ) + }, + RollForward = "LatestMinor" + }; + + // Execute task first time + task.PublicExecuteCore(); + var firstWriteTime = File.GetLastWriteTimeUtc(_runtimeConfigPath); + + // Wait a bit to ensure timestamp would change if file is rewritten + Thread.Sleep(100); + + // Execute task again with same configuration + var task2 = new TestableGenerateRuntimeConfigurationFiles + { + BuildEngine = new MockNeverCacheBuildEngine4(), + TargetFrameworkMoniker = $".NETCoreApp,Version=v{ToolsetInfo.CurrentTargetFrameworkVersion}", + RuntimeConfigPath = _runtimeConfigPath, + RuntimeFrameworks = new[] + { + new MockTaskItem( + "Microsoft.NETCore.App", + new Dictionary + { + {"FrameworkName", "Microsoft.NETCore.App"}, {"Version", $"{ToolsetInfo.CurrentTargetFrameworkVersion}.0"} + } + ) + }, + RollForward = "LatestMinor" + }; + + task2.PublicExecuteCore(); + var secondWriteTime = File.GetLastWriteTimeUtc(_runtimeConfigPath); + + // File should not have been rewritten when content is the same + secondWriteTime.Should().Be(firstWriteTime, "file should not be rewritten when content is unchanged"); + } + private class TestableGenerateRuntimeConfigurationFiles : GenerateRuntimeConfigurationFiles { public void PublicExecuteCore() diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index 8b89525c9fd1..ba77442a9f22 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -10,6 +10,7 @@ using NuGet.Packaging.Core; using NuGet.ProjectModel; using NuGet.RuntimeModel; +using System.IO.Hashing; namespace Microsoft.NET.Build.Tasks { @@ -254,9 +255,36 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) DependencyContext dependencyContext = builder.Build(UserRuntimeAssemblies); var writer = new DependencyContextWriter(); - using (var fileStream = File.Create(depsFilePath)) + + // Generate content in memory first + byte[] newContent; + using (var memoryStream = new MemoryStream()) { - writer.Write(dependencyContext, fileStream); + writer.Write(dependencyContext, memoryStream); + newContent = memoryStream.ToArray(); + } + + bool shouldWriteFile = true; + // If file exists, check if content is different + if (File.Exists(depsFilePath)) + { + byte[] existingContent = File.ReadAllBytes(depsFilePath); + + // Hash both contents using XxHash64 for fast comparison + var existingHash = XxHash64.Hash(existingContent); + var newHash = XxHash64.Hash(newContent); + + // If hashes are equal, content is the same - don't write + if (existingHash.SequenceEqual(newHash)) + { + shouldWriteFile = false; + } + } + + if (shouldWriteFile) + { + // Write the new content to file + File.WriteAllBytes(depsFilePath, newContent); } _filesWritten.Add(new TaskItem(depsFilePath)); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index d7eaad9c6fdb..d1414c1f0b65 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -10,6 +10,8 @@ using Newtonsoft.Json.Serialization; using NuGet.Frameworks; using NuGet.ProjectModel; +using System.IO.Hashing; +using System.Text; namespace Microsoft.NET.Build.Tasks { @@ -390,10 +392,33 @@ private static void WriteToJsonFile(string fileName, object value) DefaultValueHandling = DefaultValueHandling.Ignore }; - using (JsonTextWriter writer = new(new StreamWriter(File.Create(fileName)))) + // Generate content in memory first + string newContent; + using (var stringWriter = new StringWriter()) + using (var jsonWriter = new JsonTextWriter(stringWriter)) { - serializer.Serialize(writer, value); + serializer.Serialize(jsonWriter, value); + newContent = stringWriter.ToString(); } + + // If file exists, check if content is different + if (File.Exists(fileName)) + { + string existingContent = File.ReadAllText(fileName); + + // Hash both contents using XxHash64 for fast comparison + var existingHash = XxHash64.Hash(Encoding.UTF8.GetBytes(existingContent)); + var newHash = XxHash64.Hash(Encoding.UTF8.GetBytes(newContent)); + + // If hashes are equal, content is the same - don't write + if (existingHash.SequenceEqual(newHash)) + { + return; + } + } + + // Write the new content to file + File.WriteAllText(fileName, newContent); } } } From 8c75475117444c32d55f2b63f41a949cb2b9f187 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:59:01 +0000 Subject: [PATCH 02/18] Optimize hash computation to use streaming approach instead of loading full content Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../GenerateDepsFile.cs | 41 +++++++++------- .../GenerateRuntimeConfigurationFiles.cs | 48 ++++++++++++------- 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index ba77442a9f22..360e23678ab3 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -256,26 +256,32 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) var writer = new DependencyContextWriter(); - // Generate content in memory first - byte[] newContent; - using (var memoryStream = new MemoryStream()) - { - writer.Write(dependencyContext, memoryStream); - newContent = memoryStream.ToArray(); - } - bool shouldWriteFile = true; - // If file exists, check if content is different + // If file exists, check if content is different using streaming hash comparison if (File.Exists(depsFilePath)) { - byte[] existingContent = File.ReadAllBytes(depsFilePath); - - // Hash both contents using XxHash64 for fast comparison - var existingHash = XxHash64.Hash(existingContent); - var newHash = XxHash64.Hash(newContent); + // Hash existing file content using streaming approach + Span existingHashBuffer = stackalloc byte[8]; // XxHash64 produces 8-byte hash + var existingHasher = new XxHash64(); + using (var existingStream = File.OpenRead(depsFilePath)) + { + existingHasher.Append(existingStream); + } + existingHasher.GetCurrentHash(existingHashBuffer); + + // Hash new content using streaming approach + Span newHashBuffer = stackalloc byte[8]; // XxHash64 produces 8-byte hash + var newHasher = new XxHash64(); + using (var memoryStream = new MemoryStream()) + { + writer.Write(dependencyContext, memoryStream); + memoryStream.Position = 0; + newHasher.Append(memoryStream); + } + newHasher.GetCurrentHash(newHashBuffer); // If hashes are equal, content is the same - don't write - if (existingHash.SequenceEqual(newHash)) + if (existingHashBuffer.SequenceEqual(newHashBuffer)) { shouldWriteFile = false; } @@ -284,7 +290,10 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) if (shouldWriteFile) { // Write the new content to file - File.WriteAllBytes(depsFilePath, newContent); + using (var fileStream = File.Create(depsFilePath)) + { + writer.Write(dependencyContext, fileStream); + } } _filesWritten.Add(new TaskItem(depsFilePath)); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index d1414c1f0b65..0fa49f8330b2 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -392,33 +392,47 @@ private static void WriteToJsonFile(string fileName, object value) DefaultValueHandling = DefaultValueHandling.Ignore }; - // Generate content in memory first - string newContent; - using (var stringWriter = new StringWriter()) - using (var jsonWriter = new JsonTextWriter(stringWriter)) - { - serializer.Serialize(jsonWriter, value); - newContent = stringWriter.ToString(); - } - - // If file exists, check if content is different + bool shouldWriteFile = true; + // If file exists, check if content is different using streaming hash comparison if (File.Exists(fileName)) { - string existingContent = File.ReadAllText(fileName); - - // Hash both contents using XxHash64 for fast comparison - var existingHash = XxHash64.Hash(Encoding.UTF8.GetBytes(existingContent)); - var newHash = XxHash64.Hash(Encoding.UTF8.GetBytes(newContent)); + // Hash existing file content using streaming approach + Span existingHashBuffer = stackalloc byte[8]; // XxHash64 produces 8-byte hash + var existingHasher = new XxHash64(); + using (var existingStream = File.OpenRead(fileName)) + { + existingHasher.Append(existingStream); + } + existingHasher.GetCurrentHash(existingHashBuffer); + + // Hash new content using streaming approach + Span newHashBuffer = stackalloc byte[8]; // XxHash64 produces 8-byte hash + var newHasher = new XxHash64(); + using (var memoryStream = new MemoryStream()) + using (var streamWriter = new StreamWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) + using (var jsonWriter = new JsonTextWriter(streamWriter)) + { + serializer.Serialize(jsonWriter, value); + jsonWriter.Flush(); + streamWriter.Flush(); + memoryStream.Position = 0; + newHasher.Append(memoryStream); + } + newHasher.GetCurrentHash(newHashBuffer); // If hashes are equal, content is the same - don't write - if (existingHash.SequenceEqual(newHash)) + if (existingHashBuffer.SequenceEqual(newHashBuffer)) { return; } } // Write the new content to file - File.WriteAllText(fileName, newContent); + using (var fileWriter = new StreamWriter(fileName, false, Encoding.UTF8)) + using (var jsonWriter = new JsonTextWriter(fileWriter)) + { + serializer.Serialize(jsonWriter, value); + } } } } From a2b59004760330693a87ee5614b715a3c3efdf98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Jun 2025 03:05:23 +0000 Subject: [PATCH 03/18] Replace hardcoded hash buffer size with XxHash64.HashSizeInBytes property Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs | 4 ++-- .../GenerateRuntimeConfigurationFiles.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index 360e23678ab3..954ab1d6623d 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -261,7 +261,7 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) if (File.Exists(depsFilePath)) { // Hash existing file content using streaming approach - Span existingHashBuffer = stackalloc byte[8]; // XxHash64 produces 8-byte hash + Span existingHashBuffer = stackalloc byte[XxHash64.HashSizeInBytes]; var existingHasher = new XxHash64(); using (var existingStream = File.OpenRead(depsFilePath)) { @@ -270,7 +270,7 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) existingHasher.GetCurrentHash(existingHashBuffer); // Hash new content using streaming approach - Span newHashBuffer = stackalloc byte[8]; // XxHash64 produces 8-byte hash + Span newHashBuffer = stackalloc byte[XxHash64.HashSizeInBytes]; var newHasher = new XxHash64(); using (var memoryStream = new MemoryStream()) { diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index 0fa49f8330b2..73ac8a48b5b3 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -397,7 +397,7 @@ private static void WriteToJsonFile(string fileName, object value) if (File.Exists(fileName)) { // Hash existing file content using streaming approach - Span existingHashBuffer = stackalloc byte[8]; // XxHash64 produces 8-byte hash + Span existingHashBuffer = stackalloc byte[XxHash64.HashSizeInBytes]; var existingHasher = new XxHash64(); using (var existingStream = File.OpenRead(fileName)) { @@ -406,7 +406,7 @@ private static void WriteToJsonFile(string fileName, object value) existingHasher.GetCurrentHash(existingHashBuffer); // Hash new content using streaming approach - Span newHashBuffer = stackalloc byte[8]; // XxHash64 produces 8-byte hash + Span newHashBuffer = stackalloc byte[XxHash64.HashSizeInBytes]; var newHasher = new XxHash64(); using (var memoryStream = new MemoryStream()) using (var streamWriter = new StreamWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) From ba29265627fd53ca097108d477be4d7f65c43051 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Jun 2025 03:12:39 +0000 Subject: [PATCH 04/18] Use MemoryStream.CopyTo instead of double serialization for incremental file writing Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../GenerateDepsFile.cs | 21 ++++++----- .../GenerateRuntimeConfigurationFiles.cs | 35 +++++++++++-------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index 954ab1d6623d..e7d708e5aef9 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -257,6 +257,12 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) var writer = new DependencyContextWriter(); bool shouldWriteFile = true; + MemoryStream contentStream = null; + + // Generate new content + contentStream = new MemoryStream(); + writer.Write(dependencyContext, contentStream); + // If file exists, check if content is different using streaming hash comparison if (File.Exists(depsFilePath)) { @@ -272,12 +278,8 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) // Hash new content using streaming approach Span newHashBuffer = stackalloc byte[XxHash64.HashSizeInBytes]; var newHasher = new XxHash64(); - using (var memoryStream = new MemoryStream()) - { - writer.Write(dependencyContext, memoryStream); - memoryStream.Position = 0; - newHasher.Append(memoryStream); - } + contentStream.Position = 0; + newHasher.Append(contentStream); newHasher.GetCurrentHash(newHashBuffer); // If hashes are equal, content is the same - don't write @@ -289,12 +291,15 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) if (shouldWriteFile) { - // Write the new content to file + // Write the new content to file using CopyTo using (var fileStream = File.Create(depsFilePath)) { - writer.Write(dependencyContext, fileStream); + contentStream.Position = 0; + contentStream.CopyTo(fileStream); } } + + contentStream?.Dispose(); _filesWritten.Add(new TaskItem(depsFilePath)); if (ValidRuntimeIdentifierPlatformsForAssets != null) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index 73ac8a48b5b3..bb39c84036fc 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -393,6 +393,18 @@ private static void WriteToJsonFile(string fileName, object value) }; bool shouldWriteFile = true; + MemoryStream contentStream = null; + + // Generate new content + contentStream = new MemoryStream(); + using (var streamWriter = new StreamWriter(contentStream, Encoding.UTF8, leaveOpen: true)) + using (var jsonWriter = new JsonTextWriter(streamWriter)) + { + serializer.Serialize(jsonWriter, value); + jsonWriter.Flush(); + streamWriter.Flush(); + } + // If file exists, check if content is different using streaming hash comparison if (File.Exists(fileName)) { @@ -408,31 +420,26 @@ private static void WriteToJsonFile(string fileName, object value) // Hash new content using streaming approach Span newHashBuffer = stackalloc byte[XxHash64.HashSizeInBytes]; var newHasher = new XxHash64(); - using (var memoryStream = new MemoryStream()) - using (var streamWriter = new StreamWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) - using (var jsonWriter = new JsonTextWriter(streamWriter)) - { - serializer.Serialize(jsonWriter, value); - jsonWriter.Flush(); - streamWriter.Flush(); - memoryStream.Position = 0; - newHasher.Append(memoryStream); - } + contentStream.Position = 0; + newHasher.Append(contentStream); newHasher.GetCurrentHash(newHashBuffer); // If hashes are equal, content is the same - don't write if (existingHashBuffer.SequenceEqual(newHashBuffer)) { + contentStream?.Dispose(); return; } } - // Write the new content to file - using (var fileWriter = new StreamWriter(fileName, false, Encoding.UTF8)) - using (var jsonWriter = new JsonTextWriter(fileWriter)) + // Write the new content to file using CopyTo + using (var fileStream = File.Create(fileName)) { - serializer.Serialize(jsonWriter, value); + contentStream.Position = 0; + contentStream.CopyTo(fileStream); } + + contentStream?.Dispose(); } } } From 40ca057357adcf5d8ebe51f89fb36eb335e888e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Jun 2025 03:25:49 +0000 Subject: [PATCH 05/18] Fix property name: use HashLengthInBytes instead of HashSizeInBytes Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs | 4 ++-- .../GenerateRuntimeConfigurationFiles.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index e7d708e5aef9..3e473c70141b 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -267,7 +267,7 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) if (File.Exists(depsFilePath)) { // Hash existing file content using streaming approach - Span existingHashBuffer = stackalloc byte[XxHash64.HashSizeInBytes]; + Span existingHashBuffer = stackalloc byte[XxHash64.HashLengthInBytes]; var existingHasher = new XxHash64(); using (var existingStream = File.OpenRead(depsFilePath)) { @@ -276,7 +276,7 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) existingHasher.GetCurrentHash(existingHashBuffer); // Hash new content using streaming approach - Span newHashBuffer = stackalloc byte[XxHash64.HashSizeInBytes]; + Span newHashBuffer = stackalloc byte[XxHash64.HashLengthInBytes]; var newHasher = new XxHash64(); contentStream.Position = 0; newHasher.Append(contentStream); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index bb39c84036fc..cb3ec9dc5e8e 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -409,7 +409,7 @@ private static void WriteToJsonFile(string fileName, object value) if (File.Exists(fileName)) { // Hash existing file content using streaming approach - Span existingHashBuffer = stackalloc byte[XxHash64.HashSizeInBytes]; + Span existingHashBuffer = stackalloc byte[XxHash64.HashLengthInBytes]; var existingHasher = new XxHash64(); using (var existingStream = File.OpenRead(fileName)) { @@ -418,7 +418,7 @@ private static void WriteToJsonFile(string fileName, object value) existingHasher.GetCurrentHash(existingHashBuffer); // Hash new content using streaming approach - Span newHashBuffer = stackalloc byte[XxHash64.HashSizeInBytes]; + Span newHashBuffer = stackalloc byte[XxHash64.HashLengthInBytes]; var newHasher = new XxHash64(); contentStream.Position = 0; newHasher.Append(contentStream); From b27ba058386d68dc00cc5b7ad5cc00df60b9698d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:21:06 +0000 Subject: [PATCH 06/18] Fix HashLengthInBytes to use instance property instead of static access Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs | 7 +++++-- .../GenerateRuntimeConfigurationFiles.cs | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index 3e473c70141b..4820048ab6a7 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -266,8 +266,11 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) // If file exists, check if content is different using streaming hash comparison if (File.Exists(depsFilePath)) { + // Get hash length from instance + var hashLength = new XxHash64().HashLengthInBytes; + // Hash existing file content using streaming approach - Span existingHashBuffer = stackalloc byte[XxHash64.HashLengthInBytes]; + Span existingHashBuffer = stackalloc byte[hashLength]; var existingHasher = new XxHash64(); using (var existingStream = File.OpenRead(depsFilePath)) { @@ -276,7 +279,7 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) existingHasher.GetCurrentHash(existingHashBuffer); // Hash new content using streaming approach - Span newHashBuffer = stackalloc byte[XxHash64.HashLengthInBytes]; + Span newHashBuffer = stackalloc byte[hashLength]; var newHasher = new XxHash64(); contentStream.Position = 0; newHasher.Append(contentStream); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index cb3ec9dc5e8e..0e1a8e1b41b1 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -408,8 +408,11 @@ private static void WriteToJsonFile(string fileName, object value) // If file exists, check if content is different using streaming hash comparison if (File.Exists(fileName)) { + // Get hash length from instance + var hashLength = new XxHash64().HashLengthInBytes; + // Hash existing file content using streaming approach - Span existingHashBuffer = stackalloc byte[XxHash64.HashLengthInBytes]; + Span existingHashBuffer = stackalloc byte[hashLength]; var existingHasher = new XxHash64(); using (var existingStream = File.OpenRead(fileName)) { @@ -418,7 +421,7 @@ private static void WriteToJsonFile(string fileName, object value) existingHasher.GetCurrentHash(existingHashBuffer); // Hash new content using streaming approach - Span newHashBuffer = stackalloc byte[XxHash64.HashLengthInBytes]; + Span newHashBuffer = stackalloc byte[hashLength]; var newHasher = new XxHash64(); contentStream.Position = 0; newHasher.Append(contentStream); From 1ebc5a53bf0e25fa92add32e58920032c7d96a7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Jun 2025 22:52:31 +0000 Subject: [PATCH 07/18] Fix build errors: update StreamWriter constructor and fix shouldWriteFile variable usage Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../GenerateRuntimeConfigurationFiles.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index 0e1a8e1b41b1..390a41045273 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -397,7 +397,7 @@ private static void WriteToJsonFile(string fileName, object value) // Generate new content contentStream = new MemoryStream(); - using (var streamWriter = new StreamWriter(contentStream, Encoding.UTF8, leaveOpen: true)) + using (var streamWriter = new StreamWriter(contentStream, Encoding.UTF8, 1024, true)) using (var jsonWriter = new JsonTextWriter(streamWriter)) { serializer.Serialize(jsonWriter, value); @@ -430,16 +430,18 @@ private static void WriteToJsonFile(string fileName, object value) // If hashes are equal, content is the same - don't write if (existingHashBuffer.SequenceEqual(newHashBuffer)) { - contentStream?.Dispose(); - return; + shouldWriteFile = false; } } - // Write the new content to file using CopyTo - using (var fileStream = File.Create(fileName)) + if (shouldWriteFile) { - contentStream.Position = 0; - contentStream.CopyTo(fileStream); + // Write the new content to file using CopyTo + using (var fileStream = File.Create(fileName)) + { + contentStream.Position = 0; + contentStream.CopyTo(fileStream); + } } contentStream?.Dispose(); From 4d37482db3a8228ec19df548b443596b760f8557 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:36:31 +0000 Subject: [PATCH 08/18] Improve resource management and optimize hash length access in incremental tasks Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../GenerateDepsFile.cs | 72 ++++++++--------- .../GenerateRuntimeConfigurationFiles.cs | 80 +++++++++---------- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index 4820048ab6a7..3cc958b64a1c 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -257,52 +257,52 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) var writer = new DependencyContextWriter(); bool shouldWriteFile = true; - MemoryStream contentStream = null; // Generate new content - contentStream = new MemoryStream(); - writer.Write(dependencyContext, contentStream); - - // If file exists, check if content is different using streaming hash comparison - if (File.Exists(depsFilePath)) + using (var contentStream = new MemoryStream()) { - // Get hash length from instance - var hashLength = new XxHash64().HashLengthInBytes; - - // Hash existing file content using streaming approach - Span existingHashBuffer = stackalloc byte[hashLength]; - var existingHasher = new XxHash64(); - using (var existingStream = File.OpenRead(depsFilePath)) - { - existingHasher.Append(existingStream); - } - existingHasher.GetCurrentHash(existingHashBuffer); - - // Hash new content using streaming approach - Span newHashBuffer = stackalloc byte[hashLength]; - var newHasher = new XxHash64(); - contentStream.Position = 0; - newHasher.Append(contentStream); - newHasher.GetCurrentHash(newHashBuffer); + writer.Write(dependencyContext, contentStream); - // If hashes are equal, content is the same - don't write - if (existingHashBuffer.SequenceEqual(newHashBuffer)) + // If file exists, check if content is different using streaming hash comparison + if (File.Exists(depsFilePath)) { - shouldWriteFile = false; + // Get hash length from a single instance to avoid unnecessary allocations + using var hasher = new XxHash64(); + var hashLength = hasher.HashLengthInBytes; + + // Hash existing file content using streaming approach + Span existingHashBuffer = stackalloc byte[hashLength]; + var existingHasher = new XxHash64(); + using (var existingStream = File.OpenRead(depsFilePath)) + { + existingHasher.Append(existingStream); + } + existingHasher.GetCurrentHash(existingHashBuffer); + + // Hash new content using streaming approach + Span newHashBuffer = stackalloc byte[hashLength]; + var newHasher = new XxHash64(); + contentStream.Position = 0; + newHasher.Append(contentStream); + newHasher.GetCurrentHash(newHashBuffer); + + // If hashes are equal, content is the same - don't write + if (existingHashBuffer.SequenceEqual(newHashBuffer)) + { + shouldWriteFile = false; + } } - } - if (shouldWriteFile) - { - // Write the new content to file using CopyTo - using (var fileStream = File.Create(depsFilePath)) + if (shouldWriteFile) { - contentStream.Position = 0; - contentStream.CopyTo(fileStream); + // Write the new content to file using CopyTo + using (var fileStream = File.Create(depsFilePath)) + { + contentStream.Position = 0; + contentStream.CopyTo(fileStream); + } } } - - contentStream?.Dispose(); _filesWritten.Add(new TaskItem(depsFilePath)); if (ValidRuntimeIdentifierPlatformsForAssets != null) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index 390a41045273..806eec2284bf 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -393,58 +393,58 @@ private static void WriteToJsonFile(string fileName, object value) }; bool shouldWriteFile = true; - MemoryStream contentStream = null; // Generate new content - contentStream = new MemoryStream(); - using (var streamWriter = new StreamWriter(contentStream, Encoding.UTF8, 1024, true)) - using (var jsonWriter = new JsonTextWriter(streamWriter)) + using (var contentStream = new MemoryStream()) { - serializer.Serialize(jsonWriter, value); - jsonWriter.Flush(); - streamWriter.Flush(); - } - - // If file exists, check if content is different using streaming hash comparison - if (File.Exists(fileName)) - { - // Get hash length from instance - var hashLength = new XxHash64().HashLengthInBytes; - - // Hash existing file content using streaming approach - Span existingHashBuffer = stackalloc byte[hashLength]; - var existingHasher = new XxHash64(); - using (var existingStream = File.OpenRead(fileName)) + using (var streamWriter = new StreamWriter(contentStream, Encoding.UTF8, 1024, true)) + using (var jsonWriter = new JsonTextWriter(streamWriter)) { - existingHasher.Append(existingStream); + serializer.Serialize(jsonWriter, value); + jsonWriter.Flush(); + streamWriter.Flush(); } - existingHasher.GetCurrentHash(existingHashBuffer); - - // Hash new content using streaming approach - Span newHashBuffer = stackalloc byte[hashLength]; - var newHasher = new XxHash64(); - contentStream.Position = 0; - newHasher.Append(contentStream); - newHasher.GetCurrentHash(newHashBuffer); - // If hashes are equal, content is the same - don't write - if (existingHashBuffer.SequenceEqual(newHashBuffer)) + // If file exists, check if content is different using streaming hash comparison + if (File.Exists(fileName)) { - shouldWriteFile = false; + // Get hash length from a single instance to avoid unnecessary allocations + using var hasher = new XxHash64(); + var hashLength = hasher.HashLengthInBytes; + + // Hash existing file content using streaming approach + Span existingHashBuffer = stackalloc byte[hashLength]; + var existingHasher = new XxHash64(); + using (var existingStream = File.OpenRead(fileName)) + { + existingHasher.Append(existingStream); + } + existingHasher.GetCurrentHash(existingHashBuffer); + + // Hash new content using streaming approach + Span newHashBuffer = stackalloc byte[hashLength]; + var newHasher = new XxHash64(); + contentStream.Position = 0; + newHasher.Append(contentStream); + newHasher.GetCurrentHash(newHashBuffer); + + // If hashes are equal, content is the same - don't write + if (existingHashBuffer.SequenceEqual(newHashBuffer)) + { + shouldWriteFile = false; + } } - } - if (shouldWriteFile) - { - // Write the new content to file using CopyTo - using (var fileStream = File.Create(fileName)) + if (shouldWriteFile) { - contentStream.Position = 0; - contentStream.CopyTo(fileStream); + // Write the new content to file using CopyTo + using (var fileStream = File.Create(fileName)) + { + contentStream.Position = 0; + contentStream.CopyTo(fileStream); + } } } - - contentStream?.Dispose(); } } } From 18f7a20d6f74b35bf746eace88e8b67e3db8876c Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 19 Jun 2025 20:30:28 -0500 Subject: [PATCH 09/18] fix compilation --- .../Microsoft.NET.Build.Tasks/GenerateDepsFile.cs | 12 ++++++------ .../GenerateRuntimeConfigurationFiles.cs | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index 3cc958b64a1c..21f4681cf780 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -255,21 +255,21 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) DependencyContext dependencyContext = builder.Build(UserRuntimeAssemblies); var writer = new DependencyContextWriter(); - + bool shouldWriteFile = true; - + // Generate new content using (var contentStream = new MemoryStream()) { writer.Write(dependencyContext, contentStream); - + // If file exists, check if content is different using streaming hash comparison if (File.Exists(depsFilePath)) { // Get hash length from a single instance to avoid unnecessary allocations - using var hasher = new XxHash64(); + var hasher = new XxHash64(); var hashLength = hasher.HashLengthInBytes; - + // Hash existing file content using streaming approach Span existingHashBuffer = stackalloc byte[hashLength]; var existingHasher = new XxHash64(); @@ -285,7 +285,7 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) contentStream.Position = 0; newHasher.Append(contentStream); newHasher.GetCurrentHash(newHashBuffer); - + // If hashes are equal, content is the same - don't write if (existingHashBuffer.SequenceEqual(newHashBuffer)) { diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index 806eec2284bf..60d8e5768fe2 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -393,7 +393,7 @@ private static void WriteToJsonFile(string fileName, object value) }; bool shouldWriteFile = true; - + // Generate new content using (var contentStream = new MemoryStream()) { @@ -404,14 +404,14 @@ private static void WriteToJsonFile(string fileName, object value) jsonWriter.Flush(); streamWriter.Flush(); } - + // If file exists, check if content is different using streaming hash comparison if (File.Exists(fileName)) { // Get hash length from a single instance to avoid unnecessary allocations - using var hasher = new XxHash64(); + var hasher = new XxHash64(); var hashLength = hasher.HashLengthInBytes; - + // Hash existing file content using streaming approach Span existingHashBuffer = stackalloc byte[hashLength]; var existingHasher = new XxHash64(); @@ -427,7 +427,7 @@ private static void WriteToJsonFile(string fileName, object value) contentStream.Position = 0; newHasher.Append(contentStream); newHasher.GetCurrentHash(newHashBuffer); - + // If hashes are equal, content is the same - don't write if (existingHashBuffer.SequenceEqual(newHashBuffer)) { From d248e2e735d0438f5525f3ffb930098d0025ddd0 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 19 Jun 2025 20:48:48 -0500 Subject: [PATCH 10/18] deduplication --- .../GenerateDepsFile.cs | 26 +++--------------- .../GenerateRuntimeConfigurationFiles.cs | 27 ++++--------------- .../Microsoft.NET.Build.Tasks/HashingUtils.cs | 25 +++++++++++++++++ 3 files changed, 34 insertions(+), 44 deletions(-) create mode 100644 src/Tasks/Microsoft.NET.Build.Tasks/HashingUtils.cs diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index 21f4681cf780..bf9569ef52ae 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -266,28 +266,11 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) // If file exists, check if content is different using streaming hash comparison if (File.Exists(depsFilePath)) { - // Get hash length from a single instance to avoid unnecessary allocations - var hasher = new XxHash64(); - var hashLength = hasher.HashLengthInBytes; - - // Hash existing file content using streaming approach - Span existingHashBuffer = stackalloc byte[hashLength]; - var existingHasher = new XxHash64(); - using (var existingStream = File.OpenRead(depsFilePath)) - { - existingHasher.Append(existingStream); - } - existingHasher.GetCurrentHash(existingHashBuffer); - - // Hash new content using streaming approach - Span newHashBuffer = stackalloc byte[hashLength]; - var newHasher = new XxHash64(); - contentStream.Position = 0; - newHasher.Append(contentStream); - newHasher.GetCurrentHash(newHashBuffer); - + // stream positions are reset as part of these utility calls + var existingContentHash = HashingUtils.ComputeXXHash64(File.OpenRead(depsFilePath)); + var newContentHash = HashingUtils.ComputeXXHash64(contentStream); // If hashes are equal, content is the same - don't write - if (existingHashBuffer.SequenceEqual(newHashBuffer)) + if (existingContentHash.SequenceEqual(newContentHash)) { shouldWriteFile = false; } @@ -298,7 +281,6 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) // Write the new content to file using CopyTo using (var fileStream = File.Create(depsFilePath)) { - contentStream.Position = 0; contentStream.CopyTo(fileStream); } } diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index 60d8e5768fe2..37cd18ac3a27 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -408,31 +408,15 @@ private static void WriteToJsonFile(string fileName, object value) // If file exists, check if content is different using streaming hash comparison if (File.Exists(fileName)) { - // Get hash length from a single instance to avoid unnecessary allocations - var hasher = new XxHash64(); - var hashLength = hasher.HashLengthInBytes; - - // Hash existing file content using streaming approach - Span existingHashBuffer = stackalloc byte[hashLength]; - var existingHasher = new XxHash64(); - using (var existingStream = File.OpenRead(fileName)) - { - existingHasher.Append(existingStream); - } - existingHasher.GetCurrentHash(existingHashBuffer); - - // Hash new content using streaming approach - Span newHashBuffer = stackalloc byte[hashLength]; - var newHasher = new XxHash64(); - contentStream.Position = 0; - newHasher.Append(contentStream); - newHasher.GetCurrentHash(newHashBuffer); - + // stream positions are reset as part of these utility calls + var existingContentHash = HashingUtils.ComputeXXHash64(File.OpenRead(fileName)); + var newContentHash = HashingUtils.ComputeXXHash64(contentStream); // If hashes are equal, content is the same - don't write - if (existingHashBuffer.SequenceEqual(newHashBuffer)) + if (existingContentHash.SequenceEqual(newContentHash)) { shouldWriteFile = false; } + } if (shouldWriteFile) @@ -440,7 +424,6 @@ private static void WriteToJsonFile(string fileName, object value) // Write the new content to file using CopyTo using (var fileStream = File.Create(fileName)) { - contentStream.Position = 0; contentStream.CopyTo(fileStream); } } diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/HashingUtils.cs b/src/Tasks/Microsoft.NET.Build.Tasks/HashingUtils.cs new file mode 100644 index 000000000000..667ed4ff74d5 --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks/HashingUtils.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Hashing; + +namespace Microsoft.NET.Build.Tasks; + +public static class HashingUtils +{ + /// + /// Computes the XxHash64 hash of a file. + /// + /// A stream to read for the hash. If the stream is seekable it will be reset to its incoming position. + public static byte[] ComputeXXHash64(Stream content) + { + var initialPosition = content.CanSeek ? content.Position : 0; + var hasher = new XxHash64(); + hasher.Append(content); + if (content.CanSeek) + { + content.Position = initialPosition; + } + return hasher.GetCurrentHash(); + } +} From 9e4e82622dbcb5bc518f2a8639331407240aacc4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Jun 2025 02:01:47 +0000 Subject: [PATCH 11/18] Refactor tests to reduce duplication by extracting helper methods Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../GivenAGenerateDepsFile.cs | 35 ++++++------------ ...GivenAGenerateRuntimeConfigurationFiles.cs | 37 ++++++------------- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateDepsFile.cs index b3ce2c8f877f..8bcc63049b4f 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateDepsFile.cs @@ -25,23 +25,8 @@ public GivenAGenerateDepsFile() [Fact] public void ItDoesNotOverwriteFileWithSameContent() { - var task = new TestableGenerateDepsFile - { - BuildEngine = new MockNeverCacheBuildEngine4(), - ProjectPath = "TestProject.csproj", - DepsFilePath = _depsFilePath, - TargetFramework = "net8.0", - AssemblyName = "TestProject", - AssemblyExtension = ".dll", - AssemblyVersion = "1.0.0.0", - IncludeMainProject = true, - CompileReferences = new MockTaskItem[0], - ResolvedNuGetFiles = new MockTaskItem[0], - ResolvedRuntimeTargetsFiles = new MockTaskItem[0], - RuntimeGraphPath = "" - }; - // Execute task first time + var task = CreateTestTask(); task.PublicExecuteCore(); var firstWriteTime = File.GetLastWriteTimeUtc(_depsFilePath); @@ -49,7 +34,17 @@ public void ItDoesNotOverwriteFileWithSameContent() Thread.Sleep(100); // Execute task again with same configuration - var task2 = new TestableGenerateDepsFile + var task2 = CreateTestTask(); + task2.PublicExecuteCore(); + var secondWriteTime = File.GetLastWriteTimeUtc(_depsFilePath); + + // File should not have been rewritten when content is the same + secondWriteTime.Should().Be(firstWriteTime, "file should not be rewritten when content is unchanged"); + } + + private TestableGenerateDepsFile CreateTestTask() + { + return new TestableGenerateDepsFile { BuildEngine = new MockNeverCacheBuildEngine4(), ProjectPath = "TestProject.csproj", @@ -64,12 +59,6 @@ public void ItDoesNotOverwriteFileWithSameContent() ResolvedRuntimeTargetsFiles = new MockTaskItem[0], RuntimeGraphPath = "" }; - - task2.PublicExecuteCore(); - var secondWriteTime = File.GetLastWriteTimeUtc(_depsFilePath); - - // File should not have been rewritten when content is the same - secondWriteTime.Should().Be(firstWriteTime, "file should not be rewritten when content is unchanged"); } private class TestableGenerateDepsFile : GenerateDepsFile diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRuntimeConfigurationFiles.cs index f90b407c7533..9660ed4f0cec 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRuntimeConfigurationFiles.cs @@ -221,25 +221,8 @@ public void GivenTargetMonikerItGeneratesShortName() [Fact] public void ItDoesNotOverwriteFileWithSameContent() { - var task = new TestableGenerateRuntimeConfigurationFiles - { - BuildEngine = new MockNeverCacheBuildEngine4(), - TargetFrameworkMoniker = $".NETCoreApp,Version=v{ToolsetInfo.CurrentTargetFrameworkVersion}", - RuntimeConfigPath = _runtimeConfigPath, - RuntimeFrameworks = new[] - { - new MockTaskItem( - "Microsoft.NETCore.App", - new Dictionary - { - {"FrameworkName", "Microsoft.NETCore.App"}, {"Version", $"{ToolsetInfo.CurrentTargetFrameworkVersion}.0"} - } - ) - }, - RollForward = "LatestMinor" - }; - // Execute task first time + var task = CreateBasicTestTask(); task.PublicExecuteCore(); var firstWriteTime = File.GetLastWriteTimeUtc(_runtimeConfigPath); @@ -247,7 +230,17 @@ public void ItDoesNotOverwriteFileWithSameContent() Thread.Sleep(100); // Execute task again with same configuration - var task2 = new TestableGenerateRuntimeConfigurationFiles + var task2 = CreateBasicTestTask(); + task2.PublicExecuteCore(); + var secondWriteTime = File.GetLastWriteTimeUtc(_runtimeConfigPath); + + // File should not have been rewritten when content is the same + secondWriteTime.Should().Be(firstWriteTime, "file should not be rewritten when content is unchanged"); + } + + private TestableGenerateRuntimeConfigurationFiles CreateBasicTestTask() + { + return new TestableGenerateRuntimeConfigurationFiles { BuildEngine = new MockNeverCacheBuildEngine4(), TargetFrameworkMoniker = $".NETCoreApp,Version=v{ToolsetInfo.CurrentTargetFrameworkVersion}", @@ -264,12 +257,6 @@ public void ItDoesNotOverwriteFileWithSameContent() }, RollForward = "LatestMinor" }; - - task2.PublicExecuteCore(); - var secondWriteTime = File.GetLastWriteTimeUtc(_runtimeConfigPath); - - // File should not have been rewritten when content is the same - secondWriteTime.Should().Be(firstWriteTime, "file should not be rewritten when content is unchanged"); } private class TestableGenerateRuntimeConfigurationFiles : GenerateRuntimeConfigurationFiles From 6e72e0ebe8bebb4efd95af513d294796cad3f926 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 01:00:31 +0000 Subject: [PATCH 12/18] Reset MemoryStream position before copying content to destination files Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs | 1 + .../GenerateRuntimeConfigurationFiles.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index bf9569ef52ae..e867c56e5e46 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -279,6 +279,7 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) if (shouldWriteFile) { // Write the new content to file using CopyTo + contentStream.Position = 0; using (var fileStream = File.Create(depsFilePath)) { contentStream.CopyTo(fileStream); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index 37cd18ac3a27..688bd13da62b 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -422,6 +422,7 @@ private static void WriteToJsonFile(string fileName, object value) if (shouldWriteFile) { // Write the new content to file using CopyTo + contentStream.Position = 0; using (var fileStream = File.Create(fileName)) { contentStream.CopyTo(fileStream); From fa763b9b5dd04804d9a0981e97cc7d8fd199056e Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 25 Jun 2025 11:08:46 -0500 Subject: [PATCH 13/18] ensure that we close the read streams --- src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs | 4 ++-- .../GenerateRuntimeConfigurationFiles.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index e867c56e5e46..ee7c79c81380 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -266,8 +266,8 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) // If file exists, check if content is different using streaming hash comparison if (File.Exists(depsFilePath)) { - // stream positions are reset as part of these utility calls - var existingContentHash = HashingUtils.ComputeXXHash64(File.OpenRead(depsFilePath)); + using var existingFileContentStream = File.OpenRead(depsFilePath); + var existingContentHash = HashingUtils.ComputeXXHash64(existingFileContentStream); var newContentHash = HashingUtils.ComputeXXHash64(contentStream); // If hashes are equal, content is the same - don't write if (existingContentHash.SequenceEqual(newContentHash)) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index 688bd13da62b..a4ce320406e3 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -409,7 +409,8 @@ private static void WriteToJsonFile(string fileName, object value) if (File.Exists(fileName)) { // stream positions are reset as part of these utility calls - var existingContentHash = HashingUtils.ComputeXXHash64(File.OpenRead(fileName)); + using var existingContentStream = File.OpenRead(fileName); + var existingContentHash = HashingUtils.ComputeXXHash64(existingContentStream); var newContentHash = HashingUtils.ComputeXXHash64(contentStream); // If hashes are equal, content is the same - don't write if (existingContentHash.SequenceEqual(newContentHash)) From 9bad4af3e55339e5fa513aa9ba3c3ab58f0a4539 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 25 Jun 2025 16:13:08 -0500 Subject: [PATCH 14/18] remove code that obscures what's going on --- .../GenerateRuntimeConfigurationFiles.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index a4ce320406e3..ff86ade6dec6 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -397,12 +397,12 @@ private static void WriteToJsonFile(string fileName, object value) // Generate new content using (var contentStream = new MemoryStream()) { - using (var streamWriter = new StreamWriter(contentStream, Encoding.UTF8, 1024, true)) + // the explicit buffersize is because on .NET Framework this is the default value, + // and .NET Framework's constructor requires all parameters to be specified. + using (var streamWriter = new StreamWriter(contentStream, Encoding.UTF8, bufferSize: 1024, leaveOpen: true)) using (var jsonWriter = new JsonTextWriter(streamWriter)) { serializer.Serialize(jsonWriter, value); - jsonWriter.Flush(); - streamWriter.Flush(); } // If file exists, check if content is different using streaming hash comparison From 88dc75cd6942bd3e7e001e38177697e32d25d9fe Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 26 Jun 2025 11:21:14 -0500 Subject: [PATCH 15/18] runtimeconfig.json encoding needs to be UTF8 without BOM --- .../GenerateRuntimeConfigurationFiles.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index ff86ade6dec6..78523305f0b7 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -399,7 +399,7 @@ private static void WriteToJsonFile(string fileName, object value) { // the explicit buffersize is because on .NET Framework this is the default value, // and .NET Framework's constructor requires all parameters to be specified. - using (var streamWriter = new StreamWriter(contentStream, Encoding.UTF8, bufferSize: 1024, leaveOpen: true)) + using (var streamWriter = new StreamWriter(contentStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), bufferSize: 1024, leaveOpen: true)) using (var jsonWriter = new JsonTextWriter(streamWriter)) { serializer.Serialize(jsonWriter, value); From 3038ed753dce267eabd07e8fdce165c7a365614f Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 26 Jun 2025 13:17:47 -0500 Subject: [PATCH 16/18] Wrestle with encodings to generate accurate hashes for comparison --- .../GenerateDepsFile.cs | 54 +++++++++++-------- .../GenerateRuntimeConfigurationFiles.cs | 27 ++++++---- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index ee7c79c81380..de11381fd3b6 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -257,34 +257,44 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) var writer = new DependencyContextWriter(); bool shouldWriteFile = true; + var tempDepsFilePath = depsFilePath + ".tmp"; // Generate new content - using (var contentStream = new MemoryStream()) + using (var fileStream = File.Create(tempDepsFilePath)) { - writer.Write(dependencyContext, contentStream); - - // If file exists, check if content is different using streaming hash comparison - if (File.Exists(depsFilePath)) + writer.Write(dependencyContext, fileStream); + } + + // If file exists, check if content is different using streaming hash comparison + if (File.Exists(depsFilePath)) + { + Log.LogMessage("File {0} already exists, checking hash.", depsFilePath); + using var existingFileContentStream = File.OpenRead(depsFilePath); + var existingContentHash = HashingUtils.ComputeXXHash64(existingFileContentStream); + var existingContentHashRendered = BitConverter.ToString(existingContentHash).Replace("-", ""); + Log.LogMessage("Existing file hash: {0}", existingContentHashRendered); + using var newContentStream = File.OpenRead(tempDepsFilePath); + var newContentHash = HashingUtils.ComputeXXHash64(newContentStream); + var newContentHashRendered = BitConverter.ToString(newContentHash).Replace("-", ""); + Log.LogMessage("New content hash: {0}", newContentHashRendered); + // If hashes are equal, content is the same - don't write + if (existingContentHash.SequenceEqual(newContentHash)) { - using var existingFileContentStream = File.OpenRead(depsFilePath); - var existingContentHash = HashingUtils.ComputeXXHash64(existingFileContentStream); - var newContentHash = HashingUtils.ComputeXXHash64(contentStream); - // If hashes are equal, content is the same - don't write - if (existingContentHash.SequenceEqual(newContentHash)) - { - shouldWriteFile = false; - } + Log.LogMessage("File {0} is unchanged, skipping write.", depsFilePath); + shouldWriteFile = false; } + } - if (shouldWriteFile) - { - // Write the new content to file using CopyTo - contentStream.Position = 0; - using (var fileStream = File.Create(depsFilePath)) - { - contentStream.CopyTo(fileStream); - } - } + if (shouldWriteFile) + { + Log.LogMessage("Writing file {0}.", depsFilePath); + File.Move(tempDepsFilePath, depsFilePath); + } + else + { + // If we didn't write the file, delete the temporary file + Log.LogMessage("Deleting temporary file {0}.", tempDepsFilePath); + File.Delete(tempDepsFilePath); } _filesWritten.Add(new TaskItem(depsFilePath)); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index 78523305f0b7..76fc48c55714 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -178,7 +178,7 @@ private void WriteRuntimeConfig( AddAdditionalProbingPaths(config.RuntimeOptions, packageFolders); } - WriteToJsonFile(RuntimeConfigPath, config); + WriteToJsonFile(Log, RuntimeConfigPath, config); _filesWritten.Add(new TaskItem(RuntimeConfigPath)); } @@ -342,7 +342,7 @@ private void WriteDevRuntimeConfig(IList packageFolders) AddAdditionalProbingPaths(devConfig.RuntimeOptions, packageFolders); - WriteToJsonFile(RuntimeConfigDevPath, devConfig); + WriteToJsonFile(Log, RuntimeConfigDevPath, devConfig); _filesWritten.Add(new TaskItem(RuntimeConfigDevPath)); } @@ -383,7 +383,7 @@ private static string EnsureNoTrailingDirectorySeparator(string path) return path; } - private static void WriteToJsonFile(string fileName, object value) + private static void WriteToJsonFile(Logger log, string filePath, object value) { JsonSerializer serializer = new() { @@ -406,15 +406,21 @@ private static void WriteToJsonFile(string fileName, object value) } // If file exists, check if content is different using streaming hash comparison - if (File.Exists(fileName)) + if (File.Exists(filePath)) { + log.LogMessage("File {0} already exists, checking hash.", filePath); // stream positions are reset as part of these utility calls - using var existingContentStream = File.OpenRead(fileName); - var existingContentHash = HashingUtils.ComputeXXHash64(existingContentStream); - var newContentHash = HashingUtils.ComputeXXHash64(contentStream); + using var existingContentRawStream = File.OpenRead(filePath); + var existingContentHash = HashingUtils.ComputeXXHash64(existingContentRawStream); + var existingContentHashRendered = BitConverter.ToString(existingContentHash).Replace("-", ""); + log.LogMessage("Existing file hash: {0}", existingContentHashRendered); + var newContentHash = HashingUtils.ComputeXXHash64(existingContentRawStream); + var newContentHashRendered = BitConverter.ToString(newContentHash).Replace("-", ""); + log.LogMessage("New content hash: {0}", existingContentHashRendered); // If hashes are equal, content is the same - don't write if (existingContentHash.SequenceEqual(newContentHash)) { + log.LogMessage("File {0} is unchanged, skipping write.", filePath); shouldWriteFile = false; } @@ -422,12 +428,11 @@ private static void WriteToJsonFile(string fileName, object value) if (shouldWriteFile) { + log.LogMessage("Writing file {0}.", filePath); // Write the new content to file using CopyTo contentStream.Position = 0; - using (var fileStream = File.Create(fileName)) - { - contentStream.CopyTo(fileStream); - } + using var fileStream = File.Create(filePath); + contentStream.CopyTo(fileStream); } } } From 5b5c971d5c0b2c66691e91b02947060fac0caf70 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 26 Jun 2025 14:36:05 -0500 Subject: [PATCH 17/18] Safely move the generated file --- src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index de11381fd3b6..a06aceb938e0 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -264,7 +264,7 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) { writer.Write(dependencyContext, fileStream); } - + // If file exists, check if content is different using streaming hash comparison if (File.Exists(depsFilePath)) { @@ -288,7 +288,14 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) if (shouldWriteFile) { Log.LogMessage("Writing file {0}.", depsFilePath); +#if NET + File.Move(tempDepsFilePath, depsFilePath, overwrite: true); +#else + // For .NET Framework, we can't use File.Move because it doesn't overwrite the existing file + // so we delete the existing file first. + File.Delete(depsFilePath); File.Move(tempDepsFilePath, depsFilePath); +#endif } else { From 251e953adf09423cc0732c93c0f200f629dbf605 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 26 Jun 2025 16:37:28 -0500 Subject: [PATCH 18/18] Skip trying to do in-memory things and just compare temp files --- ...GivenAGenerateRuntimeConfigurationFiles.cs | 24 ++++++ .../GenerateDepsFile.cs | 1 - .../GenerateRuntimeConfigurationFiles.cs | 76 ++++++++++--------- 3 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRuntimeConfigurationFiles.cs index 9660ed4f0cec..7c298a7e4ecb 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRuntimeConfigurationFiles.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using FluentAssertions; +using Microsoft.Build.Utilities; using Microsoft.NET.TestFramework; using Xunit; @@ -238,6 +239,29 @@ public void ItDoesNotOverwriteFileWithSameContent() secondWriteTime.Should().Be(firstWriteTime, "file should not be rewritten when content is unchanged"); } + [Fact] + public void GivenDifferentRuntimeHostOptionsItWritesNewConfig() + { + // Execute task first time + var task = CreateBasicTestTask(); + task.PublicExecuteCore(); + var firstWriteTime = File.GetLastWriteTimeUtc(_runtimeConfigPath); + + // Wait a bit to ensure timestamp would change if file is rewritten + Thread.Sleep(100); + + // Execute task again with different host options + var task2 = CreateBasicTestTask(); + task2.HostConfigurationOptions = [ + new TaskItem("System.Runtime.TieredCompilation", new Dictionary{{"Value", "false"}}), + new TaskItem("System.GC.Concurrent", new Dictionary{{"Value", "false"}}), + ]; + task2.PublicExecuteCore(); + var secondWriteTime = File.GetLastWriteTimeUtc(_runtimeConfigPath); + // File should have been rewritten when content is different + secondWriteTime.Should().BeAfter(firstWriteTime, "file should be rewritten when content is different"); + } + private TestableGenerateRuntimeConfigurationFiles CreateBasicTestTask() { return new TestableGenerateRuntimeConfigurationFiles diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index a06aceb938e0..9927194e80cf 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -10,7 +10,6 @@ using NuGet.Packaging.Core; using NuGet.ProjectModel; using NuGet.RuntimeModel; -using System.IO.Hashing; namespace Microsoft.NET.Build.Tasks { diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index 76fc48c55714..b55e9ba073b6 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -10,8 +10,6 @@ using Newtonsoft.Json.Serialization; using NuGet.Frameworks; using NuGet.ProjectModel; -using System.IO.Hashing; -using System.Text; namespace Microsoft.NET.Build.Tasks { @@ -393,47 +391,53 @@ private static void WriteToJsonFile(Logger log, string filePath, object value) }; bool shouldWriteFile = true; + var tempFilePath = filePath + ".tmp"; // Generate new content - using (var contentStream = new MemoryStream()) + using (JsonTextWriter writer = new(new StreamWriter(File.Create(tempFilePath)))) { - // the explicit buffersize is because on .NET Framework this is the default value, - // and .NET Framework's constructor requires all parameters to be specified. - using (var streamWriter = new StreamWriter(contentStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), bufferSize: 1024, leaveOpen: true)) - using (var jsonWriter = new JsonTextWriter(streamWriter)) - { - serializer.Serialize(jsonWriter, value); - } + serializer.Serialize(writer, value); + } - // If file exists, check if content is different using streaming hash comparison - if (File.Exists(filePath)) + // If file exists, check if content is different using streaming hash comparison + if (File.Exists(filePath)) + { + log.LogMessage("File {0} already exists, checking hash.", filePath); + // stream positions are reset as part of these utility calls + using var existingContentStream = File.OpenRead(filePath); + var existingContentHash = HashingUtils.ComputeXXHash64(existingContentStream); + var existingContentHashRendered = BitConverter.ToString(existingContentHash).Replace("-", ""); + log.LogMessage("Existing file hash: {0}", existingContentHashRendered); + using var newContentStream = File.OpenRead(tempFilePath); + var newContentHash = HashingUtils.ComputeXXHash64(newContentStream); + var newContentHashRendered = BitConverter.ToString(newContentHash).Replace("-", ""); + log.LogMessage("New content hash: {0}", existingContentHashRendered); + // If hashes are equal, content is the same - don't write + if (existingContentHash.SequenceEqual(newContentHash)) { - log.LogMessage("File {0} already exists, checking hash.", filePath); - // stream positions are reset as part of these utility calls - using var existingContentRawStream = File.OpenRead(filePath); - var existingContentHash = HashingUtils.ComputeXXHash64(existingContentRawStream); - var existingContentHashRendered = BitConverter.ToString(existingContentHash).Replace("-", ""); - log.LogMessage("Existing file hash: {0}", existingContentHashRendered); - var newContentHash = HashingUtils.ComputeXXHash64(existingContentRawStream); - var newContentHashRendered = BitConverter.ToString(newContentHash).Replace("-", ""); - log.LogMessage("New content hash: {0}", existingContentHashRendered); - // If hashes are equal, content is the same - don't write - if (existingContentHash.SequenceEqual(newContentHash)) - { - log.LogMessage("File {0} is unchanged, skipping write.", filePath); - shouldWriteFile = false; - } - + log.LogMessage("File {0} is unchanged, skipping write.", filePath); + shouldWriteFile = false; } - if (shouldWriteFile) - { - log.LogMessage("Writing file {0}.", filePath); - // Write the new content to file using CopyTo - contentStream.Position = 0; - using var fileStream = File.Create(filePath); - contentStream.CopyTo(fileStream); - } + } + + if (shouldWriteFile) + { + log.LogMessage("Writing file {0}.", filePath); +#if NET + File.Move(tempFilePath, filePath, overwrite: true); +#else + // For .NET Framework, we can't use File.Move because it doesn't overwrite the existing file + // so we delete the existing file first. + File.Delete(filePath); + File.Move(tempFilePath, filePath); +#endif + } + else + { + // If we didn't write the file, delete the temporary file + log.LogMessage("Deleting temporary file {0}.", tempFilePath); + File.Delete(tempFilePath); } } }