From e54a6c0a6cf3bfa96449303c74c985f27877d974 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 23 Feb 2023 11:43:56 -0500 Subject: [PATCH 01/11] EmitWasmBundleObjectFile: Capture any clang output/errors --- .../wasi/EmitWasmBundleObjectFile.cs | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/tasks/WasmAppBuilder/wasi/EmitWasmBundleObjectFile.cs b/src/tasks/WasmAppBuilder/wasi/EmitWasmBundleObjectFile.cs index 4a7e1009dd72d..2695e526e211e 100644 --- a/src/tasks/WasmAppBuilder/wasi/EmitWasmBundleObjectFile.cs +++ b/src/tasks/WasmAppBuilder/wasi/EmitWasmBundleObjectFile.cs @@ -85,7 +85,7 @@ public override bool Execute() if (BuildEngine is IBuildEngine9 be9) allowedParallelism = be9.RequestCores(allowedParallelism); - Parallel.For(0, remainingObjectFilesToBundle.Length, new ParallelOptions { MaxDegreeOfParallelism = allowedParallelism, CancellationToken = BuildTaskCancelled.Token }, i => + Parallel.For(0, remainingObjectFilesToBundle.Length, new ParallelOptions { MaxDegreeOfParallelism = allowedParallelism, CancellationToken = BuildTaskCancelled.Token }, (i, state) => { var objectFile = remainingObjectFilesToBundle[i]; @@ -100,7 +100,8 @@ public override bool Execute() Log.LogMessage(MessageImportance.High, "{0}/{1} Bundling {2}...", count, remainingObjectFilesToBundle.Length, Path.GetFileName(contentSourceFile.ItemSpec)); } - EmitObjectFile(contentSourceFile, outputFile); + if (!EmitObjectFile(contentSourceFile, outputFile)) + state.Stop(); }); } @@ -109,23 +110,55 @@ public override bool Execute() return !Log.HasLoggedErrors; } - private void EmitObjectFile(ITaskItem fileToBundle, string destinationObjectFile) + private bool EmitObjectFile(ITaskItem fileToBundle, string destinationObjectFile) { Log.LogMessage(MessageImportance.Low, "Bundling {0} as {1}", fileToBundle.ItemSpec, destinationObjectFile); if (Path.GetDirectoryName(destinationObjectFile) is string destDir && !string.IsNullOrEmpty(destDir)) Directory.CreateDirectory(destDir); + object syncObj = new(); + StringBuilder outputBuilder = new(); var clangProcess = Process.Start(new ProcessStartInfo { FileName = ClangExecutable, Arguments = $"-xc -o \"{destinationObjectFile}\" -c -", RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, UseShellExecute = false, })!; - BundleFileToCSource(destinationObjectFile, fileToBundle, clangProcess.StandardInput.BaseStream); - clangProcess.WaitForExit(); + clangProcess.ErrorDataReceived += (sender, e) => + { + lock (syncObj) + { + if (!string.IsNullOrEmpty(e.Data)) + outputBuilder.AppendLine(e.Data); + } + }; + clangProcess.OutputDataReceived += (sender, e) => + { + lock (syncObj) + { + if (!string.IsNullOrEmpty(e.Data)) + outputBuilder.AppendLine(e.Data); + } + }; + clangProcess.BeginOutputReadLine(); + clangProcess.BeginErrorReadLine(); + + try + { + BundleFileToCSource(destinationObjectFile, fileToBundle, clangProcess.StandardInput.BaseStream); + clangProcess.WaitForExit(); + return true; + } + catch (IOException ioex) + { + Log.LogError($"Failed to compile because {ioex.Message}{Environment.NewLine}Output: {outputBuilder}"); + return false; + } } private static string GetBundleFileApiSource(ICollection> bundledFilesByObjectFileName) From 2b7983631e809eb6c738ac0209de3d3f4e82df32 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 23 Feb 2023 11:46:46 -0500 Subject: [PATCH 02/11] [wasi] Add wasiconsole template --- .../.template.config/template.json | 34 +++++++++++++++++++ .../templates/wasi-console/Program.cs | 3 ++ .../wasi-console/Properties/AssemblyInfo.cs | 4 +++ .../templates/wasi-console/README.md | 25 ++++++++++++++ .../wasi-console/runtimeconfig.template.json | 10 ++++++ .../wasi-console/wasi-console.0.csproj | 8 +++++ 6 files changed, 84 insertions(+) create mode 100644 src/mono/wasm/templates/templates/wasi-console/.template.config/template.json create mode 100644 src/mono/wasm/templates/templates/wasi-console/Program.cs create mode 100644 src/mono/wasm/templates/templates/wasi-console/Properties/AssemblyInfo.cs create mode 100644 src/mono/wasm/templates/templates/wasi-console/README.md create mode 100644 src/mono/wasm/templates/templates/wasi-console/runtimeconfig.template.json create mode 100644 src/mono/wasm/templates/templates/wasi-console/wasi-console.0.csproj diff --git a/src/mono/wasm/templates/templates/wasi-console/.template.config/template.json b/src/mono/wasm/templates/templates/wasi-console/.template.config/template.json new file mode 100644 index 0000000000000..b75381b1c7468 --- /dev/null +++ b/src/mono/wasm/templates/templates/wasi-console/.template.config/template.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": [ "Wasi", "WasiConsole" ], + "groupIdentity": "Wasi.Console", + "precedence": 8000, + "identity": "Wasi.Console.8.0", + "name": "Wasi Console App", + "description": "A project template for creating a .NET app that runs on a WASI runtime", + "shortName": "wasiconsole", + "sourceName": "wasi-console.0", + "preferNameDirectory": true, + "tags": { + "language": "C#", + "type": "project" + }, + "symbols": { + "framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { + "choice": "net8.0", + "description": "Target net8.0", + "displayName": ".NET 8.0" + } + ], + "defaultValue": "net8.0", + "replaces": "netX.0", + "displayName": "framework" + } + } +} diff --git a/src/mono/wasm/templates/templates/wasi-console/Program.cs b/src/mono/wasm/templates/templates/wasi-console/Program.cs new file mode 100644 index 0000000000000..dbca79f35764a --- /dev/null +++ b/src/mono/wasm/templates/templates/wasi-console/Program.cs @@ -0,0 +1,3 @@ +using System; + +Console.WriteLine("Hello, Wasi Console!"); diff --git a/src/mono/wasm/templates/templates/wasi-console/Properties/AssemblyInfo.cs b/src/mono/wasm/templates/templates/wasi-console/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000..1e407edc80422 --- /dev/null +++ b/src/mono/wasm/templates/templates/wasi-console/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +[assembly:System.Runtime.Versioning.SupportedOSPlatform("wasi")] diff --git a/src/mono/wasm/templates/templates/wasi-console/README.md b/src/mono/wasm/templates/templates/wasi-console/README.md new file mode 100644 index 0000000000000..6dfb33e1eb954 --- /dev/null +++ b/src/mono/wasm/templates/templates/wasi-console/README.md @@ -0,0 +1,25 @@ +## .NET WASI app + +## Build + +You can build the app from Visual Studio or from the command-line: + +``` +dotnet build -c Debug/Release +``` + +After building the app, the result is in the `bin/$(Configuration)/netX.0/wasi-wasm/AppBundle` directory. + +## Run + +You can build the app from Visual Studio or the command-line: + +``` +dotnet run -c Debug/Release +``` + +Or directly start node from the AppBundle directory: + +``` +wasmtime bin/$(Configuration)/netX.0/browser-wasm/AppBundle/wasi-console.0.wasm +``` diff --git a/src/mono/wasm/templates/templates/wasi-console/runtimeconfig.template.json b/src/mono/wasm/templates/templates/wasi-console/runtimeconfig.template.json new file mode 100644 index 0000000000000..1647a418b8ed9 --- /dev/null +++ b/src/mono/wasm/templates/templates/wasi-console/runtimeconfig.template.json @@ -0,0 +1,10 @@ +{ + "wasmHostProperties": { + "perHostConfig": [ + { + "name": "wasmtime", + "Host": "wasmtime" + } + ] + } +} diff --git a/src/mono/wasm/templates/templates/wasi-console/wasi-console.0.csproj b/src/mono/wasm/templates/templates/wasi-console/wasi-console.0.csproj new file mode 100644 index 0000000000000..3b897107f3943 --- /dev/null +++ b/src/mono/wasm/templates/templates/wasi-console/wasi-console.0.csproj @@ -0,0 +1,8 @@ + + + netX.0 + wasi-wasm + Exe + true + + From 42d6f2fced31551120b4f0dcb16b3da26faa0f86 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 23 Feb 2023 11:49:17 -0500 Subject: [PATCH 03/11] [wasi] Add wasmtime support for WasmAppHost --- src/mono/wasm/host/Program.cs | 1 + src/mono/wasm/host/WasmHost.cs | 7 +- .../wasm/host/wasi/WasiEngineArguments.cs | 31 ++++++ src/mono/wasm/host/wasi/WasiEngineHost.cs | 95 +++++++++++++++++++ 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/mono/wasm/host/wasi/WasiEngineArguments.cs create mode 100644 src/mono/wasm/host/wasi/WasiEngineHost.cs diff --git a/src/mono/wasm/host/Program.cs b/src/mono/wasm/host/Program.cs index e0fc4b65fb0f6..a5005dce9d0fc 100644 --- a/src/mono/wasm/host/Program.cs +++ b/src/mono/wasm/host/Program.cs @@ -27,6 +27,7 @@ public static async Task Main(string[] args) RegisterHostHandler(WasmHost.Browser, BrowserHost.InvokeAsync); RegisterHostHandler(WasmHost.V8, JSEngineHost.InvokeAsync); RegisterHostHandler(WasmHost.NodeJS, JSEngineHost.InvokeAsync); + RegisterHostHandler(WasmHost.Wasmtime, WasiEngineHost.InvokeAsync); using CancellationTokenSource cts = new(); ILoggerFactory loggerFactory = LoggerFactory.Create(builder => diff --git a/src/mono/wasm/host/WasmHost.cs b/src/mono/wasm/host/WasmHost.cs index e3272ae6c6dca..bf90ba7afb02f 100644 --- a/src/mono/wasm/host/WasmHost.cs +++ b/src/mono/wasm/host/WasmHost.cs @@ -24,5 +24,10 @@ internal enum WasmHost /// /// Browser /// - Browser + Browser, + + /// + /// wasmtime + /// + Wasmtime } diff --git a/src/mono/wasm/host/wasi/WasiEngineArguments.cs b/src/mono/wasm/host/wasi/WasiEngineArguments.cs new file mode 100644 index 0000000000000..807ecb659264d --- /dev/null +++ b/src/mono/wasm/host/wasi/WasiEngineArguments.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace Microsoft.WebAssembly.AppHost; + +internal sealed class WasiEngineArguments +{ + public WasmHost Host => CommonConfig.Host; + public CommonConfiguration CommonConfig { get; init; } + + public IEnumerable AppArgs => CommonConfig.RemainingArgs; + + public bool IsSingleFileBundle => + CommonConfig.HostProperties.Extra?.TryGetValue("singleFileBundle", out JsonElement singleFileValue) == true&& + singleFileValue.GetBoolean(); + + public WasiEngineArguments(CommonConfiguration commonConfig) + { + CommonConfig = commonConfig; + } + + public void Validate() + { + if (CommonConfig.Host is not WasmHost.Wasmtime) + throw new ArgumentException($"Internal error: host {CommonConfig.Host} not supported as a jsengine"); + } +} diff --git a/src/mono/wasm/host/wasi/WasiEngineHost.cs b/src/mono/wasm/host/wasi/WasiEngineHost.cs new file mode 100644 index 0000000000000..f1e0ac2c41ebd --- /dev/null +++ b/src/mono/wasm/host/wasi/WasiEngineHost.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.Threading; + +namespace Microsoft.WebAssembly.AppHost; + +internal sealed class WasiEngineHost +{ + private readonly WasiEngineArguments _args; + private readonly ILogger _logger; + + public WasiEngineHost(WasiEngineArguments args, ILogger logger) + { + _args = args; + _logger = logger; + } + + public static async Task InvokeAsync(CommonConfiguration commonArgs, + ILoggerFactory _, + ILogger logger, + CancellationToken _1) + { + var args = new WasiEngineArguments(commonArgs); + args.Validate(); + return await new WasiEngineHost(args, logger).RunAsync(); + } + + private async Task RunAsync() + { + string[] engineArgs = Array.Empty(); + + string engineBinary = _args.Host switch + { + WasmHost.Wasmtime => "wasmtime", + _ => throw new CommandLineException($"Unsupported engine {_args.Host}") + }; + + if (!FileUtils.TryFindExecutableInPATH(engineBinary, out string? engineBinaryPath, out string? errorMessage)) + throw new CommandLineException($"Cannot find host {engineBinary}: {errorMessage}"); + + if (_args.CommonConfig.Debugging) + throw new CommandLineException($"Debugging not supported with {_args.Host}"); + + // var runArgsJson = new RunArgumentsJson(applicationArguments: Array.Empty(), + // runtimeArguments: _args.CommonConfig.RuntimeArguments); + // runArgsJson.Save(Path.Combine(_args.CommonConfig.AppPath, "runArgs.json")); + + var args = new List() + { + "run", + "--dir", + "." + }; + + args.AddRange(engineArgs); + args.Add("--"); + + if (_args.IsSingleFileBundle) + { + args.Add($"{Path.GetFileNameWithoutExtension(_args.CommonConfig.HostProperties.MainAssembly)}.wasm"); + } + else + { + // FIXME: maybe move the assembly name to a config file + args.Add("dotnet.wasm"); + args.Add(Path.GetFileNameWithoutExtension(_args.CommonConfig.HostProperties.MainAssembly)); + } + + args.AddRange(_args.AppArgs); + + ProcessStartInfo psi = new() + { + FileName = engineBinary, + WorkingDirectory = _args.CommonConfig.AppPath + }; + + foreach (string? arg in args) + psi.ArgumentList.Add(arg!); + + int exitCode = await Utils.TryRunProcess(psi, + _logger, + msg => { if (msg != null) _logger.LogInformation(msg); }, + msg => { if (msg != null) _logger.LogInformation(msg); }); + + return exitCode; + } +} From a7a93d23efd33ce45702a4e4b5bc2c2bafcc1a02 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 23 Feb 2023 11:51:58 -0500 Subject: [PATCH 04/11] [wasi] Add wasi-experimental workload - And Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk --- ...t.NET.Runtime.WebAssembly.Wasi.Sdk.pkgproj | 47 +++++++++++++++++++ .../Sdk/AutoImport.props | 6 +++ .../Sdk/Sdk.props | 9 ++++ .../Sdk/Sdk.targets.in | 14 ++++++ ...ad.Mono.Toolchain.Current.Manifest.pkgproj | 7 +++ .../WorkloadManifest.Wasi.targets.in | 10 ++++ .../WorkloadManifest.json.in | 18 +++++++ .../WorkloadManifest.targets.in | 14 +++++- src/mono/nuget/mono-packages.proj | 5 ++ 9 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk.pkgproj create mode 100644 src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Sdk/AutoImport.props create mode 100644 src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Sdk/Sdk.props create mode 100644 src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Sdk/Sdk.targets.in create mode 100644 src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.Wasi.targets.in diff --git a/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk.pkgproj b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk.pkgproj new file mode 100644 index 0000000000000..4ee4946f36d6b --- /dev/null +++ b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk.pkgproj @@ -0,0 +1,47 @@ + + + + + Provides the tasks+targets, for consumption by wasi based workloads + + + + + + + + + + + + + + + + + + $(IntermediateOutputPath)Sdk.targets + + + + <_ReplacementValue Include="TargetFrameworkForNETCoreTasks" Value="$(TargetFrameworkForNETCoreTasks)" /> + <_ReplacementValue Include="TargetFrameworkForNETFrameworkTasks" Value="$(TargetFrameworkForNETFrameworkTasks)" /> + + + + + + + + <_WasmAppHostFiles Include="$(WasmAppHostDir)\*" TargetPath="WasmAppHost" /> + + + + + + + + diff --git a/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Sdk/AutoImport.props b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Sdk/AutoImport.props new file mode 100644 index 0000000000000..a30a907e69f37 --- /dev/null +++ b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Sdk/AutoImport.props @@ -0,0 +1,6 @@ + + + + net8.0 + + diff --git a/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Sdk/Sdk.props b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Sdk/Sdk.props new file mode 100644 index 0000000000000..f6a1d14ac17c2 --- /dev/null +++ b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Sdk/Sdk.props @@ -0,0 +1,9 @@ + + + wasm + wasi + true + Exe + true + + diff --git a/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Sdk/Sdk.targets.in b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Sdk/Sdk.targets.in new file mode 100644 index 0000000000000..9fd83ffe152b5 --- /dev/null +++ b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk/Sdk/Sdk.targets.in @@ -0,0 +1,14 @@ + + + + <_TasksDir Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)..\tasks\${TargetFrameworkForNETCoreTasks}\ + <_TasksDir Condition="'$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)..\tasks\${TargetFrameworkForNETFrameworkTasks}\ + + $(_TasksDir)WasmAppBuilder.dll + $(_TasksDir)WasmBuildTasks.dll + $([MSBuild]::NormalizeDirectory($(MSBuildThisFileDirectory), '..', 'WasmAppHost')) + + + + + diff --git a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest.pkgproj b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest.pkgproj index 1891a2aa206f1..97cffee7065eb 100644 --- a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest.pkgproj +++ b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest.pkgproj @@ -17,11 +17,13 @@ $(IntermediateOutputPath)WorkloadManifest.json $(IntermediateOutputPath)WorkloadManifest.targets + $(IntermediateOutputPath)WorkloadManifest.Wasi.targets + @@ -56,6 +58,11 @@ TemplateFile="WorkloadManifest.targets.in" Properties="@(_WorkloadManifestValues)" OutputPath="$(WorkloadManifestTargetsPath)" /> + + diff --git a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.Wasi.targets.in b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.Wasi.targets.in new file mode 100644 index 0000000000000..f6197d5e269b4 --- /dev/null +++ b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.Wasi.targets.in @@ -0,0 +1,10 @@ + + + + true + + + + $(WasiNativeWorkloadAvailable) + + diff --git a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.json.in b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.json.in index 90a30c8d3d944..89911d331bfd6 100644 --- a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.json.in +++ b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.json.in @@ -24,6 +24,16 @@ "extends": [ "wasm-tools" ], "platforms": [ "win-x64", "win-arm64", "linux-x64", "osx-x64", "osx-arm64" ] }, + "wasi-experimental": { + "description": ".NET WASI experimental", + "packs": [ + "Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk", + "Microsoft.NETCore.App.Runtime.Mono.wasi-wasm", + "Microsoft.NET.Runtime.WebAssembly.Templates" + ], + "extends": [ "microsoft-net-runtime-mono-tooling" ], + "platforms": [ "win-x64", "win-arm64", "linux-x64", "osx-x64", "osx-arm64" ] + }, "microsoft-net-runtime-android": { "abstract": true, "description": "Android Mono Runtime", @@ -155,6 +165,10 @@ "kind": "Sdk", "version": "${PackageVersion}" }, + "Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk": { + "kind": "Sdk", + "version": "${PackageVersion}" + }, "Microsoft.NET.Runtime.WebAssembly.Templates": { "kind": "template", "version": "${PackageVersion}" @@ -354,6 +368,10 @@ "kind": "framework", "version": "${PackageVersion}" }, + "Microsoft.NETCore.App.Runtime.Mono.wasi-wasm" : { + "kind": "framework", + "version": "${PackageVersion}" + }, "Microsoft.NETCore.App.Runtime.win-x64" : { "kind": "framework", "version": "${PackageVersion}" diff --git a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.targets.in b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.targets.in index fe202f8657a7c..6dc95f3afc343 100644 --- a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.targets.in +++ b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.targets.in @@ -14,6 +14,8 @@ $(WasmNativeWorkload8) + + <_BrowserWorkloadNotSupportedForTFM Condition="$([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '6.0'))">true <_BrowserWorkloadDisabled>$(_BrowserWorkloadNotSupportedForTFM) @@ -111,13 +113,23 @@ - + + + + + + + + <_MonoWorkloadTargetsMobile>true <_MonoWorkloadRuntimePackPackageVersion>$(_RuntimePackInWorkloadVersionCurrent) + + %(RuntimePackRuntimeIdentifiers);wasi-wasm + $(_MonoWorkloadRuntimePackPackageVersion) Microsoft.NETCore.App.Runtime.Mono.multithread.**RID** diff --git a/src/mono/nuget/mono-packages.proj b/src/mono/nuget/mono-packages.proj index ccf9379269923..e10d9c9945443 100644 --- a/src/mono/nuget/mono-packages.proj +++ b/src/mono/nuget/mono-packages.proj @@ -10,6 +10,11 @@ + + + + + From a211fd4cb42813b9825a0e8d8a695cc84fa6c79e Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 23 Feb 2023 11:55:43 -0500 Subject: [PATCH 05/11] [wasi] Add Wasi.Build.Tests --- src/mono/wasi/Makefile | 14 +- .../wasi/Wasi.Build.Tests/BuildTestBase.cs | 713 ++++++++++++++++++ .../Wasi.Build.Tests/Directory.Build.props | 10 + .../Wasi.Build.Tests/Directory.Build.targets | 9 + src/mono/wasi/Wasi.Build.Tests/README.md | 19 + .../Wasi.Build.Tests/Wasi.Build.Tests.csproj | 123 +++ .../Wasi.Build.Tests/WasiTemplateTests.cs | 138 ++++ .../data/Local.Directory.Build.props | 5 + .../data/Local.Directory.Build.targets | 5 + .../data/Workloads.Directory.Build.props | 6 + .../data/Workloads.Directory.Build.targets | 5 + .../wasi/Wasi.Build.Tests/data/nuget8.config | 16 + .../Common/BuildEnvironment.cs | 13 +- .../Common/EnvironmentVariables.cs | 1 + 14 files changed, 1073 insertions(+), 4 deletions(-) create mode 100644 src/mono/wasi/Wasi.Build.Tests/BuildTestBase.cs create mode 100644 src/mono/wasi/Wasi.Build.Tests/Directory.Build.props create mode 100644 src/mono/wasi/Wasi.Build.Tests/Directory.Build.targets create mode 100644 src/mono/wasi/Wasi.Build.Tests/README.md create mode 100644 src/mono/wasi/Wasi.Build.Tests/Wasi.Build.Tests.csproj create mode 100644 src/mono/wasi/Wasi.Build.Tests/WasiTemplateTests.cs create mode 100644 src/mono/wasi/Wasi.Build.Tests/data/Local.Directory.Build.props create mode 100644 src/mono/wasi/Wasi.Build.Tests/data/Local.Directory.Build.targets create mode 100644 src/mono/wasi/Wasi.Build.Tests/data/Workloads.Directory.Build.props create mode 100644 src/mono/wasi/Wasi.Build.Tests/data/Workloads.Directory.Build.targets create mode 100644 src/mono/wasi/Wasi.Build.Tests/data/nuget8.config diff --git a/src/mono/wasi/Makefile b/src/mono/wasi/Makefile index d505e8ace0f15..2d61c7e4cca24 100644 --- a/src/mono/wasi/Makefile +++ b/src/mono/wasi/Makefile @@ -60,7 +60,7 @@ run-tests-%: $(DOTNET) build $(TOP)/src/libraries/$*/tests/ /t:Test $(_MSBUILD_WASM_BUILD_ARGS) $(MSBUILD_ARGS) run-build-tests: - $(DOTNET) build $(TOP)/src/mono/wasi//Wasi.Build.Tests/ /t:Test /p:Configuration=$(CONFIG) $(MSBUILD_ARGS) + WASI_SDK_PATH=$(WASI_SDK_PATH) $(DOTNET) build $(TOP)/src/mono/wasi//Wasi.Build.Tests/ /t:Test /bl $(_MSBUILD_WASM_BUILD_ARGS) $(MSBUILD_ARGS) build-debugger-tests-helix: $(DOTNET) build -restore -bl:$(TOP)/artifacts/log/$(CONFIG)/Wasm.Debugger.Tests.binlog \ @@ -78,6 +78,18 @@ submit-debugger-tests-helix: build-debugger-tests-helix $(_MSBUILD_WASM_BUILD_ARGS) \ $(MSBUILD_ARGS) +submit-wbt-helix: + PATH="$(JSVU):$(PATH)" \ + $(DOTNET) build $(TOP)/src/mono/wasi/Wasi.Build.Tests/ /v:m /p:ArchiveTests=true /t:ArchiveTests $(_MSBUILD_WASM_BUILD_ARGS) $(MSBUILD_ARGS) && \ + WASI_SDK_PATH=$(WASI_SDK_PATH) BUILD_REASON=wasm-test SYSTEM_TEAMPROJECT=public BUILD_REPOSITORY_NAME=dotnet/runtime BUILD_SOURCEBRANCH=main \ + $(TOP)/eng/common/msbuild.sh --ci -restore $(TOP)/src/libraries/sendtohelix.proj \ + /p:TestRunNamePrefixSuffix=WasiBuildTests /p:HelixBuild=`date "+%Y%m%d.%H%M"` /p:Creator=`whoami` \ + /bl:$(TOP)/artifacts/log/$(CONFIG)/SendToHelix.binlog -v:m -p:HelixTargetQueue=$(HELIX_TARGET_QUEUE) \ + /p:RuntimeFlavor=mono /p:TargetRuntimeIdentifier= /p:MonoForceInterpreter= /p:TestScope=innerloop \ + /p:_Scenarios=buildwasmapps \ + $(_MSBUILD_WASM_BUILD_ARGS) \ + $(MSBUILD_ARGS) + submit-tests-helix: echo "\n** This will submit all the available test zip files to helix **\n" BUILD_REASON=wasm-test SYSTEM_TEAMPROJECT=public BUILD_REPOSITORY_NAME=dotnet/runtime BUILD_SOURCEBRANCH=main \ diff --git a/src/mono/wasi/Wasi.Build.Tests/BuildTestBase.cs b/src/mono/wasi/Wasi.Build.Tests/BuildTestBase.cs new file mode 100644 index 0000000000000..a66b793a08ed0 --- /dev/null +++ b/src/mono/wasi/Wasi.Build.Tests/BuildTestBase.cs @@ -0,0 +1,713 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Threading; +using System.Xml; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +#nullable enable + +// [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] + +namespace Wasm.Build.Tests +{ + public abstract class BuildTestBase : IClassFixture, IDisposable + { + public const string DefaultTargetFramework = "net8.0"; + protected static readonly bool s_skipProjectCleanup; + protected static readonly string s_xharnessRunnerCommand; + protected string? _projectDir; + protected readonly ITestOutputHelper _testOutput; + protected string _logPath; + protected bool _enablePerTestCleanup = false; + protected SharedBuildPerTestClassFixture _buildContext; + protected string _nugetPackagesDir = string.Empty; + + // FIXME: use an envvar to override this + protected static int s_defaultPerTestTimeoutMs = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 30*60*1000 : 15*60*1000; + protected static BuildEnvironment s_buildEnv; + private const string s_runtimePackPathPattern = "\\*\\* MicrosoftNetCoreAppRuntimePackDir : '([^ ']*)'"; + private const string s_nugetInsertionTag = ""; + private static Regex s_runtimePackPathRegex; + private static int s_testCounter; + private readonly int _testIdx; + + public static bool IsUsingWorkloads => s_buildEnv.IsWorkload; + public static bool IsNotUsingWorkloads => !s_buildEnv.IsWorkload; + public static string GetNuGetConfigPathFor(string targetFramework) => + Path.Combine(BuildEnvironment.TestDataPath, "nuget8.config"); // for now - we are still using net7, but with + // targetFramework == "net7.0" ? "nuget7.config" : "nuget8.config"); + + static BuildTestBase() + { + try + { + s_buildEnv = new BuildEnvironment(); + if (EnvironmentVariables.WasiSdkPath is null) + throw new Exception($"Error: WASI_SDK_PATH is not set"); + + s_buildEnv.EnvVars["WASI_SDK_PATH"] = EnvironmentVariables.WasiSdkPath; + s_runtimePackPathRegex = new Regex(s_runtimePackPathPattern); + + s_skipProjectCleanup = !string.IsNullOrEmpty(EnvironmentVariables.SkipProjectCleanup) && EnvironmentVariables.SkipProjectCleanup == "1"; + + if (string.IsNullOrEmpty(EnvironmentVariables.XHarnessCliPath)) + s_xharnessRunnerCommand = "xharness"; + else + s_xharnessRunnerCommand = EnvironmentVariables.XHarnessCliPath; + + Console.WriteLine (""); + Console.WriteLine ($"=============================================================================================="); + Console.WriteLine ($"=============== Running with {(s_buildEnv.IsWorkload ? "Workloads" : "No workloads")} ==============="); + Console.WriteLine ($"=============================================================================================="); + Console.WriteLine (""); + } + catch (Exception ex) + { + Console.WriteLine ($"Exception: {ex}"); + throw; + } + } + + public BuildTestBase(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + { + _testIdx = Interlocked.Increment(ref s_testCounter); + _buildContext = buildContext; + _testOutput = output; + _logPath = s_buildEnv.LogRootPath; // FIXME: + } + + public static IEnumerable> ConfigWithAOTData(bool aot, string? config=null) + { + if (config == null) + { + return new IEnumerable[] + { + #if TEST_DEBUG_CONFIG_ALSO + // list of each member data - for Debug+@aot + new object?[] { new BuildArgs("placeholder", "Debug", aot, "placeholder", string.Empty) }.AsEnumerable(), + #endif + // list of each member data - for Release+@aot + new object?[] { new BuildArgs("placeholder", "Release", aot, "placeholder", string.Empty) }.AsEnumerable() + }.AsEnumerable(); + } + else + { + return new IEnumerable[] + { + new object?[] { new BuildArgs("placeholder", config, aot, "placeholder", string.Empty) }.AsEnumerable() + }; + } + } + + [MemberNotNull(nameof(_projectDir), nameof(_logPath))] + protected void InitPaths(string id) + { + if (_projectDir == null) + _projectDir = Path.Combine(BuildEnvironment.TmpPath, id); + _logPath = Path.Combine(s_buildEnv.LogRootPath, id); + _nugetPackagesDir = Path.Combine(BuildEnvironment.TmpPath, "nuget", id); + + if (Directory.Exists(_nugetPackagesDir)) + Directory.Delete(_nugetPackagesDir, recursive: true); + + Directory.CreateDirectory(_nugetPackagesDir!); + Directory.CreateDirectory(_logPath); + } + + protected void InitProjectDir(string dir, bool addNuGetSourceForLocalPackages = false, string targetFramework = DefaultTargetFramework) + { + Directory.CreateDirectory(dir); + File.WriteAllText(Path.Combine(dir, "Directory.Build.props"), s_buildEnv.DirectoryBuildPropsContents); + File.WriteAllText(Path.Combine(dir, "Directory.Build.targets"), s_buildEnv.DirectoryBuildTargetsContents); + + string targetNuGetConfigPath = Path.Combine(dir, "nuget.config"); + if (addNuGetSourceForLocalPackages) + { + File.WriteAllText(targetNuGetConfigPath, + GetNuGetConfigWithLocalPackagesPath( + GetNuGetConfigPathFor(targetFramework), + s_buildEnv.BuiltNuGetsPath)); + } + else + { + File.Copy(GetNuGetConfigPathFor(targetFramework), targetNuGetConfigPath); + } + } + + protected const string SimpleProjectTemplate = + @$" + + {DefaultTargetFramework} + Exe + true + test-main.js + ##EXTRA_PROPERTIES## + + + ##EXTRA_ITEMS## + + ##INSERT_AT_END## + "; + + protected static BuildArgs ExpandBuildArgs(BuildArgs buildArgs, string extraProperties="", string extraItems="", string insertAtEnd="", string projectTemplate=SimpleProjectTemplate) + { + if (buildArgs.AOT) + { + extraProperties = $"{extraProperties}\ntrue"; + extraProperties += $"\n{RuntimeInformation.IsOSPlatform(OSPlatform.Windows)}\n"; + } + + string projectContents = projectTemplate + .Replace("##EXTRA_PROPERTIES##", extraProperties) + .Replace("##EXTRA_ITEMS##", extraItems) + .Replace("##INSERT_AT_END##", insertAtEnd); + return buildArgs with { ProjectFileContents = projectContents }; + } + + public (string projectDir, string buildOutput) BuildProject(BuildArgs buildArgs, + string id, + BuildProjectOptions options) + { + string msgPrefix = options.Label != null ? $"[{options.Label}] " : string.Empty; + if (options.UseCache && _buildContext.TryGetBuildFor(buildArgs, out BuildProduct? product)) + { + _testOutput.WriteLine ($"Using existing build found at {product.ProjectDir}, with build log at {product.LogFile}"); + + if (!product.Result) + throw new XunitException($"Found existing build at {product.ProjectDir}, but it had failed. Check build log at {product.LogFile}"); + _projectDir = product.ProjectDir; + + // use this test's id for the run logs + _logPath = Path.Combine(s_buildEnv.LogRootPath, id); + return (_projectDir, "FIXME"); + } + + if (options.CreateProject) + { + InitPaths(id); + InitProjectDir(_projectDir); + options.InitProject?.Invoke(); + + File.WriteAllText(Path.Combine(_projectDir, $"{buildArgs.ProjectName}.csproj"), buildArgs.ProjectFileContents); + File.Copy(Path.Combine(AppContext.BaseDirectory, + options.TargetFramework == "net8.0" ? "test-main.js" : "data/test-main-7.0.js"), + Path.Combine(_projectDir, "test-main.js")); + } + else if (_projectDir is null) + { + throw new Exception("_projectDir should be set, to use options.createProject=false"); + } + + StringBuilder sb = new(); + sb.Append(options.Publish ? "publish" : "build"); + if (options.Publish && options.BuildOnlyAfterPublish) + sb.Append(" -p:WasmBuildOnlyAfterPublish=true"); + sb.Append($" {s_buildEnv.DefaultBuildArgs}"); + + sb.Append($" /p:Configuration={buildArgs.Config}"); + + string logFileSuffix = options.Label == null ? string.Empty : options.Label.Replace(' ', '_'); + string logFilePath = Path.Combine(_logPath, $"{buildArgs.ProjectName}{logFileSuffix}.binlog"); + _testOutput.WriteLine($"-------- Building ---------"); + _testOutput.WriteLine($"Binlog path: {logFilePath}"); + sb.Append($" /bl:\"{logFilePath}\" /nologo"); + sb.Append($" /v:{options.Verbosity ?? "minimal"}"); + if (buildArgs.ExtraBuildArgs != null) + sb.Append($" {buildArgs.ExtraBuildArgs} "); + + _testOutput.WriteLine($"Building {buildArgs.ProjectName} in {_projectDir}"); + + (int exitCode, string buildOutput) result; + try + { + var envVars = s_buildEnv.EnvVars; + if (options.ExtraBuildEnvironmentVariables is not null) + { + envVars = new Dictionary(s_buildEnv.EnvVars); + foreach (var kvp in options.ExtraBuildEnvironmentVariables!) + envVars[kvp.Key] = kvp.Value; + } + envVars["NUGET_PACKAGES"] = _nugetPackagesDir; + result = AssertBuild(sb.ToString(), id, expectSuccess: options.ExpectSuccess, envVars: envVars); + + // check that we are using the correct runtime pack! + + if (options.ExpectSuccess && options.AssertAppBundle) + { + AssertRuntimePackPath(result.buildOutput, options.TargetFramework ?? DefaultTargetFramework); + + string bundleDir = Path.Combine(GetBinDir(config: buildArgs.Config, targetFramework: options.TargetFramework ?? DefaultTargetFramework), "AppBundle"); + AssertBasicAppBundle(bundleDir, + buildArgs.ProjectName, + buildArgs.Config, + options.MainJS ?? "test-main.js", + options.HasV8Script, + options.TargetFramework ?? DefaultTargetFramework, + options.HasIcudt, + options.DotnetWasmFromRuntimePack ?? !buildArgs.AOT); + } + + if (options.UseCache) + _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir, logFilePath, true)); + + return (_projectDir, result.buildOutput); + } + catch + { + if (options.UseCache) + _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir, logFilePath, false)); + throw; + } + } + + private static string GetNuGetConfigWithLocalPackagesPath(string templatePath, string localNuGetsPath) + { + string contents = File.ReadAllText(templatePath); + if (contents.IndexOf(s_nugetInsertionTag, StringComparison.InvariantCultureIgnoreCase) < 0) + throw new Exception($"Could not find {s_nugetInsertionTag} in {templatePath}"); + + return contents.Replace(s_nugetInsertionTag, $@""); + } + + public string CreateWasmTemplateProject(string id, string template = "wasmbrowser", string extraArgs = "", bool runAnalyzers = true) + { + InitPaths(id); + InitProjectDir(_projectDir, addNuGetSourceForLocalPackages: true); + + File.WriteAllText(Path.Combine(_projectDir, "Directory.Build.props"), ""); + File.WriteAllText(Path.Combine(_projectDir, "Directory.Build.targets"), + """ + + + + + + """); + + new DotNetCommand(s_buildEnv, _testOutput, useDefaultArgs: false) + .WithWorkingDirectory(_projectDir!) + .ExecuteWithCapturedOutput($"new {template} {extraArgs}") + .EnsureSuccessful(); + + string projectfile = Path.Combine(_projectDir!, $"{id}.csproj"); + if (runAnalyzers) + AddItemsPropertiesToProject("true"); + return projectfile; + } + + protected (CommandResult, string) BuildInternal(string id, string config, bool publish=false, bool setWasmDevel=true, params string[] extraArgs) + { + string label = publish ? "publish" : "build"; + _testOutput.WriteLine($"{Environment.NewLine}** {label} **{Environment.NewLine}"); + + string logPath = Path.Combine(s_buildEnv.LogRootPath, id, $"{id}-{label}.binlog"); + string[] combinedArgs = new[] + { + label, // same as the command name + $"-bl:{logPath}", + $"-p:Configuration={config}", + "-p:BlazorEnableCompression=false", + "-nr:false", + setWasmDevel ? "-p:_WasmDevel=true" : string.Empty + }.Concat(extraArgs).ToArray(); + + CommandResult res = new DotNetCommand(s_buildEnv, _testOutput) + .WithWorkingDirectory(_projectDir!) + .WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir) + .ExecuteWithCapturedOutput(combinedArgs) + .EnsureSuccessful(); + + return (res, logPath); + } + + static void AssertRuntimePackPath(string buildOutput, string targetFramework) + { + var match = s_runtimePackPathRegex.Match(buildOutput); + if (!match.Success || match.Groups.Count != 2) + throw new XunitException($"Could not find the pattern in the build output: '{s_runtimePackPathPattern}'.{Environment.NewLine}Build output: {buildOutput}"); + + string expectedRuntimePackDir = s_buildEnv.GetRuntimePackDir(targetFramework); + string actualPath = match.Groups[1].Value; + if (string.Compare(actualPath, expectedRuntimePackDir) != 0) + throw new XunitException($"Runtime pack path doesn't match.{Environment.NewLine}Expected: '{expectedRuntimePackDir}'{Environment.NewLine}Actual: '{actualPath}'"); + } + + protected static void AssertBasicAppBundle(string bundleDir, + string projectName, + string config, + string mainJS, + bool hasV8Script, + string targetFramework, + bool hasIcudt = true, + bool dotnetWasmFromRuntimePack = true) + { +#if false + AssertFilesExist(bundleDir, new [] + { + "index.html", + mainJS, + "dotnet.timezones.blat", + "dotnet.wasm", + "mono-config.json", + "dotnet.js" + }); + + AssertFilesExist(bundleDir, new[] { "run-v8.sh" }, expectToExist: hasV8Script); + AssertFilesExist(bundleDir, new[] { "icudt.dat" }, expectToExist: hasIcudt); + + string managedDir = Path.Combine(bundleDir, "managed"); + AssertFilesExist(managedDir, new[] { $"{projectName}.dll" }); + + bool is_debug = config == "Debug"; + if (is_debug) + { + // Use cecil to check embedded pdb? + // AssertFilesExist(managedDir, new[] { $"{projectName}.pdb" }); + + //FIXME: um.. what about these? embedded? why is linker omitting them? + //foreach (string file in Directory.EnumerateFiles(managedDir, "*.dll")) + //{ + //string pdb = Path.ChangeExtension(file, ".pdb"); + //Assert.True(File.Exists(pdb), $"Could not find {pdb} for {file}"); + //} + } + + AssertDotNetWasmJs(bundleDir, fromRuntimePack: dotnetWasmFromRuntimePack, targetFramework); +#endif + } + + protected static void AssertFilesDontExist(string dir, string[] filenames, string? label = null) + => AssertFilesExist(dir, filenames, label, expectToExist: false); + + protected static void AssertFilesExist(string dir, string[] filenames, string? label = null, bool expectToExist=true) + { + string prefix = label != null ? $"{label}: " : string.Empty; + if (!Directory.Exists(dir)) + throw new XunitException($"[{label}] {dir} not found"); + foreach (string filename in filenames) + { + string path = Path.Combine(dir, filename); + if (expectToExist && !File.Exists(path)) + throw new XunitException($"{prefix}Expected the file to exist: {path}"); + + if (!expectToExist && File.Exists(path)) + throw new XunitException($"{prefix}Expected the file to *not* exist: {path}"); + } + } + + protected static void AssertSameFile(string file0, string file1, string? label=null) => AssertFile(file0, file1, label, same: true); + protected static void AssertNotSameFile(string file0, string file1, string? label=null) => AssertFile(file0, file1, label, same: false); + + protected static void AssertFile(string file0, string file1, string? label=null, bool same=true) + { + Assert.True(File.Exists(file0), $"{label}: Expected to find {file0}"); + Assert.True(File.Exists(file1), $"{label}: Expected to find {file1}"); + + FileInfo finfo0 = new(file0); + FileInfo finfo1 = new(file1); + + if (same && finfo0.Length != finfo1.Length) + throw new XunitException($"{label}:{Environment.NewLine} File sizes don't match for {file0} ({finfo0.Length}), and {file1} ({finfo1.Length})"); + + if (!same && finfo0.Length == finfo1.Length) + throw new XunitException($"{label}:{Environment.NewLine} File sizes should not match for {file0} ({finfo0.Length}), and {file1} ({finfo1.Length})"); + } + + protected (int exitCode, string buildOutput) AssertBuild(string args, string label="build", bool expectSuccess=true, IDictionary? envVars=null, int? timeoutMs=null) + { + var result = RunProcess(s_buildEnv.DotNet, _testOutput, args, workingDir: _projectDir, label: label, envVars: envVars, timeoutMs: timeoutMs ?? s_defaultPerTestTimeoutMs); + if (expectSuccess && result.exitCode != 0) + throw new XunitException($"Build process exited with non-zero exit code: {result.exitCode}"); + if (!expectSuccess && result.exitCode == 0) + throw new XunitException($"Build should have failed, but it didn't. Process exited with exitCode : {result.exitCode}"); + + return result; + } + + private string FindSubDirIgnoringCase(string parentDir, string dirName) + { + IEnumerable matchingDirs = Directory.EnumerateDirectories(parentDir, + dirName, + new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }); + + string? first = matchingDirs.FirstOrDefault(); + if (matchingDirs.Count() > 1) + throw new Exception($"Found multiple directories with names that differ only in case. {string.Join(", ", matchingDirs.ToArray())}"); + + return first ?? Path.Combine(parentDir, dirName); + } + + protected string GetBinDir(string config, string targetFramework=DefaultTargetFramework, string? baseDir=null) + { + var dir = baseDir ?? _projectDir; + Assert.NotNull(dir); + return Path.Combine(dir!, "bin", config, targetFramework, BuildEnvironment.DefaultRuntimeIdentifier); + } + + protected string GetObjDir(string config, string targetFramework=DefaultTargetFramework, string? baseDir=null) + { + var dir = baseDir ?? _projectDir; + Assert.NotNull(dir); + return Path.Combine(dir!, "obj", config, targetFramework, BuildEnvironment.DefaultRuntimeIdentifier); + } + + public static (int exitCode, string buildOutput) RunProcess(string path, + ITestOutputHelper _testOutput, + string args = "", + IDictionary? envVars = null, + string? workingDir = null, + string? label = null, + bool logToXUnit = true, + int? timeoutMs = null) + { + var t = RunProcessAsync(path, _testOutput, args, envVars, workingDir, label, logToXUnit, timeoutMs); + t.Wait(); + return t.Result; + } + + public static async Task<(int exitCode, string buildOutput)> RunProcessAsync(string path, + ITestOutputHelper _testOutput, + string args = "", + IDictionary? envVars = null, + string? workingDir = null, + string? label = null, + bool logToXUnit = true, + int? timeoutMs = null) + { + _testOutput.WriteLine($"Running {path} {args}"); + _testOutput.WriteLine($"WorkingDirectory: {workingDir}"); + StringBuilder outputBuilder = new (); + object syncObj = new(); + + var processStartInfo = new ProcessStartInfo + { + FileName = path, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + Arguments = args, + }; + + if (workingDir == null || !Directory.Exists(workingDir)) + throw new Exception($"Working directory {workingDir} not found"); + + if (workingDir != null) + processStartInfo.WorkingDirectory = workingDir; + + if (envVars != null) + { + if (envVars.Count > 0) + _testOutput.WriteLine("Setting environment variables for execution:"); + + foreach (KeyValuePair envVar in envVars) + { + processStartInfo.EnvironmentVariables[envVar.Key] = envVar.Value; + _testOutput.WriteLine($"\t{envVar.Key} = {envVar.Value}"); + } + + // runtime repo sets this, which interferes with the tests + processStartInfo.RemoveEnvironmentVariables("MSBuildSDKsPath"); + } + + Process process = new (); + process.StartInfo = processStartInfo; + process.EnableRaisingEvents = true; + + // AutoResetEvent resetEvent = new (false); + // process.Exited += (_, _) => { _testOutput.WriteLine ($"- exited called"); resetEvent.Set(); }; + + if (!process.Start()) + throw new ArgumentException("No process was started: process.Start() return false."); + + try + { + DataReceivedEventHandler logStdErr = (sender, e) => LogData($"[{label}-stderr]", e.Data); + DataReceivedEventHandler logStdOut = (sender, e) => LogData($"[{label}]", e.Data); + + process.ErrorDataReceived += logStdErr; + process.OutputDataReceived += logStdOut; + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + using CancellationTokenSource cts = new(); + cts.CancelAfter(timeoutMs ?? s_defaultPerTestTimeoutMs); + + await process.WaitForExitAsync(cts.Token); + + if (cts.IsCancellationRequested) + { + // process didn't exit + process.Kill(entireProcessTree: true); + lock (syncObj) + { + var lastLines = outputBuilder.ToString().Split('\r', '\n').TakeLast(20); + throw new XunitException($"Process timed out. Last 20 lines of output:{Environment.NewLine}{string.Join(Environment.NewLine, lastLines)}"); + } + } + + // this will ensure that all the async event handling has completed + // and should be called after process.WaitForExit(int) + // https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit?view=net-5.0#System_Diagnostics_Process_WaitForExit_System_Int32_ + process.WaitForExit(); + + process.ErrorDataReceived -= logStdErr; + process.OutputDataReceived -= logStdOut; + process.CancelErrorRead(); + process.CancelOutputRead(); + + lock (syncObj) + { + var exitCode = process.ExitCode; + return (process.ExitCode, outputBuilder.ToString().Trim('\r', '\n')); + } + } + catch (Exception ex) + { + _testOutput.WriteLine($"-- exception -- {ex}"); + throw; + } + + void LogData(string label, string? message) + { + lock (syncObj) + { + if (logToXUnit && message != null) + { + _testOutput.WriteLine($"{label} {message}"); + } + outputBuilder.AppendLine($"{label} {message}"); + } + if (EnvironmentVariables.ShowBuildOutput) + Console.WriteLine($"{label} {message}"); + } + } + + public static string AddItemsPropertiesToProject(string projectFile, string? extraProperties=null, string? extraItems=null, string? atTheEnd=null) + { + if (extraProperties == null && extraItems == null && atTheEnd == null) + return projectFile; + + XmlDocument doc = new(); + doc.Load(projectFile); + + XmlNode root = doc.DocumentElement ?? throw new Exception(); + if (extraItems != null) + { + XmlNode node = doc.CreateNode(XmlNodeType.Element, "ItemGroup", null); + node.InnerXml = extraItems; + root.AppendChild(node); + } + + if (extraProperties != null) + { + XmlNode node = doc.CreateNode(XmlNodeType.Element, "PropertyGroup", null); + node.InnerXml = extraProperties; + root.AppendChild(node); + } + + if (atTheEnd != null) + { + XmlNode node = doc.CreateNode(XmlNodeType.DocumentFragment, "foo", null); + node.InnerXml = atTheEnd; + root.InsertAfter(node, root.LastChild); + } + + doc.Save(projectFile); + + return projectFile; + } + + public void Dispose() + { + if (_projectDir != null && _enablePerTestCleanup) + _buildContext.RemoveFromCache(_projectDir, keepDir: s_skipProjectCleanup); + } + + private static string GetEnvironmentVariableOrDefault(string envVarName, string defaultValue) + { + string? value = Environment.GetEnvironmentVariable(envVarName); + return string.IsNullOrEmpty(value) ? defaultValue : value; + } + + internal BuildPaths GetBuildPaths(BuildArgs buildArgs, bool forPublish=true) + { + string objDir = GetObjDir(buildArgs.Config); + string bundleDir = Path.Combine(GetBinDir(baseDir: _projectDir, config: buildArgs.Config), "AppBundle"); + string wasmDir = Path.Combine(objDir, "wasm", forPublish ? "for-publish" : "for-build"); + + return new BuildPaths(wasmDir, objDir, GetBinDir(buildArgs.Config), bundleDir); + } + + internal IDictionary StatFiles(IEnumerable fullpaths) + { + Dictionary table = new(); + foreach (string file in fullpaths) + { + if (File.Exists(file)) + table.Add(Path.GetFileName(file), new FileStat(FullPath: file, Exists: true, LastWriteTimeUtc: File.GetLastWriteTimeUtc(file), Length: new FileInfo(file).Length)); + else + table.Add(Path.GetFileName(file), new FileStat(FullPath: file, Exists: false, LastWriteTimeUtc: DateTime.MinValue, Length: 0)); + } + + return table; + } + + protected static string GetSkiaSharpReferenceItems() + => @" + + "; + + protected static string s_mainReturns42 = @" + public class TestClass { + public static int Main() + { + return 42; + } + }"; + } + + public record BuildArgs(string ProjectName, + string Config, + bool AOT, + string ProjectFileContents, + string? ExtraBuildArgs); + public record BuildProduct(string ProjectDir, string LogFile, bool Result); + internal record FileStat (bool Exists, DateTime LastWriteTimeUtc, long Length, string FullPath); + internal record BuildPaths(string ObjWasmDir, string ObjDir, string BinDir, string BundleDir); + + public record BuildProjectOptions + ( + Action? InitProject = null, + bool? DotnetWasmFromRuntimePack = null, + bool HasIcudt = true, + bool UseCache = true, + bool ExpectSuccess = true, + bool AssertAppBundle = true, + bool CreateProject = true, + bool Publish = true, + bool BuildOnlyAfterPublish = true, + bool HasV8Script = true, + string? Verbosity = null, + string? Label = null, + string? TargetFramework = null, + string? MainJS = null, + IDictionary? ExtraBuildEnvironmentVariables = null + ); + + public enum NativeFilesType { FromRuntimePack, Relinked, AOT }; +} diff --git a/src/mono/wasi/Wasi.Build.Tests/Directory.Build.props b/src/mono/wasi/Wasi.Build.Tests/Directory.Build.props new file mode 100644 index 0000000000000..102dd7c86919d --- /dev/null +++ b/src/mono/wasi/Wasi.Build.Tests/Directory.Build.props @@ -0,0 +1,10 @@ + + + + BuildWasmApps + true + Wasi.Build.Tests + + + + diff --git a/src/mono/wasi/Wasi.Build.Tests/Directory.Build.targets b/src/mono/wasi/Wasi.Build.Tests/Directory.Build.targets new file mode 100644 index 0000000000000..43eea04f9888d --- /dev/null +++ b/src/mono/wasi/Wasi.Build.Tests/Directory.Build.targets @@ -0,0 +1,9 @@ + + + + + $(OutDir) + $(OutDir)\RunTests.sh + $(OutDir)\RunTests.cmd + + diff --git a/src/mono/wasi/Wasi.Build.Tests/README.md b/src/mono/wasi/Wasi.Build.Tests/README.md new file mode 100644 index 0000000000000..ec8327e5dcdf5 --- /dev/null +++ b/src/mono/wasi/Wasi.Build.Tests/README.md @@ -0,0 +1,19 @@ +# Wasi.Build.Tests + +Contains tests for wasi project builds, eg. for aot, relinking, globalization +etc. The intent is to check if the build inputs result in a correct app bundle +being generated. + +- When running locally, it tests against a local workload install (based on `artifacts`) + - but this can be turned off with `/p:TestUsingWorkloads=false` + - in which case, it will run against a sdk with updated workload manifests, but no workload installed + +- On CI, both workload, and no-workload cases are tested + +- Running: + +Linux/macOS: `$ make -C src/mono/wasi run-build-tests` +Windows: `.\dotnet.cmd build .\src\mono\wasi\Wasi.Build.Tests\Wasi.Build.Tests.csproj -c Release -t:Test -p:TargetOS=Browser -p:TargetArchitecture=wasi` + +- Specific tests can be run via `XUnitClassName`, and `XUnitMethodName` + - eg. `XUnitClassName=Wasm.Build.Tests.BlazorWasmTests` diff --git a/src/mono/wasi/Wasi.Build.Tests/Wasi.Build.Tests.csproj b/src/mono/wasi/Wasi.Build.Tests/Wasi.Build.Tests.csproj new file mode 100644 index 0000000000000..0663c1bc7a620 --- /dev/null +++ b/src/mono/wasi/Wasi.Build.Tests/Wasi.Build.Tests.csproj @@ -0,0 +1,123 @@ + + + $(NetCoreAppToolCurrent) + true + true + true + xunit + true + false + TEST_DEBUG_CONFIG_ALSO + + false + + true + + false + true + true + + + false + true + + false + TARGET_WASI + <_MonoSrcWasmDir>$([MSBuild]::NormalizeDirectory($(MonoProjectRoot), 'wasm')) + + + + + + RunScriptTemplate.cmd + RunScriptTemplate.sh + + $(_MonoSrcWasmDir)Wasm.Build.Tests\data\$(RunScriptInputName) + + + + + + + + + + + + + + + + <_SdkWithWorkloadForTestingDirName>$([System.IO.Path]::GetDirectoryName($(SdkWithWorkloadForTestingPath))) + <_SdkWithWorkloadForTestingDirName>$([System.IO.Path]::GetFilename($(_SdkWithWorkloadForTestingDirName))) + + + + + + + + + + + + + + + <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' == 'true'">-notrait category=no-workload + <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' != 'true'">-trait category=no-workload + + + + + + + + + + + + + + + + + + + + + + + + <_RuntimePackVersions Include="$(PackageVersion)" EnvVarName="RUNTIME_PACK_VER8" /> + + + + + + + + + + dotnet exec xunit.console.dll $(AssemblyName).dll -xml %24XHARNESS_OUT/testResults.xml + dotnet.exe exec xunit.console.dll $(AssemblyName).dll -xml %XHARNESS_OUT%\testResults.xml + + $(RunScriptCommand) %24HELIX_XUNIT_ARGS + $(RunScriptCommand) %HELIX_XUNIT_ARGS% + + $(RunScriptCommand) -nocolor + $(RunScriptCommand) -parallel none + $(RunScriptCommand) -verbose + + $(RunScriptCommand) -method $(XUnitMethodName) + $(RunScriptCommand) -class $(XUnitClassName) + + + $(RunScriptCommand) -notrait category=IgnoreForCI -notrait category=failing + + $(RunScriptCommand) %XUnitTraitArg% + $(RunScriptCommand) %24XUnitTraitArg + + + diff --git a/src/mono/wasi/Wasi.Build.Tests/WasiTemplateTests.cs b/src/mono/wasi/Wasi.Build.Tests/WasiTemplateTests.cs new file mode 100644 index 0000000000000..cad7521379378 --- /dev/null +++ b/src/mono/wasi/Wasi.Build.Tests/WasiTemplateTests.cs @@ -0,0 +1,138 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; +using Wasm.Build.Tests; + +#nullable enable + +namespace Wasi.Build.Tests; + +public class WasiTemplateTests : BuildTestBase +{ + public WasiTemplateTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + } + + [Theory] + [InlineData("Debug")] + [InlineData("Release")] + public void ConsoleBuildThenPublish(string config) + { + string id = $"{config}_{Path.GetRandomFileName()}"; + string projectFile = CreateWasmTemplateProject(id, "wasiconsole"); + string projectName = Path.GetFileNameWithoutExtension(projectFile); + File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), s_simpleMainWithArgs); + + var buildArgs = new BuildArgs(projectName, config, false, id, null); + buildArgs = ExpandBuildArgs(buildArgs); + + BuildProject(buildArgs, + id: id, + new BuildProjectOptions( + DotnetWasmFromRuntimePack: true, + CreateProject: false, + Publish: false, + TargetFramework: BuildTestBase.DefaultTargetFramework)); + + // ActiveIssue: https://github.com/dotnet/runtime/issues/82515 + int expectedExitCode = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 1 : 42; + + string runArgs = $"run --no-build -c {config}"; + runArgs += " x y z"; + var res = new RunCommand(s_buildEnv, _testOutput, label: id) + .WithWorkingDirectory(_projectDir!) + .ExecuteWithCapturedOutput(runArgs) + .EnsureExitCode(expectedExitCode); + + Assert.Contains("Hello, Wasi Console!", res.Output); + Assert.Contains("args[0] = x", res.Output); + Assert.Contains("args[1] = y", res.Output); + Assert.Contains("args[2] = z", res.Output); + + if (!_buildContext.TryGetBuildFor(buildArgs, out BuildProduct? product)) + throw new XunitException($"Test bug: could not get the build product in the cache"); + + File.Move(product!.LogFile, Path.ChangeExtension(product.LogFile!, ".first.binlog")); + + _testOutput.WriteLine($"{Environment.NewLine}Publishing with no changes ..{Environment.NewLine}"); + + bool expectRelinking = config == "Release"; + BuildProject(buildArgs, + id: id, + new BuildProjectOptions( + DotnetWasmFromRuntimePack: !expectRelinking, + CreateProject: false, + Publish: true, + TargetFramework: BuildTestBase.DefaultTargetFramework, + UseCache: false)); + } + + public static TheoryData TestDataForConsolePublishAndRun() + { + var data = new TheoryData(); + data.Add("Debug", false); + data.Add("Debug", true); + data.Add("Release", false); // Release relinks by default + return data; + } + + [ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/82515", TestPlatforms.Windows)] + [MemberData(nameof(TestDataForConsolePublishAndRun))] + public void ConsolePublishAndRunForSingleFileBundle(string config, bool relinking) + { + string id = $"{config}_{Path.GetRandomFileName()}"; + string projectFile = CreateWasmTemplateProject(id, "wasiconsole"); + string projectName = Path.GetFileNameWithoutExtension(projectFile); + File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), s_simpleMainWithArgs); + + string extraProperties = "true"; + if (relinking) + extraProperties += "true"; + + AddItemsPropertiesToProject(projectFile, extraProperties); + + var buildArgs = new BuildArgs(projectName, config, /*aot*/false, id, null); + buildArgs = ExpandBuildArgs(buildArgs); + + bool expectRelinking = config == "Release" || relinking; + BuildProject(buildArgs, + id: id, + new BuildProjectOptions( + DotnetWasmFromRuntimePack: !expectRelinking, + CreateProject: false, + Publish: true, + TargetFramework: BuildTestBase.DefaultTargetFramework, + UseCache: false)); + + int expectedExitCode = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 1 : 42; + + string runArgs = $"run --no-build -c {config}"; + runArgs += " x y z"; + var res = new RunCommand(s_buildEnv, _testOutput, label: id) + .WithWorkingDirectory(_projectDir!) + .ExecuteWithCapturedOutput(runArgs) + .EnsureExitCode(expectedExitCode); + + Assert.Contains("Hello, Wasi Console!", res.Output); + Assert.Contains("args[0] = x", res.Output); + Assert.Contains("args[1] = y", res.Output); + Assert.Contains("args[2] = z", res.Output); + } + + private static readonly string s_simpleMainWithArgs = """ + using System; + + Console.WriteLine("Hello, Wasi Console!"); + for (int i = 0; i < args.Length; i ++) + Console.WriteLine($"args[{i}] = {args[i]}"); + return 42; + """; +} diff --git a/src/mono/wasi/Wasi.Build.Tests/data/Local.Directory.Build.props b/src/mono/wasi/Wasi.Build.Tests/data/Local.Directory.Build.props new file mode 100644 index 0000000000000..216bb0ea3f6a2 --- /dev/null +++ b/src/mono/wasi/Wasi.Build.Tests/data/Local.Directory.Build.props @@ -0,0 +1,5 @@ + + + true + + diff --git a/src/mono/wasi/Wasi.Build.Tests/data/Local.Directory.Build.targets b/src/mono/wasi/Wasi.Build.Tests/data/Local.Directory.Build.targets new file mode 100644 index 0000000000000..6f9b3ab9ef999 --- /dev/null +++ b/src/mono/wasi/Wasi.Build.Tests/data/Local.Directory.Build.targets @@ -0,0 +1,5 @@ + + + + + diff --git a/src/mono/wasi/Wasi.Build.Tests/data/Workloads.Directory.Build.props b/src/mono/wasi/Wasi.Build.Tests/data/Workloads.Directory.Build.props new file mode 100644 index 0000000000000..6f96d44119c2e --- /dev/null +++ b/src/mono/wasi/Wasi.Build.Tests/data/Workloads.Directory.Build.props @@ -0,0 +1,6 @@ + + + wasi-wasm + true + + diff --git a/src/mono/wasi/Wasi.Build.Tests/data/Workloads.Directory.Build.targets b/src/mono/wasi/Wasi.Build.Tests/data/Workloads.Directory.Build.targets new file mode 100644 index 0000000000000..6f9b3ab9ef999 --- /dev/null +++ b/src/mono/wasi/Wasi.Build.Tests/data/Workloads.Directory.Build.targets @@ -0,0 +1,5 @@ + + + + + diff --git a/src/mono/wasi/Wasi.Build.Tests/data/nuget8.config b/src/mono/wasi/Wasi.Build.Tests/data/nuget8.config new file mode 100644 index 0000000000000..dfdc8009c6a5d --- /dev/null +++ b/src/mono/wasi/Wasi.Build.Tests/data/nuget8.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs index 6d88eb47a2563..cb24f659c96c9 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs @@ -31,6 +31,12 @@ public class BuildEnvironment public static readonly string TestDataPath = Path.Combine(AppContext.BaseDirectory, "data"); public static readonly string TmpPath = Path.Combine(AppContext.BaseDirectory, "wbt"); + public static readonly string DefaultRuntimeIdentifier = +#if TARGET_WASI + "wasi-wasm"; +#else + "browser-wasm"; +#endif private static readonly Dictionary s_runtimePackVersions = new(); @@ -51,11 +57,12 @@ public BuildEnvironment() if (string.IsNullOrEmpty(sdkForWorkloadPath)) { // Is this a "local run? + string sdkDirName = string.IsNullOrEmpty(EnvironmentVariables.SdkDirName) ? "dotnet-latest" : EnvironmentVariables.SdkDirName; string probePath = Path.Combine(Path.GetDirectoryName(typeof(BuildEnvironment).Assembly.Location)!, "..", "..", "..", - "dotnet-net7+latest"); + sdkDirName); if (Directory.Exists(probePath)) sdkForWorkloadPath = Path.GetFullPath(probePath); else @@ -133,9 +140,9 @@ public BuildEnvironment() // FIXME: error checks public string GetRuntimePackVersion(string tfm = BuildTestBase.DefaultTargetFramework) => s_runtimePackVersions[tfm]; public string GetRuntimePackDir(string tfm = BuildTestBase.DefaultTargetFramework) - => Path.Combine(WorkloadPacksDir, "Microsoft.NETCore.App.Runtime.Mono.browser-wasm", GetRuntimePackVersion(tfm)); + => Path.Combine(WorkloadPacksDir, $"Microsoft.NETCore.App.Runtime.Mono.{DefaultRuntimeIdentifier}", GetRuntimePackVersion(tfm)); public string GetRuntimeNativeDir(string tfm = BuildTestBase.DefaultTargetFramework) - => Path.Combine(GetRuntimePackDir(tfm), "runtimes", "browser-wasm", "native"); + => Path.Combine(GetRuntimePackDir(tfm), "runtimes", DefaultRuntimeIdentifier, "native"); protected static string s_directoryBuildPropsForWorkloads = File.ReadAllText(Path.Combine(TestDataPath, "Workloads.Directory.Build.props")); protected static string s_directoryBuildTargetsForWorkloads = File.ReadAllText(Path.Combine(TestDataPath, "Workloads.Directory.Build.targets")); diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs b/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs index fb86d5599a8e4..d29e9677fcac8 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs @@ -20,5 +20,6 @@ internal static class EnvironmentVariables internal static readonly bool ShowBuildOutput = Environment.GetEnvironmentVariable("SHOW_BUILD_OUTPUT") is not null; internal static readonly bool UseWebcil = Environment.GetEnvironmentVariable("USE_WEBCIL_FOR_TESTS") is "true"; internal static readonly string? SdkDirName = Environment.GetEnvironmentVariable("SDK_DIR_NAME"); + internal static readonly string? WasiSdkPath = Environment.GetEnvironmentVariable("WASI_SDK_PATH"); } } From 245c995d88864bb4f7d660243453261e33f98137 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 23 Feb 2023 11:57:37 -0500 Subject: [PATCH 06/11] [wasi] cleanup --- src/mono/wasi/Makefile | 1 + src/mono/wasi/wasi.proj | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mono/wasi/Makefile b/src/mono/wasi/Makefile index 2d61c7e4cca24..2905f09f803c7 100644 --- a/src/mono/wasi/Makefile +++ b/src/mono/wasi/Makefile @@ -10,6 +10,7 @@ endif DOTNET=$(TOP)/dotnet.sh +WASI_SDK_PATH?=$(TOP)/src/mono/wasi/wasi-sdk CONFIG?=Release BINDIR?=$(TOP)/artifacts/bin OBJDIR?=$(TOP)/artifacts/obj diff --git a/src/mono/wasi/wasi.proj b/src/mono/wasi/wasi.proj index 387af89db0951..a05bc72fe9b4a 100644 --- a/src/mono/wasi/wasi.proj +++ b/src/mono/wasi/wasi.proj @@ -92,8 +92,8 @@ <_WasiFlags Include="@(_WasiCommonFlags)" /> - <_WasiCompileFlags Condition="'$(MonoWasmThreads)' == 'true'" Include="-I$([MSBuild]::NormalizePath('$(PkgMicrosoft_NETCore_Runtime_ICU_Transport)', 'runtimes', 'browser-wasm-threads', 'native', 'include').Replace('\','/'))"/> - <_WasiCompileFlags Condition="'$(MonoWasmThreads)' != 'true'" Include="-I$([MSBuild]::NormalizePath('$(PkgMicrosoft_NETCore_Runtime_ICU_Transport)', 'runtimes', 'browser-wasm', 'native', 'include').Replace('\','/'))"/> + + <_WasiCompileFlags Include="-I$([MSBuild]::NormalizePath('$(MonoProjectRoot)', 'wasi', 'include').Replace('\','/'))"/> <_WasiCompileFlags Include="-I$([MSBuild]::NormalizePath('$(MonoProjectRoot)', 'wasi', 'mono-include').Replace('\','/'))"/> <_WasiCompileFlags Include="-I$([MSBuild]::NormalizePath('$(RepoRoot)', 'src', 'native', 'public').Replace('\','/'))"/> From 2f9ce4559fc6dcdb7aa9ae4155a470f96fe99fb9 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 23 Feb 2023 11:58:32 -0500 Subject: [PATCH 07/11] [wasi] liveBuilds.targets - add dotnet.wasm, and other files --- eng/liveBuilds.targets | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/eng/liveBuilds.targets b/eng/liveBuilds.targets index 5df409f12a259..4ff614b0c5061 100644 --- a/eng/liveBuilds.targets +++ b/eng/liveBuilds.targets @@ -222,6 +222,13 @@ + + Date: Thu, 23 Feb 2023 11:59:12 -0500 Subject: [PATCH 08/11] Add CI support for Wasi.Build.Tests --- .../runtime-extra-platforms-wasm.yml | 2 + .../libraries/helix-queues-setup.yml | 8 +- eng/pipelines/runtime.yml | 7 + eng/testing/tests.wasi.targets | 3 +- eng/testing/workloads-testing.targets | 2 +- src/libraries/Directory.Build.props | 3 +- src/libraries/sendtohelix-wasi.targets | 144 ++++++++++++++---- src/libraries/sendtohelix.proj | 2 +- src/libraries/sendtohelixhelp.proj | 5 +- src/libraries/tests.proj | 7 + 10 files changed, 141 insertions(+), 42 deletions(-) diff --git a/eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml b/eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml index de1be931bf469..444c2862bad82 100644 --- a/eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml +++ b/eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml @@ -200,6 +200,8 @@ jobs: platforms: - browser_wasm - browser_wasm_win + - wasi_wasm + - wasi_wasm_win isExtraPlatformsBuild: ${{ parameters.isExtraPlatformsBuild }} isWasmOnlyBuild: ${{ parameters.isWasmOnlyBuild }} diff --git a/eng/pipelines/libraries/helix-queues-setup.yml b/eng/pipelines/libraries/helix-queues-setup.yml index b3cdc2251f3ff..7105be6475591 100644 --- a/eng/pipelines/libraries/helix-queues-setup.yml +++ b/eng/pipelines/libraries/helix-queues-setup.yml @@ -187,8 +187,12 @@ jobs: - ${{ if eq(parameters.platform, 'windows_arm64') }}: - Windows.11.Arm64.Open - # Browser/WASI WebAssembly - - ${{ if in(parameters.platform, 'browser_wasm', 'wasi_wasm') }}: + # WASI + - ${{ if eq(parameters.platform, 'wasi_wasm') }}: + - (Ubuntu.2004.Amd64)Ubuntu.2004.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-20.04-helix-wasm-amd64 + + # Browser WebAssembly + - ${{ if eq(parameters.platform, 'browser_wasm') }}: - Ubuntu.1804.Amd64.Open # Browser WebAssembly Firefox diff --git a/eng/pipelines/runtime.yml b/eng/pipelines/runtime.yml index ef7bc0067cd1e..decedc69b710d 100644 --- a/eng/pipelines/runtime.yml +++ b/eng/pipelines/runtime.yml @@ -498,6 +498,13 @@ extends: scenarios: - normal + - template: /eng/pipelines/common/templates/wasm-build-tests.yml + parameters: + platforms: + - wasi_wasm + - wasi_wasm_win + alwaysRun: ${{ variables.isRollingBuild }} + # # iOS/tvOS devices - Full AOT + AggressiveTrimming to reduce size # Build the whole product using Mono and run libraries tests diff --git a/eng/testing/tests.wasi.targets b/eng/testing/tests.wasi.targets index 139c1d1c1244e..7adc77cc2a7ab 100644 --- a/eng/testing/tests.wasi.targets +++ b/eng/testing/tests.wasi.targets @@ -134,8 +134,7 @@ + Version="$(PackageVersionForWorkloadManifests)" /> diff --git a/eng/testing/workloads-testing.targets b/eng/testing/workloads-testing.targets index d7c03e320e4dd..541e476fad3d9 100644 --- a/eng/testing/workloads-testing.targets +++ b/eng/testing/workloads-testing.targets @@ -138,7 +138,7 @@ <_SdkWithWorkloadToInstall Include="@(WorkloadCombinationsToInstall)" /> <_SdkWithWorkloadToInstall InstallPath="$(_SdkForWorkloadTestingBasePath)\dotnet-%(Identity)" /> - <_SdkWithWorkloadToInstall StampPath="%(InstallPath)\.workload-installed.stamp" /> + <_SdkWithWorkloadToInstall StampPath="%(InstallPath)\.workload-installed.$(RuntimeIdentifier).stamp" /> diff --git a/src/libraries/Directory.Build.props b/src/libraries/Directory.Build.props index fee272a9edca6..7237fe3701da5 100644 --- a/src/libraries/Directory.Build.props +++ b/src/libraries/Directory.Build.props @@ -94,7 +94,8 @@ $(SdkWithNoWorkloadForTestingPath)version-$(SdkVersionForWorkloadTesting).stamp $(SdkWithNoWorkloadForTestingPath)workload.stamp - $(ArtifactsBinDir)dotnet-net7+latest\ + $(ArtifactsBinDir)dotnet-net7+latest\ + $(ArtifactsBinDir)dotnet-latest\ $([MSBuild]::NormalizeDirectory($(SdkWithWorkloadForTestingPath))) $(SdkWithWorkloadForTestingPath)version-$(SdkVersionForWorkloadTesting).stamp diff --git a/src/libraries/sendtohelix-wasi.targets b/src/libraries/sendtohelix-wasi.targets index 2ceec87c66e47..9998b62ecfd59 100644 --- a/src/libraries/sendtohelix-wasi.targets +++ b/src/libraries/sendtohelix-wasi.targets @@ -1,23 +1,49 @@ + + true - wasmtime + wasmtime true + <_ShippingPackagesPath>$([MSBuild]::NormalizeDirectory($(ArtifactsDir), 'packages', $(Configuration), 'Shipping')) PrepareHelixCorrelationPayload_Wasi; - _AddWorkItemsForLibraryTests + _AddWorkItemsForLibraryTests; + _AddWorkItemsForBuildWasmApps - $(BuildHelixWorkItemsDependsOn);PrepareForBuildHelixWorkItems_Wasi + $(BuildHelixWorkItemsDependsOn);StageWasiSdkForHelix;PrepareForBuildHelixWorkItems_Wasi false false - false + $(RepoRoot)src\mono\wasi\wasi-sdk\ + $([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'src', 'mono', 'wasi', 'wasi-sdk')) - + true + true + true true false true + + dotnet-latest + dotnet-none @@ -25,6 +51,7 @@ + @@ -32,45 +59,49 @@ + - + + <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' == 'true'">-notrait category=no-workload + <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' != 'true'">-trait category=no-workload + - - $(IsRunningLibraryTests) + + + - - true - + + - - - - true - - true - false - - - - - true - false - - - + + + + + + + + + + + + true + + true + false + - + + - + + + + @@ -107,4 +138,51 @@ + + + + Workloads- + NoWorkload- + + + + + <_WasmWorkItem Include="$(WorkItemArchiveWildCard)" Exclude="$(HelixCorrelationPayload)" /> + + + <_BuildWasmAppsPayloadArchive>@(_WasmWorkItem) + + + + + $(_BuildWasmAppsPayloadArchive) + $(HelixCommand) + $(_workItemTimeout) + + + + $(_BuildWasmAppsPayloadArchive) + $(HelixCommand) + $(_workItemTimeout) + + + + + + + + + + + + <_WasiSdkFiles Include="$(WASI_SDK_PATH)\**\*" Exclude="$(WASI_SDK_PATH)\.git\**\*" /> + + + + diff --git a/src/libraries/sendtohelix.proj b/src/libraries/sendtohelix.proj index 980c63e6ceb93..eee6da49b4583 100644 --- a/src/libraries/sendtohelix.proj +++ b/src/libraries/sendtohelix.proj @@ -81,7 +81,7 @@ <_TestUsingWorkloadsValues Include="true;false" /> - <_TestUsingWebcilValues Include="true;false" /> + <_TestUsingWebcilValues Include="true;false" Condition="'$(TargetOS)' == 'browser'" /> <_TestUsingCrossProductValuesTemp Include="@(_TestUsingWorkloadsValues)"> diff --git a/src/libraries/sendtohelixhelp.proj b/src/libraries/sendtohelixhelp.proj index b2e072c486c84..e51814d790978 100644 --- a/src/libraries/sendtohelixhelp.proj +++ b/src/libraries/sendtohelixhelp.proj @@ -128,8 +128,9 @@ - - + + + diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 8e5c828464045..046b71d5c5da9 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -591,6 +591,13 @@ ('$(ContinuousIntegrationBuild)' != 'true' and '$(TestAssemblies)' == 'true'))" BuildInParallel="false" /> + + $(ArtifactsBinDir)dotnet-net7+latest\ $(ArtifactsBinDir)dotnet-latest\ - $([MSBuild]::NormalizeDirectory($(SdkWithWorkloadForTestingPath))) + $([MSBuild]::NormalizeDirectory($(SdkWithWorkloadForTestingPath))) $(SdkWithWorkloadForTestingPath)version-$(SdkVersionForWorkloadTesting).stamp $(SdkWithWorkloadForTestingPath)workload.stamp From dbbcdd17665f7a69c598c5475ac5347c0c74d24d Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 23 Feb 2023 20:22:46 +0000 Subject: [PATCH 10/11] Link required libc++*.a from wasi-sdk --- src/mono/wasi/build/WasiApp.Native.targets | 3 +++ src/mono/wasi/build/WasiSdk.Defaults.props | 1 + 2 files changed, 4 insertions(+) diff --git a/src/mono/wasi/build/WasiApp.Native.targets b/src/mono/wasi/build/WasiApp.Native.targets index 98a63f0312e64..9fa1d9f0b9d1c 100644 --- a/src/mono/wasi/build/WasiApp.Native.targets +++ b/src/mono/wasi/build/WasiApp.Native.targets @@ -280,6 +280,9 @@ Exclude="@(_MonoRuntimeComponentDontLink->'$(MicrosoftNetCoreAppRuntimePackRidNativeDir)%(Identity)')" /> <_WasmNativeFileForLinking Condition="'$(_WasmEHLib)' != ''" Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)$(_WasmEHLib)" /> <_WasmNativeFileForLinking Remove="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)$(_WasmEHLibToExclude)" /> + + <_WasmNativeFileForLinking Include="$(WasiSysRoot)\lib\wasm32-wasi\libc++.a" /> + <_WasmNativeFileForLinking Include="$(WasiSysRoot)\lib\wasm32-wasi\libc++abi.a" /> diff --git a/src/mono/wasi/build/WasiSdk.Defaults.props b/src/mono/wasi/build/WasiSdk.Defaults.props index 51ce6b50a2eff..67e2e51380200 100644 --- a/src/mono/wasi/build/WasiSdk.Defaults.props +++ b/src/mono/wasi/build/WasiSdk.Defaults.props @@ -1,6 +1,7 @@ $(WASI_SDK_PATH) + $([MSBuild]::NormalizeDirectory($(WasiSdkRoot), 'share', 'wasi-sysroot')) $(WasiSdkRoot)\bin\clang $(WasiClang).exe From 70bc1851665ac58ca7ce47e845cf0f259e35a89e Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 23 Feb 2023 20:22:59 +0000 Subject: [PATCH 11/11] Address earlier review feedback from @pavelsavara --- src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs b/src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs index 11d0f7fd9d6ac..7ecf89b8c36e1 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs @@ -68,7 +68,7 @@ public override bool Execute() protected virtual bool ValidateArguments() => true; - protected void ProcessSatelliteAssemblies(Action<(string fullPath, string culture)> fn) + protected void ProcessSatelliteAssemblies(Action<(string fullPath, string culture)> addSatelliteAssemblyFunc) { foreach (var assembly in SatelliteAssemblies) { @@ -81,7 +81,7 @@ protected void ProcessSatelliteAssemblies(Action<(string fullPath, string cultur } // FIXME: validate the culture? - fn((fullPath, culture)); + addSatelliteAssemblyFunc((fullPath, culture)); } }