// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "AssetRegistry/AssetRegistryState.h" #include "Commandlets/Commandlet.h" #include "Containers/Array.h" #include "Containers/DirectoryTree.h" #include "Containers/Map.h" #include "Containers/StringView.h" #include "Containers/UnrealString.h" #include "Logging/LogVerbosity.h" #include "Misc/PackagePath.h" #include "Misc/StringBuilder.h" #include "Templates/UniquePtr.h" #include "UObject/ObjectPtr.h" #include "DiffCookCommandlet.generated.h" class ITargetPlatform; class UCookOnTheFlyServer; namespace UE::DiffCook::DiffDepotUtils { struct FDepotData; } /** * Compares two depots that were written out by the CookCommandlet. Similar to running the cookcommandlet * with -diffonly, but compares two depots on disk rather than comparing one depot on disk against the * current in-memory results. */ UCLASS() class UDiffCookCommandlet : public UCommandlet { GENERATED_BODY() public: virtual int32 Main(const FString& CmdLineParams) override; private: /** * List of the string arguments parsed by name from commandline that need interpretation/transform, before * we do the transforms. */ struct FCommandLineArgs { FString BasePath; FString CompPath; TArray RequestedPackages; }; /** * Holds data about a Plugin's mount point for special-case plugins * @see MountsWithLeafFolderNameNotEqualLongPackageNameRoot */ struct FMountWithLeafFolderNameNotEqualLongPackageNameRoot { FString PathStartingWithPlugins; FString LongPackageNameRoot; }; /** All the information we collect about a package within a cooked depot. */ struct FPackageData { FName PackageName; FString CookPath; uint32 HasExtensionBitfield = 0; EPackageExtension HeaderExtension = EPackageExtension::Count; bool HasExtension(EPackageExtension Extension) const; void SetHasExtension(EPackageExtension Extension, bool bValue); }; /** All the information we collect about a cooked depot. */ struct FCookedDepot { FAssetRegistryState ARState; FAssetRegistryState DevARState; TMap LongPackageNameRoots; TMap LocalPathRoots; TMap PackageDatasByPackageName; TArray> PackageDatas; FString CookPath; FString ProjectName; const ITargetPlatform* TargetPlatform = nullptr; bool bARStateValid = false; bool bDevARStateValid = false; bool bValid = false; }; /** Types of diff results after comparing the versions of a package in two cooked depots. */ enum class EPackageDiffResult { Identical, Removed, Added, Modified, Error, }; /** * Diff result and diagnostic information about the diff after comparing the versions of a package in two * cooked depots. */ struct FPackageDiffResult { FName PackageName; EPackageDiffResult Result = EPackageDiffResult::Error; EPackageExtension Extension = EPackageExtension::Count; int64 Offset = -1; int64 CombinedOffset = -1; FPackageDiffResult(FName InPackageName = NAME_None, EPackageDiffResult InResult = EPackageDiffResult::Error, EPackageExtension InExtension = EPackageExtension::Count, int64 InOffset = 1, int64 InCombinedOffset = -1); }; /** All diff results found when comparing two cooked depots. */ struct FDiffResult { TArray PackageDiffs; }; private: /** Like a destructor, but called at the end of Main. */ void Shutdown(); /** * Read information about plugins from the current project running the commandlet. In some cases this information * is necessary to interpret the data in a cooked depot. */ void InitializePlugins(); bool TryParseCommandLine(const FString& CmdLineParams); /** Load all information about each cooked depot we will compare, including package lists. */ bool TryLoadDepots(); /** * Load the non-package data from each cooked depot we will compare. This is relatively quick since we do not have * to scan the disk for package lists. */ bool TryLoadDepotSummaries(); /** Load package lists for each cooked depot we will compare. Scans the disk, can be slow. */ void LoadDepotPackageLists(); /** Find all of the /Engine, /Game, and / mount points in the depot. Scans the disk, can be slow. */ void LoadDepotContentRoots(FCookedDepot& Depot); /** * Convert commandline arguments for -package= (and any other filters). Has to be done after loading depots and * might fail, because it interprets the data using packagelist information from the depots. */ bool TryConstructFilterLists(); /** * Compares the two depots as sets of binary files, without any interpretation of the bytes within each file. * All the extensions for a package are grouped together, and it does apply a little reasoning about the * relationships between recognized extensions. * Returns a diffresult that lists just the differences. */ FDiffResult DiffDepotAsBinary(); /** Print summary information about the diffed depots, without listing any per-package results. */ void PrintSummary(FDiffResult& Diff); /** Print per-package results about the diffed depots. */ void PrintPackageDiffs(FDiffResult& Diff); /** FPackageName::TryConvertFilenameToLongPackageName, but for a cooked depot. */ static bool TryConvertCookPathToLongPackageName(FCookedDepot& Depot, FStringView CookPath, FString& OutLongPackageName); /** FPackageName::TryConvertLongPackageNameToFilename, but for a cooked depot. */ static bool TryConvertLongPackageNameToCookPath(FCookedDepot& Depot, FStringView LongPackageName, EPackageExtension Extension, FString& OutCookPath); /** * If the give PackageData was found during LoadDepotPackageLists to have the given extension, return * a pointer to a string for the cookpath to package's file for that extension. Returns &Buffer if the, * file exists, otherwise returns nullptr. */ static FString* GetFileNameForExtension(UDiffCookCommandlet::FPackageData& PackageData, FString& Buffer, EPackageExtension Extension); /** Helper for LoadDepotContentRoots. Finds all of the directories with a "Content" subdirectory. */ static TArray FindContentRootsFromDepotTree(FCookedDepot& Depot, FString& OutEngineLocalPath, FString& OutGameLocalPath); // Normalization functions, like FPathViews::NormalizeDirectoryName, but potentially specific to our // needs. [[nodiscard]] static FString GetNormalizedLocalDir(FStringView Dir); static void NormalizeLocalDir(FStringBuilderBase& Dir); [[nodiscard]] static FString GetNormalizedLocalPath(FStringView Path); static void NormalizeLocalPath(FStringBuilderBase& Path); [[nodiscard]] static FString GetNormalizedFlexPath(FStringView Path); static void NormalizeFlexPath(FStringBuilderBase& Path); static ELogVerbosity::Type ParseDiffVerbosity(const FString& Text); private: FCommandLineArgs Args; TArray TargetPlatforms; TSet FilterByPackageName; /** * Map from leaf name of the plugin's directory to the array of plugins that use that directory. * Only contains plugins that need it because plugin name does not match leaf directory name. * Some Plugins, such as Engine/Plugins/Interchange/Runtime/Interchange.uplugin, have a folder name that does not * match its plugin name. The cooked data does not have yet enough information to map those; we look them up from * the plugins registered with the editor running the commandlet. */ TMap> MountsWithLeafFolderNameNotEqualLongPackageNameRoot; FCookedDepot BaseDepot; FCookedDepot CompDepot; uint32 NumAdded = 0; uint32 NumRemoved = 0; uint32 NumModified = 0; ELogVerbosity::Type AddedVerbosity = ELogVerbosity::Display; ELogVerbosity::Type RemovedVerbosity = ELogVerbosity::Display; ELogVerbosity::Type ModifiedVerbosity = ELogVerbosity::Display; bool bShowSummary = true; bool bShowPackages = true; bool bShowAddedPackages = true; bool bShowRemovedPackages = true; bool bShowModifiedPackages = true; bool bShowHeaders = false; // Not yet implemented, change default to true once implemented bool bShowSerialize = false; friend UE::DiffCook::DiffDepotUtils::FDepotData; };