Browse Source

Emit compiled XAML from msbuild task

xamlil-debug-info
Nikita Tsukanov 7 years ago
parent
commit
a3f3a06478
  1. 31
      packages/Avalonia/AvaloniaBuildTasks.targets
  2. 5
      src/Avalonia.Base/Data/Core/ExpressionParseException.cs
  3. 5
      src/Avalonia.Base/Utilities/CharacterReader.cs
  4. 5
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  5. 65
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  6. 48
      src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs
  7. 3
      src/Avalonia.Build.Tasks/Extensions.cs
  8. 59
      src/Avalonia.Build.Tasks/Program.cs
  9. 73
      src/Avalonia.Build.Tasks/SpanCompat.cs
  10. 117
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  11. 1
      src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj
  12. 3
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  13. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs
  14. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
  15. 2
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs

31
packages/Avalonia/AvaloniaBuildTasks.targets

@ -8,6 +8,10 @@
AssemblyFile="$(AvaloniaBuildTasksLocation)" AssemblyFile="$(AvaloniaBuildTasksLocation)"
/> />
<UsingTask TaskName="CompileAvaloniaXamlTask"
AssemblyFile="$(AvaloniaBuildTasksLocation)"
/>
<Target Name="AddAvaloniaResources" BeforeTargets="ResolveReferences"> <Target Name="AddAvaloniaResources" BeforeTargets="ResolveReferences">
<PropertyGroup> <PropertyGroup>
@ -36,6 +40,33 @@
Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration)"/> Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration)"/>
</Target> </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> <ItemGroup>
<UpToDateCheckInput Include="@(AvaloniaResource)" /> <UpToDateCheckInput Include="@(AvaloniaResource)" />

5
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 /// Exception thrown when <see cref="ExpressionObserver"/> could not parse the provided
/// expression string. /// expression string.
/// </summary> /// </summary>
public class ExpressionParseException : Exception #if !BUILDTASK
public
#endif
class ExpressionParseException : Exception
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ExpressionParseException"/> class. /// Initializes a new instance of the <see cref="ExpressionParseException"/> class.

5
src/Avalonia.Base/Utilities/CharacterReader.cs

@ -5,7 +5,10 @@ using System;
namespace Avalonia.Utilities namespace Avalonia.Utilities
{ {
public ref struct CharacterReader #if !BUILDTASK
public
#endif
ref struct CharacterReader
{ {
private ReadOnlySpan<char> _s; private ReadOnlySpan<char> _s;

5
src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -6,7 +6,10 @@ using System.Globalization;
namespace Avalonia.Utilities namespace Avalonia.Utilities
{ {
public static class IdentifierParser #if !BUILDTASK
public
#endif
static class IdentifierParser
{ {
public static ReadOnlySpan<char> ParseIdentifier(this ref CharacterReader r) public static ReadOnlySpan<char> ParseIdentifier(this ref CharacterReader r)
{ {

65
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>netstandard2.0</TargetFrameworks>
<TargetFrameworks Condition="$(Configuration) == 'Debug'">netstandard2.0;netcoreapp2.0</TargetFrameworks>
<OutputType>exe</OutputType>
<BuildOutputTargetFolder>tools</BuildOutputTargetFolder> <BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
<DefineConstants>$(DefineConstants);BUILDTASK</DefineConstants> <DefineConstants>$(DefineConstants);BUILDTASK;XAMLIL_CECIL_INTERNAL</DefineConstants>
<CopyLocalLockFileAssemblies Condition="$(TargetFramework) == 'netstandard2.0'">true</CopyLocalLockFileAssemblies>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -13,6 +15,59 @@
<Compile Include="../Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaResourceXamlInfo.cs"> <Compile Include="../Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaResourceXamlInfo.cs">
<Link>Shared/AvaloniaResourceXamlInfo.cs</Link> <Link>Shared/AvaloniaResourceXamlInfo.cs</Link>
</Compile> </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> </ItemGroup>
<Delete Files="@(DeleteNonNeededResults)"/>
</Target>
</Project> </Project>

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

3
src/Avalonia.Build.Tasks/Extensions.cs

@ -1,8 +1,9 @@
using System;
using Microsoft.Build.Framework; using Microsoft.Build.Framework;
namespace Avalonia.Build.Tasks namespace Avalonia.Build.Tasks
{ {
public static class Extensions static class Extensions
{ {
static string FormatErrorCode(BuildEngineErrorCode code) => $"AVLN:{(int)code:0000}"; static string FormatErrorCode(BuildEngineErrorCode code) => $"AVLN:{(int)code:0000}";

59
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
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
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
src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj

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

3
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -74,8 +74,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
} }
Transform(parsed); Transform(parsed);
Compile(parsed, tb, _contextType, PopulateName, BuildName, Compile(parsed, tb, _contextType, PopulateName, BuildName, "__AvaloniaXamlIlNsInfo", baseUri);
"__AvaloniaXamlIlContext", "__AvaloniaXamlIlNsInfo", baseUri);
} }

2
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs

@ -1,8 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Markup.Xaml.Parsers;
using Avalonia.Utilities;
using XamlIl; using XamlIl;
using XamlIl.Ast; using XamlIl.Ast;
using XamlIl.Transform; using XamlIl.Transform;

2
src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github

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

2
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) private static TSyntax ParseType<TSyntax>(ref CharacterReader r, TSyntax syntax)
where TSyntax : ITypeSyntax where TSyntax : ITypeSyntax
{ {
ReadOnlySpan<char> ns = null; ReadOnlySpan<char> ns = default;
ReadOnlySpan<char> type; ReadOnlySpan<char> type;
var namespaceOrTypeName = r.ParseIdentifier(); var namespaceOrTypeName = r.ParseIdentifier();

Loading…
Cancel
Save