Files
BQP/srcs/RobotNet10/Commons/RobotNet10.ScriptEngine/Helpers/ScriptBuilder.cs
2026-07-13 09:25:40 +07:00

139 lines
6.5 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Scripting;
using RobotNet10.ScriptEngine;
using RobotNet10.ScriptEngine.Models;
using RobotNet10.ScriptEngine.Shared;
using System.Collections.Immutable;
using System.Text;
namespace RobotNet10.ScriptEngine.Helpers;
public class ScriptBuilder
{
public readonly string DllPath = "dlls";
public ScriptOptions ScriptOptions { get; private set; } = ScriptOptions.Default;
public ImmutableArray<MetadataReference> MetadataReferences { get; private set; } = [];
public readonly string UsingNamespacesScript = "";
public string VerifyGlobalsScript = "";
public string RuntimeGlobalsScript = "";
public ScriptBuilder(IScriptEngineResource scriptResource, string dllPath)
{
if (!string.IsNullOrEmpty(dllPath))
{
DllPath = dllPath;
}
UsingNamespacesScript = string.Join(Environment.NewLine, scriptResource.UsingNamespaces.Select(ns => $"using {ns};"));
var sb = new StringBuilder();
sb.AppendLine($"private Dictionary<string, object?> {nameof(ScriptGlobals.RobotNet)} = [];");
sb.AppendLine($"private Dictionary<string, object?> {nameof(ScriptGlobals.AppApis)} = [];");
sb.AppendLine($"private Dictionary<string, object?> {nameof(ScriptGlobals.GlobalVariables)} = [];");
sb.AppendLine($"private Dictionary<string, object?> {nameof(ScriptGlobals.MissionParameters)} = [];");
VerifyGlobalsScript = sb.ToString();
RuntimeGlobalsScript = ScriptHelper.BuildGlobalsScript(scriptResource.AppGlobalType);
List<MetadataReference> metadataRefs = [];
var currentDirectory = Directory.GetCurrentDirectory();
if (Directory.Exists(DllPath))
{
foreach (var dll in Directory.GetFiles(DllPath, "*.dll"))
{
metadataRefs.Add(MetadataReference.CreateFromFile(Path.Combine(currentDirectory, dll), properties: MetadataReferenceProperties.Assembly));
}
}
MetadataReferences = [.. metadataRefs];
var options = ScriptOptions.Default;
options.MetadataReferences.Clear();
ScriptOptions = options.AddReferences(MetadataReferences).AddImports(scriptResource.UsingNamespaces).WithEmitDebugInformation(false);
}
public void Build(string code, out IEnumerable<ScriptVariableModel> variables, out IEnumerable<ScriptTaskModel> tasks, out IEnumerable<ScriptMissionModel> missions)
{
try
{
var listVariables = new List<ScriptVariableModel>();
var wrappedCode = string.Join(Environment.NewLine, [UsingNamespacesScript, "public class DummyClass", "{", VerifyGlobalsScript, RuntimeGlobalsScript, code, "}"]);
var devCompilation = CSharpCompilation.Create("CodeAnalysis")
.WithReferences(MetadataReferences)
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddSyntaxTrees(CSharpSyntaxTree.ParseText(wrappedCode));
var diagnostics = devCompilation.GetDiagnostics();
if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error))
{
var message = new StringBuilder("Script compilation errors:\r\n");
foreach (var diag in diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error))
{
var location = diag.Location;
var lineSpan = location.GetLineSpan();
message.AppendLine($"\t❌ Error: {diag.GetMessage()}");
message.AppendLine($"\t Location: Line {lineSpan.StartLinePosition.Line + 1}, Column {lineSpan.StartLinePosition.Character + 1}");
if (location.SourceTree != null && lineSpan.IsValid)
{
var line = location.SourceTree.GetText().Lines[lineSpan.StartLinePosition.Line];
message.AppendLine($"\t Code: {line.ToString().Trim()}");
}
}
throw new ScriptCompilationException(message.ToString().TrimEnd());
}
wrappedCode = string.Join(Environment.NewLine, [UsingNamespacesScript, "public class DummyClass", "{", code, "}"]);
var syntaxTree = CSharpSyntaxTree.ParseText(wrappedCode);
var root = syntaxTree.GetCompilationUnitRoot();
var runCompilation = CSharpCompilation.Create("CodeAnalysis")
.AddSyntaxTrees(syntaxTree)
.WithReferences(MetadataReferences)
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var classNode = root.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault(c => c.Identifier.Text.Equals("DummyClass"));
if (classNode == null)
{
throw new ScriptCompilationException("No class named 'DummyClass' found in the script. This is an internal error.");
}
var semanticModel = runCompilation.GetSemanticModel(syntaxTree);
try
{
variables = ScriptHelper.ExportScriptVariables(classNode, semanticModel, ScriptOptions);
}
catch (Exception ex)
{
throw new ScriptCompilationException($"Failed to export script variables: {ex.Message}", ex);
}
try
{
ScriptHelper.ExportScriptTasksAndMissions(classNode, semanticModel, RuntimeGlobalsScript, ScriptOptions, out var scriptTasks, out var scriptMissions);
tasks = scriptTasks;
missions = scriptMissions;
}
catch (Exception ex)
{
throw new ScriptCompilationException($"Failed to export script tasks and missions: {ex.Message}", ex);
}
}
catch (ScriptCompilationException)
{
throw;
}
catch (Exception ex)
{
throw new ScriptCompilationException($"Unexpected error during script compilation: {ex.Message}", ex);
}
finally
{
// Only force GC if we're dealing with large scripts or many compilations
// In normal cases, let GC handle it naturally
// GC.Collect();
}
}
}