2014-08-01 15:48:59 -04:00
// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved.
/*=============================================================================
BuildPatchToolMain . cpp : Implements the BuildPatchTool application ' s main loop .
The tool can be used in two modes of operation :
( 1 ) to generate patch data ( manifest , chunks , files ) for build images given the existing cloud data ; or
( 2 ) to " compactify " a cloud directory by removing all orphaned chunks , not referenced by any manifest file .
In order to trigger compactify functionality , the - compactify commandline argument should be specified .
GENERATE PATCH DATA MODE
The tool supports generating chunk based patches or simple file based patches . Chunk based patch data will be generated by default
but you can switch to file based patch data by adding the - nochunks commandline argument .
File based patch data is only recommended for small build images that probably wouldn ' t benefit from chunking . I . e . Updates are commonly only a few small changed files .
Required arguments :
- BuildRoot = " " Specifies in quotes the directory containing the build image to be read .
- CloudDir = " " Specifies in quotes the cloud directory where existing data will be recognised from , and new data added to .
- AppID = 123456 Specifies without quotes , the ID number for the app
- AppName = " " Specifies in quotes , the name of the app
- BuildVersion = " " Specifies in quotes , the version string for the build image
- AppLaunch = " " Specifies in quotes , the path to the app executable , must be relative to , and inside of BuildRoot .
- AppArgs = " " Specifies in quotes , the commandline to send to the app on launch .
Optional arguments :
- stdout Adds stdout logging to the app .
- nochunks Creates file based patch data instead of chunk based patch data .
- FileIgnoreList = " " Specifies in quotes , the path to a text file containing BuildRoot relative files , separated by \ r \ n line endings , to not be included in the build .
- PrereqName = " " Specifies in quotes , the display name for the prerequisites installer
- PrereqPath = " " Specifies in quotes , the prerequisites installer to launch on successful product install
- PrereqArgs = " " Specifies in quotes , the commandline to send to prerequisites installer on launch
2014-09-12 12:11:13 -04:00
- DataAgeThreshold = 12.5 Specified the maximum age ( in days ) of existing patch data which can be reused in the generated manifest
2014-09-16 10:26:24 -04:00
NB : - DataAgeThreshold can also be present in the [ PatchGeneration ] section of BuildPatchTool . ini in the cloud directory
NB : If - DataAgeThreshold is not supplied , either on the command - line or in BuildPatchTool . ini , then all existing data is eligible for reuse in the generated manifest
2014-09-12 12:11:13 -04:00
Example command lines :
- BuildRoot = " D: \ Builds \ ExampleGame_[07-02_03.00] " - FileIgnoreList = " D: \ Builds \ ExampleGame_[07-02_03.00] \ Manifest_DebugFiles.txt " - CloudDir = " D: \ BuildPatchCloud " - AppID = 123456 - AppName = " Example " - BuildVersion = " ExampleGame_[07-02_03.00] " - AppLaunch = " . \ ExampleGame \ Binaries \ Win32 \ ExampleGame.exe " - AppArgs = " -pak -nosteam " - DataAgeThreshold = 12
- BuildRoot = " C: \ Program Files (x86) \ Community Portal " - CloudDir = " E: \ BuildPatchCloud " - AppID = 0 - AppName = " Community Portal " - BuildVersion = " ++depot+UE4-CL-1791234 " - AppLaunch = " . \ Engine \ Binaries \ Win32 \ CommunityPortal.exe " - AppArgs = " " - nochunks - DataAgeThreshold = 12
2014-08-01 15:48:59 -04:00
- custom = " field=value " Adds a custom string field to the build manifest
- customint = " field=number " Adds a custom int64 field to the build manifest
- customfloat = " field=number " Adds a custom double field to the build manifest
COMPACTIFY MODE
Required arguments :
- CloudDir = " " Specifies in quotes the cloud directory where manifest files and chunks to be compactified can be found .
- compactify Must be specified to launch the tool in compactify mode
Optional arguments :
- stdout Adds stdout logging to the app .
- preview Log all the actions it will take to update internal structures , but don ' t actually execute them .
2014-09-12 12:11:13 -04:00
- ManifestsList = " " Specifies in quotes , the list of manifest filenames to keep following the operation . If omitted , all manifests are kept .
- ManifestsFile = " " Specifies in quotes , the name of the file ( relative to CloudDir ) which contains a list of manifests to keep , one manifest filename per line
- DataAgeThreshold = 14.25 The maximum age in days of chunk files that will be retained . All older chunks will be deleted .
2014-08-01 15:48:59 -04:00
2014-09-12 12:11:13 -04:00
NB : If - ManifestsList is specified , then - ManifestsFile is ignored .
2014-09-16 10:26:24 -04:00
NB : - DataAgeThreshold can also be present in the [ Compactify ] section of BuildPatchTool . ini in the cloud directory
NB : If - DataAgeThreshold is not supplied , either on the command - line or in BuildPatchTool . ini , then all unreferenced existing data is eligible for deletion by the compactify process
2014-08-01 15:48:59 -04:00
Example command lines :
2014-09-12 12:11:13 -04:00
- CloudDir = " E: \ BuildPatchCloud " - compactify - DataAgeThreshold = 14
- CloudDir = " E: \ BuildPatchCloud " - compactify - DataAgeThreshold = 14 - ManifestsList = " Example_V1.manifest,Example_V2.manifest "
- CloudDir = " E: \ BuildPatchCloud " - compactify - DataAgeThreshold = 14 - ManifestsFile = " preserve_manifests.txt "
2014-08-01 15:48:59 -04:00
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = */
# include "RequiredProgramMainCPPInclude.h"
# include "BuildPatchServices.h"
// Ensure compiled with patch generation code
# if WITH_BUILDPATCHGENERATION
# else
# error BuildPatchTool must define WITH_BUILDPATCHGENERATION
# endif
IMPLEMENT_APPLICATION ( BuildPatchTool , " BuildPatchTool " ) ;
struct FCommandLineMatcher
{
FString Command ;
FCommandLineMatcher ( ) { }
FORCEINLINE bool Matches ( const FString & ToMatch ) const
{
if ( ToMatch . StartsWith ( Command , ESearchCase : : CaseSensitive ) )
{
return true ;
}
return false ;
}
} ;
class FBuildPatchOutputDevice : public FOutputDevice
{
public :
virtual void Serialize ( const TCHAR * V , ELogVerbosity : : Type Verbosity , const class FName & Category ) override
{
# if PLATFORM_USE_LS_SPEC_FOR_WIDECHAR
printf ( " \n %ls " , * FOutputDevice : : FormatLogLine ( Verbosity , Category , V , GPrintLogTimes ) ) ;
# else
wprintf ( TEXT ( " \n %s " ) , * FOutputDevice : : FormatLogLine ( Verbosity , Category , V , GPrintLogTimes ) ) ;
# endif
fflush ( stdout ) ;
}
} ;
int32 BuildPatchToolMain ( const TCHAR * CommandLine )
{
// Initialize the command line
FCommandLine : : Set ( CommandLine ) ;
// Add log devices
if ( FParse : : Param ( FCommandLine : : Get ( ) , TEXT ( " stdout " ) ) )
{
GLog - > AddOutputDevice ( new FBuildPatchOutputDevice ( ) ) ;
}
if ( FPlatformMisc : : IsDebuggerPresent ( ) )
{
GLog - > AddOutputDevice ( new FOutputDeviceDebug ( ) ) ;
}
GLog - > Logf ( TEXT ( " BuildPatchToolMain ran with: %s " ) , CommandLine ) ;
FPlatformProcess : : SetCurrentWorkingDirectoryToBaseDir ( ) ;
bool bSuccess = false ;
FString RootDirectory ;
FString CloudDirectory ;
uint32 AppID = 0 ;
FString AppName ;
FString BuildVersion ;
FString LaunchExe ;
FString LaunchCommand ;
FString IgnoreListFile ;
FString PrereqName ;
FString PrereqPath ;
FString PrereqArgs ;
2014-09-12 12:11:13 -04:00
FString ManifestsList ;
FString ManifestsFile ;
float DataAgeThreshold = 0.0f ;
FString IniFile ;
2014-08-01 15:48:59 -04:00
TMap < FString , FVariant > CustomFields ;
bool bCompactify = false ;
2014-09-12 12:11:13 -04:00
bool bPatchGeneration = true ;
bool bPreview = false ;
2014-09-16 10:26:24 -04:00
bool bPatchWithReuseAgeThreshold = true ;
2014-08-01 15:48:59 -04:00
// Collect all the info from the CommandLine
TArray < FString > Tokens , Switches ;
FCommandLine : : Parse ( FCommandLine : : Get ( ) , Tokens , Switches ) ;
if ( Switches . Num ( ) > 0 )
{
int32 BuildRootIdx ;
int32 CloudDirIdx ;
int32 AppIDIdx ;
int32 AppNameIdx ;
int32 BuildVersionIdx ;
int32 AppLaunchIdx ;
int32 AppArgsIdx ;
int32 FileIgnoreListIdx ;
int32 PrereqNameIdx ;
int32 PrereqPathIdx ;
int32 PrereqArgsIdx ;
2014-09-12 12:11:13 -04:00
int32 ManifestsListIdx ;
int32 ManifestsFileIdx ;
int32 DataAgeThresholdIdx ;
2014-08-01 15:48:59 -04:00
FCommandLineMatcher Matcher ;
bSuccess = true ;
Matcher . Command = TEXT ( " compactify " ) ;
bCompactify = Switches . FindMatch ( Matcher ) ! = INDEX_NONE ;
2014-09-12 12:11:13 -04:00
bPatchGeneration = ! bCompactify ;
2014-08-01 15:48:59 -04:00
Matcher . Command = TEXT ( " preview " ) ;
2014-09-12 12:11:13 -04:00
bPreview = bCompactify & & Switches . FindMatch ( Matcher ) ! = INDEX_NONE ;
2014-08-01 15:48:59 -04:00
Matcher . Command = TEXT ( " BuildRoot " ) ;
BuildRootIdx = Switches . FindMatch ( Matcher ) ;
Matcher . Command = TEXT ( " CloudDir " ) ;
CloudDirIdx = Switches . FindMatch ( Matcher ) ;
Matcher . Command = TEXT ( " AppID " ) ;
AppIDIdx = Switches . FindMatch ( Matcher ) ;
Matcher . Command = TEXT ( " AppName " ) ;
AppNameIdx = Switches . FindMatch ( Matcher ) ;
Matcher . Command = TEXT ( " BuildVersion " ) ;
BuildVersionIdx = Switches . FindMatch ( Matcher ) ;
Matcher . Command = TEXT ( " AppLaunch " ) ;
AppLaunchIdx = Switches . FindMatch ( Matcher ) ;
Matcher . Command = TEXT ( " AppArgs " ) ;
AppArgsIdx = Switches . FindMatch ( Matcher ) ;
Matcher . Command = TEXT ( " FileIgnoreList " ) ;
FileIgnoreListIdx = Switches . FindMatch ( Matcher ) ;
Matcher . Command = TEXT ( " PrereqName " ) ;
PrereqNameIdx = Switches . FindMatch ( Matcher ) ;
Matcher . Command = TEXT ( " PrereqPath " ) ;
PrereqPathIdx = Switches . FindMatch ( Matcher ) ;
Matcher . Command = TEXT ( " PrereqArgs " ) ;
PrereqArgsIdx = Switches . FindMatch ( Matcher ) ;
2014-09-12 12:11:13 -04:00
Matcher . Command = TEXT ( " ManifestsList " ) ;
ManifestsListIdx = Switches . FindMatch ( Matcher ) ;
Matcher . Command = TEXT ( " ManifestsFile " ) ;
ManifestsFileIdx = Switches . FindMatch ( Matcher ) ;
Matcher . Command = TEXT ( " DataAgeThreshold " ) ;
DataAgeThresholdIdx = Switches . FindMatch ( Matcher ) ;
2014-08-01 15:48:59 -04:00
// Check required param indexes
bSuccess = bSuccess & & CloudDirIdx ! = INDEX_NONE ;
2014-09-12 12:11:13 -04:00
if ( bPatchGeneration )
2014-08-01 15:48:59 -04:00
{
bSuccess = bSuccess & & BuildRootIdx ! = INDEX_NONE ;
bSuccess = bSuccess & & AppIDIdx ! = INDEX_NONE ;
bSuccess = bSuccess & & AppNameIdx ! = INDEX_NONE ;
bSuccess = bSuccess & & BuildVersionIdx ! = INDEX_NONE ;
bSuccess = bSuccess & & AppLaunchIdx ! = INDEX_NONE ;
bSuccess = bSuccess & & AppArgsIdx ! = INDEX_NONE ;
}
// Get required param values
bSuccess = bSuccess & & FParse : : Value ( * Switches [ CloudDirIdx ] , TEXT ( " CloudDir= " ) , CloudDirectory ) ;
2014-09-12 12:11:13 -04:00
if ( bPatchGeneration )
2014-08-01 15:48:59 -04:00
{
bSuccess = bSuccess & & FParse : : Value ( * Switches [ BuildRootIdx ] , TEXT ( " BuildRoot= " ) , RootDirectory ) ;
bSuccess = bSuccess & & FParse : : Value ( * Switches [ AppIDIdx ] , TEXT ( " AppID= " ) , AppID ) ;
bSuccess = bSuccess & & FParse : : Value ( * Switches [ AppNameIdx ] , TEXT ( " AppName= " ) , AppName ) ;
bSuccess = bSuccess & & FParse : : Value ( * Switches [ BuildVersionIdx ] , TEXT ( " BuildVersion= " ) , BuildVersion ) ;
bSuccess = bSuccess & & FParse : : Value ( * Switches [ AppLaunchIdx ] , TEXT ( " AppLaunch= " ) , LaunchExe ) ;
bSuccess = bSuccess & & FParse : : Value ( * Switches [ AppArgsIdx ] , TEXT ( " AppArgs= " ) , LaunchCommand ) ;
}
// Get optional param values
if ( FileIgnoreListIdx ! = INDEX_NONE )
{
FParse : : Value ( * Switches [ FileIgnoreListIdx ] , TEXT ( " FileIgnoreList= " ) , IgnoreListFile ) ;
}
if ( PrereqNameIdx ! = INDEX_NONE )
{
FParse : : Value ( * Switches [ PrereqNameIdx ] , TEXT ( " FileIgnoreList= " ) , PrereqName ) ;
}
if ( PrereqPathIdx ! = INDEX_NONE )
{
FParse : : Value ( * Switches [ PrereqPathIdx ] , TEXT ( " FileIgnoreList= " ) , PrereqPath ) ;
}
if ( PrereqArgsIdx ! = INDEX_NONE )
{
FParse : : Value ( * Switches [ PrereqArgsIdx ] , TEXT ( " FileIgnoreList= " ) , PrereqArgs ) ;
}
2014-09-12 12:11:13 -04:00
if ( ManifestsListIdx ! = INDEX_NONE )
{
bool bShouldStopOnComma = false ;
FParse : : Value ( * Switches [ ManifestsListIdx ] , TEXT ( " ManifestsList= " ) , ManifestsList , bShouldStopOnComma ) ;
}
else if ( ManifestsFileIdx ! = INDEX_NONE )
{
FParse : : Value ( * Switches [ ManifestsFileIdx ] , TEXT ( " ManifestsFile= " ) , ManifestsFile ) ;
}
2014-08-01 15:48:59 -04:00
FString CustomValue ;
FString Left ;
FString Right ;
for ( const auto & Switch : Switches )
{
if ( FParse : : Value ( * Switch , TEXT ( " custom= " ) , CustomValue ) )
{
if ( CustomValue . Split ( TEXT ( " = " ) , & Left , & Right ) )
{
Left . Trim ( ) ;
Left . TrimTrailing ( ) ;
Right . Trim ( ) ;
Right . TrimTrailing ( ) ;
CustomFields . Add ( Left , FVariant ( Right ) ) ;
}
}
else if ( FParse : : Value ( * Switch , TEXT ( " customfloat= " ) , CustomValue ) )
{
if ( CustomValue . Split ( TEXT ( " = " ) , & Left , & Right ) )
{
Left . Trim ( ) ;
Left . TrimTrailing ( ) ;
Right . Trim ( ) ;
Right . TrimTrailing ( ) ;
if ( ! Right . IsNumeric ( ) )
{
GLog - > Log ( ELogVerbosity : : Error , TEXT ( " An error occurred processing token -customint. Non Numeric character found right of = " ) ) ;
bSuccess = false ;
}
CustomFields . Add ( Left , FVariant ( TCString < TCHAR > : : Atod ( * Right ) ) ) ;
}
}
else if ( FParse : : Value ( * Switch , TEXT ( " customint= " ) , CustomValue ) )
{
if ( CustomValue . Split ( TEXT ( " = " ) , & Left , & Right ) )
{
Left . Trim ( ) ;
Left . TrimTrailing ( ) ;
Right . Trim ( ) ;
Right . TrimTrailing ( ) ;
if ( ! Right . IsNumeric ( ) )
{
GLog - > Log ( ELogVerbosity : : Error , TEXT ( " An error occurred processing token -customfloat. Non Numeric character found right of = " ) ) ;
bSuccess = false ;
}
CustomFields . Add ( Left , FVariant ( TCString < TCHAR > : : Atoi64 ( * Right ) ) ) ;
}
}
}
FPaths : : NormalizeDirectoryName ( RootDirectory ) ;
FPaths : : NormalizeDirectoryName ( CloudDirectory ) ;
2014-09-12 12:11:13 -04:00
if ( bSuccess )
{
// Initialize the configuration system, we can only do this reliably if we have CloudDirectory (i.e. bSuccess is true)
IniFile = CloudDirectory / TEXT ( " BuildPatchTool.ini " ) ;
GConfig - > InitializeConfigSystem ( ) ;
}
if ( DataAgeThresholdIdx ! = INDEX_NONE )
{
FParse : : Value ( * Switches [ DataAgeThresholdIdx ] , TEXT ( " DataAgeThreshold= " ) , DataAgeThreshold ) ;
}
else if ( bSuccess & & bCompactify )
{
2014-09-16 10:26:24 -04:00
// For compactification, if we don't pass in DataAgeThreshold, and it's not in BuildPatchTool.ini,
// then we set it to zero, to indicate that any unused chunks are valid for deletion
2014-09-12 12:11:13 -04:00
if ( ! GConfig - > GetFloat ( TEXT ( " Compactify " ) , TEXT ( " DataAgeThreshold " ) , DataAgeThreshold , IniFile ) )
{
2014-09-16 10:26:24 -04:00
GLog - > Log ( ELogVerbosity : : Warning , TEXT ( " DataAgeThreshold not supplied, so all unreferenced data is eliglble for deletion. Note that this process is NOT compatible with any concurrently running patch generaiton processes " ) ) ;
DataAgeThreshold = 0.0f ;
2014-09-12 12:11:13 -04:00
}
}
else if ( bSuccess & & bPatchGeneration )
{
2014-09-16 10:26:24 -04:00
// For patch generation, if we don't pass in DataAgeThreshold, and it's not specified in BuildPatchTool.ini,
// then we set bChunkWithReuseAgeThreshold to false, which indicates that *all* patch data is valid for reuse
2014-09-12 12:11:13 -04:00
if ( ! GConfig - > GetFloat ( TEXT ( " PatchGeneration " ) , TEXT ( " DataAgeThreshold " ) , DataAgeThreshold , IniFile ) )
{
2014-09-16 10:26:24 -04:00
GLog - > Log ( ELogVerbosity : : Warning , TEXT ( " DataAgeThreshold not supplied, so all existing data is eligible for reuse. Note that this process is NOT compatible with any concurrently running compactify processes " ) ) ;
DataAgeThreshold = 0.0f ;
bPatchWithReuseAgeThreshold = false ;
2014-09-12 12:11:13 -04:00
}
}
2014-08-01 15:48:59 -04:00
}
// Check for argument error
if ( ! bSuccess )
{
GLog - > Log ( ELogVerbosity : : Error , TEXT ( " An error occurred processing arguments " ) ) ;
return 1 ;
}
// Initialize the file manager
IFileManager : : Get ( ) . ProcessCommandLineOptions ( ) ;
// Populate cultures for localization (used by BuildPatch progress info)
BeginInitTextLocalization ( ) ;
FInternationalization I18N = FInternationalization : : Get ( ) ;
// Load the BuildPatchServices Module
TSharedPtr < IBuildPatchServicesModule > BuildPatchServicesModule = StaticCastSharedPtr < IBuildPatchServicesModule > ( FModuleManager : : Get ( ) . LoadModule ( TEXT ( " BuildPatchServices " ) ) ) ;
// Setup the module
BuildPatchServicesModule - > SetCloudDirectory ( CloudDirectory + TEXT ( " / " ) ) ;
if ( bCompactify )
{
2014-09-12 12:11:13 -04:00
// Split out our manifests to keep arg (if any) into an array of manifest filenames
TArray < FString > ManifestsArr ;
if ( ManifestsList . Len ( ) > 0 )
{
ManifestsList . ParseIntoArray ( & ManifestsArr , TEXT ( " , " ) , true ) ;
}
else if ( ManifestsFile . Len ( ) > 0 )
{
FString ManifestsFilePath = CloudDirectory / ManifestsFile ;
FString Temp ;
if ( FFileHelper : : LoadFileToString ( Temp , * ManifestsFilePath ) )
{
Temp . ReplaceInline ( TEXT ( " \r " ) , TEXT ( " \n " ) ) ;
Temp . ParseIntoArray ( & ManifestsArr , TEXT ( " \n " ) , true ) ;
}
else
{
GLog - > Log ( ELogVerbosity : : Error , TEXT ( " Could not open specified manifests to keep file " ) ) ;
BuildPatchServicesModule . Reset ( ) ;
return 2 ;
}
}
2014-08-01 15:48:59 -04:00
// Run the compactify routine
2014-09-12 12:11:13 -04:00
bSuccess = BuildPatchServicesModule - > CompactifyCloudDirectory ( ManifestsArr , DataAgeThreshold , bPreview ) ;
2014-08-01 15:48:59 -04:00
}
2014-09-12 12:11:13 -04:00
else if ( bPatchGeneration )
2014-08-01 15:48:59 -04:00
{
FBuildPatchSettings Settings ;
Settings . RootDirectory = RootDirectory + TEXT ( " / " ) ;
Settings . InAppID = AppID ;
Settings . AppName = AppName ;
Settings . BuildVersion = BuildVersion ;
Settings . LaunchExe = LaunchExe ;
Settings . LaunchCommand = LaunchCommand ;
Settings . IgnoreListFile = IgnoreListFile ;
Settings . PrereqName = PrereqName ;
Settings . PrereqPath = PrereqPath ;
Settings . PrereqArgs = PrereqArgs ;
2014-09-12 12:11:13 -04:00
Settings . DataAgeThreshold = DataAgeThreshold ;
2014-09-16 10:26:24 -04:00
Settings . bShouldHonorReuseThreshold = bPatchWithReuseAgeThreshold ;
2014-08-01 15:48:59 -04:00
Settings . CustomFields = CustomFields ;
// Run the build generation
if ( FParse : : Param ( FCommandLine : : Get ( ) , TEXT ( " nochunks " ) ) )
{
bSuccess = BuildPatchServicesModule - > GenerateFilesManifestFromDirectory ( Settings ) ;
}
else
{
bSuccess = BuildPatchServicesModule - > GenerateChunksManifestFromDirectory ( Settings ) ;
}
}
2014-09-12 12:11:13 -04:00
else
{
GLog - > Log ( ELogVerbosity : : Error , TEXT ( " Unknown tool mode " ) ) ;
BuildPatchServicesModule . Reset ( ) ;
return 3 ;
}
2014-08-01 15:48:59 -04:00
// Release the module ptr
BuildPatchServicesModule . Reset ( ) ;
// Check for processing error
if ( ! bSuccess )
{
2014-09-12 12:11:13 -04:00
GLog - > Log ( ELogVerbosity : : Error , TEXT ( " A fatal error occurred executing BuildPatchTool.exe " ) ) ;
return 4 ;
2014-08-01 15:48:59 -04:00
}
GLog - > Log ( TEXT ( " BuildPatchToolMain completed successfuly " ) ) ;
return 0 ;
}
INT32_MAIN_INT32_ARGC_TCHAR_ARGV ( )
{
FString CommandLine ;
for ( int32 Option = 1 ; Option < ArgC ; Option + + )
{
CommandLine + = TEXT ( " " ) ;
FString Argument ( ArgV [ Option ] ) ;
if ( Argument . Contains ( TEXT ( " " ) ) )
{
if ( Argument . Contains ( TEXT ( " = " ) ) )
{
FString ArgName ;
FString ArgValue ;
Argument . Split ( TEXT ( " = " ) , & ArgName , & ArgValue ) ;
Argument = FString : : Printf ( TEXT ( " %s= \" %s \" " ) , * ArgName , * ArgValue ) ;
}
else
{
Argument = FString : : Printf ( TEXT ( " \" %s \" " ) , * Argument ) ;
}
}
CommandLine + = Argument ;
}
return BuildPatchToolMain ( * CommandLine ) ;
}