// Copyright Epic Games, Inc. All Rights Reserved. #include "Commands/VirtualizeCommand.h" #include "HAL/FileManager.h" #include "Misc/FileHelper.h" #include "Misc/ScopeExit.h" #include "ProjectFiles.h" #include "UnrealVirtualizationTool.h" #include "Virtualization/VirtualizationSystem.h" #include "ISourceControlModule.h" #include "ISourceControlProvider.h" #include "SourceControlOperations.h" namespace UE::Virtualization { /** Utility */ TArray BuildFinalTagDescriptions(const TArray>& OutputArray) { TArray CleanedDescriptions; for (const TUniquePtr& Output : OutputArray) { if (Output) { const FVirtualizeCommandOutput* CmdOutput = (const FVirtualizeCommandOutput*)Output.Get(); for (const FString& Tag : CmdOutput->DescriptionTags) { CleanedDescriptions.AddUnique(Tag); } } } return CleanedDescriptions; } FVirtualizeCommandOutput::FVirtualizeCommandOutput(FStringView InProjectName, const TArray& InDescriptionTags) : FCommandOutput(InProjectName) { DescriptionTags.Reserve(InDescriptionTags.Num()); for (const FText& Description : InDescriptionTags) { DescriptionTags.Add(Description.ToString()); } } FVirtualizeCommand::FVirtualizeCommand(FStringView CommandName) : FCommand(CommandName) { } void FVirtualizeCommand::PrintCmdLineHelp() { UE_LOG(LogVirtualizationTool, Display, TEXT(" -Mode=Virtualize -Changelist= -Submit [optional]")); UE_LOG(LogVirtualizationTool, Display, TEXT(" -Mode=Virtualize -Path=")); UE_LOG(LogVirtualizationTool, Display, TEXT("")); } bool FVirtualizeCommand::Initialize(const TCHAR* CmdLine) { TRACE_CPUPROFILER_EVENT_SCOPE(FVirtualizeCommand::Initialize); TArray Tokens; TArray Switches; ParseCommandLine(CmdLine, Tokens, Switches); FString SwitchValue; for (const FString& Switch : Switches) { EPathResult Result = ParseSwitchForPaths(Switch, AllPackages); if (Result == EPathResult::Error) { return false; } else if (Result == EPathResult::Success) { continue; // If we already matched the switch we don't need to check against any others } if (FParse::Value(*Switch, TEXT("Changelist="), SwitchValue)) { SourceChangelistNumber = SwitchValue; } else if (Switch == TEXT("Submit")) { bShouldSubmitChangelist = true; } else if (Switch == TEXT("Checkout")) { bShouldCheckout = true; } } // Process the provided changelist if one was found if (!SourceChangelistNumber.IsEmpty() ) { // If no client spec was provided we need to find it for the changelist // In theory this duplicates a lot of the work found in ::TryParseChangelist // but at the moment the FGetChangelistDetails operation is not compatible // with the FSourceControlChangelistStateRef/FSourceControlStateRef API // so we are stuck with duplication of work. if (ClientSpecName.IsEmpty()) { ClientSpecName = FindClientSpecForChangelist(SourceChangelistNumber); if (!ClientSpecName.IsEmpty()) { FSourceControlResultInfo Info; if (SCCProvider->SwitchWorkspace(ClientSpecName, Info, nullptr) != ECommandResult::Succeeded) { UE_LOG(LogVirtualization, Error, TEXT("Failed to switch to workspace '%s'"), *ClientSpecName); return false; } } else { UE_LOG(LogVirtualization, Error, TEXT("Count not find a valid workspace for the changelist '%s'"), *SourceChangelistNumber); return false; } } if (!TryParseChangelist(ClientSpecName, SourceChangelistNumber, AllPackages, &SourceChangelist)) { UE_LOG(LogVirtualization, Error, TEXT("Failed to find the files in the changelist '%s'"), *SourceChangelistNumber); return false; } } return true; } void FVirtualizeCommand::Serialize(FJsonSerializerBase& Serializer) { TRACE_CPUPROFILER_EVENT_SCOPE(FVirtualizeCommand::Serialize); Serializer.Serialize(TEXT("ShouldCheckout"), bShouldCheckout); } bool FVirtualizeCommand::ProcessProject(const FProject& Project, TUniquePtr& Output) { TRACE_CPUPROFILER_EVENT_SCOPE(FVirtualizeCommand::ProcessProject); UE_LOG(LogVirtualizationTool, Display, TEXT("Running the virtualization process...")); TStringBuilder<128> ProjectName; ProjectName << Project.GetProjectName(); UE_LOG(LogVirtualizationTool, Display, TEXT("\tRunning the virtualization process for the project '%s'..."), ProjectName.ToString()); FConfigFile EngineConfigWithProject; if (!Project.TryLoadConfig(EngineConfigWithProject)) { return false; } Project.RegisterMountPoints(); ON_SCOPE_EXIT { Project.UnRegisterMountPoints(); }; UE::Virtualization::FInitParams InitParams(ProjectName, EngineConfigWithProject); UE::Virtualization::Initialize(InitParams, UE::Virtualization::EInitializationFlags::ForceInitialize); ON_SCOPE_EXIT { UE::Virtualization::Shutdown(); }; if (!UE::Virtualization::IVirtualizationSystem::Get().IsEnabled()) { UE_LOG(LogVirtualizationTool, Display, TEXT("\tVirtualization is not enabled for this project")); Output = MakeUnique(Project.GetProjectName(), TArray()); return true; } TArray ProjectPackages = Project.GetAllPackages(); EVirtualizationOptions Options = EVirtualizationOptions::None; if (bShouldCheckout) { Options |= EVirtualizationOptions::Checkout; // Make sure that we have a valid source control connection if we might try to checkout packages TryConnectToSourceControl(); } FVirtualizationResult Result = UE::Virtualization::IVirtualizationSystem::Get().TryVirtualizePackages(ProjectPackages, Options); if (!Result.WasSuccessful()) { UE_LOG(LogVirtualizationTool, Error, TEXT("The virtualization process failed with the following errors:")); for (const FText& Error : Result.Errors) { UE_LOG(LogVirtualizationTool, Error, TEXT("\t%s"), *Error.ToString()); } return false; } if (bShouldCheckout) { UE_LOG(LogVirtualizationTool, Display, TEXT("\t\t%d packages were checked out of revision control"), Result.CheckedOutPackages.Num()); } UE_LOG(LogVirtualizationTool, Display, TEXT("\t\tTime taken %.2f(s)"), Result.TimeTaken); UE_LOG(LogVirtualizationTool, Display, TEXT("\t\tVirtualization of project packages complete!"), ProjectName.ToString()); Output = MakeUnique(Project.GetProjectName(), Result.DescriptionTags); return true; } bool FVirtualizeCommand::ProcessOutput(const TArray>& CmdOutputArray) { TRACE_CPUPROFILER_EVENT_SCOPE(FVirtualizeCommand::ProcessOutput); if (bShouldSubmitChangelist) { TArray FinalDescriptionTags = BuildFinalTagDescriptions(CmdOutputArray); if (!TrySubmitChangelist(SourceChangelist, FinalDescriptionTags)) { return false; } } return true; } TUniquePtr FVirtualizeCommand::CreateOutputObject() const { return MakeUnique(); } const TArray& FVirtualizeCommand::GetPackages() const { return AllPackages; } bool FVirtualizeCommand::TrySubmitChangelist(const FSourceControlChangelistPtr& ChangelistToSubmit, const TArray& InDescriptionTags) { if (!ChangelistToSubmit.IsValid()) { return true; } if (!TryConnectToSourceControl(ClientSpecName)) { UE_LOG(LogVirtualizationTool, Error, TEXT("Submit failed, cannot find a valid source control provider")); return false; } TRACE_CPUPROFILER_EVENT_SCOPE(FVirtualizeCommand::TrySubmitChangelist); 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; } const FString ChangelistNumber = ChangelistState->GetDisplayText().ToString(); UE_LOG(LogVirtualizationTool, Display, TEXT("Attempting to submit the changelist '%s'"), *ChangelistNumber); 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 : InDescriptionTags) { 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 FVirtualizeCommand::TryParsePackageList(const FString& PackageListPath, TArray& OutPackages) { TRACE_CPUPROFILER_EVENT_SCOPE(FVirtualizeCommand::TryParsePackageList); UE_LOG(LogVirtualizationTool, Display, TEXT("Parsing the package list '%s'..."), *PackageListPath); 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); } return true; } else { UE_LOG(LogVirtualizationTool, Error, TEXT("\tFailed to parse the package list '%s'"), *PackageListPath); return false; } } FVirtualizeLegacyChangeListCommand::FVirtualizeLegacyChangeListCommand(FStringView CommandName) : FVirtualizeCommand(CommandName) { } bool FVirtualizeLegacyChangeListCommand::Initialize(const TCHAR* CmdLine) { TRACE_CPUPROFILER_EVENT_SCOPE(FVirtualizeLegacyChangeListCommand::Initialize); UE_LOG(LogVirtualizationTool, Warning, TEXT("Using legacy -Mode=Changelist command, use '-Mode=Virtualization -Changelist=123' instead!")); if (!FParse::Value(CmdLine, TEXT("-ClientSpecName="), ClientSpecName)) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find cmdline switch 'ClientSpecName', this is a required parameter!")); return false; } if (!FParse::Value(CmdLine, TEXT("-Changelist="), SourceChangelistNumber)) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find cmdline switch 'Changelist', this is a required parameter!")); return false; } if (!TryParseChangelist(ClientSpecName, SourceChangelistNumber, AllPackages, &SourceChangelist)) { return false; } if (FParse::Param(CmdLine, TEXT("NoSubmit"))) { UE_LOG(LogVirtualizationTool, Display, TEXT("Cmdline parameter '-NoSubmit' found, the changelist will be virtualized but not submitted!")); bShouldSubmitChangelist = false; } else { // The legacy command was opt out when it came to submitting the changelist, we need to maintain those defaults bShouldSubmitChangelist = true; } return true; } FVirtualizeLegacyPackageListCommand::FVirtualizeLegacyPackageListCommand(FStringView CommandName) : FVirtualizeCommand(CommandName) { } bool FVirtualizeLegacyPackageListCommand::Initialize(const TCHAR* CmdLine) { TRACE_CPUPROFILER_EVENT_SCOPE(FVirtualizeLegacyPackageListCommand::Initialize); UE_LOG(LogVirtualizationTool, Warning, TEXT("Using legacy -Mode=Packagelist command, use '-Mode=Virtualization -Path=PathToFile' instead!")); FString PackageListPath; if (FParse::Value(CmdLine, TEXT("-Path="), PackageListPath)) { return TryParsePackageList(PackageListPath, AllPackages); } else { return false; } } } // namespace UE::Virtualization