diff --git a/src/mono/wasi/build/WasiApp.Native.targets b/src/mono/wasi/build/WasiApp.Native.targets
index 439b6360393f8..2b6fff044afd0 100644
--- a/src/mono/wasi/build/WasiApp.Native.targets
+++ b/src/mono/wasi/build/WasiApp.Native.targets
@@ -432,6 +432,7 @@
Include="-D WASI_AFTER_RUNTIME_LOADED_CALLS="@(WasiAfterRuntimeLoadedCalls, ' ')"" />
<_WasiSdkClangArgs Include="-o "$(_WasmOutputFileName.Replace('\', '/'))"" />
+
diff --git a/src/mono/wasi/build/WasiApp.targets b/src/mono/wasi/build/WasiApp.targets
index 11bba4b8d6dd2..0688e932d0e16 100644
--- a/src/mono/wasi/build/WasiApp.targets
+++ b/src/mono/wasi/build/WasiApp.targets
@@ -1,6 +1,6 @@
-
-
+
+
+ icudt.dat
<_HasDotnetWasm Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.wasm'">true
@@ -328,7 +328,7 @@
-
+
@@ -341,10 +341,23 @@
Condition="'$(WasmGenerateAppBundle)' == 'true'"
DependsOnTargets="_WasmGenerateRuntimeConfig;_GetWasiGenerateAppBundleDependencies;_WasiDefaultGenerateAppBundle;_GenerateRunWasmtimeScript" />
-
-
-
-
+
+
+
diff --git a/src/mono/wasm/host/CommonConfiguration.cs b/src/mono/wasm/host/CommonConfiguration.cs
index 7d854cc47141e..db7a59564dcfa 100644
--- a/src/mono/wasm/host/CommonConfiguration.cs
+++ b/src/mono/wasm/host/CommonConfiguration.cs
@@ -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");
diff --git a/src/mono/wasm/host/FileUtils.cs b/src/mono/wasm/host/FileUtils.cs
new file mode 100644
index 0000000000000..3dd5de48e09ad
--- /dev/null
+++ b/src/mono/wasm/host/FileUtils.cs
@@ -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 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;
+ }
+}
diff --git a/src/mono/wasm/host/JSEngineHost.cs b/src/mono/wasm/host/JSEngineHost.cs
index cc80f068a18a5..cfcdf3569e5a6 100644
--- a/src/mono/wasm/host/JSEngineHost.cs
+++ b/src/mono/wasm/host/JSEngineHost.cs
@@ -46,21 +46,8 @@ private async Task 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}");
@@ -105,24 +92,4 @@ private async Task 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;
- }
}
diff --git a/src/mono/wasm/host/RunConfiguration.cs b/src/mono/wasm/host/RunConfiguration.cs
index 67a4f2e89540c..caf5b183f58bc 100644
--- a/src/mono/wasm/host/RunConfiguration.cs
+++ b/src/mono/wasm/host/RunConfiguration.cs
@@ -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");
diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs
index 3b39af3a8e603..c99a05445407c 100644
--- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs
+++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs
@@ -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))
@@ -131,6 +134,14 @@ protected override bool ExecuteInternal()
return false;
}
+ return true;
+ }
+
+ protected override bool ExecuteInternal()
+ {
+ if (!ValidateArguments())
+ return false;
+
var _assemblies = new List();
foreach (var asm in Assemblies!)
{
@@ -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);
@@ -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)
{
diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs b/src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs
index 942a4461b9d06..11d0f7fd9d6ac 100644
--- a/src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs
+++ b/src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs
@@ -22,6 +22,7 @@ public abstract class WasmAppBuilderBaseTask : Task
[Required]
public string[] Assemblies { get; set; } = Array.Empty();
+ // files like dotnet.wasm, icudt.dat etc
[NotNull]
[Required]
public ITaskItem[] NativeAssets { get; set; } = Array.Empty();
@@ -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();
diff --git a/src/tasks/WasmAppBuilder/wasi/WasiAppBuilder.cs b/src/tasks/WasmAppBuilder/wasi/WasiAppBuilder.cs
new file mode 100644
index 0000000000000..0269e6721cf2a
--- /dev/null
+++ b/src/tasks/WasmAppBuilder/wasi/WasiAppBuilder.cs
@@ -0,0 +1,111 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json.Nodes;
+using Microsoft.Build.Framework;
+
+namespace Microsoft.WebAssembly.Build.Tasks;
+
+public class WasiAppBuilder : WasmAppBuilderBaseTask
+{
+ public bool IsSingleFileBundle { get; set; }
+
+ protected override bool ValidateArguments()
+ {
+ if (!base.ValidateArguments())
+ return false;
+
+ if (!InvariantGlobalization && string.IsNullOrEmpty(IcuDataFileName))
+ throw new LogAsErrorException("IcuDataFileName property shouldn't be empty if InvariantGlobalization=false");
+
+ if (Assemblies.Length == 0 && !IsSingleFileBundle)
+ {
+ Log.LogError("Cannot build Wasm app without any assemblies");
+ return false;
+ }
+
+ return true;
+ }
+
+ protected override bool ExecuteInternal()
+ {
+ if (!ValidateArguments())
+ return false;
+
+ var _assemblies = new List();
+ foreach (string asm in Assemblies!)
+ {
+ if (!_assemblies.Contains(asm))
+ _assemblies.Add(asm);
+ }
+ MainAssemblyName = Path.GetFileName(MainAssemblyName);
+
+ // Create app
+ Directory.CreateDirectory(AppDir!);
+
+ if (!IsSingleFileBundle)
+ {
+ string asmRootPath = Path.Combine(AppDir, "managed");
+ Directory.CreateDirectory(asmRootPath);
+ foreach (string assembly in _assemblies)
+ {
+ FileCopyChecked(assembly, Path.Combine(asmRootPath, Path.GetFileName(assembly)), "Assemblies");
+
+ if (DebugLevel != 0)
+ {
+ string pdb = Path.ChangeExtension(assembly, ".pdb");
+ if (File.Exists(pdb))
+ FileCopyChecked(pdb, Path.Combine(asmRootPath, Path.GetFileName(pdb)), "Assemblies");
+ }
+ }
+ }
+
+ foreach (ITaskItem item in NativeAssets)
+ {
+ string dest = Path.Combine(AppDir, Path.GetFileName(item.ItemSpec));
+ if (!FileCopyChecked(item.ItemSpec, dest, "NativeAssets"))
+ return false;
+ }
+
+ ProcessSatelliteAssemblies(args =>
+ {
+ string name = Path.GetFileName(args.fullPath);
+ string directory = Path.Combine(AppDir, "managed", args.culture);
+ Directory.CreateDirectory(directory);
+ FileCopyChecked(args.fullPath, Path.Combine(directory, name), "SatelliteAssemblies");
+ });
+
+ foreach (ITaskItem item in ExtraFilesToDeploy!)
+ {
+ string src = item.ItemSpec;
+ string dst;
+
+ string tgtPath = item.GetMetadata("TargetPath");
+ if (!string.IsNullOrEmpty(tgtPath))
+ {
+ dst = Path.Combine(AppDir!, tgtPath);
+ string? dstDir = Path.GetDirectoryName(dst);
+ if (!string.IsNullOrEmpty(dstDir) && !Directory.Exists(dstDir))
+ Directory.CreateDirectory(dstDir!);
+ }
+ else
+ {
+ dst = Path.Combine(AppDir!, Path.GetFileName(src));
+ }
+
+ if (!FileCopyChecked(src, dst, "ExtraFilesToDeploy"))
+ return false;
+ }
+
+ UpdateRuntimeConfigJson();
+ return !Log.HasLoggedErrors;
+ }
+
+ protected override void AddToRuntimeConfig(JsonObject wasmHostProperties, JsonArray runtimeArgsArray, JsonArray perHostConfigs)
+ {
+ if (IsSingleFileBundle)
+ wasmHostProperties["singleFileBundle"] = true;
+ }
+}