Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions src/Tasks.UnitTests/RegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

using System;
using System.IO;
using System.Threading;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
using Shouldly;
using Xunit;

#nullable disable
Expand Down Expand Up @@ -102,5 +104,99 @@ public void IsWellKnownAttributeValuePreserved()
MockLogger logger = new MockLogger(_output);
ObjectModelHelpers.BuildTempProjectFileExpectSuccess("Myapp.proj", logger);
}

/// <summary>
/// Test for https://github.com/dotnet/msbuild/issues/13478.
/// </summary>
[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");
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);
markerAfterNoOpBuild.ShouldBe(markerAfterFirstBuild);

string intermediateRefAssemblyPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "obj", "Debug", "refint", "MarkerTest.dll");
File.Exists(intermediateRefAssemblyPath).ShouldBeTrue($"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);
markerAfterReferenceAssemblyOnlyChange.ShouldBe(markerAfterNoOpBuild);

Thread.Sleep(TimeSpan.FromSeconds(2));
File.WriteAllText(Path.Combine(ObjectModelHelpers.TempProjectDir, "input.txt"), "changed");

ObjectModelHelpers.BuildTempProjectFileExpectSuccess("MarkerTest.csproj", logger);

DateTime markerAfterImplementationChange = File.GetLastWriteTimeUtc(markerPath);
(markerAfterImplementationChange > markerAfterReferenceAssemblyOnlyChange).ShouldBeTrue(
$"Expected marker to advance from {markerAfterReferenceAssemblyOnlyChange:O}, but it was {markerAfterImplementationChange:O}.");
}

/// <summary>
/// Test for https://github.com/dotnet/msbuild/issues/13478.
/// </summary>
[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");
File.Exists(markerPath).ShouldBeFalse($"Did not expect marker to exist at {markerPath}");
}

private static void CreateCopyMarkerTestProject(bool produceReferenceAssembly)
{
ObjectModelHelpers.CreateFileInTempProjectDirectory("input.txt", "initial");
ObjectModelHelpers.CreateFileInTempProjectDirectory("MarkerTest.csproj", $@"
<Project DefaultTargets=""Build"" xmlns=""msbuildnamespace"" ToolsVersion=""msbuilddefaulttoolsversion"">
<Import Project=""$(MSBuildToolsPath)\Microsoft.Common.props"" />

<PropertyGroup>
<AssemblyName>MarkerTest</AssemblyName>
<OutputType>Library</OutputType>
<TargetFrameworkVersion>{MSBuildConstants.StandardTestTargetFrameworkVersion}</TargetFrameworkVersion>
<ProduceReferenceAssembly>{produceReferenceAssembly.ToString().ToLowerInvariant()}</ProduceReferenceAssembly>
</PropertyGroup>

<Import Project=""$(MSBuildToolsPath)\Microsoft.CSharp.targets"" />

<Target Name=""GetReferenceAssemblyPaths"" />
<Target Name=""ResolveAssemblyReferences"" />

<Target Name=""CoreCompile""
Inputs=""input.txt""
Outputs=""@(IntermediateAssembly)"">
<MakeDir Directories=""@(IntermediateAssembly->'%(RootDir)%(Directory)');@(IntermediateRefAssembly->'%(RootDir)%(Directory)')"" />
<WriteLinesToFile File=""@(IntermediateAssembly)""
Lines=""Implementation output""
Overwrite=""true"" />
<WriteLinesToFile File=""@(IntermediateRefAssembly)""
Lines=""Reference output""
Overwrite=""true""
Condition=""'@(IntermediateRefAssembly)' != '' and !Exists('@(IntermediateRefAssembly)')"" />
</Target>
</Project>");
}
}
}
34 changes: 27 additions & 7 deletions src/Tasks/Microsoft.Common.CurrentVersion.targets
Original file line number Diff line number Diff line change
Expand Up @@ -4931,6 +4931,7 @@ Copyright (C) Microsoft Corporation. All rights reserved.

<Output TaskParameter="DestinationFiles" ItemName="MainAssembly"/>
<Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
<Output TaskParameter="WroteAtLeastOneFile" PropertyName="_WroteMainAssemblyToOutputDirectory"/>

</Copy>

Expand All @@ -4948,6 +4949,25 @@ Copyright (C) Microsoft Corporation. All rights reserved.

<Message Importance="High" Text="$(MSBuildProjectName) -&gt; @(MainAssembly->'%(FullPath)')" Condition="'$(CopyBuildOutputToOutputDirectory)' == 'true' and '$(SkipCopyBuildProduct)'!='true'" />

<!--
Advance the marker considered by projects that reference this one during their up-to-date
checks. It must move forward when either:
* this project produces reference assemblies and its implementation assembly changed on
this build (referencing projects may need to copy the updated implementation), or
* copy-local (possibly transitive) references were actually copied on this build, as
captured by _CopyFilesMarkedCopyLocal in $(_CopyLocalReferencesCopiedInThisBuild).
Both signals are OR'd into this single Touch so the marker is written at most once per
build. _CopyFilesMarkedCopyLocal is a DependsOnTargets dependency of this target, so its
capture always runs before this Touch.
-->
<Touch Files="@(CopyUpToDateMarker)"
AlwaysCreate="true"
Condition="('$(ProduceReferenceAssembly)' == 'true' and '$(_WroteMainAssemblyToOutputDirectory)' == 'true') or '$(_CopyLocalReferencesCopiedInThisBuild)' == 'true'" />

<ItemGroup Condition="'$(ProduceReferenceAssembly)' == 'true'">
<FileWrites Include="@(CopyUpToDateMarker)" />
</ItemGroup>

<!-- Copy the additional modules. -->
<Copy
SourceFiles="@(AddModules)"
Expand Down Expand Up @@ -5111,13 +5131,13 @@ Copyright (C) Microsoft Corporation. All rights reserved.

</Copy>

<!-- If this project produces reference assemblies *and* copied (possibly transitive)
references on this build, subsequent builds of projects that depend on it must
not be considered up to date, so touch this marker file that is considered an
input to projects that reference this one. -->
<Touch Files="@(CopyUpToDateMarker)"
AlwaysCreate="true"
Condition="'@(ReferencesCopiedInThisBuild)' != '' and '$(WroteAtLeastOneFile)' == 'true'" />
<!-- Record whether copy-local (possibly transitive) references were actually copied on this
build. When they were, projects that depend on this one must not be considered up to date.
The marker file that carries that signal is touched once, later, in CopyFilesToOutputDirectory
(which depends on this target), so the same file isn't written twice in a normal build. -->
<PropertyGroup>
<_CopyLocalReferencesCopiedInThisBuild Condition="'@(ReferencesCopiedInThisBuild)' != '' and '$(WroteAtLeastOneFile)' == 'true'">true</_CopyLocalReferencesCopiedInThisBuild>
</PropertyGroup>

<ItemGroup>
<FileWrites Include="@(CopyUpToDateMarker)" />
Expand Down
Loading