Skip to content
11 changes: 11 additions & 0 deletions src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,17 @@ static int ProcessBuildResponse(BuildResponse response, out bool fallbackToNorma
{
case CompletedBuildResponse completed:
Reporter.Verbose.WriteLine("Compiler server processed compilation.");

// Check if the compilation failed with CS0006 error (metadata file not found).
// This can happen when NuGet cache is cleared and referenced DLLs (e.g., analyzers or libraries) are missing.
if (completed.ReturnCode != 0 && completed.Output.Contains("error CS0006:", StringComparison.Ordinal))
{
Reporter.Verbose.WriteLine("CS0006 error detected in fast compilation path, falling back to full MSBuild.");
Reporter.Verbose.Write(completed.Output);
fallbackToNormalBuild = true;
return completed.ReturnCode;
}

Reporter.Output.Write(completed.Output);
fallbackToNormalBuild = false;
return completed.ReturnCode;
Expand Down
10 changes: 5 additions & 5 deletions src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,6 @@ public override int Execute()

if (buildLevel is BuildLevel.Csc)
{
if (binaryLogger is not null)
{
Reporter.Output.WriteLine(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc.Yellow());
}

MarkBuildStart();

// Execute CSC.
Expand All @@ -277,6 +272,11 @@ public override int Execute()
MarkBuildSuccess(cache);
}

if (binaryLogger is not null)
{
Reporter.Output.WriteLine(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc.Yellow());
}

return result;
}

Expand Down
116 changes: 112 additions & 4 deletions test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3214,7 +3214,13 @@ Release config
Build(testInstance, BuildLevel.Csc);
}

private void Build(TestDirectory testInstance, BuildLevel expectedLevel, ReadOnlySpan<string> args = default, string expectedOutput = "Hello from Program", string programFileName = "Program.cs")
private void Build(
TestDirectory testInstance,
BuildLevel expectedLevel,
ReadOnlySpan<string> args = default,
string expectedOutput = "Hello from Program",
string programFileName = "Program.cs",
Func<TestCommand, TestCommand>? customizeCommand = null)
{
string prefix = expectedLevel switch
{
Expand All @@ -3224,9 +3230,15 @@ private void Build(TestDirectory testInstance, BuildLevel expectedLevel, ReadOnl
_ => throw new ArgumentOutOfRangeException(paramName: nameof(expectedLevel)),
};

new DotnetCommand(Log, ["run", programFileName, "-bl", .. args])
.WithWorkingDirectory(testInstance.Path)
.Execute()
var command = new DotnetCommand(Log, ["run", programFileName, "-bl", .. args])
.WithWorkingDirectory(testInstance.Path);

if (customizeCommand != null)
{
command = customizeCommand(command);
}

command.Execute()
.Should().Pass()
.And.HaveStdOut(prefix + expectedOutput);

Expand Down Expand Up @@ -3873,6 +3885,102 @@ public void CscOnly_AfterMSBuild_AuxiliaryFilesNotReused()
Build(testInstance, BuildLevel.Csc, expectedOutput: "v3 ");
}

/// <summary>
/// Testing <see cref="CscOnly"/> optimization when the NuGet cache is cleared between builds.
/// See <see href="https://github.com/dotnet/sdk/issues/45169"/>.
/// </summary>
[Fact]
public void CscOnly_NuGetCacheCleared()
{
var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);

var code = """
Console.Write("v1");
""";

var programPath = Path.Join(testInstance.Path, "Program.cs");
File.WriteAllText(programPath, code);

var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);

var packageDir = Path.Join(testInstance.Path, "packages");
TestCommand CustomizeCommand(TestCommand command) => command.WithEnvironmentVariable("NUGET_PACKAGES", packageDir);

Assert.False(Directory.Exists(packageDir));

// Ensure the packages exist first.
Build(testInstance, BuildLevel.All, expectedOutput: "v1", customizeCommand: CustomizeCommand);

Assert.True(Directory.Exists(packageDir));

// Now clear the build outputs (but not packages) to verify CSC is used even from "first run".
if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);

code = code.Replace("v1", "v2");
File.WriteAllText(programPath, code);

Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", customizeCommand: CustomizeCommand);

code = code.Replace("v2", "v3");
File.WriteAllText(programPath, code);

// Clear NuGet cache.
Directory.Delete(packageDir, recursive: true);
Assert.False(Directory.Exists(packageDir));

Build(testInstance, BuildLevel.All, expectedOutput: "v3", customizeCommand: CustomizeCommand);

Assert.True(Directory.Exists(packageDir));
}

/// <summary>
/// Combination of <see cref="CscOnly_NuGetCacheCleared"/> and <see cref="CscOnly_AfterMSBuild"/>.
/// </summary>
[Fact]
public void CscOnly_AfterMSBuild_NuGetCacheCleared()
{
var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory);

var code = """
#:property PublishAot=false
#:package [email protected]
new System.CommandLine.RootCommand("v1");
Console.WriteLine("v1");
""";

var programPath = Path.Join(testInstance.Path, "Program.cs");
File.WriteAllText(programPath, code);

var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true);

var packageDir = Path.Join(testInstance.Path, "packages");
TestCommand CustomizeCommand(TestCommand command) => command.WithEnvironmentVariable("NUGET_PACKAGES", packageDir);

Assert.False(Directory.Exists(packageDir));

Build(testInstance, BuildLevel.All, expectedOutput: "v1", customizeCommand: CustomizeCommand);

Assert.True(Directory.Exists(packageDir));

code = code.Replace("v1", "v2");
File.WriteAllText(programPath, code);

Build(testInstance, BuildLevel.Csc, expectedOutput: "v2", customizeCommand: CustomizeCommand);

code = code.Replace("v2", "v3");
File.WriteAllText(programPath, code);

// Clear NuGet cache.
Directory.Delete(packageDir, recursive: true);
Assert.False(Directory.Exists(packageDir));

Build(testInstance, BuildLevel.All, expectedOutput: "v3", customizeCommand: CustomizeCommand);

Assert.True(Directory.Exists(packageDir));
}

private static string ToJson(string s) => JsonSerializer.Serialize(s);

/// <summary>
Expand Down