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.