diff --git a/eng/pipelines/mono/templates/workloads-build.yml b/eng/pipelines/mono/templates/workloads-build.yml index a6b6e759306880..0b6e057e8222d9 100644 --- a/eng/pipelines/mono/templates/workloads-build.yml +++ b/eng/pipelines/mono/templates/workloads-build.yml @@ -65,6 +65,7 @@ jobs: IntermediateArtifacts/MonoRuntimePacks/Shipping/Microsoft.NET.Runtime.MonoTargets.Sdk*.nupkg IntermediateArtifacts/MonoRuntimePacks/Shipping/Microsoft.NET.Runtime.MonoAOTCompiler.Task*.nupkg IntermediateArtifacts/MonoRuntimePacks/Shipping/Microsoft.NET.Runtime.WebAssembly.Sdk*.nupkg + IntermediateArtifacts/MonoRuntimePacks/Shipping/Microsoft.NET.Runtime.WebAssembly.Templates*.nupkg - task: CopyFiles@2 displayName: Flatten packages diff --git a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Manifest/WorkloadManifest.json.in b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Manifest/WorkloadManifest.json.in index e34ff6d173b85a..b0fd53947e1351 100644 --- a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Manifest/WorkloadManifest.json.in +++ b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Manifest/WorkloadManifest.json.in @@ -8,6 +8,7 @@ "description": ".NET WebAssembly build tools", "packs": [ "Microsoft.NET.Runtime.WebAssembly.Sdk", + "Microsoft.NET.Runtime.WebAssembly.Templates", "Microsoft.NETCore.App.Runtime.Mono.browser-wasm", "Microsoft.NETCore.App.Runtime.AOT.Cross.browser-wasm" ], @@ -140,6 +141,10 @@ "kind": "Sdk", "version": "${PackageVersion}" }, + "Microsoft.NET.Runtime.WebAssembly.Templates": { + "kind": "template", + "version": "${PackageVersion}" + }, "Microsoft.NETCore.App.Runtime.Mono.android-arm": { "kind": "framework", "version": "${PackageVersion}" diff --git a/src/mono/nuget/mono-packages.proj b/src/mono/nuget/mono-packages.proj index a56c63f8827642..e57f6855d975f9 100644 --- a/src/mono/nuget/mono-packages.proj +++ b/src/mono/nuget/mono-packages.proj @@ -7,6 +7,7 @@ + diff --git a/src/mono/wasm/README.md b/src/mono/wasm/README.md index 37d5fa29643445..a016b9a1aa3001 100644 --- a/src/mono/wasm/README.md +++ b/src/mono/wasm/README.md @@ -161,7 +161,25 @@ To build and run the samples with AOT, add `/p:RunAOTCompilation=true` to the ab Also check [bench](../sample/wasm/browser-bench/README.md) sample to measure mono/wasm runtime performance. -### Upgrading Emscripten +## Templates + +The wasm templates, located in the `templates` directory, are templates for `dotnet new`, VS and VS for Mac. They are packaged and distributed as part of the `wasm-tools` workload. We have 2 templates, `wasmbrowser` and `wasmconsole`, for browser and console WebAssembly applications. + +For details about using `dotnet new` see the dotnet tool [documentation](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-new). + +To test changes in the templates, use `dotnet new -i `. + +Example use of the `wasmconsole` template: + + > dotnet new wasmconsole + > dotnet publish + > cd bin/Debug/net7.0/browser-wasm/AppBundle + > node main.cjs + mono_wasm_runtime_ready fe00e07a-5519-4dfe-b35a-f867dbaf2e28 + Hello World! + Args: + +## Upgrading Emscripten Bumping Emscripten version involves these steps: diff --git a/src/mono/wasm/templates/Microsoft.NET.Runtime.WebAssembly.Templates.csproj b/src/mono/wasm/templates/Microsoft.NET.Runtime.WebAssembly.Templates.csproj new file mode 100644 index 00000000000000..77f730d55b23db --- /dev/null +++ b/src/mono/wasm/templates/Microsoft.NET.Runtime.WebAssembly.Templates.csproj @@ -0,0 +1,33 @@ + + + + Template + Microsoft.NET.Runtime.WebAssembly.Templates + WebAssembly Templates + Microsoft + Templates to create WebAssembly projects. + dotnet-new;templates + + net6.0 + + true + false + content + $(NoWarn);NU5128 + true + + + + + + + + + + + + $(ProductVersion) + + + + diff --git a/src/mono/wasm/templates/templates/browser/.template.config/template.json b/src/mono/wasm/templates/templates/browser/.template.config/template.json new file mode 100644 index 00000000000000..e9736c68a1ba9c --- /dev/null +++ b/src/mono/wasm/templates/templates/browser/.template.config/template.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": [ "Web", "WebAssembly", "Browser" ], + "identity": "WebAssembly.Browser", + "name": "WebAssembly Browser App", + "shortName": "wasmbrowser", + "tags": { + "language": "C#", + "type": "project" + } +} diff --git a/src/mono/wasm/templates/templates/browser/Program.cs b/src/mono/wasm/templates/templates/browser/Program.cs new file mode 100644 index 00000000000000..baa90549b70363 --- /dev/null +++ b/src/mono/wasm/templates/templates/browser/Program.cs @@ -0,0 +1,12 @@ +using System; +using System.Runtime.CompilerServices; + +Console.WriteLine ("Hello, Console!"); + +public class MyClass { + [MethodImpl(MethodImplOptions.NoInlining)] + public static string CallMeFromJS() + { + return "Hello, World!"; + } +} diff --git a/src/mono/wasm/templates/templates/browser/browser.csproj b/src/mono/wasm/templates/templates/browser/browser.csproj new file mode 100644 index 00000000000000..6b36bc95f7f585 --- /dev/null +++ b/src/mono/wasm/templates/templates/browser/browser.csproj @@ -0,0 +1,17 @@ + + + net7.0 + wasm + Browser + browser-wasm + true + main.js + Exe + true + + + + + + + diff --git a/src/mono/wasm/templates/templates/browser/index.html b/src/mono/wasm/templates/templates/browser/index.html new file mode 100644 index 00000000000000..3cc12e3212d6b5 --- /dev/null +++ b/src/mono/wasm/templates/templates/browser/index.html @@ -0,0 +1,19 @@ + + + + + + + Sample ES6 + + + + + + + + + + + + diff --git a/src/mono/wasm/templates/templates/browser/main.js b/src/mono/wasm/templates/templates/browser/main.js new file mode 100644 index 00000000000000..56c9b350313ec1 --- /dev/null +++ b/src/mono/wasm/templates/templates/browser/main.js @@ -0,0 +1,13 @@ +import createDotnetRuntime from './dotnet.js' + +try { + const { MONO, BINDING, Module, RuntimeBuildInfo } = await createDotnetRuntime(); + const managedMethod = BINDING.bind_static_method("[browser] MyClass:CallMeFromJS"); + const text = managedMethod(); + document.getElementById("out").innerHTML = `${text}`; + + await MONO.mono_run_main("browser.dll", []); +} catch (err) { + console.log(`WASM ERROR ${err}`); + document.getElementById("out").innerHTML = `error: ${err}`; +} diff --git a/src/mono/wasm/templates/templates/console/.template.config/template.json b/src/mono/wasm/templates/templates/console/.template.config/template.json new file mode 100644 index 00000000000000..0a67c332bb7d7e --- /dev/null +++ b/src/mono/wasm/templates/templates/console/.template.config/template.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": [ "Web", "WebAssembly", "Console" ], + "identity": "WebAssembly.Console", + "name": "WebAssembly Console App", + "shortName": "wasmconsole", + "tags": { + "language": "C#", + "type": "project" + } +} diff --git a/src/mono/wasm/templates/templates/console/Program.cs b/src/mono/wasm/templates/templates/console/Program.cs new file mode 100644 index 00000000000000..3a242248ba2090 --- /dev/null +++ b/src/mono/wasm/templates/templates/console/Program.cs @@ -0,0 +1,9 @@ +using System; +using System.Threading.Tasks; + +Console.WriteLine("Hello World!"); + +Console.WriteLine("Args:"); +for (int i = 0; i < args.Length; i++) { + Console.WriteLine($" args[{i}] = {args[i]}"); +} diff --git a/src/mono/wasm/templates/templates/console/README.md b/src/mono/wasm/templates/templates/console/README.md new file mode 100644 index 00000000000000..0153a8574c1a94 --- /dev/null +++ b/src/mono/wasm/templates/templates/console/README.md @@ -0,0 +1,7 @@ +Node/CommonJS console App + +Run the published application like: + + node main.cjs + +in `bin/$(Configuration)/net7.0/browser-wasm/AppBundle` directory. diff --git a/src/mono/wasm/templates/templates/console/console.csproj b/src/mono/wasm/templates/templates/console/console.csproj new file mode 100644 index 00000000000000..fad622389dc305 --- /dev/null +++ b/src/mono/wasm/templates/templates/console/console.csproj @@ -0,0 +1,12 @@ + + + net7.0 + wasm + Browser + browser-wasm + true + main.cjs + Exe + false + + diff --git a/src/mono/wasm/templates/templates/console/main.cjs b/src/mono/wasm/templates/templates/console/main.cjs new file mode 100644 index 00000000000000..18823502c64fa7 --- /dev/null +++ b/src/mono/wasm/templates/templates/console/main.cjs @@ -0,0 +1,8 @@ +const createDotnetRuntime = require("./dotnet.js"); + +async function main() { + const { MONO } = await createDotnetRuntime(); + const app_args = process.argv.slice(2); + await MONO.mono_run_main_and_exit("console.dll", app_args); +}; +main(); diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs index ec37a933562bd7..4d3abaa401abec 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs @@ -127,7 +127,8 @@ protected string RunAndTestWasmApp(BuildArgs buildArgs, string? buildDir = null, int expectedExitCode = 0, string? args = null, - Dictionary? envVars = null) + Dictionary? envVars = null, + string targetFramework = "net6.0") { buildDir ??= _projectDir; envVars ??= new(); @@ -144,7 +145,7 @@ protected string RunAndTestWasmApp(BuildArgs buildArgs, envVars[kvp.Key] = kvp.Value; } - string bundleDir = Path.Combine(GetBinDir(baseDir: buildDir, config: buildArgs.Config), "AppBundle"); + string bundleDir = Path.Combine(GetBinDir(baseDir: buildDir, config: buildArgs.Config, targetFramework: targetFramework), "AppBundle"); (string testCommand, string extraXHarnessArgs) = host switch { RunHost.V8 => ("wasm test", "--js-file=test-main.js --engine=V8 -v trace"), @@ -341,8 +342,8 @@ protected static BuildArgs ExpandBuildArgs(BuildArgs buildArgs, string extraProp if (options.ExpectSuccess) { - string bundleDir = Path.Combine(GetBinDir(config: buildArgs.Config), "AppBundle"); - AssertBasicAppBundle(bundleDir, buildArgs.ProjectName, buildArgs.Config, options.HasIcudt, options.DotnetWasmFromRuntimePack ?? !buildArgs.AOT); + string bundleDir = Path.Combine(GetBinDir(config: buildArgs.Config, targetFramework: options.TargetFramework ?? "net6.0"), "AppBundle"); + AssertBasicAppBundle(bundleDir, buildArgs.ProjectName, buildArgs.Config, options.MainJS ?? "test-main.js", options.HasV8Script, options.HasIcudt, options.DotnetWasmFromRuntimePack ?? !buildArgs.AOT); } if (options.UseCache) @@ -371,6 +372,18 @@ public void InitBlazorWasmProjectDir(string id) File.Copy(Path.Combine(BuildEnvironment.TestDataPath, "Blazor.Directory.Build.targets"), Path.Combine(_projectDir, "Directory.Build.targets")); } + public string CreateWasmTemplateProject(string id, string template = "wasmbrowser") + { + InitPaths(id); + InitProjectDir(id); + new DotNetCommand(s_buildEnv, useDefaultArgs: false) + .WithWorkingDirectory(_projectDir!) + .ExecuteWithCapturedOutput($"new {template}") + .EnsureSuccessful(); + + return Path.Combine(_projectDir!, $"{id}.csproj"); + } + public string CreateBlazorWasmTemplateProject(string id) { InitBlazorWasmProjectDir(id); @@ -476,19 +489,19 @@ static void AssertRuntimePackPath(string buildOutput) throw new XunitException($"Runtime pack path doesn't match.{Environment.NewLine}Expected: {s_buildEnv.RuntimePackDir}{Environment.NewLine}Actual: {actualPath}"); } - protected static void AssertBasicAppBundle(string bundleDir, string projectName, string config, bool hasIcudt=true, bool dotnetWasmFromRuntimePack=true) + protected static void AssertBasicAppBundle(string bundleDir, string projectName, string config, string mainJS, bool hasV8Script, bool hasIcudt=true, bool dotnetWasmFromRuntimePack=true) { AssertFilesExist(bundleDir, new [] { "index.html", - "test-main.js", + mainJS, "dotnet.timezones.blat", "dotnet.wasm", "mono-config.json", - "dotnet.js", - "run-v8.sh" + "dotnet.js" }); + AssertFilesExist(bundleDir, new[] { "run-v8.sh" }, expectToExist: hasV8Script); AssertFilesExist(bundleDir, new[] { "icudt.dat" }, expectToExist: hasIcudt); string managedDir = Path.Combine(bundleDir, "managed"); @@ -866,7 +879,10 @@ public record BuildProjectOptions bool CreateProject = true, bool Publish = true, bool BuildOnlyAfterPublish = true, + bool HasV8Script = true, string? Verbosity = null, - string? Label = null + string? Label = null, + string? TargetFramework = null, + string? MainJS = null ); } diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmTemplateTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmTemplateTests.cs new file mode 100644 index 00000000000000..d3684c3cfe3db9 --- /dev/null +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmTemplateTests.cs @@ -0,0 +1,107 @@ +// 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 Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +#nullable enable + +namespace Wasm.Build.Tests +{ + public class WasmTemplateTests : BuildTestBase + { + public WasmTemplateTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + } + + [Theory] + [InlineData("Debug")] + [InlineData("Release")] + public void BrowserBuildThenPublish(string config) + { + string id = $"{config}_{Path.GetRandomFileName()}"; + string projectName = $"browser"; + CreateWasmTemplateProject(id, "wasmbrowser"); + + var buildArgs = new BuildArgs(projectName, config, false, id, null); + buildArgs = ExpandBuildArgs(buildArgs); + + BuildProject(buildArgs, + id: id, + new BuildProjectOptions( + DotnetWasmFromRuntimePack: false, + CreateProject: false, + HasV8Script: false, + MainJS: "main.js", + Publish: false, + TargetFramework: "net7.0" + )); + + 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}"); + Console.WriteLine($"{Environment.NewLine}Publishing with no changes ..{Environment.NewLine}"); + + BuildProject(buildArgs, + id: id, + new BuildProjectOptions( + DotnetWasmFromRuntimePack: false, + CreateProject: false, + HasV8Script: false, + MainJS: "main.js", + Publish: true, + TargetFramework: "net7.0", + UseCache: false)); + } + + [Theory] + [InlineData("Debug")] + [InlineData("Release")] + public void ConsoleBuildThenPublish(string config) + { + string id = $"{config}_{Path.GetRandomFileName()}"; + string projectName = $"console"; + CreateWasmTemplateProject(id, "wasmconsole"); + + var buildArgs = new BuildArgs(projectName, config, false, id, null); + buildArgs = ExpandBuildArgs(buildArgs); + + BuildProject(buildArgs, + id: id, + new BuildProjectOptions( + DotnetWasmFromRuntimePack: false, + CreateProject: false, + HasV8Script: false, + MainJS: "main.mjs", + Publish: false, + TargetFramework: "net7.0" + )); + + 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}"); + Console.WriteLine($"{Environment.NewLine}Publishing with no changes ..{Environment.NewLine}"); + + BuildProject(buildArgs, + id: id, + new BuildProjectOptions( + DotnetWasmFromRuntimePack: false, + CreateProject: false, + HasV8Script: false, + MainJS: "main.mjs", + Publish: true, + TargetFramework: "net7.0", + UseCache: false)); + } + } +}