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