Skip to content

Commit

Permalink
[wasi] Add WasiAppBuilder task (#82126)
Browse files Browse the repository at this point in the history
* Address review feedback from @pavelsavara
  • Loading branch information
radical committed Feb 22, 2023
1 parent e881c66 commit 134de4b
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 79 deletions.
1 change: 1 addition & 0 deletions src/mono/wasi/build/WasiApp.Native.targets
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@
Include="-D WASI_AFTER_RUNTIME_LOADED_CALLS="@(WasiAfterRuntimeLoadedCalls, ' ')"" />
<_WasiSdkClangArgs Include="-o &quot;$(_WasmOutputFileName.Replace('\', '/'))&quot;" />
</ItemGroup>

<WriteLinesToFile Lines="@(_WasiSdkClangArgs)" File="$(_WasmIntermediateOutputPath)clang-compile.rsp" Overwrite="true" />
<!--<Message Importance="High" Text="Performing WASI SDK build: &quot;$(WasiClang)&quot; @(_WasiSdkClangArgs, ' ')" />-->
<Message Importance="High" Text="Performing WASI SDK build: &quot;$(WasiClang)&quot; @$(_WasmIntermediateOutputPath)clang-compile.rsp" />
Expand Down
29 changes: 21 additions & 8 deletions src/mono/wasi/build/WasiApp.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<UsingTask TaskName="WasmAppBuilder" AssemblyFile="$(WasmAppBuilderTasksAssemblyPath)" />
<UsingTask TaskName="WasmLoadAssembliesAndReferences" AssemblyFile="$(WasmAppBuilderTasksAssemblyPath)" />
<UsingTask TaskName="Microsoft.WebAssembly.Build.Tasks.WasiAppBuilder" AssemblyFile="$(WasmAppBuilderTasksAssemblyPath)" />
<UsingTask TaskName="Microsoft.WebAssembly.Build.Tasks.WasmLoadAssembliesAndReferences" AssemblyFile="$(WasmAppBuilderTasksAssemblyPath)" />

<!--
Required public items/properties:
Expand Down Expand Up @@ -319,7 +319,7 @@

<Target Name="_GetWasiGenerateAppBundleDependencies">
<PropertyGroup>
<!--<WasmIcuDataFileName Condition="'$(InvariantGlobalization)' != 'true'">icudt.dat</WasmIcuDataFileName>-->
<WasmIcuDataFileName Condition="'$(InvariantGlobalization)' != 'true'">icudt.dat</WasmIcuDataFileName>

<_HasDotnetWasm Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.wasm'">true</_HasDotnetWasm>
</PropertyGroup>
Expand All @@ -328,7 +328,7 @@
<!-- If dotnet.{wasm,js} weren't added already (eg. AOT can add them), then add the default ones -->
<WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.wasm" Condition="'$(_HasDotnetWasm)' != 'true'" />

<!--<WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)$(WasmIcuDataFileName)" Condition="'$(InvariantGlobalization)' != 'true'" />-->
<WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)$(WasmIcuDataFileName)" Condition="'$(InvariantGlobalization)' != 'true'" />
<WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.timezones.blat" />

<WasmFilesToIncludeInFileSystem Include="@(WasmNativeAsset)" Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.js.symbols'" />
Expand All @@ -341,10 +341,23 @@
Condition="'$(WasmGenerateAppBundle)' == 'true'"
DependsOnTargets="_WasmGenerateRuntimeConfig;_GetWasiGenerateAppBundleDependencies;_WasiDefaultGenerateAppBundle;_GenerateRunWasmtimeScript" />

<Target Name="_WasiDefaultGenerateAppBundle" Condition="'$(WasmSingleFileBundle)' != 'true'">
<!-- placeholder bundle generator -->
<Copy SourceFiles="@(_WasmAssembliesInternal)" DestinationFolder="$(WasmAppDir)\managed" SkipUnchangedFiles="true" />
<Copy SourceFiles="@(WasmNativeAsset)" DestinationFolder="$(WasmAppDir)" SkipUnchangedFiles="true" />
<Target Name="_WasiDefaultGenerateAppBundle">

<WasiAppBuilder
AppDir="$(WasmAppDir)"
Assemblies="@(_WasmAssembliesInternal)"
MainAssemblyName="$(WasmMainAssemblyFileName)"
IsSingleFileBundle="$(WasmSingleFileBundle)"
HostConfigs="@(HostConfig)"
RuntimeArgsForHost="@(WasmMonoRuntimeArgs)"
DefaultHostConfig="$(DefaultWasmHostConfig)"
InvariantGlobalization="$(InvariantGlobalization)"
SatelliteAssemblies="@(_WasmSatelliteAssemblies)"
IcuDataFileName="$(WasmIcuDataFileName)"
ExtraFilesToDeploy="@(WasmExtraFilesToDeploy)"
NativeAssets="@(WasmNativeAsset)"
DebugLevel="$(WasmDebugLevel)"
/>
</Target>

<Target Name="_GenerateRunWasmtimeScript">
Expand Down
2 changes: 1 addition & 1 deletion src/mono/wasm/host/CommonConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private CommonConfiguration(string[] args)

HostProperties = rconfig.RuntimeOptions.WasmHostProperties;
if (HostProperties == null)
throw new CommandLineException($"Failed to deserialize {_runtimeConfigPath} - config");
throw new CommandLineException($"Could not find any {nameof(RuntimeOptions.WasmHostProperties)} in {_runtimeConfigPath}");

if (HostProperties.HostConfigs is null || HostProperties.HostConfigs.Count == 0)
throw new CommandLineException($"no perHostConfigs found");
Expand Down
68 changes: 68 additions & 0 deletions src/mono/wasm/host/FileUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Diagnostics.CodeAnalysis;

namespace Microsoft.WebAssembly.AppHost;

public static class FileUtils
{
private static readonly string[] s_extensions = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? new[] { ".exe", ".cmd", ".bat" }
: new[] { "" };

public static bool TryFindExecutableInPATH(string filename, [NotNullWhen(true)] out string? fullPath, [NotNullWhen(false)] out string? errorMessage)
{
errorMessage = null;
fullPath = null;
if (File.Exists(filename))
{
fullPath = Path.GetFullPath(filename);
return true;
}

if (Path.IsPathRooted(filename))
{
fullPath = filename;
return true;
}

var path = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrEmpty(path))
{
errorMessage = "Could not find environment variable PATH";
return false;
}

string[] searchPaths = path.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);
if (searchPaths.Length == 0)
{
errorMessage = $"No paths set in environment variable PATH";
return false;
}

List<string> filenamesTried = new(s_extensions.Length);
foreach (string extn in s_extensions)
{
string filenameWithExtn = filename + extn;
filenamesTried.Add(filenameWithExtn);
foreach (string searchPath in searchPaths)
{
var pathToCheck = Path.Combine(searchPath, filenameWithExtn);
if (File.Exists(pathToCheck))
{
fullPath = pathToCheck;
return true;
}
}
}

// Could not find the path
errorMessage = $"Tried to look for {string.Join(", ", filenamesTried)} in PATH: {string.Join(", ", searchPaths)} .";
return false;
}
}
37 changes: 2 additions & 35 deletions src/mono/wasm/host/JSEngineHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,8 @@ private async Task<int> RunAsync()
_ => throw new CommandLineException($"Unsupported engine {_args.Host}")
};

string? engineBinaryPath;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (engineBinary.Equals("node"))
engineBinaryPath = FindEngineInPath(engineBinary + ".exe"); // NodeJS ships as .exe rather than .cmd
else
engineBinaryPath = FindEngineInPath(engineBinary + ".cmd");
}
else
{
engineBinaryPath = FindEngineInPath(engineBinary);
}

if (engineBinaryPath is null)
throw new CommandLineException($"Cannot find host {engineBinary} in PATH");
if (!FileUtils.TryFindExecutableInPATH(engineBinary, out string? engineBinaryPath, out string? errorMessage))
throw new CommandLineException($"Cannot find host {engineBinary}: {errorMessage}");

if (_args.CommonConfig.Debugging)
throw new CommandLineException($"Debugging not supported with {_args.Host}");
Expand Down Expand Up @@ -105,24 +92,4 @@ private async Task<int> RunAsync()

return exitCode;
}

private static string? FindEngineInPath(string engineBinary)
{
if (File.Exists(engineBinary) || Path.IsPathRooted(engineBinary))
return engineBinary;

var path = Environment.GetEnvironmentVariable("PATH");

if (path == null)
return engineBinary;

foreach (var folder in path.Split(Path.PathSeparator))
{
var fullPath = Path.Combine(folder, engineBinary);
if (File.Exists(fullPath))
return fullPath;
}

return null;
}
}
2 changes: 1 addition & 1 deletion src/mono/wasm/host/RunConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public RunConfiguration(string runtimeConfigPath, string? hostArg)

HostProperties = rconfig.RuntimeOptions.WasmHostProperties;
if (HostProperties == null)
throw new Exception($"Failed to deserialize {runtimeConfigPath} - config");
throw new Exception($"Could not find any {nameof(RuntimeOptions.WasmHostProperties)} in {runtimeConfigPath}");

if (HostProperties.HostConfigs is null || HostProperties.HostConfigs.Count == 0)
throw new Exception($"no perHostConfigs found");
Expand Down
67 changes: 33 additions & 34 deletions src/tasks/WasmAppBuilder/WasmAppBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,11 @@ private sealed class IcuData : AssetEntry
public bool LoadRemote { get; set; }
}

protected override bool ExecuteInternal()
protected override bool ValidateArguments()
{
if (!base.ValidateArguments())
return false;

if (!File.Exists(MainJS))
throw new LogAsErrorException($"File MainJS='{MainJS}' doesn't exist.");
if (!InvariantGlobalization && string.IsNullOrEmpty(IcuDataFileName))
Expand All @@ -131,6 +134,14 @@ protected override bool ExecuteInternal()
return false;
}

return true;
}

protected override bool ExecuteInternal()
{
if (!ValidateArguments())
return false;

var _assemblies = new List<string>();
foreach (var asm in Assemblies!)
{
Expand Down Expand Up @@ -183,6 +194,7 @@ protected override bool ExecuteInternal()
if (!FileCopyChecked(item.ItemSpec, dest, "NativeAssets"))
return false;
}

var mainFileName=Path.GetFileName(MainJS);
Log.LogMessage(MessageImportance.Low, $"MainJS path: '{MainJS}', fileName : '{mainFileName}', destination: '{Path.Combine(AppDir, mainFileName)}'");
FileCopyChecked(MainJS!, Path.Combine(AppDir, mainFileName), string.Empty);
Expand Down Expand Up @@ -226,43 +238,30 @@ protected override bool ExecuteInternal()

config.DebugLevel = DebugLevel;

if (SatelliteAssemblies != null)
ProcessSatelliteAssemblies(args =>
{
foreach (var assembly in SatelliteAssemblies)
string name = Path.GetFileName(args.fullPath);
string directory = Path.Combine(AppDir, config.AssemblyRootFolder, args.culture);
Directory.CreateDirectory(directory);
if (UseWebcil)
{
string culture = assembly.GetMetadata("CultureName") ?? string.Empty;
string fullPath = assembly.GetMetadata("Identity");
if (string.IsNullOrEmpty(culture))
{
Log.LogWarning(null, "WASM0002", "", "", 0, 0, 0, 0, $"Missing CultureName metadata for satellite assembly {fullPath}");
continue;
}
// FIXME: validate the culture?

string name = Path.GetFileName(fullPath);
string directory = Path.Combine(AppDir, config.AssemblyRootFolder, culture);
Directory.CreateDirectory(directory);
if (UseWebcil)
{
var tmpWebcil = Path.GetTempFileName();
var webcilWriter = Microsoft.WebAssembly.Build.Tasks.WebcilConverter.FromPortableExecutable(inputPath: fullPath, outputPath: tmpWebcil, logger: Log);
webcilWriter.ConvertToWebcil();
var finalWebcil = Path.Combine(directory, Path.ChangeExtension(name, ".webcil"));
if (Utils.CopyIfDifferent(tmpWebcil, finalWebcil, useHash: true))
Log.LogMessage(MessageImportance.Low, $"Generated {finalWebcil} .");
else
Log.LogMessage(MessageImportance.Low, $"Skipped generating {finalWebcil} as the contents are unchanged.");
_fileWrites.Add(finalWebcil);
config.Assets.Add(new SatelliteAssemblyEntry(Path.GetFileName(finalWebcil), culture));
}
var tmpWebcil = Path.GetTempFileName();
var webcilWriter = Microsoft.WebAssembly.Build.Tasks.WebcilConverter.FromPortableExecutable(inputPath: args.fullPath, outputPath: tmpWebcil, logger: Log);
webcilWriter.ConvertToWebcil();
var finalWebcil = Path.Combine(directory, Path.ChangeExtension(name, ".webcil"));
if (Utils.CopyIfDifferent(tmpWebcil, finalWebcil, useHash: true))
Log.LogMessage(MessageImportance.Low, $"Generated {finalWebcil} .");
else
{
FileCopyChecked(fullPath, Path.Combine(directory, name), "SatelliteAssemblies");
config.Assets.Add(new SatelliteAssemblyEntry(name, culture));
}

Log.LogMessage(MessageImportance.Low, $"Skipped generating {finalWebcil} as the contents are unchanged.");
_fileWrites.Add(finalWebcil);
config.Assets.Add(new SatelliteAssemblyEntry(Path.GetFileName(finalWebcil), args.culture));
}
}
else
{
FileCopyChecked(args.fullPath, Path.Combine(directory, name), "SatelliteAssemblies");
config.Assets.Add(new SatelliteAssemblyEntry(name, args.culture));
}
});

if (FilesToIncludeInFileSystem != null)
{
Expand Down
20 changes: 20 additions & 0 deletions src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public abstract class WasmAppBuilderBaseTask : Task
[Required]
public string[] Assemblies { get; set; } = Array.Empty<string>();

// files like dotnet.wasm, icudt.dat etc
[NotNull]
[Required]
public ITaskItem[] NativeAssets { get; set; } = Array.Empty<ITaskItem>();
Expand Down Expand Up @@ -65,6 +66,25 @@ public override bool Execute()

protected abstract bool ExecuteInternal();

protected virtual bool ValidateArguments() => true;

protected void ProcessSatelliteAssemblies(Action<(string fullPath, string culture)> fn)
{
foreach (var assembly in SatelliteAssemblies)
{
string culture = assembly.GetMetadata("CultureName") ?? string.Empty;
string fullPath = assembly.GetMetadata("Identity");
if (string.IsNullOrEmpty(culture))
{
Log.LogWarning(null, "WASM0002", "", "", 0, 0, 0, 0, $"Missing CultureName metadata for satellite assembly {fullPath}");
continue;
}

// FIXME: validate the culture?
fn((fullPath, culture));
}
}

protected virtual void UpdateRuntimeConfigJson()
{
string[] matchingAssemblies = Assemblies.Where(asm => Path.GetFileName(asm) == MainAssemblyName).ToArray();
Expand Down
Loading

0 comments on commit 134de4b

Please sign in to comment.