15 changed files with 406 additions and 15 deletions
@ -0,0 +1,48 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using Microsoft.Build.Framework; |
||||
|
|
||||
|
namespace Avalonia.Build.Tasks |
||||
|
{ |
||||
|
public class CompileAvaloniaXamlTask: ITask |
||||
|
{ |
||||
|
public bool Execute() |
||||
|
{ |
||||
|
OutputPath = OutputPath ?? AssemblyFile; |
||||
|
var input = AssemblyFile; |
||||
|
// Make a copy and delete the original file to prevent MSBuild from thinking that everything is OK
|
||||
|
if (OriginalCopyPath != null) |
||||
|
{ |
||||
|
File.Copy(AssemblyFile, OriginalCopyPath, true); |
||||
|
input = OriginalCopyPath; |
||||
|
File.Delete(AssemblyFile); |
||||
|
} |
||||
|
|
||||
|
var data = XamlCompilerTaskExecutor.Compile(BuildEngine, input, |
||||
|
File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray()); |
||||
|
if(data == null) |
||||
|
File.Copy(input, OutputPath); |
||||
|
else |
||||
|
File.WriteAllBytes(OutputPath, data); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
[Required] |
||||
|
public string AssemblyFile { get; set; } |
||||
|
[Required] |
||||
|
public string ReferencesFilePath { get; set; } |
||||
|
[Required] |
||||
|
public string OriginalCopyPath { get; set; } |
||||
|
|
||||
|
public string OutputPath { get; set; } |
||||
|
|
||||
|
public IBuildEngine BuildEngine { get; set; } |
||||
|
public ITaskHost HostObject { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using Microsoft.Build.Framework; |
||||
|
|
||||
|
namespace Avalonia.Build.Tasks |
||||
|
{ |
||||
|
public class Program |
||||
|
{ |
||||
|
static int Main(string[] args) |
||||
|
{ |
||||
|
if (args.Length != 3) |
||||
|
{ |
||||
|
Console.Error.WriteLine("input references output"); |
||||
|
return 1; |
||||
|
} |
||||
|
|
||||
|
return new CompileAvaloniaXamlTask() |
||||
|
{ |
||||
|
AssemblyFile = args[0], |
||||
|
ReferencesFilePath = args[1], |
||||
|
OutputPath = args[2], |
||||
|
BuildEngine = new ConsoleBuildEngine() |
||||
|
}.Execute() ? |
||||
|
0 : |
||||
|
2; |
||||
|
} |
||||
|
|
||||
|
class ConsoleBuildEngine : IBuildEngine |
||||
|
{ |
||||
|
public void LogErrorEvent(BuildErrorEventArgs e) |
||||
|
{ |
||||
|
Console.WriteLine($"ERROR: {e.Code} {e.Message} in {e.File} {e.LineNumber}:{e.ColumnNumber}-{e.EndLineNumber}:{e.EndColumnNumber}"); |
||||
|
} |
||||
|
|
||||
|
public void LogWarningEvent(BuildWarningEventArgs e) |
||||
|
{ |
||||
|
Console.WriteLine($"WARNING: {e.Code} {e.Message} in {e.File} {e.LineNumber}:{e.ColumnNumber}-{e.EndLineNumber}:{e.EndColumnNumber}"); |
||||
|
} |
||||
|
|
||||
|
public void LogMessageEvent(BuildMessageEventArgs e) |
||||
|
{ |
||||
|
Console.WriteLine($"MESSAGE: {e.Code} {e.Message} in {e.File} {e.LineNumber}:{e.ColumnNumber}-{e.EndLineNumber}:{e.EndColumnNumber}"); |
||||
|
} |
||||
|
|
||||
|
public void LogCustomEvent(CustomBuildEventArgs e) |
||||
|
{ |
||||
|
Console.WriteLine($"CUSTOM: {e.Message}"); |
||||
|
} |
||||
|
|
||||
|
public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, |
||||
|
IDictionary targetOutputs) => throw new NotSupportedException(); |
||||
|
|
||||
|
public bool ContinueOnError { get; } |
||||
|
public int LineNumberOfTaskNode { get; } |
||||
|
public int ColumnNumberOfTaskNode { get; } |
||||
|
public string ProjectFileOfTaskNode { get; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,73 @@ |
|||||
|
namespace System |
||||
|
{ |
||||
|
// This is a hack to enable our span code to work inside MSBuild task without referencing System.Memory
|
||||
|
struct ReadOnlySpan<T> |
||||
|
{ |
||||
|
private string _s; |
||||
|
private int _start; |
||||
|
private int _length; |
||||
|
public int Length => _length; |
||||
|
|
||||
|
public ReadOnlySpan(string s) : this(s, 0, s.Length) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
public ReadOnlySpan(string s, int start, int len) |
||||
|
{ |
||||
|
_s = s; |
||||
|
_length = len; |
||||
|
_start = start; |
||||
|
if (_start > s.Length) |
||||
|
_length = 0; |
||||
|
else if (_start + _length > s.Length) |
||||
|
_length = s.Length - _start; |
||||
|
} |
||||
|
|
||||
|
public char this[int c] => _s[_start + c]; |
||||
|
|
||||
|
public bool IsEmpty => _length == 0; |
||||
|
|
||||
|
public ReadOnlySpan<char> Slice(int start, int len) |
||||
|
{ |
||||
|
return new ReadOnlySpan<char>(_s, _start + start, len); |
||||
|
} |
||||
|
|
||||
|
public static ReadOnlySpan<char> Empty => default; |
||||
|
|
||||
|
public ReadOnlySpan<char> Slice(int start) |
||||
|
{ |
||||
|
return new ReadOnlySpan<char>(_s, _start + start, _length - start); |
||||
|
} |
||||
|
|
||||
|
public bool SequenceEqual(ReadOnlySpan<char> other) |
||||
|
{ |
||||
|
if (_length != other.Length) |
||||
|
return false; |
||||
|
for(var c=0; c<_length;c++) |
||||
|
if (this[c] != other[c]) |
||||
|
return false; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public ReadOnlySpan<char> TrimStart() |
||||
|
{ |
||||
|
int start = 0; |
||||
|
for (; start < Length; start++) |
||||
|
{ |
||||
|
if (!char.IsWhiteSpace(this[start])) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
return Slice(start); |
||||
|
} |
||||
|
|
||||
|
public override string ToString() => _length == 0 ? string.Empty : _s.Substring(_start, _length); |
||||
|
} |
||||
|
|
||||
|
static class SpanCompatExtensions |
||||
|
{ |
||||
|
public static ReadOnlySpan<char> AsSpan(this string s) => new ReadOnlySpan<char>(s); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,117 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Reflection; |
||||
|
using System.Text; |
||||
|
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; |
||||
|
using Microsoft.Build.Framework; |
||||
|
using Mono.Cecil; |
||||
|
using XamlIl.TypeSystem; |
||||
|
using Avalonia.Utilities; |
||||
|
using Mono.Cecil.Rocks; |
||||
|
using XamlIl.Parsers; |
||||
|
using XamlIl.Transform; |
||||
|
using TypeAttributes = Mono.Cecil.TypeAttributes; |
||||
|
|
||||
|
namespace Avalonia.Build.Tasks |
||||
|
{ |
||||
|
|
||||
|
public static class XamlCompilerTaskExecutor |
||||
|
{ |
||||
|
static bool CheckXamlName(string n) => n.ToLowerInvariant().EndsWith(".xaml") |
||||
|
|| n.ToLowerInvariant().EndsWith(".paml"); |
||||
|
static Dictionary<string, byte[]> ReadAvaloniaXamlResources(AssemblyDefinition asm) |
||||
|
{ |
||||
|
var rv = new Dictionary<string, byte[]>(); |
||||
|
var stream = ((EmbeddedResource)asm.MainModule.Resources.FirstOrDefault(r => |
||||
|
r.ResourceType == ResourceType.Embedded && r.Name == "!AvaloniaResources"))?.GetResourceStream(); |
||||
|
if (stream == null) |
||||
|
return rv; |
||||
|
var br = new BinaryReader(stream); |
||||
|
var index = AvaloniaResourcesIndexReaderWriter.Read(new MemoryStream(br.ReadBytes(br.ReadInt32()))); |
||||
|
var baseOffset = stream.Position; |
||||
|
foreach (var e in index.Where(e => CheckXamlName(e.Path))) |
||||
|
{ |
||||
|
stream.Position = e.Offset + baseOffset; |
||||
|
rv[e.Path] = br.ReadBytes(e.Size); |
||||
|
} |
||||
|
return rv; |
||||
|
} |
||||
|
|
||||
|
static Dictionary<string, byte[]> ReadEmbeddedXamlResources(AssemblyDefinition asm) |
||||
|
{ |
||||
|
var rv = new Dictionary<string, byte[]>(); |
||||
|
foreach (var r in asm.MainModule.Resources.OfType<EmbeddedResource>().Where(r => CheckXamlName(r.Name))) |
||||
|
rv[r.Name] = r.GetResourceData(); |
||||
|
return rv; |
||||
|
} |
||||
|
|
||||
|
public static byte[] Compile(IBuildEngine engine, string input, string[] references) |
||||
|
{ |
||||
|
var typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input); |
||||
|
var asm = typeSystem.TargetAssemblyDefinition; |
||||
|
var emres = ReadEmbeddedXamlResources(asm); |
||||
|
var avares = ReadAvaloniaXamlResources(asm); |
||||
|
if (avares.Count == 0 && emres.Count == 0) |
||||
|
// Nothing to do
|
||||
|
return null; |
||||
|
var xamlLanguage = AvaloniaXamlIlLanguage.Configure(typeSystem); |
||||
|
var compilerConfig = new XamlIlTransformerConfiguration(typeSystem, |
||||
|
typeSystem.TargetAssembly, |
||||
|
xamlLanguage, |
||||
|
XamlIlXmlnsMappings.Resolve(typeSystem, xamlLanguage), |
||||
|
AvaloniaXamlIlLanguage.CustomValueConverter); |
||||
|
|
||||
|
|
||||
|
var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext", |
||||
|
TypeAttributes.Class, asm.MainModule.TypeSystem.Object); |
||||
|
asm.MainModule.Types.Add(contextDef); |
||||
|
|
||||
|
var contextClass = XamlIlContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem, |
||||
|
xamlLanguage); |
||||
|
|
||||
|
var compiler = new AvaloniaXamlIlCompiler(compilerConfig, contextClass); |
||||
|
|
||||
|
var editorBrowsableAttribute = typeSystem |
||||
|
.GetTypeReference(typeSystem.FindType("System.ComponentModel.EditorBrowsableAttribute")) |
||||
|
.Resolve(); |
||||
|
var editorBrowsableCtor = |
||||
|
asm.MainModule.ImportReference(editorBrowsableAttribute.GetConstructors() |
||||
|
.First(c => c.Parameters.Count == 1)); |
||||
|
|
||||
|
void CompileGroup(Dictionary<string, byte[]> resources, string name, Func<string, string> uriTransform) |
||||
|
{ |
||||
|
var typeDef = new TypeDefinition("CompiledAvaloniaXaml", name, |
||||
|
TypeAttributes.Class, asm.MainModule.TypeSystem.Object); |
||||
|
|
||||
|
|
||||
|
typeDef.CustomAttributes.Add(new CustomAttribute(editorBrowsableCtor) |
||||
|
{ |
||||
|
ConstructorArguments = {new CustomAttributeArgument(editorBrowsableCtor.Parameters[0].ParameterType, 1)} |
||||
|
}); |
||||
|
asm.MainModule.Types.Add(typeDef); |
||||
|
var builder = typeSystem.CreateTypeBuilder(typeDef); |
||||
|
foreach (var res in resources) |
||||
|
{ |
||||
|
var xaml = Encoding.UTF8.GetString(res.Value); |
||||
|
var parsed = XDocumentXamlIlParser.Parse(xaml); |
||||
|
compiler.Transform(parsed); |
||||
|
compiler.Compile(parsed, builder, contextClass, |
||||
|
"Populate:" + res.Key, "Build:" + res.Key, |
||||
|
"NamespaceInfo:" + res.Key, uriTransform(res.Key)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (emres.Count != 0) |
||||
|
CompileGroup(emres, "EmbeddedResource", name => $"resm:{name}?assembly={asm.Name}"); |
||||
|
if (avares.Count != 0) |
||||
|
CompileGroup(avares, "AvaloniaResource", name => $"avares://{asm.Name}/{name}"); |
||||
|
|
||||
|
var ms = new MemoryStream(); |
||||
|
asm.Write(ms); |
||||
|
return ms.ToArray(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -1 +1 @@ |
|||||
Subproject commit c9f6ffedbc27cadbd6f393b1a142bbf2e6eaf78f |
Subproject commit 8fcce31fad28cb24b647ca3aed90199553ed0ca4 |
||||
Loading…
Reference in new issue