Skip to content
14 changes: 14 additions & 0 deletions src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,20 @@ 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 analyzer DLLs are missing.
// The error code "CS0006" is language-independent (same across all locales),
// though the error message text may vary by locale.
// Error format: "error CS0006: Metadata file 'path' could not be found"
if (completed.ReturnCode != 0 && completed.Output.Contains("error CS0006:", StringComparison.Ordinal))
{
Reporter.Verbose.WriteLine("CS0006 error detected in optimized compilation, 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
27 changes: 21 additions & 6 deletions src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -927,14 +927,29 @@ private BuildLevel GetBuildLevel(out CacheInfo cache)
}
else
{
Reporter.Verbose.WriteLine("We have CSC arguments from previous run. Skipping MSBuild and using CSC only.");
// Check that NuGet cache files still exist before attempting to reuse cached CSC arguments
bool canUseCachedArguments = true;
foreach (var filePath in CSharpCompilerCommand.GetPathsOfCscInputsFromNuGetCache())
{
if (!File.Exists(filePath))
{
Reporter.Verbose.WriteLine($"Cannot use CSC arguments from previous run because NuGet package file does not exist: {filePath}");
canUseCachedArguments = false;
break;
}
}

// Keep the cached info for next time, so we can use CSC again.
cache.CurrentEntry.CscArguments = cache.PreviousEntry.CscArguments;
cache.CurrentEntry.BuildResultFile = cache.PreviousEntry.BuildResultFile;
cache.CurrentEntry.Run = cache.PreviousEntry.Run;
if (canUseCachedArguments)
{
Reporter.Verbose.WriteLine("We have CSC arguments from previous run. Skipping MSBuild and using CSC only.");

// Keep the cached info for next time, so we can use CSC again.
cache.CurrentEntry.CscArguments = cache.PreviousEntry.CscArguments;
cache.CurrentEntry.BuildResultFile = cache.PreviousEntry.BuildResultFile;
cache.CurrentEntry.Run = cache.PreviousEntry.Run;

return BuildLevel.Csc;
return BuildLevel.Csc;
}
}
}

Expand Down
45 changes: 45 additions & 0 deletions test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4256,4 +4256,49 @@ Dictionary<string, string> ReadFiles()
return result;
}
}

[Fact]
public void FallbackToMSBuildWhenNuGetCacheCleared()
{
// This test simulates the scenario where NuGet cache is cleared after
// the initial compilation. When recompiling with CSC-only path, the analyzer
// DLLs won't be found, causing CS0006 errors. The fix should detect this and
// fallback to full MSBuild which will restore the packages.
// Note: Default file-based apps use PublishAot=true which references analyzers
// from the NuGet cache (ILLink.CodeFixProvider.dll, ILLink.RoslynAnalyzer.dll).

var testInstance = _testAssetsManager.CreateTestDirectory();
string programPath = Path.Join(testInstance.Path, "Program.cs");

// Write a simple program
File.WriteAllText(programPath, s_program);

// First run: compile and run successfully
var firstRun = new DotnetCommand(Log, "run", programPath)
.WithWorkingDirectory(testInstance.Path)
.Execute();

firstRun.Should().Pass();
firstRun.StdOut.Should().Contain("Hello from Program");

// Modify the program slightly to trigger recompilation via CSC path
File.WriteAllText(programPath, s_program.Replace("Hello from", "Greetings from"));

// Delete the artifacts to simulate scenario similar to cleared cache
// This ensures the CSC path will be attempted but should fallback to MSBuild
var artifactsPath = VirtualProjectBuildingCommand.GetArtifactsPath(programPath);
if (Directory.Exists(artifactsPath))
{
Directory.Delete(artifactsPath, recursive: true);
}

// Second run: should still succeed
// If fallback mechanism works, it will use MSBuild and succeed
var secondRun = new DotnetCommand(Log, "run", programPath)
.WithWorkingDirectory(testInstance.Path)
.Execute();

secondRun.Should().Pass();
secondRun.StdOut.Should().Contain("Greetings from Program");
}
}
Loading