2020-12-14 14:46:33 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
using AutomationTool ;
using System ;
using System.Collections.Generic ;
using System.Linq ;
2021-01-07 15:27:48 -04:00
using System.IO ;
2020-12-14 14:46:33 -04:00
using System.Text.RegularExpressions ;
2020-12-21 23:07:37 -04:00
using EpicGames.Core ;
2020-12-14 14:46:33 -04:00
using UnrealBuildTool ;
namespace Turnkey.Commands
{
class ExecuteBuild : TurnkeyCommand
{
2021-02-19 11:59:55 -04:00
protected override CommandGroup Group = > CommandGroup . Builds ;
2021-01-07 15:27:48 -04:00
// could move this to TurnkeyUtils!
static string GetStructEntry ( string Input , string Property , bool bIsArrayProperty )
{
string PrimaryRegex ;
string AltRegex = null ;
if ( bIsArrayProperty )
{
PrimaryRegex = string . Format ( "{0}\\s*=\\s*\\((.*?)\\)" , Property ) ;
}
else
{
// handle quoted strings, allowing for quoited quotation marks (basically doing " followed by whatever, until we see a quote that was not proceeded by a \, and gather the whole mess in an outer group)
PrimaryRegex = string . Format ( "{0}\\s*=\\s*\"((.*?)[^\\\\])\"" , Property ) ;
AltRegex = string . Format ( "{0}\\s*=\\s*(.*?)\\," , Property ) ;
}
// attempt to match it!
Match Result = Regex . Match ( Input , PrimaryRegex ) ;
2021-01-19 11:12:59 -04:00
if ( ! Result . Success & & AltRegex ! = null )
2021-01-07 15:27:48 -04:00
{
Result = Regex . Match ( Input , AltRegex ) ;
}
// if we got a success, return the main match value
if ( Result . Success )
{
return Result . Groups [ 1 ] . Value . ToString ( ) ;
}
return null ;
}
2020-12-14 14:46:33 -04:00
protected override void Execute ( string [ ] CommandOptions )
{
// we need a platform to execute
List < UnrealTargetPlatform > Platforms = TurnkeyUtils . GetPlatformsFromCommandLineOrUser ( CommandOptions , null ) ;
2021-01-07 15:27:48 -04:00
FileReference ProjectFile = TurnkeyUtils . GetProjectFromCommandLineOrUser ( CommandOptions ) ;
2020-12-14 14:46:33 -04:00
2021-01-07 15:27:48 -04:00
// we need a project file, so if canceled, abore this command
if ( ProjectFile = = null )
2020-12-14 14:46:33 -04:00
{
2021-01-07 15:27:48 -04:00
return ;
2020-12-14 14:46:33 -04:00
}
string DesiredBuild = TurnkeyUtils . ParseParamValue ( "Build" , null , CommandOptions ) ;
// get a list of builds from config
foreach ( UnrealTargetPlatform Platform in Platforms )
{
2021-01-07 15:27:48 -04:00
ConfigHierarchy GameConfig = ConfigCache . ReadHierarchy ( ConfigHierarchyType . Game , ProjectFile . Directory , Platform ) ;
2020-12-14 14:46:33 -04:00
2021-01-19 11:12:59 -04:00
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 ) ;
}
2020-12-14 14:46:33 -04:00
Dictionary < string , string > BuildCommands = new Dictionary < string , string > ( StringComparer . InvariantCultureIgnoreCase ) ;
2021-01-07 15:27:48 -04:00
if ( Builds ! = null )
2020-12-14 14:46:33 -04:00
{
2021-01-07 15:27:48 -04:00
foreach ( string Build in Builds )
2020-12-14 14:46:33 -04:00
{
2021-01-07 15:27:48 -04:00
string Name = GetStructEntry ( Build , "Name" , false ) ;
2021-01-19 11:12:59 -04:00
string SpecificPlatforms = GetStructEntry ( Build , "SpecificPlatforms" , true ) ;
2021-01-07 15:27:48 -04:00
string Params = GetStructEntry ( Build , "BuildCookRunParams" , false ) ;
2020-12-14 14:46:33 -04:00
2021-01-07 15:27:48 -04:00
// make sure required entries are there
if ( Name = = null | | Params = = null )
2020-12-14 14:46:33 -04:00
{
continue ;
}
2021-01-07 15:27:48 -04:00
// if platforms are specified, and this platform isn't one of them, skip it
if ( ! string . IsNullOrEmpty ( SpecificPlatforms ) )
{
string [ ] PlatformList = SpecificPlatforms . Split ( ",\"" . ToCharArray ( ) , StringSplitOptions . RemoveEmptyEntries ) ;
// case insensitive Contains
if ( PlatformList . Length > 0 & & ! PlatformList . Any ( x = > x . Equals ( Platform . ToString ( ) , StringComparison . OrdinalIgnoreCase ) ) )
{
continue ;
}
}
// add to list of commands
BuildCommands . Add ( Name , Params ) ;
}
2020-12-14 14:46:33 -04:00
}
if ( BuildCommands . Count = = 0 )
{
2021-01-07 15:27:48 -04:00
TurnkeyUtils . Log ( "Unable to find a build for platform {0} and project {1}" , Platform , ProjectFile . GetFileNameWithoutAnyExtensions ( ) ) ;
2020-12-14 14:46:33 -04:00
continue ;
}
string FinalParams ;
if ( ! string . IsNullOrEmpty ( DesiredBuild ) & & BuildCommands . ContainsKey ( DesiredBuild ) )
{
FinalParams = BuildCommands [ DesiredBuild ] ;
}
else
{
List < string > BuildNames = BuildCommands . Keys . ToList ( ) ;
int Choice = TurnkeyUtils . ReadInputInt ( "Choose a build to execute" , BuildNames , true ) ;
if ( Choice = = 0 )
{
continue ;
}
FinalParams = BuildCommands [ BuildNames [ Choice - 1 ] ] ;
}
FinalParams = FinalParams . Replace ( "{Project}" , "\"" + ProjectFile . FullName + "\"" ) ;
FinalParams = FinalParams . Replace ( "{Platform}" , Platform . ToString ( ) ) ;
FinalParams = PerformIniReplacements ( FinalParams , ProjectFile . Directory , Platform ) ;
TurnkeyUtils . Log ( "Executing '{0}'..." , FinalParams ) ;
ExecuteBuildCookRun ( FinalParams ) ;
}
}
private 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 ( ) ;
AutomationTool . CommandInfo BCRCommand = new AutomationTool . CommandInfo ( ) ;
BCRCommand . CommandName = "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 . Execute ( new List < AutomationTool . CommandInfo > ( ) { BCRCommand } , ScriptCompiler . Commands ) ;
}
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]
2021-01-07 15:27:48 -04:00
string [ ] CommandAndModifiers = Spec . Split ( "|" . ToCharArray ( ) , StringSplitOptions . RemoveEmptyEntries ) ;
string [ ] Tokens = CommandAndModifiers [ 0 ] . Split ( ":" . ToCharArray ( ) ) ;
2020-12-14 14:46:33 -04:00
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 ) ;
2021-01-07 15:27:48 -04:00
2020-12-14 14:46:33 -04:00
if ( Tokens [ 0 ] . Equals ( "iniif" , StringComparison . InvariantCultureIgnoreCase ) )
{
bool bIsTrue ;
if ( bool . TryParse ( FoundValue , out bIsTrue ) & & bIsTrue )
{
2021-01-07 15:27:48 -04:00
FoundValue = IniIfValue ;
2020-12-14 14:46:33 -04:00
}
2021-01-07 15:27:48 -04:00
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 ] ) ;
2020-12-14 14:46:33 -04:00
}
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 ;
}
}
}