Skip to content
Open
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
75 changes: 46 additions & 29 deletions src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -852,12 +852,15 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro
return true;
}

// Check that the source file is not modified.
var reasonToNotReuseCscArguments = GetReasonToNotReuseCscArguments(cache);
var targetFile = ResolveLinkTargetOrSelf(entryPointFile);
if (targetFile.LastWriteTimeUtc > buildTimeUtc)

// Check that the source file is not modified.
// Only do this here if we cannot reuse CSC arguments (then checking this first is faster); otherwise we need to check implicit build files anyway.
if (reasonToNotReuseCscArguments != null && targetFile.LastWriteTimeUtc > buildTimeUtc)
{
cache.CanUseCscViaPreviousArguments = true;
Reporter.Verbose.WriteLine("Compiling because entry point file is modified: " + targetFile.FullName);
Reporter.Verbose.WriteLine(reasonToNotReuseCscArguments);
return true;
}

Expand All @@ -882,6 +885,15 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro
}
}

// If we might be able to reuse CSC arguments, check whether the source file is modified.
// NOTE: This must be the last check (otherwise setting cache.CanUseCscViaPreviousArguments would be incorrect).
if (reasonToNotReuseCscArguments == null && targetFile.LastWriteTimeUtc > buildTimeUtc)
{
cache.CanUseCscViaPreviousArguments = true;
Reporter.Verbose.WriteLine("Compiling because entry point file is modified: " + targetFile.FullName);
return true;
}

return false;

static FileSystemInfo ResolveLinkTargetOrSelf(FileSystemInfo fileSystemInfo)
Expand All @@ -893,6 +905,30 @@ static FileSystemInfo ResolveLinkTargetOrSelf(FileSystemInfo fileSystemInfo)

return fileSystemInfo.ResolveLinkTarget(returnFinalTarget: true) ?? fileSystemInfo;
}

static string? GetReasonToNotReuseCscArguments(CacheInfo cache)
{
if (cache.PreviousEntry?.CscArguments.IsDefaultOrEmpty != false)
{
return "No CSC arguments from previous run.";
}
else if (cache.PreviousEntry.Run == null)
{
return "We have CSC arguments but not run properties. That's unexpected.";
}
else if (cache.PreviousEntry.BuildResultFile == null)
{
return "We have CSC arguments but not build result file. That's unexpected.";
}
else if (!cache.PreviousEntry.Directives.SequenceEqual(cache.CurrentEntry.Directives))
{
return "Cannot use CSC arguments from previous run because directives changed.";
}
else
{
return null;
}
}
}

private static RunFileBuildCacheEntry? DeserializeCacheEntry(string path)
Expand Down Expand Up @@ -931,36 +967,17 @@ private BuildLevel GetBuildLevel(out CacheInfo cache)
return BuildLevel.None;
}

// Determine whether we can invoke CSC using previous arguments.
if (cache.CanUseCscViaPreviousArguments)
{
if (cache.PreviousEntry?.CscArguments.IsDefaultOrEmpty != false)
{
Reporter.Verbose.WriteLine("No CSC arguments from previous run.");
}
else if (cache.PreviousEntry?.Run == null)
{
Reporter.Verbose.WriteLine("We have CSC arguments but not run properties. That's unexpected.");
}
else if (cache.PreviousEntry?.BuildResultFile == null)
{
Reporter.Verbose.WriteLine("We have CSC arguments but not build result file. That's unexpected.");
}
else if (!cache.PreviousEntry.Directives.SequenceEqual(cache.CurrentEntry.Directives))
{
Reporter.Verbose.WriteLine("Cannot use CSC arguments from previous run because directives changed.");
}
else
{
Reporter.Verbose.WriteLine("We have CSC arguments from previous run. Skipping MSBuild and using CSC only.");
Reporter.Verbose.WriteLine("We have CSC arguments from previous run. Skipping MSBuild and using CSC only.");

// Keep the cached info for next time, so we can use CSC again.
cache.CurrentEntry.CscArguments = cache.PreviousEntry.CscArguments;
cache.CurrentEntry.BuildResultFile = cache.PreviousEntry.BuildResultFile;
cache.CurrentEntry.Run = cache.PreviousEntry.Run;
// Keep the cached info for next time, so we can use CSC again.
Debug.Assert(cache.PreviousEntry != null);
cache.CurrentEntry.CscArguments = cache.PreviousEntry.CscArguments;
cache.CurrentEntry.BuildResultFile = cache.PreviousEntry.BuildResultFile;
cache.CurrentEntry.Run = cache.PreviousEntry.Run;

return BuildLevel.Csc;
}
return BuildLevel.Csc;
}

// Determine whether we can use CSC only or need to use MSBuild.
Expand Down
51 changes: 51 additions & 0 deletions test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4141,6 +4141,57 @@ public void CscOnly_AfterMSBuild_SymbolicLink()
Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
}

/// <summary>
/// Interaction of optimization <see cref="CscOnly_AfterMSBuild"/> and <c>Directory.Build.props</c> file.
/// </summary>
[Theory, CombinatorialData]
public void CscOnly_AfterMSBuild_DirectoryBuildProps(bool touch1, bool touch2)
{
var testInstance = _testAssetsManager.CreateTestDirectory();

var propsPath = Path.Join(testInstance.Path, "Directory.Build.props");
var propsContent = """
<Project>
<PropertyGroup>
<AssemblyName>CustomAssemblyName</AssemblyName>
</PropertyGroup>
</Project>
""";
File.WriteAllText(propsPath, propsContent);

var programPath = Path.Join(testInstance.Path, "Program.cs");
var programVersion = 0;
void WriteProgramContent()
{
programVersion++;

// #: directive ensures we get CscOnly_AfterMSBuild optimization instead of CscOnly.
File.WriteAllText(programPath, $"""
#:property Configuration=Debug
Console.WriteLine("v{programVersion} " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
""");
}
WriteProgramContent();

// Remove artifacts from possible previous runs of this test.
var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);

Build(testInstance, BuildLevel.All, expectedOutput: $"v{programVersion} CustomAssemblyName");

File.Delete(propsPath);

if (touch1) WriteProgramContent();

Build(testInstance, BuildLevel.All, expectedOutput: $"v{programVersion} Program");

File.WriteAllText(propsPath, propsContent);

if (touch2) WriteProgramContent();

Build(testInstance, BuildLevel.All, expectedOutput: $"v{programVersion} CustomAssemblyName");
}

/// <summary>
/// See <see cref="CscOnly_AfterMSBuild"/>.
/// This optimization currently does not support <c>#:project</c> references and hence is disabled if those are present.
Expand Down
Loading