From 535d1f1ab0557fbb6d3fa755450d141ca9bd94b3 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 6 Oct 2025 15:41:20 +0200 Subject: [PATCH] Handle hardlinks with file-based app csc optimization --- .../Commands/Run/CSharpCompilerCommand.cs | 36 +++++++++++++++++-- .../CommandTests/Run/RunFileTests.cs | 31 ++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs index ac20c4286ba0..84ac2b073fe0 100644 --- a/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs +++ b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs @@ -110,9 +110,18 @@ public int Execute(out bool fallbackToNormalBuild) if (BuildResultFile != null && CSharpCommandLineParser.Default.Parse(CscArguments, BaseDirectory, sdkDirectory: null) is { OutputFileName: { } outputFileName } parsedArgs) { - var objFile = parsedArgs.GetOutputFilePath(outputFileName); - Reporter.Verbose.WriteLine($"Copying '{objFile}' to '{BuildResultFile}'."); - File.Copy(objFile, BuildResultFile, overwrite: true); + var objFile = new FileInfo(parsedArgs.GetOutputFilePath(outputFileName)); + var binFile = new FileInfo(BuildResultFile); + + if (HaveMatchingSizeAndTimeStamp(objFile, binFile)) + { + Reporter.Verbose.WriteLine($"Skipping copy of '{objFile}' to '{BuildResultFile}' because the files have matching size and timestamp."); + } + else + { + Reporter.Verbose.WriteLine($"Copying '{objFile}' to '{BuildResultFile}'."); + File.Copy(objFile.FullName, binFile.FullName, overwrite: true); + } } return exitCode; @@ -153,6 +162,27 @@ static int ProcessBuildResponse(BuildResponse response, out bool fallbackToNorma return 1; } } + + // Inspired by MSBuild: https://github.com/dotnet/msbuild/blob/a7a4d5af02be5aa6dc93a492d6d03056dc811388/src/Tasks/Copy.cs#L208 + static bool HaveMatchingSizeAndTimeStamp(FileInfo sourceFile, FileInfo destinationFile) + { + if (!destinationFile.Exists) + { + return false; + } + + if (sourceFile.LastWriteTimeUtc != destinationFile.LastWriteTimeUtc) + { + return false; + } + + if (sourceFile.Length != destinationFile.Length) + { + return false; + } + + return true; + } } private void PrepareAuxiliaryFiles(out string rspPath) diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index 5cdf670159a3..66a56562076e 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -3258,6 +3258,37 @@ Release config """); } + /// + /// See . + /// If hard links are enabled, the bin/app.dll and obj/app.dll files are going to be the same, + /// so our "copy obj to bin" logic must account for that. + /// + [Fact] + public void CscOnly_AfterMSBuild_HardLinks() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + var programPath = Path.Join(testInstance.Path, "Program.cs"); + + var code = $""" + #:property CreateHardLinksForCopyFilesToOutputDirectoryIfPossible=true + #:property CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible=true + {s_program} + """; + + File.WriteAllText(programPath, code); + + // 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); + + code = code.Replace("Hello", "Hi"); + File.WriteAllText(programPath, code); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "Hi from Program"); + } + /// /// See . /// This optimization currently does not support #:project references and hence is disabled if those are present.