Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion eng/AcquireEmscriptenSdk.proj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

<Import Project="$(RepositoryEngineeringDir)AcquireEmscriptenSdk.targets" />

<Target Name="Build" DependsOnTargets="AcquireEmscriptenSdkUnconditional" />
<!--
Microsoft.Build.NoTargets' 'Build' target would override a <Target Name="Build" ...>
definition here, so hook via BeforeTargets instead to guarantee that
AcquireEmscriptenSdkUnconditional runs when this project is built (e.g. via
'./build.sh -s provision.emsdk -os browser').
-->
<Target Name="_AcquireEmscriptenSdkOnBuild"
BeforeTargets="Build"
DependsOnTargets="AcquireEmscriptenSdkUnconditional" />

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 66 additions & 8 deletions src/libraries/sendtohelix-browser.targets
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@

<NeedsWorkload Condition="'$(Scenario)' == 'BuildWasmApps'">true</NeedsWorkload>
<NeedsEMSDK Condition="'$(NeedsToBuildWasmAppsOnHelix)' == 'true'">true</NeedsEMSDK>
<!-- CoreCLR WBT re-links dotnet.native.wasm per-app via emcc, so it needs the full EMSDK payload too. -->
<NeedsEMSDK Condition="'$(RuntimeFlavor)' == 'CoreCLR' and '$(Scenario)' == 'BuildWasmApps'">true</NeedsEMSDK>
<NeedsEMSDKNode Condition="'$(Scenario)' == 'BuildWasmApps'">false</NeedsEMSDKNode>
<NeedsToRunOnChrome Condition="'$(Scenario)' == 'WasmTestOnChrome' or '$(Scenario)' == 'BuildWasmApps'">true</NeedsToRunOnChrome>
<NeedsToRunOnFirefox Condition="'$(Scenario)' == 'WasmTestOnFirefox'">true</NeedsToRunOnFirefox>
Expand All @@ -88,13 +90,22 @@
<_HelixLocalNodePath Condition="'$(NeedsEMSDK)' == 'true' and '$(WindowsShell)' == 'true'">%HELIX_CORRELATION_PAYLOAD%\build\emsdk\node</_HelixLocalNodePath>
</PropertyGroup>

<ItemGroup>
<!--
Stage emsdk as a Helix dependency so it ends up at $(EmSdkDirForHelixPayload) and can be
packaged into the correlation payload. This is needed for CoreCLR WBT agents, which do not
build the wasm runtime locally and therefore do not pre-populate emsdk at $(ProvisionEmscriptenDir).
Those agents provision emsdk in a separate pipeline step before sendtohelix runs, at which
point AcquireEmscriptenSdk.props/targets set $(EMSDK_PATH) during project evaluation.

This ItemGroup must be at project-evaluation scope (not inside a target), because
StageDependenciesForHelix guards its body with a condition over @(HelixDependenciesToStage)
that is evaluated before any BeforeTargets hook can populate the item.
-->
<ItemGroup Condition="('$(NeedsEMSDK)' == 'true' or '$(IncludeNodePayload)' == 'true') and '$(EMSDK_PATH)' != ''">
<HelixDependenciesToStage
Name="emsdk"
Include="$(EmSdkDirForHelixPayload)"
Condition="'$(NeedsEMSDK)' == 'true' or '$(IncludeNodePayload)' == 'true'"
SourcePath="$(EMSDK_PATH)"
/>
SourcePath="$(EMSDK_PATH)" />
</ItemGroup>
Comment thread
maraf marked this conversation as resolved.

<ItemGroup>
Expand Down Expand Up @@ -135,6 +146,27 @@
<HelixPreCommand Include="$(EnvVarCommand) PATH=$(_HelixLocalNodePath)/%_HELIX_NODEJS_VERSION%/bin%3B%PATH%" />
</ItemGroup>

<!--
CoreCLR WBT (BuildWasmApps) needs BrowserWasmApp.CoreCLR.targets and its import chain on Helix.
The NeedsEMSDK payload already ships BrowserBuildTargetsDir (build/wasm), WasmAppBuilderDir
(build/WasmAppBuilder) and EmSdk (build/emsdk); eng/native.wasm.targets + AcquireEmscriptenSdk.targets
are shipped as build/eng. These env vars bridge those payload locations to the MSBuild properties
consumed by BrowserWasmApp.CoreCLR.targets when the test-created project imports it.
-->
<ItemGroup Condition="'$(RuntimeFlavor)' == 'CoreCLR' and '$(Scenario)' == 'BuildWasmApps' and '$(WindowsShell)' != 'true'">
<HelixPreCommand Include="$(EnvVarCommand) REPOSITORY_ENGINEERING_DIR=$HELIX_CORRELATION_PAYLOAD/build/eng/" />
<HelixPreCommand Include="$(EnvVarCommand) BROWSER_BUILD_TARGETS_DIR=$HELIX_CORRELATION_PAYLOAD/build/wasm/" />
<HelixPreCommand Include="$(EnvVarCommand) WASM_APP_BUILDER_TASKS_ASSEMBLY_PATH=$HELIX_CORRELATION_PAYLOAD/build/WasmAppBuilder/WasmAppBuilder.dll" />
<HelixPreCommand Include="$(EnvVarCommand) EMSDK_PATH=$HELIX_CORRELATION_PAYLOAD/build/emsdk/" />
</ItemGroup>

<ItemGroup Condition="'$(RuntimeFlavor)' == 'CoreCLR' and '$(Scenario)' == 'BuildWasmApps' and '$(WindowsShell)' == 'true'">
<HelixPreCommand Include="$(EnvVarCommand) REPOSITORY_ENGINEERING_DIR=%HELIX_CORRELATION_PAYLOAD%\build\eng\" />
<HelixPreCommand Include="$(EnvVarCommand) BROWSER_BUILD_TARGETS_DIR=%HELIX_CORRELATION_PAYLOAD%\build\wasm\" />
<HelixPreCommand Include="$(EnvVarCommand) WASM_APP_BUILDER_TASKS_ASSEMBLY_PATH=%HELIX_CORRELATION_PAYLOAD%\build\WasmAppBuilder\WasmAppBuilder.dll" />
<HelixPreCommand Include="$(EnvVarCommand) EMSDK_PATH=%HELIX_CORRELATION_PAYLOAD%\build\emsdk\" />
</ItemGroup>

<PropertyGroup Condition="'$(Scenario)' == 'BuildWasmApps'">
<BuildWasmAppsJobsList Condition="'$(RuntimeFlavor)' == 'Mono'">$(RepositoryEngineeringDir)testing\scenarios\BuildWasmAppsJobsList.txt</BuildWasmAppsJobsList>
<BuildWasmAppsJobsList Condition="'$(RuntimeFlavor)' == 'CoreCLR'">$(RepositoryEngineeringDir)testing\scenarios\BuildWasmAppsJobsListCoreCLR.txt</BuildWasmAppsJobsList>
Expand Down Expand Up @@ -214,15 +246,41 @@
<ItemGroup Condition="'$(NeedsEMSDK)' == 'true'">
<HelixCorrelationPayload Include="$(EmSdkDirForHelixPayload)" Destination="build/emsdk" />
<HelixCorrelationPayload Include="$(WasmAppBuilderDir)" Destination="build/WasmAppBuilder" />
<HelixCorrelationPayload Include="$(MonoAOTCompilerDir)" Destination="build/MonoAOTCompiler" />
<HelixCorrelationPayload Include="$(MicrosoftNetCoreAppRuntimePackDir)" Destination="build/microsoft.netcore.app.runtime.browser-wasm" />
<HelixCorrelationPayload Include="$(BrowserBuildTargetsDir)" Destination="build/wasm" />

<!-- WebAssembly SDK pack is required by the `dotnet new wasmbrowser` template regardless of runtime flavor. -->
<HelixCorrelationPayload Include="$(MonoProjectRoot)\nuget\Microsoft.NET.Sdk.WebAssembly.Pack\build\" Destination="build/WasmSdkTargets" />
<HelixCorrelationPayload Include="$(ArtifactsBinDir)\Microsoft.NET.Sdk.WebAssembly.Pack.Tasks\$(RuntimeConfiguration)\" Destination="build/WasmSdkTools" />
</ItemGroup>

<!-- Mono-only build-tooling payloads. CoreCLR per-app relink does not use MonoAOT / Mono build tasks,
and acquires the runtime pack on the Helix worker via the downloaded nupkg instead of shipping
the raw pack layout as a correlation payload. -->
<ItemGroup Condition="'$(NeedsEMSDK)' == 'true' and '$(RuntimeFlavor)' != 'CoreCLR'">
<HelixCorrelationPayload Include="$(MicrosoftNetCoreAppRuntimePackDir)" Destination="build/microsoft.netcore.app.runtime.browser-wasm" />
<HelixCorrelationPayload Include="$(MonoAOTCompilerDir)" Destination="build/MonoAOTCompiler" />
<HelixCorrelationPayload Include="$(WasmBuildTargetsDir)" Destination="build/wasm-shared" />
<HelixCorrelationPayload Include="$(MonoAotCrossDir)" Destination="build/cross" />
<HelixCorrelationPayload Include="$(MonoTargetsTasksDir)" Destination="build/MonoTargetsTasks" />
</ItemGroup>

<HelixCorrelationPayload Include="$(MonoProjectRoot)\nuget\Microsoft.NET.Sdk.WebAssembly.Pack\build\" Destination="build/WasmSdkTargets" />
<HelixCorrelationPayload Include="$(ArtifactsBinDir)\Microsoft.NET.Sdk.WebAssembly.Pack.Tasks\$(RuntimeConfiguration)\" Destination="build/WasmSdkTools" />
<!-- CoreCLR WBT needs eng/native.wasm.targets + eng/AcquireEmscriptenSdk.targets (the import chain
of BrowserWasmApp.CoreCLR.targets) shipped alongside BrowserBuildTargetsDir. Stage them into
a directory first — HelixCorrelationPayload on individual files causes the Helix client to
try unzipping them as archives. -->
<PropertyGroup Condition="'$(NeedsEMSDK)' == 'true' and '$(RuntimeFlavor)' == 'CoreCLR'">
<_CoreCLRWbtEngPayloadDir>$(ArtifactsObjDir)helix-staging\coreclr-wbt-eng\</_CoreCLRWbtEngPayloadDir>
</PropertyGroup>
<ItemGroup Condition="'$(NeedsEMSDK)' == 'true' and '$(RuntimeFlavor)' == 'CoreCLR'">
<_CoreCLRWbtEngFiles Include="$(RepositoryEngineeringDir)native.wasm.targets" />
<_CoreCLRWbtEngFiles Include="$(RepositoryEngineeringDir)AcquireEmscriptenSdk.targets" />
</ItemGroup>
<Copy Condition="'$(NeedsEMSDK)' == 'true' and '$(RuntimeFlavor)' == 'CoreCLR'"
SourceFiles="@(_CoreCLRWbtEngFiles)"
DestinationFolder="$(_CoreCLRWbtEngPayloadDir)"
SkipUnchangedFiles="true" />
<ItemGroup Condition="'$(NeedsEMSDK)' == 'true' and '$(RuntimeFlavor)' == 'CoreCLR'">
<HelixCorrelationPayload Include="$(_CoreCLRWbtEngPayloadDir)" Destination="build/eng" />
</ItemGroup>

<!-- copy node separately only if EMSDK is not being included -->
Expand Down
14 changes: 14 additions & 0 deletions src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -173,6 +179,96 @@ private static void AddCoreClrProjectProperties(ref string extraProperties, ref
""";
}

/// <summary>
/// Installs the WASM browser/console templates from the built nugets path
/// using <c>dotnet new install</c> if needed. This is a no-op when
/// the workload is already installed (templates come with the workload).
/// </summary>
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;
Comment thread
maraf marked this conversation as resolved.
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();

Comment thread
maraf marked this conversation as resolved.
if (process.ExitCode != 0)
throw new InvalidOperationException(
$"'dotnet new install' failed with exit code {process.ExitCode}.\nStdout: {stdout}\nStderr: {stderr}");

Comment thread
maraf marked this conversation as resolved.
Console.WriteLine($"[templates] WASM template install completed successfully");
s_wasmTemplatesInstalled = true;
}
}

public virtual (string projectDir, string buildOutput) PublishProject(
ProjectInfo info,
Configuration configuration,
Expand Down
Loading
Loading