433 lines
14 KiB
C++
433 lines
14 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "ContentValidationCommandlet.h"
|
|
#include "HAL/PlatformProcess.h"
|
|
#include "HAL/PlatformFileManager.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Interfaces/IPluginManager.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/Paths.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "UObject/UObjectHash.h"
|
|
#include "UObject/Package.h"
|
|
#include "AssetData.h"
|
|
#include "AssetRegistryModule.h"
|
|
#include "ARFilter.h"
|
|
#include "SourceControlHelpers.h"
|
|
#include "ISourceControlModule.h"
|
|
#include "ISourceControlState.h"
|
|
#include "ISourceControlProvider.h"
|
|
#include "SourceControlOperations.h"
|
|
#include "SourceControlHelpers.h"
|
|
#include "ShaderCompiler.h"
|
|
#include "Engine/BlueprintCore.h"
|
|
#include "Blueprint/BlueprintSupport.h"
|
|
#include "DataValidationModule.h"
|
|
|
|
#include "Validation/EditorValidator.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogLyraContentValidation, Log, Log);
|
|
|
|
class FScopedContentValidationMessageGatherer : public FOutputDevice
|
|
{
|
|
public:
|
|
|
|
FScopedContentValidationMessageGatherer()
|
|
: bAtLeastOneError(false)
|
|
{
|
|
GLog->AddOutputDevice(this);
|
|
}
|
|
|
|
~FScopedContentValidationMessageGatherer()
|
|
{
|
|
GLog->RemoveOutputDevice(this);
|
|
}
|
|
|
|
virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const class FName& Category) override
|
|
{
|
|
if (Verbosity <= ELogVerbosity::Error)
|
|
{
|
|
bAtLeastOneError = true;
|
|
}
|
|
}
|
|
|
|
bool bAtLeastOneError;
|
|
};
|
|
|
|
UContentValidationCommandlet::UContentValidationCommandlet(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
}
|
|
|
|
int32 UContentValidationCommandlet::Main(const FString& FullCommandLine)
|
|
{
|
|
UE_LOG(LogLyraContentValidation, Display, TEXT("Running ContentValidationCommandlet commandlet..."));
|
|
|
|
TArray<FString> Tokens;
|
|
TArray<FString> Switches;
|
|
TMap<FString, FString> Params;
|
|
ParseCommandLine(*FullCommandLine, Tokens, Switches, Params);
|
|
|
|
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
|
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
|
|
AssetRegistry.SearchAllAssets(true);
|
|
|
|
int32 ReturnVal = 0;
|
|
|
|
TArray<FString> ChangedPackageNames;
|
|
TArray<FString> DeletedPackageNames;
|
|
TArray<FString> ChangedCode;
|
|
TArray<FString> ChangedOtherFiles;
|
|
FString* P4FilterString = Params.Find(TEXT("P4Filter"));
|
|
if (P4FilterString && !P4FilterString->IsEmpty())
|
|
{
|
|
FString P4CmdString = TEXT("files ") + *P4FilterString;
|
|
if (!GetAllChangedFiles(AssetRegistry, P4CmdString, ChangedPackageNames, DeletedPackageNames, ChangedCode, ChangedOtherFiles))
|
|
{
|
|
UE_LOG(LogLyraContentValidation, Display, TEXT("ContentValidation returning 1. Failed to get changed files."));
|
|
ReturnVal = 1;
|
|
}
|
|
}
|
|
|
|
FString* P4ChangelistString = Params.Find(TEXT("P4Changelist"));
|
|
if (P4ChangelistString && !P4ChangelistString->IsEmpty())
|
|
{
|
|
FString P4CmdString = TEXT("opened -c ") + *P4ChangelistString;
|
|
if (!GetAllChangedFiles(AssetRegistry, P4CmdString, ChangedPackageNames, DeletedPackageNames, ChangedCode, ChangedOtherFiles))
|
|
{
|
|
UE_LOG(LogLyraContentValidation, Display, TEXT("ContentValidation returning 1. Failed to get changed files."));
|
|
ReturnVal = 1;
|
|
}
|
|
}
|
|
|
|
bool bP4Opened = Switches.Contains(TEXT("P4Opened"));
|
|
if (bP4Opened)
|
|
{
|
|
check(GConfig);
|
|
|
|
FString Workspace;
|
|
FString* P4ClientString = Params.Find(TEXT("P4Client"));
|
|
|
|
if (P4ClientString && !P4ClientString->IsEmpty())
|
|
{
|
|
Workspace = *P4ClientString;
|
|
}
|
|
else
|
|
{
|
|
const FString& SSCIniFile = SourceControlHelpers::GetSettingsIni();
|
|
GConfig->GetString(TEXT("PerforceSourceControl.PerforceSourceControlSettings"), TEXT("Workspace"), Workspace, SSCIniFile);
|
|
}
|
|
|
|
if (!Workspace.IsEmpty())
|
|
{
|
|
FString P4CmdString = FString::Printf(TEXT("-c%s opened"), *Workspace);
|
|
if (!GetAllChangedFiles(AssetRegistry, P4CmdString, ChangedPackageNames, DeletedPackageNames, ChangedCode, ChangedOtherFiles))
|
|
{
|
|
UE_LOG(LogLyraContentValidation, Display, TEXT("ContentValidation returning 1. Failed to get changed files."));
|
|
ReturnVal = 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogLyraContentValidation, Error, TEXT("P4 workspace was not found when using P4Opened"));
|
|
UE_LOG(LogLyraContentValidation, Display, TEXT("ContentValidation returning 1. Workspace not found."));
|
|
ReturnVal = 1;
|
|
}
|
|
}
|
|
|
|
int32 MaxPackagesToLoad = 2000;
|
|
|
|
FString* InPathString = Params.Find(TEXT("InPath"));
|
|
if (InPathString && !InPathString->IsEmpty())
|
|
{
|
|
GetAllPackagesInPath(AssetRegistry, *InPathString, ChangedPackageNames);
|
|
}
|
|
|
|
FString* OfTypeString = Params.Find(TEXT("OfType"));
|
|
if (OfTypeString && !OfTypeString->IsEmpty())
|
|
{
|
|
const int32 InitialPackages = ChangedPackageNames.Num();
|
|
GetAllPackagesOfType(*OfTypeString, ChangedPackageNames);
|
|
MaxPackagesToLoad += ChangedPackageNames.Num() - InitialPackages;
|
|
}
|
|
|
|
FString* SpecificPackagesString = Params.Find(TEXT("Packages"));
|
|
if (SpecificPackagesString && !SpecificPackagesString->IsEmpty())
|
|
{
|
|
TArray<FString> PackagePaths;
|
|
SpecificPackagesString->ParseIntoArray(PackagePaths, TEXT("+"));
|
|
ChangedPackageNames.Append(PackagePaths);
|
|
}
|
|
|
|
// We will be flushing shader compile as we load materials, so don't let other shader warnings be attributed incorrectly to the package that is loading.
|
|
if (GShaderCompilingManager)
|
|
{
|
|
GShaderCompilingManager->FinishAllCompilation();
|
|
}
|
|
|
|
FString* InMaxPackagesToLoadString = Params.Find(TEXT("MaxPackagesToLoad"));
|
|
if (InMaxPackagesToLoadString)
|
|
{
|
|
MaxPackagesToLoad = FCString::Atoi(**InMaxPackagesToLoadString);
|
|
}
|
|
|
|
TArray<FString> AllWarningsAndErrors;
|
|
UEditorValidator::ValidatePackages(ChangedPackageNames, DeletedPackageNames, MaxPackagesToLoad, AllWarningsAndErrors, EDataValidationUsecase::Commandlet);
|
|
|
|
if (!UEditorValidator::ValidateProjectSettings())
|
|
{
|
|
ReturnVal = 1;
|
|
}
|
|
|
|
return ReturnVal;
|
|
}
|
|
|
|
bool UContentValidationCommandlet::GetAllChangedFiles(IAssetRegistry& AssetRegistry, const FString& P4CmdString, TArray<FString>& OutChangedPackageNames, TArray<FString>& DeletedPackageNames, TArray<FString>& OutChangedCode, TArray<FString>& OutChangedOtherFiles) const
|
|
{
|
|
TArray<FString> Results;
|
|
int32 ReturnCode = 0;
|
|
if (LaunchP4(P4CmdString, Results, ReturnCode))
|
|
{
|
|
if (ReturnCode == 0)
|
|
{
|
|
for (const FString& Result : Results)
|
|
{
|
|
FString DepotPathName;
|
|
FString ExtraInfoAfterPound;
|
|
if (Result.Split(TEXT("#"), &DepotPathName, &ExtraInfoAfterPound))
|
|
{
|
|
if (DepotPathName.EndsWith(TEXT(".uasset")) || DepotPathName.EndsWith(TEXT(".umap")))
|
|
{
|
|
FString FullPackageName;
|
|
{
|
|
// Check for /Game/ assets
|
|
FString PostContentPath;
|
|
if (DepotPathName.Split(TEXT("LyraGame/Content/"), nullptr, &PostContentPath)) //@TODO: RENAME: Potential issue when modules are renamed
|
|
{
|
|
if (!PostContentPath.IsEmpty())
|
|
{
|
|
const FString PostContentPathWithoutExtension = FPaths::GetBaseFilename(PostContentPath, false);
|
|
FString PackageNameToTest = TEXT("/Game/") + PostContentPathWithoutExtension;
|
|
if (!UEditorValidator::IsInUncookedFolder(PackageNameToTest))
|
|
{
|
|
FullPackageName = PackageNameToTest;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (FullPackageName.IsEmpty())
|
|
{
|
|
// Check for plugin assets
|
|
FString PostPluginsPath;
|
|
if (DepotPathName.Split(TEXT("LyraGame/Plugins/"), nullptr, &PostPluginsPath))
|
|
{
|
|
const int32 ContentFolderIdx = PostPluginsPath.Find(TEXT("/Content/"));
|
|
if (ContentFolderIdx != INDEX_NONE)
|
|
{
|
|
int32 PluginFolderIdx = PostPluginsPath.Find(TEXT("/"), ESearchCase::CaseSensitive, ESearchDir::FromEnd, ContentFolderIdx - 1);
|
|
if (PluginFolderIdx == INDEX_NONE)
|
|
{
|
|
// No leading /. Directly in the /Plugins/ folder
|
|
PluginFolderIdx = 0;
|
|
}
|
|
else
|
|
{
|
|
// Skip the leading /. A subfolder in the /Plugins/ folder
|
|
PluginFolderIdx++;
|
|
}
|
|
|
|
const int32 PostContentFolderIdx = ContentFolderIdx + FCString::Strlen(TEXT("/Content/"));
|
|
const FString PostContentPath = PostPluginsPath.RightChop(PostContentFolderIdx);
|
|
const FString PluginName = PostPluginsPath.Mid(PluginFolderIdx, ContentFolderIdx - PluginFolderIdx);
|
|
if (!PostContentPath.IsEmpty() && !PluginName.IsEmpty())
|
|
{
|
|
TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(PluginName);
|
|
if (Plugin.IsValid() && Plugin->IsEnabled())
|
|
{
|
|
const FString PostContentPathWithoutExtension = FPaths::GetBaseFilename(PostContentPath, false);
|
|
FullPackageName = FString::Printf(TEXT("/%s/%s"), *PluginName, *PostContentPathWithoutExtension);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!FullPackageName.IsEmpty())
|
|
{
|
|
if (ExtraInfoAfterPound.Contains(TEXT("delete")))
|
|
{
|
|
DeletedPackageNames.AddUnique(FullPackageName);
|
|
}
|
|
else
|
|
{
|
|
OutChangedPackageNames.AddUnique(FullPackageName);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FString PostLyraGamePath;
|
|
if (DepotPathName.Split(TEXT("/LyraGame/"), nullptr, &PostLyraGamePath))
|
|
{
|
|
if (DepotPathName.EndsWith(TEXT(".cpp")))
|
|
{
|
|
OutChangedCode.Add(PostLyraGamePath);
|
|
}
|
|
else if (DepotPathName.EndsWith(TEXT(".h")))
|
|
{
|
|
OutChangedCode.Add(PostLyraGamePath);
|
|
|
|
FString ChangedHeaderLocalFilename = GetLocalPathFromDepotPath(DepotPathName);
|
|
if (!ChangedHeaderLocalFilename.IsEmpty())
|
|
{
|
|
UEditorValidator::GetChangedAssetsForCode(AssetRegistry, ChangedHeaderLocalFilename, OutChangedPackageNames);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OutChangedOtherFiles.Add(PostLyraGamePath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogLyraContentValidation, Error, TEXT("p4 returned non-zero return code %d"), ReturnCode);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void UContentValidationCommandlet::GetAllPackagesInPath(IAssetRegistry& AssetRegistry, const FString& InPathString, TArray<FString>& OutPackageNames) const
|
|
{
|
|
TArray<FString> Paths;
|
|
InPathString.ParseIntoArray(Paths, TEXT("+"));
|
|
|
|
FARFilter Filter;
|
|
Filter.bRecursivePaths = true;
|
|
Filter.bIncludeOnlyOnDiskAssets = true;
|
|
|
|
for (const FString& Path : Paths)
|
|
{
|
|
Filter.PackagePaths.Add(FName(*Path));
|
|
}
|
|
|
|
TArray<FAssetData> AssetsInPaths;
|
|
if (AssetRegistry.GetAssets(Filter, AssetsInPaths))
|
|
{
|
|
for (const FAssetData& AssetData : AssetsInPaths)
|
|
{
|
|
OutPackageNames.Add(AssetData.PackageName.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
void UContentValidationCommandlet::GetAllPackagesOfType(const FString& OfTypeString, TArray<FString>& OutPackageNames) const
|
|
{
|
|
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
|
|
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
|
|
|
|
TArray<FString> Types;
|
|
OfTypeString.ParseIntoArray(Types, TEXT("+"));
|
|
|
|
FARFilter Filter;
|
|
Filter.bRecursivePaths = true;
|
|
Filter.bIncludeOnlyOnDiskAssets = true;
|
|
|
|
for (const FString& Type : Types)
|
|
{
|
|
Filter.ClassNames.Add(FName(*Type));
|
|
}
|
|
|
|
TArray<FAssetData> AssetsOfType;
|
|
if (AssetRegistry.GetAssets(Filter, AssetsOfType))
|
|
{
|
|
for (const FAssetData& AssetData : AssetsOfType)
|
|
{
|
|
OutPackageNames.Add(AssetData.PackageName.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UContentValidationCommandlet::LaunchP4(const FString& Args, TArray<FString>& Output, int32& OutReturnCode) const
|
|
{
|
|
void* PipeRead = nullptr;
|
|
void* PipeWrite = nullptr;
|
|
|
|
verify(FPlatformProcess::CreatePipe(PipeRead, PipeWrite));
|
|
|
|
bool bInvoked = false;
|
|
OutReturnCode = -1;
|
|
FString StringOutput;
|
|
FProcHandle ProcHandle = FPlatformProcess::CreateProc(TEXT("p4.exe"), *Args, false, true, true, nullptr, 0, nullptr, PipeWrite);
|
|
if (ProcHandle.IsValid())
|
|
{
|
|
while (FPlatformProcess::IsProcRunning(ProcHandle))
|
|
{
|
|
StringOutput += FPlatformProcess::ReadPipe(PipeRead);
|
|
FPlatformProcess::Sleep(0.1f);
|
|
}
|
|
|
|
StringOutput += FPlatformProcess::ReadPipe(PipeRead);
|
|
FPlatformProcess::GetProcReturnCode(ProcHandle, &OutReturnCode);
|
|
bInvoked = true;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogLyraContentValidation, Error, TEXT("Failed to launch p4."));
|
|
}
|
|
|
|
FPlatformProcess::ClosePipe(PipeRead, PipeWrite);
|
|
|
|
StringOutput.ParseIntoArrayLines(Output);
|
|
|
|
return bInvoked;
|
|
}
|
|
|
|
FString UContentValidationCommandlet::GetLocalPathFromDepotPath(const FString& DepotPathName) const
|
|
{
|
|
FString ReturnString;
|
|
|
|
const FString& SSCIniFile = SourceControlHelpers::GetSettingsIni();
|
|
FString Workspace;
|
|
GConfig->GetString(TEXT("PerforceSourceControl.PerforceSourceControlSettings"), TEXT("Workspace"), Workspace, SSCIniFile);
|
|
|
|
if (Workspace.IsEmpty())
|
|
{
|
|
FString ParameterValue;
|
|
if (FParse::Value(FCommandLine::Get(), TEXT("P4Client="), ParameterValue))
|
|
{
|
|
Workspace = ParameterValue;
|
|
}
|
|
}
|
|
|
|
if (!Workspace.IsEmpty())
|
|
{
|
|
TArray<FString> WhereResults;
|
|
int32 ReturnCode = 0;
|
|
FString P4WhereCommand = FString::Printf(TEXT("-ztag -c%s where %s"), *Workspace, *DepotPathName);
|
|
if (LaunchP4(P4WhereCommand, WhereResults, ReturnCode))
|
|
{
|
|
if (WhereResults.Num() >= 2)
|
|
{
|
|
ReturnString = WhereResults[2];
|
|
ReturnString.RemoveFromStart(TEXT("... path "));
|
|
FPaths::NormalizeFilename(ReturnString);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogLyraContentValidation, Warning, TEXT("GetAllChangedFiles failed to run p4 'where'. WhereResults[0] = '%s'. Not adding any validation for %s"), WhereResults.Num() > 0 ? *WhereResults[0] : TEXT("Invalid"), *DepotPathName);
|
|
}
|
|
}
|
|
}
|
|
|
|
return ReturnString;
|
|
}
|