diff --git a/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp b/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp index c0188ffe2..74216f070 100644 --- a/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp +++ b/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp @@ -14,13 +14,14 @@ void PrecompileAssembliesStep::OnBuildStarted(CookingData& data) const DotNetAOTModes aotMode = data.Tools->UseAOT(); if (aotMode == DotNetAOTModes::None) return; + const auto& buildSettings = *BuildSettings::Get(); // Redirect C# assemblies to intermediate cooking directory (processed by ILC) data.ManagedCodeOutputPath = data.CacheDirectory / TEXT("AOTAssemblies"); // Reset any AOT cache from previous run if the AOT mode has changed (eg. Mono AOT -> ILC on Desktop) const String aotModeCacheFilePath = data.ManagedCodeOutputPath / TEXT("AOTMode.txt"); - String aotModeCacheValue = String::Format(TEXT("{};{}"), (int32)aotMode, (int32)data.Configuration); + String aotModeCacheValue = String::Format(TEXT("{};{};{}"), (int32)aotMode, (int32)data.Configuration, (int32)buildSettings.SkipUnusedDotnetLibsPackaging); for (const String& define : data.CustomDefines) aotModeCacheValue += define; if (FileSystem::DirectoryExists(data.ManagedCodeOutputPath)) @@ -60,9 +61,11 @@ bool PrecompileAssembliesStep::Perform(CookingData& data) const Char *platform, *architecture, *configuration = ::ToString(data.Configuration); data.GetBuildPlatformName(platform, architecture); const String logFile = data.CacheDirectory / TEXT("AotLog.txt"); - auto args = String::Format( + String args = String::Format( TEXT("-log -logfile=\"{}\" -runDotNetAOT -mutex -platform={} -arch={} -configuration={} -aotMode={} -binaries=\"{}\" -intermediate=\"{}\""), logFile, platform, architecture, configuration, ToString(aotMode), data.DataOutputPath, data.ManagedCodeOutputPath); + if (!buildSettings.SkipUnusedDotnetLibsPackaging) + args += TEXT(" -skipUnusedDotnetLibs=false"); // Run AOT on whole class library (not just used libs) for (const String& define : data.CustomDefines) { args += TEXT(" -D"); diff --git a/Source/Engine/Core/Config/BuildSettings.h b/Source/Engine/Core/Config/BuildSettings.h index 90fb3c144..318c304e0 100644 --- a/Source/Engine/Core/Config/BuildSettings.h +++ b/Source/Engine/Core/Config/BuildSettings.h @@ -77,11 +77,17 @@ public: bool ShadersGenerateDebugData = false; /// - /// If checked, .NET 7 Runtime won't be packaged with a game and will be required by user to be installed on system upon running game build. Available only on supported platforms such as Windows, Linux and macOS. + /// If checked, .NET Runtime won't be packaged with a game and will be required by user to be installed on system upon running game build. Available only on supported platforms such as Windows, Linux and macOS. /// API_FIELD(Attributes="EditorOrder(3000), EditorDisplay(\"Scripting\", \"Skip .NET Runtime Packaging\")") bool SkipDotnetPackaging = false; + /// + /// If checked, .NET Runtime packaging will skip unused libraries from packaging resulting in smaller game builds. + /// + API_FIELD(Attributes="EditorOrder(3010), EditorDisplay(\"Scripting\", \"Skip Unused .NET Runtime Libs Packaging\")") + bool SkipUnusedDotnetLibsPackaging = true; + public: /// /// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use. @@ -101,5 +107,6 @@ public: DESERIALIZE(ShadersNoOptimize); DESERIALIZE(ShadersGenerateDebugData); DESERIALIZE(SkipDotnetPackaging); + DESERIALIZE(SkipUnusedDotnetLibsPackaging); } }; diff --git a/Source/Tools/Flax.Build/Build/DotNet/DotNetAOT.cs b/Source/Tools/Flax.Build/Build/DotNet/DotNetAOT.cs index cab9a5c5f..c8ed9836a 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/DotNetAOT.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/DotNetAOT.cs @@ -43,6 +43,12 @@ namespace Flax.Build [CommandLine("aotMode", "")] public static DotNetAOTModes AOTMode; + /// + /// See SkipUnusedDotnetLibsPackaging in BuildSettings. + /// + [CommandLine("skipUnusedDotnetLibs", "")] + public static bool SkipUnusedDotnetLibsPackaging = true; + /// /// Executes AOT process as a part of the game cooking (called by PrecompileAssembliesStep in Editor). /// @@ -85,7 +91,7 @@ namespace Flax.Build // Find input files var inputFiles = Directory.GetFiles(aotAssembliesPath, "*.dll", SearchOption.TopDirectoryOnly).ToList(); - inputFiles.RemoveAll(x => x.EndsWith(".dll.dll") || Path.GetFileName(x) == "BuilderRulesCache.dll"); + inputFiles.RemoveAll(FilterAssembly); for (int i = 0; i < inputFiles.Count; i++) inputFiles[i] = Utilities.NormalizePath(inputFiles[i]); inputFiles.Sort(); @@ -239,27 +245,43 @@ namespace Flax.Build // Build list of assemblies to process (use game assemblies as root to walk over used references from stdlib) var assembliesPaths = new List(); - using (var assemblyResolver = new MonoCecil.BasicAssemblyResolver()) + if (Configuration.SkipUnusedDotnetLibsPackaging) { - assemblyResolver.SearchDirectories.Add(aotAssembliesPath); - assemblyResolver.SearchDirectories.Add(dotnetLibPath); - - var warnings = new HashSet(); - foreach (var inputFile in inputFiles) + // Use game assemblies as root to walk over used references from stdlib + using (var assemblyResolver = new MonoCecil.BasicAssemblyResolver()) { - try + assemblyResolver.SearchDirectories.Add(aotAssembliesPath); + assemblyResolver.SearchDirectories.Add(dotnetLibPath); + + var warnings = new HashSet(); + foreach (var inputFile in inputFiles) { - BuildAssembliesList(inputFile, assembliesPaths, assemblyResolver, string.Empty, warnings); - } - catch (Exception) - { - Log.Error($"Failed to load assembly '{inputFile}'"); - throw; + try + { + BuildAssembliesList(inputFile, assembliesPaths, assemblyResolver, string.Empty, warnings); + } + catch (Exception) + { + Log.Error($"Failed to load assembly '{inputFile}'"); + throw; + } } } } + else + { + // Use all libs + var stdLibFiles = Directory.GetFiles(dotnetLibPath, "*.dll", SearchOption.TopDirectoryOnly).ToList(); + stdLibFiles.RemoveAll(FilterAssembly); + for (int i = 0; i < stdLibFiles.Count; i++) + stdLibFiles[i] = Utilities.NormalizePath(stdLibFiles[i]); + stdLibFiles.Sort(); + assembliesPaths.AddRange(stdLibFiles); + assembliesPaths.AddRange(inputFiles); + } // Run compilation + bool failed = false; var compileAssembly = (string assemblyPath) => { // Determinate whether use debug information for that assembly @@ -297,7 +319,13 @@ namespace Flax.Build // Run cross-compiler compiler Log.Info(" * " + assemblyPath); - Utilities.Run(aotCompilerPath, $"{aotCompilerArgs} \"{assemblyPath}\"", null, platformToolsRoot, Utilities.RunOptions.AppMustExist | Utilities.RunOptions.ThrowExceptionOnError | Utilities.RunOptions.ConsoleLogOutput, envVars); + int result = Utilities.Run(aotCompilerPath, $"{aotCompilerArgs} \"{assemblyPath}\"", null, platformToolsRoot, Utilities.RunOptions.AppMustExist | Utilities.RunOptions.ConsoleLogOutput, envVars); + if (result != 0) + { + Log.Error("Failed to run AOT on assembly " + assemblyPath); + failed = true; + return; + } } // Skip if deployed file is already valid @@ -323,6 +351,8 @@ namespace Flax.Build foreach (var assemblyPath in assembliesPaths) compileAssembly(assemblyPath); } + if (failed) + throw new Exception($"Failed to run AOT. See log ({Configuration.LogFile})."); } else { @@ -334,6 +364,28 @@ namespace Flax.Build Utilities.FileCopy(Path.Combine(aotAssembliesPath, "THIRD-PARTY-NOTICES.TXT"), Path.Combine(dotnetOutputPath, "THIRD-PARTY-NOTICES.TXT")); } + internal static bool FilterAssembly(string x) + { + // Skip AOT output products + if (x.EndsWith(".dll.dll")) + return true; + + // Skip Flax.Build rules assembly + if (Path.GetFileName(x) == "BuilderRulesCache.dll") + return true; + + // Skip non-C# DLLs + try + { + using (AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(x, new ReaderParameters { ReadSymbols = false, InMemory = true, ReadingMode = ReadingMode.Deferred })) + return false; + } + catch + { + return true; + } + } + internal static void BuildAssembliesList(string assemblyPath, List outputList, IAssemblyResolver assemblyResolver, string callerPath, HashSet warnings) { // Skip if already processed