Files
UnrealEngine/Engine/Source/Programs/UGSCore/Workspace.cpp
2025-05-18 13:04:45 +08:00

1272 lines
45 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Workspace.h"
#include "Misc/Paths.h"
#include "Misc/ScopeLock.h"
#include "HAL/FileManager.h"
#include "HAL/RunnableThread.h"
#include "HAL/PlatformProcess.h"
#include "HAL/Event.h"
#include "Algo/AnyOf.h"
#include "Misc/FileHelper.h"
#include "BuildStep.h"
#include "Telemetry.h"
#include "Utility.h"
namespace UGSCore
{
const TCHAR* FWorkspace::DefaultBuildTargets[] =
{
TEXT("UnrealHeaderTool Win64 Development, 0.1"),
TEXT("$(EditorTarget) Win64 $(EditorConfiguration), 0.7"),
TEXT("ShaderCompileWorker Win64 Development, 0.8"),
TEXT("UnrealLightmass Win64 Development, 0.9"),
TEXT("CrashReportClient Win64 Shipping, 1.0"),
TEXT("InterchangeWorker Win64 Development, 1.0"),
};
const FWorkspaceSyncCategory FWorkspace::DefaultSyncCategories[] =
{
FWorkspaceSyncCategory(FGuid(0x6703E989, 0xD912451D, 0x93ADB48D, 0xE748D282), TEXT("Content"), TEXT("*.uasset")),
FWorkspaceSyncCategory(FGuid(0x6507C2FB, 0x19DD403A, 0xAFA3BBF8, 0x98248D5A), TEXT("Documentation"), TEXT("/Engine/Documentation/...")),
FWorkspaceSyncCategory(FGuid(0xFD7C716E, 0x4BAD43AE, 0x8FAE8748, 0xEF9EE44D), TEXT("Platform Support: Android"), TEXT("/Engine/Source/ThirdParty/.../Android/...")),
FWorkspaceSyncCategory(FGuid(0x3299A73D, 0x21764C0F, 0xBC99C1C6, 0x631AF6C4), TEXT("Platform Support: HTML5"), TEXT("/Engine/Source/ThirdParty/.../HTML5/...;/Engine/Extras/ThirdPartyNotUE/emsdk/...")),
FWorkspaceSyncCategory(FGuid(0x176B2EB2, 0x35F74E8E, 0xB1315F1C, 0x5F0959AF), TEXT("Platform Support: iOS"), TEXT("/Engine/Source/ThirdParty/.../IOS/...")),
FWorkspaceSyncCategory(FGuid(0xF44B2D25, 0xCBC04A8F, 0xB6B3E4A8, 0x125533DD), TEXT("Platform Support: Linux"), TEXT("/Engine/Source/ThirdParty/.../Linux/...")),
FWorkspaceSyncCategory(FGuid(0x2AF45231, 0x0D75463B, 0xBF9FABB3, 0x231091BB), TEXT("Platform Support: Mac"), TEXT("/Engine/Source/ThirdParty/.../Mac/...")),
FWorkspaceSyncCategory(FGuid(0xC8CB4934, 0xADE946C9, 0xB6E361A6, 0x59E1FAF5), TEXT("Platform Support: PS4"), TEXT(".../PS4/...")),
FWorkspaceSyncCategory(FGuid(0xF8AE5AC3, 0xDA2D4719, 0xBABF8A90, 0xD878379E), TEXT("Platform Support: Switch"), TEXT(".../Switch/...")),
FWorkspaceSyncCategory(FGuid(0x3788A0BC, 0x188C4A0D, 0x950AD681, 0x75F0D110), TEXT("Platform Support: tvOS"), TEXT("/Engine/Source/ThirdParty/.../TVOS/...")),
FWorkspaceSyncCategory(FGuid(0x1144E719, 0xFCD7491B, 0xB0FC8B4C, 0x3565BF79), TEXT("Platform Support: Win32"), TEXT("/Engine/Source/ThirdParty/.../Win32/...")),
FWorkspaceSyncCategory(FGuid(0x5206CCEE, 0x90244E36, 0x8B89F5F5, 0xA7D288D2), TEXT("Platform Support: Win64"), TEXT("/Engine/Source/ThirdParty/.../Win64/...")),
FWorkspaceSyncCategory(FGuid(0x06887423, 0xB0944718, 0x9B55C7A2, 0x1EE67EE4), TEXT("Platform Support: XboxOne"), TEXT(".../XboxOne/...")),
FWorkspaceSyncCategory(FGuid(0xCFEC942A, 0xBB904F0C, 0xACCF238E, 0xCAAD9430), TEXT("Source Code"), TEXT("/Engine/Source/...")),
};
const TCHAR* FWorkspace::BuildVersionFileName = TEXT("/Engine/Build/Build.version");
const TCHAR* FWorkspace::VersionHeaderFileName = TEXT("/Engine/Source/Runtime/Launch/Resources/Version.h");
const TCHAR* FWorkspace::ObjectVersionFileName = TEXT("/Engine/Source/Runtime/Core/Private/UObject/ObjectVersion.cpp");
FWorkspace* FWorkspace::ActiveWorkspace = nullptr;
//// EWorkspaceUpdateResult ////
FString ToString(EWorkspaceUpdateResult WorkspaceUpdateResult)
{
switch(WorkspaceUpdateResult)
{
case EWorkspaceUpdateResult::Canceled:
return TEXT("Canceled");
case EWorkspaceUpdateResult::FailedToSync:
return TEXT("FailedToSync");
case EWorkspaceUpdateResult::FilesToResolve:
return TEXT("FilesToResolve");
case EWorkspaceUpdateResult::FilesToClobber:
return TEXT("FilesToClobber");
case EWorkspaceUpdateResult::FailedToCompile:
return TEXT("FailedToCompile");
case EWorkspaceUpdateResult::FailedToCompileWithCleanWorkspace:
return TEXT("FailedToCompileWithCleanWorkspace");
case EWorkspaceUpdateResult::Success:
return TEXT("Success");
default:
check(false);
return FString();
}
}
bool TryParse(const TCHAR* Text, EWorkspaceUpdateResult& OutWorkspaceUpdateResult)
{
if(FCString::Stricmp(Text, TEXT("Canceled")) == 0)
{
OutWorkspaceUpdateResult = EWorkspaceUpdateResult::Canceled;
return true;
}
if(FCString::Stricmp(Text, TEXT("FailedToSync")) == 0)
{
OutWorkspaceUpdateResult = EWorkspaceUpdateResult::FailedToSync;
return true;
}
if(FCString::Stricmp(Text, TEXT("FilesToResolve")) == 0)
{
OutWorkspaceUpdateResult = EWorkspaceUpdateResult::FilesToResolve;
return true;
}
if(FCString::Stricmp(Text, TEXT("FilesToClobber")) == 0)
{
OutWorkspaceUpdateResult = EWorkspaceUpdateResult::FilesToClobber;
return true;
}
if(FCString::Stricmp(Text, TEXT("FailedToCompile")) == 0)
{
OutWorkspaceUpdateResult = EWorkspaceUpdateResult::FailedToCompile;
return true;
}
if(FCString::Stricmp(Text, TEXT("FailedToCompileWithCleanWorkspace")) == 0)
{
OutWorkspaceUpdateResult = EWorkspaceUpdateResult::FailedToCompileWithCleanWorkspace;
return true;
}
if(FCString::Stricmp(Text, TEXT("Success")) == 0)
{
OutWorkspaceUpdateResult = EWorkspaceUpdateResult::Success;
return true;
}
return false;
}
//// FWorkspaceUpdateContext ////
FWorkspaceUpdateContext::FWorkspaceUpdateContext(int InChangeNumber, EWorkspaceUpdateOptions InOptions, const TArray<FString>& InSyncFilter, const TMap<FGuid, FCustomConfigObject>& InDefaultBuildSteps, const TArray<FCustomConfigObject>& InUserBuildSteps, const TSet<FGuid>& InCustomBuildSteps, const TMap<FString, FString>& InVariables)
: StartTime(FDateTime::UtcNow())
, ChangeNumber(InChangeNumber)
, Options(InOptions)
, SyncFilter(InSyncFilter)
, DefaultBuildSteps(InDefaultBuildSteps)
, UserBuildStepObjects(InUserBuildSteps)
, CustomBuildSteps(InCustomBuildSteps)
, Variables(InVariables)
{
}
//// FWorkspaceSyncCategory ////
FWorkspaceSyncCategory::FWorkspaceSyncCategory(const FGuid& InUniqueId)
: UniqueId(InUniqueId)
, bEnable(true)
, Name(TEXT("Unnamed"))
{
}
FWorkspaceSyncCategory::FWorkspaceSyncCategory(const FGuid& InUniqueId, const TCHAR* InName, const TCHAR* InPaths)
: UniqueId(InUniqueId)
, bEnable(true)
, Name(InName)
{
FString(InPaths).ParseIntoArray(Paths, TEXT(";"));
}
//// FWorkspace ////
FWorkspace::FWorkspace(TSharedRef<FPerforceConnection> InPerforce, const FString& InLocalRootPath, const FString& InSelectedLocalFileName, const FString& InClientRootPath, const FString& InSelectedClientFileName, const FString& InSelectedProjectIdentifier, int InInitialChangeNumber, int InLastBuiltChangeNumber, const FString& InTelemetryProjectPath, TSharedRef<FLineBasedTextWriter> InLog)
: Perforce(InPerforce)
, SyncPaths(GetSyncPaths(InClientRootPath, InSelectedClientFileName))
, LocalRootPath(InLocalRootPath)
, SelectedLocalFileName(InSelectedLocalFileName)
, ClientRootPath(InClientRootPath)
, SelectedClientFileName(InSelectedClientFileName)
, SelectedProjectIdentifier(InSelectedProjectIdentifier)
, TelemetryProjectPath(InTelemetryProjectPath)
, CurrentChangeNumber(InInitialChangeNumber)
, PendingChangeNumber(InInitialChangeNumber)
, LastBuiltChangeNumber(InLastBuiltChangeNumber)
, Log(InLog)
, bSyncing(false)
, ProjectConfigFile(ReadProjectConfigFile(InLocalRootPath, InSelectedLocalFileName, InLog.Get()))
, AbortEvent(FPlatformProcess::GetSynchEventFromPool(true))
, WorkerThread(nullptr)
{
ProjectStreamFilter = ReadProjectStreamFilter(InPerforce.Get(), ProjectConfigFile.Get(), AbortEvent, InLog.Get());
UpdateStatusPanel();
}
FWorkspace::~FWorkspace()
{
CancelUpdate();
}
void FWorkspace::UpdateStatusPanel()
{
TSharedPtr<const FCustomConfigSection> ProjectSection = ProjectConfigFile->FindSection(*SelectedProjectIdentifier);
if (ProjectSection.IsValid())
{
PanelColor = ProjectSection->GetValue(TEXT("StatusPanelColor"), TEXT(""));
AlertMessage = ProjectSection->GetValue(TEXT("Message"), TEXT(""));
}
}
FString FWorkspace::GetPanelColor() const
{
return PanelColor;
}
FString FWorkspace::GetAlertMessage() const
{
return AlertMessage;
}
TMap<FGuid, FWorkspaceSyncCategory> FWorkspace::GetSyncCategories() const
{
TMap<FGuid, FWorkspaceSyncCategory> UniqueIdToCategory;
// Add the default filters
for(const FWorkspaceSyncCategory& DefaultSyncCategory : DefaultSyncCategories)
{
UniqueIdToCategory.Add(DefaultSyncCategory.UniqueId, FWorkspaceSyncCategory(DefaultSyncCategory));
}
// Add the custom filters
TArray<FString> CategoryLines;
if(ProjectConfigFile->TryGetValues(TEXT("Options.SyncCategory"), CategoryLines))
{
for(const FString& CategoryLine : CategoryLines)
{
const FCustomConfigObject Object(*CategoryLine);
FGuid UniqueId;
if(Object.TryGetValue(TEXT("UniqueId"), UniqueId))
{
FWorkspaceSyncCategory* Category = UniqueIdToCategory.Find(UniqueId);
if(Category == nullptr)
{
Category = &(UniqueIdToCategory.Add(UniqueId, FWorkspaceSyncCategory(UniqueId)));
}
if(Object.GetValueOrDefault(TEXT("Clear"), false))
{
Category->Paths.Empty();
}
Category->Name = Object.GetValueOrDefault(TEXT("Name"), *Category->Name);
Category->bEnable = Object.GetValueOrDefault(TEXT("Enable"), Category->bEnable);
FString ObjectPaths;
if(Object.TryGetValue(TEXT("Paths"), ObjectPaths))
{
TArray<FString> NewPaths;
ObjectPaths.ParseIntoArray(NewPaths, TEXT(";"));
Category->Paths.Append(NewPaths);
Category->Paths.Sort();
for(int Idx = Category->Paths.Num() - 1; Idx > 0; Idx--)
{
if(Category->Paths[Idx - 1] == Category->Paths[Idx])
{
Category->Paths.RemoveAt(Idx);
}
}
}
}
}
}
return UniqueIdToCategory;
}
TSharedPtr<FCustomConfigFile, ESPMode::ThreadSafe> FWorkspace::GetProjectConfigFile() const
{
FScopeLock Lock(&CriticalSection);
return ProjectConfigFile;
}
void FWorkspace::GetProjectStreamFilter(TArray<FString>& Filter)
{
FScopeLock Lock(&CriticalSection);
Filter = ProjectStreamFilter;
}
bool FWorkspace::IsBusy() const
{
return bSyncing;
}
TTuple<FString, float> FWorkspace::GetCurrentProgress() const
{
FScopeLock Lock(&CriticalSection);
return Progress.GetCurrent();
}
int FWorkspace::GetCurrentChangeNumber() const
{
return CurrentChangeNumber;
}
int FWorkspace::GetPendingChangeNumber() const
{
return PendingChangeNumber;
}
int FWorkspace::GetLastBuiltChangeNumber() const
{
return LastBuiltChangeNumber;
}
FString FWorkspace::GetClientName() const
{
return Perforce->ClientName;
}
void FWorkspace::Update(const TSharedRef<FWorkspaceUpdateContext, ESPMode::ThreadSafe>& Context)
{
// Kill any existing sync
CancelUpdate();
// Set the initial progress message
if(CurrentChangeNumber != Context->ChangeNumber)
{
PendingChangeNumber = Context->ChangeNumber;
if(!EnumHasAnyFlags(Context->Options, EWorkspaceUpdateOptions::SyncSingleChange))
{
CurrentChangeNumber = -1;
}
}
Progress.Clear();
bSyncing = true;
// Spawn the new thread
AbortEvent->Reset();
WorkerThreadContext = Context;
WorkerThread = FRunnableThread::Create(this, *FString::Printf(TEXT("Update Workspace: %s"), *SelectedClientFileName));
}
void FWorkspace::CancelUpdate()
{
if(bSyncing)
{
Log->WriteLine(TEXT("OPERATION ABORTED"));
if(WorkerThread != nullptr)
{
AbortEvent->Trigger();
WorkerThread->WaitForCompletion();
WorkerThread = nullptr;
}
PendingChangeNumber = CurrentChangeNumber;
WorkerThreadContext.Reset();
bSyncing = false;
FPlatformAtomics::InterlockedCompareExchangePointer((void**)&ActiveWorkspace, nullptr, this);
}
}
uint32 FWorkspace::Run()
{
FString StatusMessage;
EWorkspaceUpdateResult Result = EWorkspaceUpdateResult::FailedToSync;
// TODO enable exceptions or dont do this
//try
{
Result = UpdateWorkspaceInternal(*WorkerThreadContext.Get(), StatusMessage);
if(Result != EWorkspaceUpdateResult::Success)
{
Log->Logf(TEXT("%s"), *StatusMessage);
}
}
//catch(FAbortException)
{
//StatusMessage = "Canceled.";
//Log->Logf(TEXT("Canceled."));
}
// catch(Exception Ex)
// {
// StatusMessage = "Failed with exception - " + Ex.ToString();
// Log.WriteException(Ex, "Failed with exception");
// }
bSyncing = false;
PendingChangeNumber = CurrentChangeNumber;
FPlatformAtomics::InterlockedCompareExchangePointer((void**)&ActiveWorkspace, nullptr, this);
if (OnUpdateComplete)
{
OnUpdateComplete(WorkerThreadContext.ToSharedRef(), Result, StatusMessage);
}
WorkerThreadContext.Reset();
return 0;
}
EWorkspaceUpdateResult FWorkspace::UpdateWorkspaceInternal(FWorkspaceUpdateContext& Context, FString& OutStatusMessage)
{
IFileManager& FileManager = IFileManager::Get();
// string CmdExe = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");
#if PLATFORM_WINDOWS
FString CmdExe = TEXT("C:\\Windows\\System32\\cmd.exe");
#else
FString CmdExe = TEXT("/usr/bin/env");
#endif
//if(!FileManager.FileExists(*CmdExe))
//{
//OutStatusMessage = FString::Printf(TEXT("Missing %s."), *CmdExe);
//return EWorkspaceUpdateResult::FailedToSync;
//}
TArray<TTuple<FString, FTimespan>> Times;
int NumFilesSynced = 0;
if(EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::Sync) || EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::SyncSingleChange))
{
// using(TelemetryStopwatch Stopwatch = new TelemetryStopwatch("Sync", TelemetryProjectPath))
{
Log->Logf(TEXT("Syncing to %d..."), PendingChangeNumber);
// Find all the files that are out of date
Progress.Set(TEXT("Finding files to sync..."));
// Get the user's sync filter
TUniquePtr<FFileFilter> UserFilter;
if(Context.SyncFilter.Num() > 0)
{
UserFilter = TUniquePtr<FFileFilter>(new FFileFilter(EFileFilterType::Include));
for(const FString& UserFilterLine : Context.SyncFilter)
{
FString Line = UserFilterLine.TrimStartAndEnd();
if(Line.Len() > 0 && Line[0] != ';' && Line[0] != '#')
{
UserFilter->AddRule(Line);
}
}
}
// Find all the server changes, and anything that's opened for edit locally. We need to sync files we have open to schedule a resolve.
TArray<FString> SyncFiles;
for(const FString& SyncPath : SyncPaths)
{
TArray<FPerforceFileRecord> SyncRecords;
if(!Perforce->SyncPreview(SyncPath, PendingChangeNumber, !EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::Sync), SyncRecords, AbortEvent, Log.Get()))
{
OutStatusMessage = FString::Printf(TEXT("Couldn't enumerate changes matching %s."), *SyncPath);
return EWorkspaceUpdateResult::FailedToSync;
}
if(UserFilter.IsValid())
{
SyncRecords.RemoveAll([&UserFilter](const FPerforceFileRecord& SyncRecord){ return SyncRecord.ClientPath.Len() > 0 && !UserFilter->Matches(SyncRecord.ClientPath); });
}
for(const FPerforceFileRecord& SyncRecord : SyncRecords)
{
SyncFiles.Add(SyncRecord.DepotPath);
}
TArray<FPerforceFileRecord> OpenRecords;
if(!Perforce->GetOpenFiles(SyncPath, OpenRecords, AbortEvent, Log.Get()))
{
OutStatusMessage = FString::Printf(TEXT("Couldn't find open files matching %s."), *SyncPath);
return EWorkspaceUpdateResult::FailedToSync;
}
// don't force a sync on added files
for(const FPerforceFileRecord& OpenRecord : OpenRecords)
{
if(OpenRecord.Action != TEXT("add") && OpenRecord.Action != TEXT("branch") && OpenRecord.Action != TEXT("move/add"))
{
SyncFiles.Add(OpenRecord.DepotPath);
}
}
}
// Filter out all the binaries that we don't want
FFileFilter Filter(EFileFilterType::Include);
Filter.AddRule(FString::Printf(TEXT("...%s"), BuildVersionFileName), EFileFilterType::Exclude);
Filter.AddRule(FString::Printf(TEXT("...%s"), VersionHeaderFileName), EFileFilterType::Exclude);
Filter.AddRule(FString::Printf(TEXT("...%s"), ObjectVersionFileName), EFileFilterType::Exclude);
if(EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::ContentOnly))
{
Filter.AddRule(TEXT("*.usf"), EFileFilterType::Exclude);
}
SyncFiles.RemoveAll([&Filter](const FString& FileName){ return !Filter.Matches(FileName); });
// Sync them all
TArray<FString> TamperedFiles;
TSet<FString> RemainingFiles(SyncFiles);
if(!Perforce->Sync(SyncFiles, PendingChangeNumber, [this, &RemainingFiles, &SyncFiles](const FPerforceFileRecord& Record){ UpdateSyncProgress(Record, RemainingFiles, SyncFiles.Num()); }, TamperedFiles, &Context.PerforceSyncOptions, AbortEvent, Log.Get()))
{
OutStatusMessage = TEXT("Aborted sync due to errors.");
return EWorkspaceUpdateResult::FailedToSync;
}
// If any files need to be clobbered, defer to the main thread to figure out which ones
if(TamperedFiles.Num() > 0)
{
int NumNewFilesToClobber = 0;
for(const FString& TamperedFile : TamperedFiles)
{
if(!Context.ClobberFiles.Contains(TamperedFile))
{
Context.ClobberFiles[TamperedFile] = true;
NumNewFilesToClobber++;
}
}
if(NumNewFilesToClobber > 0)
{
OutStatusMessage = FString::Printf(TEXT("Cancelled sync after checking files to clobber (%d new files)."), NumNewFilesToClobber);
return EWorkspaceUpdateResult::FilesToClobber;
}
for(const FString& TamperedFile : TamperedFiles)
{
if(Context.ClobberFiles[TamperedFile] && !Perforce->ForceSync(TamperedFile, PendingChangeNumber, AbortEvent, Log.Get()))
{
OutStatusMessage = FString::Printf(TEXT("Couldn't sync %s."), *TamperedFile);
return EWorkspaceUpdateResult::FailedToSync;
}
}
}
int VersionChangeNumber = -1;
if(EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::Sync))
{
// Read the new config file
ProjectConfigFile = ReadProjectConfigFile(LocalRootPath, SelectedLocalFileName, Log.Get());
ProjectStreamFilter = ReadProjectStreamFilter(Perforce.Get(), ProjectConfigFile.Get(), AbortEvent, Log.Get());
// Get the branch name
FString BranchOrStreamName;
if(!Perforce->GetActiveStream(BranchOrStreamName, AbortEvent, Log.Get()))
{
FString DepotFileName;
#if PLATFORM_WINDOWS
if(!Perforce->ConvertToDepotPath(ClientRootPath + TEXT("/GenerateProjectFiles.bat"), DepotFileName, AbortEvent, Log.Get()))
#else
if(!Perforce->ConvertToDepotPath(ClientRootPath + TEXT("/GenerateProjectFiles.sh"), DepotFileName, AbortEvent, Log.Get()))
#endif
{
OutStatusMessage = FString::Printf(TEXT("Couldn't determine branch name for %s."), *SelectedClientFileName);
return EWorkspaceUpdateResult::FailedToSync;
}
BranchOrStreamName = FPerforceUtils::GetClientOrDepotDirectoryName(*DepotFileName);
}
// Get all the paths for code changes
TArray<FString> CodeFilters;
for(const FString& SyncPath : SyncPaths)
{
const TCHAR* CodeExtensions[] = { TEXT(".cs"), TEXT(".h"), TEXT(".cpp"), TEXT(".usf") };
for(const TCHAR* CodeExtension : CodeExtensions)
{
CodeFilters.Add(FString::Printf(TEXT("%s%s@<=%d"), *SyncPath, CodeExtension, PendingChangeNumber));
}
}
// Find the last code change before this changelist. For consistency in versioning between local builds and precompiled binaries, we need to use the last submitted code changelist as our version number.
TArray<FPerforceChangeSummary> CodeChanges;
if(!Perforce->FindChanges(CodeFilters, 1, CodeChanges, AbortEvent, Log.Get()))
{
OutStatusMessage = FString::Printf(TEXT("Couldn't determine last code changelist before CL %d."), PendingChangeNumber);
return EWorkspaceUpdateResult::FailedToSync;
}
if(CodeChanges.Num() == 0)
{
OutStatusMessage = FString::Printf(TEXT("Could not find any code changes before CL %d."), PendingChangeNumber);
return EWorkspaceUpdateResult::FailedToSync;
}
// Get the last code change
if(ProjectConfigFile->GetValue(TEXT("Options.VersionToLastCodeChange"), true))
{
for(const FPerforceChangeSummary& CodeChange : CodeChanges)
{
VersionChangeNumber = FMath::Max(VersionChangeNumber, CodeChange.Number);
}
}
else
{
VersionChangeNumber = PendingChangeNumber;
}
// Update the version files
if(ProjectConfigFile->GetValue(TEXT("Options.UseFastModularVersioning"), false))
{
TMap<FString, FString> BuildVersionStrings;
BuildVersionStrings.Add(TEXT("\"Changelist\":"), FString::Printf(TEXT(" %d,"), PendingChangeNumber));
BuildVersionStrings.Add(TEXT("\"CompatibleChangelist\":"), FString::Printf(TEXT(" %d,"), VersionChangeNumber));
BuildVersionStrings.Add(TEXT("\"BranchName\":"), FString::Printf(TEXT(" \"%s\""), *BranchOrStreamName.Replace(TEXT("/"), TEXT("+"))));
BuildVersionStrings.Add(TEXT("\"IsPromotedBuild\":"), TEXT(" 0,"));
if(!UpdateVersionFile(*(ClientRootPath + BuildVersionFileName), BuildVersionStrings, PendingChangeNumber))
{
OutStatusMessage = FString::Printf(TEXT("Failed to update %s."), BuildVersionFileName);
return EWorkspaceUpdateResult::FailedToSync;
}
TMap<FString, FString> VersionHeaderStrings;
VersionHeaderStrings.Add(TEXT("#define ENGINE_IS_PROMOTED_BUILD"), TEXT(" (0)"));
VersionHeaderStrings.Add(TEXT("#define BUILT_FROM_CHANGELIST"), TEXT(" 0"));
VersionHeaderStrings.Add(TEXT("#define BRANCH_NAME"), TEXT(" \"") + BranchOrStreamName.Replace(TEXT("/"), TEXT("+")) + TEXT("\""));
if(!UpdateVersionFile(*(ClientRootPath + VersionHeaderFileName), VersionHeaderStrings, PendingChangeNumber))
{
OutStatusMessage = FString::Printf(TEXT("Failed to update %s."), VersionHeaderFileName);
return EWorkspaceUpdateResult::FailedToSync;
}
if(!UpdateVersionFile(*(ClientRootPath + ObjectVersionFileName), TMap<FString,FString>(), PendingChangeNumber))
{
OutStatusMessage = FString::Printf(TEXT("Failed to update %s."), ObjectVersionFileName);
return EWorkspaceUpdateResult::FailedToSync;
}
}
else
{
if(!UpdateVersionFile(*(ClientRootPath + BuildVersionFileName), TMap<FString, FString>(), PendingChangeNumber))
{
OutStatusMessage = FString::Printf(TEXT("Failed to update %s"), BuildVersionFileName);
return EWorkspaceUpdateResult::FailedToSync;
}
TMap<FString, FString> VersionStrings;
VersionStrings.Add(TEXT("#define ENGINE_VERSION"), FString::Printf(TEXT(" %d"), VersionChangeNumber));
VersionStrings.Add(TEXT("#define ENGINE_IS_PROMOTED_BUILD"), TEXT(" (0)"));
VersionStrings.Add(TEXT("#define BUILT_FROM_CHANGELIST"), FString::Printf(TEXT(" %d"), VersionChangeNumber));
VersionStrings.Add(TEXT("#define BRANCH_NAME"), FString::Printf(TEXT(" \"%s\""), *BranchOrStreamName.Replace(TEXT("/"), TEXT("+"))));
if(!UpdateVersionFile(*(ClientRootPath + VersionHeaderFileName), VersionStrings, PendingChangeNumber))
{
OutStatusMessage = FString::Printf(TEXT("Failed to update %s"), VersionHeaderFileName);
return EWorkspaceUpdateResult::FailedToSync;
}
if(!UpdateVersionFile(*(ClientRootPath + ObjectVersionFileName), VersionStrings, PendingChangeNumber))
{
OutStatusMessage = FString::Printf(TEXT("Failed to update %s"), ObjectVersionFileName);
return EWorkspaceUpdateResult::FailedToSync;
}
}
// Remove all the receipts for build targets in this branch
if(SelectedClientFileName.EndsWith(TEXT(".uproject")))
{
Perforce->Sync(FPerforceUtils::GetClientOrDepotDirectoryName(*SelectedClientFileName) + TEXT("/Build/Receipts/...#0"), AbortEvent, Log.Get());
}
}
// Check if there are any files which need resolving
TArray<FPerforceFileRecord> UnresolvedFiles;
if(!FindUnresolvedFiles(UnresolvedFiles))
{
OutStatusMessage = TEXT("Couldn't get list of unresolved files.");
return EWorkspaceUpdateResult::FailedToSync;
}
if(UnresolvedFiles.Num() > 0 && EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::AutoResolveChanges))
{
for(const FPerforceFileRecord& UnresolvedFile : UnresolvedFiles)
{
Perforce->AutoResolveFile(UnresolvedFile.DepotPath, AbortEvent, Log.Get());
}
if(!FindUnresolvedFiles(UnresolvedFiles))
{
OutStatusMessage = "Couldn't get list of unresolved files.";
return EWorkspaceUpdateResult::FailedToSync;
}
}
if(UnresolvedFiles.Num() > 0)
{
Log->Logf(TEXT("%d files need resolving:"), UnresolvedFiles.Num());
for(const FPerforceFileRecord& UnresolvedFile : UnresolvedFiles)
{
Log->Logf(TEXT(" %s"), *UnresolvedFile.ClientPath);
}
OutStatusMessage = TEXT("Files need resolving.");
return EWorkspaceUpdateResult::FilesToResolve;
}
// Continue processing sync-only actions
if (EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::Sync))
{
// Execute any project specific post-sync steps
TArray<FString> PostSyncSteps;
if(ProjectConfigFile->TryGetValues(TEXT("Sync.Step"), PostSyncSteps))
{
Log->Logf(TEXT(""));
Log->Logf(TEXT("Executing post-sync steps..."));
TMap<FString, FString> PostSyncVariables(Context.Variables);
PostSyncVariables.Add("Change", FString::Printf(TEXT("%d"), PendingChangeNumber));
PostSyncVariables.Add("CodeChange", FString::Printf(TEXT("%d"), VersionChangeNumber));
for(const FString& PostSyncStep : PostSyncSteps)
{
FCustomConfigObject PostSyncStepObject(*PostSyncStep);
FString ToolFileName = FUtility::ExpandVariables(PostSyncStepObject.GetValueOrDefault(TEXT("FileName"), TEXT("")), &PostSyncVariables);
if (ToolFileName.Len() > 0)
{
FString ToolArguments = FUtility::ExpandVariables(PostSyncStepObject.GetValueOrDefault(TEXT("Arguments"), TEXT("")), &PostSyncVariables);
Log->Logf(TEXT("post-sync> Running %s %s"), *ToolFileName, *ToolArguments);
FProgressTextWriter PostSyncWriter(Progress, MakeShared<FPrefixedTextWriter>(TEXT("post-sync> "), Log));
int ResultFromTool = FUtility::ExecuteProcess(*ToolFileName, *ToolArguments, NULL, AbortEvent, PostSyncWriter);
if (ResultFromTool != 0)
{
OutStatusMessage = FString::Printf(TEXT("Post-sync step terminated with exit code %d."), ResultFromTool);
return EWorkspaceUpdateResult::FailedToSync;
}
}
}
}
// Update the current change number. Everything else happens for the new change.
CurrentChangeNumber = PendingChangeNumber;
}
// Update the timing info
// Times.Add(TTuple<FString,FTimespan>(TEXT("Sync"), Stopwatch.Stop("Success")));
// Save the number of files synced
NumFilesSynced = SyncFiles.Num();
Log->Logf(TEXT(""));
}
}
// Extract an archive from the depot path
if(EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::SyncArchives))
{
FTelemetryStopwatch Stopwatch(TEXT("Archives"), TelemetryProjectPath);
// Create the directory for extracted archive manifests
FString ManifestDirectoryName;
if(SelectedLocalFileName.EndsWith(TEXT(".uproject")))
{
ManifestDirectoryName = FPaths::GetPath(SelectedLocalFileName) / TEXT("Saved/UnrealGameSync");
}
else
{
ManifestDirectoryName = FPaths::GetPath(SelectedLocalFileName) / TEXT("Engine/Saved/UnrealGameSync");
}
IFileManager::Get().MakeDirectory(*ManifestDirectoryName);
// Sync and extract (or just remove) the given archives
for(const TTuple<FString, FString>& ArchiveTypeAndDepotPath : Context.ArchiveTypeToDepotPath)
{
// Remove any existing binaries
FString ManifestFileName = ManifestDirectoryName / FString::Printf(TEXT("%s.zipmanifest"), *ArchiveTypeAndDepotPath.Key);
if(IFileManager::Get().FileExists(*ManifestFileName))
{
Log->Logf(TEXT("Removing %s binaries..."), *ArchiveTypeAndDepotPath.Key);
Progress.Set(*FString::Printf(TEXT("Removing %s binaries..."), *ArchiveTypeAndDepotPath.Key), 0.0f);
// ArchiveUtils.RemoveExtractedFiles(LocalRootPath, ManifestFileName, Progress, Log);
Log->Logf(TEXT(""));
}
// If we have a new depot path, sync it down and extract it
if(ArchiveTypeAndDepotPath.Value.Len() > 0)
{
// string TempZipFileName = Path.GetTempFileName();
// try
// {
// Log.WriteLine("Syncing {0} binaries...", ArchiveTypeAndDepotPath.Key.ToLowerInvariant());
// Progress.Set(String.Format("Syncing {0} binaries...", ArchiveTypeAndDepotPath.Key.ToLowerInvariant()), 0.0f);
// if(!Perforce.PrintToFile(ArchiveTypeAndDepotPath.Value, TempZipFileName, Log) || new FileInfo(TempZipFileName).Length == 0)
// {
// StatusMessage = String.Format("Couldn't read {0}", ArchiveTypeAndDepotPath.Value);
// return WorkspaceUpdateResult.FailedToSync;
// }
// ArchiveUtils.ExtractFiles(TempZipFileName, LocalRootPath, ManifestFileName, Progress, Log);
// Log.WriteLine();
// }
// finally
// {
// File.SetAttributes(TempZipFileName, FileAttributes.Normal);
// File.Delete(TempZipFileName);
// }
}
}
// Add the finish time
Times.Add(TTuple<FString,FTimespan>(TEXT("Archive"), Stopwatch.Stop(TEXT("Success"))));
}
// Take the lock before doing anything else. Building and generating project files can only be done on one workspace at a time.
if(FPlatformAtomics::InterlockedCompareExchangePointer((void**)&ActiveWorkspace, this, nullptr) != nullptr)
{
Log->Logf(TEXT("Waiting for other workspaces to finish..."));
while(FPlatformAtomics::InterlockedCompareExchangePointer((void**)&ActiveWorkspace, this, nullptr) != nullptr)
{
FPlatformProcess::Sleep(0.1f);
}
}
// Generate project files in the workspace
if(EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::GenerateProjectFiles))
{
FTelemetryStopwatch Stopwatch(TEXT("Prj gen"), TelemetryProjectPath);
Progress.Set(TEXT("Generating project files..."), 0.0f);
FString ProjectFileArgument;
if(SelectedLocalFileName.EndsWith(TEXT(".uproject")))
{
ProjectFileArgument = FString::Printf(TEXT("\"%s\" "), *SelectedLocalFileName);
}
#if PLATFORM_WINDOWS
FString CommandLine = FString::Printf(TEXT("/C \"\"%s\" %s-progress\""), *(LocalRootPath / TEXT("GenerateProjectFiles.bat")), *ProjectFileArgument);
#else
FString CommandLine = FString::Printf(TEXT("\"%s\" %s-progress"), *(LocalRootPath / TEXT("GenerateProjectFiles.sh")), *ProjectFileArgument);
#endif
Log->Logf(TEXT("Generating project files..."));
Log->Logf(TEXT("gpf> Running %s %s"), *CmdExe, *CommandLine);
FProgressTextWriter ProjectFilesWriter(Progress, MakeShared<FPrefixedTextWriter>(TEXT("gpf> "), Log));
int GenerateProjectFilesResult = FUtility::ExecuteProcess(*CmdExe, *CommandLine, nullptr, AbortEvent, ProjectFilesWriter);
if(GenerateProjectFilesResult != 0)
{
OutStatusMessage = FString::Printf(TEXT("Failed to generate project files (exit code %d)."), GenerateProjectFilesResult);
return EWorkspaceUpdateResult::FailedToCompile;
}
Log->Logf(TEXT(""));
Times.Add(TTuple<FString,FTimespan>(TEXT("Prj gen"), Stopwatch.Stop(TEXT("Success"))));
}
// Build everything using MegaXGE
if(EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::Build))
{
// Get all the project build steps
TArray<FCustomConfigObject> ProjectBuildSteps;
TArray<FString> ProjectBuildStepStrings;
if(ProjectConfigFile->TryGetValues(TEXT("Build.Step"), ProjectBuildStepStrings))
{
for(const FString& ProjectBuildStepString : ProjectBuildStepStrings)
{
ProjectBuildSteps.Add(FCustomConfigObject(*ProjectBuildStepString));
}
}
// Compile all the build steps together
TMap<FGuid, FCustomConfigObject> BuildStepObjects = Context.DefaultBuildSteps;
FBuildStep::MergeBuildStepObjects(BuildStepObjects, ProjectBuildSteps);
FBuildStep::MergeBuildStepObjects(BuildStepObjects, Context.UserBuildStepObjects);
// Construct build steps from them
TArray<FBuildStep> BuildSteps;
for(const TTuple<FGuid, FCustomConfigObject>& BuildStepObject : BuildStepObjects)
{
BuildSteps.Add(FBuildStep(BuildStepObject.Value));
}
BuildSteps.Sort([](const FBuildStep& A, const FBuildStep& B){ return A.OrderIndex < B.OrderIndex; });
// Remove any that we're not running
if(Context.CustomBuildSteps.Num() > 0)
{
BuildSteps.RemoveAll([&Context](const FBuildStep& Step){ return !Context.CustomBuildSteps.Contains(Step.UniqueId); });
}
else if(EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::ScheduledBuild))
{
BuildSteps.RemoveAll([](const FBuildStep& Step){ return !Step.bScheduledSync; });
}
else
{
BuildSteps.RemoveAll([](const FBuildStep& Step){ return !Step.bNormalSync; });
}
// Check if the last successful build was before a change that we need to force a clean for
bool bForceClean = false;
if(LastBuiltChangeNumber != 0)
{
TArray<FString> CleanBuildChanges;
if(ProjectConfigFile->TryGetValues(TEXT("ForceClean.Changelist"), CleanBuildChanges))
{
for(const FString& CleanBuildChange : CleanBuildChanges)
{
int ChangeNumber;
if(FUtility::TryParse(*CleanBuildChange, ChangeNumber))
{
if((LastBuiltChangeNumber >= ChangeNumber) != (CurrentChangeNumber >= ChangeNumber))
{
Log->Logf(TEXT("Forcing clean build due to changelist %d."), ChangeNumber);
Log->Logf(TEXT(""));
bForceClean = true;
break;
}
}
}
}
}
// Execute them all
const TCHAR* TelemetryEventName = (Context.UserBuildStepObjects.Num() > 0)? TEXT("CustomBuild") : EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::UseIncrementalBuilds) ? TEXT("Compile") : TEXT("FullCompile");
FTelemetryStopwatch Stopwatch(TelemetryEventName, TelemetryProjectPath);
Progress.Set(TEXT("Starting build..."), 0.0f);
// Check we've built UBT (it should have been compiled by generating project files)
#if PLATFORM_WINDOWS
FString UnrealBuildToolPath = LocalRootPath / TEXT("Engine/Build/BatchFiles/Build.bat");
#elif PLATFORM_MAC
FString UnrealBuildToolPath = LocalRootPath / TEXT("Engine/Build/BatchFiles/Mac/Build.sh");
#else
FString UnrealBuildToolPath = LocalRootPath / TEXT("Engine/Build/BatchFiles/Linux/Build.sh");
#endif
if(!IFileManager::Get().FileExists(*UnrealBuildToolPath))
{
OutStatusMessage = FString::Printf(TEXT("Couldn't find %s"), *UnrealBuildToolPath);
return EWorkspaceUpdateResult::FailedToCompile;
}
// Calculate the total estimated duration
float TotalEstimatedDuration = 0.0f;
for(const FBuildStep& Step : BuildSteps)
{
TotalEstimatedDuration += Step.EstimatedDuration;
}
// Execute all the steps
float MaxProgressFraction = 0.0f;
for(const FBuildStep& Step : BuildSteps)
{
MaxProgressFraction += (float)Step.EstimatedDuration / (float)FMath::Max(TotalEstimatedDuration, 1.0f);
Progress.Set(*Step.StatusText);
Progress.Push(MaxProgressFraction);
Log->Logf(TEXT("%s"), *Step.StatusText);
switch(Step.Type)
{
case EBuildStepType::Compile:
{
FTelemetryStopwatch StepStopwatch(FString::Printf(TEXT("Compile:%s"), *Step.Target), TelemetryProjectPath);
FString CommandLine = FString::Printf(TEXT("%s %s %s %s -NoHotReloadFromIDE"), *Step.Target, *Step.Platform, *Step.Configuration, *FUtility::ExpandVariables(*Step.Arguments, &Context.Variables));
if(!EnumHasAnyFlags(Context.Options, EWorkspaceUpdateOptions::UseIncrementalBuilds) || bForceClean)
{
Log->Logf(TEXT("ubt> Running %s %s -clean"), *UnrealBuildToolPath, *CommandLine);
FProgressTextWriter CleanWriter(Progress, MakeShared<FPrefixedTextWriter>(TEXT("ubt> "), Log));
FUtility::ExecuteProcess(*UnrealBuildToolPath, *(CommandLine + TEXT(" -clean")), nullptr, AbortEvent, CleanWriter);
}
Log->Logf(TEXT("ubt> Running %s %s -progress"), *UnrealBuildToolPath, *CommandLine);
FProgressTextWriter BuildWriter(Progress, MakeShared<FPrefixedTextWriter>(TEXT("ubt> "), Log));
int ResultFromBuild = FUtility::ExecuteProcess(*UnrealBuildToolPath, *(CommandLine + TEXT(" -progress")), nullptr, AbortEvent, BuildWriter);
if(ResultFromBuild != 0)
{
StepStopwatch.Stop(TEXT("Failed"));
OutStatusMessage = FString::Printf(TEXT("Failed to compile %s."), *Step.Target);
return (HasModifiedSourceFiles() || Context.UserBuildStepObjects.Num() > 0)? EWorkspaceUpdateResult::FailedToCompile : EWorkspaceUpdateResult::FailedToCompileWithCleanWorkspace;
}
StepStopwatch.Stop(TEXT("Success"));
}
break;
case EBuildStepType::Cook:
{
FTelemetryStopwatch StepStopwatch(FString::Printf(TEXT("Cook/Launch: %s"), *FPaths::GetBaseFilename(Step.FileName)), TelemetryProjectPath);
FString LocalRunUAT = LocalRootPath / TEXT("Engine/Build/BatchFiles/RunUAT.sh");
#if PLATFORM_WINDOWS
FString Arguments = FString::Printf(TEXT("/C \"\"%s\" -profile=\"%s\"\""), *LocalRunUAT, *(LocalRootPath / Step.FileName));
#else
FString Arguments = FString::Printf(TEXT("\"%s\" -profile=\"%s\""), *LocalRunUAT, *(LocalRootPath / Step.FileName));
#endif
Log->Logf(TEXT("uat> Running %s %s"), *LocalRunUAT, *Arguments);
FProgressTextWriter CookLogWriter(Progress, MakeShared<FPrefixedTextWriter>(TEXT("uat> "), Log));
int ResultFromUAT = FUtility::ExecuteProcess(*CmdExe, *Arguments, nullptr, AbortEvent, CookLogWriter);
if(ResultFromUAT != 0)
{
StepStopwatch.Stop(TEXT("Failed"));
OutStatusMessage = FString::Printf(TEXT("Cook failed. (%d)"), ResultFromUAT);
return EWorkspaceUpdateResult::FailedToCompile;
}
StepStopwatch.Stop(TEXT("Success"));
}
break;
case EBuildStepType::Other:
{
FTelemetryStopwatch StepStopwatch(FString::Printf(TEXT("Custom: %s"), *FPaths::GetBaseFilename(Step.FileName)), TelemetryProjectPath);
FString ToolFileName = LocalRootPath / FUtility::ExpandVariables(*Step.FileName, &Context.Variables);
FString ToolArguments = FUtility::ExpandVariables(*Step.Arguments, &Context.Variables);
Log->Logf(TEXT("tool> Running %s %s"), *ToolFileName, *ToolArguments);
if(Step.bUseLogWindow)
{
FProgressTextWriter ToolWriter(Progress, MakeShared<FPrefixedTextWriter>(TEXT("tool> "), Log));
int ResultFromTool = FUtility::ExecuteProcess(*ToolFileName, *ToolArguments, nullptr, AbortEvent, ToolWriter);
if(ResultFromTool != 0)
{
StepStopwatch.Stop(TEXT("Failed"));
OutStatusMessage = FString::Printf(TEXT("Tool terminated with exit code %d."), ResultFromTool);
return EWorkspaceUpdateResult::FailedToCompile;
}
}
else
{
// using(Process.Start(ToolFileName, ToolArguments))
// {
// }
}
StepStopwatch.Stop(TEXT("Success"));
}
break;
}
Log->Logf(TEXT(""));
Progress.Pop();
}
Times.Add(TTuple<FString,FTimespan>(TEXT("Build"), Stopwatch.Stop("Success")));
// Update the last successful build change number
if(Context.CustomBuildSteps.Num() == 0)
{
LastBuiltChangeNumber = CurrentChangeNumber;
}
}
// Calculate the total time
long TotalSeconds = 0;
for(const TTuple<FString, FTimespan>& Time : Times)
{
TotalSeconds += (long)Time.Value.GetTotalSeconds();
}
// Write out all the timing information
Log->Logf(TEXT("Total time : %s"), *FormatTime(TotalSeconds));
for(const TTuple<FString, FTimespan>& Time : Times)
{
Log->Logf(TEXT(" %-8s: %s"), *Time.Key, *FormatTime((long)Time.Value.GetTotalSeconds()));
}
if(NumFilesSynced > 0)
{
Log->Logf(TEXT("{0} files synced."), NumFilesSynced);
}
Log->Logf(TEXT(""));
Log->Logf(TEXT("UPDATE SUCCEEDED"));
OutStatusMessage = TEXT("Update succeeded");
return EWorkspaceUpdateResult::Success;
}
TArray<FString> FWorkspace::GetSyncPaths(const FString& ClientRootPath, const FString& SelectedClientFileName)
{
TArray<FString> SyncPaths;
if(SelectedClientFileName.EndsWith(TEXT(".uproject")))
{
SyncPaths.Add(ClientRootPath + TEXT("/*"));
SyncPaths.Add(ClientRootPath + TEXT("/Engine/..."));
SyncPaths.Add(FPerforceUtils::GetClientOrDepotDirectoryName(*SelectedClientFileName) + "/...");
}
else
{
SyncPaths.Add(ClientRootPath + TEXT("/..."));
}
return SyncPaths;
}
TSharedRef<FCustomConfigFile, ESPMode::ThreadSafe> FWorkspace::ReadProjectConfigFile(const FString& LocalRootPath, const FString& SelectedLocalFileName, FLineBasedTextWriter& Log)
{
// Find the valid config file paths
TArray<FString> ProjectConfigFileNames;
ProjectConfigFileNames.Add(LocalRootPath / TEXT("Engine/Programs/UnrealGameSync/UnrealGameSync.ini"));
ProjectConfigFileNames.Add(LocalRootPath / TEXT("Engine/Programs/UnrealGameSync/NotForLicensees/UnrealGameSync.ini"));
if(SelectedLocalFileName.EndsWith(TEXT(".uproject")))
{
ProjectConfigFileNames.Add(FPaths::GetPath(SelectedLocalFileName) / TEXT("Build/UnrealGameSync.ini"));
ProjectConfigFileNames.Add(FPaths::GetPath(SelectedLocalFileName) / TEXT("Build/NotForLicensees/UnrealGameSync.ini"));
}
else
{
ProjectConfigFileNames.Add(LocalRootPath / TEXT("Engine/Programs/UnrealGameSync/DefaultProject.ini"));
ProjectConfigFileNames.Add(LocalRootPath / TEXT("Engine/Programs/UnrealGameSync/NotForLicensees/DefaultProject.ini"));
}
// Read them in
TSharedRef<FCustomConfigFile, ESPMode::ThreadSafe> ProjectConfig(new FCustomConfigFile());
for(const FString& ProjectConfigFileName : ProjectConfigFileNames)
{
if(IFileManager::Get().FileExists(*ProjectConfigFileName))
{
TArray<FString> Lines;
FFileHelper::LoadFileToStringArray(Lines, *ProjectConfigFileName);
ProjectConfig->Parse(Lines);
Log.WriteLine(FString::Printf(TEXT("Read config file from %s"), *ProjectConfigFileName));
}
}
return ProjectConfig;
}
TArray<FString> FWorkspace::ReadProjectStreamFilter(FPerforceConnection& Perforce, const FCustomConfigFile& ProjectConfigFile, FEvent* AbortEvent, FLineBasedTextWriter& Log)
{
const TCHAR* StreamListDepotPath = ProjectConfigFile.GetValue(TEXT("Options.QuickSelectStreamList"), nullptr);
if(StreamListDepotPath == nullptr)
{
return TArray<FString>();
}
TArray<FString> Lines;
if(!Perforce.Print(StreamListDepotPath, Lines, AbortEvent, Log))
{
return TArray<FString>();
}
TArray<FString> FilteredLines;
for(const FString& Line: Lines)
{
FString TrimLine = Line.TrimStartAndEnd();
if(TrimLine.Len() > 0)
{
FilteredLines.Add(TrimLine);
}
}
return FilteredLines;
}
FString FWorkspace::FormatTime(long Seconds)
{
if(Seconds >= 60)
{
return FString::Printf(TEXT("%3dm %02ds"), Seconds / 60, Seconds % 60);
}
else
{
return FString::Printf(TEXT(" %02ds"), Seconds);
}
}
bool FWorkspace::HasModifiedSourceFiles() const
{
TArray<FPerforceFileRecord> OpenFiles;
if(!Perforce->GetOpenFiles(ClientRootPath + TEXT("/..."), OpenFiles, AbortEvent, Log.Get()))
{
return true;
}
if(Algo::AnyOf(OpenFiles, [](const FPerforceFileRecord& Record){ return Record.DepotPath.Contains(TEXT("/Source/")); }))
{
return true;
}
return false;
}
bool FWorkspace::FindUnresolvedFiles(TArray<FPerforceFileRecord>& OutUnresolvedFiles) const
{
for(const FString& SyncPath : SyncPaths)
{
TArray<FPerforceFileRecord> Records;
if(!Perforce->GetUnresolvedFiles(SyncPath, Records, AbortEvent, Log.Get()))
{
Log->Logf(TEXT("Couldn't find open files matching %s"), *SyncPath);
return false;
}
OutUnresolvedFiles.Append(Records);
}
return true;
}
void FWorkspace::UpdateSyncProgress(const FPerforceFileRecord& Record, TSet<FString>& RemainingFiles, int NumFiles)
{
RemainingFiles.Remove(Record.DepotPath);
FString Message = FString::Printf(TEXT("Syncing files... (%d/%d)"), NumFiles - RemainingFiles.Num(), NumFiles);
float Fraction = FMath::Min((float)(NumFiles - RemainingFiles.Num()) / (float)NumFiles, 1.0f);
Progress.Set(*Message, Fraction);
Log->Logf(TEXT("p4> %s %s"), *Record.Action, *Record.ClientPath);
}
bool FWorkspace::UpdateVersionFile(const TCHAR* LocalPath, const TMap<FString, FString>& VersionStrings, int ChangeNumber) const
{
TSharedPtr<FPerforceWhereRecord> WhereRecord;
if(!Perforce->Where(LocalPath, WhereRecord, AbortEvent, Log.Get()))
{
Log->Logf(TEXT("P4 where failed for %s"), *LocalPath);
return false;
}
TArray<FString> Lines;
if(!Perforce->Print(FString::Printf(TEXT("%s@%d"), *WhereRecord->DepotPath, ChangeNumber), Lines, AbortEvent, Log.Get()))
{
Log->Logf(TEXT("Couldn't get default contents of %s"), *WhereRecord->DepotPath);
return false;
}
FString NewFileContents;
for(const FString& Line : Lines)
{
FString NewLine = Line;
for(const TTuple<FString, FString>& VersionString : VersionStrings)
{
if(UpdateVersionLine(NewLine, VersionString.Key, VersionString.Value))
{
break;
}
}
NewFileContents += NewLine + LINE_TERMINATOR;
}
return WriteVersionFile(*WhereRecord, NewFileContents);
}
bool FWorkspace::WriteVersionFile(const FPerforceWhereRecord& WhereRecord, const FString& NewText) const
{
IFileManager& FileManager = IFileManager::Get();
if(FileManager.FileExists(*WhereRecord.LocalPath))
{
FString OldText;
if(FFileHelper::LoadFileToString(OldText, *WhereRecord.LocalPath) && OldText == NewText)
{
Log->Logf(TEXT("Ignored %s; contents haven't changed"), *WhereRecord.LocalPath);
return true;
}
}
FileManager.Delete(*WhereRecord.LocalPath, false, true);
if(WhereRecord.DepotPath.Len() > 0)
{
Perforce->Sync(FString::Printf(TEXT("%s#0"), *WhereRecord.DepotPath), AbortEvent, Log.Get());
}
FFileHelper::SaveStringToFile(NewText, *WhereRecord.LocalPath);
Log->Logf(TEXT("Written %s"), *WhereRecord.LocalPath);
return true;
}
bool FWorkspace::UpdateVersionLine(FString& Line, const FString& Prefix, const FString& Suffix)
{
int LineIdx = 0;
int PrefixIdx = 0;
for(;;)
{
FString PrefixToken;
if(!ReadToken(Prefix, PrefixIdx, PrefixToken))
{
break;
}
FString LineToken;
if(!ReadToken(Line, LineIdx, LineToken) || LineToken != PrefixToken)
{
return false;
}
}
Line = Line.Mid(0, LineIdx) + Suffix;
return true;
}
bool FWorkspace::ReadToken(const FString& Line, int32& LineIdx, FString &OutToken)
{
for(;; LineIdx++)
{
if(LineIdx == Line.Len())
{
return false;
}
else if(!FChar::IsWhitespace(Line[LineIdx]))
{
break;
}
}
int StartIdx = LineIdx++;
if(FChar::IsAlnum(Line[StartIdx]) || Line[StartIdx] == '_')
{
while(LineIdx < Line.Len() && (FChar::IsAlnum(Line[LineIdx]) || Line[LineIdx] == '_'))
{
LineIdx++;
}
}
OutToken = Line.Mid(StartIdx, LineIdx - StartIdx);
return true;
}
} // namespace UGSCore