Add Unity Pipeline package support
This commit is contained in:
+327
@@ -0,0 +1,327 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Unity.Pipeline.Models;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Compilation
|
||||
{
|
||||
/// <summary>
|
||||
/// C# code evaluation service using shared Roslyn compilation infrastructure.
|
||||
/// Wraps user code in Execute() method, compiles, and executes with result serialization.
|
||||
/// Supports both Editor and Runtime compilation for desktop development builds.
|
||||
/// </summary>
|
||||
public static class EvalCodeCompiler
|
||||
{
|
||||
#if UNITY_EDITOR || (UNITY_STANDALONE && DEBUG)
|
||||
|
||||
// Fixed lines in the generated source before user code begins
|
||||
private const int BaseCodeLineOffset = 11;
|
||||
|
||||
/// <summary>
|
||||
/// Compile and execute code on Unity's main thread using Roslyn.
|
||||
/// Eval is a MainThreadRequired command, so the server marshals the caller to the main
|
||||
/// thread before invoking this; compilation + execution run synchronously here.
|
||||
/// </summary>
|
||||
public static EvalResponse CompileAndExecuteOnMainThread(string code, int timeoutMs, System.Diagnostics.Stopwatch stopwatch)
|
||||
{
|
||||
if (stopwatch == null)
|
||||
{
|
||||
stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Compile on current thread
|
||||
var compilationResult = CompileCodeToAssembly(code, timeoutMs, stopwatch);
|
||||
|
||||
if (!compilationResult.Success)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
return EvalResponse.EvalFailure(
|
||||
"Compilation Failed",
|
||||
"Code compilation failed",
|
||||
stopwatch.ElapsedMilliseconds,
|
||||
compilationResult.Diagnostics);
|
||||
}
|
||||
|
||||
// Execute on current thread
|
||||
var executionResult = ExecuteCompiledAssembly(compilationResult.Assembly);
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
if (executionResult.Success)
|
||||
{
|
||||
return EvalResponse.EvalSuccess(
|
||||
executionResult.ReturnValue,
|
||||
executionResult.Output,
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
return EvalResponse.EvalFailure(
|
||||
executionResult.Error,
|
||||
executionResult.ErrorDetails,
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
return EvalResponse.EvalFailure(
|
||||
"Execution Failed",
|
||||
ex.ToString(),
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile C# code to assembly using shared RoslynCompilationService.
|
||||
/// Thread-safe and can run on background thread.
|
||||
/// </summary>
|
||||
private static CompilationResult CompileCodeToAssembly(string userCode, int timeoutMs, System.Diagnostics.Stopwatch stopwatch)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString("N").Substring(0, 8);
|
||||
|
||||
// Generate wrapper code
|
||||
var sourceCode = BuildSourceCode(id, userCode, Application.isEditor);
|
||||
|
||||
// Use shared compilation service
|
||||
var request = new CompilationRequest
|
||||
{
|
||||
SourceCode = sourceCode,
|
||||
AssemblyName = $"PipelineEval_{id}",
|
||||
LineNumberOffset = BaseCodeLineOffset // Adjust diagnostics for wrapper code
|
||||
};
|
||||
|
||||
var result = RoslynCompilationService.Compile(request);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
// Add execution context for eval-specific needs
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = true,
|
||||
Assembly = result.Assembly,
|
||||
ExecutionContext = new ExecutionContext
|
||||
{
|
||||
AssemblyId = id,
|
||||
TypeName = $"PipelineEvaluation.PipelineEval_{id}",
|
||||
MethodName = "Execute"
|
||||
},
|
||||
Diagnostics = result.Diagnostics
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
Diagnostics = result.Diagnostics
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate C# source code wrapper for user code with conditional editor support.
|
||||
/// </summary>
|
||||
private static string BuildSourceCode(string id, string userCode, bool isEditorContext)
|
||||
{
|
||||
var editorUsing = isEditorContext ? "using UnityEditor;" : "";
|
||||
|
||||
return $@"using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
{editorUsing}
|
||||
|
||||
namespace PipelineEvaluation
|
||||
{{
|
||||
public static class PipelineEval_{id}
|
||||
{{
|
||||
public static object Execute()
|
||||
{{
|
||||
{userCode}
|
||||
return null;
|
||||
}}
|
||||
}}
|
||||
}}";
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Execute compiled assembly and return result.
|
||||
/// Must run on Unity's main thread for safe Unity API access.
|
||||
/// </summary>
|
||||
private static ExecutionResult ExecuteCompiledAssembly(Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
var executionContext = GetExecutionContextFromAssembly(assembly);
|
||||
|
||||
var type = assembly.GetType(executionContext.TypeName);
|
||||
var method = type?.GetMethod(executionContext.MethodName, BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
Error = "Assembly Load Error",
|
||||
ErrorDetails = $"Could not find type {executionContext.TypeName} in compiled assembly"
|
||||
};
|
||||
}
|
||||
|
||||
if (method == null)
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
Error = "Method Not Found",
|
||||
ErrorDetails = $"Could not find method {executionContext.MethodName} in compiled type"
|
||||
};
|
||||
}
|
||||
|
||||
var result = method.Invoke(null, null);
|
||||
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = true,
|
||||
ReturnValue = SerializeResult(result)
|
||||
};
|
||||
}
|
||||
catch (TargetInvocationException tie) when (tie.InnerException != null)
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
Error = "Runtime Error",
|
||||
ErrorDetails = tie.InnerException.Message,
|
||||
StackTrace = tie.InnerException.StackTrace
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
Error = "Execution Error",
|
||||
ErrorDetails = ex.Message,
|
||||
StackTrace = ex.StackTrace
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get execution context from compiled assembly.
|
||||
/// </summary>
|
||||
private static ExecutionContext GetExecutionContextFromAssembly(Assembly assembly)
|
||||
{
|
||||
// Find PipelineEvaluation types
|
||||
var evalTypes = assembly.GetTypes()
|
||||
.Where(t => t.Namespace == "PipelineEvaluation" && t.Name.StartsWith("PipelineEval_"))
|
||||
.ToList();
|
||||
|
||||
if (evalTypes.Any())
|
||||
{
|
||||
var evalType = evalTypes.First();
|
||||
return new ExecutionContext
|
||||
{
|
||||
TypeName = evalType.FullName,
|
||||
MethodName = "Execute"
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return new ExecutionContext
|
||||
{
|
||||
TypeName = "PipelineEvaluation.PipelineEval_Unknown",
|
||||
MethodName = "Execute"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize result for JSON response.
|
||||
/// </summary>
|
||||
private static object SerializeResult(object value)
|
||||
{
|
||||
if (value == null) return null;
|
||||
|
||||
// Handle primitive types directly
|
||||
if (value is string || value is bool || value is int || value is long || value is float || value is double)
|
||||
return value;
|
||||
|
||||
// Handle Unity objects
|
||||
if (value is UnityEngine.Object unityObj)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Newtonsoft.Json.JsonConvert.DeserializeObject(UnityEngine.JsonUtility.ToJson(unityObj));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle other objects via JSON serialization
|
||||
try
|
||||
{
|
||||
return Newtonsoft.Json.JsonConvert.DeserializeObject(
|
||||
Newtonsoft.Json.JsonConvert.SerializeObject(value));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from compilation process.
|
||||
/// </summary>
|
||||
private class CompilationResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public Assembly Assembly { get; set; }
|
||||
public ExecutionContext ExecutionContext { get; set; }
|
||||
public List<DiagnosticInfo> Diagnostics { get; set; } = new List<DiagnosticInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from code execution.
|
||||
/// </summary>
|
||||
private class ExecutionResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public object ReturnValue { get; set; }
|
||||
public string Output { get; set; }
|
||||
public string Error { get; set; }
|
||||
public string ErrorDetails { get; set; }
|
||||
public string StackTrace { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execution context for compiled assembly.
|
||||
/// </summary>
|
||||
private class ExecutionContext
|
||||
{
|
||||
public string AssemblyId { get; set; }
|
||||
public string TypeName { get; set; }
|
||||
public string MethodName { get; set; }
|
||||
}
|
||||
|
||||
#else
|
||||
/// <summary>
|
||||
/// Runtime compilation not supported on this platform.
|
||||
/// Desktop development builds only (Windows/Mac/Linux).
|
||||
/// </summary>
|
||||
public static EvalResponse CompileAndExecuteOnMainThread(string code, int timeoutMs, System.Diagnostics.Stopwatch stopwatch)
|
||||
{
|
||||
return EvalResponse.EvalFailure(
|
||||
"Platform Not Supported",
|
||||
"Runtime code compilation only supported on Desktop development builds");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1ea6a44edd711194a968a40f1eb16f59
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+804
@@ -0,0 +1,804 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Unity.Pipeline.HotReload;
|
||||
using Unity.Pipeline.Models;
|
||||
using Unity.Pipeline.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Compilation
|
||||
{
|
||||
/// <summary>
|
||||
/// Compiles hot reload source files using shared RoslynCompilationService.
|
||||
/// Creates versioned DLLs and manages hot reload assembly lifecycle.
|
||||
/// Fixed deadlock issue by adding main thread detection like EvalCodeCompiler.
|
||||
/// </summary>
|
||||
public static class HotReloadCompiler
|
||||
{
|
||||
#if UNITY_EDITOR || (UNITY_STANDALONE && DEBUG)
|
||||
|
||||
private static readonly Dictionary<string, int> _versionTracker = new();
|
||||
private const string HotReloadTempDir = "Temp/HotReload";
|
||||
|
||||
/// <summary>
|
||||
/// Compile a hot reload source file and apply the overrides immediately.
|
||||
/// Fixed deadlock by detecting main thread and running synchronously when needed.
|
||||
/// </summary>
|
||||
/// <param name="filename">Hot reload file to compile</param>
|
||||
/// <param name="assemblyDir">Optional directory to save compiled assembly to disk. If null, assembly stays in memory only.</param>
|
||||
public static Task<HotReloadCompileResult> CompileAndApplyAsync(string filename, string assemblyDir = null)
|
||||
{
|
||||
// Hot reload commands are MainThreadRequired, so we are already on the main thread.
|
||||
// Run synchronously (no background thread / dispatcher). Returns a completed Task for
|
||||
// signature compatibility.
|
||||
return Task.FromResult(CompileAndApplyOnMainThread(filename, assemblyDir));
|
||||
}
|
||||
|
||||
public static string GetHotReloadPath(string filePath)
|
||||
{
|
||||
if (Path.IsPathRooted(filePath))
|
||||
return filePath;
|
||||
|
||||
// Relative paths are resolved against the current working directory (the Unity
|
||||
// project root). Override files can live in any folder; there is no special
|
||||
// "HotReload" location.
|
||||
return Path.GetFullPath(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile and apply on main thread synchronously to avoid deadlocks.
|
||||
/// </summary>
|
||||
/// <param name="filename">Hot reload file to compile</param>
|
||||
/// <param name="assemblyDir">Optional directory to save compiled assembly to disk. If null, assembly stays in memory only.</param>
|
||||
public static HotReloadCompileResult CompileAndApplyOnMainThread(string filename, string assemblyDir = null)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// Resolve the override file path (absolute, or relative to the project root).
|
||||
var hotReloadPath = GetHotReloadPath(filename);
|
||||
if (!File.Exists(hotReloadPath))
|
||||
{
|
||||
return HotReloadCompileResult.Failure(
|
||||
"File Not Found",
|
||||
$"Hot reload file not found: {hotReloadPath}",
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
Debug.Log($"HotReload: Starting synchronous compilation of {filename}");
|
||||
|
||||
// Read source code
|
||||
var sourceCode = File.ReadAllText(hotReloadPath);
|
||||
if (string.IsNullOrWhiteSpace(sourceCode))
|
||||
{
|
||||
return HotReloadCompileResult.Failure(
|
||||
"Empty File",
|
||||
$"Hot reload file is empty: {hotReloadPath}",
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
// Generate versioned assembly name
|
||||
var baseFilename = Path.GetFileNameWithoutExtension(filename);
|
||||
var nextVersion = GetNextVersion(baseFilename);
|
||||
var assemblyName = $"{baseFilename}_{nextVersion:D3}";
|
||||
|
||||
// Ensure temp directory exists
|
||||
Directory.CreateDirectory(HotReloadTempDir);
|
||||
|
||||
// Compile using shared RoslynCompilationService
|
||||
var compilationResult = CompileHotReloadAssembly(sourceCode, assemblyName);
|
||||
|
||||
if (!compilationResult.Success)
|
||||
{
|
||||
return HotReloadCompileResult.Failure(
|
||||
"Compilation Failed",
|
||||
"Hot reload file compilation failed",
|
||||
stopwatch.ElapsedMilliseconds,
|
||||
compilationResult.Diagnostics.Select(d => d.Message).ToList());
|
||||
}
|
||||
|
||||
// Register hot reload methods from compiled assembly
|
||||
var registeredMethods = RegisterHotReloadMethods(compilationResult.Assembly, assemblyName, out var skippedOverrides);
|
||||
|
||||
// Save assembly to disk if assemblyDir is specified
|
||||
string actualAssemblyPath = null;
|
||||
if (!string.IsNullOrWhiteSpace(assemblyDir))
|
||||
{
|
||||
Directory.CreateDirectory(assemblyDir);
|
||||
actualAssemblyPath = Path.Combine(assemblyDir, $"{assemblyName}.dll");
|
||||
File.WriteAllBytes(actualAssemblyPath, compilationResult.AssemblyBytes);
|
||||
Debug.Log($"HotReload: Assembly saved to disk: {actualAssemblyPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
actualAssemblyPath = Path.Combine(HotReloadTempDir, $"{assemblyName}.dll"); // For compatibility (in-memory)
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
Debug.Log($"HotReload: Successfully compiled {filename} -> {assemblyName}.dll with {registeredMethods.Count} methods in {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
||||
var compileResult = HotReloadCompileResult.Success(
|
||||
assemblyName,
|
||||
actualAssemblyPath,
|
||||
registeredMethods,
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
compileResult.Diagnostics = skippedOverrides;
|
||||
return compileResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
Debug.LogError($"HotReload: Synchronous compilation failed for {filename}: {ex.Message}");
|
||||
return HotReloadCompileResult.Failure(
|
||||
"Exception",
|
||||
ex.ToString(),
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal async compilation implementation for background threads.
|
||||
/// </summary>
|
||||
/// <param name="filename">Hot reload file to compile</param>
|
||||
/// <param name="assemblyDir">Optional directory to save compiled assembly to disk. If null, assembly stays in memory only.</param>
|
||||
private static async Task<HotReloadCompileResult> CompileAndApplyInternalAsync(string filename, string assemblyDir = null)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// Resolve the override file path (absolute, or relative to the project root).
|
||||
var hotReloadPath = GetHotReloadPath(filename);
|
||||
if (!File.Exists(hotReloadPath))
|
||||
{
|
||||
return HotReloadCompileResult.Failure(
|
||||
"File Not Found",
|
||||
$"Hot reload file not found: {hotReloadPath}",
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
Debug.Log($"HotReload: Starting async compilation of {filename}");
|
||||
|
||||
// Read source code
|
||||
var sourceCode = File.ReadAllText(hotReloadPath);
|
||||
if (string.IsNullOrWhiteSpace(sourceCode))
|
||||
{
|
||||
return HotReloadCompileResult.Failure(
|
||||
"Empty File",
|
||||
$"Hot reload file is empty: {hotReloadPath}",
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
// Generate versioned assembly name
|
||||
var baseFilename = Path.GetFileNameWithoutExtension(filename);
|
||||
var nextVersion = GetNextVersion(baseFilename);
|
||||
var assemblyName = $"{baseFilename}_{nextVersion:D3}";
|
||||
|
||||
// Ensure temp directory exists
|
||||
Directory.CreateDirectory(HotReloadTempDir);
|
||||
|
||||
// Compile using shared RoslynCompilationService async
|
||||
var compilationResult = await CompileHotReloadAssemblyAsync(sourceCode, assemblyName);
|
||||
|
||||
if (!compilationResult.Success)
|
||||
{
|
||||
return HotReloadCompileResult.Failure(
|
||||
"Compilation Failed",
|
||||
"Hot reload file compilation failed",
|
||||
stopwatch.ElapsedMilliseconds,
|
||||
compilationResult.Diagnostics.Select(d => d.Message).ToList());
|
||||
}
|
||||
|
||||
// Register hot reload methods from compiled assembly
|
||||
var registeredMethods = RegisterHotReloadMethods(compilationResult.Assembly, assemblyName, out var skippedOverrides);
|
||||
|
||||
// Save assembly to disk if assemblyDir is specified
|
||||
string actualAssemblyPath = null;
|
||||
if (!string.IsNullOrWhiteSpace(assemblyDir))
|
||||
{
|
||||
Directory.CreateDirectory(assemblyDir);
|
||||
actualAssemblyPath = Path.Combine(assemblyDir, $"{assemblyName}.dll");
|
||||
File.WriteAllBytes(actualAssemblyPath, compilationResult.AssemblyBytes);
|
||||
Debug.Log($"HotReload: Assembly saved to disk: {actualAssemblyPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
actualAssemblyPath = Path.Combine(HotReloadTempDir, $"{assemblyName}.dll"); // For compatibility (in-memory)
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
Debug.Log($"HotReload: Successfully compiled {filename} -> {assemblyName}.dll with {registeredMethods.Count} methods in {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
||||
var compileResult = HotReloadCompileResult.Success(
|
||||
assemblyName,
|
||||
actualAssemblyPath,
|
||||
registeredMethods,
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
compileResult.Diagnostics = skippedOverrides;
|
||||
return compileResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
Debug.LogError($"HotReload: Async compilation failed for {filename}: {ex.Message}");
|
||||
return HotReloadCompileResult.Failure(
|
||||
"Exception",
|
||||
ex.ToString(),
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile hot reload source code directly (for in-place editing workflow).
|
||||
/// </summary>
|
||||
/// <param name="sourceCode">Hot reload source code to compile</param>
|
||||
/// <param name="baseFileName">Base filename for versioned assembly naming</param>
|
||||
/// <param name="assemblyDir">Optional directory to save compiled assembly to disk</param>
|
||||
public static Task<HotReloadCompilationResult> CompileSourceCodeAsync(string sourceCode, string baseFileName, string assemblyDir = null, bool emitPdb = false, string documentPath = null)
|
||||
{
|
||||
// Runs synchronously on the calling (main) thread; returns a completed Task.
|
||||
return Task.FromResult(CompileSourceCodeOnMainThread(sourceCode, baseFileName, assemblyDir, emitPdb, documentPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile source code on main thread synchronously to avoid deadlocks.
|
||||
/// </summary>
|
||||
/// <param name="emitPdb">Emit a portable PDB and load symbols so breakpoints can bind.</param>
|
||||
/// <param name="documentPath">Source document path recorded in the PDB (the original .cs file).</param>
|
||||
public static HotReloadCompilationResult CompileSourceCodeOnMainThread(string sourceCode, string baseFileName, string assemblyDir = null, bool emitPdb = false, string documentPath = null)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
Debug.Log($"HotReload: Starting synchronous source code compilation for {baseFileName}");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sourceCode))
|
||||
{
|
||||
return HotReloadCompilationResult.Failure(
|
||||
"Empty Source Code",
|
||||
"Source code cannot be empty",
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
// Generate versioned assembly name
|
||||
var nextVersion = GetNextVersion(baseFileName);
|
||||
var assemblyName = $"{baseFileName}_{nextVersion:D3}";
|
||||
|
||||
// Ensure temp directory exists
|
||||
Directory.CreateDirectory(HotReloadTempDir);
|
||||
|
||||
// Compile using shared RoslynCompilationService
|
||||
var compilationResult = CompileHotReloadAssembly(sourceCode, assemblyName, emitPdb, documentPath);
|
||||
|
||||
if (!compilationResult.Success)
|
||||
{
|
||||
return HotReloadCompilationResult.Failure(
|
||||
"Compilation Failed",
|
||||
"Hot reload source code compilation failed",
|
||||
stopwatch.ElapsedMilliseconds,
|
||||
compilationResult.Diagnostics.Select(d => d.Message).ToList());
|
||||
}
|
||||
|
||||
// Register hot reload methods from compiled assembly
|
||||
var registeredMethods = RegisterHotReloadMethods(compilationResult.Assembly, assemblyName, out var skippedOverrides);
|
||||
|
||||
// Save assembly to disk if assemblyDir is specified
|
||||
string actualAssemblyPath = null;
|
||||
if (!string.IsNullOrWhiteSpace(assemblyDir))
|
||||
{
|
||||
Directory.CreateDirectory(assemblyDir);
|
||||
actualAssemblyPath = Path.Combine(assemblyDir, $"{assemblyName}.dll");
|
||||
File.WriteAllBytes(actualAssemblyPath, compilationResult.AssemblyBytes);
|
||||
Debug.Log($"HotReload: Assembly saved to disk: {actualAssemblyPath}");
|
||||
|
||||
// Symbols are loaded in-memory; also write the .pdb next to the .dll for convenience.
|
||||
if (compilationResult.PdbBytes != null)
|
||||
{
|
||||
var pdbPath = Path.Combine(assemblyDir, $"{assemblyName}.pdb");
|
||||
File.WriteAllBytes(pdbPath, compilationResult.PdbBytes);
|
||||
Debug.Log($"HotReload: Symbols saved to disk: {pdbPath}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
actualAssemblyPath = Path.Combine(HotReloadTempDir, $"{assemblyName}.dll"); // For compatibility (in-memory)
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
Debug.Log($"HotReload: Successfully compiled source code -> {assemblyName}.dll with {registeredMethods.Count} methods in {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
||||
var sourceCompileResult = HotReloadCompilationResult.Success(
|
||||
assemblyName,
|
||||
actualAssemblyPath,
|
||||
registeredMethods,
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
sourceCompileResult.Diagnostics = skippedOverrides;
|
||||
return sourceCompileResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
Debug.LogError($"HotReload: Synchronous source code compilation failed for {baseFileName}: {ex.Message}");
|
||||
return HotReloadCompilationResult.Failure(
|
||||
"Exception",
|
||||
ex.ToString(),
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal async source code compilation implementation for background threads.
|
||||
/// </summary>
|
||||
private static async Task<HotReloadCompilationResult> CompileSourceCodeInternalAsync(string sourceCode, string baseFileName, string assemblyDir = null)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
Debug.Log($"HotReload: Starting async source code compilation for {baseFileName}");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sourceCode))
|
||||
{
|
||||
return HotReloadCompilationResult.Failure(
|
||||
"Empty Source Code",
|
||||
"Source code cannot be empty",
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
// Generate versioned assembly name
|
||||
var nextVersion = GetNextVersion(baseFileName);
|
||||
var assemblyName = $"{baseFileName}_{nextVersion:D3}";
|
||||
|
||||
// Ensure temp directory exists
|
||||
Directory.CreateDirectory(HotReloadTempDir);
|
||||
|
||||
// Compile using shared RoslynCompilationService async
|
||||
var compilationResult = await CompileHotReloadAssemblyAsync(sourceCode, assemblyName);
|
||||
|
||||
if (!compilationResult.Success)
|
||||
{
|
||||
return HotReloadCompilationResult.Failure(
|
||||
"Compilation Failed",
|
||||
"Hot reload source code compilation failed",
|
||||
stopwatch.ElapsedMilliseconds,
|
||||
compilationResult.Diagnostics.Select(d => d.Message).ToList());
|
||||
}
|
||||
|
||||
// Register hot reload methods from compiled assembly
|
||||
var registeredMethods = RegisterHotReloadMethods(compilationResult.Assembly, assemblyName, out var skippedOverrides);
|
||||
|
||||
// Save assembly to disk if assemblyDir is specified
|
||||
string actualAssemblyPath = null;
|
||||
if (!string.IsNullOrWhiteSpace(assemblyDir))
|
||||
{
|
||||
Directory.CreateDirectory(assemblyDir);
|
||||
actualAssemblyPath = Path.Combine(assemblyDir, $"{assemblyName}.dll");
|
||||
File.WriteAllBytes(actualAssemblyPath, compilationResult.AssemblyBytes);
|
||||
Debug.Log($"HotReload: Assembly saved to disk: {actualAssemblyPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
actualAssemblyPath = Path.Combine(HotReloadTempDir, $"{assemblyName}.dll"); // For compatibility (in-memory)
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
Debug.Log($"HotReload: Successfully compiled source code -> {assemblyName}.dll with {registeredMethods.Count} methods in {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
||||
var sourceCompileResult = HotReloadCompilationResult.Success(
|
||||
assemblyName,
|
||||
actualAssemblyPath,
|
||||
registeredMethods,
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
sourceCompileResult.Diagnostics = skippedOverrides;
|
||||
return sourceCompileResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
Debug.LogError($"HotReload: Async source code compilation failed for {baseFileName}: {ex.Message}");
|
||||
return HotReloadCompilationResult.Failure(
|
||||
"Exception",
|
||||
ex.ToString(),
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clean up old hot reload DLL versions, keeping only the latest for each base filename.
|
||||
/// </summary>
|
||||
public static HotReloadCleanupResult CleanupHotReloadDlls(string assemblyDir = null)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
var deletedFiles = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
// Use specified assemblyDir or default to HotReloadTempDir
|
||||
var targetDir = !string.IsNullOrWhiteSpace(assemblyDir) ? assemblyDir : HotReloadTempDir;
|
||||
|
||||
if (!Directory.Exists(targetDir))
|
||||
{
|
||||
return new HotReloadCleanupResult
|
||||
{
|
||||
Success = true,
|
||||
DeletedFiles = deletedFiles,
|
||||
Message = $"No hot reload directory found: {targetDir}",
|
||||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
|
||||
var dllFiles = Directory.GetFiles(targetDir, "*.dll");
|
||||
var groupedFiles = dllFiles
|
||||
.Select(f => new
|
||||
{
|
||||
FilePath = f,
|
||||
FileName = Path.GetFileNameWithoutExtension(f),
|
||||
BaseFilename = ExtractBaseFilename(Path.GetFileNameWithoutExtension(f)),
|
||||
Version = ExtractVersion(Path.GetFileNameWithoutExtension(f))
|
||||
})
|
||||
.Where(f => f.Version > 0) // Only versioned hot reload DLLs
|
||||
.GroupBy(f => f.BaseFilename)
|
||||
.ToList();
|
||||
|
||||
foreach (var group in groupedFiles)
|
||||
{
|
||||
var sortedFiles = group.OrderByDescending(f => f.Version).ToList();
|
||||
|
||||
// Keep the latest version, delete older ones
|
||||
for (int i = 1; i < sortedFiles.Count; i++)
|
||||
{
|
||||
var fileToDelete = sortedFiles[i];
|
||||
File.Delete(fileToDelete.FilePath);
|
||||
deletedFiles.Add(fileToDelete.FileName + ".dll");
|
||||
Debug.Log($"HotReload: Deleted old version {fileToDelete.FileName}.dll");
|
||||
}
|
||||
}
|
||||
|
||||
// Clear registry and reset version tracker
|
||||
HotReloadRegistry.ClearAllOverrides();
|
||||
_versionTracker.Clear();
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
Debug.Log($"HotReload: Cleanup completed - deleted {deletedFiles.Count} files in {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
||||
return new HotReloadCleanupResult
|
||||
{
|
||||
Success = true,
|
||||
DeletedFiles = deletedFiles,
|
||||
Message = $"Deleted {deletedFiles.Count} old DLL versions",
|
||||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
Debug.LogError($"HotReload: Cleanup failed: {ex.Message}");
|
||||
|
||||
return new HotReloadCleanupResult
|
||||
{
|
||||
Success = false,
|
||||
DeletedFiles = deletedFiles,
|
||||
Message = $"Cleanup failed: {ex.Message}",
|
||||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the next version number for a base filename.
|
||||
/// </summary>
|
||||
private static int GetNextVersion(string baseFilename)
|
||||
{
|
||||
if (!_versionTracker.ContainsKey(baseFilename))
|
||||
{
|
||||
_versionTracker[baseFilename] = 0;
|
||||
}
|
||||
|
||||
return ++_versionTracker[baseFilename];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrap hot reload user code with proper using statements and namespace.
|
||||
/// </summary>
|
||||
|
||||
/// <summary>
|
||||
/// Compile hot reload source code using shared RoslynCompilationService.
|
||||
/// Synchronous compilation for main thread use.
|
||||
/// </summary>
|
||||
private static CompilationResult CompileHotReloadAssembly(string sourceCode, string assemblyName, bool emitPdb = false, string documentPath = null)
|
||||
{
|
||||
var request = new CompilationRequest
|
||||
{
|
||||
SourceCode = sourceCode,
|
||||
AssemblyName = assemblyName,
|
||||
// Ensure Unity.Pipeline assemblies are included (contains HotReloadOverrideMethodAttribute)
|
||||
// Also include test assemblies for testing scenarios
|
||||
AdditionalAssemblyPrefixes = new[] { "Unity.Pipeline", "Unity.Pipeline.Tests" },
|
||||
EmitDebugInformation = emitPdb,
|
||||
DocumentPath = documentPath
|
||||
};
|
||||
|
||||
return RoslynCompilationService.Compile(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile hot reload source code using shared RoslynCompilationService.
|
||||
/// Async compilation for background thread use.
|
||||
/// </summary>
|
||||
private static async Task<CompilationResult> CompileHotReloadAssemblyAsync(string sourceCode, string assemblyName)
|
||||
{
|
||||
var request = new CompilationRequest
|
||||
{
|
||||
SourceCode = sourceCode,
|
||||
AssemblyName = assemblyName,
|
||||
// Ensure Unity.Pipeline assemblies are included (contains HotReloadOverrideMethodAttribute)
|
||||
// Also include test assemblies for testing scenarios
|
||||
AdditionalAssemblyPrefixes = new[] { "Unity.Pipeline", "Unity.Pipeline.Tests" }
|
||||
};
|
||||
|
||||
return await RoslynCompilationService.CompileAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register hot reload methods from compiled assembly with the registry.
|
||||
/// Also registers any [HotReloadWithOverrides] target methods found in the assembly.
|
||||
/// Only overrides that actually bind are returned; overrides that were skipped (e.g. the
|
||||
/// target is not currently [HotReloadWithOverrides], or a signature mismatch) are reported via
|
||||
/// <paramref name="skipped"/> with a user-facing reason.
|
||||
/// </summary>
|
||||
private static List<string> RegisterHotReloadMethods(Assembly assembly, string assemblyId, out List<string> skipped)
|
||||
{
|
||||
var registeredMethods = new List<string>();
|
||||
skipped = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
Debug.Log($"HotReload: Registering methods from assembly {assemblyId} with {assembly.GetTypes().Length} types");
|
||||
|
||||
// Register assembly types for discovery
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
Debug.Log($"HotReload: Processing type {type.Name} in {type.Namespace}");
|
||||
HotReloadRegistry.RegisterHotReloadType(type, assemblyId);
|
||||
|
||||
// First, scan for [HotReloadWithOverrides] methods to register as targets
|
||||
var allMethods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
|
||||
foreach (var method in allMethods)
|
||||
{
|
||||
var reloadableAttr = method.GetCustomAttribute<HotReloadWithOverridesAttribute>();
|
||||
if (reloadableAttr != null)
|
||||
{
|
||||
Debug.Log($"HotReload: Found reloadable method {method.Name} with ID {reloadableAttr.Id}");
|
||||
HotReloadRegistry.RegisterReloadableMethod(method, reloadableAttr);
|
||||
}
|
||||
}
|
||||
|
||||
// Then, find methods with [HotReloadOverrideMethod] attribute for overrides
|
||||
var staticMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Static);
|
||||
Debug.Log($"HotReload: Found {staticMethods.Length} static methods in {type.Name}");
|
||||
|
||||
foreach (var method in staticMethods)
|
||||
{
|
||||
var customAttributes = method.GetCustomAttributes(true);
|
||||
Debug.Log($"HotReload: Method {method.Name} has {customAttributes.Length} custom attributes: {string.Join(", ", customAttributes.Select(a => a.GetType().Name))}");
|
||||
|
||||
// Try multiple ways to find the attribute
|
||||
var hotReloadAttr = method.GetCustomAttribute<HotReloadOverrideMethodAttribute>();
|
||||
if (hotReloadAttr == null)
|
||||
{
|
||||
// Try by name in case of type loading issues
|
||||
var attrByName = customAttributes.FirstOrDefault(a => a.GetType().Name == "HotReloadOverrideMethodAttribute");
|
||||
if (attrByName != null)
|
||||
{
|
||||
Debug.Log($"HotReload: Found HotReloadOverrideMethodAttribute by name on {method.Name}");
|
||||
// Cast to the attribute type
|
||||
hotReloadAttr = attrByName as HotReloadOverrideMethodAttribute;
|
||||
}
|
||||
}
|
||||
|
||||
if (hotReloadAttr != null)
|
||||
{
|
||||
Debug.Log($"HotReload: Registering method override {method.Name} -> {hotReloadAttr.TargetMethodId}");
|
||||
if (HotReloadRegistry.RegisterMethodOverride(method, hotReloadAttr, type, out var skipReason))
|
||||
{
|
||||
registeredMethods.Add(hotReloadAttr.TargetMethodId);
|
||||
}
|
||||
else
|
||||
{
|
||||
skipped.Add($"{method.Name} -> {hotReloadAttr.TargetMethodId}: {skipReason}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"HotReload: Registered {registeredMethods.Count} override methods: {string.Join(", ", registeredMethods)}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"HotReload: Error registering methods from assembly {assemblyId}: {ex.Message}");
|
||||
Debug.LogError($"HotReload: Stack trace: {ex.StackTrace}");
|
||||
}
|
||||
|
||||
return registeredMethods;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract base filename from versioned filename (e.g., "PlayerMovement_001" -> "PlayerMovement").
|
||||
/// </summary>
|
||||
private static string ExtractBaseFilename(string versionedFilename)
|
||||
{
|
||||
var lastUnderscoreIndex = versionedFilename.LastIndexOf('_');
|
||||
if (lastUnderscoreIndex > 0)
|
||||
{
|
||||
var potentialVersion = versionedFilename.Substring(lastUnderscoreIndex + 1);
|
||||
if (int.TryParse(potentialVersion, out _))
|
||||
{
|
||||
return versionedFilename.Substring(0, lastUnderscoreIndex);
|
||||
}
|
||||
}
|
||||
return versionedFilename;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract version number from versioned filename (e.g., "PlayerMovement_001" -> 1).
|
||||
/// </summary>
|
||||
private static int ExtractVersion(string versionedFilename)
|
||||
{
|
||||
var lastUnderscoreIndex = versionedFilename.LastIndexOf('_');
|
||||
if (lastUnderscoreIndex > 0)
|
||||
{
|
||||
var potentialVersion = versionedFilename.Substring(lastUnderscoreIndex + 1);
|
||||
if (int.TryParse(potentialVersion, out var version))
|
||||
{
|
||||
return version;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#else
|
||||
// Hot reload requires Roslyn compilation, which is only available in the Editor and in
|
||||
// Desktop development builds. In all other builds the methods below are compiled instead,
|
||||
// matching the public surface used by callers (HotReloadCommands, InPlaceReloadProcessor)
|
||||
// so the project still builds, and returning a clear "not supported" failure at runtime.
|
||||
const string NotSupportedMessage =
|
||||
"Hot reload compilation is only supported on Desktop development builds (Windows/Mac/Linux).";
|
||||
|
||||
/// <summary>
|
||||
/// Hot reload compilation not supported on this build.
|
||||
/// </summary>
|
||||
public static Task<HotReloadCompileResult> CompileAndApplyAsync(string filename, string assemblyDir = null)
|
||||
{
|
||||
return Task.FromResult(HotReloadCompileResult.Failure("Platform Not Supported", NotSupportedMessage));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hot reload source compilation (in-place editing) not supported on this build.
|
||||
/// </summary>
|
||||
public static HotReloadCompilationResult CompileSourceCodeOnMainThread(string sourceCode, string baseFileName, string assemblyDir = null, bool emitPdb = false, string documentPath = null)
|
||||
{
|
||||
return HotReloadCompilationResult.Failure("Platform Not Supported", NotSupportedMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hot reload source compilation (in-place editing) not supported on this build.
|
||||
/// </summary>
|
||||
public static Task<HotReloadCompilationResult> CompileSourceCodeAsync(string sourceCode, string baseFileName, string assemblyDir = null, bool emitPdb = false, string documentPath = null)
|
||||
{
|
||||
return Task.FromResult(HotReloadCompilationResult.Failure("Platform Not Supported", NotSupportedMessage));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hot reload cleanup not supported on this build.
|
||||
/// </summary>
|
||||
public static HotReloadCleanupResult CleanupHotReloadDlls(string assemblyDir = null)
|
||||
{
|
||||
return new HotReloadCleanupResult
|
||||
{
|
||||
Success = false,
|
||||
Message = NotSupportedMessage,
|
||||
ExecutionTimeMs = 0
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from hot reload compilation operation.
|
||||
/// </summary>
|
||||
public class HotReloadCompileResult
|
||||
{
|
||||
public bool IsSuccess { get; set; }
|
||||
public string AssemblyName { get; set; }
|
||||
public string OutputPath { get; set; }
|
||||
public List<string> RegisteredMethods { get; set; } = new List<string>();
|
||||
public long ExecutionTimeMs { get; set; }
|
||||
public string Error { get; set; }
|
||||
public string ErrorDetails { get; set; }
|
||||
public List<string> Diagnostics { get; set; } = new List<string>();
|
||||
|
||||
public static HotReloadCompileResult Success(string assemblyName, string outputPath, List<string> registeredMethods, long executionTimeMs)
|
||||
{
|
||||
return new HotReloadCompileResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
AssemblyName = assemblyName,
|
||||
OutputPath = outputPath,
|
||||
RegisteredMethods = registeredMethods,
|
||||
ExecutionTimeMs = executionTimeMs
|
||||
};
|
||||
}
|
||||
|
||||
public static HotReloadCompileResult Failure(string error, string errorDetails, long executionTimeMs = 0, List<string> diagnostics = null)
|
||||
{
|
||||
return new HotReloadCompileResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
Error = error,
|
||||
ErrorDetails = errorDetails,
|
||||
ExecutionTimeMs = executionTimeMs,
|
||||
Diagnostics = diagnostics ?? new List<string>()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from hot reload cleanup operation.
|
||||
/// </summary>
|
||||
public class HotReloadCleanupResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public List<string> DeletedFiles { get; set; } = new List<string>();
|
||||
public string Message { get; set; }
|
||||
public long ExecutionTimeMs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from hot reload source code compilation operation (for in-place editing).
|
||||
/// </summary>
|
||||
public class HotReloadCompilationResult
|
||||
{
|
||||
public bool IsSuccess { get; set; }
|
||||
public string AssemblyName { get; set; }
|
||||
public string OutputPath { get; set; }
|
||||
public List<string> RegisteredMethods { get; set; } = new List<string>();
|
||||
public long ExecutionTimeMs { get; set; }
|
||||
public string Error { get; set; }
|
||||
public string ErrorDetails { get; set; }
|
||||
public List<string> Diagnostics { get; set; } = new List<string>();
|
||||
|
||||
public static HotReloadCompilationResult Success(string assemblyName, string outputPath, List<string> registeredMethods, long executionTimeMs)
|
||||
{
|
||||
return new HotReloadCompilationResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
AssemblyName = assemblyName,
|
||||
OutputPath = outputPath,
|
||||
RegisteredMethods = registeredMethods,
|
||||
ExecutionTimeMs = executionTimeMs
|
||||
};
|
||||
}
|
||||
|
||||
public static HotReloadCompilationResult Failure(string error, string errorDetails, long executionTimeMs = 0, List<string> diagnostics = null)
|
||||
{
|
||||
return new HotReloadCompilationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
Error = error,
|
||||
ErrorDetails = errorDetails,
|
||||
ExecutionTimeMs = executionTimeMs,
|
||||
Diagnostics = diagnostics ?? new List<string>()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 29ede4b57fcc6c64b84e5f6ee75df373
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Emit;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Unity.Pipeline.Models;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Compilation
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared Roslyn compilation service for both code evaluation and hot reload systems.
|
||||
/// Eliminates code duplication by providing common compilation infrastructure.
|
||||
/// Thread-safe and can be called from any thread.
|
||||
/// </summary>
|
||||
public static class RoslynCompilationService
|
||||
{
|
||||
#if UNITY_EDITOR || (UNITY_STANDALONE && DEBUG)
|
||||
|
||||
/// <summary>
|
||||
/// Compile source code to assembly with customizable options.
|
||||
/// Thread-safe compilation that can run on any thread.
|
||||
/// </summary>
|
||||
public static CompilationResult Compile(CompilationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse syntax tree. When emitting debug info, the tree needs a document path and
|
||||
// UTF-8 encoding so the portable PDB can reference a source document.
|
||||
var syntaxTree = request.EmitDebugInformation
|
||||
? CSharpSyntaxTree.ParseText(
|
||||
SourceText.From(request.SourceCode, Encoding.UTF8),
|
||||
path: request.DocumentPath ?? request.AssemblyName + ".cs")
|
||||
: CSharpSyntaxTree.ParseText(request.SourceCode);
|
||||
|
||||
// Get metadata references with optional additional prefixes
|
||||
var references = GetMetadataReferences(request.AdditionalAssemblyPrefixes);
|
||||
|
||||
// Create compilation. Disable optimizations when emitting debug info so the JIT
|
||||
// keeps full sequence points (otherwise breakpoints can't bind).
|
||||
var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
|
||||
if (request.EmitDebugInformation)
|
||||
options = options.WithOptimizationLevel(OptimizationLevel.Debug);
|
||||
|
||||
var compilation = CSharpCompilation.Create(
|
||||
request.AssemblyName,
|
||||
new[] { syntaxTree },
|
||||
references,
|
||||
options);
|
||||
|
||||
// Get diagnostics
|
||||
var diagnostics = compilation.GetDiagnostics();
|
||||
var diagnosticInfos = ConvertDiagnostics(diagnostics, request.LineNumberOffset);
|
||||
|
||||
// Check for compilation errors
|
||||
var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList();
|
||||
if (errors.Any())
|
||||
{
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
Diagnostics = diagnosticInfos
|
||||
};
|
||||
}
|
||||
|
||||
// Compile to memory
|
||||
using (var peStream = new MemoryStream())
|
||||
{
|
||||
byte[] pdbBytes = null;
|
||||
EmitResult emitResult;
|
||||
|
||||
if (request.EmitDebugInformation)
|
||||
{
|
||||
using (var pdbStream = new MemoryStream())
|
||||
{
|
||||
emitResult = compilation.Emit(
|
||||
peStream,
|
||||
pdbStream,
|
||||
options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb));
|
||||
|
||||
if (emitResult.Success)
|
||||
pdbBytes = pdbStream.ToArray();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
emitResult = compilation.Emit(peStream);
|
||||
}
|
||||
|
||||
if (!emitResult.Success)
|
||||
{
|
||||
var emitDiagnostics = ConvertDiagnostics(emitResult.Diagnostics, request.LineNumberOffset);
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
Diagnostics = diagnosticInfos.Concat(emitDiagnostics).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
// Load assembly from memory. When symbols are present, load them alongside the
|
||||
// assembly so an attached debugger can bind breakpoints into the emitted code.
|
||||
var assemblyBytes = peStream.ToArray();
|
||||
var assembly = pdbBytes != null
|
||||
? PipelineUtils.LoadFromBytes(assemblyBytes, pdbBytes)
|
||||
: PipelineUtils.LoadFromBytes(assemblyBytes);
|
||||
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = true,
|
||||
Assembly = assembly,
|
||||
AssemblyBytes = assemblyBytes,
|
||||
PdbBytes = pdbBytes,
|
||||
Diagnostics = diagnosticInfos
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
Diagnostics = new List<DiagnosticInfo>
|
||||
{
|
||||
new DiagnosticInfo
|
||||
{
|
||||
Severity = "error",
|
||||
Message = $"Compilation exception: {ex.Message}",
|
||||
Line = 0,
|
||||
Column = 0,
|
||||
Id = "ROSLYN001"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async wrapper for compilation that runs on background thread.
|
||||
/// Use when you need to avoid blocking the calling thread.
|
||||
/// </summary>
|
||||
public static Task<CompilationResult> CompileAsync(CompilationRequest request)
|
||||
{
|
||||
return Task.Run(() => Compile(request));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get metadata references for compilation with optional additional assembly prefixes.
|
||||
/// Handles both Editor and Runtime scenarios with appropriate filtering.
|
||||
/// </summary>
|
||||
public static List<MetadataReference> GetMetadataReferences(string[] additionalPrefixes = null)
|
||||
{
|
||||
var references = new List<MetadataReference>();
|
||||
var assemblies = PipelineUtils.GetLoadedAssemblies()
|
||||
.Where(a => !a.IsDynamic && !string.IsNullOrEmpty(PipelineUtils.GetLoadedAssemblyPath(a)));
|
||||
|
||||
if (Application.isEditor)
|
||||
{
|
||||
// Editor: Include all loaded assemblies for maximum compatibility
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
try
|
||||
{
|
||||
references.Add(MetadataReference.CreateFromFile(PipelineUtils.GetLoadedAssemblyPath(assembly)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip problematic assemblies
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Runtime: Use curated filtering for Unity + user assemblies
|
||||
var allowedPrefixes = new List<string>
|
||||
{
|
||||
"UnityEngine",
|
||||
"Assembly-CSharp",
|
||||
"netstandard",
|
||||
"mscorlib",
|
||||
"System.",
|
||||
"Unity.Pipeline"
|
||||
};
|
||||
|
||||
// Add any additional prefixes (e.g., for hot reload: "Microsoft.CodeAnalysis")
|
||||
if (additionalPrefixes != null)
|
||||
{
|
||||
allowedPrefixes.AddRange(additionalPrefixes);
|
||||
}
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assemblyName = assembly.GetName().Name;
|
||||
if (allowedPrefixes.Any(prefix => assemblyName.StartsWith(prefix)))
|
||||
{
|
||||
references.Add(MetadataReference.CreateFromFile(PipelineUtils.GetLoadedAssemblyPath(assembly)));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip problematic assemblies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert Roslyn diagnostics to DiagnosticInfo list with optional line number adjustment.
|
||||
/// Handles line number offset for wrapped code scenarios.
|
||||
/// </summary>
|
||||
private static List<DiagnosticInfo> ConvertDiagnostics(IEnumerable<Diagnostic> diagnostics, int lineOffset = 0)
|
||||
{
|
||||
var result = new List<DiagnosticInfo>();
|
||||
|
||||
foreach (var diagnostic in diagnostics.Where(d => d.Severity >= DiagnosticSeverity.Warning))
|
||||
{
|
||||
var location = diagnostic.Location.GetLineSpan();
|
||||
var line = Math.Max(0, location.StartLinePosition.Line - lineOffset);
|
||||
var column = location.StartLinePosition.Character;
|
||||
|
||||
result.Add(new DiagnosticInfo
|
||||
{
|
||||
Severity = diagnostic.Severity.ToString().ToLower(),
|
||||
Message = diagnostic.GetMessage(),
|
||||
Line = line,
|
||||
Column = column,
|
||||
Id = diagnostic.Id
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
/// <summary>
|
||||
/// Runtime compilation not supported on this platform.
|
||||
/// Desktop development builds only (Windows/Mac/Linux).
|
||||
/// </summary>
|
||||
public static CompilationResult Compile(CompilationRequest request)
|
||||
{
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
Diagnostics = new List<DiagnosticInfo>
|
||||
{
|
||||
new DiagnosticInfo
|
||||
{
|
||||
Severity = "error",
|
||||
Message = "Runtime code compilation only supported on Desktop development builds",
|
||||
Line = 0,
|
||||
Column = 0,
|
||||
Id = "PLATFORM001"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime compilation not supported on this platform.
|
||||
/// </summary>
|
||||
public static Task<CompilationResult> CompileAsync(CompilationRequest request)
|
||||
{
|
||||
return Task.FromResult(Compile(request));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime compilation not supported on this platform.
|
||||
/// </summary>
|
||||
public static List<MetadataReference> GetMetadataReferences(string[] additionalPrefixes = null)
|
||||
{
|
||||
return new List<MetadataReference>();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request for Roslyn compilation with customizable options.
|
||||
/// </summary>
|
||||
public class CompilationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Source code to compile.
|
||||
/// </summary>
|
||||
public string SourceCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name for the generated assembly (should be unique).
|
||||
/// </summary>
|
||||
public string AssemblyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional assembly prefixes to include in metadata references.
|
||||
/// Useful for hot reload scenarios that need "Microsoft.CodeAnalysis" etc.
|
||||
/// </summary>
|
||||
public string[] AdditionalAssemblyPrefixes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Line number offset to subtract from diagnostic line numbers.
|
||||
/// Used when source code has wrapper code that should be hidden from diagnostics.
|
||||
/// </summary>
|
||||
public int LineNumberOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, emit a portable PDB and load the assembly with its symbols so an attached
|
||||
/// managed debugger can bind breakpoints. Compiles unoptimized. Default false (no symbols).
|
||||
/// </summary>
|
||||
public bool EmitDebugInformation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Document path recorded in the syntax tree when <see cref="EmitDebugInformation"/> is set.
|
||||
/// Source that is not covered by explicit <c>#line</c> directives maps to this path.
|
||||
/// </summary>
|
||||
public string DocumentPath { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from Roslyn compilation.
|
||||
/// </summary>
|
||||
public class CompilationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether compilation was successful.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compiled assembly (only available if Success = true).
|
||||
/// </summary>
|
||||
public Assembly Assembly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw assembly bytes (for potential disk saving or caching).
|
||||
/// </summary>
|
||||
public byte[] AssemblyBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Portable PDB bytes when <see cref="CompilationRequest.EmitDebugInformation"/> was set;
|
||||
/// null otherwise.
|
||||
/// </summary>
|
||||
public byte[] PdbBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compilation diagnostics (errors, warnings, info).
|
||||
/// </summary>
|
||||
public List<DiagnosticInfo> Diagnostics { get; set; } = new List<DiagnosticInfo>();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 957feb25e68236e45b15cd5e9f095c31
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user