// Copyright Epic Games, Inc. All Rights Reserved. #include "UnrealVirtualizationToolApp.h" #include "GenericPlatform/GenericPlatformOutputDevices.h" #include "HAL/FeedbackContextAnsi.h" #include "HAL/FileManager.h" #include "Interfaces/IPluginManager.h" #include "ISourceControlModule.h" #include "ISourceControlProvider.h" #include "Misc/CommandLine.h" #include "Misc/ConfigCacheIni.h" #include "Misc/FileHelper.h" #include "Misc/Parse.h" #include "Misc/Paths.h" #include "Misc/PathViews.h" #include "SourceControlInitSettings.h" #include "SourceControlOperations.h" #include "UnrealVirtualizationTool.h" #include "Virtualization/VirtualizationSystem.h" namespace UE::Virtualization { namespace { /** Utility for testing if a file path resolves to a valid package file or not */ bool IsPackageFile(const FString FilePath) { // ::IsPackageExtension requires a TCHAR so we cannot use FPathViews here const FString Extension = FPaths::GetExtension(FilePath); // Currently we don't virtualize text based assets so no call to FPackageName::IsTextPackageExtension return FPackageName::IsPackageExtension(*Extension); } /** Utility to find the two string values we need for a mount point based on the project file path */ void ConvertToMountPoint(const FString& ProjectFilePath, FString& OutRootPath, FString& OutContentPath) { FStringView BaseFilename = FPathViews::GetBaseFilename(ProjectFilePath); OutRootPath = *WriteToString<260>(TEXT("/"), BaseFilename, TEXT("/")); OutContentPath = FPaths::GetPath(ProjectFilePath) / TEXT("Content"); } /** * Utility to clean up the tags we got from the virtualization system. Convert the FText to FString and * discard any duplicate entries. */ TArray BuildFinalTagDescriptions(TArray& DescriptionTags) { TArray CleanedDescriptions; CleanedDescriptions.Reserve(DescriptionTags.Num()); for (const FText& Tag : DescriptionTags) { CleanedDescriptions.AddUnique(Tag.ToString()); } return CleanedDescriptions; } /** * Utility taken from UGameFeatureData::ReloadConfigs that allows us to apply changes to the ini files (after * loading them from game feature plugins for example) and have the changes applied to UObjects. * For our use case we need this so that optin/optout settings for UVirtualizationFilterSettings are applied. * * This is required because we perform filtering at payload submission time. If we change filtering to be * applied when a package is saved (i.e. when the package trailer is created) then we can remove this. * If we opt to keep the current strategy then this code should be moved to a location where it can be shared * by both this tool and the game feature plugin system rather than maintaining two copies. */ void ReloadConfigs(FConfigFile& PluginConfig) { // Reload configs so objects get the changes for (const auto& ConfigEntry : PluginConfig) { // Skip out if someone put a config section in the INI without any actual data if (ConfigEntry.Value.Num() == 0) { continue; } const FString& SectionName = ConfigEntry.Key; // @todo: This entire overarching process is very similar in its goals as that of UOnlineHotfixManager::HotfixIniFile. // Could consider a combined refactor of the hotfix manager, the base config cache system, etc. to expose an easier way to support this pattern // INI files might be handling per-object config items, so need to handle them specifically const int32 PerObjConfigDelimIdx = SectionName.Find(" "); if (PerObjConfigDelimIdx != INDEX_NONE) { const FString ObjectName = SectionName.Left(PerObjConfigDelimIdx); const FString ClassName = SectionName.Mid(PerObjConfigDelimIdx + 1); // Try to find the class specified by the per-object config UClass* ObjClass = FindObject(ANY_PACKAGE, *ClassName); if (ObjClass) { // Now try to actually find the object it's referencing specifically and update it // @note: Choosing not to warn on not finding it for now, as Fortnite has transient uses instantiated at run-time (might not be constructed yet) UObject* PerObjConfigObj = StaticFindObject(ObjClass, ANY_PACKAGE, *ObjectName, true); if (PerObjConfigObj) { // Intentionally using LoadConfig instead of ReloadConfig, since we do not want to call modify/preeditchange/posteditchange on the objects changed when GIsEditor PerObjConfigObj->LoadConfig(nullptr, nullptr, UE::LCPF_ReloadingConfigData | UE::LCPF_ReadParentSections, nullptr); } } else { PLATFORM_BREAK(); // UE_LOG(LogGameFeatures, Warning, TEXT("[GameFeatureData %s]: Couldn't find PerObjectConfig class %s for %s while processing %s, config changes won't be reloaded."), *GetPathNameSafe(this), *ClassName, *ObjectName, *PluginConfig.Name.ToString()); } } // Standard INI section case else { // Find the affected class and push updates to all instances of it, including children // @note: Intentionally not using the propagation flags inherent in ReloadConfig to handle this, as it utilizes a naive complete object iterator // and tanks performance pretty badly UClass* ObjClass = FindObjectSafe(ANY_PACKAGE, *SectionName, true); if (ObjClass) { TArray FoundObjects; GetObjectsOfClass(ObjClass, FoundObjects, true, RF_NoFlags); for (UObject* CurFoundObj : FoundObjects) { if (IsValid(CurFoundObj)) { // Intentionally using LoadConfig instead of ReloadConfig, since we do not want to call modify/preeditchange/posteditchange on the objects changed when GIsEditor CurFoundObj->LoadConfig(nullptr, nullptr, UE::LCPF_ReloadingConfigData | UE::LCPF_ReadParentSections, nullptr); } } } } } } /** Utility to get EMode from a string */ void LexFromString(EMode& OutValue, const FStringView& InString) { if (InString == TEXT("Changelist")) { OutValue = EMode::Changelist; } else if (InString == TEXT("PackageList")) { OutValue = EMode::PackageList; } else { OutValue = EMode::Unknown; } } /** * This class can be used to prevent log messages from other systems being logged with the Display verbosity. * In practical terms this means as long as the class is alive, only LogVirtualizationTool messages will * be logged to the display meaning the user will have less information to deal with. */ class FOverrideOutputDevice final : public FFeedbackContextAnsi { public: FOverrideOutputDevice() { OriginalLog = GWarn; GWarn = this; } virtual ~FOverrideOutputDevice() { GWarn = OriginalLog; } virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const class FName& Category) override { if (Verbosity == ELogVerbosity::Display && Category != LogVirtualizationTool.GetCategoryName()) { Verbosity = ELogVerbosity::Log; } FFeedbackContextAnsi::Serialize(V, Verbosity, Category); } private: FFeedbackContext* OriginalLog; }; } // namespace FUnrealVirtualizationToolApp::FUnrealVirtualizationToolApp() { } FUnrealVirtualizationToolApp::~FUnrealVirtualizationToolApp() { } EInitResult FUnrealVirtualizationToolApp::Initialize() { TRACE_CPUPROFILER_EVENT_SCOPE(Initialize); UE_LOG(LogVirtualizationTool, Display, TEXT("Initializing...")); // Display the log path to the user so that they can more easily find it // Note that ::GetAbsoluteLogFilename does not always return an absolute filename FString LogFilePath = FGenericPlatformOutputDevices::GetAbsoluteLogFilename(); LogFilePath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*LogFilePath); UE_LOG(LogVirtualizationTool, Display, TEXT("Logging process to '%s'"), *LogFilePath); EInitResult CmdLineResult = TryParseCmdLine(); if (CmdLineResult != EInitResult::Success) { return CmdLineResult; } if (!TryLoadModules()) { return EInitResult::Error; } if (!TryInitEnginePlugins()) { return EInitResult::Error; } TArray Packages; switch (Mode) { case EMode::Changelist: if (!TryParseChangelist(Packages)) { return EInitResult::Error; } break; case EMode::PackageList: if (!TryParsePackageList(Packages)) { return EInitResult::Error; } break; default: UE_LOG(LogVirtualizationTool, Display, TEXT("Unknown mode, cannot find packages!")); return EInitResult::Error; break; } if (!TrySortFilesByProject(Packages)) { return EInitResult::Error; } UE_LOG(LogVirtualizationTool, Display, TEXT("Initialization complete!")); return EInitResult::Success; } bool FUnrealVirtualizationToolApp::Run() { TRACE_CPUPROFILER_EVENT_SCOPE(Run); TArray FinalDescriptionTags; if (EnumHasAllFlags(ProcessOptions, EProcessOptions::Virtualize)) { UE_LOG(LogVirtualizationTool, Display, TEXT("Running the virtualization process...")); TArray DescriptionTags; for (const FProject& Project : Projects) { TStringBuilder<128> ProjectName; ProjectName << Project.GetProjectName(); UE_LOG(LogVirtualizationTool, Display, TEXT("\tChecking package(s) for the project '%s'..."), ProjectName.ToString()); FConfigFile EngineConfigWithProject; if (!Project.TryLoadConfig(EngineConfigWithProject)) { return false; } Project.RegisterMountPoints(); UE::Virtualization::FInitParams InitParams(ProjectName, EngineConfigWithProject); UE::Virtualization::Initialize(InitParams); TArray Packages = Project.GetAllPackages(); TArray Errors; UE::Virtualization::IVirtualizationSystem::Get().TryVirtualizePackages(Packages, DescriptionTags, Errors); if (!Errors.IsEmpty()) { UE_LOG(LogVirtualizationTool, Error, TEXT("The virtualization process failed with the following errors:")); for (const FText& Error : Errors) { UE_LOG(LogVirtualizationTool, Error, TEXT("\t%s"), *Error.ToString()); } return false; } UE_LOG(LogVirtualizationTool, Display, TEXT("\tCheck complete")); UE::Virtualization::Shutdown(); Project.UnRegisterMountPoints(); } FinalDescriptionTags = BuildFinalTagDescriptions(DescriptionTags); } else { UE_LOG(LogVirtualizationTool, Display, TEXT("Skipping the virtualization process")); } if (Mode == EMode::Changelist) { if (!TrySubmitChangelist(FinalDescriptionTags)) { return false; } } return true; } void FUnrealVirtualizationToolApp::PrintCmdLineHelp() const { UE_LOG(LogVirtualizationTool, Display, TEXT("Usage:")); UE_LOG(LogVirtualizationTool, Display, TEXT("UnrealVirtualizationTool -ClientSpecName= -Mode=Changelist -Changelist= [-nosubmit] [global options]")); UE_LOG(LogVirtualizationTool, Display, TEXT("\t[optional]-nosubmit (the changelist will be virtualized but not submitted)")); UE_LOG(LogVirtualizationTool, Display, TEXT("UnrealVirtualizationTool -ClientSpecName= -Mode=PackageList -Path= [global options]")); UE_LOG(LogVirtualizationTool, Display, TEXT("Global Options:")); UE_LOG(LogVirtualizationTool, Display, TEXT("\t-verbose (all log messages with display verbosity will be displayed, not just LogVirtualizationTool)")); } bool FUnrealVirtualizationToolApp::TrySubmitChangelist(const TArray& DescriptionTags) { if (!EnumHasAllFlags(ProcessOptions, EProcessOptions::Submit)) { UE_LOG(LogVirtualizationTool, Display, TEXT("Skipping submit of changelist '%s' due to cmdline options"), *ChangelistNumber); return true; } UE_LOG(LogVirtualizationTool, Display, TEXT("Attempting to submit the changelist '%s'"), *ChangelistNumber); if (!SCCProvider.IsValid()) { if (!TryConnectToSourceControl()) { UE_LOG(LogVirtualizationTool, Error, TEXT("Submit failed, cannot find a valid source control provider")); return false; } } if (!ChangelistToSubmit.IsValid()) { // This should not be possible, the check and error message is to guard against potential future problems only. UE_LOG(LogVirtualizationTool, Error, TEXT("Submit failed, could not find the changelist")); return false; } FSourceControlChangelistRef Changelist = ChangelistToSubmit.ToSharedRef(); FSourceControlChangelistStatePtr ChangelistState = SCCProvider->GetState(Changelist, EStateCacheUsage::Use); if (!ChangelistState.IsValid()) { UE_LOG(LogVirtualizationTool, Error, TEXT("Submit failed, failed to find the state for the changelist")); return false; } TSharedRef CheckInOperation = ISourceControlOperation::Create(); // Grab the original changelist description then append our tags afterwards TStringBuilder<512> Description; Description << ChangelistState->GetDescriptionText().ToString(); for (const FString& Tag : DescriptionTags) { Description << TEXT("\n") << Tag; } CheckInOperation->SetDescription(FText::FromString(Description.ToString())); if (SCCProvider->Execute(CheckInOperation, Changelist) == ECommandResult::Succeeded) { UE_LOG(LogVirtualizationTool, Display, TEXT("%s"), *CheckInOperation->GetSuccessMessage().ToString()); return true; } else { // Even when log suppression is active we still show errors to the users and as the source control // operation should have logged the problem as an error the user will see it. This means we don't // have to extract it from CheckInOperation . UE_LOG(LogVirtualizationTool, Error, TEXT("Submit failed, please check the log!")); return false; } } bool FUnrealVirtualizationToolApp::TryLoadModules() { if (FModuleManager::Get().LoadModule(TEXT("Virtualization"), ELoadModuleFlags::LogFailures) == nullptr) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to load the 'Virtualization' module")); } return true; } bool FUnrealVirtualizationToolApp::TryInitEnginePlugins() { TRACE_CPUPROFILER_EVENT_SCOPE(TryInitEnginePlugins); UE_LOG(LogVirtualizationTool, Log, TEXT("Loading Engine Plugins")); IPluginManager& PluginMgr = IPluginManager::Get(); const FString PerforcePluginPath = FPaths::EnginePluginsDir() / TEXT("Developer/PerforceSourceControl/PerforceSourceControl.uplugin"); FText ErrorMsg; if (!PluginMgr.AddToPluginsList(PerforcePluginPath, &ErrorMsg)) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find 'PerforceSourceControl' plugin due to: %s"), *ErrorMsg.ToString()); return false; } PluginMgr.MountNewlyCreatedPlugin(TEXT("PerforceSourceControl")); TSharedPtr Plugin = PluginMgr.FindPlugin(TEXT("PerforceSourceControl")); if (Plugin == nullptr || !Plugin->IsEnabled()) { UE_LOG(LogVirtualizationTool, Error, TEXT("The 'PerforceSourceControl' plugin is disabled.")); return false; } return true; } bool FUnrealVirtualizationToolApp::TryConnectToSourceControl() { TRACE_CPUPROFILER_EVENT_SCOPE(TryConnectToSourceControl); UE_LOG(LogVirtualizationTool, Log, TEXT("Trying to connect to source control...")); FSourceControlInitSettings SCCSettings(FSourceControlInitSettings::EBehavior::OverrideAll); SCCSettings.AddSetting(TEXT("P4Client"), ClientSpecName); SCCProvider = ISourceControlModule::Get().CreateProvider(FName("Perforce"), TEXT("UnrealVirtualizationTool"), SCCSettings); if (SCCProvider.IsValid()) { return true; } else { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to create a perforce connection")); return false; } } EInitResult FUnrealVirtualizationToolApp::TryParseCmdLine() { TRACE_CPUPROFILER_EVENT_SCOPE(TryParseCmdLine); UE_LOG(LogVirtualizationTool, Log, TEXT("Parsing the commandline")); const TCHAR* CmdLine = FCommandLine::Get(); if (CmdLine == nullptr || CmdLine[0] == TEXT('\0')) { UE_LOG(LogVirtualizationTool, Error, TEXT("No commandline parameters found!")); PrintCmdLineHelp(); return EInitResult::Error; } if (FParse::Param(CmdLine, TEXT("Help")) || FParse::Param(CmdLine, TEXT("?"))) { UE_LOG(LogVirtualizationTool, Display, TEXT("Commandline help requested")); PrintCmdLineHelp(); return EInitResult::EarlyOut; } // First parse the command line options that can apply to all modes if (!FParse::Value(CmdLine, TEXT("-ClientSpecName="), ClientSpecName)) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find cmdline switch 'ClientSpecName', this is a required parameter!")); PrintCmdLineHelp(); return EInitResult::Error; } if (FParse::Param(CmdLine, TEXT("Verbose"))) { UE_LOG(LogVirtualizationTool, Display, TEXT("Cmdline parameter '-Verbose' found, no longer supressing Display log messages!")); } else { OutputDeviceOverride = MakeUnique(); } // Now parse the mode specific command line options FString ModeAsString; if (!FParse::Value(CmdLine, TEXT("-Mode="), ModeAsString)) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find cmdline switch 'Mode', this is a required parameter!")); PrintCmdLineHelp(); return EInitResult::Error; } LexFromString(Mode, ModeAsString); switch (Mode) { case EMode::Changelist: return TryParseChangelistCmdLine(CmdLine); break; case EMode::PackageList: return TryParsePackageListCmdLine(CmdLine); break; case EMode::Unknown: default: UE_LOG(LogVirtualizationTool, Error, TEXT("Unexpected value for the cmdline switch 'Mode', this is a required parameter!")); PrintCmdLineHelp(); return EInitResult::Error; break; } } EInitResult FUnrealVirtualizationToolApp::TryParseChangelistCmdLine(const TCHAR* CmdLine) { if (FParse::Value(CmdLine, TEXT("-Changelist="), ChangelistNumber)) { // Optional switches if (FParse::Param(CmdLine, TEXT("NoSubmit"))) { UE_LOG(LogVirtualizationTool, Display, TEXT("Cmdline parameter '-NoSubmit' found, the changelist will be virtualized but not submitted!")); } else { ProcessOptions |= EProcessOptions::Submit; } UE_LOG(LogVirtualizationTool, Display, TEXT("Attempting to virtualize changelist '%s'"), *ChangelistNumber); return EInitResult::Success; } else { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find cmdline switch 'Changelist', this is a required parameter for the 'Changelist' mode!")); PrintCmdLineHelp(); return EInitResult::Error; } } EInitResult FUnrealVirtualizationToolApp::TryParsePackageListCmdLine(const TCHAR* CmdLine) { if (FParse::Value(CmdLine, TEXT("-Path="), PackageListPath)) { UE_LOG(LogVirtualizationTool, Display, TEXT("Virtualizing packages found in package list: '%s'"), *PackageListPath); return EInitResult::Success; } else { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find cmdline switch 'Path', this is a required parameter for the 'PackageList mode!")); PrintCmdLineHelp(); return EInitResult::Error; } } bool FUnrealVirtualizationToolApp::TryParseChangelist(TArray& OutPackages) { TRACE_CPUPROFILER_EVENT_SCOPE(TryParseChangelist); if (!TryConnectToSourceControl()) { return false; } UE_LOG(LogVirtualizationTool, Display, TEXT("Attempting to parse changelist '%s' in workspace '%s'"), *ChangelistNumber, *ClientSpecName); if (!SCCProvider.IsValid()) { UE_LOG(LogVirtualizationTool, Error, TEXT("No valid source control connection found!")); return false; } SCCProvider->Init(true); if (!SCCProvider->UsesChangelists()) { UE_LOG(LogVirtualizationTool, Error, TEXT("The source control provider does not support the use of changelists")); return false; } TArray Changelists = SCCProvider->GetChangelists(EStateCacheUsage::ForceUpdate); if (Changelists.IsEmpty()) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find any changelists")); return false; } TArray ChangelistsStates; if (SCCProvider->GetState(Changelists, ChangelistsStates, EStateCacheUsage::Use) != ECommandResult::Succeeded) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find changelist data")); return false; } for (FSourceControlChangelistStateRef& ChangelistState : ChangelistsStates) { const FText DisplayText = ChangelistState->GetDisplayText(); if (ChangelistNumber == DisplayText.ToString()) { TSharedRef Operation = ISourceControlOperation::Create(); // TODO: Updating only the CL we want does not currently work and even if it did we still end up with a pointless // p4 changes command before updating the files. Given we know the changelist number via FSourceControlChangelistRef // we should be able to just request the file states be updated. // This is also a lot of code to write for a simple "give me all files in a changelist" operation, if we don't add // support directly in the API we should move this to a utility namespace in the source control module. FSourceControlChangelistRef Changelist = ChangelistState->GetChangelist(); Operation->SetChangelistsToUpdate(MakeArrayView(&Changelist, 1)); Operation->SetUpdateFilesStates(true); if (SCCProvider->Execute(Operation, EConcurrency::Synchronous) != ECommandResult::Succeeded) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find the files in changelist '%s'"), *ChangelistNumber); return false; } const TArray& FilesinChangelist = ChangelistState->GetFilesStates(); for (const FSourceControlStateRef& FileState : FilesinChangelist) { if (IsPackageFile(FileState->GetFilename())) { OutPackages.Add(FileState->GetFilename()); } else { UE_LOG(LogVirtualizationTool, Log, TEXT("Ignoring non-package file '%s'"), *FileState->GetFilename()); } } ChangelistToSubmit = Changelist; UE_LOG(LogVirtualizationTool, Display, TEXT("Found '%d' package file(s)"), OutPackages.Num()); return true; } } UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find the changelist '%s'"), *ChangelistNumber); return false; } bool FUnrealVirtualizationToolApp::TryParsePackageList(TArray& OutPackages) { TRACE_CPUPROFILER_EVENT_SCOPE(TrySortFilesByProject); UE_LOG(LogVirtualizationTool, Display, TEXT("Parsing the package list...")); if (!IFileManager::Get().FileExists(*PackageListPath)) { UE_LOG(LogVirtualizationTool, Error, TEXT("\tThe package list '%s' does not exist"), *PackageListPath); return false; } if (FFileHelper::LoadFileToStringArray(OutPackages, *PackageListPath)) { // We don't have control over how the package list was generated so make sure that the paths // are in the format that we want. for (FString& PackagePath : OutPackages) { FPaths::NormalizeFilename(PackagePath); } UE_LOG(LogVirtualizationTool, Display, TEXT("\tFound '%d' package file(s)"), OutPackages.Num()); return true; } else { UE_LOG(LogVirtualizationTool, Error, TEXT("\tFailed to parse the package list '%s'"), *PackageListPath); return false; } } bool FUnrealVirtualizationToolApp::TrySortFilesByProject(const TArray& Packages) { TRACE_CPUPROFILER_EVENT_SCOPE(TrySortFilesByProject); UE_LOG(LogVirtualizationTool, Display, TEXT("Sorting files by project...")); for (const FString& PackagePath : Packages) { FString ProjectFilePath; FString PluginFilePath; if (TryFindProject(PackagePath, ProjectFilePath, PluginFilePath)) { FProject& Project = FindOrAddProject(ProjectFilePath); if (PluginFilePath.IsEmpty()) { Project.AddFile(PackagePath); } else { Project.AddPluginFile(PackagePath, PluginFilePath); } } } UE_LOG(LogVirtualizationTool, Display, TEXT("\tThe package files are associated with '%d' projects(s)"), Projects.Num()); return true; } bool FUnrealVirtualizationToolApp::TryFindProject(const FString& PackagePath, FString& ProjectFilePath, FString& PluginFilePath) const { TRACE_CPUPROFILER_EVENT_SCOPE(TryFindProject); // TODO: This could be heavily optimized by caching known project files const int32 ContentIndex = PackagePath.Find(TEXT("/content/"), ESearchCase::IgnoreCase, ESearchDir::FromEnd); if (ContentIndex != INDEX_NONE) { FString ProjectDirectory = PackagePath.Left(ContentIndex); FString PluginDirectory; TArray ProjectFile; TArray PluginFile; IFileManager::Get().FindFiles(ProjectFile, *ProjectDirectory, TEXT(".uproject")); if (ProjectFile.IsEmpty()) { // Could be a plugin so lets check PluginDirectory = ProjectDirectory; IFileManager::Get().FindFiles(PluginFile, *PluginDirectory, TEXT(".uplugin")); if (PluginFile.Num() == 1) { PluginFilePath = PluginDirectory / PluginFile[0]; const int32 PluginIndex = PluginDirectory.Find(TEXT("/plugins/"), ESearchCase::IgnoreCase, ESearchDir::FromEnd); if (PluginIndex != INDEX_NONE) { ProjectDirectory = PluginDirectory.Left(PluginIndex); IFileManager::Get().FindFiles(ProjectFile, *ProjectDirectory, TEXT(".uproject")); } } else if (PluginFile.Num() > 1) { UE_LOG(LogVirtualizationTool, Verbose, TEXT("Found multiple project files for '%s' at '%s'"), *PackagePath, *PluginDirectory); return false; } } if (ProjectFile.Num() == 1) { ProjectFilePath = ProjectDirectory / ProjectFile[0]; return true; } else if (ProjectFile.IsEmpty()) { UE_LOG(LogVirtualizationTool, Log, TEXT("Failed to find project file for '%s'"), *PackagePath); return false; } else { UE_LOG(LogVirtualizationTool, Verbose, TEXT("Found multiple project files for '%s' at '%s'"), *PackagePath, *ProjectDirectory); return false; } } else { UE_LOG(LogVirtualizationTool, Log, TEXT("File '%s' is not under a content directory"), *PackagePath); return false; } } FProject& FUnrealVirtualizationToolApp::FindOrAddProject(const FString& ProjectFilePath) { FProject* Project = Projects.FindByPredicate([&ProjectFilePath](const FProject& Project)->bool { return Project.ProjectFilePath == ProjectFilePath; }); if (Project != nullptr) { return *Project; } else { FProject& NewProject = Projects.AddDefaulted_GetRef(); NewProject.ProjectFilePath = ProjectFilePath; return NewProject; } } void FProject::AddFile(const FString& PackagePath) { PackagePaths.Add(PackagePath); } void FProject::AddPluginFile(const FString& PackagePath, const FString& PluginFilePath) { FPlugin* Plugin = Plugins.FindByPredicate([&PluginFilePath](const FPlugin& Plugin)->bool { return Plugin.PluginFilePath == PluginFilePath; }); if (Plugin == nullptr) { Plugin = &Plugins.AddDefaulted_GetRef(); Plugin->PluginFilePath = PluginFilePath; } check(Plugin != nullptr); Plugin->PackagePaths.Add(PackagePath); }; FStringView FProject::GetProjectName() const { return FPathViews::GetBaseFilename(ProjectFilePath); } TArray FProject::GetAllPackages() const { TArray Packages = PackagePaths; for (const FPlugin& Plugin : Plugins) { Packages.Append(Plugin.PackagePaths); } return Packages; } void FProject::RegisterMountPoints() const { TRACE_CPUPROFILER_EVENT_SCOPE(FProject::RegisterMountPoints); FString ProjectRootPath; FString ProjectContentPath; ConvertToMountPoint(ProjectFilePath, ProjectRootPath, ProjectContentPath); FPackageName::RegisterMountPoint(ProjectRootPath, ProjectContentPath); for (const FPlugin& Plugin : Plugins) { FString PluginRootPath; FString PluginContentPath; ConvertToMountPoint(Plugin.PluginFilePath, PluginRootPath, PluginContentPath); FPackageName::RegisterMountPoint(PluginRootPath, PluginContentPath); } } void FProject::UnRegisterMountPoints() const { TRACE_CPUPROFILER_EVENT_SCOPE(FProject::UnRegisterMountPoints); for (const FPlugin& Plugin : Plugins) { FString PluginRootPath; FString PluginContentPath; ConvertToMountPoint(Plugin.PluginFilePath, PluginRootPath, PluginContentPath); FPackageName::UnRegisterMountPoint(PluginRootPath, PluginContentPath); } FString ProjectRootPath; FString ProjectContentPath; ConvertToMountPoint(ProjectFilePath, ProjectRootPath, ProjectContentPath); FPackageName::UnRegisterMountPoint(ProjectRootPath, ProjectContentPath); } bool FProject::TryLoadConfig(FConfigFile& OutConfig) const { TRACE_CPUPROFILER_EVENT_SCOPE(FProject::TryLoadConfig); const FString ProjectPath = FPaths::GetPath(ProjectFilePath); const FString EngineConfigPath = FPaths::Combine(FPaths::EngineDir(), TEXT("Config/")); const FString ProjectConfigPath = FPaths::Combine(ProjectPath, TEXT("Config/")); OutConfig.Reset(); if (!FConfigCacheIni::LoadExternalIniFile(OutConfig, TEXT("Engine"), *EngineConfigPath, *ProjectConfigPath, true)) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to load config files for the project '%s"), *ProjectFilePath); return false; } // Note that the following is taken from UGameFeatureData::InitializeHierarchicalPluginIniFiles as with // ReloadConfigs if we decide to keep filtering at submission time, rather than save time then we should // probably move this code to a shared location rather than the copy/paste. for (const FPlugin& Plugin : Plugins) { const FString PluginName = FPaths::GetBaseFilename(Plugin.PluginFilePath); const FString PluginIniName = PluginName + TEXT("Engine"); const FString PluginPath = FPaths::GetPath(Plugin.PluginFilePath); const FString PluginConfigPath = FPaths::Combine(PluginPath, TEXT("Config/")); FConfigFile PluginConfig; if (FConfigCacheIni::LoadExternalIniFile(PluginConfig, *PluginIniName, *EngineConfigPath, *PluginConfigPath, false) && (PluginConfig.Num() > 0)) { const FString IniFile = GConfig->GetConfigFilename(TEXT("Engine")); if (FConfigFile* ExistingConfig = GConfig->FindConfigFile(IniFile)) { const FString PluginIniPath = FString::Printf(TEXT("%s%s.ini"), *PluginConfigPath, *PluginIniName); if (ExistingConfig->Combine(PluginIniPath)) { ReloadConfigs(PluginConfig); } } } } return true; } } // namespace UE::Virtualization