// Copyright (c) Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using Flax.Build.Graph; using Flax.Build.NativeCpp; namespace Flax.Build { partial class Configuration { /// /// Specifies the initial memory size (in MB) to use by Web app. /// [CommandLine("webInitialMemory", "", "Specifies the initial memory size (in MB) to use by Web app.")] public static int WebInitialMemory = 32; } } namespace Flax.Build.Platforms { /// /// The build toolchain for Web with Emscripten. /// /// public sealed class WebToolchain : Toolchain { private string _sysrootPath; private string _compilerPath; private Version _compilerVersion; /// /// Initializes a new instance of the class. /// /// The platform. /// The target architecture. public WebToolchain(WebPlatform platform, TargetArchitecture architecture) : base(platform, architecture) { var sdkPath = EmscriptenSdk.Instance.EmscriptenPath; // Setup tools _compilerPath = Path.Combine(sdkPath, "emscripten", "emcc"); var clangPath = Path.Combine(sdkPath, "bin", "clang++"); if (Platform.BuildTargetPlatform == TargetPlatform.Windows) { _compilerPath += ".bat"; clangPath += ".exe"; } // Determinate compiler version _compilerVersion = UnixToolchain.GetClangVersion(platform.Target, clangPath); // Setup system paths SystemIncludePaths.Add(Path.Combine(sdkPath, "lib", "clang", _compilerVersion.Major.ToString(), "include")); SystemIncludePaths.Add(Path.Combine(sdkPath, "emscripten", "system", "include")); SystemIncludePaths.Add(Path.Combine(sdkPath, "emscripten", "system", "lib")); _sysrootPath = Path.Combine(sdkPath, "emscripten/cache/sysroot/include"); } public static string GetLibName(string path) { var libName = Path.GetFileNameWithoutExtension(path); if (libName.StartsWith("lib")) libName = libName.Substring(3); return libName; } /// public override string DllExport => "__attribute__((__visibility__(\\\"default\\\")))"; /// public override string DllImport => ""; /// public override TargetCompiler Compiler => TargetCompiler.Clang; /// public override string NativeCompilerPath => _compilerPath; /// public override void LogInfo() { Log.Info("Clang version: " + _compilerVersion); } /// public override void SetupEnvironment(BuildOptions options) { base.SetupEnvironment(options); options.CompileEnv.PreprocessorDefinitions.Add("PLATFORM_WEB"); options.CompileEnv.PreprocessorDefinitions.Add("PLATFORM_UNIX"); options.CompileEnv.PreprocessorDefinitions.Add("__EMSCRIPTEN__"); options.CompileEnv.EnableExceptions = false; options.CompileEnv.CpuArchitecture = CpuArchitecture.None; // TODO: try SIMD support in Emscripten } private void AddSharedArgs(List args, BuildOptions options, bool debugInformation, bool optimization) { //args.Add("-pthread"); if (debugInformation) args.Add("-g2"); else args.Add("-g0"); if (options.CompileEnv.FavorSizeOrSpeed == FavorSizeOrSpeed.SmallCode) args.Add("-Os"); if (options.CompileEnv.FavorSizeOrSpeed == FavorSizeOrSpeed.FastCode) args.Add("-O3"); else if (optimization && options.Configuration == TargetConfiguration.Release) args.Add("-O3"); else if (optimization) args.Add("-O2"); else args.Add("-O0"); if (options.CompileEnv.RuntimeTypeInfo) args.Add("-frtti"); else args.Add("-fno-rtti"); if (options.CompileEnv.TreatWarningsAsErrors) args.Add("-Wall -Werror"); if (options.CompileEnv.EnableExceptions) args.Add("-fexceptions"); else args.Add("-fno-exceptions"); if (options.LinkEnv.LinkTimeCodeGeneration) args.Add("-flto"); //if (options.LinkEnv.Output == LinkerOutput.SharedLibrary) args.Add("-fPIC"); var sanitizers = options.CompileEnv.Sanitizers; if (sanitizers.HasFlag(Sanitizer.Address)) args.Add("-fsanitize=address"); if (sanitizers.HasFlag(Sanitizer.Undefined)) args.Add("-fsanitize=undefined"); if (sanitizers == Sanitizer.None) args.Add("-fsanitize=null -fsanitize-minimal-runtime"); // Minimal Runtime } /// public override CompileOutput CompileCppFiles(TaskGraph graph, BuildOptions options, List sourceFiles, string outputPath) { var output = new CompileOutput(); // Setup arguments shared by all source files var commonArgs = new List(); commonArgs.AddRange(options.CompileEnv.CustomArgs); { commonArgs.Add("-c"); AddSharedArgs(commonArgs, options, options.CompileEnv.DebugInformation, options.CompileEnv.Optimization); // Hack to pull SDL3 port via emcc //if (options.CompileEnv.PreprocessorDefinitions.Contains("PLATFORM_SDL")) // commonArgs.Add("--use-port=sdl3"); } // Add preprocessor definitions foreach (var definition in options.CompileEnv.PreprocessorDefinitions) { commonArgs.Add(string.Format("-D \"{0}\"", definition)); } // Add include paths foreach (var includePath in options.CompileEnv.IncludePaths) { if (SystemIncludePaths.Contains(includePath)) // TODO: fix SystemIncludePaths so this chack can be removed continue; // Skip system includes as those break compilation (need to fix sys root linking for emscripten) commonArgs.Add(string.Format("-I\"{0}\"", includePath.Replace('\\', '/'))); } // Hack for sysroot includes commonArgs.Add(string.Format("-I\"{0}\"", _sysrootPath.Replace('\\', '/'))); // Compile all C/C++ files var args = new List(); foreach (var sourceFile in sourceFiles) { var sourceFilename = Path.GetFileNameWithoutExtension(sourceFile); var task = graph.Add(); // Use shared arguments args.Clear(); args.AddRange(commonArgs); // Language for the file args.Add("-x"); if (sourceFile.EndsWith(".c", StringComparison.OrdinalIgnoreCase)) args.Add("c"); else { args.Add("c++"); // C++ version switch (options.CompileEnv.CppVersion) { case CppVersion.Cpp14: args.Add("-std=c++14"); break; case CppVersion.Cpp17: case CppVersion.Latest: args.Add("-std=c++17"); break; case CppVersion.Cpp20: args.Add("-std=c++20"); break; } } // Object File Name var objFile = Path.Combine(outputPath, sourceFilename + ".o"); args.Add(string.Format("-o \"{0}\"", objFile.Replace('\\', '/'))); output.ObjectFiles.Add(objFile); task.ProducedFiles.Add(objFile); // Source File Name args.Add("\"" + sourceFile.Replace('\\', '/') + "\""); // Request included files to exist var includes = IncludesCache.FindAllIncludedFiles(sourceFile); task.PrerequisiteFiles.AddRange(includes); // Compile task.WorkingDirectory = options.WorkingDirectory; task.CommandPath = _compilerPath; task.CommandArguments = string.Join(" ", args); task.PrerequisiteFiles.Add(sourceFile); task.InfoMessage = Path.GetFileName(sourceFile); task.Cost = task.PrerequisiteFiles.Count; // TODO: include source file size estimation to improve tasks sorting } return output; } public override bool CompileCSharp(ref CSharpOptions options) { switch (options.Action) { /*case CSharpOptions.ActionTypes.GetOutputFiles: { foreach (var inputFile in options.InputFiles) { string assemblyPath; if (Configuration.AOTMode == DotNetAOTModes.MonoAOTDynamic) assemblyPath = inputFile + Platform.SharedLibraryFileExtension; else assemblyPath = Path.Combine(Path.GetDirectoryName(inputFile), Platform.StaticLibraryFilePrefix + Path.GetFileName(inputFile) + Platform.StaticLibraryFileExtension); if (Path.GetFileNameWithoutExtension(inputFile) == "System.Private.CoreLib") { // Use pre-compiled binaries DotNetSdk.Instance.GetHostRuntime(TargetPlatform.Web, TargetArchitecture.x86, out var hostRuntime); assemblyPath = Path.Combine(hostRuntime.Path, "libmonosgen-2.0.a"); } options.OutputFiles.Add(assemblyPath); } return false; }*/ case CSharpOptions.ActionTypes.GetPlatformTools: { string arch = Platform.BuildTargetArchitecture switch { TargetArchitecture.x64 => "x64", TargetArchitecture.ARM64 => "arm64", _ => throw new PlatformNotSupportedException(Platform.BuildTargetArchitecture.ToString()), }; string os; switch (Platform.BuildPlatform.Target) { case TargetPlatform.Windows: os = "win"; break; default: throw new PlatformNotSupportedException(Platform.BuildPlatform.Target.ToString()); } options.PlatformToolsPath = Path.Combine(DotNetSdk.SelectVersionFolder(Path.Combine(DotNetSdk.Instance.RootPath, $"packs/Microsoft.NETCore.App.Runtime.AOT.{os}-{arch}.Cross.browser-wasm")), "tools"); return false; } case CSharpOptions.ActionTypes.MonoLink: { // Setup arguments var args = new List(); //args.AddRange(options.LinkEnv.CustomArgs); { args.Add(string.Format("-o \"{0}\"", options.OutputFiles[0].Replace('\\', '/'))); //AddSharedArgs(args, options, false, false); // Setup memory var initialMemory = Configuration.WebInitialMemory; //if (options.CompileEnv.Sanitizers.HasFlag(Sanitizer.Address)) // initialMemory = Math.Max(initialMemory, 64); // Address Sanitizer needs more memory /*args.Add($"-sINITIAL_MEMORY={initialMemory}MB"); args.Add("-sSTACK_SIZE=4MB"); args.Add("-sALLOW_MEMORY_GROWTH=1"); // Setup file access (Game Cooker packs files with file_packager tool) args.Add("-sFORCE_FILESYSTEM");*/ //args.Add("-sLZ4"); // https://emscripten.org/docs/compiling/Dynamic-Linking.html#dynamic-linking // TODO: use -sMAIN_MODULE=2 and -sSIDE_MODULE=2 to strip unused code (mark public APIs with EMSCRIPTEN_KEEPALIVE) /*if (options.LinkEnv.Output == LinkerOutput.Executable) { args.Add("-sMAIN_MODULE"); args.Add("-sEXPORT_ALL"); } else*/ { args.Add("-sSIDE_MODULE"); } } args.Add("-Wl,--allow-multiple-definition"); // Multiple pthread-related definitions in dotnet runtime args.Add("-Wl,--start-group"); // Input libraries var libraryPaths = new HashSet(); var dynamicLibExt = Platform.SharedLibraryFileExtension; var executableExt = Platform.ExecutableFileExtension; foreach (var library in options.InputFiles) { /*var dir = Path.GetDirectoryName(library); var ext = Path.GetExtension(library); if (library.StartsWith("--use-port=")) { // Ports (https://emscripten.org/docs/compiling/Building-Projects.html#emscripten-ports) args.Add(library); } else if (string.IsNullOrEmpty(dir)) { args.Add(string.Format("\"-l{0}\"", library)); } else if (ext == executableExt) { // Skip executable } else if (ext == dynamicLibExt) { // Link against dynamic library //task.PrerequisiteFiles.Add(library); libraryPaths.Add(dir); args.Add(string.Format("\"-l{0}\"", GetLibName(library))); } else { //task.PrerequisiteFiles.Add(library); args.Add(string.Format("\"-l{0}\"", GetLibName(library))); libraryPaths.Add(dir); // FIXME }*/ args.Add(library.Replace(Path.DirectorySeparatorChar, '/') + Platform.StaticLibraryFileExtension); } // Input files (link static libraries last) /*task.PrerequisiteFiles.AddRange(options.LinkEnv.InputFiles); foreach (var file in options.LinkEnv.InputFiles.Where(x => !x.EndsWith(".a")).Concat(options.LinkEnv.InputFiles.Where(x => x.EndsWith(".a")))) { args.Add(string.Format("\"{0}\"", file.Replace('\\', '/'))); } // Additional lib paths libraryPaths.AddRange(options.LinkEnv.LibraryPaths); foreach (var path in libraryPaths) { args.Add(string.Format("-L\"{0}\"", path.Replace('\\', '/'))); }*/ args.Add("-Wl,--end-group"); // Use a response file (it can contain any commands that you would specify on the command line) bool useResponseFile = true; string responseFile = null; if (useResponseFile) { responseFile = Path.Combine(options.AssembliesPath, Path.GetFileName(options.OutputFiles[0]) + ".response"); //task.PrerequisiteFiles.Add(responseFile); Utilities.WriteFileIfChanged(responseFile, string.Join(Environment.NewLine, args)); args.Clear(); args.Add(string.Format("@\"{0}\"", responseFile)); } //task.WorkingDirectory = options.WorkingDirectory; //task.CommandPath = _compilerPath; //task.CommandArguments = string.Join(" ", args); int result = Utilities.Run(NativeCompilerPath, $"{string.Join(" ", args)}", null, Path.GetDirectoryName(options.OutputFiles[0]), Utilities.RunOptions.AppMustExist | Utilities.RunOptions.ConsoleLogOutput); if (result != 0) return true; return false; } case CSharpOptions.ActionTypes.MonoCompile: { string binaryExtension = Platform.BuildPlatform.Target == TargetPlatform.Windows ? ".exe" : ""; var aotCompilerPath = Path.Combine(options.PlatformToolsPath, "mono-aot-cross") + binaryExtension; //var clangPath = Path.Combine(ToolchainPath, "usr/bin/clang"); var inputFile = options.InputFiles[0]; var inputFileAsm = inputFile + ".s"; var inputFileObj = inputFile + ".o"; var outputFileDylib = options.OutputFiles[0]; var inputFileFolder = Path.GetDirectoryName(inputFile); /*if (Path.GetFileNameWithoutExtension(inputFile) == "System.Private.CoreLib") { // Pre-compiled, skip return false; }*/ string outputFile; { if (Configuration.AOTMode == DotNetAOTModes.MonoAOTDynamic) outputFile = inputFile + Platform.SharedLibraryFileExtension; else outputFile = Path.Combine(Path.GetDirectoryName(inputFile), Platform.StaticLibraryFilePrefix + Path.GetFileName(inputFile) + Platform.StaticLibraryFileExtension); } // Setup options bool debugSymbols = options.EnableDebugSymbols; bool useLLVM = true; //if (useLLVM) debugSymbols = false; var llvmPath = "\"C:\\Program Files\\LLVM\\bin\"";//Path.Combine(EmscriptenSdk.Instance.EmscriptenPath, "bin"); var monoAotMode = "full"; if (useLLVM) monoAotMode = "llvmonly"; //if ("mscorlib.dll" == "") // monoAotMode = "interp"; var monoDebugMode = debugSymbols ? "soft-debug" : "nodebug"; var aotCompilerArgs = $"{(useLLVM ? "--llvm" : "")} --aot={monoAotMode},asmonly,verbose,stats,print-skipped,{monoDebugMode}{(useLLVM ? $",llvm-path={llvmPath},llvm-outfile=" + Path.GetFileName(outputFile) : "")} -O=float32"; //aotCompilerArgs += " --verbose"; //aotCompilerArgs += $" --aot-path=\"{inputFileFolder}\""; //var aotCompilerArgs = $"{(useLLVM ? " --llvm" : "")} --aot=static,llvmonly,verbose,stats,print-skipped,llvm-path={llvmPath},{monoDebugMode}{(useLLVM ? ",llvm-outfile=" + Path.GetFileName(outputFile) : "")} -O=float32"; //var aotCompilerArgs = $"{(useLLVM ? " --llvm" : "")} --aot=static,llvmonly,verbose,stats,print-skipped,{monoDebugMode}{(useLLVM ? ",llvm-outfile=" + Path.GetFileName(outputFile) : "")} -O=float32"; //if (debugSymbols || options.EnableToolDebug) // aotCompilerArgs = "--debug " + aotCompilerArgs; var envVars = new Dictionary(); envVars["MONO_PATH"] = options.ClassLibraryPath.Replace('/', Path.DirectorySeparatorChar) + Path.PathSeparator + options.AssembliesPath.Replace('/', Path.DirectorySeparatorChar); Log.Info("MONO_PATH: " + envVars["MONO_PATH"]); if (options.EnableToolDebug) { envVars["MONO_LOG_LEVEL"] = "debug"; } //envVars["MONO_DEBUG"] = "gen-seq-points"; // Run cross-compiler compiler (outputs assembly code) int result = Utilities.Run(aotCompilerPath, $"{aotCompilerArgs} \"{inputFile}\"", null, inputFileFolder, Utilities.RunOptions.AppMustExist | Utilities.RunOptions.ConsoleLogOutput, envVars); if (result != 0) return true; // Get build args for iOS /*var clangArgs = new List(); AddArgsCommon(null, clangArgs); var clangArgsText = string.Join(" ", clangArgs); // Build object file result = Utilities.Run(clangPath, $"\"{inputFileAsm}\" -c -o \"{inputFileObj}\" " + clangArgsText, null, inputFileFolder, Utilities.RunOptions.AppMustExist | Utilities.RunOptions.ConsoleLogOutput, envVars); if (result != 0) return true; // Build dylib file result = Utilities.Run(clangPath, $"\"{inputFileObj}\" -dynamiclib -fPIC -o \"{outputFileDylib}\" " + clangArgsText, null, inputFileFolder, Utilities.RunOptions.AppMustExist | Utilities.RunOptions.ConsoleLogOutput, envVars); if (result != 0) return true; // Clean intermediate results File.Delete(inputFileAsm); File.Delete(inputFileObj); // Fix rpath id result = Utilities.Run("install_name_tool", $"-id \"@rpath/{Path.GetFileName(outputFileDylib)}\" \"{outputFileDylib}\"", null, inputFileFolder, Utilities.RunOptions.ConsoleLogOutput, envVars); if (result != 0) return true;*/ return false; } } return base.CompileCSharp(ref options); } #if false /// public override bool CompileCSharp(ref CSharpOptions options) { switch (options.Action) { case CSharpOptions.ActionTypes.MonoCompile: { var aotCompilerPath = Path.Combine(options.PlatformToolsPath, "mono-aot-cross.exe"); // Setup options var monoAotMode = "full"; var monoDebugMode = options.EnableDebugSymbols ? "soft-debug" : "nodebug"; var aotCompilerArgs = $"--aot={monoAotMode},verbose,stats,print-skipped,{monoDebugMode} -O=all"; if (options.EnableDebugSymbols || options.EnableToolDebug) aotCompilerArgs = "--debug " + aotCompilerArgs; var envVars = new Dictionary(); envVars["MONO_PATH"] = options.AssembliesPath + ";" + options.ClassLibraryPath; if (options.EnableToolDebug) { envVars["MONO_LOG_LEVEL"] = "debug"; } // Run cross-compiler compiler int result = Utilities.Run(aotCompilerPath, $"{aotCompilerArgs} \"{options.InputFiles[0]}\"", null, ""/*options.PlatformToolsPath*/, Utilities.RunOptions.AppMustExist | Utilities.RunOptions.ConsoleLogOutput, envVars); return result != 0; } } return base.CompileCSharp(ref options); } #endif private Task CreateBinary(TaskGraph graph, BuildOptions options, string outputFilePath) { var task = graph.Add(); // Setup arguments var args = new List(); args.AddRange(options.LinkEnv.CustomArgs); { args.Add(string.Format("-o \"{0}\"", outputFilePath.Replace('\\', '/'))); AddSharedArgs(args, options, options.LinkEnv.DebugInformation, options.LinkEnv.Optimization); // Setup memory var initialMemory = Configuration.WebInitialMemory; if (options.CompileEnv.Sanitizers.HasFlag(Sanitizer.Address)) initialMemory = Math.Max(initialMemory, 64); // Address Sanitizer needs more memory args.Add($"-sINITIAL_MEMORY={initialMemory}MB"); args.Add("-sSTACK_SIZE=4MB"); args.Add("-sALLOW_MEMORY_GROWTH=1"); // Setup file access (Game Cooker packs files with file_packager tool) args.Add("-sFORCE_FILESYSTEM"); args.Add("-sLZ4"); // https://emscripten.org/docs/compiling/Dynamic-Linking.html#dynamic-linking // TODO: use -sMAIN_MODULE=2 and -sSIDE_MODULE=2 to strip unused code (mark public APIs with EMSCRIPTEN_KEEPALIVE) if (options.LinkEnv.Output == LinkerOutput.Executable) { args.Add("-sMAIN_MODULE"); args.Add("-sEXPORT_ALL"); } else { args.Add("-sSIDE_MODULE"); } } args.Add("-Wl,--allow-multiple-definition"); // Multiple pthread-related definitions in dotnet runtime args.Add("-Wl,--start-group"); // Input libraries var libraryPaths = new HashSet(); var dynamicLibExt = Platform.SharedLibraryFileExtension; var executableExt = Platform.ExecutableFileExtension; foreach (var library in options.LinkEnv.InputLibraries.Concat(options.Libraries)) { var dir = Path.GetDirectoryName(library); var ext = Path.GetExtension(library); if (library.StartsWith("--use-port=")) { // Ports (https://emscripten.org/docs/compiling/Building-Projects.html#emscripten-ports) args.Add(library); } else if (string.IsNullOrEmpty(dir)) { args.Add(string.Format("\"-l{0}\"", library)); } else if (ext == executableExt) { // Skip executable } else if (ext == dynamicLibExt) { // Link against dynamic library task.PrerequisiteFiles.Add(library); libraryPaths.Add(dir); args.Add(string.Format("\"-l{0}\"", GetLibName(library))); } else { task.PrerequisiteFiles.Add(library); args.Add(string.Format("\"-l{0}\"", GetLibName(library))); libraryPaths.Add(dir); // FIXME } } // Input files (link static libraries last) task.PrerequisiteFiles.AddRange(options.LinkEnv.InputFiles); foreach (var file in options.LinkEnv.InputFiles.Where(x => !x.EndsWith(".a")).Concat(options.LinkEnv.InputFiles.Where(x => x.EndsWith(".a")))) { args.Add(string.Format("\"{0}\"", file.Replace('\\', '/'))); } // Additional lib paths libraryPaths.AddRange(options.LinkEnv.LibraryPaths); foreach (var path in libraryPaths) { args.Add(string.Format("-L\"{0}\"", path.Replace('\\', '/'))); } args.Add("-Wl,--end-group"); // Use a response file (it can contain any commands that you would specify on the command line) bool useResponseFile = true; string responseFile = null; if (useResponseFile) { responseFile = Path.Combine(options.IntermediateFolder, Path.GetFileName(outputFilePath) + ".response"); task.PrerequisiteFiles.Add(responseFile); Utilities.WriteFileIfChanged(responseFile, string.Join(Environment.NewLine, args)); } // Link task.WorkingDirectory = options.WorkingDirectory; task.CommandPath = _compilerPath; task.CommandArguments = useResponseFile ? string.Format("@\"{0}\"", responseFile) : string.Join(" ", args); task.InfoMessage = "Linking " + outputFilePath; task.Cost = task.PrerequisiteFiles.Count; task.ProducedFiles.Add(outputFilePath); return task; } /// public override void LinkFiles(TaskGraph graph, BuildOptions options, string outputFilePath) { outputFilePath = Utilities.NormalizePath(outputFilePath); Task linkTask; switch (options.LinkEnv.Output) { case LinkerOutput.Executable: case LinkerOutput.SharedLibrary: linkTask = CreateBinary(graph, options, outputFilePath); break; case LinkerOutput.StaticLibrary: case LinkerOutput.ImportLibrary: default: throw new ArgumentOutOfRangeException(); } } } }