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.