XamlCompilerTaskExecutor.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. using System;
  2. using System.IO;
  3. using System.Linq;
  4. using System.Reflection;
  5. using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions;
  6. using Microsoft.Build.Framework;
  7. using Mono.Cecil;
  8. using Mono.Cecil.Cil;
  9. using Mono.Cecil.Rocks;
  10. using XamlX;
  11. using XamlX.Ast;
  12. using XamlX.Parsers;
  13. using XamlX.Transform;
  14. using XamlX.TypeSystem;
  15. using FieldAttributes = Mono.Cecil.FieldAttributes;
  16. using MethodAttributes = Mono.Cecil.MethodAttributes;
  17. using TypeAttributes = Mono.Cecil.TypeAttributes;
  18. using XamlX.IL;
  19. namespace Avalonia.Build.Tasks
  20. {
  21. public static partial class XamlCompilerTaskExecutor
  22. {
  23. static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
  24. || r.Name.ToLowerInvariant().EndsWith(".paml")
  25. || r.Name.ToLowerInvariant().EndsWith(".axaml");
  26. public class CompileResult
  27. {
  28. public bool Success { get; set; }
  29. public bool WrittenFile { get; }
  30. public CompileResult(bool success, bool writtenFile = false)
  31. {
  32. Success = success;
  33. WrittenFile = writtenFile;
  34. }
  35. }
  36. public static CompileResult Compile(IBuildEngine engine, string input, string[] references,
  37. string projectDirectory,
  38. string output, bool verifyIl, MessageImportance logImportance, string strongNameKey, bool patchCom,
  39. bool skipXamlCompilation)
  40. {
  41. return Compile(engine, input, references, projectDirectory, output, verifyIl, logImportance, strongNameKey, patchCom, skipXamlCompilation, debuggerLaunch:false);
  42. }
  43. internal static CompileResult Compile(IBuildEngine engine, string input, string[] references,
  44. string projectDirectory,
  45. string output, bool verifyIl, MessageImportance logImportance, string strongNameKey, bool patchCom, bool skipXamlCompilation, bool debuggerLaunch)
  46. {
  47. var typeSystem = new CecilTypeSystem(
  48. references.Where(r => !r.ToLowerInvariant().EndsWith("avalonia.build.tasks.dll")),
  49. input);
  50. var asm = typeSystem.TargetAssemblyDefinition;
  51. if (!skipXamlCompilation)
  52. {
  53. var compileRes = CompileCore(engine, typeSystem, projectDirectory, verifyIl, logImportance, debuggerLaunch);
  54. if (compileRes == null && !patchCom)
  55. return new CompileResult(true);
  56. if (compileRes == false)
  57. return new CompileResult(false);
  58. }
  59. if (patchCom)
  60. ComInteropHelper.PatchAssembly(asm, typeSystem);
  61. var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols };
  62. if (!string.IsNullOrWhiteSpace(strongNameKey))
  63. writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey);
  64. asm.Write(output, writerParameters);
  65. return new CompileResult(true, true);
  66. }
  67. static bool? CompileCore(IBuildEngine engine, CecilTypeSystem typeSystem,
  68. string projectDirectory, bool verifyIl,
  69. MessageImportance logImportance
  70. , bool debuggerLaunch = false)
  71. {
  72. if (debuggerLaunch)
  73. {
  74. // According this https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.debugger.launch?view=net-6.0#remarks
  75. // documentation, on not windows platform Debugger.Launch() always return true without running a debugger.
  76. if (System.Diagnostics.Debugger.Launch())
  77. {
  78. // Set timeout at 1 minut.
  79. var time = new System.Diagnostics.Stopwatch();
  80. var timeout = TimeSpan.FromMinutes(1);
  81. time.Start();
  82. // wait for the debugger to be attacked or timeout.
  83. while (!System.Diagnostics.Debugger.IsAttached && time.Elapsed < timeout)
  84. {
  85. engine.LogMessage($"[PID:{System.Diagnostics.Process.GetCurrentProcess().Id}] Wating attach debugger. Elapsed {time.Elapsed}...", MessageImportance.High);
  86. System.Threading.Thread.Sleep(100);
  87. }
  88. time.Stop();
  89. if (time.Elapsed >= timeout)
  90. {
  91. engine.LogMessage("Wating attach debugger timeout.", MessageImportance.Normal);
  92. }
  93. }
  94. else
  95. {
  96. engine.LogMessage("Debugging cancelled.", MessageImportance.Normal);
  97. }
  98. }
  99. var asm = typeSystem.TargetAssemblyDefinition;
  100. var emres = new EmbeddedResources(asm);
  101. var avares = new AvaloniaResources(asm, projectDirectory);
  102. if (avares.Resources.Count(CheckXamlName) == 0 && emres.Resources.Count(CheckXamlName) == 0)
  103. // Nothing to do
  104. return null;
  105. var clrPropertiesDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlHelpers",
  106. TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
  107. asm.MainModule.Types.Add(clrPropertiesDef);
  108. var indexerAccessorClosure = new TypeDefinition("CompiledAvaloniaXaml", "!IndexerAccessorFactoryClosure",
  109. TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
  110. asm.MainModule.Types.Add(indexerAccessorClosure);
  111. var trampolineBuilder = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlTrampolines",
  112. TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
  113. asm.MainModule.Types.Add(trampolineBuilder);
  114. var (xamlLanguage , emitConfig) = AvaloniaXamlIlLanguage.Configure(typeSystem);
  115. var compilerConfig = new AvaloniaXamlIlCompilerConfiguration(typeSystem,
  116. typeSystem.TargetAssembly,
  117. xamlLanguage,
  118. XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage),
  119. AvaloniaXamlIlLanguage.CustomValueConverter,
  120. new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)),
  121. new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)),
  122. new XamlIlTrampolineBuilder(typeSystem.CreateTypeBuilder(trampolineBuilder)),
  123. new DeterministicIdGenerator());
  124. var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext",
  125. TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
  126. asm.MainModule.Types.Add(contextDef);
  127. var contextClass = XamlILContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem,
  128. xamlLanguage, emitConfig);
  129. var compiler = new AvaloniaXamlIlCompiler(compilerConfig, emitConfig, contextClass) { EnableIlVerification = verifyIl };
  130. var editorBrowsableAttribute = typeSystem
  131. .GetTypeReference(typeSystem.FindType("System.ComponentModel.EditorBrowsableAttribute"))
  132. .Resolve();
  133. var editorBrowsableCtor =
  134. asm.MainModule.ImportReference(editorBrowsableAttribute.GetConstructors()
  135. .First(c => c.Parameters.Count == 1));
  136. var runtimeHelpers = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers");
  137. var createRootServiceProviderMethod = asm.MainModule.ImportReference(
  138. typeSystem.GetTypeReference(runtimeHelpers).Resolve().Methods
  139. .First(x => x.Name == "CreateRootServiceProviderV2"));
  140. var loaderDispatcherDef = new TypeDefinition("CompiledAvaloniaXaml", "!XamlLoader",
  141. TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
  142. loaderDispatcherDef.CustomAttributes.Add(new CustomAttribute(editorBrowsableCtor)
  143. {
  144. ConstructorArguments = {new CustomAttributeArgument(editorBrowsableCtor.Parameters[0].ParameterType, 1)}
  145. });
  146. var loaderDispatcherMethod = new MethodDefinition("TryLoad",
  147. MethodAttributes.Static | MethodAttributes.Public,
  148. asm.MainModule.TypeSystem.Object)
  149. {
  150. Parameters = {new ParameterDefinition(asm.MainModule.TypeSystem.String)}
  151. };
  152. loaderDispatcherDef.Methods.Add(loaderDispatcherMethod);
  153. asm.MainModule.Types.Add(loaderDispatcherDef);
  154. var stringEquals = asm.MainModule.ImportReference(asm.MainModule.TypeSystem.String.Resolve().Methods.First(
  155. m =>
  156. m.IsStatic && m.Name == "Equals" && m.Parameters.Count == 2 &&
  157. m.ReturnType.FullName == "System.Boolean"
  158. && m.Parameters[0].ParameterType.FullName == "System.String"
  159. && m.Parameters[1].ParameterType.FullName == "System.String"));
  160. bool CompileGroup(IResourceGroup group)
  161. {
  162. var typeDef = new TypeDefinition("CompiledAvaloniaXaml", "!"+ group.Name,
  163. TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
  164. typeDef.CustomAttributes.Add(new CustomAttribute(editorBrowsableCtor)
  165. {
  166. ConstructorArguments = {new CustomAttributeArgument(editorBrowsableCtor.Parameters[0].ParameterType, 1)}
  167. });
  168. asm.MainModule.Types.Add(typeDef);
  169. var builder = typeSystem.CreateTypeBuilder(typeDef);
  170. foreach (var res in group.Resources.Where(CheckXamlName).OrderBy(x=>x.FilePath.ToLowerInvariant()))
  171. {
  172. try
  173. {
  174. engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance);
  175. // StreamReader is needed here to handle BOM
  176. var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
  177. var parsed = XDocumentXamlParser.Parse(xaml);
  178. var initialRoot = (XamlAstObjectNode)parsed.Root;
  179. var precompileDirective = initialRoot.Children.OfType<XamlAstXmlDirective>()
  180. .FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Precompile");
  181. if (precompileDirective != null)
  182. {
  183. var precompileText = (precompileDirective.Values[0] as XamlAstTextNode)?.Text.Trim()
  184. .ToLowerInvariant();
  185. if (precompileText == "false")
  186. continue;
  187. if (precompileText != "true")
  188. throw new XamlParseException("Invalid value for x:Precompile", precompileDirective);
  189. }
  190. var classDirective = initialRoot.Children.OfType<XamlAstXmlDirective>()
  191. .FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Class");
  192. IXamlType classType = null;
  193. if (classDirective != null)
  194. {
  195. if (classDirective.Values.Count != 1 || !(classDirective.Values[0] is XamlAstTextNode tn))
  196. throw new XamlParseException("x:Class should have a string value", classDirective);
  197. classType = typeSystem.TargetAssembly.FindType(tn.Text);
  198. if (classType == null)
  199. throw new XamlParseException($"Unable to find type `{tn.Text}`", classDirective);
  200. compiler.OverrideRootType(parsed,
  201. new XamlAstClrTypeReference(classDirective, classType, false));
  202. initialRoot.Children.Remove(classDirective);
  203. }
  204. compiler.Transform(parsed);
  205. var populateName = classType == null ? "Populate:" + res.Name : "!XamlIlPopulate";
  206. var buildName = classType == null ? "Build:" + res.Name : null;
  207. var classTypeDefinition =
  208. classType == null ? null : typeSystem.GetTypeReference(classType).Resolve();
  209. var populateBuilder = classTypeDefinition == null ?
  210. builder :
  211. typeSystem.CreateTypeBuilder(classTypeDefinition);
  212. compiler.Compile(parsed, contextClass,
  213. compiler.DefinePopulateMethod(populateBuilder, parsed, populateName,
  214. classTypeDefinition == null),
  215. buildName == null ? null : compiler.DefineBuildMethod(builder, parsed, buildName, true),
  216. builder.DefineSubType(compilerConfig.WellKnownTypes.Object, "NamespaceInfo:" + res.Name,
  217. true),
  218. (closureName, closureBaseType) =>
  219. populateBuilder.DefineSubType(closureBaseType, closureName, false),
  220. (closureName, returnType, parameterTypes) =>
  221. populateBuilder.DefineDelegateSubType(closureName, false, returnType, parameterTypes),
  222. res.Uri, res
  223. );
  224. if (classTypeDefinition != null)
  225. {
  226. var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve()
  227. .Methods.First(m => m.Name == populateName);
  228. var designLoaderFieldType = typeSystem
  229. .GetType("System.Action`1")
  230. .MakeGenericType(typeSystem.GetType("System.Object"));
  231. var designLoaderFieldTypeReference = (GenericInstanceType)typeSystem.GetTypeReference(designLoaderFieldType);
  232. designLoaderFieldTypeReference.GenericArguments[0] =
  233. asm.MainModule.ImportReference(designLoaderFieldTypeReference.GenericArguments[0]);
  234. designLoaderFieldTypeReference = (GenericInstanceType)
  235. asm.MainModule.ImportReference(designLoaderFieldTypeReference);
  236. var designLoaderLoad =
  237. typeSystem.GetMethodReference(
  238. designLoaderFieldType.Methods.First(m => m.Name == "Invoke"));
  239. designLoaderLoad =
  240. asm.MainModule.ImportReference(designLoaderLoad);
  241. designLoaderLoad.DeclaringType = designLoaderFieldTypeReference;
  242. var designLoaderField = new FieldDefinition("!XamlIlPopulateOverride",
  243. FieldAttributes.Static | FieldAttributes.Private, designLoaderFieldTypeReference);
  244. classTypeDefinition.Fields.Add(designLoaderField);
  245. const string TrampolineName = "!XamlIlPopulateTrampoline";
  246. var trampoline = new MethodDefinition(TrampolineName,
  247. MethodAttributes.Static | MethodAttributes.Private, asm.MainModule.TypeSystem.Void);
  248. trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition));
  249. classTypeDefinition.Methods.Add(trampoline);
  250. var regularStart = Instruction.Create(OpCodes.Call, createRootServiceProviderMethod);
  251. trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldsfld, designLoaderField));
  252. trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Brfalse, regularStart));
  253. trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldsfld, designLoaderField));
  254. trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
  255. trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Call, designLoaderLoad));
  256. trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
  257. trampoline.Body.Instructions.Add(regularStart);
  258. trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
  259. trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Call, compiledPopulateMethod));
  260. trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
  261. CopyDebugDocument(trampoline, compiledPopulateMethod);
  262. var foundXamlLoader = false;
  263. // Find AvaloniaXamlLoader.Load(this) and replace it with !XamlIlPopulateTrampoline(this)
  264. foreach (var method in classTypeDefinition.Methods
  265. .Where(m => !m.Attributes.HasFlag(MethodAttributes.Static)))
  266. {
  267. var i = method.Body.Instructions;
  268. for (var c = 1; c < i.Count; c++)
  269. {
  270. if (i[c].OpCode == OpCodes.Call)
  271. {
  272. var op = i[c].Operand as MethodReference;
  273. // TODO: Throw an error
  274. // This usually happens when same XAML resource was added twice for some weird reason
  275. // We currently support it for dual-named default theme resource
  276. if (op != null
  277. && op.Name == TrampolineName)
  278. {
  279. foundXamlLoader = true;
  280. break;
  281. }
  282. if (op != null
  283. && op.Name == "Load"
  284. && op.Parameters.Count == 1
  285. && op.Parameters[0].ParameterType.FullName == "System.Object"
  286. && op.DeclaringType.FullName == "Avalonia.Markup.Xaml.AvaloniaXamlLoader")
  287. {
  288. if (MatchThisCall(i, c - 1))
  289. {
  290. i[c].Operand = trampoline;
  291. foundXamlLoader = true;
  292. }
  293. }
  294. }
  295. }
  296. }
  297. if (!foundXamlLoader)
  298. {
  299. var ctors = classTypeDefinition.GetConstructors()
  300. .Where(c => !c.IsStatic).ToList();
  301. // We can inject xaml loader into default constructor
  302. if (ctors.Count == 1 && ctors[0].Body.Instructions.Count(o=>o.OpCode != OpCodes.Nop) == 3)
  303. {
  304. var i = ctors[0].Body.Instructions;
  305. var retIdx = i.IndexOf(i.Last(x => x.OpCode == OpCodes.Ret));
  306. i.Insert(retIdx, Instruction.Create(OpCodes.Call, trampoline));
  307. i.Insert(retIdx, Instruction.Create(OpCodes.Ldarg_0));
  308. }
  309. else
  310. {
  311. throw new InvalidProgramException(
  312. $"No call to AvaloniaXamlLoader.Load(this) call found anywhere in the type {classType.FullName} and type seems to have custom constructors.");
  313. }
  314. }
  315. }
  316. if (buildName != null || classTypeDefinition != null)
  317. {
  318. var compiledBuildMethod = buildName == null ?
  319. null :
  320. typeSystem.GetTypeReference(builder).Resolve()
  321. .Methods.First(m => m.Name == buildName);
  322. var parameterlessConstructor = compiledBuildMethod != null ?
  323. null :
  324. classTypeDefinition.GetConstructors().FirstOrDefault(c =>
  325. c.IsPublic && !c.IsStatic && !c.HasParameters);
  326. if (compiledBuildMethod != null || parameterlessConstructor != null)
  327. {
  328. var i = loaderDispatcherMethod.Body.Instructions;
  329. var nop = Instruction.Create(OpCodes.Nop);
  330. i.Add(Instruction.Create(OpCodes.Ldarg_0));
  331. i.Add(Instruction.Create(OpCodes.Ldstr, res.Uri));
  332. i.Add(Instruction.Create(OpCodes.Call, stringEquals));
  333. i.Add(Instruction.Create(OpCodes.Brfalse, nop));
  334. if (parameterlessConstructor != null)
  335. i.Add(Instruction.Create(OpCodes.Newobj, parameterlessConstructor));
  336. else
  337. {
  338. i.Add(Instruction.Create(OpCodes.Call, createRootServiceProviderMethod));
  339. i.Add(Instruction.Create(OpCodes.Call, compiledBuildMethod));
  340. }
  341. i.Add(Instruction.Create(OpCodes.Ret));
  342. i.Add(nop);
  343. }
  344. }
  345. }
  346. catch (Exception e)
  347. {
  348. int lineNumber = 0, linePosition = 0;
  349. if (e is XamlParseException xe)
  350. {
  351. lineNumber = xe.LineNumber;
  352. linePosition = xe.LinePosition;
  353. }
  354. engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", res.FilePath,
  355. lineNumber, linePosition, lineNumber, linePosition,
  356. e.Message, "", "Avalonia"));
  357. return false;
  358. }
  359. res.Remove();
  360. }
  361. // Technically that's a hack, but it fixes corert incompatibility caused by deterministic builds
  362. int dupeCounter = 1;
  363. foreach (var grp in typeDef.NestedTypes.GroupBy(x => x.Name))
  364. {
  365. if (grp.Count() > 1)
  366. {
  367. foreach (var dupe in grp)
  368. dupe.Name += "_dup" + dupeCounter++;
  369. }
  370. }
  371. return true;
  372. }
  373. if (emres.Resources.Count(CheckXamlName) != 0)
  374. if (!CompileGroup(emres))
  375. return false;
  376. if (avares.Resources.Count(CheckXamlName) != 0)
  377. {
  378. if (!CompileGroup(avares))
  379. return false;
  380. avares.Save();
  381. }
  382. loaderDispatcherMethod.Body.Instructions.Add(Instruction.Create(OpCodes.Ldnull));
  383. loaderDispatcherMethod.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
  384. return true;
  385. }
  386. }
  387. }