diff options
author | Ankit Jain <radical@gmail.com> | 2021-09-15 16:16:48 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-15 16:16:48 +0300 |
commit | f38d58f1b1abc1b2c394481236f5cbcaaaf24545 (patch) | |
tree | 1491c953831e589a96d760d6854e993eb09dd9a8 /src/tasks | |
parent | e72aafc9b51cf2c0a59608a8a73bf1b9cffd8753 (diff) |
[release/6.0][wasm] Add support for native relinking after Build, and AOT after publish (#58913)
Diffstat (limited to 'src/tasks')
-rw-r--r-- | src/tasks/AotCompilerTask/MonoAOTCompiler.cs | 190 | ||||
-rw-r--r-- | src/tasks/Common/Utils.cs | 2 | ||||
-rw-r--r-- | src/tasks/WasmAppBuilder/EmccCompile.cs | 19 | ||||
-rw-r--r-- | src/tasks/WasmAppBuilder/IcallTableGenerator.cs | 4 | ||||
-rw-r--r-- | src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs | 42 | ||||
-rw-r--r-- | src/tasks/WasmAppBuilder/WasmAppBuilder.cs | 25 | ||||
-rw-r--r-- | src/tasks/WasmAppBuilder/WasmAppBuilder.csproj | 1 |
7 files changed, 203 insertions, 80 deletions
diff --git a/src/tasks/AotCompilerTask/MonoAOTCompiler.cs b/src/tasks/AotCompilerTask/MonoAOTCompiler.cs index 71ae231a0fb..4ee7389c04d 100644 --- a/src/tasks/AotCompilerTask/MonoAOTCompiler.cs +++ b/src/tasks/AotCompilerTask/MonoAOTCompiler.cs @@ -191,11 +191,15 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task /// </summary> public string? CacheFilePath { get; set; } + [Required] + public string IntermediateOutputPath { get; set; } = string.Empty; + [Output] public string[]? FileWrites { get; private set; } private List<string> _fileWrites = new(); + private IList<ITaskItem>? _assembliesToCompile; private ConcurrentDictionary<string, ITaskItem> compiledAssemblies = new(); private MonoAotMode parsedAotMode; @@ -207,7 +211,7 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task private int _numCompiled; private int _totalNumAssemblies; - public override bool Execute() + private bool ProcessAndValidateArguments() { if (!File.Exists(CompilerBinaryPath)) { @@ -230,6 +234,9 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task return false; } + if (!Directory.Exists(IntermediateOutputPath)) + Directory.CreateDirectory(IntermediateOutputPath); + if (AotProfilePath != null) { foreach (var path in AotProfilePath) @@ -246,7 +253,7 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task { if (string.IsNullOrEmpty(LLVMPath)) // prevent using some random llc/opt from PATH (installed with clang) - throw new ArgumentException($"'{nameof(LLVMPath)}' is required when '{nameof(UseLLVM)}' is true.", nameof(LLVMPath)); + throw new LogAsErrorException($"'{nameof(LLVMPath)}' is required when '{nameof(UseLLVM)}' is true."); if (!Directory.Exists(LLVMPath)) { @@ -270,7 +277,7 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task Log.LogWarning($"'{nameof(OutputType)}=Normal' is deprecated, use 'ObjectFile' instead."); parsedOutputType = MonoAotOutputType.ObjectFile; break; default: - throw new ArgumentException($"'{nameof(OutputType)}' must be one of: '{nameof(MonoAotOutputType.ObjectFile)}', '{nameof(MonoAotOutputType.AsmOnly)}', '{nameof(MonoAotOutputType.Library)}'. Received: '{OutputType}'.", nameof(OutputType)); + throw new LogAsErrorException($"'{nameof(OutputType)}' must be one of: '{nameof(MonoAotOutputType.ObjectFile)}', '{nameof(MonoAotOutputType.AsmOnly)}', '{nameof(MonoAotOutputType.Library)}'. Received: '{OutputType}'."); } switch (LibraryFormat) @@ -280,13 +287,13 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task case "So": parsedLibraryFormat = MonoAotLibraryFormat.So; break; default: if (parsedOutputType == MonoAotOutputType.Library) - throw new ArgumentException($"'{nameof(LibraryFormat)}' must be one of: '{nameof(MonoAotLibraryFormat.Dll)}', '{nameof(MonoAotLibraryFormat.Dylib)}', '{nameof(MonoAotLibraryFormat.So)}'. Received: '{LibraryFormat}'.", nameof(LibraryFormat)); + throw new LogAsErrorException($"'{nameof(LibraryFormat)}' must be one of: '{nameof(MonoAotLibraryFormat.Dll)}', '{nameof(MonoAotLibraryFormat.Dylib)}', '{nameof(MonoAotLibraryFormat.So)}'. Received: '{LibraryFormat}'."); break; } if (parsedAotMode == MonoAotMode.LLVMOnly && !UseLLVM) { - throw new ArgumentException($"'{nameof(UseLLVM)}' must be true when '{nameof(Mode)}' is {nameof(MonoAotMode.LLVMOnly)}.", nameof(UseLLVM)); + throw new LogAsErrorException($"'{nameof(UseLLVM)}' must be true when '{nameof(Mode)}' is {nameof(MonoAotMode.LLVMOnly)}."); } switch (AotModulesTableLanguage) @@ -294,32 +301,55 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task case "C": parsedAotModulesTableLanguage = MonoAotModulesTableLanguage.C; break; case "ObjC": parsedAotModulesTableLanguage = MonoAotModulesTableLanguage.ObjC; break; default: - throw new ArgumentException($"'{nameof(AotModulesTableLanguage)}' must be one of: '{nameof(MonoAotModulesTableLanguage.C)}', '{nameof(MonoAotModulesTableLanguage.ObjC)}'. Received: '{AotModulesTableLanguage}'.", nameof(AotModulesTableLanguage)); + throw new LogAsErrorException($"'{nameof(AotModulesTableLanguage)}' must be one of: '{nameof(MonoAotModulesTableLanguage.C)}', '{nameof(MonoAotModulesTableLanguage.ObjC)}'. Received: '{AotModulesTableLanguage}'."); } if (!string.IsNullOrEmpty(AotModulesTablePath)) { // AOT modules for static linking, needs the aot modules table UseStaticLinking = true; - - if (!GenerateAotModulesTable(Assemblies, Profilers, AotModulesTablePath)) - return false; } if (UseDirectIcalls && !UseStaticLinking) { - throw new ArgumentException($"'{nameof(UseDirectIcalls)}' can only be used with '{nameof(UseStaticLinking)}=true'.", nameof(UseDirectIcalls)); + throw new LogAsErrorException($"'{nameof(UseDirectIcalls)}' can only be used with '{nameof(UseStaticLinking)}=true'."); } if (UseDirectPInvoke && !UseStaticLinking) { - throw new ArgumentException($"'{nameof(UseDirectPInvoke)}' can only be used with '{nameof(UseStaticLinking)}=true'.", nameof(UseDirectPInvoke)); + throw new LogAsErrorException($"'{nameof(UseDirectPInvoke)}' can only be used with '{nameof(UseStaticLinking)}=true'."); } if (UseStaticLinking && (parsedOutputType == MonoAotOutputType.Library)) { - throw new ArgumentException($"'{nameof(OutputType)}=Library' can not be used with '{nameof(UseStaticLinking)}=true'.", nameof(OutputType)); + throw new LogAsErrorException($"'{nameof(OutputType)}=Library' can not be used with '{nameof(UseStaticLinking)}=true'."); + } + + return !Log.HasLoggedErrors; + } + + public override bool Execute() + { + try + { + return ExecuteInternal(); + } + catch (LogAsErrorException laee) + { + Log.LogError(laee.Message); + return false; } + } + + private bool ExecuteInternal() + { + if (!ProcessAndValidateArguments()) + return false; + + _assembliesToCompile = EnsureAndGetAssembliesInTheSameDir(Assemblies); + + if (!string.IsNullOrEmpty(AotModulesTablePath) && !GenerateAotModulesTable(_assembliesToCompile, Profilers, AotModulesTablePath)) + return false; string? monoPaths = null; if (AdditionalAssemblySearchPaths != null) @@ -329,14 +359,14 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task //FIXME: check the nothing changed at all case - _totalNumAssemblies = Assemblies.Length; - int allowedParallelism = Math.Min(Assemblies.Length, Environment.ProcessorCount); + _totalNumAssemblies = _assembliesToCompile.Count; + int allowedParallelism = Math.Min(_assembliesToCompile.Count, Environment.ProcessorCount); if (BuildEngine is IBuildEngine9 be9) allowedParallelism = be9.RequestCores(allowedParallelism); if (DisableParallelAot || allowedParallelism == 1) { - foreach (var assemblyItem in Assemblies) + foreach (var assemblyItem in _assembliesToCompile) { if (!PrecompileLibrarySerial(assemblyItem, monoPaths)) return !Log.HasLoggedErrors; @@ -345,15 +375,12 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task else { ParallelLoopResult result = Parallel.ForEach( - Assemblies, + _assembliesToCompile, new ParallelOptions { MaxDegreeOfParallelism = allowedParallelism }, (assemblyItem, state) => PrecompileLibraryParallel(assemblyItem, monoPaths, state)); if (!result.IsCompleted) { - if (!Log.HasLoggedErrors) - Log.LogError("Unknown failure occured while compiling"); - return false; } } @@ -365,15 +392,71 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task if (_cache.Save(CacheFilePath!)) _fileWrites.Add(CacheFilePath!); - CompiledAssemblies = ConvertAssembliesDictToOrderedList(compiledAssemblies, Assemblies).ToArray(); + CompiledAssemblies = ConvertAssembliesDictToOrderedList(compiledAssemblies, _assembliesToCompile).ToArray(); FileWrites = _fileWrites.ToArray(); return !Log.HasLoggedErrors; } + private IList<ITaskItem> EnsureAndGetAssembliesInTheSameDir(ITaskItem[] originalAssemblies) + { + List<ITaskItem> filteredAssemblies = new(); + string firstAsmDir = Path.GetDirectoryName(originalAssemblies[0].GetMetadata("FullPath")) ?? string.Empty; + bool allInSameDir = true; + + foreach (var origAsm in originalAssemblies) + { + if (allInSameDir && Path.GetDirectoryName(origAsm.GetMetadata("FullPath")) != firstAsmDir) + allInSameDir = false; + + if (ShouldSkip(origAsm)) + { + if (parsedAotMode == MonoAotMode.LLVMOnly) + throw new LogAsErrorException($"Building in AOTMode=LLVMonly is not compatible with excluding any assemblies for AOT. Excluded assembly: {origAsm.ItemSpec}"); + + Log.LogMessage(MessageImportance.Low, $"Skipping {origAsm.ItemSpec} because it has %(AOT_InternalForceToInterpret)=true"); + continue; + } + + filteredAssemblies.Add(origAsm); + } + + if (allInSameDir) + return filteredAssemblies; + + // Copy to aot-in + + string aotInPath = Path.Combine(IntermediateOutputPath, "aot-in"); + Directory.CreateDirectory(aotInPath); + + List<ITaskItem> newAssemblies = new(); + foreach (var origAsm in originalAssemblies) + { + string asmPath = origAsm.GetMetadata("FullPath"); + string newPath = Path.Combine(aotInPath, Path.GetFileName(asmPath)); + + // FIXME: delete files not in originalAssemblies though + // FIXME: or .. just delete the whole dir? + if (Utils.CopyIfDifferent(asmPath, newPath, useHash: true)) + Log.LogMessage(MessageImportance.Low, $"Copying {asmPath} to {newPath}"); + + if (!ShouldSkip(origAsm)) + { + ITaskItem newAsm = new TaskItem(newPath); + origAsm.CopyMetadataTo(newAsm); + newAssemblies.Add(newAsm); + } + } + + return newAssemblies; + + static bool ShouldSkip(ITaskItem asmItem) + => bool.TryParse(asmItem.GetMetadata("AOT_InternalForceToInterpret"), out bool skip) && skip; + } + private bool PrecompileLibrary(ITaskItem assemblyItem, string? monoPaths) { - string assembly = assemblyItem.ItemSpec; + string assembly = assemblyItem.GetMetadata("FullPath"); string assemblyDir = Path.GetDirectoryName(assembly)!; var aotAssembly = new TaskItem(assembly); var aotArgs = new List<string>(); @@ -567,38 +650,20 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task // values, which wont work. processArgs.Add($"\"--aot={string.Join(",", aotArgs)}\""); - string paths = ""; if (isDedup) { - StringBuilder sb = new StringBuilder(); - HashSet<string> allPaths = new HashSet<string>(); - foreach (var aItem in Assemblies) - { - string filename = aItem.ItemSpec; - processArgs.Add(filename); - string dir = Path.GetDirectoryName(filename)!; - if (!allPaths.Contains(dir)) - { - allPaths.Add(dir); - if (sb.Length > 0) - sb.Append(Path.PathSeparator); - sb.Append(dir); - } - } - if (sb.Length > 0) - sb.Append(Path.PathSeparator); - sb.Append(monoPaths); - paths = sb.ToString(); + foreach (var aItem in _assembliesToCompile!) + processArgs.Add(aItem.ItemSpec); } else { - paths = $"{assemblyDir}{Path.PathSeparator}{monoPaths}"; processArgs.Add('"' + assemblyFilename + '"'); } + monoPaths = $"{assemblyDir}{Path.PathSeparator}{monoPaths}"; var envVariables = new Dictionary<string, string> { - {"MONO_PATH", paths}, + {"MONO_PATH", monoPaths }, {"MONO_ENV_OPTIONS", string.Empty} // we do not want options to be provided out of band to the cross compilers }; @@ -713,7 +778,7 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task state.Break(); } - private bool GenerateAotModulesTable(ITaskItem[] assemblies, string[]? profilers, string outputFile) + private bool GenerateAotModulesTable(IEnumerable<ITaskItem> assemblies, string[]? profilers, string outputFile) { var symbols = new List<string>(); foreach (var asm in assemblies) @@ -831,12 +896,12 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task } } - private IList<ITaskItem> ConvertAssembliesDictToOrderedList(ConcurrentDictionary<string, ITaskItem> dict, ITaskItem[] items) + private static IList<ITaskItem> ConvertAssembliesDictToOrderedList(ConcurrentDictionary<string, ITaskItem> dict, IList<ITaskItem> originalAssemblies) { - List<ITaskItem> outItems = new(items.Length); - foreach (ITaskItem item in items) + List<ITaskItem> outItems = new(originalAssemblies.Count); + foreach (ITaskItem item in originalAssemblies) { - if (!dict.TryGetValue(item.ItemSpec, out ITaskItem? dictItem)) + if (!dict.TryGetValue(item.GetMetadata("FullPath"), out ITaskItem? dictItem)) throw new LogAsErrorException($"Bug: Could not find item in the dict with key {item.ItemSpec}"); outItems.Add(dictItem); @@ -931,23 +996,26 @@ internal class ProxyFile if (!_cache.Enabled) return true; - if (!File.Exists(TempFile)) - throw new LogAsErrorException($"Could not find output file {TempFile}"); - - if (!_cache.ShouldCopy(this, out string? cause)) + try { - _cache.Log.LogMessage(MessageImportance.Low, $"Skipping copying over {TargetFile} as the contents are unchanged"); - return false; - } + if (!_cache.ShouldCopy(this, out string? cause)) + { + _cache.Log.LogMessage(MessageImportance.Low, $"Skipping copying over {TargetFile} as the contents are unchanged"); + return false; + } - if (File.Exists(TargetFile)) - File.Delete(TargetFile); + if (File.Exists(TargetFile)) + File.Delete(TargetFile); - File.Copy(TempFile, TargetFile); - File.Delete(TempFile); + File.Copy(TempFile, TargetFile); - _cache.Log.LogMessage(MessageImportance.Low, $"Copying {TempFile} to {TargetFile} because {cause}"); - return true; + _cache.Log.LogMessage(MessageImportance.Low, $"Copying {TempFile} to {TargetFile} because {cause}"); + return true; + } + finally + { + File.Delete(TempFile); + } } } diff --git a/src/tasks/Common/Utils.cs b/src/tasks/Common/Utils.cs index 2c1a8b49370..2c624214937 100644 --- a/src/tasks/Common/Utils.cs +++ b/src/tasks/Common/Utils.cs @@ -212,7 +212,7 @@ internal static class Utils throw new ArgumentException($"Cannot find {src} file to copy", nameof(src)); bool areDifferent = !File.Exists(dst) || - (useHash && Utils.ComputeHash(src) != Utils.ComputeHash(dst)) || + (useHash && ComputeHash(src) != ComputeHash(dst)) || (File.ReadAllText(src) != File.ReadAllText(dst)); if (areDifferent) diff --git a/src/tasks/WasmAppBuilder/EmccCompile.cs b/src/tasks/WasmAppBuilder/EmccCompile.cs index e869e98e856..a5d6af6c3b4 100644 --- a/src/tasks/WasmAppBuilder/EmccCompile.cs +++ b/src/tasks/WasmAppBuilder/EmccCompile.cs @@ -45,6 +45,19 @@ namespace Microsoft.WebAssembly.Build.Tasks public override bool Execute() { + try + { + return ExecuteActual(); + } + catch (LogAsErrorException laee) + { + Log.LogError(laee.Message); + return false; + } + } + + private bool ExecuteActual() + { if (SourceFiles.Length == 0) { Log.LogError($"No SourceFiles to compile"); @@ -133,7 +146,7 @@ namespace Microsoft.WebAssembly.Build.Tasks }); if (!result.IsCompleted && !Log.HasLoggedErrors) - Log.LogError("Unknown failed occured while compiling"); + Log.LogError("Unknown failure occured while compiling. Check logs to get more details."); } if (!Log.HasLoggedErrors) @@ -211,7 +224,7 @@ namespace Microsoft.WebAssembly.Build.Tasks private bool ShouldCompile(string srcFile, string objFile, string[] depFiles, out string reason) { if (!File.Exists(srcFile)) - throw new ArgumentException($"Could not find source file {srcFile}"); + throw new LogAsErrorException($"Could not find source file {srcFile}"); if (!File.Exists(objFile)) { @@ -228,7 +241,7 @@ namespace Microsoft.WebAssembly.Build.Tasks return true; } - reason = "everything is up-to-date."; + reason = "everything is up-to-date"; return false; bool IsNewerThanOutput(string inFile, string outFile, out string reason) diff --git a/src/tasks/WasmAppBuilder/IcallTableGenerator.cs b/src/tasks/WasmAppBuilder/IcallTableGenerator.cs index 13c75c39bc6..cc4d8af1ac7 100644 --- a/src/tasks/WasmAppBuilder/IcallTableGenerator.cs +++ b/src/tasks/WasmAppBuilder/IcallTableGenerator.cs @@ -23,6 +23,9 @@ public class IcallTableGenerator : Task [Required, NotNull] public string? OutputPath { get; set; } + [Output] + public string? FileWrites { get; private set; } = ""; + private List<Icall> _icalls = new List<Icall> (); private Dictionary<string, IcallClass> _runtimeIcalls = new Dictionary<string, IcallClass> (); @@ -58,6 +61,7 @@ public class IcallTableGenerator : Task Log.LogMessage(MessageImportance.Low, $"Generating icall table to '{OutputPath}'."); else Log.LogMessage(MessageImportance.Low, $"Icall table in {OutputPath} is unchanged."); + FileWrites = OutputPath; File.Delete(tmpFileName); } diff --git a/src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs b/src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs index f6b89533f98..825de67d6e2 100644 --- a/src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs +++ b/src/tasks/WasmAppBuilder/PInvokeTableGenerator.cs @@ -16,20 +16,43 @@ using Microsoft.Build.Utilities; public class PInvokeTableGenerator : Task { - [Required] - public ITaskItem[]? Modules { get; set; } - [Required] - public ITaskItem[]? Assemblies { get; set; } + [Required, NotNull] + public string[]? Modules { get; set; } + [Required, NotNull] + public string[]? Assemblies { get; set; } [Required, NotNull] public string? OutputPath { get; set; } + [Output] + public string FileWrites { get; private set; } = string.Empty; + private static char[] s_charsToReplace = new[] { '.', '-', }; public override bool Execute() { - GenPInvokeTable(Modules!.Select(item => item.ItemSpec).ToArray(), Assemblies!.Select(item => item.ItemSpec).ToArray()); - return true; + if (Assemblies.Length == 0) + { + Log.LogError($"No assemblies given to scan for pinvokes"); + return false; + } + + if (Modules.Length == 0) + { + Log.LogError($"{nameof(PInvokeTableGenerator)}.{nameof(Modules)} cannot be empty"); + return false; + } + + try + { + GenPInvokeTable(Modules, Assemblies); + return !Log.HasLoggedErrors; + } + catch (LogAsErrorException laee) + { + Log.LogError(laee.Message); + return false; + } } public void GenPInvokeTable(string[] pinvokeModules, string[] assemblies) @@ -61,6 +84,7 @@ public class PInvokeTableGenerator : Task Log.LogMessage(MessageImportance.Low, $"Generating pinvoke table to '{OutputPath}'."); else Log.LogMessage(MessageImportance.Low, $"PInvoke table in {OutputPath} is unchanged."); + FileWrites = OutputPath; File.Delete(tmpFileName); } @@ -339,11 +363,7 @@ public class PInvokeTableGenerator : Task return false; } - private static void Error (string msg) - { - // FIXME: - throw new Exception(msg); - } + private static void Error (string msg) => throw new LogAsErrorException(msg); } internal class PInvoke diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index 57cb7c383f1..58b4b9259e6 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -118,10 +118,23 @@ public class WasmAppBuilder : Task public override bool Execute () { + try + { + return ExecuteInternal(); + } + catch (LogAsErrorException laee) + { + Log.LogError(laee.Message); + return false; + } + } + + private bool ExecuteInternal () + { if (!File.Exists(MainJS)) - throw new ArgumentException($"File MainJS='{MainJS}' doesn't exist."); + throw new LogAsErrorException($"File MainJS='{MainJS}' doesn't exist."); if (!InvariantGlobalization && string.IsNullOrEmpty(IcuDataFileName)) - throw new ArgumentException("IcuDataFileName property shouldn't be empty if InvariantGlobalization=false"); + throw new LogAsErrorException("IcuDataFileName property shouldn't be empty if InvariantGlobalization=false"); if (Assemblies?.Length == 0) { @@ -162,8 +175,12 @@ public class WasmAppBuilder : Task } FileCopyChecked(MainJS!, Path.Combine(AppDir, "runtime.js"), string.Empty); - var html = @"<html><body><script type=""text/javascript"" src=""runtime.js""></script></body></html>"; - File.WriteAllText(Path.Combine(AppDir, "index.html"), html); + string indexHtmlPath = Path.Combine(AppDir, "index.html"); + if (!File.Exists(indexHtmlPath)) + { + var html = @"<html><body><script type=""text/javascript"" src=""runtime.js""></script></body></html>"; + File.WriteAllText(indexHtmlPath, html); + } foreach (var assembly in _assemblies) { diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj b/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj index d7cb1b1f5b7..a487a4f270a 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj @@ -15,6 +15,7 @@ <ItemGroup> <Compile Include="..\Common\Utils.cs" /> + <Compile Include="..\Common\LogAsErrorException.cs" /> <PackageReference Include="Microsoft.Build" Version="$(RefOnlyMicrosoftBuildVersion)" /> <PackageReference Include="Microsoft.Build.Framework" Version="$(RefOnlyMicrosoftBuildFrameworkVersion)" /> |