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 { /// /// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs /// Adjusted for our UI-Framework /// 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 { 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() .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")); } } 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 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 Resources { get; } } }