Skip to content

Commit 82acc88

Browse files
authored
Fix dotnet-run-file up-to-date checks across symlinks (#52064)
1 parent 283babf commit 82acc88

File tree

2 files changed

+244
-3
lines changed

2 files changed

+244
-3
lines changed

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -847,17 +847,18 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro
847847
}
848848

849849
// Check that the source file is not modified.
850-
if (entryPointFile.LastWriteTimeUtc > buildTimeUtc)
850+
var targetFile = ResolveLinkTargetOrSelf(entryPointFile);
851+
if (targetFile.LastWriteTimeUtc > buildTimeUtc)
851852
{
852853
cache.CanUseCscViaPreviousArguments = true;
853-
Reporter.Verbose.WriteLine("Compiling because entry point file is modified: " + entryPointFile.FullName);
854+
Reporter.Verbose.WriteLine("Compiling because entry point file is modified: " + targetFile.FullName);
854855
return true;
855856
}
856857

857858
// Check that implicit build files are not modified.
858859
foreach (var implicitBuildFilePath in previousCacheEntry.ImplicitBuildFiles)
859860
{
860-
var implicitBuildFileInfo = new FileInfo(implicitBuildFilePath);
861+
var implicitBuildFileInfo = ResolveLinkTargetOrSelf(new FileInfo(implicitBuildFilePath));
861862
if (!implicitBuildFileInfo.Exists || implicitBuildFileInfo.LastWriteTimeUtc > buildTimeUtc)
862863
{
863864
Reporter.Verbose.WriteLine("Building because implicit build file is missing or modified: " + implicitBuildFileInfo.FullName);
@@ -876,6 +877,16 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro
876877
}
877878

878879
return false;
880+
881+
static FileSystemInfo ResolveLinkTargetOrSelf(FileSystemInfo fileSystemInfo)
882+
{
883+
if (!fileSystemInfo.Exists)
884+
{
885+
return fileSystemInfo;
886+
}
887+
888+
return fileSystemInfo.ResolveLinkTarget(returnFinalTarget: true) ?? fileSystemInfo;
889+
}
879890
}
880891

881892
private static RunFileBuildCacheEntry? DeserializeCacheEntry(string path)

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

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,62 @@ public void DirectoryBuildProps()
10321032
.And.HaveStdOut("Hello from TestName");
10331033
}
10341034

1035+
/// <summary>
1036+
/// Implicit build files are taken from the folder of the symbolic link itself, not its target.
1037+
/// This is equivalent to the behavior of symlinked project files.
1038+
/// See <see href="https://github.com/dotnet/sdk/pull/52064#issuecomment-3628958688"/>.
1039+
/// </summary>
1040+
[Fact]
1041+
public void DirectoryBuildProps_SymbolicLink()
1042+
{
1043+
var testInstance = _testAssetsManager.CreateTestDirectory();
1044+
1045+
var dir1 = Path.Join(testInstance.Path, "dir1");
1046+
Directory.CreateDirectory(dir1);
1047+
1048+
var originalPath = Path.Join(dir1, "original.cs");
1049+
File.WriteAllText(originalPath, s_program);
1050+
1051+
File.WriteAllText(Path.Join(dir1, "Directory.Build.props"), """
1052+
<Project>
1053+
<PropertyGroup>
1054+
<AssemblyName>OriginalAssemblyName</AssemblyName>
1055+
</PropertyGroup>
1056+
</Project>
1057+
""");
1058+
1059+
var dir2 = Path.Join(testInstance.Path, "dir2");
1060+
Directory.CreateDirectory(dir2);
1061+
1062+
var programFileName = "linked.cs";
1063+
var programPath = Path.Join(dir2, programFileName);
1064+
1065+
File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath);
1066+
1067+
File.WriteAllText(Path.Join(dir2, "Directory.Build.props"), """
1068+
<Project>
1069+
<PropertyGroup>
1070+
<AssemblyName>LinkedAssemblyName</AssemblyName>
1071+
</PropertyGroup>
1072+
</Project>
1073+
""");
1074+
1075+
new DotnetCommand(Log, "run", programFileName)
1076+
.WithWorkingDirectory(dir2)
1077+
.Execute()
1078+
.Should().Pass()
1079+
.And.HaveStdOut("Hello from LinkedAssemblyName");
1080+
1081+
// Removing the Directory.Build.props should be detected by up-to-date check.
1082+
File.Delete(Path.Join(dir2, "Directory.Build.props"));
1083+
1084+
new DotnetCommand(Log, "run", programFileName)
1085+
.WithWorkingDirectory(dir2)
1086+
.Execute()
1087+
.Should().Pass()
1088+
.And.HaveStdOut("Hello from linked");
1089+
}
1090+
10351091
/// <summary>
10361092
/// Overriding default (implicit) properties of file-based apps via implicit build files.
10371093
/// </summary>
@@ -3414,6 +3470,82 @@ public void UpToDate_InvalidOptions()
34143470
.And.HaveStdErrContaining(string.Format(CliCommandStrings.CannotCombineOptions, RunCommandDefinition.NoCacheOption.Name, RunCommandDefinition.NoBuildOption.Name));
34153471
}
34163472

3473+
/// <summary>
3474+
/// <see cref="UpToDate"/> optimization should see through symlinks.
3475+
/// See <see href="https://github.com/dotnet/sdk/issues/52063"/>.
3476+
/// </summary>
3477+
[Fact]
3478+
public void UpToDate_SymbolicLink()
3479+
{
3480+
var testInstance = _testAssetsManager.CreateTestDirectory();
3481+
3482+
var originalPath = Path.Join(testInstance.Path, "original.cs");
3483+
var code = """
3484+
#!/usr/bin/env dotnet
3485+
Console.WriteLine("v1");
3486+
""";
3487+
var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
3488+
File.WriteAllText(originalPath, code, utf8NoBom);
3489+
3490+
var programFileName = "linked";
3491+
var programPath = Path.Join(testInstance.Path, programFileName);
3492+
3493+
File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath);
3494+
3495+
// Remove artifacts from possible previous runs of this test.
3496+
var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
3497+
if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
3498+
3499+
Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName);
3500+
3501+
Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName);
3502+
3503+
code = code.Replace("v1", "v2");
3504+
File.WriteAllText(originalPath, code, utf8NoBom);
3505+
3506+
Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
3507+
}
3508+
3509+
/// <summary>
3510+
/// Similar to <see cref="UpToDate_SymbolicLink"/> but with a chain of symlinks.
3511+
/// </summary>
3512+
[Fact]
3513+
public void UpToDate_SymbolicLink2()
3514+
{
3515+
var testInstance = _testAssetsManager.CreateTestDirectory();
3516+
3517+
var originalPath = Path.Join(testInstance.Path, "original.cs");
3518+
var code = """
3519+
#!/usr/bin/env dotnet
3520+
Console.WriteLine("v1");
3521+
""";
3522+
var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
3523+
File.WriteAllText(originalPath, code, utf8NoBom);
3524+
3525+
var intermediateFileName = "linked1";
3526+
var intermediatePath = Path.Join(testInstance.Path, intermediateFileName);
3527+
3528+
File.CreateSymbolicLink(path: intermediatePath, pathToTarget: originalPath);
3529+
3530+
var programFileName = "linked2";
3531+
var programPath = Path.Join(testInstance.Path, programFileName);
3532+
3533+
File.CreateSymbolicLink(path: programPath, pathToTarget: intermediatePath);
3534+
3535+
// Remove artifacts from possible previous runs of this test.
3536+
var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
3537+
if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
3538+
3539+
Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName);
3540+
3541+
Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName);
3542+
3543+
code = code.Replace("v1", "v2");
3544+
File.WriteAllText(originalPath, code, utf8NoBom);
3545+
3546+
Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
3547+
}
3548+
34173549
/// <summary>
34183550
/// Up-to-date checks and optimizations currently don't support other included files.
34193551
/// </summary>
@@ -3723,6 +3855,41 @@ Hello from Program
37233855
""");
37243856
}
37253857

3858+
/// <summary>
3859+
/// Combination of <see cref="UpToDate_SymbolicLink"/> and <see cref="CscOnly"/>.
3860+
/// </summary>
3861+
[Fact]
3862+
public void CscOnly_SymbolicLink()
3863+
{
3864+
var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
3865+
3866+
var originalPath = Path.Join(testInstance.Path, "original.cs");
3867+
var code = """
3868+
#!/usr/bin/env dotnet
3869+
Console.WriteLine("v1");
3870+
""";
3871+
var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
3872+
File.WriteAllText(originalPath, code, utf8NoBom);
3873+
3874+
var programFileName = "linked";
3875+
var programPath = Path.Join(testInstance.Path, programFileName);
3876+
3877+
File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath);
3878+
3879+
// Remove artifacts from possible previous runs of this test.
3880+
var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
3881+
if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
3882+
3883+
Build(testInstance, BuildLevel.Csc, expectedOutput: "v1", programFileName: programFileName);
3884+
3885+
Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName);
3886+
3887+
code = code.Replace("v1", "v2");
3888+
File.WriteAllText(originalPath, code, utf8NoBom);
3889+
3890+
Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
3891+
}
3892+
37263893
/// <summary>
37273894
/// Tests an optimization which remembers CSC args from prior MSBuild runs and can skip subsequent MSBuild invocations and call CSC directly.
37283895
/// This optimization kicks in when the file has some <c>#:</c> directives (then the simpler "hard-coded CSC args" optimization cannot be used).
@@ -3882,6 +4049,40 @@ public void CscOnly_AfterMSBuild_HardLinks()
38824049
Build(testInstance, BuildLevel.Csc, expectedOutput: "Hi from Program");
38834050
}
38844051

4052+
/// <summary>
4053+
/// Combination of <see cref="UpToDate_SymbolicLink"/> and <see cref="CscOnly_AfterMSBuild"/>.
4054+
/// </summary>
4055+
[Fact]
4056+
public void CscOnly_AfterMSBuild_SymbolicLink()
4057+
{
4058+
var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
4059+
4060+
var originalPath = Path.Join(testInstance.Path, "original.cs");
4061+
var code = """
4062+
#!/usr/bin/env dotnet
4063+
#:property Configuration=Release
4064+
Console.WriteLine("v1");
4065+
""";
4066+
var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
4067+
File.WriteAllText(originalPath, code, utf8NoBom);
4068+
4069+
var programFileName = "linked";
4070+
var programPath = Path.Join(testInstance.Path, programFileName);
4071+
4072+
File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath);
4073+
4074+
// Remove artifacts from possible previous runs of this test.
4075+
var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
4076+
if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);
4077+
4078+
Build(testInstance, BuildLevel.All, expectedOutput: "v1", programFileName: programFileName);
4079+
4080+
code = code.Replace("v1", "v2");
4081+
File.WriteAllText(originalPath, code, utf8NoBom);
4082+
4083+
Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
4084+
}
4085+
38854086
/// <summary>
38864087
/// See <see cref="CscOnly_AfterMSBuild"/>.
38874088
/// This optimization currently does not support <c>#:project</c> references and hence is disabled if those are present.
@@ -4409,6 +4610,35 @@ public void EntryPointFilePath_WithUnicodeCharacters()
44094610
.And.HaveStdOut($"EntryPointFilePath: {filePath}");
44104611
}
44114612

4613+
[Fact]
4614+
public void EntryPointFilePath_SymbolicLink()
4615+
{
4616+
var testInstance = _testAssetsManager.CreateTestDirectory();
4617+
var fileName = "Program.cs";
4618+
var programPath = Path.Join(testInstance.Path, fileName);
4619+
File.WriteAllText(programPath, """
4620+
#!/usr/bin/env dotnet
4621+
var entryPointFilePath = AppContext.GetData("EntryPointFilePath") as string;
4622+
Console.WriteLine($"EntryPointFilePath: {entryPointFilePath}");
4623+
""");
4624+
4625+
new DotnetCommand(Log, "run", fileName)
4626+
.WithWorkingDirectory(testInstance.Path)
4627+
.Execute()
4628+
.Should().Pass()
4629+
.And.HaveStdOut($"EntryPointFilePath: {programPath}");
4630+
4631+
var linkName = "linked";
4632+
var linkPath = Path.Join(testInstance.Path, linkName);
4633+
File.CreateSymbolicLink(linkPath, programPath);
4634+
4635+
new DotnetCommand(Log, "run", linkName)
4636+
.WithWorkingDirectory(testInstance.Path)
4637+
.Execute()
4638+
.Should().Pass()
4639+
.And.HaveStdOut($"EntryPointFilePath: {linkPath}");
4640+
}
4641+
44124642
[Fact]
44134643
public void MSBuildGet_Simple()
44144644
{

0 commit comments

Comments
 (0)