diff --git a/eng/AcquireEmscriptenSdk.proj b/eng/AcquireEmscriptenSdk.proj
index 1b5f158d778604..dad96ee3a08e67 100644
--- a/eng/AcquireEmscriptenSdk.proj
+++ b/eng/AcquireEmscriptenSdk.proj
@@ -6,6 +6,14 @@
-
+
+
diff --git a/eng/pipelines/common/templates/browser-wasm-coreclr-build-tests.yml b/eng/pipelines/common/templates/browser-wasm-coreclr-build-tests.yml
index cdb9b26ba1dd69..ac76b6332c3801 100644
--- a/eng/pipelines/common/templates/browser-wasm-coreclr-build-tests.yml
+++ b/eng/pipelines/common/templates/browser-wasm-coreclr-build-tests.yml
@@ -100,6 +100,21 @@ jobs:
TargetFolder: '$(Build.SourcesDirectory)/artifacts'
CleanTargetFolder: false
+ # Provision emsdk on this agent so src/mono/browser/emsdk/ is populated
+ # before sendtohelix runs. The upstream runtime-build job acquires emsdk on
+ # its own agent (via src/coreclr/runtime.proj's BuildRuntimeDependsOnTargets),
+ # but that state is not shipped in its build artifacts. Mono WBT provisions
+ # implicitly because it builds the runtime in the same job; CoreCLR WBT is
+ # split across two agents, so we acquire it here on the WBT agent.
+ - ${{ if eq(platform, 'browser_wasm_win') }}:
+ - script: build.cmd -s provision.emsdk -os browser -c $(_BuildConfig)
+ displayName: Provision emsdk
+ workingDirectory: $(Build.SourcesDirectory)
+ - ${{ else }}:
+ - script: ./build.sh -s provision.emsdk -os browser -c $(_BuildConfig)
+ displayName: Provision emsdk
+ workingDirectory: $(Build.SourcesDirectory)
+
# build WBT
buildArgs: >-
$(wbtProjectArg) $(Build.SourcesDirectory)/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj
diff --git a/src/libraries/sendtohelix-browser.targets b/src/libraries/sendtohelix-browser.targets
index 3db4fb049625a5..3af0478473c414 100644
--- a/src/libraries/sendtohelix-browser.targets
+++ b/src/libraries/sendtohelix-browser.targets
@@ -62,6 +62,8 @@
true
true
+
+ true
false
true
true
@@ -88,13 +90,22 @@
<_HelixLocalNodePath Condition="'$(NeedsEMSDK)' == 'true' and '$(WindowsShell)' == 'true'">%HELIX_CORRELATION_PAYLOAD%\build\emsdk\node
-
+
+
+ SourcePath="$(EMSDK_PATH)" />
@@ -135,6 +146,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
$(RepositoryEngineeringDir)testing\scenarios\BuildWasmAppsJobsList.txt
$(RepositoryEngineeringDir)testing\scenarios\BuildWasmAppsJobsListCoreCLR.txt
@@ -214,15 +246,41 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+ <_CoreCLRWbtEngPayloadDir>$(ArtifactsObjDir)helix-staging\coreclr-wbt-eng\
+
+
+ <_CoreCLRWbtEngFiles Include="$(RepositoryEngineeringDir)native.wasm.targets" />
+ <_CoreCLRWbtEngFiles Include="$(RepositoryEngineeringDir)AcquireEmscriptenSdk.targets" />
+
+
+
+
diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs
index 8bdb3cd76134d7..77be019fbfaf5b 100644
--- a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs
@@ -151,6 +151,20 @@ public BuildEnvironment()
// EnvVars["WasmTestForwardConsole"] = "true"; // only necessary for firefox, because chromedriver supports it natively
// EnvVars["WasmTestAsyncFlushOnExit"] = "true"; // only necessary for old nodejs versions
// EnvVars["WasmTestAppendElementOnExit"] = "true"; // only used by xharness // https://github.com/dotnet/xharness/blob/799df8d4c86ff50c83b7a57df9e3691eeab813ec/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmBrowserTestRunner.cs#L122-L141
+
+ // Flow paths required by BrowserWasmApp.CoreCLR.targets into the dotnet-new-generated
+ // test project builds so its import chain (native.wasm.targets / AcquireEmscriptenSdk.targets)
+ // and UsingTask for WasmAppBuilder.dll resolve on both Helix and local runs.
+ // Names match those read by data/Local.Directory.Build.{props,targets}; MSBuild exposes
+ // env vars as properties with the same name, so SCREAMING_SNAKE keys stay consistent.
+ if (!string.IsNullOrEmpty(EnvironmentVariables.RepositoryEngineeringDir))
+ EnvVars["REPOSITORY_ENGINEERING_DIR"] = EnvironmentVariables.RepositoryEngineeringDir;
+ if (!string.IsNullOrEmpty(EnvironmentVariables.BrowserBuildTargetsDir))
+ EnvVars["BROWSER_BUILD_TARGETS_DIR"] = EnvironmentVariables.BrowserBuildTargetsDir;
+ if (!string.IsNullOrEmpty(EnvironmentVariables.WasmAppBuilderTasksAssemblyPath))
+ EnvVars["WASM_APP_BUILDER_TASKS_ASSEMBLY_PATH"] = EnvironmentVariables.WasmAppBuilderTasksAssemblyPath;
+ if (!string.IsNullOrEmpty(EnvironmentVariables.EmsdkPath))
+ EnvVars["EMSDK_PATH"] = EnvironmentVariables.EmsdkPath;
}
DotNet = Path.Combine(sdkForWorkloadPath!, "dotnet");
diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs b/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs
index fb12b0830c7a0f..c121945a78c226 100644
--- a/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs
@@ -28,5 +28,9 @@ internal static class EnvironmentVariables
internal static readonly string? WasiSdkPath = Environment.GetEnvironmentVariable("WASI_SDK_PATH");
internal static readonly bool WorkloadsTestPreviousVersions = Environment.GetEnvironmentVariable("WORKLOADS_TEST_PREVIOUS_VERSIONS") is "true";
internal static readonly string? RuntimeFlavor = Environment.GetEnvironmentVariable("RUNTIME_FLAVOR_FOR_TESTS");
+ internal static readonly string? RepositoryEngineeringDir = Environment.GetEnvironmentVariable("REPOSITORY_ENGINEERING_DIR");
+ internal static readonly string? BrowserBuildTargetsDir = Environment.GetEnvironmentVariable("BROWSER_BUILD_TARGETS_DIR");
+ internal static readonly string? WasmAppBuilderTasksAssemblyPath = Environment.GetEnvironmentVariable("WASM_APP_BUILDER_TASKS_ASSEMBLY_PATH");
+ internal static readonly string? EmsdkPath = Environment.GetEnvironmentVariable("EMSDK_PATH");
}
}
diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/NativeBuildTests.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/NativeBuildTests.cs
index 34a81b98004a9d..000513a7c766be 100644
--- a/src/mono/wasm/Wasm.Build.Tests/Templates/NativeBuildTests.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/Templates/NativeBuildTests.cs
@@ -19,6 +19,8 @@ public NativeBuildTests(ITestOutputHelper output, SharedBuildPerTestClassFixture
{
}
+ // Excluded on CoreCLR via the `category=native` trait filter: WasmAllowUndefinedSymbols=false
+ // is not honored on the CoreCLR native-build path. See https://github.com/dotnet/runtime/pull/127073.
[Theory]
[InlineData(true)]
[InlineData(false)]
@@ -57,6 +59,10 @@ public void BuildWithUndefinedNativeSymbol(bool allowUndefined)
}
}
+ // Excluded on CoreCLR via the `category=native` trait filter: the default template's main.js calls
+ // getAssemblyExports() which throws on CoreCLR when the user assembly has no [JSExport]
+ // (JSHostImplementation.CoreCLR.BindAssemblyExports uses throwOnError: true, while Mono's native
+ // path is tolerant). See https://github.com/dotnet/runtime/pull/127073.
[Theory]
[InlineData(Configuration.Debug)]
[InlineData(Configuration.Release)]
diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs
index dd89863382693b..c5d56480561582 100644
--- a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
@@ -27,6 +28,9 @@ public class WasmTemplateTestsBase : BuildTestBase
protected readonly BuildOptions _defaultBuildOptions;
protected const string DefaultRuntimeAssetsRelativePath = "./_framework/";
+ private static bool s_wasmTemplatesInstalled;
+ private static readonly object s_wasmTemplatesLock = new();
+
public WasmTemplateTestsBase(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext, ProjectProviderBase? provider = null)
: base(provider ?? new WasmSdkBasedProjectProvider(output, DefaultTargetFramework), output, buildContext)
{
@@ -83,6 +87,8 @@ public ProjectInfo CreateWasmTemplateProject(
extraArgs += $" -f {defaultTarget}";
}
+ EnsureWasmTemplatesInstalled();
+
using DotNetCommand cmd = new DotNetCommand(s_buildEnv, _testOutput, useDefaultArgs: false);
CommandResult result = cmd.WithWorkingDirectory(_projectDir)
.WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir)
@@ -173,6 +179,96 @@ private static void AddCoreClrProjectProperties(ref string extraProperties, ref
""";
}
+ ///
+ /// Installs the WASM browser/console templates from the built nugets path
+ /// using dotnet new install if needed. This is a no-op when
+ /// the workload is already installed (templates come with the workload).
+ ///
+ private static void EnsureWasmTemplatesInstalled()
+ {
+ if (s_buildEnv.IsWorkload)
+ return;
+
+ if (s_wasmTemplatesInstalled)
+ return;
+
+ lock (s_wasmTemplatesLock)
+ {
+ if (s_wasmTemplatesInstalled)
+ return;
+
+ string? templateNupkg = Directory.GetFiles(s_buildEnv.BuiltNuGetsPath, "Microsoft.NET.Runtime.WebAssembly.Templates.*.nupkg")
+ .Where(f => !f.EndsWith(".symbols.nupkg", StringComparison.OrdinalIgnoreCase))
+ .FirstOrDefault();
+
+ if (templateNupkg is null)
+ throw new InvalidOperationException(
+ $"Could not find WebAssembly template nupkg in '{s_buildEnv.BuiltNuGetsPath}'");
+
+ Console.WriteLine($"[templates] Installing WASM templates from {templateNupkg} using {s_buildEnv.DotNet}");
+
+ var psi = new ProcessStartInfo
+ {
+ FileName = s_buildEnv.DotNet,
+ Arguments = $"new install \"{templateNupkg}\" --force",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false
+ };
+ // Isolate the template cache so the install doesn't touch the default user profile
+ // and can't collide across Helix jobs/agents (or fail if the profile is not writable).
+ string dotnetCliHome = s_buildEnv.EnvVars.TryGetValue("NUGET_PACKAGES", out string? nugetPackagesPath) && !string.IsNullOrWhiteSpace(nugetPackagesPath)
+ ? Path.Combine(Path.GetDirectoryName(nugetPackagesPath)!, ".dotnet-cli-home")
+ : Path.Combine(Path.GetDirectoryName(templateNupkg)!, ".dotnet-cli-home");
+ Directory.CreateDirectory(dotnetCliHome);
+
+ // Use the same isolated environment as the rest of the test suite
+ // (DOTNET_ROOT/DOTNET_INSTALL_DIR/PATH/NUGET_PACKAGES overrides), so
+ // `dotnet new install` picks up the harness's SDK and NuGet config.
+ foreach (var kvp in s_buildEnv.EnvVars)
+ psi.Environment[kvp.Key] = kvp.Value;
+ psi.Environment["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1";
+ psi.Environment["DOTNET_CLI_HOME"] = dotnetCliHome;
+
+ using var process = Process.Start(psi)
+ ?? throw new InvalidOperationException("Failed to start 'dotnet new install' process");
+
+ var stdoutTask = process.StandardOutput.ReadToEndAsync();
+ var stderrTask = process.StandardError.ReadToEndAsync();
+
+ const int processTimeoutMilliseconds = 120_000;
+ if (!process.WaitForExit(processTimeoutMilliseconds))
+ {
+ try
+ {
+ if (!process.HasExited)
+ process.Kill(entireProcessTree: true);
+ }
+ catch (InvalidOperationException)
+ {
+ }
+
+ process.WaitForExit();
+
+ string timedOutStdout = stdoutTask.GetAwaiter().GetResult();
+ string timedOutStderr = stderrTask.GetAwaiter().GetResult();
+
+ throw new InvalidOperationException(
+ $"'dotnet new install' timed out after {processTimeoutMilliseconds} ms.\nStdout: {timedOutStdout}\nStderr: {timedOutStderr}");
+ }
+
+ string stdout = stdoutTask.GetAwaiter().GetResult();
+ string stderr = stderrTask.GetAwaiter().GetResult();
+
+ if (process.ExitCode != 0)
+ throw new InvalidOperationException(
+ $"'dotnet new install' failed with exit code {process.ExitCode}.\nStdout: {stdout}\nStderr: {stderr}");
+
+ Console.WriteLine($"[templates] WASM template install completed successfully");
+ s_wasmTemplatesInstalled = true;
+ }
+ }
+
public virtual (string projectDir, string buildOutput) PublishProject(
ProjectInfo info,
Configuration configuration,
diff --git a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj
index a39b1f10b2c9e5..950f47c9b3ebe0 100644
--- a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj
+++ b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj
@@ -114,6 +114,11 @@
<_SdkPathForLocalTesting Condition="'$(TestUsingWorkloads)' != 'true'">$([System.IO.Path]::GetDirectoryName($(SdkWithNoWorkloadForTestingPath)))
<_SdkPathForLocalTesting>$([System.IO.Path]::GetFilename($(_SdkPathForLocalTesting)))
+
+
+ <_LocalEmsdkPathForTests Condition="'$(RuntimeFlavor)' == 'CoreCLR' and '$(EMSDK_PATH)' != ''">$(EMSDK_PATH)
+ <_LocalEmsdkPathForTests Condition="'$(RuntimeFlavor)' == 'CoreCLR' and '$(_LocalEmsdkPathForTests)' == ''">$([MSBuild]::NormalizeDirectory('$(BrowserProjectRoot)', 'emsdk'))
@@ -145,6 +150,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/mono/wasm/Wasm.Build.Tests/data/Local.Directory.Build.props b/src/mono/wasm/Wasm.Build.Tests/data/Local.Directory.Build.props
index 058246e4086204..cfc91b807cc6c3 100644
--- a/src/mono/wasm/Wasm.Build.Tests/data/Local.Directory.Build.props
+++ b/src/mono/wasm/Wasm.Build.Tests/data/Local.Directory.Build.props
@@ -1 +1,20 @@
-
+
+
+
+ $(RUNTIME_FLAVOR_FOR_TESTS)
+
+
+
+ $([MSBuild]::EnsureTrailingSlash($([System.IO.Path]::GetFullPath('$(REPOSITORY_ENGINEERING_DIR)'))))
+ $([System.IO.Path]::GetFullPath('$(WASM_APP_BUILDER_TASKS_ASSEMBLY_PATH)'))
+
+
\ No newline at end of file
diff --git a/src/mono/wasm/Wasm.Build.Tests/data/Local.Directory.Build.targets b/src/mono/wasm/Wasm.Build.Tests/data/Local.Directory.Build.targets
index 6f9b3ab9ef9994..4349163c8f8f88 100644
--- a/src/mono/wasm/Wasm.Build.Tests/data/Local.Directory.Build.targets
+++ b/src/mono/wasm/Wasm.Build.Tests/data/Local.Directory.Build.targets
@@ -2,4 +2,18 @@
+
+
+
+ <_CoreCLRBrowserBuildTargetsDir Condition="'$(BROWSER_BUILD_TARGETS_DIR)' != ''">$([MSBuild]::EnsureTrailingSlash($([System.IO.Path]::GetFullPath('$(BROWSER_BUILD_TARGETS_DIR)'))))
+
+
+
diff --git a/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.sh b/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.sh
index 9cac0dd1442ed3..fee3487daae1d2 100644
--- a/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.sh
+++ b/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.sh
@@ -55,6 +55,20 @@ function set_env_vars()
export RUNTIME_FLAVOR_FOR_TESTS=$RUNTIME_FLAVOR
fi
+ # CoreCLR WBT: make payload-relative paths visible to any child process that calls this helper.
+ if [[ -n "$REPOSITORY_ENGINEERING_DIR" ]]; then
+ export REPOSITORY_ENGINEERING_DIR
+ fi
+ if [[ -n "$BROWSER_BUILD_TARGETS_DIR" ]]; then
+ export BROWSER_BUILD_TARGETS_DIR
+ fi
+ if [[ -n "$WASM_APP_BUILDER_TASKS_ASSEMBLY_PATH" ]]; then
+ export WASM_APP_BUILDER_TASKS_ASSEMBLY_PATH
+ fi
+ if [[ -n "$EMSDK_PATH" ]]; then
+ export EMSDK_PATH
+ fi
+
local _SDK_DIR=
if [[ -n "$HELIX_WORKITEM_UPLOAD_ROOT" ]]; then
cp -r $BASE_DIR/$SDK_DIR_NAME $EXECUTION_DIR