GenerateAvaloniaResourcesTask.cs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Runtime.Serialization;
  6. using System.Runtime.Serialization.Json;
  7. using System.Text;
  8. using Avalonia.Markup.Xaml.PortableXaml;
  9. using Avalonia.Utilities;
  10. using Microsoft.Build.Framework;
  11. using SPath=System.IO.Path;
  12. namespace Avalonia.Build.Tasks
  13. {
  14. public class GenerateAvaloniaResourcesTask : ITask
  15. {
  16. [Required]
  17. public ITaskItem[] Resources { get; set; }
  18. [Required]
  19. public string Root { get; set; }
  20. [Required]
  21. public string Output { get; set; }
  22. [Required]
  23. public ITaskItem[] EmbeddedResources { get; set; }
  24. public string ReportImportance { get; set; }
  25. private MessageImportance _reportImportance;
  26. class Source
  27. {
  28. public string Path { get; set; }
  29. public int Size { get; set; }
  30. private byte[] _data;
  31. private string _sourcePath;
  32. public Source(string relativePath, string root)
  33. {
  34. root = SPath.GetFullPath(root);
  35. Path = "/" + relativePath.Replace('\\', '/');
  36. _sourcePath = SPath.Combine(root, relativePath);
  37. Size = (int)new FileInfo(_sourcePath).Length;
  38. }
  39. public string SystemPath => _sourcePath ?? Path;
  40. public Source(string path, byte[] data)
  41. {
  42. Path = path;
  43. _data = data;
  44. Size = data.Length;
  45. }
  46. public Stream Open()
  47. {
  48. if (_data != null)
  49. return new MemoryStream(_data, false);
  50. return File.OpenRead(_sourcePath);
  51. }
  52. public string ReadAsString()
  53. {
  54. if (_data != null)
  55. return Encoding.UTF8.GetString(_data);
  56. return File.ReadAllText(_sourcePath);
  57. }
  58. }
  59. List<Source> BuildResourceSources()
  60. => Resources.Select(r =>
  61. {
  62. var src = new Source(r.ItemSpec, Root);
  63. BuildEngine.LogMessage($"avares -> name:{src.Path}, path: {src.SystemPath}, size:{src.Size}, ItemSpec:{r.ItemSpec}", _reportImportance);
  64. return src;
  65. }).ToList();
  66. private void Pack(Stream output, List<Source> sources)
  67. {
  68. var offsets = new Dictionary<Source, int>();
  69. var coffset = 0;
  70. foreach (var s in sources)
  71. {
  72. offsets[s] = coffset;
  73. coffset += s.Size;
  74. }
  75. var index = sources.Select(s => new AvaloniaResourcesIndexEntry
  76. {
  77. Path = s.Path,
  78. Size = s.Size,
  79. Offset = offsets[s]
  80. }).ToList();
  81. var ms = new MemoryStream();
  82. AvaloniaResourcesIndexReaderWriter.Write(ms, index);
  83. new BinaryWriter(output).Write((int)ms.Length);
  84. ms.Position = 0;
  85. ms.CopyTo(output);
  86. foreach (var s in sources)
  87. {
  88. using(var input = s.Open())
  89. input.CopyTo(output);
  90. }
  91. }
  92. private bool PreProcessXamlFiles(List<Source> sources)
  93. {
  94. var typeToXamlIndex = new Dictionary<string, string>();
  95. foreach (var s in sources.ToList())
  96. {
  97. if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml") || s.Path.ToLowerInvariant().EndsWith(".axaml"))
  98. {
  99. XamlFileInfo info;
  100. try
  101. {
  102. info = XamlFileInfo.Parse(s.ReadAsString());
  103. }
  104. catch(Exception e)
  105. {
  106. BuildEngine.LogError(BuildEngineErrorCode.InvalidXAML, s.SystemPath, "File doesn't contain valid XAML: " + e);
  107. return false;
  108. }
  109. if (info.XClass != null)
  110. {
  111. if (typeToXamlIndex.ContainsKey(info.XClass))
  112. {
  113. BuildEngine.LogError(BuildEngineErrorCode.DuplicateXClass, s.SystemPath,
  114. $"Duplicate x:Class directive, {info.XClass} is already used in {typeToXamlIndex[info.XClass]}");
  115. return false;
  116. }
  117. typeToXamlIndex[info.XClass] = s.Path;
  118. }
  119. }
  120. }
  121. var xamlInfo = new AvaloniaResourceXamlInfo
  122. {
  123. ClassToResourcePathIndex = typeToXamlIndex
  124. };
  125. var ms = new MemoryStream();
  126. new DataContractSerializer(typeof(AvaloniaResourceXamlInfo)).WriteObject(ms, xamlInfo);
  127. sources.Add(new Source("/!AvaloniaResourceXamlInfo", ms.ToArray()));
  128. return true;
  129. }
  130. public bool Execute()
  131. {
  132. Enum.TryParse<MessageImportance>(ReportImportance, out _reportImportance);
  133. BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance);
  134. foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml") || r.ItemSpec.EndsWith(".axaml")))
  135. BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec,
  136. "XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work");
  137. var resources = BuildResourceSources();
  138. if (!PreProcessXamlFiles(resources))
  139. return false;
  140. var dir = Path.GetDirectoryName(Output);
  141. Directory.CreateDirectory(dir);
  142. using (var file = File.Create(Output))
  143. Pack(file, resources);
  144. return true;
  145. }
  146. public IBuildEngine BuildEngine { get; set; }
  147. public ITaskHost HostObject { get; set; }
  148. }
  149. }