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; + } +}