Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/7.0] Fix running WebAssembly Browser App from VS #75508

Merged
merged 11 commits into from
Sep 13, 2022
30 changes: 22 additions & 8 deletions src/mono/wasm/build/WasmApp.targets
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,34 @@

<!-- if DebuggerSupport==true, then ensure that WasmDebugLevel isn't disabling debugging -->
<WasmDebugLevel Condition="('$(WasmDebugLevel)' == '' or '$(WasmDebugLevel)' == '0') and ('$(DebuggerSupport)' == 'true' or '$(Configuration)' == 'Debug')">-1</WasmDebugLevel>
</PropertyGroup>

<PropertyGroup Label="Identify app bundle directory to run from">
<!-- Allow running from custom WasmAppDir -->
<_AppBundleDirForRunCommand Condition="'$(WasmAppDir)' != ''">$(WasmAppDir)</_AppBundleDirForRunCommand>

<!--
This is the default path. We have to build it explicitly because
RuntimeIdentifierInference.targets is imported after this file, and
updates OutputPath to include the RID. So, we don't have the correct
final OutputPath here. But we need it for `dotnet run` to work, as it
just reads the RunCommand after evaluation.

<!-- workaround: RuntimeIdentifierInference.targets is imported after this file, and updates OutputPath to include
the RID. So, we don't have the correct final OutputPath here. But we need it for `dotnet run` to work,
as it just reads the RunCommand after evaluation. -->
<_AppBundleDirForRunCommand Condition="Exists('$(WasmAppDir)/$(AssemblyName).runtimeconfig.json')">$(WasmAppDir)</_AppBundleDirForRunCommand>
<_AppBundleDirForRunCommand Condition="'$(_AppBundleDirForRunCommand)' == '' and Exists('$(OutputPath)/AppBundle')">$([MSBuild]::NormalizeDirectory($(OutputPath), 'AppBundle'))</_AppBundleDirForRunCommand>
<_AppBundleDirForRunCommand Condition="'$(_AppBundleDirForRunCommand)' == '' and Exists('$(OutputPath)/browser-wasm/AppBundle')">$([MSBuild]::NormalizeDirectory($(OutputPath), 'browser-wasm', 'AppBundle'))</_AppBundleDirForRunCommand>
<_AppBundleDirForRunCommand Condition="'$(_AppBundleDirForRunCommand)' == ''">OutputPath=$(OutputPath), OutDir=$(OutDir)</_AppBundleDirForRunCommand>
The path might not have been created yet, for example when creating a new project in VS, so don't use an Exists() check
-->
<_AppBundleDirForRunCommand Condition="'$(_AppBundleDirForRunCommand)' == ''">$([System.IO.Path]::Combine($(OutputPath), 'browser-wasm', 'AppBundle'))</_AppBundleDirForRunCommand>

<!-- Ensure the path is absolute. In case of VS, the cwd might not be the correct one, so explicitly
use $(MSBuildProjectDirectory). -->
<_AppBundleDirForRunCommand Condition="'$(_AppBundleDirForRunCommand)' != '' and !$([System.IO.Path]::IsPathRooted($(_AppBundleDirForRunCommand)))">$([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(_AppBundleDirForRunCommand)))</_AppBundleDirForRunCommand>
</PropertyGroup>

<PropertyGroup Condition="'$(WasmGenerateAppBundle)' == 'true'">
<RunCommand Condition="'$(DOTNET_HOST_PATH)' != '' and Exists($(DOTNET_HOST_PATH))">$(DOTNET_HOST_PATH)</RunCommand>
<RunCommand Condition="'$(RunCommand)' == ''">dotnet</RunCommand>
<RunArguments Condition="'$(RunArguments)' == ''">exec &quot;$([MSBuild]::NormalizePath($(WasmAppHostDir), 'WasmAppHost.dll'))&quot; --runtime-config &quot;$(_AppBundleDirForRunCommand)/$(AssemblyName).runtimeconfig.json&quot; $(WasmHostArguments)</RunArguments>

<_RuntimeConfigJsonPath>$([MSBuild]::NormalizePath($(_AppBundleDirForRunCommand), '$(AssemblyName).runtimeconfig.json'))</_RuntimeConfigJsonPath>
<RunArguments Condition="'$(RunArguments)' == ''">exec &quot;$([MSBuild]::NormalizePath($(WasmAppHostDir), 'WasmAppHost.dll'))&quot; --runtime-config &quot;$(_RuntimeConfigJsonPath)&quot; $(WasmHostArguments)</RunArguments>
<RunWorkingDirectory Condition="'$(RunWorkingDirectory)' == ''">$(_AppBundleDirForRunCommand)</RunWorkingDirectory>
</PropertyGroup>

Expand Down
112 changes: 109 additions & 3 deletions src/tests/BuildWasmApps/Wasm.Build.Tests/WasmTemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public WasmTemplateTests(ITestOutputHelper output, SharedBuildPerTestClassFixtur
{
}

private void updateProgramCS()
private void UpdateProgramCS()
{
string programText = """
Console.WriteLine("Hello, Console!");
Expand Down Expand Up @@ -188,7 +188,7 @@ public void ConsoleBuildAndRun(string config, bool relinking)
string projectFile = CreateWasmTemplateProject(id, "wasmconsole");
string projectName = Path.GetFileNameWithoutExtension(projectFile);

updateProgramCS();
UpdateProgramCS();
UpdateConsoleMainJs();
if (relinking)
AddItemsPropertiesToProject(projectFile, "<WasmBuildNative>true</WasmBuildNative>");
Expand Down Expand Up @@ -216,6 +216,112 @@ public void ConsoleBuildAndRun(string config, bool relinking)
Assert.Contains("args[2] = z", output);
}

public static TheoryData<bool, bool, string> TestDataForAppBundleDir()
{
var data = new TheoryData<bool, bool, string>();
AddTestData(forConsole: true, runOutsideProjectDirectory: false);
AddTestData(forConsole: true, runOutsideProjectDirectory: true);

AddTestData(forConsole: false, runOutsideProjectDirectory: false);
AddTestData(forConsole: false, runOutsideProjectDirectory: true);

void AddTestData(bool forConsole, bool runOutsideProjectDirectory)
{
data.Add(runOutsideProjectDirectory, forConsole, string.Empty);

data.Add(runOutsideProjectDirectory, forConsole,
$"<OutputPath>{Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())}</OutputPath>");
data.Add(runOutsideProjectDirectory, forConsole,
$"<WasmAppDir>{Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())}</WasmAppDir>");
}

return data;
}

[ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
[MemberData(nameof(TestDataForAppBundleDir))]
public async Task RunWithDifferentAppBundleLocations(bool forConsole, bool runOutsideProjectDirectory, string extraProperties)
=> await (forConsole
? ConsoleRunWithAndThenWithoutBuildAsync("Release", extraProperties, runOutsideProjectDirectory)
: BrowserRunTwiceWithAndThenWithoutBuildAsync("Release", extraProperties, runOutsideProjectDirectory));

private async Task BrowserRunTwiceWithAndThenWithoutBuildAsync(string config, string extraProperties = "", bool runOutsideProjectDirectory = false)
{
string id = $"browser_{config}_{Path.GetRandomFileName()}";
string projectFile = CreateWasmTemplateProject(id, "wasmbrowser");

UpdateBrowserMainJs();

if (!string.IsNullOrEmpty(extraProperties))
AddItemsPropertiesToProject(projectFile, extraProperties: extraProperties);

string workingDir = runOutsideProjectDirectory ? Path.GetTempPath() : _projectDir!;

{
using var runCommand = new RunCommand(s_buildEnv, _testOutput)
.WithWorkingDirectory(workingDir);

await using var runner = new BrowserRunner();
var page = await runner.RunAsync(runCommand, $"run -c {config} --project {projectFile} --forward-console");
await runner.WaitForExitMessageAsync(TimeSpan.FromMinutes(2));
Assert.Contains("Hello, Browser!", string.Join(Environment.NewLine, runner.OutputLines));
}

{
using var runCommand = new RunCommand(s_buildEnv, _testOutput)
.WithWorkingDirectory(workingDir);

await using var runner = new BrowserRunner();
var page = await runner.RunAsync(runCommand, $"run -c {config} --no-build --project {projectFile} --forward-console");
await runner.WaitForExitMessageAsync(TimeSpan.FromMinutes(2));
Assert.Contains("Hello, Browser!", string.Join(Environment.NewLine, runner.OutputLines));
}
}

private Task ConsoleRunWithAndThenWithoutBuildAsync(string config, string extraProperties = "", bool runOutsideProjectDirectory = false)
{
string id = $"console_{config}_{Path.GetRandomFileName()}";
string projectFile = CreateWasmTemplateProject(id, "wasmconsole");

UpdateProgramCS();
UpdateConsoleMainJs();

if (!string.IsNullOrEmpty(extraProperties))
AddItemsPropertiesToProject(projectFile, extraProperties: extraProperties);

string workingDir = runOutsideProjectDirectory ? Path.GetTempPath() : _projectDir!;

{
string runArgs = $"run -c {config} --project {projectFile}";
runArgs += " x y z";
using var cmd = new RunCommand(s_buildEnv, _testOutput, label: id)
.WithWorkingDirectory(workingDir)
.WithEnvironmentVariables(s_buildEnv.EnvVars);
var res = cmd.ExecuteWithCapturedOutput(runArgs).EnsureExitCode(42);

Assert.Contains("args[0] = x", res.Output);
Assert.Contains("args[1] = y", res.Output);
Assert.Contains("args[2] = z", res.Output);
}

_testOutput.WriteLine($"{Environment.NewLine}[{id}] Running again with --no-build{Environment.NewLine}");

{
// Run with --no-build
string runArgs = $"run -c {config} --project {projectFile} --no-build";
runArgs += " x y z";
using var cmd = new RunCommand(s_buildEnv, _testOutput, label: id)
.WithWorkingDirectory(workingDir);
var res = cmd.ExecuteWithCapturedOutput(runArgs).EnsureExitCode(42);

Assert.Contains("args[0] = x", res.Output);
Assert.Contains("args[1] = y", res.Output);
Assert.Contains("args[2] = z", res.Output);
}

return Task.CompletedTask;
}

public static TheoryData<string, bool, bool> TestDataForConsolePublishAndRun()
{
var data = new TheoryData<string, bool, bool>();
Expand All @@ -242,7 +348,7 @@ public void ConsolePublishAndRun(string config, bool aot, bool relinking)
string projectFile = CreateWasmTemplateProject(id, "wasmconsole");
string projectName = Path.GetFileNameWithoutExtension(projectFile);

updateProgramCS();
UpdateProgramCS();
UpdateConsoleMainJs();

if (aot)
Expand Down