diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
index 7721153da981..1ea2e2e68235 100644
--- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
+++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
@@ -847,17 +847,18 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro
}
// Check that the source file is not modified.
- if (entryPointFile.LastWriteTimeUtc > buildTimeUtc)
+ var targetFile = ResolveLinkTargetOrSelf(entryPointFile);
+ if (targetFile.LastWriteTimeUtc > buildTimeUtc)
{
cache.CanUseCscViaPreviousArguments = true;
- Reporter.Verbose.WriteLine("Compiling because entry point file is modified: " + entryPointFile.FullName);
+ Reporter.Verbose.WriteLine("Compiling because entry point file is modified: " + targetFile.FullName);
return true;
}
// Check that implicit build files are not modified.
foreach (var implicitBuildFilePath in previousCacheEntry.ImplicitBuildFiles)
{
- var implicitBuildFileInfo = new FileInfo(implicitBuildFilePath);
+ var implicitBuildFileInfo = ResolveLinkTargetOrSelf(new FileInfo(implicitBuildFilePath));
if (!implicitBuildFileInfo.Exists || implicitBuildFileInfo.LastWriteTimeUtc > buildTimeUtc)
{
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
}
return false;
+
+ static FileSystemInfo ResolveLinkTargetOrSelf(FileSystemInfo fileSystemInfo)
+ {
+ if (!fileSystemInfo.Exists)
+ {
+ return fileSystemInfo;
+ }
+
+ return fileSystemInfo.ResolveLinkTarget(returnFinalTarget: true) ?? fileSystemInfo;
+ }
}
private static RunFileBuildCacheEntry? DeserializeCacheEntry(string path)
diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
index ebe57115cb42..7556c10de1d5 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
@@ -1021,6 +1021,62 @@ public void DirectoryBuildProps()
.And.HaveStdOut("Hello from TestName");
}
+ ///
+ /// Implicit build files are taken from the folder of the symbolic link itself, not its target.
+ /// This is equivalent to the behavior of symlinked project files.
+ /// See .
+ ///
+ [Fact]
+ public void DirectoryBuildProps_SymbolicLink()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+
+ var dir1 = Path.Join(testInstance.Path, "dir1");
+ Directory.CreateDirectory(dir1);
+
+ var originalPath = Path.Join(dir1, "original.cs");
+ File.WriteAllText(originalPath, s_program);
+
+ File.WriteAllText(Path.Join(dir1, "Directory.Build.props"), """
+
+
+ OriginalAssemblyName
+
+
+ """);
+
+ var dir2 = Path.Join(testInstance.Path, "dir2");
+ Directory.CreateDirectory(dir2);
+
+ var programFileName = "linked.cs";
+ var programPath = Path.Join(dir2, programFileName);
+
+ File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath);
+
+ File.WriteAllText(Path.Join(dir2, "Directory.Build.props"), """
+
+
+ LinkedAssemblyName
+
+
+ """);
+
+ new DotnetCommand(Log, "run", programFileName)
+ .WithWorkingDirectory(dir2)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut("Hello from LinkedAssemblyName");
+
+ // Removing the Directory.Build.props should be detected by up-to-date check.
+ File.Delete(Path.Join(dir2, "Directory.Build.props"));
+
+ new DotnetCommand(Log, "run", programFileName)
+ .WithWorkingDirectory(dir2)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut("Hello from linked");
+ }
+
///
/// Overriding default (implicit) properties of file-based apps via implicit build files.
///
@@ -3403,6 +3459,82 @@ public void UpToDate_InvalidOptions()
.And.HaveStdErrContaining(string.Format(CliCommandStrings.CannotCombineOptions, RunCommandParser.NoCacheOption.Name, RunCommandParser.NoBuildOption.Name));
}
+ ///
+ /// optimization should see through symlinks.
+ /// See .
+ ///
+ [Fact]
+ public void UpToDate_SymbolicLink()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+
+ var originalPath = Path.Join(testInstance.Path, "original.cs");
+ var code = """
+ #!/usr/bin/env dotnet
+ Console.WriteLine("v1");
+ """;
+ var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+ File.WriteAllText(originalPath, code, utf8NoBom);
+
+ var programFileName = "linked";
+ var programPath = Path.Join(testInstance.Path, programFileName);
+
+ File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath);
+
+ // 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: "v1", programFileName: programFileName);
+
+ Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName);
+
+ code = code.Replace("v1", "v2");
+ File.WriteAllText(originalPath, code, utf8NoBom);
+
+ Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
+ }
+
+ ///
+ /// Similar to but with a chain of symlinks.
+ ///
+ [Fact]
+ public void UpToDate_SymbolicLink2()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+
+ var originalPath = Path.Join(testInstance.Path, "original.cs");
+ var code = """
+ #!/usr/bin/env dotnet
+ Console.WriteLine("v1");
+ """;
+ var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+ File.WriteAllText(originalPath, code, utf8NoBom);
+
+ var intermediateFileName = "linked1";
+ var intermediatePath = Path.Join(testInstance.Path, intermediateFileName);
+
+ File.CreateSymbolicLink(path: intermediatePath, pathToTarget: originalPath);
+
+ var programFileName = "linked2";
+ var programPath = Path.Join(testInstance.Path, programFileName);
+
+ File.CreateSymbolicLink(path: programPath, pathToTarget: intermediatePath);
+
+ // 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: "v1", programFileName: programFileName);
+
+ Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName);
+
+ code = code.Replace("v1", "v2");
+ File.WriteAllText(originalPath, code, utf8NoBom);
+
+ Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
+ }
+
///
/// Up-to-date checks and optimizations currently don't support other included files.
///
@@ -3712,6 +3844,41 @@ Hello from Program
""");
}
+ ///
+ /// Combination of and .
+ ///
+ [Fact]
+ public void CscOnly_SymbolicLink()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
+
+ var originalPath = Path.Join(testInstance.Path, "original.cs");
+ var code = """
+ #!/usr/bin/env dotnet
+ Console.WriteLine("v1");
+ """;
+ var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+ File.WriteAllText(originalPath, code, utf8NoBom);
+
+ var programFileName = "linked";
+ var programPath = Path.Join(testInstance.Path, programFileName);
+
+ File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath);
+
+ // 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.Csc, expectedOutput: "v1", programFileName: programFileName);
+
+ Build(testInstance, BuildLevel.None, expectedOutput: "v1", programFileName: programFileName);
+
+ code = code.Replace("v1", "v2");
+ File.WriteAllText(originalPath, code, utf8NoBom);
+
+ Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
+ }
+
///
/// Tests an optimization which remembers CSC args from prior MSBuild runs and can skip subsequent MSBuild invocations and call CSC directly.
/// This optimization kicks in when the file has some #: directives (then the simpler "hard-coded CSC args" optimization cannot be used).
@@ -3871,6 +4038,40 @@ public void CscOnly_AfterMSBuild_HardLinks()
Build(testInstance, BuildLevel.Csc, expectedOutput: "Hi from Program");
}
+ ///
+ /// Combination of and .
+ ///
+ [Fact]
+ public void CscOnly_AfterMSBuild_SymbolicLink()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);
+
+ var originalPath = Path.Join(testInstance.Path, "original.cs");
+ var code = """
+ #!/usr/bin/env dotnet
+ #:property Configuration=Release
+ Console.WriteLine("v1");
+ """;
+ var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+ File.WriteAllText(originalPath, code, utf8NoBom);
+
+ var programFileName = "linked";
+ var programPath = Path.Join(testInstance.Path, programFileName);
+
+ File.CreateSymbolicLink(path: programPath, pathToTarget: originalPath);
+
+ // 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: "v1", programFileName: programFileName);
+
+ code = code.Replace("v1", "v2");
+ File.WriteAllText(originalPath, code, utf8NoBom);
+
+ Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", programFileName: programFileName);
+ }
+
///
/// See .
/// This optimization currently does not support #:project references and hence is disabled if those are present.
@@ -4398,6 +4599,35 @@ public void EntryPointFilePath_WithUnicodeCharacters()
.And.HaveStdOut($"EntryPointFilePath: {filePath}");
}
+ [Fact]
+ public void EntryPointFilePath_SymbolicLink()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var fileName = "Program.cs";
+ var programPath = Path.Join(testInstance.Path, fileName);
+ File.WriteAllText(programPath, """
+ #!/usr/bin/env dotnet
+ var entryPointFilePath = AppContext.GetData("EntryPointFilePath") as string;
+ Console.WriteLine($"EntryPointFilePath: {entryPointFilePath}");
+ """);
+
+ new DotnetCommand(Log, "run", fileName)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut($"EntryPointFilePath: {programPath}");
+
+ var linkName = "linked";
+ var linkPath = Path.Join(testInstance.Path, linkName);
+ File.CreateSymbolicLink(linkPath, programPath);
+
+ new DotnetCommand(Log, "run", linkName)
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut($"EntryPointFilePath: {linkPath}");
+ }
+
[Fact]
public void MSBuildGet_Simple()
{