Files
ft/Client/Packages/io.github.hatayama.uloopmcp/Editor/Compilation/RoslynCompilerBackend.cs

444 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
namespace io.github.hatayama.uLoopMCP
{
internal static class RoslynCompilerBackend
{
private sealed class OneShotCompileResult
{
public DynamicCompilationBackendResult BackendResult { get; }
public bool ShouldFallback { get; }
private OneShotCompileResult(DynamicCompilationBackendResult backendResult, bool shouldFallback)
{
BackendResult = backendResult;
ShouldFallback = shouldFallback;
}
public static OneShotCompileResult Successful(CompilerMessage[] compilerMessages)
{
return new OneShotCompileResult(
new DynamicCompilationBackendResult(
compilerMessages,
DynamicCompilationBackendKind.OneShotRoslyn),
false);
}
public static OneShotCompileResult Fallback()
{
return new OneShotCompileResult(null, true);
}
}
public static async Task<DynamicCompilationBackendResult> CompileAsync(
string sourcePath,
string dllPath,
List<string> references,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
Action markBuildStarted,
Action markBuildFinished,
Action incrementBuildCount)
{
string workerRequestFilePath = Path.ChangeExtension(sourcePath, ".worker");
string[] defineSymbols = GetActiveDefineSymbols();
bool allowUnsafeCode = PlayerSettings.allowUnsafeCode;
try
{
WriteWorkerRequestFile(
workerRequestFilePath,
sourcePath,
dllPath,
references,
defineSymbols,
allowUnsafeCode);
CompilerMessage[] workerMessages = SharedRoslynCompilerWorkerHost.TryCompile(
workerRequestFilePath,
externalCompilerPaths,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount);
if (workerMessages != null)
{
return new DynamicCompilationBackendResult(
workerMessages,
DynamicCompilationBackendKind.SharedRoslynWorker);
}
DynamicCompilationHealthMonitor.ReportSharedWorkerFallback(
"worker_unavailable",
new
{
platform = Application.platform.ToString(),
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
compiler_dll_path = externalCompilerPaths.CompilerDllPath
});
ct.ThrowIfCancellationRequested();
OneShotCompileResult oneShotResult = await CompileWithOneShotAsync(
sourcePath,
dllPath,
references,
defineSymbols,
allowUnsafeCode,
externalCompilerPaths,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount).ConfigureAwait(false);
if (oneShotResult.ShouldFallback)
{
return await AssemblyBuilderFallbackCompilerBackend.CompileAsync(
sourcePath,
dllPath,
references,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount).ConfigureAwait(false);
}
return oneShotResult.BackendResult;
}
finally
{
if (File.Exists(workerRequestFilePath))
{
File.Delete(workerRequestFilePath);
}
}
}
private static async Task<OneShotCompileResult> CompileWithOneShotAsync(
string sourcePath,
string dllPath,
List<string> references,
IReadOnlyCollection<string> defineSymbols,
bool allowUnsafeCode,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
Action markBuildStarted,
Action markBuildFinished,
Action incrementBuildCount)
{
string responseFilePath = Path.ChangeExtension(sourcePath, ".rsp");
WriteCompilerResponseFile(
responseFilePath,
sourcePath,
dllPath,
references,
defineSymbols,
allowUnsafeCode);
try
{
incrementBuildCount();
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = externalCompilerPaths.DotnetHostPath,
Arguments = $"{QuoteCommandLineArgument(externalCompilerPaths.CompilerDllPath)} @{QuoteCommandLineArgument(responseFilePath)}",
WorkingDirectory = Directory.GetCurrentDirectory(),
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
markBuildStarted();
try
{
using Process process = ProcessStartHelper.TryStart(startInfo);
if (process == null)
{
DynamicCompilationHealthMonitor.ReportOneShotCompilerStartFailure(new
{
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
compiler_dll_path = externalCompilerPaths.CompilerDllPath
});
return OneShotCompileResult.Fallback();
}
OneShotProcessCompletionResult completionResult = await WaitForOneShotCompilerAsync(process, ct)
.ConfigureAwait(false);
CompilerMessage[] compilerMessages = ExternalCompilerMessageParser.Parse(
completionResult.StandardOutput,
completionResult.StandardError,
completionResult.ExitCode);
if (ShouldRetryWithAssemblyBuilder(process.ExitCode, compilerMessages))
{
ReportInfrastructureFallback(externalCompilerPaths, process.ExitCode);
return OneShotCompileResult.Fallback();
}
return OneShotCompileResult.Successful(compilerMessages);
}
finally
{
markBuildFinished();
}
}
finally
{
if (File.Exists(responseFilePath))
{
File.Delete(responseFilePath);
}
}
}
internal static void WriteCompilerResponseFile(
string responseFilePath,
string sourcePath,
string dllPath,
IReadOnlyCollection<string> references,
IReadOnlyCollection<string> defineSymbols,
bool allowUnsafeCode)
{
List<string> lines = new List<string>
{
"-nologo",
"-nostdlib+",
"-target:library",
"-optimize+",
"-debug-",
allowUnsafeCode ? "-unsafe+" : "-unsafe-",
QuoteResponseFileArgument("-out:", dllPath)
};
string defineOption = BuildDefineOption(defineSymbols);
if (!string.IsNullOrEmpty(defineOption))
{
lines.Add(defineOption);
}
foreach (string reference in references)
{
lines.Add(QuoteResponseFileArgument("-r:", reference));
}
lines.Add(QuoteResponseFilePath(sourcePath));
File.WriteAllLines(responseFilePath, lines);
}
internal static void WriteWorkerRequestFile(
string requestFilePath,
string sourcePath,
string dllPath,
IReadOnlyCollection<string> references,
IReadOnlyCollection<string> defineSymbols,
bool allowUnsafeCode)
{
List<string> lines = new List<string> { Path.GetFullPath(sourcePath), Path.GetFullPath(dllPath) };
lines.Add(allowUnsafeCode ? "unsafe:1" : "unsafe:0");
string serializedDefines = SerializeDefineSymbols(defineSymbols);
if (!string.IsNullOrEmpty(serializedDefines))
{
lines.Add($"define:{serializedDefines}");
}
foreach (string reference in references)
{
lines.Add($"ref:{Path.GetFullPath(reference)}");
}
File.WriteAllLines(Path.GetFullPath(requestFilePath), lines);
}
private static string[] GetActiveDefineSymbols()
{
string[] activeDefines = EditorUserBuildSettings.activeScriptCompilationDefines;
if (activeDefines == null || activeDefines.Length == 0)
{
return Array.Empty<string>();
}
List<string> filteredDefines = new List<string>(activeDefines.Length);
foreach (string define in activeDefines)
{
if (!string.IsNullOrWhiteSpace(define))
{
filteredDefines.Add(define);
}
}
return filteredDefines.ToArray();
}
private static string BuildDefineOption(IReadOnlyCollection<string> defineSymbols)
{
string serializedDefines = SerializeDefineSymbols(defineSymbols);
if (string.IsNullOrEmpty(serializedDefines))
{
return null;
}
return $"-define:{serializedDefines}";
}
private static string SerializeDefineSymbols(IReadOnlyCollection<string> defineSymbols)
{
if (defineSymbols == null || defineSymbols.Count == 0)
{
return string.Empty;
}
List<string> filteredDefines = new List<string>(defineSymbols.Count);
foreach (string defineSymbol in defineSymbols)
{
if (!string.IsNullOrWhiteSpace(defineSymbol))
{
filteredDefines.Add(defineSymbol);
}
}
return filteredDefines.Count == 0 ? string.Empty : string.Join(";", filteredDefines);
}
private static string QuoteResponseFileArgument(string prefix, string value)
{
return $"{prefix}{QuoteResponseFilePath(value)}";
}
private static string QuoteResponseFilePath(string path)
{
return $"\"{path}\"";
}
// Infrastructure-level failures (non-zero exit without file-specific diagnostics)
// indicate the compiler itself broke, not the user's code.
private static bool ShouldRetryWithAssemblyBuilder(
int exitCode,
IReadOnlyCollection<CompilerMessage> compilerMessages)
{
if (exitCode == 0)
{
return false;
}
foreach (CompilerMessage compilerMessage in compilerMessages)
{
if (compilerMessage.type == CompilerMessageType.Error &&
!string.IsNullOrWhiteSpace(compilerMessage.file))
{
return false;
}
}
return true;
}
private static string QuoteCommandLineArgument(string value)
{
return $"\"{value}\"";
}
internal sealed class OneShotProcessCompletionResult
{
public string StandardOutput { get; }
public string StandardError { get; }
public int ExitCode { get; }
public OneShotProcessCompletionResult(
string standardOutput,
string standardError,
int exitCode)
{
StandardOutput = standardOutput;
StandardError = standardError;
ExitCode = exitCode;
}
}
private static async Task<OneShotProcessCompletionResult> WaitForOneShotCompilerAsync(
Process process,
CancellationToken ct)
{
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync();
Task<string> stderrTask = process.StandardError.ReadToEndAsync();
Task waitForExitTask = Task.Run(() => process.WaitForExit());
return await AwaitOneShotProcessCompletionAsync(
stdoutTask,
stderrTask,
waitForExitTask,
() => process.ExitCode,
() => RequestCancellation(process),
ct).ConfigureAwait(false);
}
private static void RequestCancellation(Process process)
{
if (!process.HasExited)
{
process.Kill();
}
}
internal static async Task<OneShotProcessCompletionResult> AwaitOneShotProcessCompletionAsync(
Task<string> stdoutTask,
Task<string> stderrTask,
Task waitForExitTask,
Func<int> getExitCode,
Action requestCancellation,
CancellationToken ct)
{
Task completionTask = Task.WhenAll(stdoutTask, stderrTask, waitForExitTask);
Task cancellationTask = Task.Delay(Timeout.Infinite, ct);
Task finishedTask = await Task.WhenAny(completionTask, cancellationTask).ConfigureAwait(false);
if (!ReferenceEquals(finishedTask, completionTask))
{
requestCancellation();
ObserveTaskFault(completionTask);
ct.ThrowIfCancellationRequested();
}
await completionTask.ConfigureAwait(false);
return new OneShotProcessCompletionResult(
stdoutTask.Result,
stderrTask.Result,
getExitCode());
}
internal static void ReportInfrastructureFallback(
ExternalCompilerPaths externalCompilerPaths,
int exitCode)
{
DynamicCompilationHealthMonitor.ReportOneShotCompilerStartFailure(new
{
reason = "infrastructure_failure",
exit_code = exitCode,
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
compiler_dll_path = externalCompilerPaths.CompilerDllPath
});
}
private static void ObserveTaskFault(Task task)
{
_ = task.ContinueWith(
static observedTask => _ = observedTask.Exception,
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
}
}