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