From 274633894744966e66c4df7bdcff3d5e6d7d2927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Prokop?= Date: Wed, 1 Jul 2026 13:06:43 +0200 Subject: [PATCH 1/5] Refresh copy marker when implementation output changes Touch CopyUpToDateMarker when the main implementation assembly is actually copied to the output directory. This lets implementation-only changes advance the marker even when no copy-local references were copied, without reintroducing no-op marker updates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tasks.UnitTests/RegressionTests.cs | 93 +++++++++++++++++++ .../Microsoft.Common.CurrentVersion.targets | 17 ++++ 2 files changed, 110 insertions(+) diff --git a/src/Tasks.UnitTests/RegressionTests.cs b/src/Tasks.UnitTests/RegressionTests.cs index e82e09f9445..1063e56f6e4 100644 --- a/src/Tasks.UnitTests/RegressionTests.cs +++ b/src/Tasks.UnitTests/RegressionTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading; using Microsoft.Build.Evaluation; using Microsoft.Build.Shared; using Microsoft.Build.UnitTests; @@ -102,5 +103,97 @@ public void IsWellKnownAttributeValuePreserved() MockLogger logger = new MockLogger(_output); ObjectModelHelpers.BuildTempProjectFileExpectSuccess("Myapp.proj", logger); } + + /// + /// Test for https://github.com/dotnet/msbuild/issues/13478. + /// + [Fact] + public void CopyUpToDateMarkerTracksImplementationAssemblyWrites() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + CreateCopyMarkerTestProject(produceReferenceAssembly: true); + + MockLogger logger = new MockLogger(_output); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess("MarkerTest.csproj", logger); + + string markerPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "obj", "Debug", "MarkerTest.csproj.Up2Date"); + Assert.True(File.Exists(markerPath), $"Expected marker to exist at {markerPath}"); + DateTime markerAfterFirstBuild = File.GetLastWriteTimeUtc(markerPath); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess("MarkerTest.csproj", logger); + + DateTime markerAfterNoOpBuild = File.GetLastWriteTimeUtc(markerPath); + Assert.Equal(markerAfterFirstBuild, markerAfterNoOpBuild); + + string intermediateRefAssemblyPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "obj", "Debug", "refint", "MarkerTest.dll"); + Assert.True(File.Exists(intermediateRefAssemblyPath), $"Expected intermediate reference assembly to exist at {intermediateRefAssemblyPath}"); + + Thread.Sleep(TimeSpan.FromSeconds(2)); + File.WriteAllText(intermediateRefAssemblyPath, "Reference output changed"); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess("MarkerTest.csproj", logger); + + DateTime markerAfterReferenceAssemblyOnlyChange = File.GetLastWriteTimeUtc(markerPath); + Assert.Equal(markerAfterNoOpBuild, markerAfterReferenceAssemblyOnlyChange); + + Thread.Sleep(TimeSpan.FromSeconds(2)); + File.WriteAllText(Path.Combine(ObjectModelHelpers.TempProjectDir, "input.txt"), "changed"); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess("MarkerTest.csproj", logger); + + DateTime markerAfterImplementationChange = File.GetLastWriteTimeUtc(markerPath); + Assert.True( + markerAfterImplementationChange > markerAfterReferenceAssemblyOnlyChange, + $"Expected marker to advance from {markerAfterReferenceAssemblyOnlyChange:O}, but it was {markerAfterImplementationChange:O}."); + } + + /// + /// Test for https://github.com/dotnet/msbuild/issues/13478. + /// + [Fact] + public void CopyUpToDateMarkerIsNotCreatedForMainAssemblyWritesWhenReferenceAssembliesAreNotProduced() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + CreateCopyMarkerTestProject(produceReferenceAssembly: false); + + MockLogger logger = new MockLogger(_output); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess("MarkerTest.csproj", logger); + + string markerPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "obj", "Debug", "MarkerTest.csproj.Up2Date"); + Assert.False(File.Exists(markerPath), $"Did not expect marker to exist at {markerPath}"); + } + + private static void CreateCopyMarkerTestProject(bool produceReferenceAssembly) + { + ObjectModelHelpers.CreateFileInTempProjectDirectory("input.txt", "initial"); + ObjectModelHelpers.CreateFileInTempProjectDirectory("MarkerTest.csproj", $@" + + + + + MarkerTest + Library + {MSBuildConstants.StandardTestTargetFrameworkVersion} + {produceReferenceAssembly.ToString().ToLowerInvariant()} + + + + + + '%(RootDir)%(Directory)');@(IntermediateRefAssembly->'%(RootDir)%(Directory)')"" /> + + + +"); + } } } diff --git a/src/Tasks/Microsoft.Common.CurrentVersion.targets b/src/Tasks/Microsoft.Common.CurrentVersion.targets index 10436590525..2d17a9aefb2 100644 --- a/src/Tasks/Microsoft.Common.CurrentVersion.targets +++ b/src/Tasks/Microsoft.Common.CurrentVersion.targets @@ -4913,6 +4913,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. true true true + <_WroteMainAssemblyToOutputDirectory>false @@ -4931,6 +4932,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. + @@ -4948,6 +4950,21 @@ Copyright (C) Microsoft Corporation. All rights reserved. + + + + + + + Date: Fri, 3 Jul 2026 09:22:26 +0200 Subject: [PATCH 2/5] addressing comment --- src/Tasks.UnitTests/RegressionTests.cs | 13 ++++++------- src/Tasks/Microsoft.Common.CurrentVersion.targets | 1 - 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Tasks.UnitTests/RegressionTests.cs b/src/Tasks.UnitTests/RegressionTests.cs index 1063e56f6e4..a08ed35201f 100644 --- a/src/Tasks.UnitTests/RegressionTests.cs +++ b/src/Tasks.UnitTests/RegressionTests.cs @@ -118,16 +118,16 @@ public void CopyUpToDateMarkerTracksImplementationAssemblyWrites() ObjectModelHelpers.BuildTempProjectFileExpectSuccess("MarkerTest.csproj", logger); string markerPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "obj", "Debug", "MarkerTest.csproj.Up2Date"); - Assert.True(File.Exists(markerPath), $"Expected marker to exist at {markerPath}"); + File.Exists(markerPath).ShouldBeTrue($"Expected marker to exist at {markerPath}"); DateTime markerAfterFirstBuild = File.GetLastWriteTimeUtc(markerPath); ObjectModelHelpers.BuildTempProjectFileExpectSuccess("MarkerTest.csproj", logger); DateTime markerAfterNoOpBuild = File.GetLastWriteTimeUtc(markerPath); - Assert.Equal(markerAfterFirstBuild, markerAfterNoOpBuild); + markerAfterNoOpBuild.ShouldBe(markerAfterFirstBuild); string intermediateRefAssemblyPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "obj", "Debug", "refint", "MarkerTest.dll"); - Assert.True(File.Exists(intermediateRefAssemblyPath), $"Expected intermediate reference assembly to exist at {intermediateRefAssemblyPath}"); + File.Exists(intermediateRefAssemblyPath).ShouldBeTrue($"Expected intermediate reference assembly to exist at {intermediateRefAssemblyPath}"); Thread.Sleep(TimeSpan.FromSeconds(2)); File.WriteAllText(intermediateRefAssemblyPath, "Reference output changed"); @@ -135,7 +135,7 @@ public void CopyUpToDateMarkerTracksImplementationAssemblyWrites() ObjectModelHelpers.BuildTempProjectFileExpectSuccess("MarkerTest.csproj", logger); DateTime markerAfterReferenceAssemblyOnlyChange = File.GetLastWriteTimeUtc(markerPath); - Assert.Equal(markerAfterNoOpBuild, markerAfterReferenceAssemblyOnlyChange); + markerAfterReferenceAssemblyOnlyChange.ShouldBe(markerAfterNoOpBuild); Thread.Sleep(TimeSpan.FromSeconds(2)); File.WriteAllText(Path.Combine(ObjectModelHelpers.TempProjectDir, "input.txt"), "changed"); @@ -143,8 +143,7 @@ public void CopyUpToDateMarkerTracksImplementationAssemblyWrites() ObjectModelHelpers.BuildTempProjectFileExpectSuccess("MarkerTest.csproj", logger); DateTime markerAfterImplementationChange = File.GetLastWriteTimeUtc(markerPath); - Assert.True( - markerAfterImplementationChange > markerAfterReferenceAssemblyOnlyChange, + (markerAfterImplementationChange > markerAfterReferenceAssemblyOnlyChange).ShouldBeTrue( $"Expected marker to advance from {markerAfterReferenceAssemblyOnlyChange:O}, but it was {markerAfterImplementationChange:O}."); } @@ -162,7 +161,7 @@ public void CopyUpToDateMarkerIsNotCreatedForMainAssemblyWritesWhenReferenceAsse ObjectModelHelpers.BuildTempProjectFileExpectSuccess("MarkerTest.csproj", logger); string markerPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "obj", "Debug", "MarkerTest.csproj.Up2Date"); - Assert.False(File.Exists(markerPath), $"Did not expect marker to exist at {markerPath}"); + File.Exists(markerPath).ShouldBeFalse($"Did not expect marker to exist at {markerPath}"); } private static void CreateCopyMarkerTestProject(bool produceReferenceAssembly) diff --git a/src/Tasks/Microsoft.Common.CurrentVersion.targets b/src/Tasks/Microsoft.Common.CurrentVersion.targets index 2d17a9aefb2..c79b25f1487 100644 --- a/src/Tasks/Microsoft.Common.CurrentVersion.targets +++ b/src/Tasks/Microsoft.Common.CurrentVersion.targets @@ -4913,7 +4913,6 @@ Copyright (C) Microsoft Corporation. All rights reserved. true true true - <_WroteMainAssemblyToOutputDirectory>false From ebe1ae93d289cccded6c107e925bf7345a98a74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Prokop?= Date: Fri, 3 Jul 2026 09:34:05 +0200 Subject: [PATCH 3/5] fix unit tests --- src/Tasks.UnitTests/RegressionTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tasks.UnitTests/RegressionTests.cs b/src/Tasks.UnitTests/RegressionTests.cs index a08ed35201f..1a90ac033c0 100644 --- a/src/Tasks.UnitTests/RegressionTests.cs +++ b/src/Tasks.UnitTests/RegressionTests.cs @@ -7,6 +7,7 @@ using Microsoft.Build.Evaluation; using Microsoft.Build.Shared; using Microsoft.Build.UnitTests; +using Shouldly; using Xunit; #nullable disable From d9811a9dbf6d18945aafa92b728a04cf05080855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Prokop?= Date: Fri, 3 Jul 2026 15:41:38 +0200 Subject: [PATCH 4/5] touch only once --- .../Microsoft.Common.CurrentVersion.targets | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Tasks/Microsoft.Common.CurrentVersion.targets b/src/Tasks/Microsoft.Common.CurrentVersion.targets index c79b25f1487..b737725cca4 100644 --- a/src/Tasks/Microsoft.Common.CurrentVersion.targets +++ b/src/Tasks/Microsoft.Common.CurrentVersion.targets @@ -4950,15 +4950,19 @@ Copyright (C) Microsoft Corporation. All rights reserved. + Condition="('$(ProduceReferenceAssembly)' == 'true' and '$(_WroteMainAssemblyToOutputDirectory)' == 'true') or '$(_CopyLocalReferencesCopiedInThisBuild)' == 'true'" /> @@ -5127,13 +5131,13 @@ Copyright (C) Microsoft Corporation. All rights reserved. - - + + + <_CopyLocalReferencesCopiedInThisBuild Condition="'@(ReferencesCopiedInThisBuild)' != '' and '$(WroteAtLeastOneFile)' == 'true'">true + From 71e36f92bea6bfe02fa21fcaa360fda98160f52c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Prokop?= Date: Tue, 7 Jul 2026 14:00:50 +0200 Subject: [PATCH 5/5] Fix copy marker tests on Unix Avoid full-framework reference assembly lookup in the synthetic marker test project so the tests can run on agents without .NET Framework reference assemblies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tasks.UnitTests/RegressionTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Tasks.UnitTests/RegressionTests.cs b/src/Tasks.UnitTests/RegressionTests.cs index 1a90ac033c0..3b75698f51e 100644 --- a/src/Tasks.UnitTests/RegressionTests.cs +++ b/src/Tasks.UnitTests/RegressionTests.cs @@ -181,6 +181,9 @@ private static void CreateCopyMarkerTestProject(bool produceReferenceAssembly) + + +