374 lines
14 KiB
C#
374 lines
14 KiB
C#
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-webassembly/108/highsierra/sdks/wasm/mono-wasm-a14f41ca260.zip";
|
|
|
|
[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 ();
|
|
CopyRuntime ();
|
|
LinkAssemblies ();
|
|
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<string, string> 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 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<string> linkedAsmPaths;
|
|
List<string> refpaths;
|
|
|
|
void LinkAssemblies ()
|
|
{
|
|
var references = ReferencePath.Split (';').Select (x => x.Trim ()).Where (x => x.Length > 0).ToList ();
|
|
refpaths = new List<string> ();
|
|
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 AsmResolver (this);
|
|
using (var context = new LinkContext (pipeline, resolver)) {
|
|
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));
|
|
|
|
foreach (var dll in Directory.GetFiles (managedPath, "*.dll")) {
|
|
File.Delete (dll);
|
|
}
|
|
pipeline.Process (context);
|
|
}
|
|
|
|
linkedAsmPaths = Directory.GetFiles (managedPath, "*.dll").OrderBy (x => Path.GetFileName (x)).ToList ();
|
|
}
|
|
|
|
class PreserveUsingAttributesStep : ResolveStep
|
|
{
|
|
readonly HashSet<string> ignoreAsmNames;
|
|
|
|
public PreserveUsingAttributesStep (IEnumerable<string> ignoreAsmNames)
|
|
{
|
|
this.ignoreAsmNames = new HashSet<string> (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 ()
|
|
{
|
|
var p = new Pipeline ();
|
|
p.AppendStep (new DontLinkExeStep ());
|
|
p.AppendStep (new LoadReferencesStep ());
|
|
p.AppendStep (new PreserveUsingAttributesStep (bclAssemblies.Values.Select (Path.GetFileNameWithoutExtension)));
|
|
p.AppendStep (new BlacklistStep ());
|
|
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 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 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 (@"<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset=""utf-8"" />
|
|
<meta name=""viewport"" content=""width=device-width, initial-scale=1"" />
|
|
<link rel=""stylesheet"" href=""https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"" />
|
|
<link rel=""stylesheet"" href=""https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"">
|
|
</head>
|
|
<body>
|
|
<div id=""ooui-body"" class=""container-fluid"">
|
|
<p id=""loading""><i class=""fa fa-refresh fa-spin"" style=""font-size:14px;margin-right:0.5em;""></i> Loading...</p>
|
|
</div>
|
|
<script defer type=""text/javascript"" src=""ooui.js""></script>
|
|
<script type=""text/javascript"">
|
|
var assemblies = [");
|
|
var head = "";
|
|
foreach (var l in linkedAsmPaths.Select (x => Path.GetFileName (x))) {
|
|
w.Write (head);
|
|
w.Write ('\"');
|
|
w.Write (l);
|
|
w.Write ('\"');
|
|
head = ",";
|
|
}
|
|
w.WriteLine ($@"];
|
|
document.addEventListener(""DOMContentLoaded"", function(event) {{
|
|
oouiWasm(""{entryPoint.DeclaringType.Module.Assembly.Name.Name}"", ""{entryPoint.DeclaringType.Namespace}"", ""{entryPoint.DeclaringType.Name}"", ""{entryPoint.Name}"", assemblies);
|
|
}});
|
|
</script>
|
|
<script defer type=""text/javascript"" src=""mono.js""></script>
|
|
</body>
|
|
</html>");
|
|
}
|
|
Log.LogMessage ($"HTML {htmlPath}");
|
|
}
|
|
|
|
class AsmResolver : Mono.Linker.AssemblyResolver
|
|
{
|
|
BuildDistTask task;
|
|
|
|
public AsmResolver (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;
|
|
}
|
|
}
|
|
}
|
|
}
|