324 lines
11 KiB
C#
324 lines
11 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using AutomationTool;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.IO;
|
|
using System.Text.RegularExpressions;
|
|
using EpicGames.Core;
|
|
using UnrealBuildTool;
|
|
|
|
namespace Turnkey.Commands
|
|
{
|
|
class ExecuteBuild : TurnkeyCommand
|
|
{
|
|
protected override CommandGroup Group => CommandGroup.Builds;
|
|
|
|
protected override void Execute(string[] CommandOptions)
|
|
{
|
|
// we need a platform to execute
|
|
FileReference ProjectFile = TurnkeyUtils.GetProjectFromCommandLineOrUser(CommandOptions);
|
|
List<UnrealTargetPlatform> Platforms = TurnkeyUtils.GetPlatformsFromCommandLineOrUser(CommandOptions, null);
|
|
string DesiredBuild = TurnkeyUtils.ParseParamValue("Build", null, CommandOptions);
|
|
string ExtraOptions = TurnkeyUtils.ParseParamValue("ExtraOptions", "", CommandOptions);
|
|
string OutputDir = TurnkeyUtils.ParseParamValue("OutputDir", null, CommandOptions);
|
|
bool bPrintCommandOnly = TurnkeyUtils.ParseParam("PrintOnly", CommandOptions);
|
|
bool bInteractive = TurnkeyUtils.ParseParam("Interactive", CommandOptions);
|
|
|
|
// we need a project file, so if canceled, abore this command
|
|
if (ProjectFile == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// get a list of builds from config
|
|
foreach (UnrealTargetPlatform Platform in Platforms)
|
|
{
|
|
ConfigHierarchy GameConfig = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, ProjectFile.Directory, Platform);
|
|
|
|
List<string> EngineBuilds;
|
|
List<string> ProjectBuilds;
|
|
GameConfig.GetArray("/Script/UnrealEd.ProjectPackagingSettings", "EngineCustomBuilds", out EngineBuilds);
|
|
GameConfig.GetArray("/Script/UnrealEd.ProjectPackagingSettings", "ProjectCustomBuilds", out ProjectBuilds);
|
|
|
|
List<string> Builds = new List<string>();
|
|
if (EngineBuilds != null)
|
|
{
|
|
Builds.AddRange(EngineBuilds);
|
|
}
|
|
if (ProjectBuilds != null)
|
|
{
|
|
Builds.AddRange(ProjectBuilds);
|
|
}
|
|
|
|
Dictionary<string, Tuple<string, string>> BuildCommands = new Dictionary<string, Tuple<string, string>>(StringComparer.InvariantCultureIgnoreCase);
|
|
if (Builds != null)
|
|
{
|
|
foreach (string Build in Builds)
|
|
{
|
|
string Name = ConfigHierarchy.GetStructEntry(Build, "Name", false);
|
|
string Help = ConfigHierarchy.GetStructEntry(Build, "HelpText", false);
|
|
string SpecificPlatforms = ConfigHierarchy.GetStructEntry(Build, "SpecificPlatforms", true);
|
|
string Params = ConfigHierarchy.GetStructEntry(Build, "BuildCookRunParams", false);
|
|
|
|
// make sure required entries are there
|
|
if (Name == null || Params == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// if platforms are specified, and this platform isn't one of them, skip it
|
|
if (!string.IsNullOrEmpty(SpecificPlatforms))
|
|
{
|
|
string IniPlatformName = ConfigHierarchy.GetIniPlatformName(Platform);
|
|
string[] PlatformList = SpecificPlatforms.Split(",\"".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
|
|
// case insensitive Contains
|
|
if (PlatformList.Length > 0 && !PlatformList.Any(x => x.Equals(IniPlatformName, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// add to list of commands
|
|
BuildCommands.Add(Name, new Tuple<string, string>(Help, Params));
|
|
}
|
|
}
|
|
|
|
if (BuildCommands.Count == 0)
|
|
{
|
|
TurnkeyUtils.Log("Unable to find a build for platform {0} and project {1}", Platform, ProjectFile.GetFileNameWithoutAnyExtensions());
|
|
continue;
|
|
}
|
|
|
|
string FinalParams;
|
|
if (!string.IsNullOrEmpty(DesiredBuild) && BuildCommands.ContainsKey(DesiredBuild))
|
|
{
|
|
FinalParams = BuildCommands[DesiredBuild].Item2;
|
|
}
|
|
else
|
|
{
|
|
List<string> BuildNames = BuildCommands.Keys.ToList();
|
|
List<string> BuildItems = BuildNames.Select(x => x + (string.IsNullOrEmpty(BuildCommands[x].Item1) ? "" : $" [{BuildCommands[x].Item1}]")).ToList();
|
|
int Choice = TurnkeyUtils.ReadInputInt("Choose a build to execute", BuildItems, true);
|
|
if (Choice == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FinalParams = BuildCommands[BuildNames[Choice - 1]].Item2;
|
|
}
|
|
|
|
// make sure a project option is specified
|
|
if (!FinalParams.Contains("-project=", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
FinalParams = "-project={Project} " + FinalParams;
|
|
}
|
|
|
|
FinalParams = FinalParams.Replace("{Project}", "\"" + ProjectFile.FullName + "\"", StringComparison.InvariantCultureIgnoreCase);
|
|
FinalParams = FinalParams.Replace("{Platform}", Platform.ToString(), StringComparison.InvariantCultureIgnoreCase);
|
|
FinalParams = FinalParams.Replace("{ProjectPackagingSettings}", CreateBuild.MakeCommandLineFromPackagingSettings(Platform, ProjectFile, FinalParams));
|
|
FinalParams = PerformIniReplacements(FinalParams, ProjectFile.Directory, Platform);
|
|
|
|
// if there is a {DeviceId} param, get it from the commandline or user
|
|
if (FinalParams.Contains("{DeviceId}", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
List<DeviceInfo> Devices = TurnkeyUtils.GetDevicesFromCommandLineOrUser(CommandOptions, Platform);
|
|
string DeviceIds = string.Join("+", Devices.Select(x => x.Id));
|
|
FinalParams = FinalParams.Replace("{DeviceId}", DeviceIds, StringComparison.InvariantCultureIgnoreCase);
|
|
}
|
|
|
|
FinalParams += " " + ExtraOptions;
|
|
|
|
if (bPrintCommandOnly)
|
|
{
|
|
TurnkeyUtils.Log("To execute this build manually, run:");
|
|
TurnkeyUtils.Log("");
|
|
TurnkeyUtils.Report($"RunUAT BuildCookRun {FinalParams}");
|
|
TurnkeyUtils.Log("");
|
|
return;
|
|
}
|
|
|
|
// handle browsing for BrowseForDir after printing out, as it doesn't make much sense to ask for a directory or print out one
|
|
if (FinalParams.Contains("{BrowseForDir}"))
|
|
{
|
|
// if the -outputdir option was specified, use that, otherwise ask user for directory
|
|
if (OutputDir == null)
|
|
{
|
|
//if (OperatingSystem.IsWindows())
|
|
//{
|
|
// string ChosenFile = null;
|
|
// System.Threading.Thread t = new System.Threading.Thread(x =>
|
|
// {
|
|
// ChosenFile = UnrealWindowsForms.Utils.ShowOpenFileDialogAndReturnFilename("Project Files (*.uproject)|*.uproject");
|
|
// });
|
|
|
|
// t.SetApartmentState(System.Threading.ApartmentState.STA);
|
|
// t.Start();
|
|
// t.Join();
|
|
|
|
// if (ChosenFile == null)
|
|
// {
|
|
// continue;
|
|
// }
|
|
//}
|
|
//else
|
|
//{
|
|
// while (true)
|
|
{
|
|
OutputDir = TurnkeyUtils.ReadInput("Enter output directory:");
|
|
}
|
|
//}
|
|
|
|
}
|
|
if (string.IsNullOrEmpty(OutputDir))
|
|
{
|
|
TurnkeyUtils.Log("Cancelling...");
|
|
return;
|
|
}
|
|
// handle {BrowseForDir} with or without quotes already around it (if already has quotes, then replace with the path, and if
|
|
// without quotes, replace with path wrapped in quotes
|
|
FinalParams = FinalParams.Replace("\"{BrowseForDir}\"", OutputDir);
|
|
FinalParams = FinalParams.Replace("{BrowseForDir}", $"\"{OutputDir}\"");
|
|
}
|
|
|
|
TurnkeyUtils.Log("Executing '{0}'...", FinalParams);
|
|
ExecuteBuildCookRun(FinalParams);
|
|
}
|
|
}
|
|
|
|
internal static void ExecuteBuildCookRun(string Params)
|
|
{
|
|
// split the params on whitespace not inside quotes (see https://stackoverflow.com/questions/4780728/regex-split-string-preserving-quotes/4780801#4780801 to explain the regex)
|
|
Regex Matcher = new Regex("(?<=^[^\"]*(?:\"[^\"]*\"[^\"]*)*)\\s(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
|
|
// split the string, removing empty results
|
|
List<string> Arguments = Matcher.Split(Params).Where(x => x != "").ToList();
|
|
|
|
UnrealBuildBase.CommandInfo BCRCommand = new UnrealBuildBase.CommandInfo("BuildCookRun");
|
|
// chop off the first - character in all the commands (see Automation.ParseParam)
|
|
BCRCommand.Arguments = Arguments.Select(x => x.Substring(1)).ToList();
|
|
|
|
// use the BCR's exitcode as Turnkey's exitcode
|
|
TurnkeyUtils.ExitCode = Automation.ExecuteAsync(new List<UnrealBuildBase.CommandInfo>() { BCRCommand }, ScriptManager.Commands).Result;
|
|
}
|
|
|
|
string GetIniSetting(string Spec, DirectoryReference ProjectDir, UnrealTargetPlatform Platform)
|
|
{
|
|
// handle these cases:
|
|
// iniif:-option:Engine:/Script/Module.Class:bUseOption
|
|
// iniif:-option:bUseOption [convenience for ProjectPackagingSettings setting]
|
|
// inivalue:Engine:/Script/Module.Class:SomeSetting
|
|
// inivalue:SomeSetting [convenience for ProjectPackagingSettings setting]
|
|
|
|
string[] CommandAndModifiers = Spec.Split("|".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
|
|
string[] Tokens = CommandAndModifiers[0].Split(":".ToCharArray());
|
|
|
|
string ConfigName;
|
|
string SectionName;
|
|
string Key;
|
|
string IniIfValue = "";
|
|
if (Tokens[0].Equals("iniif", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
if (Tokens.Length == 3)
|
|
{
|
|
ConfigName = "Game";
|
|
SectionName = "/Script/UnrealEd.ProjectPackagingSettings";
|
|
Key = Tokens[2];
|
|
IniIfValue = Tokens[1];
|
|
}
|
|
else if (Tokens.Length == 5)
|
|
{
|
|
ConfigName = Tokens[2];
|
|
SectionName = Tokens[3];
|
|
Key = Tokens[4];
|
|
IniIfValue = Tokens[1];
|
|
}
|
|
else
|
|
{
|
|
TurnkeyUtils.Log("Found a bad iniif spec: {0}", Spec);
|
|
return "";
|
|
}
|
|
}
|
|
else if (Tokens[0].Equals("inivalue", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
if (Tokens.Length == 2)
|
|
{
|
|
ConfigName = "Game";
|
|
SectionName = "/Script/UnrealEd.ProjectPackagingSettings";
|
|
Key = Tokens[1];
|
|
}
|
|
else if (Tokens.Length == 4)
|
|
{
|
|
ConfigName = Tokens[1];
|
|
SectionName = Tokens[2];
|
|
Key = Tokens[3];
|
|
}
|
|
else
|
|
{
|
|
TurnkeyUtils.Log("Found a bad inivalue spec: {0}", Spec);
|
|
return "";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TurnkeyUtils.Log("Found a bad ini spec: {0}", Spec);
|
|
return "";
|
|
}
|
|
|
|
// get the value, if it exists (or empty string if not)
|
|
ConfigHierarchyType ConfigType;
|
|
if (!ConfigHierarchyType.TryParse(ConfigName, out ConfigType))
|
|
{
|
|
TurnkeyUtils.Log("Found a bad config name {0} in spec {1}", ConfigName, Spec);
|
|
return "";
|
|
}
|
|
ConfigHierarchy Config = ConfigCache.ReadHierarchy(ConfigType, ProjectDir, Platform);
|
|
string FoundValue;
|
|
Config.GetString(SectionName, Key, out FoundValue);
|
|
|
|
|
|
if (Tokens[0].Equals("iniif", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
bool bIsTrue;
|
|
if (bool.TryParse(FoundValue, out bIsTrue) && bIsTrue)
|
|
{
|
|
FoundValue = IniIfValue;
|
|
}
|
|
else
|
|
{
|
|
return "";
|
|
}
|
|
}
|
|
|
|
// look to see if we have a replace modifier to update the ini value, and apply it if so
|
|
if (CommandAndModifiers.Length > 1)
|
|
{
|
|
string[] SearchAndReplace = CommandAndModifiers[1].Split("=".ToCharArray());
|
|
if (SearchAndReplace.Length != 2)
|
|
{
|
|
TurnkeyUtils.Log("Found a search/replace modifier {0} in spec {1}", CommandAndModifiers, Spec);
|
|
return "";
|
|
}
|
|
FoundValue = FoundValue.Replace(SearchAndReplace[0], SearchAndReplace[1]);
|
|
}
|
|
|
|
return FoundValue;
|
|
}
|
|
|
|
private string PerformIniReplacements(string Params, DirectoryReference ProjectDir, UnrealTargetPlatform Platform)
|
|
{
|
|
Regex IniMatch = new Regex("({(ini.*?)})+");
|
|
foreach (Match Match in IniMatch.Matches(Params))
|
|
{
|
|
if (Match.Success)
|
|
{
|
|
// group[1] is {ini.....}, groups[2] is the same but without the {}
|
|
Params = Params.Replace(Match.Groups[1].Value, GetIniSetting(Match.Groups[2].Value, ProjectDir, Platform));
|
|
}
|
|
}
|
|
|
|
return Params;
|
|
}
|
|
}
|
|
}
|