using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Mono.Cecil; using Mono.Linker; using Mono.Linker.Steps; namespace Ooui.Wasm.Build.Tasks { public class BuildDistTask : Task { const string SdkUrl = "https://xamjenkinsartifact.azureedge.net/test-mono-mainline-wasm/916/ubuntu-1804-amd64/sdks/wasm/mono-wasm-f25f9e5f2b5.zip"; const string AssemblyExtension = ".bin"; [Required] public string Assembly { get; set; } [Required] public string OutputPath { get; set; } public string ReferencePath { get; set; } bool ok = false; public override bool Execute () { try { ok = true; InstallSdk (); GetBcl (); CreateDist (); DeleteOldAssemblies (); CopyRuntime (); LinkAssemblies (); RenameAssemblies (); ExtractClientJs (); DiscoverEntryPoint (); GenerateHtml (); return ok; } catch (Exception ex) { //Console.WriteLine (ex); Log.LogErrorFromException (ex); return false; } } string sdkPath; void InstallSdk () { var sdkName = Path.GetFileNameWithoutExtension (new Uri (SdkUrl).AbsolutePath.Replace ('/', Path.DirectorySeparatorChar)); Log.LogMessage ("SDK: " + sdkName); string tmpDir = Path.GetTempPath (); sdkPath = Path.Combine (tmpDir, sdkName); Log.LogMessage ("SDK Path: " + sdkPath); if (Directory.Exists (sdkPath) && Directory.Exists (Path.Combine (sdkPath, "release"))) return; var client = new WebClient (); var zipPath = sdkPath + ".zip"; Log.LogMessage ($"Downloading {sdkName} to {zipPath}"); if (File.Exists (zipPath)) File.Delete (zipPath); client.DownloadFile (SdkUrl, zipPath); var sdkTempPath = Path.Combine (tmpDir, Guid.NewGuid ().ToString ()); ZipFile.ExtractToDirectory (zipPath, sdkTempPath); if (Directory.Exists (sdkPath)) Directory.Delete (sdkPath, true); Directory.Move (sdkTempPath, sdkPath); Log.LogMessage ($"Extracted {sdkName} to {sdkPath}"); } string bclPath; Dictionary bclAssemblies; void GetBcl () { bclPath = Path.Combine (sdkPath, "bcl"); var reals = Directory.GetFiles (bclPath, "*.dll"); var facades = Directory.GetFiles (Path.Combine (bclPath, "Facades"), "*.dll"); var allFiles = reals.Concat (facades); bclAssemblies = allFiles.ToDictionary (x => Path.GetFileName (x)); } string distPath; string managedPath; void CreateDist () { var outputPath = Path.GetFullPath (OutputPath); distPath = Path.Combine (outputPath, "dist"); managedPath = Path.Combine (distPath, "managed"); Directory.CreateDirectory (managedPath); } void DeleteOldAssemblies () { foreach (var dll in Directory.GetFiles (managedPath, "*.dll")) { File.Delete (dll); } foreach (var dll in Directory.GetFiles (managedPath, "*" + AssemblyExtension)) { File.Delete (dll); } } void CopyRuntime () { var rtPath = Path.Combine (sdkPath, "release"); var files = new[] { "mono.wasm", "mono.js" }; foreach (var f in files) { var src = Path.Combine (rtPath, f); var dest = Path.Combine (distPath, f); Log.LogMessage ($"Runtime {src} -> {dest}"); File.Copy (src, dest, true); } File.Copy (Path.Combine (sdkPath, "server.py"), Path.Combine (distPath, "server.py"), true); } List linkedAsmPaths; List refpaths; void LinkAssemblies () { var references = ReferencePath.Split (';').Select (x => x.Trim ()).Where (x => x.Length > 0).ToList (); refpaths = new List (); foreach (var r in references) { var name = Path.GetFileName (r); if (bclAssemblies.ContainsKey (name)) { refpaths.Add (bclAssemblies[name]); //Console.WriteLine ($"+ {name}"); } else { refpaths.Add (r); //Console.WriteLine ($"- {r}"); } } var asmPath = Path.GetFullPath (Assembly); var pipeline = GetLinkerPipeline (); var resolver = new LinkerAssemblyResolver (this); var asmParameters = new ReaderParameters { AssemblyResolver = resolver, MetadataResolver = new LinkerMetadataResolver (resolver) }; using (var context = new LinkContext (pipeline, resolver, asmParameters, new UnintializedContextFactory ())) { context.CoreAction = AssemblyAction.CopyUsed; context.UserAction = AssemblyAction.CopyUsed; context.OutputDirectory = managedPath; context.IgnoreUnresolved = false; pipeline.PrependStep (new ResolveFromAssemblyStep (asmPath, ResolveFromAssemblyStep.RootVisibility.Any)); var refdirs = refpaths.Select (x => Path.GetDirectoryName (x)).Distinct ().ToList (); refdirs.Insert (0, Path.Combine (bclPath, "Facades")); refdirs.Insert (0, bclPath); foreach (var d in refdirs.Distinct ()) { context.Resolver.AddSearchDirectory (d); } pipeline.AddStepAfter (typeof (LoadReferencesStep), new LoadI18nAssemblies (I18nAssemblies.None)); DeleteOldAssemblies (); pipeline.Process (context); } linkedAsmPaths = Directory.GetFiles (managedPath, "*.dll").OrderBy (x => Path.GetFileName (x)).ToList (); } class PreserveUsingAttributesStep : ResolveStep { readonly HashSet ignoreAsmNames; public PreserveUsingAttributesStep (IEnumerable ignoreAsmNames) { this.ignoreAsmNames = new HashSet (ignoreAsmNames); } protected override void Process () { var asms = Context.GetAssemblies (); foreach (var a in asms.Where (x => !ignoreAsmNames.Contains (x.Name.Name))) { foreach (var m in a.Modules) { foreach (var t in m.Types) { PreserveTypeIfRequested (t); } } } } void PreserveTypeIfRequested (TypeDefinition type) { var typePreserved = IsTypePreserved (type); if (IsTypePreserved (type)) { MarkAndPreserveAll (type); } else { foreach (var m in type.Methods.Where (IsMethodPreserved)) { Annotations.AddPreservedMethod (type, m); } foreach (var t in type.NestedTypes) { PreserveTypeIfRequested (t); } } } static bool IsTypePreserved (TypeDefinition m) { return m.CustomAttributes.FirstOrDefault (x => x.AttributeType.Name.StartsWith ("Preserve", StringComparison.Ordinal)) != null; } static bool IsMethodPreserved (MethodDefinition m) { return m.CustomAttributes.FirstOrDefault (x => x.AttributeType.Name.StartsWith ("Preserve", StringComparison.Ordinal)) != null; } void MarkAndPreserveAll (TypeDefinition type) { Annotations.MarkAndPush (type); Annotations.SetPreserve (type, TypePreserve.All); if (!type.HasNestedTypes) { Tracer.Pop (); return; } foreach (TypeDefinition nested in type.NestedTypes) MarkAndPreserveAll (nested); Tracer.Pop (); } } Pipeline GetLinkerPipeline () { IEnumerable bclNames = bclAssemblies.Values.Select (Path.GetFileNameWithoutExtension); var p = new Pipeline (); p.AppendStep (new DontLinkExeStep ()); p.AppendStep (new LoadReferencesStep ()); p.AppendStep (new PreserveUsingAttributesStep (bclNames)); p.AppendStep (new BlacklistStep ()); p.AppendStep (new LinkBclStep (bclNames)); p.AppendStep (new TypeMapStep ()); p.AppendStep (new MarkStepWithUnresolvedLogging (this)); p.AppendStep (new SweepStep ()); p.AppendStep (new CleanStep ()); p.AppendStep (new RegenerateGuidStep ()); p.AppendStep (new OutputStep ()); return p; } class DontLinkExeStep : BaseStep { protected override void Process () { foreach (var a in Context.GetAssemblies ()) { Annotations.SetAction (a, AssemblyAction.Copy); } } } class LinkBclStep : BaseStep { HashSet bclNames; List> preserveTypeNames; public LinkBclStep (IEnumerable bclNames) { // CSLA cannot tolerate mscorlib being linked (uses reflection over types it doesn't reference) this.bclNames = new HashSet (bclNames.Where(x => x != "mscorlib")); preserveTypeNames = new List> { Tuple.Create ("System", "System.ComponentModel.IEditableObject"), Tuple.Create ("System", "System.ComponentModel.IDataErrorInfo"), }; } protected override void Process () { var asms = Context.GetAssemblies (); foreach (var a in asms) { if (bclNames.Contains (a.Name.Name)) { Annotations.SetAction (a, AssemblyAction.Link); } } foreach (var p in preserveTypeNames) { var asm = asms.FirstOrDefault (x => x.Name.Name == p.Item1); if (asm == null) throw new Exception ($"Could not find assembly {p.Item1}"); var t = asm.MainModule.GetType (p.Item2); if (t == null) throw new Exception ($"Could not find type {p.Item2} in {p.Item1}"); Annotations.SetPreserve (t, TypePreserve.All); } //foreach (var a in asms) { // var act = Annotations.GetAction (a); // Console.WriteLine ($"{act} {a.Name.Name}"); //} } } class MarkStepWithUnresolvedLogging : MarkStep { BuildDistTask task; public MarkStepWithUnresolvedLogging (BuildDistTask task) { this.task = task; } protected override void HandleUnresolvedType (TypeReference reference) { task.ok = false; task.Log.LogError ($"Linker failed to resolve type {reference} in {reference.Scope}"); } protected override void HandleUnresolvedMethod (MethodReference reference) { task.ok = false; task.Log.LogError ($"Linker failed to resolve method {reference}"); } } void RenameAssemblies () { for (int i = 0; i < linkedAsmPaths.Count; i++) { var path = linkedAsmPaths[i]; var newPath = Path.ChangeExtension(path, AssemblyExtension); File.Move (path, newPath); linkedAsmPaths[i] = newPath; } } void ExtractClientJs () { var oouiPath = refpaths.FirstOrDefault (x => Path.GetFileName (x).Equals ("Ooui.dll", StringComparison.InvariantCultureIgnoreCase)); if (oouiPath == null) { Log.LogError ("Ooui.dll not included in the project"); return; } var oouiAsm = AssemblyDefinition.ReadAssembly (oouiPath); var clientJs = oouiAsm.MainModule.Resources.FirstOrDefault (x => x.Name.EndsWith ("Client.js", StringComparison.InvariantCultureIgnoreCase)) as EmbeddedResource; if (clientJs == null) { Log.LogError ("Ooui.dll missing client javascript"); return; } var dest = Path.Combine (distPath, "ooui.js"); using (var srcs = clientJs.GetResourceStream ()) { using (var dests = new FileStream (dest, FileMode.Create, FileAccess.Write)) { srcs.CopyTo (dests); } } Log.LogMessage ($"Client JS {dest}"); } MethodDefinition entryPoint; void DiscoverEntryPoint () { var asm = AssemblyDefinition.ReadAssembly (Assembly); entryPoint = asm.EntryPoint; if (entryPoint == null) { throw new Exception ($"{Path.GetFileName (Assembly)} is missing an entry point"); } } void GenerateHtml () { var htmlPath = Path.Combine (distPath, "index.html"); using (var w = new StreamWriter (htmlPath, false, new UTF8Encoding (false))) { w.Write (@"

Loading...

"); } Log.LogMessage ($"HTML {htmlPath}"); } class LinkerAssemblyResolver : Mono.Linker.AssemblyResolver { BuildDistTask task; public LinkerAssemblyResolver (BuildDistTask buildDistTask) { task = buildDistTask; } public override AssemblyDefinition Resolve (AssemblyNameReference name, ReaderParameters parameters) { AssemblyDefinition asm = null; if (!AssemblyCache.TryGetValue (name.Name, out asm)) { var path = task.refpaths.FirstOrDefault (x => { var rname = Path.GetFileNameWithoutExtension (x); var eq = rname.Equals (name.Name, StringComparison.InvariantCultureIgnoreCase); return eq; }); if (path != null) { //Console.WriteLine ($"SUCCESS {path}"); asm = ModuleDefinition.ReadModule (path, parameters).Assembly; CacheAssembly (asm); } return base.Resolve (name, parameters); } return asm; } } class LinkerMetadataResolver : MetadataResolver { readonly AssemblyNameReference mscorlibScope = new AssemblyNameReference ("mscorlib", new Version (1, 0)); public LinkerMetadataResolver (LinkerAssemblyResolver asmResolver) : base (asmResolver) { } public override TypeDefinition Resolve (TypeReference type) { var def = base.Resolve (type); if (def != null) return def; var scope = type.Scope; if (scope == null) return null; switch (scope.MetadataScopeType) { case MetadataScopeType.AssemblyNameReference: { AssemblyNameReference asmRef = (AssemblyNameReference)scope; if (asmRef.Name == "System.Runtime") { return base.Resolve (new TypeReference (type.Namespace, type.Name, type.Module, mscorlibScope)); } } break; } return def; } } } } namespace Mono.Linker { [Flags] public enum CodeOptimizations { BeforeFieldInit = 1 << 0, } }