Files
UnrealEngineUWP/Engine/Source/Programs/AutomationTool/AutomationUtils/ScriptCompiler.cs
Wes Hunt 7fa290bb33 Summary: running UAT from VS is simpler and faster.
UEB-261 - Ensure that compiling AutomationTool in VS will compile all other Automation Projects
* Just set AutomationTool as your startup project and pass the command to execute.
* VS will build the script modules at build time, instead of every time at runtime.
* To make this happen, "UBT.exe -ProjectFiles" now generates a companion AutomationTool.csproj.References that make AutomationTool depend on all Automation modules.
* AutomationTool.exe defaults to not building script modules at runtime. Pass -compile if you want to dynamically build them.
* Without the .references file, AutomationTool will only build itself and you will need to pass -compile.
* RunUAT.bat still works that same, defaulting to runtime compilation and supporting -nocompile flag. It then passes -compile (or nothing) to AutomationTool.

Other
* All Automation projects target .Net 4.5. Some already were and had hard dependencies on them (Rocket and SyncGithub -> Octokit). Now that AutomationTool directly depends on them, everything had to use .Net 4.5.
* Decoupled logic for -NoCompile and -NoCompileEditor. The flags are still confusing, but -NoCompile is no longer linked to -NoCompileEditor.
* Had to leave in stub support in UAT for -NoCompile else RunUAT.bat passes it along and UAT complains that it doesn't understand it.
* Added a CommandUtils.Run option to support run command, but still output the run duration.
* Reduced the verbosity when UAT.proj is run from dozens of lines per module to a single Module -> Output line. It was looking like there were problems, but it was just msbuild spew.
#codereview:ben.marsh

[CL 2615060 by Wes Hunt in Main branch]
2015-07-09 10:15:37 -04:00

337 lines
12 KiB
C#

// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using Microsoft.Win32;
using System.Reflection;
using System.Diagnostics;
using UnrealBuildTool;
using Tools.DotNETCommon.CaselessDictionary;
namespace AutomationTool
{
/// <summary>
/// Exception thrown by PreprocessScriptFile.
/// </summary>
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)))
{
}
}
/// <summary>
/// Compiles and loads script assemblies.
/// </summary>
class ScriptCompiler
{
#region Fields
private CaselessDictionary<Type> ScriptCommands;
#if DEBUG
const string BuildConfig = "Debug";
#else
const string BuildConfig = "Development";
#endif
const string DefaultScriptsDLLName = "AutomationScripts.Automation.dll";
#endregion
#region Compilation
public ScriptCompiler()
{
}
/// <summary>
/// Finds and/or compiles all script files and assemblies.
/// </summary>
/// <param name="AdditionalScriptsFolders">Additional script fodlers to look for source files in.</param>
public void FindAndCompileAllScripts(List<string> AdditionalScriptsFolders)
{
bool DoCompile = false;
if (GlobalCommandLine.Compile)
{
DoCompile = true;
}
// Change to Engine\Source (if exists) to properly discover all UBT classes
var OldCWD = Environment.CurrentDirectory;
var UnrealBuildToolCWD = CommandUtils.CombinePaths(CommandUtils.CmdEnv.LocalRoot, "Engine", "Source");
if (Directory.Exists(UnrealBuildToolCWD))
{
Environment.CurrentDirectory = UnrealBuildToolCWD;
}
// Register all the classes inside UBT
Log.TraceVerbose("Registering UBT Classes.");
UnrealBuildTool.UnrealBuildTool.RegisterAllUBTClasses();
Environment.CurrentDirectory = OldCWD;
// Compile only if not disallowed.
if (DoCompile && !String.IsNullOrEmpty(CommandUtils.CmdEnv.MsBuildExe))
{
CleanupScriptsAssemblies();
FindAndCompileScriptModules(AdditionalScriptsFolders);
}
var ScriptAssemblies = new List<Assembly>();
LoadPreCompiledScriptAssemblies(ScriptAssemblies);
// Setup platforms
Platform.InitializePlatforms(ScriptAssemblies.ToArray());
// Instantiate all the automation classes for interrogation
Log.TraceVerbose("Creating commands.");
ScriptCommands = new CaselessDictionary<Type>();
foreach (var CompiledScripts in ScriptAssemblies)
{
foreach (var ClassType in CompiledScripts.GetTypes())
{
if (ClassType.IsSubclassOf(typeof(BuildCommand)) && ClassType.IsAbstract == false)
{
if (ScriptCommands.ContainsKey(ClassType.Name) == false)
{
ScriptCommands.Add(ClassType.Name, ClassType);
}
else
{
Log.TraceWarning("Unable to add command {0} twice. Previous: {1}, Current: {2}", ClassType.Name,
ClassType.AssemblyQualifiedName, ScriptCommands[ClassType.Name].AssemblyQualifiedName);
}
}
}
}
}
private static void FindAndCompileScriptModules(List<string> AdditionalScriptsFolders)
{
var OldCWD = Environment.CurrentDirectory;
var UnrealBuildToolCWD = CommandUtils.CombinePaths(CommandUtils.CmdEnv.LocalRoot, "Engine", "Source");
// Convert script folders to be relative to UnrealBuildTool's expected CWD
var RemappedAdditionalScriptFolders = new List<string>();
foreach (var CurFolder in AdditionalScriptsFolders)
{
RemappedAdditionalScriptFolders.Add(UnrealBuildTool.Utils.MakePathRelativeTo(CurFolder, UnrealBuildToolCWD));
}
Environment.CurrentDirectory = UnrealBuildToolCWD;
// Configure the rules compiler
// Get all game folders and convert them to build subfolders.
var AllGameFolders = UnrealBuildTool.UEBuildTarget.DiscoverAllGameFolders();
var BuildFolders = new List<string>(AllGameFolders.Count);
foreach (var Folder in AllGameFolders)
{
var GameBuildFolder = CommandUtils.CombinePaths(Folder, "Build");
if (Directory.Exists(GameBuildFolder))
{
BuildFolders.Add(GameBuildFolder);
}
}
RemappedAdditionalScriptFolders.AddRange(BuildFolders);
Log.TraceVerbose("Discovering game folders.");
UnrealBuildTool.RulesCompiler.SetAssemblyNameAndGameFolders("UnrealAutomationToolRules", AllGameFolders);
var DiscoveredModules = UnrealBuildTool.RulesCompiler.FindAllRulesSourceFiles(UnrealBuildTool.RulesCompiler.RulesFileType.AutomationModule, RemappedAdditionalScriptFolders);
var ModulesToCompile = new List<string>(DiscoveredModules.Count);
foreach (var ModuleFilename in DiscoveredModules)
{
if (HostPlatform.Current.IsScriptModuleSupported(CommandUtils.GetFilenameWithoutAnyExtensions(ModuleFilename)))
{
ModulesToCompile.Add(ModuleFilename);
}
else
{
CommandUtils.LogVerbose("Script module {0} filtered by the Host Platform and will not be compiled.", ModuleFilename);
}
}
if ((UnrealBuildTool.BuildHostPlatform.Current.Platform == UnrealBuildTool.UnrealTargetPlatform.Win64) ||
(UnrealBuildTool.BuildHostPlatform.Current.Platform == UnrealBuildTool.UnrealTargetPlatform.Win32))
{
string Modules = string.Join(";", ModulesToCompile.ToArray());
var UATProj = CommandUtils.CombinePaths(CommandUtils.CmdEnv.LocalRoot, @"Engine\Source\Programs\AutomationTool\Scripts\UAT.proj");
var CmdLine = String.Format("\"{0}\" /p:Modules=\"{1}\" /p:Configuration={2} /verbosity:minimal", UATProj, Modules, BuildConfig);
Log.TraceInformation("Building Automation projects in parallel...");
// supress the run command because it can be long and intimidating, making the logs around this code harder to read.
var Result = CommandUtils.Run(CommandUtils.CmdEnv.MsBuildExe, CmdLine, Options: CommandUtils.ERunOptions.Default | CommandUtils.ERunOptions.NoLoggingOfRunCommand | CommandUtils.ERunOptions.LoggingOfRunDuration);
if (Result.ExitCode != 0)
{
throw new AutomationException(String.Format("Failed to build \"{0}\":{1}{2}", UATProj, Environment.NewLine, Result.Output));
}
}
else
{
CompileModules(ModulesToCompile);
}
Environment.CurrentDirectory = OldCWD;
}
/// <summary>
/// Compiles all script modules.
/// </summary>
/// <param name="Modules">Module project filenames.</param>
/// <param name="CompiledModuleFilenames">The resulting compiled module assembly filenames.</param>
private static void CompileModules(List<string> Modules)
{
Log.TraceInformation("Building script modules");
// Make sure DefaultScriptsDLLName is compiled first
var DefaultScriptsProjName = Path.ChangeExtension(DefaultScriptsDLLName, "csproj");
foreach (var ModuleName in Modules)
{
if (ModuleName.IndexOf(DefaultScriptsProjName, StringComparison.InvariantCultureIgnoreCase) >= 0)
{
Log.TraceInformation("Building script module: {0}", ModuleName);
try
{
CompileScriptModule(ModuleName);
}
catch (Exception Ex)
{
CommandUtils.Log(TraceEventType.Error, Ex);
throw new AutomationException("Failed to compile module {0}", ModuleName);
}
break;
}
}
// Second pass, compile everything else
foreach (var ModuleName in Modules)
{
if (ModuleName.IndexOf(DefaultScriptsProjName, StringComparison.InvariantCultureIgnoreCase) < 0)
{
Log.TraceInformation("Building script module: {0}", ModuleName);
try
{
CompileScriptModule(ModuleName);
}
catch (Exception Ex)
{
CommandUtils.Log(TraceEventType.Error, Ex);
throw new AutomationException("Failed to compile module {0}", ModuleName);
}
}
}
}
/// <summary>
/// Builds a script module (csproj file)
/// </summary>
/// <param name="ProjectFile"></param>
/// <returns></returns>
private static bool CompileScriptModule(string ProjectFile)
{
if (!ProjectFile.EndsWith(".csproj", StringComparison.InvariantCultureIgnoreCase))
{
throw new AutomationException(String.Format("Unable to build Project {0}. Not a valid .csproj file.", ProjectFile));
}
if (!CommandUtils.FileExists(ProjectFile))
{
throw new AutomationException(String.Format("Unable to build Project {0}. Project file not found.", ProjectFile));
}
var CmdLine = String.Format("\"{0}\" /verbosity:quiet /nologo /target:Build /property:Configuration={1} /property:Platform=AnyCPU /p:TreatWarningsAsErrors=true /p:BuildProjectReferences=true",
ProjectFile, BuildConfig);
// Compile the project
var Result = CommandUtils.Run(CommandUtils.CmdEnv.MsBuildExe, CmdLine);
if (Result.ExitCode != 0)
{
throw new AutomationException(String.Format("Failed to build \"{0}\":{1}{2}", ProjectFile, Environment.NewLine, Result.Output));
}
else
{
// Remove .Automation.csproj and copy to target dir
Log.TraceVerbose("Successfully compiled {0}", ProjectFile);
}
return Result.ExitCode == 0;
}
/// <summary>
/// Loads all precompiled assemblies (DLLs that end with *Scripts.dll).
/// </summary>
/// <param name="OutScriptAssemblies">List to store all loaded assemblies.</param>
private static void LoadPreCompiledScriptAssemblies(List<Assembly> OutScriptAssemblies)
{
CommandUtils.Log("Loading precompiled script DLLs");
bool DefaultScriptsDLLFound = false;
var ScriptsLocation = GetScriptAssemblyFolder();
if (CommandUtils.DirectoryExists(ScriptsLocation))
{
var ScriptDLLFiles = Directory.GetFiles(ScriptsLocation, "*.Automation.dll", SearchOption.AllDirectories);
CommandUtils.Log("Found {0} script DLL(s).", ScriptDLLFiles.Length);
foreach (var ScriptsDLLFilename in ScriptDLLFiles)
{
if (!HostPlatform.Current.IsScriptModuleSupported(CommandUtils.GetFilenameWithoutAnyExtensions(ScriptsDLLFilename)))
{
CommandUtils.LogVerbose("Script module {0} filtered by the Host Platform and will not be loaded.", ScriptsDLLFilename);
continue;
}
// Load the assembly into our app domain
CommandUtils.LogVerbose("Loading script DLL: {0}", ScriptsDLLFilename);
try
{
var Dll = AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(ScriptsDLLFilename));
OutScriptAssemblies.Add(Dll);
// Check if this is the default scripts DLL.
if (!DefaultScriptsDLLFound && String.Compare(Path.GetFileName(ScriptsDLLFilename), DefaultScriptsDLLName, true) == 0)
{
DefaultScriptsDLLFound = true;
}
}
catch (Exception Ex)
{
throw new AutomationException("Failed to load script DLL: {0}: {1}", ScriptsDLLFilename, Ex.Message);
}
}
}
else
{
CommandUtils.LogError("Scripts folder {0} does not exist!", ScriptsLocation);
}
// The default scripts DLL is required!
if (!DefaultScriptsDLLFound)
{
throw new AutomationException("{0} was not found or could not be loaded, can't run scripts.", DefaultScriptsDLLName);
}
}
private void CleanupScriptsAssemblies()
{
Log.TraceInformation("Cleaning up script DLL folder");
CommandUtils.DeleteDirectory(GetScriptAssemblyFolder());
}
private static string GetScriptAssemblyFolder()
{
return CommandUtils.CombinePaths(CommandUtils.CmdEnv.LocalRoot, "Engine", "Binaries", "DotNET", "AutomationScripts");
}
#endregion
#region Properties
public CaselessDictionary<Type> Commands
{
get { return ScriptCommands; }
}
#endregion
}
}