mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
390 lines
16 KiB
C#
390 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Microsoft.Build.Framework;
|
|
using Mono.Cecil;
|
|
using Mono.Cecil.Cil;
|
|
using Mono.Cecil.Rocks;
|
|
using Pidgin;
|
|
using XamlX;
|
|
using XamlX.Ast;
|
|
using XamlX.Emit;
|
|
using XamlX.IL;
|
|
using XamlX.Parsers;
|
|
using XamlX.Transform;
|
|
using XamlX.TypeSystem;
|
|
|
|
namespace Robust.Build.Tasks
|
|
{
|
|
/// <summary>
|
|
/// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
|
|
/// Adjusted for our UI-Framework
|
|
/// </summary>
|
|
public partial class XamlCompiler
|
|
{
|
|
public static (bool success, bool writtentofile) Compile(IBuildEngine engine, string input, string[] references,
|
|
string projectDirectory, string output, string strongNameKey)
|
|
{
|
|
var typeSystem = new CecilTypeSystem(references
|
|
.Where(r => !r.ToLowerInvariant().EndsWith("robust.build.tasks.dll"))
|
|
.Concat(new[] { input }), input);
|
|
|
|
var asm = typeSystem.TargetAssemblyDefinition;
|
|
|
|
if (asm.MainModule.GetType("CompiledRobustXaml", "XamlIlContext") != null)
|
|
{
|
|
// If this type exists, the assembly has already been processed by us.
|
|
// Do not run again, it would corrupt the file.
|
|
// This *shouldn't* be possible due to Inputs/Outputs dependencies in the build system,
|
|
// but better safe than sorry eh?
|
|
engine.LogWarningEvent(new BuildWarningEventArgs("XAMLIL", "", "", 0, 0, 0, 0, "Ran twice on same assembly file; ignoring.", "", ""));
|
|
return (true, false);
|
|
}
|
|
|
|
var compileRes = CompileCore(engine, typeSystem);
|
|
if (compileRes == null)
|
|
return (true, false);
|
|
if (compileRes == false)
|
|
return (false, false);
|
|
|
|
var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols };
|
|
if (!string.IsNullOrWhiteSpace(strongNameKey))
|
|
writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey);
|
|
|
|
asm.Write(output, writerParameters);
|
|
|
|
return (true, true);
|
|
|
|
}
|
|
|
|
static bool? CompileCore(IBuildEngine engine, CecilTypeSystem typeSystem)
|
|
{
|
|
var asm = typeSystem.TargetAssemblyDefinition;
|
|
var embrsc = new EmbeddedResources(asm);
|
|
|
|
if (embrsc.Resources.Count(CheckXamlName) == 0)
|
|
// Nothing to do
|
|
return null;
|
|
|
|
var xamlLanguage = new XamlLanguageTypeMappings(typeSystem)
|
|
{
|
|
XmlnsAttributes =
|
|
{
|
|
typeSystem.GetType("Avalonia.Metadata.XmlnsDefinitionAttribute"),
|
|
|
|
},
|
|
ContentAttributes =
|
|
{
|
|
typeSystem.GetType("Avalonia.Metadata.ContentAttribute")
|
|
},
|
|
UsableDuringInitializationAttributes =
|
|
{
|
|
typeSystem.GetType("Robust.Client.UserInterface.XAML.UsableDuringInitializationAttribute")
|
|
},
|
|
DeferredContentPropertyAttributes =
|
|
{
|
|
typeSystem.GetType("Robust.Client.UserInterface.XAML.DeferredContentAttribute")
|
|
},
|
|
RootObjectProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestRootObjectProvider"),
|
|
UriContextProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestUriContext"),
|
|
ProvideValueTarget = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestProvideValueTarget"),
|
|
};
|
|
var emitConfig = new XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult>
|
|
{
|
|
ContextTypeBuilderCallback = (b,c) => EmitNameScopeField(xamlLanguage, typeSystem, b, c)
|
|
};
|
|
|
|
var transformerconfig = new TransformerConfiguration(
|
|
typeSystem,
|
|
typeSystem.TargetAssembly,
|
|
xamlLanguage,
|
|
XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage), CustomValueConverter);
|
|
|
|
var contextDef = new TypeDefinition("CompiledRobustXaml", "XamlIlContext",
|
|
TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
|
|
asm.MainModule.Types.Add(contextDef);
|
|
var contextClass = XamlILContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem,
|
|
xamlLanguage, emitConfig);
|
|
|
|
var compiler =
|
|
new RobustXamlILCompiler(transformerconfig, emitConfig, true);
|
|
|
|
bool CompileGroup(IResourceGroup group)
|
|
{
|
|
var typeDef = new TypeDefinition("CompiledRobustXaml", "!" + group.Name, TypeAttributes.Class,
|
|
asm.MainModule.TypeSystem.Object);
|
|
|
|
//typeDef.CustomAttributes.Add(new CustomAttribute(ed));
|
|
asm.MainModule.Types.Add(typeDef);
|
|
var builder = typeSystem.CreateTypeBuilder(typeDef);
|
|
|
|
foreach (var res in group.Resources.Where(CheckXamlName))
|
|
{
|
|
try
|
|
{
|
|
engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", MessageImportance.Low);
|
|
|
|
var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
|
|
var parsed = XDocumentXamlParser.Parse(xaml);
|
|
|
|
var initialRoot = (XamlAstObjectNode) parsed.Root;
|
|
|
|
var classDirective = initialRoot.Children.OfType<XamlAstXmlDirective>()
|
|
.FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Class");
|
|
string classname;
|
|
if (classDirective != null && classDirective.Values[0] is XamlAstTextNode tn)
|
|
{
|
|
classname = tn.Text;
|
|
}
|
|
else
|
|
{
|
|
classname = res.Name.Replace(".xaml","");
|
|
}
|
|
|
|
var classType = typeSystem.TargetAssembly.FindType(classname);
|
|
if (classType == null)
|
|
throw new Exception($"Unable to find type '{classname}'");
|
|
|
|
compiler.Transform(parsed);
|
|
|
|
var populateName = $"Populate:{res.Name}";
|
|
var buildName = $"Build:{res.Name}";
|
|
|
|
var classTypeDefinition = typeSystem.GetTypeReference(classType).Resolve();
|
|
|
|
var populateBuilder = typeSystem.CreateTypeBuilder(classTypeDefinition);
|
|
|
|
compiler.Compile(parsed, contextClass,
|
|
compiler.DefinePopulateMethod(populateBuilder, parsed, populateName,
|
|
classTypeDefinition == null),
|
|
compiler.DefineBuildMethod(builder, parsed, buildName, true),
|
|
null,
|
|
(closureName, closureBaseType) =>
|
|
populateBuilder.DefineSubType(closureBaseType, closureName, false),
|
|
res.Uri, res
|
|
);
|
|
|
|
//add compiled populate method
|
|
var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve().Methods
|
|
.First(m => m.Name == populateName);
|
|
|
|
const string TrampolineName = "!XamlIlPopulateTrampoline";
|
|
var trampoline = new MethodDefinition(TrampolineName,
|
|
MethodAttributes.Static | MethodAttributes.Private, asm.MainModule.TypeSystem.Void);
|
|
trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition));
|
|
classTypeDefinition.Methods.Add(trampoline);
|
|
|
|
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldnull));
|
|
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
|
|
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Call, compiledPopulateMethod));
|
|
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
|
|
|
|
var foundXamlLoader = false;
|
|
// Find RobustXamlLoader.Load(this) and replace it with !XamlIlPopulateTrampoline(this)
|
|
foreach (var method in classTypeDefinition.Methods
|
|
.Where(m => !m.Attributes.HasFlag(MethodAttributes.Static)))
|
|
{
|
|
var i = method.Body.Instructions;
|
|
for (var c = 1; c < i.Count; c++)
|
|
{
|
|
if (i[c].OpCode == OpCodes.Call)
|
|
{
|
|
var op = i[c].Operand as MethodReference;
|
|
|
|
if (op != null
|
|
&& op.Name == TrampolineName)
|
|
{
|
|
foundXamlLoader = true;
|
|
break;
|
|
}
|
|
|
|
if (op != null
|
|
&& op.Name == "Load"
|
|
&& op.Parameters.Count == 1
|
|
&& op.Parameters[0].ParameterType.FullName == "System.Object"
|
|
&& op.DeclaringType.FullName == "Robust.Client.UserInterface.XAML.RobustXamlLoader")
|
|
{
|
|
if (MatchThisCall(i, c - 1))
|
|
{
|
|
i[c].Operand = trampoline;
|
|
foundXamlLoader = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!foundXamlLoader)
|
|
{
|
|
var ctors = classTypeDefinition.GetConstructors()
|
|
.Where(c => !c.IsStatic).ToList();
|
|
// We can inject xaml loader into default constructor
|
|
if (ctors.Count == 1 && ctors[0].Body.Instructions.Count(o=>o.OpCode != OpCodes.Nop) == 3)
|
|
{
|
|
var i = ctors[0].Body.Instructions;
|
|
var retIdx = i.IndexOf(i.Last(x => x.OpCode == OpCodes.Ret));
|
|
i.Insert(retIdx, Instruction.Create(OpCodes.Call, trampoline));
|
|
i.Insert(retIdx, Instruction.Create(OpCodes.Ldarg_0));
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidProgramException(
|
|
$"No call to RobustXamlLoader.Load(this) call found anywhere in the type {classType.FullName} and type seems to have custom constructors.");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
engine.LogErrorEvent(new BuildErrorEventArgs("XAMLIL", "", res.FilePath, 0, 0, 0, 0,
|
|
$"{res.FilePath}: {e.Message}", "", "CompileRobustXaml"));
|
|
}
|
|
res.Remove();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (embrsc.Resources.Count(CheckXamlName) != 0)
|
|
{
|
|
if (!CompileGroup(embrsc))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool CustomValueConverter(
|
|
AstTransformationContext context,
|
|
IXamlAstValueNode node,
|
|
IXamlType type,
|
|
out IXamlAstValueNode result)
|
|
{
|
|
if (!(node is XamlAstTextNode textNode))
|
|
{
|
|
result = null;
|
|
return false;
|
|
}
|
|
|
|
var text = textNode.Text;
|
|
var types = context.GetRobustTypes();
|
|
|
|
if (type.Equals(types.Vector2))
|
|
{
|
|
var foo = MathParsing.Single2.Parse(text);
|
|
|
|
if (!foo.Success)
|
|
throw new XamlLoadException($"Unable to parse \"{text}\" as a Vector2", node);
|
|
|
|
var (x, y) = foo.Value;
|
|
|
|
result = new RXamlSingleVecLikeConstAstNode(
|
|
node,
|
|
types.Vector2, types.Vector2ConstructorFull,
|
|
types.Single, new[] {x, y});
|
|
return true;
|
|
}
|
|
|
|
if (type.Equals(types.Thickness))
|
|
{
|
|
var foo = MathParsing.Thickness.Parse(text);
|
|
|
|
if (!foo.Success)
|
|
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
|
|
|
|
var val = foo.Value;
|
|
float[] full;
|
|
if (val.Length == 1)
|
|
{
|
|
var u = val[0];
|
|
full = new[] {u, u, u, u};
|
|
}
|
|
else if (val.Length == 2)
|
|
{
|
|
var h = val[0];
|
|
var v = val[1];
|
|
full = new[] {h, v, h, v};
|
|
}
|
|
else // 4
|
|
{
|
|
full = val;
|
|
}
|
|
|
|
result = new RXamlSingleVecLikeConstAstNode(
|
|
node,
|
|
types.Thickness, types.ThicknessConstructorFull,
|
|
types.Single, full);
|
|
return true;
|
|
}
|
|
|
|
if (type.Equals(types.Thickness))
|
|
{
|
|
var foo = MathParsing.Thickness.Parse(text);
|
|
|
|
if (!foo.Success)
|
|
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
|
|
|
|
var val = foo.Value;
|
|
float[] full;
|
|
if (val.Length == 1)
|
|
{
|
|
var u = val[0];
|
|
full = new[] {u, u, u, u};
|
|
}
|
|
else if (val.Length == 2)
|
|
{
|
|
var h = val[0];
|
|
var v = val[1];
|
|
full = new[] {h, v, h, v};
|
|
}
|
|
else // 4
|
|
{
|
|
full = val;
|
|
}
|
|
|
|
result = new RXamlSingleVecLikeConstAstNode(
|
|
node,
|
|
types.Thickness, types.ThicknessConstructorFull,
|
|
types.Single, full);
|
|
return true;
|
|
}
|
|
|
|
if (type.Equals(types.Color))
|
|
{
|
|
// TODO: Interpret these colors at XAML compile time instead of at runtime.
|
|
result = new RXamlColorAstNode(node, types, text);
|
|
return true;
|
|
}
|
|
|
|
result = null;
|
|
return false;
|
|
}
|
|
|
|
public const string ContextNameScopeFieldName = "RobustNameScope";
|
|
|
|
private static void EmitNameScopeField(XamlLanguageTypeMappings xamlLanguage, CecilTypeSystem typeSystem, IXamlTypeBuilder<IXamlILEmitter> typeBuilder, IXamlILEmitter constructor)
|
|
{
|
|
var nameScopeType = typeSystem.FindType("Robust.Client.UserInterface.XAML.NameScope");
|
|
var field = typeBuilder.DefineField(nameScopeType,
|
|
ContextNameScopeFieldName, true, false);
|
|
constructor
|
|
.Ldarg_0()
|
|
.Newobj(nameScopeType.GetConstructor())
|
|
.Stfld(field);
|
|
}
|
|
}
|
|
|
|
interface IResource : IFileSource
|
|
{
|
|
string Uri { get; }
|
|
string Name { get; }
|
|
void Remove();
|
|
|
|
}
|
|
|
|
interface IResourceGroup
|
|
{
|
|
string Name { get; }
|
|
IEnumerable<IResource> Resources { get; }
|
|
}
|
|
}
|