RefAssemblyGenerator.cs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.IO.Compression;
  6. using System.Linq;
  7. using Mono.Cecil;
  8. using Mono.Cecil.Cil;
  9. public class RefAssemblyGenerator
  10. {
  11. class Resolver : DefaultAssemblyResolver, IAssemblyResolver
  12. {
  13. readonly string _dir;
  14. readonly Dictionary<string, AssemblyDefinition> _cache = new();
  15. public Resolver(string dir)
  16. {
  17. _dir = dir;
  18. }
  19. public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
  20. {
  21. if (_cache.TryGetValue(name.Name, out var asm))
  22. return asm;
  23. var path = Path.Combine(_dir, name.Name + ".dll");
  24. if (File.Exists(path))
  25. return _cache[name.Name] = AssemblyDefinition.ReadAssembly(path, parameters);
  26. return base.Resolve(name, parameters);
  27. }
  28. }
  29. public static void PatchRefAssembly(string file)
  30. {
  31. var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk")!;
  32. var snk = new byte[reader.Length];
  33. reader.ReadExactly(snk, 0, snk.Length);
  34. var def = AssemblyDefinition.ReadAssembly(file, new ReaderParameters
  35. {
  36. ReadWrite = true,
  37. InMemory = true,
  38. ReadSymbols = true,
  39. SymbolReaderProvider = new DefaultSymbolReaderProvider(throwIfNoSymbol: true),
  40. AssemblyResolver = new Resolver(Path.GetDirectoryName(file)!)
  41. });
  42. var obsoleteAttribute = def.MainModule.ImportReference(new TypeReference("System", "ObsoleteAttribute", def.MainModule,
  43. def.MainModule.TypeSystem.CoreLibrary));
  44. var obsoleteCtor = def.MainModule.ImportReference(new MethodReference(".ctor",
  45. def.MainModule.TypeSystem.Void, obsoleteAttribute)
  46. {
  47. Parameters = { new ParameterDefinition(def.MainModule.TypeSystem.String) }
  48. });
  49. foreach(var t in def.MainModule.Types)
  50. ProcessType(t, obsoleteCtor);
  51. def.Write(file, new WriterParameters()
  52. {
  53. StrongNameKeyBlob = snk,
  54. WriteSymbols = def.MainModule.HasSymbols,
  55. SymbolWriterProvider = new PortablePdbWriterProvider(),
  56. DeterministicMvid = def.MainModule.HasSymbols
  57. });
  58. }
  59. static bool HasPrivateApi(IEnumerable<CustomAttribute> attrs) => attrs.Any(a =>
  60. a.AttributeType.FullName == "Avalonia.Metadata.PrivateApiAttribute");
  61. static void ProcessType(TypeDefinition type, MethodReference obsoleteCtor)
  62. {
  63. var hideMembers = (type.IsInterface && type.Name.EndsWith("Impl"))
  64. || HasPrivateApi(type.CustomAttributes);
  65. var injectMethod = hideMembers
  66. || type.CustomAttributes.Any(a =>
  67. a.AttributeType.FullName == "Avalonia.Metadata.NotClientImplementableAttribute");
  68. if (injectMethod)
  69. {
  70. type.Methods.Add(new MethodDefinition(
  71. "(This interface or abstract class is -not- implementable by user code !)",
  72. MethodAttributes.Assembly
  73. | MethodAttributes.Virtual
  74. | MethodAttributes.Abstract
  75. | MethodAttributes.NewSlot
  76. | MethodAttributes.HideBySig, type.Module.TypeSystem.Void));
  77. }
  78. var forceUnstable = type.CustomAttributes.FirstOrDefault(a =>
  79. a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute");
  80. foreach (var m in type.Methods)
  81. {
  82. if (!m.IsPrivate && (hideMembers || HasPrivateApi(m.CustomAttributes)))
  83. {
  84. HideMethod(m);
  85. }
  86. MarkAsUnstable(m, obsoleteCtor, forceUnstable);
  87. }
  88. foreach (var p in type.Properties)
  89. {
  90. if (HasPrivateApi(p.CustomAttributes))
  91. {
  92. if (p.SetMethod is { IsPrivate: false } setMethod)
  93. HideMethod(setMethod);
  94. if (p.GetMethod is { IsPrivate: false } getMethod)
  95. HideMethod(getMethod);
  96. }
  97. }
  98. foreach (var f in type.Fields)
  99. {
  100. if (!f.IsPrivate && (hideMembers || HasPrivateApi(f.CustomAttributes)))
  101. {
  102. f.IsAssembly = true;
  103. }
  104. }
  105. foreach (var cl in type.NestedTypes)
  106. {
  107. ProcessType(cl, obsoleteCtor);
  108. if (hideMembers && cl.IsNestedPublic)
  109. {
  110. cl.IsNestedAssembly = true;
  111. }
  112. }
  113. foreach (var m in type.Properties)
  114. MarkAsUnstable(m, obsoleteCtor, forceUnstable);
  115. foreach (var m in type.Events)
  116. MarkAsUnstable(m, obsoleteCtor, forceUnstable);
  117. }
  118. static void HideMethod(MethodDefinition m)
  119. {
  120. m.IsAssembly = true;
  121. }
  122. static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute? unstableAttribute)
  123. {
  124. if (def.CustomAttributes.Any(a => a.AttributeType.FullName == "System.ObsoleteAttribute"))
  125. return;
  126. unstableAttribute = def.CustomAttributes.FirstOrDefault(a =>
  127. a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute") ?? unstableAttribute;
  128. if (unstableAttribute is null)
  129. return;
  130. var message = unstableAttribute.ConstructorArguments.FirstOrDefault().Value?.ToString();
  131. if (string.IsNullOrEmpty(message))
  132. {
  133. message = "This is a part of unstable API and can be changed in minor releases. Consider replacing it with alternatives or reach out developers on GitHub.";
  134. }
  135. def.CustomAttributes.Add(new CustomAttribute(obsoleteCtor)
  136. {
  137. ConstructorArguments =
  138. {
  139. new CustomAttributeArgument(obsoleteCtor.Module.TypeSystem.String, message)
  140. }
  141. });
  142. }
  143. public static void GenerateRefAsmsInPackage(string mainPackagePath, string symbolsPackagePath)
  144. {
  145. using var mainArchive = OpenPackage(mainPackagePath);
  146. using var symbolsArchive = OpenPackage(symbolsPackagePath);
  147. foreach (var entry in mainArchive.Entries
  148. .Where(e => e.FullName.StartsWith("ref/", StringComparison.Ordinal))
  149. .ToArray())
  150. {
  151. entry.Delete();
  152. }
  153. foreach (var libEntry in GetLibEntries(mainArchive, ".xml"))
  154. {
  155. var refEntry = mainArchive.CreateEntry("ref/" + libEntry.FullName.Substring(4), CompressionLevel.Optimal);
  156. using var src = libEntry.Open();
  157. using var dst = refEntry.Open();
  158. src.CopyTo(dst);
  159. }
  160. var pdbEntries = GetLibEntries(symbolsArchive, ".pdb").ToDictionary(e => e.FullName);
  161. var libs = GetLibEntries(mainArchive, ".dll")
  162. .Select(e => (NameParts: e.FullName.Split('/'), Entry: e))
  163. .Select(e => (
  164. Tfm: e.NameParts[1],
  165. DllName: e.NameParts[2],
  166. DllEntry: e.Entry,
  167. PdbName: Path.ChangeExtension(e.NameParts[2], ".pdb"),
  168. PdbEntry: pdbEntries.TryGetValue(Path.ChangeExtension(e.Entry.FullName, ".pdb"), out var pdbEntry) ?
  169. pdbEntry :
  170. throw new InvalidOperationException($"Missing symbols for {e.Entry.FullName}")))
  171. .GroupBy(e => e.Tfm);
  172. foreach (var tfm in libs)
  173. {
  174. using var _ = Helpers.UseTempDir(out var temp);
  175. foreach (var lib in tfm)
  176. {
  177. var extractedDllPath = Path.Combine(temp, lib.DllName);
  178. var extractedPdbPath = Path.Combine(temp, lib.PdbName);
  179. lib.DllEntry.ExtractToFile(extractedDllPath);
  180. lib.PdbEntry.ExtractToFile(extractedPdbPath);
  181. PatchRefAssembly(extractedDllPath);
  182. mainArchive.CreateEntryFromFile(extractedDllPath, $"ref/{lib.Tfm}/{lib.DllName}");
  183. symbolsArchive.CreateEntryFromFile(extractedPdbPath, $"ref/{lib.Tfm}/{lib.PdbName}");
  184. }
  185. }
  186. static ZipArchive OpenPackage(string packagePath)
  187. => new(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), ZipArchiveMode.Update);
  188. static ZipArchiveEntry[] GetLibEntries(ZipArchive archive, string extension)
  189. => archive.Entries
  190. .Where(e => e.FullName.StartsWith("lib/", StringComparison.Ordinal)
  191. && e.FullName.EndsWith(extension, StringComparison.Ordinal))
  192. .ToArray();
  193. }
  194. }