Skip to content

Commit 51297ba

Browse files
authored
Handle hardlinks with file-based app csc optimization (#51135)
Fixes a bug which I discovered while using file-based apps in roslyn repo with hardlinks in build opted-in (via `ROSLYNUSEHARDLINKS` env var). The `File.Copy` resulted in "Access denied" exception. I was puzzled at first, but after some investigation discovered that's because the MSBuild task hardlinks `obj/app.dll` with `bin/app.dll` and when we then try to `File.Copy("obj/app.dll", "bin/app.dll")` it fails with that exception because it tries to write to a file which it has locked for reading (because both files are actually the same file via the hardlink). MSBuild's Copy task doesn't have this problem because it by default checks the files have the same size and timestamp and if so, the copy is skipped. I have implemented the same behavior to fix the issue.
1 parent b77e55c commit 51297ba

File tree

2 files changed

+64
-3
lines changed

2 files changed

+64
-3
lines changed

src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,18 @@ public int Execute(out bool fallbackToNormalBuild)
110110
if (BuildResultFile != null &&
111111
CSharpCommandLineParser.Default.Parse(CscArguments, BaseDirectory, sdkDirectory: null) is { OutputFileName: { } outputFileName } parsedArgs)
112112
{
113-
var objFile = parsedArgs.GetOutputFilePath(outputFileName);
114-
Reporter.Verbose.WriteLine($"Copying '{objFile}' to '{BuildResultFile}'.");
115-
File.Copy(objFile, BuildResultFile, overwrite: true);
113+
var objFile = new FileInfo(parsedArgs.GetOutputFilePath(outputFileName));
114+
var binFile = new FileInfo(BuildResultFile);
115+
116+
if (HaveMatchingSizeAndTimeStamp(objFile, binFile))
117+
{
118+
Reporter.Verbose.WriteLine($"Skipping copy of '{objFile}' to '{BuildResultFile}' because the files have matching size and timestamp.");
119+
}
120+
else
121+
{
122+
Reporter.Verbose.WriteLine($"Copying '{objFile}' to '{BuildResultFile}'.");
123+
File.Copy(objFile.FullName, binFile.FullName, overwrite: true);
124+
}
116125
}
117126

118127
return exitCode;
@@ -153,6 +162,27 @@ static int ProcessBuildResponse(BuildResponse response, out bool fallbackToNorma
153162
return 1;
154163
}
155164
}
165+
166+
// Inspired by MSBuild: https://github.com/dotnet/msbuild/blob/a7a4d5af02be5aa6dc93a492d6d03056dc811388/src/Tasks/Copy.cs#L208
167+
static bool HaveMatchingSizeAndTimeStamp(FileInfo sourceFile, FileInfo destinationFile)
168+
{
169+
if (!destinationFile.Exists)
170+
{
171+
return false;
172+
}
173+
174+
if (sourceFile.LastWriteTimeUtc != destinationFile.LastWriteTimeUtc)
175+
{
176+
return false;
177+
}
178+
179+
if (sourceFile.Length != destinationFile.Length)
180+
{
181+
return false;
182+
}
183+
184+
return true;
185+
}
156186
}
157187

158188
private void PrepareAuxiliaryFiles(out string rspPath)

test/dotnet.Tests/CommandTests/Run/RunFileTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3258,6 +3258,37 @@ Release config
32583258
""");
32593259
}
32603260

3261+
/// <summary>
3262+
/// See <see cref="CscOnly_AfterMSBuild"/>.
3263+
/// If hard links are enabled, the <c>bin/app.dll</c> and <c>obj/app.dll</c> files are going to be the same,
3264+
/// so our "copy obj to bin" logic must account for that.
3265+
/// </summary>
3266+
[Fact]
3267+
public void CscOnly_AfterMSBuild_HardLinks()
3268+
{
3269+
var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
3270+
var programPath = Path.Join(testInstance.Path, "Program.cs");
3271+
3272+
var code = $"""
3273+
#:property CreateHardLinksForCopyFilesToOutputDirectoryIfPossible=true
3274+
#:property CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible=true
3275+
{s_program}
3276+
""";
3277+
3278+
File.WriteAllText(programPath, code);
3279+
3280+
// Remove artifacts from possible previous runs of this test.
3281+
var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
3282+
if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
3283+
3284+
Build(testInstance, BuildLevel.All);
3285+
3286+
code = code.Replace("Hello", "Hi");
3287+
File.WriteAllText(programPath, code);
3288+
3289+
Build(testInstance, BuildLevel.Csc, expectedOutput: "Hi from Program");
3290+
}
3291+
32613292
/// <summary>
32623293
/// See <see cref="CscOnly_AfterMSBuild"/>.
32633294
/// This optimization currently does not support <c>#:project</c> references and hence is disabled if those are present.

0 commit comments

Comments
 (0)