// 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 -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. 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" -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 -CloudDir="E:\BuildPatchCloude" -compactify =============================================================================*/ #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; TMap CustomFields; bool bCompactify = false; bool bPreviewCompactify = false; // 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; FCommandLineMatcher Matcher; bSuccess = true; Matcher.Command = TEXT("compactify"); bCompactify = Switches.FindMatch(Matcher) != INDEX_NONE; Matcher.Command = TEXT("preview"); bPreviewCompactify = bCompactify && Switches.FindMatch(Matcher) != INDEX_NONE; 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 ); // Check required param indexes bSuccess = bSuccess && CloudDirIdx != INDEX_NONE; if (!bCompactify) { 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 ); if (!bCompactify) { 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 ); } 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::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::Atoi64(*Right))); } } } FPaths::NormalizeDirectoryName( RootDirectory ); FPaths::NormalizeDirectoryName( CloudDirectory ); } // 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 BuildPatchServicesModule = StaticCastSharedPtr( FModuleManager::Get().LoadModule( TEXT( "BuildPatchServices" ) ) ); // Setup the module BuildPatchServicesModule->SetCloudDirectory( CloudDirectory + TEXT( "/" ) ); if (bCompactify) { // Run the compactify routine bSuccess = BuildPatchServicesModule->CompactifyCloudDirectory(bPreviewCompactify); } else { 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; Settings.CustomFields = CustomFields; // Run the build generation if (FParse::Param(FCommandLine::Get(), TEXT("nochunks"))) { bSuccess = BuildPatchServicesModule->GenerateFilesManifestFromDirectory(Settings); } else { bSuccess = BuildPatchServicesModule->GenerateChunksManifestFromDirectory(Settings); } } // Release the module ptr BuildPatchServicesModule.Reset(); // Check for processing error if (!bSuccess) { GLog->Log(ELogVerbosity::Error, TEXT("An fatal error occurred executing BuildPatchTool.exe")); return 2; } 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 ); }