浏览代码

Emit compiled XAML from msbuild task

Nikita Tsukanov 6 年之前
父节点
当前提交
a3f3a06478

+ 31 - 0
packages/Avalonia/AvaloniaBuildTasks.targets

@@ -8,6 +8,10 @@
              AssemblyFile="$(AvaloniaBuildTasksLocation)"
              />
 
+  <UsingTask TaskName="CompileAvaloniaXamlTask"
+             AssemblyFile="$(AvaloniaBuildTasksLocation)"
+             />
+
 
   <Target Name="AddAvaloniaResources" BeforeTargets="ResolveReferences">
     <PropertyGroup>
@@ -36,6 +40,33 @@
       Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration)"/>
 
   </Target>
+
+  <Target
+    Name="CompileAvaloniaXaml"
+    AfterTargets="AfterCompile"
+    Condition="Exists('@(IntermediateAssembly)') And $(DesignTimeBuild) != true And $(EnableAvaloniaXamlCompilation) == true"
+    Inputs="@(IntermediateAssembly);"
+    Outputs="$(IntermediateOutputPath)$(MSBuildProjectFile).Fody.CopyLocal.cache">
+    <PropertyGroup>
+      <AvaloniaXamlReferencesTemporaryFilePath Condition="'$(AvaloniaXamlReferencesTemporaryFilePath)' == ''">$(IntermediateOutputPath)/Avalonia/references</AvaloniaXamlReferencesTemporaryFilePath>
+      <AvaloniaXamlOriginalCopyFilePath Condition="'$(AvaloniaXamlOriginalCopyFilePath)' == ''">$(IntermediateOutputPath)/Avalonia/original.dll</AvaloniaXamlOriginalCopyFilePath>
+    </PropertyGroup>
+    <WriteLinesToFile
+      Condition="'$(_AvaloniaForceInternalMSBuild)' != 'true'"
+      File="$(AvaloniaXamlReferencesTemporaryFilePath)"
+      Lines="@(ReferencePathWithRefAssemblies)"
+      Overwrite="true" />
+    <CompileAvaloniaXamlTask
+      Condition="'$(_AvaloniaUseExternalMSBuild)' != 'true'"
+      AssemblyFile="@(IntermediateAssembly)"
+      ReferencesFilePath="$(AvaloniaXamlReferencesTemporaryFilePath)"
+      OriginalCopyPath="$(AvaloniaXamlOriginalCopyFilePath)"
+    />
+    <Exec
+      Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"
+      Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileAvaloniaXaml /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration)"/>
+  </Target>
+
   
   <ItemGroup>
     <UpToDateCheckInput Include="@(AvaloniaResource)" />

+ 4 - 1
src/Avalonia.Base/Data/Core/ExpressionParseException.cs

@@ -9,7 +9,10 @@ namespace Avalonia.Data.Core
     /// Exception thrown when <see cref="ExpressionObserver"/> could not parse the provided
     /// expression string.
     /// </summary>
-    public class ExpressionParseException : Exception
+#if !BUILDTASK
+    public
+#endif
+    class ExpressionParseException : Exception
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="ExpressionParseException"/> class.

+ 4 - 1
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<char> _s;
 

+ 4 - 1
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<char> ParseIdentifier(this ref CharacterReader r)
         {

+ 60 - 5
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@@ -1,9 +1,11 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
+<Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
-        <TargetFramework>netstandard2.0</TargetFramework>
+        <TargetFrameworks>netstandard2.0</TargetFrameworks>
+        <TargetFrameworks Condition="$(Configuration) == 'Debug'">netstandard2.0;netcoreapp2.0</TargetFrameworks>
+        <OutputType>exe</OutputType>
         <BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
-        <DefineConstants>$(DefineConstants);BUILDTASK</DefineConstants>
+        <DefineConstants>$(DefineConstants);BUILDTASK;XAMLIL_CECIL_INTERNAL</DefineConstants>
+        <CopyLocalLockFileAssemblies Condition="$(TargetFramework) == 'netstandard2.0'">true</CopyLocalLockFileAssemblies>
     </PropertyGroup>
 
     <ItemGroup>
@@ -13,6 +15,59 @@
       <Compile Include="../Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaResourceXamlInfo.cs">
         <Link>Shared/AvaloniaResourceXamlInfo.cs</Link>
       </Compile>
-      <PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" />
+      <Compile Include="../Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/**/*.cs">
+        <Link>XamlIlExtensions/%(RecursiveDir)%(FileName)%(Extension)</Link>
+      </Compile>
+      <Compile Remove="external/cecil/**/*.*" />
+      <Compile Include="../Markup/Avalonia.Markup.Xaml/XamlIl\xamlil.github\src\XamlIl\**\*.cs">
+        <Link>XamlIl/%(RecursiveDir)%(FileName)%(Extension)</Link>
+      </Compile>
+      <Compile Include="../Markup/Avalonia.Markup.Xaml/XamlIl\xamlil.github\src\XamlIl.Cecil\**\*.cs">
+        <Link>XamlIl.Cecil/%(RecursiveDir)%(FileName)%(Extension)</Link>
+      </Compile>
+      <Compile Include="../Markup/Avalonia.Markup\Markup\Parsers\SelectorGrammar.cs">
+        <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
+      </Compile>
+      <Compile Include="../Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs">
+        <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
+      </Compile>
+      <Compile Include="../Avalonia.Base/Data/Core/ExpressionParseException.cs">
+        <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
+      </Compile>
+      <Compile Include="../Avalonia.Base/Utilities/CharacterReader.cs">
+        <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
+      </Compile>      
+      <Compile Include="../Avalonia.Base/Utilities/IdentifierParser.cs">
+        <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
+      </Compile>
+      
+      <Compile Remove="../Markup/Avalonia.Markup.Xaml/XamlIl\xamlil.github\**\obj\**\*.cs" />
+      <Compile Remove="../Markup/Avalonia.Markup.Xaml/XamlIl\xamlil.github\src\XamlIl\TypeSystem\SreTypeSystem.cs" />
+      <PackageReference Include="Avalonia.Unofficial.Cecil" Version="20190417.2.0" PrivateAssets="All"/>
+      <PackageReference Condition="$(TargetFramework) == 'netstandard2.0'" Include="ILRepack.MSBuild.Task" Version="2.0.3" PrivateAssets="All" />
+      <PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" />
+    </ItemGroup>
+
+  <Target Name="ILRepack" AfterTargets="Build" Condition="$(TargetFramework) == 'netstandard2.0'">
+
+    <PropertyGroup>
+      <WorkingDirectory>$(MSBuildThisFileDirectory)bin\$(Configuration)\$(TargetFramework)</WorkingDirectory>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <InputAssemblies Include="Mono.Cecil.dll" />
+    </ItemGroup>
+    <ILRepack
+      OutputType="$(OutputType)"
+      MainAssembly="$(AssemblyName).dll"
+      OutputAssembly="$(AssemblyName).dll"
+      InputAssemblies="@(InputAssemblies)"
+      WorkingDirectory="$(WorkingDirectory)" />
+    <ItemGroup>
+      <DeleteNonNeededResults Include="$(WorkingDirectory)/*.dll"/>
+      <DeleteNonNeededResults Remove="$(WorkingDirectory)/Avalonia.Build.Tasks.dll"/>
     </ItemGroup>
+    <Delete Files="@(DeleteNonNeededResults)"/>
+
+  </Target>
 </Project>

+ 48 - 0
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; }
+    }
+}

+ 2 - 1
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}";
 

+ 59 - 0
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; }
+        }
+    }
+}

+ 73 - 0
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<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);
+    }
+
+}

+ 117 - 0
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<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 - 0
src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
+    <EnableAvaloniaXamlCompilation>true</EnableAvaloniaXamlCompilation>
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />

+ 1 - 2
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);
             
         }
         

+ 0 - 2
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;

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github

@@ -1 +1 @@
-Subproject commit c9f6ffedbc27cadbd6f393b1a142bbf2e6eaf78f
+Subproject commit 8fcce31fad28cb24b647ca3aed90199553ed0ca4

+ 1 - 1
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs

@@ -275,7 +275,7 @@ namespace Avalonia.Markup.Parsers
         private static TSyntax ParseType<TSyntax>(ref CharacterReader r, TSyntax syntax)
             where TSyntax : ITypeSyntax
         {
-            ReadOnlySpan<char> ns = null;
+            ReadOnlySpan<char> ns = default;
             ReadOnlySpan<char> type;
             var namespaceOrTypeName = r.ParseIdentifier();