diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 10f971cc4c..f98b1fe4f4 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -8,6 +8,10 @@ AssemblyFile="$(AvaloniaBuildTasksLocation)" /> + + @@ -36,6 +40,33 @@ Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration)"/> + + + + $(IntermediateOutputPath)/Avalonia/references + $(IntermediateOutputPath)/Avalonia/original.dll + + + + + + diff --git a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs index 0365ead24d..2432410203 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs @@ -9,7 +9,10 @@ namespace Avalonia.Data.Core /// Exception thrown when could not parse the provided /// expression string. /// - public class ExpressionParseException : Exception +#if !BUILDTASK + public +#endif + class ExpressionParseException : Exception { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Utilities/CharacterReader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs index 56f3f7abd4..9be2d230d4 100644 --- a/src/Avalonia.Base/Utilities/CharacterReader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -5,7 +5,10 @@ using System; namespace Avalonia.Utilities { - public ref struct CharacterReader +#if !BUILDTASK + public +#endif + ref struct CharacterReader { private ReadOnlySpan _s; diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs index 09c50bf147..a57a2b7ba5 100644 --- a/src/Avalonia.Base/Utilities/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -6,7 +6,10 @@ using System.Globalization; namespace Avalonia.Utilities { - public static class IdentifierParser +#if !BUILDTASK + public +#endif + static class IdentifierParser { public static ReadOnlySpan ParseIdentifier(this ref CharacterReader r) { diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index ce767aaa87..072c78f7ef 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -1,9 +1,11 @@ - - + - netstandard2.0 + netstandard2.0 + netstandard2.0;netcoreapp2.0 + exe tools - $(DefineConstants);BUILDTASK + $(DefineConstants);BUILDTASK;XAMLIL_CECIL_INTERNAL + true @@ -13,6 +15,59 @@ Shared/AvaloniaResourceXamlInfo.cs - + + XamlIlExtensions/%(RecursiveDir)%(FileName)%(Extension) + + + + XamlIl/%(RecursiveDir)%(FileName)%(Extension) + + + XamlIl.Cecil/%(RecursiveDir)%(FileName)%(Extension) + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + + + + + + + + + + + + $(MSBuildThisFileDirectory)bin\$(Configuration)\$(TargetFramework) + + + + + + + + + + + + diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs new file mode 100644 index 0000000000..028643f7c0 --- /dev/null +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -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; } + } +} diff --git a/src/Avalonia.Build.Tasks/Extensions.cs b/src/Avalonia.Build.Tasks/Extensions.cs index faecee0f37..440c6d7489 100644 --- a/src/Avalonia.Build.Tasks/Extensions.cs +++ b/src/Avalonia.Build.Tasks/Extensions.cs @@ -1,8 +1,9 @@ +using System; using Microsoft.Build.Framework; namespace Avalonia.Build.Tasks { - public static class Extensions + static class Extensions { static string FormatErrorCode(BuildEngineErrorCode code) => $"AVLN:{(int)code:0000}"; diff --git a/src/Avalonia.Build.Tasks/Program.cs b/src/Avalonia.Build.Tasks/Program.cs new file mode 100644 index 0000000000..2f206acf2a --- /dev/null +++ b/src/Avalonia.Build.Tasks/Program.cs @@ -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; } + } + } +} diff --git a/src/Avalonia.Build.Tasks/SpanCompat.cs b/src/Avalonia.Build.Tasks/SpanCompat.cs new file mode 100644 index 0000000000..d5c406293e --- /dev/null +++ b/src/Avalonia.Build.Tasks/SpanCompat.cs @@ -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 + { + 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 Slice(int start, int len) + { + return new ReadOnlySpan(_s, _start + start, len); + } + + public static ReadOnlySpan Empty => default; + + public ReadOnlySpan Slice(int start) + { + return new ReadOnlySpan(_s, _start + start, _length - start); + } + + public bool SequenceEqual(ReadOnlySpan 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 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 AsSpan(this string s) => new ReadOnlySpan(s); + } + +} diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs new file mode 100644 index 0000000000..acaa80b608 --- /dev/null +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -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 ReadAvaloniaXamlResources(AssemblyDefinition asm) + { + var rv = new Dictionary(); + 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 ReadEmbeddedXamlResources(AssemblyDefinition asm) + { + var rv = new Dictionary(); + foreach (var r in asm.MainModule.Resources.OfType().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 resources, string name, Func 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(); + } + + } +} diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index ea53f308ab..045cc49142 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -1,6 +1,7 @@  netstandard2.0 + true diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 4b205cb2e8..22d66e3b9d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -74,8 +74,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } Transform(parsed); - Compile(parsed, tb, _contextType, PopulateName, BuildName, - "__AvaloniaXamlIlContext", "__AvaloniaXamlIlNsInfo", baseUri); + Compile(parsed, tb, _contextType, PopulateName, BuildName, "__AvaloniaXamlIlNsInfo", baseUri); } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs index a15c162f84..beec47afa4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Markup.Xaml.Parsers; -using Avalonia.Utilities; using XamlIl; using XamlIl.Ast; using XamlIl.Transform; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index c9f6ffedbc..8fcce31fad 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit c9f6ffedbc27cadbd6f393b1a142bbf2e6eaf78f +Subproject commit 8fcce31fad28cb24b647ca3aed90199553ed0ca4 diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index e11e333a49..c57b8c4cf6 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -275,7 +275,7 @@ namespace Avalonia.Markup.Parsers private static TSyntax ParseType(ref CharacterReader r, TSyntax syntax) where TSyntax : ITypeSyntax { - ReadOnlySpan ns = null; + ReadOnlySpan ns = default; ReadOnlySpan type; var namespaceOrTypeName = r.ParseIdentifier();