// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.IO; using System.Reflection; using System.Diagnostics; using UnrealBuildTool; using EpicGames.Core; using System.Threading.Tasks; using UnrealBuildBase; namespace AutomationTool { /// /// Exception thrown by PreprocessScriptFile. /// public class CompilationException : AutomationException { public CompilationException(string Filename, int StartLine, int StartColumn, int EndLine, int EndColumn, string Message, params string[] Args) : base(String.Format("Compilation Failed.\n>{0}({1},{2},{3},{4}): error: {5}", Path.GetFullPath(Filename), StartLine + 1, StartColumn + 1, EndLine + 1, EndColumn + 1, String.Format(Message, Args))) { } } /// /// Compiles and loads script assemblies. /// public static class ScriptCompiler { private static Dictionary ScriptCommands; private static HashSet AllCompiledAssemblies; #if DEBUG const string BuildConfig = "Debug"; #else const string BuildConfig = "Development"; #endif const string DefaultScriptsDLLName = "AutomationScripts.Automation.dll"; /// /// Finds and/or compiles all script files and assemblies. /// /// Path to the current project. May be null, in which case we compile scripts for all projects. /// Additional script fodlers to look for source files in. public static void FindAndCompileAllScripts(string ScriptsForProjectFileName, List AdditionalScriptsFolders) { // Find all the project files Stopwatch SearchTimer = Stopwatch.StartNew(); List ProjectFiles = FindAutomationProjects(ScriptsForProjectFileName, AdditionalScriptsFolders); Log.TraceLog("Found {0} project files in {1:0.000}s", ProjectFiles.Count, SearchTimer.Elapsed.TotalSeconds); foreach(FileReference ProjectFile in ProjectFiles) { Log.TraceLog(" {0}", ProjectFile); } // Get the default properties for compiling the projects Dictionary MsBuildProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); MsBuildProperties.Add("Platform", "AnyCPU"); MsBuildProperties.Add("Configuration", BuildConfig); MsBuildProperties.Add("EngineDir", value: Unreal.EngineDirectory.FullName); // Read all the projects Stopwatch ParsingTimer = Stopwatch.StartNew(); CsProjectInfo[] Projects = new CsProjectInfo[ProjectFiles.Count]; Parallel.For(0, ProjectFiles.Count, Idx => Projects[Idx] = CsProjectInfo.Read(ProjectFiles[Idx], MsBuildProperties)); Log.TraceLog("Parsed project files in {0:0.000}s", ParsingTimer.Elapsed.TotalSeconds); // Get all the build artifacts BuildProducts = new HashSet(); HashSet OutputDirs = new HashSet(); OutputDirs.Add(DirectoryReference.Combine(Unreal.EngineDirectory, "Binaries", "DotNET")); // Don't want any artifacts from this directory (just AutomationTool.exe and AutomationScripts.dll) foreach (CsProjectInfo Project in Projects) { DirectoryReference OutputDir; if (!Project.TryGetOutputDir(out OutputDir)) { throw new AutomationException("Unable to get output directory for {0}", Project.ProjectPath); } if (OutputDirs.Add(OutputDir)) { if (DirectoryReference.Exists(OutputDir)) { BuildProducts.UnionWith(DirectoryReference.EnumerateFiles(OutputDir)); } else { Log.TraceLog("Output directory {0} does not exist; ignoring", OutputDir); } } } // Load everything Stopwatch LoadTimer = Stopwatch.StartNew(); List Assemblies = LoadAutomationAssemblies(Projects); AllCompiledAssemblies = new HashSet(Assemblies); Log.TraceLog("Loaded assemblies in {0:0.000}s", LoadTimer.Elapsed.TotalSeconds); // Setup platforms Platform.InitializePlatforms(Assemblies.ToArray()); // Instantiate all the automation classes for interrogation Log.TraceVerbose("Creating commands."); ScriptCommands = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (Assembly CompiledScripts in Assemblies) { try { foreach (Type ClassType in CompiledScripts.GetTypes()) { if (ClassType.IsSubclassOf(typeof(BuildCommand)) && ClassType.IsAbstract == false) { if (ScriptCommands.ContainsKey(ClassType.Name) == false) { ScriptCommands.Add(ClassType.Name, ClassType); } else { bool IsSame = string.Equals(ClassType.AssemblyQualifiedName, ScriptCommands[ClassType.Name].AssemblyQualifiedName); if (IsSame == false) { Log.TraceWarning("Unable to add command {0} twice. Previous: {1}, Current: {2}", ClassType.Name, ClassType.AssemblyQualifiedName, ScriptCommands[ClassType.Name].AssemblyQualifiedName); } } } } } catch (ReflectionTypeLoadException LoadEx) { foreach (Exception SubEx in LoadEx.LoaderExceptions) { Log.TraceWarning("Got type loader exception: {0}", SubEx.ToString()); } throw new AutomationException("Failed to add commands from {0}. {1}", CompiledScripts, LoadEx); } catch (Exception Ex) { throw new AutomationException("Failed to add commands from {0}. {1}", CompiledScripts, Ex); } } } private static List FindAutomationProjects(string ScriptsForProjectFileName, List AdditionalScriptsFolders) { // Configure the rules compiler // Get all game folders and convert them to build subfolders. List AllGameFolders = new List(); if (ScriptsForProjectFileName == null) { AllGameFolders = NativeProjects.EnumerateProjectFiles().Select(x => x.Directory).ToList(); } else { // Project automation scripts currently require source engine builds if (!Unreal.IsEngineInstalled()) { AllGameFolders = new List { new DirectoryReference(Path.GetDirectoryName(ScriptsForProjectFileName)) }; } } List AllAdditionalScriptFolders = AdditionalScriptsFolders.Select(x => new DirectoryReference(x)).ToList(); foreach (DirectoryReference Folder in AllGameFolders) { DirectoryReference GameBuildFolder = DirectoryReference.Combine(Folder, "Build"); if (DirectoryReference.Exists(GameBuildFolder)) { AllAdditionalScriptFolders.Add(GameBuildFolder); } } Log.TraceVerbose("Discovering game folders."); List DiscoveredModules = UnrealBuildTool.RulesCompiler.FindAllRulesSourceFiles(UnrealBuildTool.RulesCompiler.RulesFileType.AutomationModule, GameFolders: AllGameFolders, ForeignPlugins: null, AdditionalSearchPaths: AllAdditionalScriptFolders); List ModulesToCompile = new List(DiscoveredModules.Count); foreach (FileReference ModuleFilename in DiscoveredModules) { if (HostPlatform.Current.IsScriptModuleSupported(ModuleFilename.GetFileNameWithoutAnyExtensions())) { ModulesToCompile.Add(ModuleFilename); } else { CommandUtils.LogVerbose("Script module {0} filtered by the Host Platform and will not be compiled.", ModuleFilename); } } return ModulesToCompile; } /// /// Loads all precompiled assemblies (DLLs that end with *Scripts.dll). /// /// Projects to load /// List of compiled assemblies private static List LoadAutomationAssemblies(IEnumerable Projects) { List Assemblies = new List(); foreach (CsProjectInfo Project in Projects) { // Get the output assembly name FileReference AssemblyLocation; if (!Project.TryGetOutputFile(out AssemblyLocation)) { throw new AutomationException("Unable to get output file for {0}", Project.ProjectPath); } // Load the assembly into our app domain CommandUtils.LogLog("Loading script DLL: {0}", AssemblyLocation); try { AssemblyUtils.AddFileToAssemblyCache(AssemblyLocation.FullName); // Add a resolver for the Assembly directory, so that its dependencies may be found alongside it AssemblyUtils.InstallRecursiveAssemblyResolver(AssemblyLocation.Directory.FullName); Assembly Assembly = AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(AssemblyLocation.FullName)); Assemblies.Add(Assembly); } catch (Exception Ex) { throw new AutomationException("Failed to load script DLL: {0}: {1}", AssemblyLocation, Ex.Message); } } return Assemblies; } public static HashSet GetCompiledAssemblies() { if (AllCompiledAssemblies == null) { return new HashSet(); } else { return AllCompiledAssemblies; } } public static Dictionary Commands { get { return ScriptCommands; } } public static HashSet BuildProducts { get; private set; } } }