// 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.Reflection; using System.Diagnostics; using UnrealBuildTool; using Tools.DotNETCommon.CaselessDictionary; namespace AutomationTool { #region Command info /// /// Command to execute info. /// class CommandInfo { public string CommandName; public List Arguments = new List(); public override string ToString() { string Result = CommandName; Result += "("; for (int Index = 0; Index < Arguments.Count; ++Index) { if (Index > 0) { Result += ", "; } Result += Arguments[Index]; } Result += ")"; return Result; } } #endregion #region Command Line helpers /// /// Helper class for automatically registering command line params. /// public class CommandLineArg { public delegate void OnSetDelegate(); public string Name; public OnSetDelegate SetDelegate; /// /// True of this arg was set in the command line. /// public bool IsSet { get; private set; } public CommandLineArg(string InName, OnSetDelegate InSet = null) { Name = InName; SetDelegate = InSet; GlobalCommandLine.RegisteredArgs.Add(Name, this); } public void Set() { IsSet = true; if (SetDelegate != null) { SetDelegate(); } } public override string ToString() { return String.Format("{0}, {1}", Name, IsSet); } /// /// Returns true of this arg was set in the command line. /// public static implicit operator bool(CommandLineArg Arg) { return Arg.IsSet; } } /// /// Global command line parameters. /// public static class GlobalCommandLine { /// /// List of all registered command line parameters (global, not command-specific) /// public static CaselessDictionary RegisteredArgs = new CaselessDictionary(); public static CommandLineArg CompileOnly = new CommandLineArg("-CompileOnly"); public static CommandLineArg Verbose = new CommandLineArg("-Verbose"); public static CommandLineArg Submit = new CommandLineArg("-Submit"); public static CommandLineArg NoSubmit = new CommandLineArg("-NoSubmit"); public static CommandLineArg ForceLocal = new CommandLineArg("-ForceLocal"); public static CommandLineArg NoP4 = new CommandLineArg("-NoP4"); public static CommandLineArg P4 = new CommandLineArg("-P4"); public static CommandLineArg Preprocess = new CommandLineArg("-Preprocess"); public static CommandLineArg Compile = new CommandLineArg("-Compile"); /// /// This command is LEGACY because we used to run UAT.exe to compile scripts by default. /// Now we only compile by default when run via RunUAT.bat, which still understands -nocompile. /// However, the batch file simply passes on all arguments, so UAT will choke when encountering -nocompile. /// Keep this CommandLineArg around so that doesn't happen. /// public static CommandLineArg NoCompileLegacyDontUse = new CommandLineArg("-NoCompile"); public static CommandLineArg NoCompileEditor = new CommandLineArg("-NoCompileEditor"); public static CommandLineArg Help = new CommandLineArg("-Help"); public static CommandLineArg List = new CommandLineArg("-List"); public static CommandLineArg Rocket = new CommandLineArg("-Rocket"); public static CommandLineArg VS2015 = new CommandLineArg("-2015"); public static CommandLineArg NoKill = new CommandLineArg("-NoKill"); public static CommandLineArg Installed = new CommandLineArg("-Installed"); public static CommandLineArg UTF8Output = new CommandLineArg("-UTF8Output"); public static CommandLineArg NoAutoSDK = new CommandLineArg("-NoAutoSDK"); public static CommandLineArg IgnoreJunk = new CommandLineArg("-ignorejunk"); /// /// Allows you to use local storage for your root build storage dir (default of P:\Builds (on PC) is changed to Engine\Saved\LocalBuilds). Used for local testing. /// public static CommandLineArg UseLocalBuildStorage = new CommandLineArg("-UseLocalBuildStorage"); /// /// Force initialize static members by calling this. /// public static void Init() { Log.TraceVerbose("Registered {0} command line parameters.", RegisteredArgs.Count); } } #endregion [Help( @"Executes scripted commands AutomationTool.exe [-verbose] [-compileonly] [-p4] Command0 [-Arg0 -Arg1 -Arg2 …] Command1 [-Arg0 -Arg1 …] Command2 [-Arg0 …] Commandn … [EnvVar0=MyValue0 … EnvVarn=MyValuen]" )] [Help("verbose", "Enables verbose logging")] [Help("nop4", "Disables Perforce functionality (default if not run on a build machine)")] [Help("p4", "Enables Perforce functionality (default if run on a build machine)")] [Help("compileonly", "Does not run any commands, only compiles them")] [Help("compile", "Dynamically compiles all commands (otherwise assumes they are already built)")] [Help("forcelocal", "Forces local execution")] [Help("help", "Displays help")] [Help("list", "Lists all available commands")] [Help("submit", "Allows UAT command to submit changes")] [Help("nosubmit", "Prevents any submit attempts")] [Help("nokill", "Does not kill any spawned processes on exit")] [Help("ignorejunk", "Prevents UBT from cleaning junk files")] [Help("UseLocalBuildStorage", @"Allows you to use local storage for your root build storage dir (default of P:\Builds (on PC) is changed to Engine\Saved\LocalBuilds). Used for local testing.")] public static class Automation { #region Command line parsing /// /// Parses command line parameter. /// /// Parameter index /// Command line /// Recently parsed command /// The only project to build scripts for /// Additional script locations /// True if the parameter has been successfully parsed. private static void ParseParam(string CurrentParam, CommandInfo CurrentCommand, ref string OutScriptsForProjectFileName, List OutAdditionalScriptsFolders) { bool bGlobalParam = false; foreach (var RegisteredParam in GlobalCommandLine.RegisteredArgs) { if (String.Compare(CurrentParam, RegisteredParam.Key, StringComparison.InvariantCultureIgnoreCase) == 0) { // Register and exit, we're done here. RegisteredParam.Value.Set(); bGlobalParam = true; break; } } // The parameter was not found in the list of global parameters, continue looking... if (CurrentParam.StartsWith("-ScriptsForProject=", StringComparison.InvariantCultureIgnoreCase)) { if(OutScriptsForProjectFileName != null) { throw new AutomationException("The -ProjectScripts argument may only be specified once"); } var ProjectFileName = CurrentParam.Substring(CurrentParam.IndexOf('=') + 1); if(!File.Exists(ProjectFileName)) { throw new AutomationException("Project '{0}' does not exist", ProjectFileName); } OutScriptsForProjectFileName = Path.GetFullPath(ProjectFileName); } else if (CurrentParam.StartsWith("-ScriptDir=", StringComparison.InvariantCultureIgnoreCase)) { var ScriptDir = CurrentParam.Substring(CurrentParam.IndexOf('=') + 1); if (Directory.Exists(ScriptDir)) { OutAdditionalScriptsFolders.Add(ScriptDir); Log.TraceVerbose("Found additional script dir: {0}", ScriptDir); } else { throw new AutomationException("Specified ScriptDir doesn't exist: {0}", ScriptDir); } } else if (CurrentParam.StartsWith("-")) { if (CurrentCommand != null) { CurrentCommand.Arguments.Add(CurrentParam.Substring(1)); } else if (!bGlobalParam) { throw new AutomationException("Unknown parameter {0} in the command line that does not belong to any command.", CurrentParam); } } else if (CurrentParam.Contains("=")) { // Environment variable int ValueStartIndex = CurrentParam.IndexOf('=') + 1; string EnvVarName = CurrentParam.Substring(0, ValueStartIndex - 1); if (String.IsNullOrEmpty(EnvVarName)) { throw new AutomationException("Unable to parse environment variable that has no name. Error when parsing command line param {0}", CurrentParam); } string EnvVarValue = CurrentParam.Substring(ValueStartIndex); CommandUtils.SetEnvVar(EnvVarName, EnvVarValue); } } /// /// Parse the command line and create a list of commands to execute. /// /// Command line /// List containing the names of commands to execute /// Optional list of additional paths to look for scripts in private static void ParseCommandLine(string[] CommandLine, List OutCommandsToExecute, out string OutScriptsForProjectFileName, List OutAdditionalScriptsFolders) { // Initialize global command line parameters GlobalCommandLine.Init(); Log.TraceInformation("Parsing command line: {0}", String.Join(" ", CommandLine)); OutScriptsForProjectFileName = null; CommandInfo CurrentCommand = null; for (int Index = 0; Index < CommandLine.Length; ++Index) { var Param = CommandLine[Index]; if (Param.StartsWith("-") || Param.Contains("=")) { ParseParam(CommandLine[Index], CurrentCommand, ref OutScriptsForProjectFileName, OutAdditionalScriptsFolders); } else { CurrentCommand = new CommandInfo(); CurrentCommand.CommandName = CommandLine[Index]; OutCommandsToExecute.Add(CurrentCommand); } } // Validate var Result = OutCommandsToExecute.Count > 0 || GlobalCommandLine.Help || GlobalCommandLine.CompileOnly || GlobalCommandLine.List; if (OutCommandsToExecute.Count > 0) { Log.TraceVerbose("Found {0} scripts to execute:", OutCommandsToExecute.Count); foreach (var Command in OutCommandsToExecute) { Log.TraceVerbose(" " + Command.ToString()); } } else if (!Result) { throw new AutomationException("Failed to find scripts to execute in the command line params."); } if (GlobalCommandLine.NoP4 && GlobalCommandLine.P4) { throw new AutomationException("{0} and {1} can't be set simultaneously.", GlobalCommandLine.NoP4.Name, GlobalCommandLine.P4.Name); } if (GlobalCommandLine.NoSubmit && GlobalCommandLine.Submit) { throw new AutomationException("{0} and {1} can't be set simultaneously.", GlobalCommandLine.NoSubmit.Name, GlobalCommandLine.Submit.Name); } if (GlobalCommandLine.Rocket) { UnrealBuildTool.UnrealBuildTool.bRunningRocket = true; } } #endregion #region Main Program /// /// Main method. /// /// Command line public static ExitCode Process(string[] CommandLine) { // Initial check for local or build machine runs BEFORE we parse the command line (We need this value set // in case something throws the exception while parsing the command line) IsBuildMachine = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("uebp_LOCAL_ROOT")); // Scan the command line for commands to execute. var CommandsToExecute = new List(); string OutScriptsForProjectFileName; var AdditionalScriptsFolders = new List(); ParseCommandLine(CommandLine, CommandsToExecute, out OutScriptsForProjectFileName, AdditionalScriptsFolders); // Check for build machine override (force local) IsBuildMachine = GlobalCommandLine.ForceLocal ? false : IsBuildMachine; Log.TraceVerbose("IsBuildMachine={0}", IsBuildMachine); Environment.SetEnvironmentVariable("IsBuildMachine", IsBuildMachine ? "1" : "0"); // should we kill processes on exit ShouldKillProcesses = !GlobalCommandLine.NoKill; Log.TraceVerbose("ShouldKillProcesses={0}", ShouldKillProcesses); if (CommandsToExecute.Count == 0 && GlobalCommandLine.Help) { DisplayHelp(); return ExitCode.Success; } // Disable AutoSDKs if specified on the command line if (GlobalCommandLine.NoAutoSDK) { UEBuildPlatformSDK.bAllowAutoSDKSwitching = false; } // Setup environment Log.TraceInformation("Setting up command environment."); CommandUtils.InitCommandEnvironment(); // Change CWD to UE4 root. Environment.CurrentDirectory = CommandUtils.CmdEnv.LocalRoot; // Fill in the project info UnrealBuildTool.UProjectInfo.FillProjectInfo(); // Clean rules folders up ProjectUtils.CleanupFolders(); // Compile scripts. Log.TraceInformation("Compiling scripts."); ScriptCompiler Compiler = new ScriptCompiler(); using(TelemetryStopwatch ScriptCompileStopwatch = new TelemetryStopwatch("ScriptCompile")) { Compiler.FindAndCompileAllScripts(OutScriptsForProjectFileName, AdditionalScriptsFolders); } if (GlobalCommandLine.CompileOnly) { Log.TraceInformation("Compilation successful, exiting (CompileOnly)"); return ExitCode.Success; } if (GlobalCommandLine.List) { ListAvailableCommands(Compiler.Commands); return ExitCode.Success; } if (GlobalCommandLine.Help) { DisplayHelp(CommandsToExecute, Compiler.Commands); return ExitCode.Success; } // Enable or disable P4 support CommandUtils.InitP4Support(CommandsToExecute, Compiler.Commands); if (CommandUtils.P4Enabled) { Log.TraceInformation("Setting up Perforce environment."); CommandUtils.InitP4Environment(); CommandUtils.InitDefaultP4Connection(); } // Find and execute commands. return Execute(CommandsToExecute, Compiler.Commands); } /// /// Execute commands specified in the command line. /// /// /// private static ExitCode Execute(List CommandsToExecute, CaselessDictionary Commands) { for (int CommandIndex = 0; CommandIndex < CommandsToExecute.Count; ++CommandIndex) { var CommandInfo = CommandsToExecute[CommandIndex]; Log.TraceVerbose("Attempting to execute {0}", CommandInfo.ToString()); Type CommandType; if (!Commands.TryGetValue(CommandInfo.CommandName, out CommandType)) { throw new AutomationException("Failed to find command {0}", CommandInfo.CommandName); } BuildCommand Command = (BuildCommand)Activator.CreateInstance(CommandType); Command.Params = CommandInfo.Arguments.ToArray(); try { ExitCode Result = Command.Execute(); if(Result != ExitCode.Success) { return Result; } CommandUtils.Log("BUILD SUCCESSFUL"); } finally { // dispose of the class if necessary var CommandDisposable = Command as IDisposable; if (CommandDisposable != null) { CommandDisposable.Dispose(); } } // Make sure there's no directories on the stack. CommandUtils.ClearDirStack(); } return ExitCode.Success; } #endregion #region Help /// /// Display help for the specified commands (to execute) /// /// List of commands specified in the command line. /// All discovered command objects. private static void DisplayHelp(List CommandsToExecute, CaselessDictionary Commands) { for (int CommandIndex = 0; CommandIndex < CommandsToExecute.Count; ++CommandIndex) { var CommandInfo = CommandsToExecute[CommandIndex]; Type CommandType; if (Commands.TryGetValue(CommandInfo.CommandName, out CommandType) == false) { Log.TraceError("Help: Failed to find command {0}", CommandInfo.CommandName); } else { CommandUtils.Help(CommandType); } } } /// /// Display AutomationTool.exe help. /// private static void DisplayHelp() { CommandUtils.LogHelp(typeof(Automation)); } /// /// List all available commands. /// /// All vailable commands. private static void ListAvailableCommands(CaselessDictionary Commands) { string Message = Environment.NewLine; Message += "Available commands:" + Environment.NewLine; foreach (var AvailableCommand in Commands) { Message += String.Format(" {0}{1}", AvailableCommand.Key, Environment.NewLine); } CommandUtils.Log(Message); } #endregion #region Properties /// /// True if this process is running on a build machine, false if locally. /// /// /// The reason one this property exists in Automation class and not BuildEnvironment is that /// it's required long before BuildEnvironment is initialized. /// public static bool IsBuildMachine { get { if (!bIsBuildMachine.HasValue) { throw new AutomationException("Trying to access IsBuildMachine property before it was initialized."); } return (bool)bIsBuildMachine; } private set { bIsBuildMachine = value; } } private static bool? bIsBuildMachine; public static bool ShouldKillProcesses { get { if (!bShouldKillProcesses.HasValue) { throw new AutomationException("Trying to access ShouldKillProcesses property before it was initialized."); } return (bool)bShouldKillProcesses; } private set { bShouldKillProcesses = value; } } private static bool? bShouldKillProcesses; #endregion } }