From 139948dd2348bc955a3ba582781c8962ed3d37c6 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Thu, 11 Dec 2025 13:42:22 +0100 Subject: [PATCH 1/3] Fix interaction of "csc only after msbuild" optimization and implicit build files --- .../Run/VirtualProjectBuildingCommand.cs | 19 +++---- .../CommandTests/Run/RunFileTests.cs | 51 +++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index 68a2665b070e..bd2c912c9552 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -852,15 +852,6 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro return true; } - // Check that the source file is not modified. - var targetFile = ResolveLinkTargetOrSelf(entryPointFile); - if (targetFile.LastWriteTimeUtc > buildTimeUtc) - { - cache.CanUseCscViaPreviousArguments = true; - Reporter.Verbose.WriteLine("Compiling because entry point file is modified: " + targetFile.FullName); - return true; - } - // Check that implicit build files are not modified. foreach (var implicitBuildFilePath in previousCacheEntry.ImplicitBuildFiles) { @@ -882,6 +873,16 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro } } + // Check that the source file is not modified. + // NOTE: This should be the last check (otherwise setting cache.CanUseCscViaPreviousArguments would be incorrect). + var targetFile = ResolveLinkTargetOrSelf(entryPointFile); + if (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) 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. From f9e57b553a0af11b6f988a65491fa9402f882665 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Thu, 11 Dec 2025 13:43:43 +0100 Subject: [PATCH 2/3] Remove unnecessary `?.` --- src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index bd2c912c9552..b886a9ac6205 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -939,11 +939,11 @@ private BuildLevel GetBuildLevel(out CacheInfo cache) { Reporter.Verbose.WriteLine("No CSC arguments from previous run."); } - else if (cache.PreviousEntry?.Run == null) + 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) + else if (cache.PreviousEntry.BuildResultFile == null) { Reporter.Verbose.WriteLine("We have CSC arguments but not build result file. That's unexpected."); } From 7d3df775454eefff39d6485f3e449bb395500280 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Thu, 11 Dec 2025 13:56:32 +0100 Subject: [PATCH 3/3] Check source file sooner if possible --- .../Run/VirtualProjectBuildingCommand.cs | 76 +++++++++++-------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index b886a9ac6205..6fd756473da2 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -852,6 +852,18 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro return true; } + var reasonToNotReuseCscArguments = GetReasonToNotReuseCscArguments(cache); + var targetFile = ResolveLinkTargetOrSelf(entryPointFile); + + // 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) + { + Reporter.Verbose.WriteLine("Compiling because entry point file is modified: " + targetFile.FullName); + Reporter.Verbose.WriteLine(reasonToNotReuseCscArguments); + return true; + } + // Check that implicit build files are not modified. foreach (var implicitBuildFilePath in previousCacheEntry.ImplicitBuildFiles) { @@ -873,10 +885,9 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro } } - // Check that the source file is not modified. - // NOTE: This should be the last check (otherwise setting cache.CanUseCscViaPreviousArguments would be incorrect). - var targetFile = ResolveLinkTargetOrSelf(entryPointFile); - if (targetFile.LastWriteTimeUtc > buildTimeUtc) + // 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); @@ -894,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) @@ -932,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.