diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
index 68a2665b070e..6fd756473da2 100644
--- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
+++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
@@ -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;
}
@@ -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)
@@ -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)
@@ -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.
diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
index 38049ac50e16..4fc14f3be4c2 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
@@ -4141,6 +4141,57 @@ public void CscOnly_AfterMSBuild_SymbolicLink()
Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
}
+ ///
+ /// Interaction of optimization and Directory.Build.props file.
+ ///
+ [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 = """
+
+
+ CustomAssemblyName
+
+
+ """;
+ 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");
+ }
+
///
/// See .
/// This optimization currently does not support #:project references and hence is disabled if those are present.