Files
UnrealEngine/Engine/Source/Developer/IoStoreUtilities/Private/ZenFileSystemManifest.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

1027 lines
37 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ZenFileSystemManifest.h"
#include "Algo/Sort.h"
#include "HAL/PlatformFileManager.h"
#include "Interfaces/ITargetPlatform.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/App.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Misc/DataDrivenPlatformInfoRegistry.h"
#include "Misc/PathViews.h"
#include "Settings/ProjectPackagingSettings.h"
#include "UObject/ICookInfo.h"
DEFINE_LOG_CATEGORY_STATIC(LogZenFileSystemManifest, Display, All);
namespace UE::ZenFileSystemManifest
{
/** Reports whether files should pass a filter based on extension include/exclude and directory exclude properties. */
class FFileFilter
{
public:
/** Mark that directories with the given leafname are excluded. Returns *this for chained function calls. */
FFileFilter& ExcludeDirectoryLeafName(const TCHAR* LeafName);
/**
* Mark that filenames with the given extension (which must be provided without a leading dot) are excluded.
* Returns *this for chained function calls.
*/
FFileFilter& ExcludeExtension(const TCHAR* NoDotExtension);
/**
* Mark that filenames with the given extension (which must be provided without a leading dot) are included, if
* not excluded. Overruled by ExcludeExtension.
* If no extensions are explicitly included then all non-excluded extensions are included.
* Returns *this for chained function calls.
*/
FFileFilter& IncludeExtension(const TCHAR* NoDotExtension);
/** Report whether a directory with the given LeafName is kept by the filter. */
bool IsDirectoryLeafNameKept(FStringView LeafName) const;
/** Report whether the given extension (which must be provided without a leading dot) is kept by the filter. */
bool IsFileExtensionKept(FStringView NoDotExtension);
private:
TArray<FString> DirectoryExclusionFilter;
TArray<FString> ExtensionExclusionFilter;
TArray<FString> ExtensionInclusionFilter;
};
/**
* A visitor used by FManifestGenerator::AddFilesFromDirectory to receive the results from
* IPlatformFile::IterateDirectory.
*/
struct FAddFilesFromDirectoryVisitor
{
public:
FAddFilesFromDirectoryVisitor(FManifestGenerator& InGenerator, const FString& InClientRoot,
const FString& InLocalRoot, bool bInIncludeSubDirs, FFileFilter* InAdditionalFilter);
/**
* Callback passed to IPlatformFile::IterateDirectory. Filters and adds files
* and subdirectories. Kept files are added to Generator.Manifest. Kept subdirectories are added to
* this->DirectoryVisitQueue. Return value is whether IterateDirectory should continue.
*/
bool VisitorFunction(const TCHAR* InnerFileNameOrDirectory, bool bIsDirectory);
public:
FManifestGenerator& Generator;
TArray<FString> DirectoryVisitQueue;
const FString& ClientRoot;
const FString& LocalRoot;
FFileFilter* AdditionalFilter = nullptr;
bool bIncludeSubDirs = false;
};
/**
* Helper class for FManifestGenerator::Generate. Has functions that support collecting files from directories in the
* uncooked folders used by or the cooked folders created by the cook. Specifications of which files to collect from
* which kinds of folders are driven by the top-level code in Generate.
*/
class FManifestGenerator
{
public:
FManifestGenerator(FZenFileSystemManifest& InManifest,
const TOptional<ICookedPackageWriter::FReferencedPluginsInfo>& InReferencedPlugins);
/** FPaths::RootDir, the root of the UnrealEngine tree, parent directory of EngineDir. */
const FString& GetUnrealEngineRootDir() const;
/** FPaths::EngineDir, the root of assets and source code shared across all Unreal projects. */
const FString& GetEngineDir() const;
/**
* FPaths::ProjectDir, the root of the project's project-specific assets and source code. Contains the .uproject
* file. Not necessarily the parent directory of all of the project's assets; some assets can be present in plugin
* folders outside of the ProjectDir.
*/
const FString& GetProjectDir() const;
/**
* FPaths::ProjectName, the name of the project, which we expect to find as a direct subdirectory of the cooked
* output's root directory.
*/
const FString& GetProjectName() const;
/** <CookDirectory>/Engine */
FString GetCookedEngineDir() const;
/** <CookDirectory>/<ProjectName> */
FString GetCookedProjectDir() const;
/** <CookDirectory>/<ProjectName>/Metadata */
FString GetCookedMetadataDir() const;
/** TargetPlatform's PlatformInfo's UBTPlatformString. */
const FString& GetUBTPlatformString() const;
/** Read/Write the BaseFilter the Generator uses for adding files from directories. */
FFileFilter& GetBaseFilter();
/**
* Add the given file to the manifest, stored relative to the given PathMappingPair of ClientRoot,LocalRoot. A
* PathMappingPair represents the two possible formats for the same root directory (the format for loading the path
* on the UnrealGame client and the format for loading the path on the cooker's machine) and the relative path
* to a file under the mapping pair is the same for each root. LocalRoot should be a parent directory of
* LocalFilePath; if it is not, the stored path falls back to absolute path on the cooker's machine and will not
* be loadable on the client.
*/
void AddSingleFile(const FString& ClientRoot, const FString& LocalRoot, FStringView LocalFilePath);
/** If file exists at LocalRoot\RelPathFromRoot, AddSingleFile on it. Return whether it was added. */
bool TryAddSingleRelpath(const FString& ClientRoot, const FString& LocalRoot, const FString& RelPathFromRoot);
/**
* Add to the manifest all (optionally recursive) files under LocalAddRoot, if they pass the Generator's basefilter
* and the given Additional Filter. ClientRoot,LocalRoot are a PathMappingPair, with LocalRoot being a parent of
* LocalAddRoot, @see AddSingleFile. If LocalAddRoot is not provided it is set equal to LocalRoot.
*/
void AddFilesFromDirectory(const FString& ClientRoot, const FString& LocalRoot, bool bIncludeSubdirs,
FFileFilter* AdditionalFilter = nullptr);
void AddFilesFromDirectory(const FString& ClientRoot, const FString& LocalRoot, const FString& LocalAddRoot,
bool bIncludeSubdirs, FFileFilter* AdditionalFilter = nullptr);
/**
* If the given path is a subpath of one of the known client roots (EngineDir, ProjectDir), call
* AddFilesFromDirectory on it and return true, else return false.
*/
bool TryAddFilesFromFlexDirectory(const FString& FullOrProjectRelativePath, bool bIncludeSubDirs,
FFileFilter* AdditionalFilter = nullptr);
/**
* A collector AddFiles function. It looks in all GetExtensionDirectories() folders for a direct subdirectory
* with leafname equal to ExtensionSubDir, and calls AddFilesFromDirectory on each of those ExtensionSubDir
* directories, with bRecursive==true and with the given filter.
*/
void AddFilesFromExtensionDirectories(const TCHAR* ExtensionSubDir, FFileFilter* AdditionalFilter = nullptr);
/**
* Return all the plugins that could be used by the runtime the manifest will be used in - plugins are filtered by
* the current project and platform.
*/
TArray<TSharedRef<IPlugin>> GetApplicablePlugins();
/**
* Return the ClientRoot,LocalRoot PathMappingPair that is one of the manifest's recognized root directories
* and is the parent directory of the plugin's uncooked directory.
* These roots allow the construction of the relative path used to store the plugin's files on the ZenServer.
*/
void GetPluginClientAndLocalRoots(TSharedRef<IPlugin>& Plugin, FString& OutClientRoot, FString& OutLocalRoot);
/**
* For a given plugin, return full paths to all of the (possibly-platform-specific) ExtensionBaseDirs that are
* present in the plugin and are applicable to the current project and platform. These directories need the same
* set of relative paths added to the manifest that are added for the plugin's base dir.
*/
TArray<FString> GetApplicablePluginExtensionBaseDirs(TSharedRef<IPlugin>& Plugin);
/** Return the subfolder used for localization assets based on the project's Internationalization settings. */
FString GetInternationalizationPresetPath();
/** Return bSSLCertificatesWillStage from network settings for the cook's targetplatform, false by default. */
bool IsSSLCertificatesWillStage();
/** Return true if the manifest should gather files from the Engine. This can be false during DLC cooks. */
bool IsIncludeEngine();
/** Return true if the manifest should gather files from the Project. This can be false during DLC cooks. */
bool IsIncludeProject();
private:
void PopulatePlatformDirectoryNames();
static void GetExtensionDirs(TArray<FString>& OutExtensionDirs, const TCHAR* BaseDir, const TCHAR* SubDir,
const TArray<FString>& PlatformDirectoryNames);
private:
FZenFileSystemManifest& Manifest;
const TOptional<ICookedPackageWriter::FReferencedPluginsInfo>& ReferencedPlugins;
IPlatformFile& PlatformFile;
FString UnrealEngineRootDir;
FString EngineDir;
FString ProjectDir;
FString ProjectName;
FFileFilter BaseFilter;
TArray<FString> PlatformDirectoryNames;
TSet<FString> PlatformDirectoryNameSet;
FString UBTPlatformString;
friend FAddFilesFromDirectoryVisitor;
};
} // namespace UE::ZenFileSystemManifest
const FZenFileSystemManifestEntry FZenFileSystemManifest::InvalidEntry = FZenFileSystemManifestEntry();
FZenFileSystemManifest::FZenFileSystemManifest(const ITargetPlatform& InTargetPlatform, FString InCookDirectory)
: TargetPlatform(InTargetPlatform)
, CookDirectory(MoveTemp(InCookDirectory))
{
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
ServerRoot = PlatformFile.ConvertToAbsolutePathForExternalAppForRead(*FPaths::RootDir());
FPaths::NormalizeDirectoryName(ServerRoot);
#if WITH_EDITOR
ReferencedSetClientPath = FString::Printf(TEXT("/{project}/Metadata/%s"), UE::Cook::GetReferencedSetFilename());
#endif
}
int32 FZenFileSystemManifest::Generate(const FString& MetadataDirectoryPath,
const TOptional<ICookedPackageWriter::FReferencedPluginsInfo>& ReferencedPlugins)
{
using namespace UE::ZenFileSystemManifest;
TRACE_CPUPROFILER_EVENT_SCOPE(GenerateStorageServerFileSystemManifest);
const UProjectPackagingSettings* const PackagingSettings = GetDefault<UProjectPackagingSettings>();
const int32 PreviousEntryCount = NumEntries();
FManifestGenerator Generator(*this, ReferencedPlugins);
Generator.GetBaseFilter()
.ExcludeDirectoryLeafName(TEXT("Binaries"))
.ExcludeDirectoryLeafName(TEXT("Intermediate"))
.ExcludeDirectoryLeafName(TEXT("Saved"))
.ExcludeDirectoryLeafName(TEXT("Source"));
FFileFilter CookedFilter = FFileFilter()
.ExcludeDirectoryLeafName(TEXT("Metadata"))
.ExcludeExtension(TEXT("uasset"))
.ExcludeExtension(TEXT("ubulk"))
.ExcludeExtension(TEXT("uexp"))
.ExcludeExtension(TEXT("umap"))
.ExcludeExtension(TEXT("uregs"));
Generator.AddFilesFromDirectory(TEXT("/{engine}"), Generator.GetCookedEngineDir(), true, &CookedFilter);
Generator.AddFilesFromDirectory(TEXT("/{project}"), Generator.GetCookedProjectDir(), true, &CookedFilter);
FFileFilter CookedMetadataFilter = FFileFilter()
.ExcludeDirectoryLeafName(TEXT("ShaderLibrarySource"))
.ExcludeExtension(TEXT("manifest"));
Generator.AddFilesFromDirectory(TEXT("/{project}"), Generator.GetCookedProjectDir(), MetadataDirectoryPath,
true, &CookedMetadataFilter);
if (Generator.IsIncludeProject())
{
FFileFilter ProjectSourceFilter = FFileFilter()
.IncludeExtension(TEXT("uproject"));
Generator.AddFilesFromDirectory(TEXT("/{project}"), Generator.GetProjectDir(), false, &ProjectSourceFilter);
}
FFileFilter ConfigFilter = FFileFilter()
.IncludeExtension(TEXT("ini"));
Generator.AddFilesFromExtensionDirectories(TEXT("Config"), &ConfigFilter);
FFileFilter LocalizationFilter = FFileFilter()
.IncludeExtension(TEXT("locmeta"))
.IncludeExtension(TEXT("locres"));
FFileFilter PluginFilter = FFileFilter()
.IncludeExtension(TEXT("uplugin"));
for (TSharedRef<IPlugin>& Plugin : Generator.GetApplicablePlugins())
{
const FString& PluginName = Plugin->GetName();
FString BaseDir = Plugin->GetBaseDir();
const FString& UPluginFile = Plugin->GetDescriptorFileName();
FString ContentDir = Plugin->GetContentDir();
FString LocalizationDir = ContentDir / TEXT("Localization");
FString ConfigDir = BaseDir / TEXT("Config");
UE_LOG(LogZenFileSystemManifest, Verbose, TEXT("Plugin '%s': BaseDir: '%s'"), *PluginName, *BaseDir);
FString ClientRoot;
FString LocalRoot;
Generator.GetPluginClientAndLocalRoots(Plugin, ClientRoot, LocalRoot);
Generator.AddSingleFile(ClientRoot, LocalRoot, UPluginFile);
Generator.AddFilesFromDirectory(ClientRoot, LocalRoot, LocalizationDir, true, &LocalizationFilter);
Generator.AddFilesFromDirectory(ClientRoot, LocalRoot, ConfigDir, true, &ConfigFilter);
// Next add any valid plugin extension directories of this plugin.
for (const FString& ExtensionBaseDir : Generator.GetApplicablePluginExtensionBaseDirs(Plugin))
{
FString ExtensionLocalizationDir = ExtensionBaseDir / TEXT("Content") / TEXT("Localization");
FString ExtensionConfigDir = ExtensionBaseDir / TEXT("Config");
UE_LOG(LogZenFileSystemManifest, Verbose, TEXT("Plugin '%s': ExtensionBaseDir: '%s'"), *PluginName, *ExtensionBaseDir);
Generator.AddFilesFromDirectory(ClientRoot, LocalRoot, ExtensionBaseDir, false, &PluginFilter);
Generator.AddFilesFromDirectory(ClientRoot, LocalRoot, ExtensionLocalizationDir, true, &LocalizationFilter);
Generator.AddFilesFromDirectory(ClientRoot, LocalRoot, ExtensionConfigDir, true, &ConfigFilter);
}
}
if (Generator.IsIncludeEngine())
{
FString InternationalizationPresetPath = Generator.GetInternationalizationPresetPath();
const TCHAR* ICUDataVersion = TEXT("icudt64l"); // TODO: Could this go into datadriven platform info? But it's basically always this.
Generator.AddFilesFromDirectory(
FPaths::Combine(TEXT("/{engine}"),
TEXT("Content"), TEXT("Internationalization"), ICUDataVersion),
FPaths::Combine(Generator.GetEngineDir(),
TEXT("Content"), TEXT("Internationalization"), InternationalizationPresetPath, ICUDataVersion),
true /* bRecursive */);
}
Generator.AddFilesFromExtensionDirectories(TEXT("Content/Localization"), &LocalizationFilter);
if (Generator.IsIncludeEngine() || Generator.IsIncludeProject())
{
if (Generator.IsSSLCertificatesWillStage())
{
if (!Generator.TryAddSingleRelpath(TEXT("/{project}"), Generator.GetProjectDir(),
FPaths::Combine(TEXT("Content"), TEXT("Certificates"), TEXT("cacert.pem"))))
{
Generator.TryAddSingleRelpath(TEXT("/{engine}"), Generator.GetEngineDir(),
FPaths::Combine(TEXT("Content"), TEXT("Certificates"), TEXT("ThirdParty"), TEXT("cacert.pem")));
}
FFileFilter CertificateFilter = FFileFilter()
.IncludeExtension(TEXT("pem"));
Generator.AddFilesFromDirectory(TEXT("/{project}/Certificates"),
FPaths::Combine(Generator.GetProjectDir(), TEXT("Certificates")), true, &CertificateFilter);
}
}
FFileFilter ContentFilter = FFileFilter()
.ExcludeExtension(TEXT("uasset"))
.ExcludeExtension(TEXT("ubulk"))
.ExcludeExtension(TEXT("uexp"))
.ExcludeExtension(TEXT("umap"));
if (Generator.IsIncludeEngine())
{
Generator.AddFilesFromDirectory(TEXT("/{engine}/Content/Slate"),
FPaths::Combine(Generator.GetEngineDir(), TEXT("Content"), TEXT("Slate")),
true, &ContentFilter);
Generator.AddFilesFromDirectory(TEXT("/{engine}/Content/Movies"),
FPaths::Combine(Generator.GetEngineDir(), TEXT("Content"), TEXT("Movies")),
true, &ContentFilter);
}
if (Generator.IsIncludeProject())
{
Generator.AddFilesFromDirectory(TEXT("/{project}/Content/Slate"),
FPaths::Combine(Generator.GetProjectDir(), TEXT("Content"), TEXT("Slate")),
true, &ContentFilter);
Generator.AddFilesFromDirectory(TEXT("/{project}/Content/Movies"),
FPaths::Combine(Generator.GetProjectDir(), TEXT("Content"), TEXT("Movies")),
true, &ContentFilter);
}
if (Generator.IsIncludeProject())
{
FFileFilter OoodleDictionaryFilter = FFileFilter()
.IncludeExtension(TEXT("udic"));
Generator.AddFilesFromDirectory(TEXT("/{project}/Content/Oodle"),
FPaths::Combine(Generator.GetProjectDir(), TEXT("Content"), TEXT("Oodle")),
false, &OoodleDictionaryFilter);
}
FFileFilter ShaderCacheFilter = FFileFilter()
.IncludeExtension(TEXT("ushadercache"))
.IncludeExtension(TEXT("upipelinecache"));
if (Generator.IsIncludeProject())
{
Generator.AddFilesFromDirectory(TEXT("/{project}/Content"),
FPaths::Combine(Generator.GetProjectDir(), TEXT("Content")),
false, &ShaderCacheFilter);
Generator.AddFilesFromDirectory(FPaths::Combine(TEXT("/{project}"), TEXT("Content"), TEXT("PipelineCaches"), TargetPlatform.IniPlatformName()),
FPaths::Combine(Generator.GetProjectDir(), TEXT("Content"), TEXT("PipelineCaches"), TargetPlatform.IniPlatformName()),
false, &ShaderCacheFilter);
}
if (Generator.IsIncludeProject())
{
for (const FDirectoryPath& AdditionalFolderToStage : PackagingSettings->DirectoriesToAlwaysStageAsUFS)
{
Generator.TryAddFilesFromFlexDirectory(AdditionalFolderToStage.Path, true, &ContentFilter);
}
for (const FDirectoryPath& AdditionalFolderToStage : PackagingSettings->DirectoriesToAlwaysStageAsUFSServer)
{
Generator.TryAddFilesFromFlexDirectory(AdditionalFolderToStage.Path, true, &ContentFilter);
}
}
const int32 CurrentEntryCount = NumEntries();
return CurrentEntryCount - PreviousEntryCount;
}
const FZenFileSystemManifestEntry& FZenFileSystemManifest::CreateManifestEntry(const FString& Filename)
{
const FString FullFilename = FPaths::ConvertRelativePathToFull(Filename);
FString CookedEngineDirectory = FPaths::Combine(CookDirectory, TEXT("Engine"));
FString CookedEngineDirectoryTrailingSeparator;
CookedEngineDirectoryTrailingSeparator.Reserve(CookedEngineDirectory.Len() + 1);
CookedEngineDirectoryTrailingSeparator.Append(CookedEngineDirectory);
CookedEngineDirectoryTrailingSeparator.AppendChar(TEXT('/'));
auto AddEntry = [this, &FullFilename](const FString& ClientDirectory, const FString& LocalDirectory)
-> const FZenFileSystemManifestEntry&
{
FStringView RelativePath = FullFilename;
RelativePath.RightChopInline(LocalDirectory.Len() + 1);
FString ServerRelativeDirectory = LocalDirectory;
FPaths::MakePathRelativeTo(ServerRelativeDirectory, *FPaths::RootDir());
ServerRelativeDirectory = TEXT("/") + ServerRelativeDirectory;
FString ServerPath = FPaths::Combine(ServerRelativeDirectory, RelativePath.GetData());
FString ClientPath = FPaths::Combine(ClientDirectory, RelativePath.GetData());
const FIoChunkId FileChunkId = CreateExternalFileChunkId(ClientPath);
return AddManifestEntry(FileChunkId, MoveTemp(ServerPath), MoveTemp(ClientPath));
};
if (FullFilename.StartsWith(CookedEngineDirectoryTrailingSeparator))
{
return AddEntry(TEXT("/{engine}"), CookedEngineDirectory);
}
FString CookedProjectDirectory = FPaths::Combine(CookDirectory, FApp::GetProjectName());
FString CookedProjectDirectoryTrailingSeparator;
CookedProjectDirectoryTrailingSeparator.Reserve(CookedProjectDirectory.Len() + 1);
CookedProjectDirectoryTrailingSeparator.Append(CookedProjectDirectory);
CookedProjectDirectoryTrailingSeparator.AppendChar(TEXT('/'));
if (FullFilename.StartsWith(CookedProjectDirectoryTrailingSeparator))
{
return AddEntry(TEXT("/{project}"), CookedProjectDirectory);
}
return InvalidEntry;
}
const FZenFileSystemManifestEntry& FZenFileSystemManifest::AddManifestEntry(const FIoChunkId& FileChunkId,
FString ServerPath, FString ClientPath)
{
check(ServerPath.Len() > 0 && ClientPath.Len() > 0);
ServerPath.ReplaceInline(TEXT("\\"), TEXT("/"));
ClientPath.ReplaceInline(TEXT("\\"), TEXT("/"));
// The server path is always relative to project root
if (ServerPath[0] == '/')
{
ServerPath.RightChopInline(1);
}
int32& EntryIndex = ServerPathToEntry.FindOrAdd(ServerPath, INDEX_NONE);
if (EntryIndex != INDEX_NONE)
{
return Entries[EntryIndex];
}
EntryIndex = Entries.Num();
FZenFileSystemManifestEntry Entry;
Entry.ServerPath = MoveTemp(ServerPath);
Entry.ClientPath = MoveTemp(ClientPath);
Entry.FileChunkId = FileChunkId;
#if WITH_EDITOR
if (Entry.ClientPath == ReferencedSetClientPath)
{
ReferencedSet.Emplace(MoveTemp(Entry));
return *ReferencedSet;
}
else
#endif
{
Entries.Add(MoveTemp(Entry));
return Entries[EntryIndex];
}
}
bool FZenFileSystemManifest::Save(const TCHAR* Filename)
{
check(Filename);
TArray<FString> CsvLines;
CsvLines.Add(FString::Printf(TEXT(";ServerRoot=%s, Platform=%s, CookDirectory=%s"), *ServerRoot, *TargetPlatform.PlatformName(), *CookDirectory));
CsvLines.Add(TEXT("FileId, ServerPath, ClientPath"));
TStringBuilder<2048> Sb;
TArray<const FZenFileSystemManifestEntry*> SortedEntries;
SortedEntries.Reserve(Entries.Num());
for (const FZenFileSystemManifestEntry& Entry : Entries)
{
SortedEntries.Add(&Entry);
}
Algo::Sort(SortedEntries, [](const FZenFileSystemManifestEntry* A, const FZenFileSystemManifestEntry* B) { return A->ClientPath < B->ClientPath; });
for (const FZenFileSystemManifestEntry* Entry : SortedEntries)
{
Sb.Reset();
Sb << Entry->FileChunkId << TEXT(", ") << Entry->ServerPath << TEXT(", ") << *Entry->ClientPath;
CsvLines.Add(Sb.ToString());
}
return FFileHelper::SaveStringArrayToFile(CsvLines, Filename);
}
namespace UE::ZenFileSystemManifest
{
FManifestGenerator::FManifestGenerator(FZenFileSystemManifest& InManifest,
const TOptional<ICookedPackageWriter::FReferencedPluginsInfo>& InReferencedPlugins)
: Manifest(InManifest)
, ReferencedPlugins(InReferencedPlugins)
, PlatformFile(FPlatformFileManager::Get().GetPlatformFile())
{
UnrealEngineRootDir = FPaths::RootDir();
EngineDir = FPaths::EngineDir();
FPaths::NormalizeDirectoryName(EngineDir);
ProjectDir = FPaths::ProjectDir();
FPaths::NormalizeDirectoryName(ProjectDir);
ProjectName = FApp::GetProjectName();
PopulatePlatformDirectoryNames();
}
const FString& FManifestGenerator::GetUnrealEngineRootDir() const
{
return UnrealEngineRootDir;
}
const FString& FManifestGenerator::GetEngineDir() const
{
return EngineDir;
}
const FString& FManifestGenerator::GetProjectDir() const
{
return ProjectDir;
}
const FString& FManifestGenerator::GetProjectName() const
{
return ProjectName;
}
FString FManifestGenerator::GetCookedEngineDir() const
{
return FPaths::Combine(Manifest.CookDirectory, TEXT("Engine"));
}
FString FManifestGenerator::GetCookedProjectDir() const
{
return FPaths::Combine(Manifest.CookDirectory, GetProjectName());
}
FString FManifestGenerator::GetCookedMetadataDir() const
{
return FPaths::Combine(Manifest.CookDirectory, GetProjectName(), TEXT("Metadata"));
}
const FString& FManifestGenerator::GetUBTPlatformString() const
{
return UBTPlatformString;
}
FFileFilter& FManifestGenerator::GetBaseFilter()
{
return BaseFilter;
}
void FManifestGenerator::AddSingleFile(const FString& ClientRoot, const FString& LocalRoot, FStringView LocalFilePath)
{
//TRACE_CPUPROFILER_EVENT_SCOPE(AddManifestEntry);
FString ClientPath(LocalFilePath);
// MakePathRelative calls GetPath (aka the parent path) on InRelativeTo argument before using it to look for the
// common prefix. Since we want our root paths to be used directly, rather than their parent path, add a
// terminating / to our root paths when passing them into MakePathRelativeTo; a terminating slash causes GetPath
// to return the path before the terminating slash.
FPaths::MakePathRelativeTo(ClientPath, *(LocalRoot / TEXT("")));
ClientPath = FPaths::Combine(ClientRoot, ClientPath);
FString ServerRelativePath(LocalFilePath);
FPaths::MakePathRelativeTo(ServerRelativePath, *(UnrealEngineRootDir / TEXT("")));
ServerRelativePath = FString(TEXT("/")) + ServerRelativePath;
const FIoChunkId FileChunkId = CreateExternalFileChunkId(ClientPath);
Manifest.AddManifestEntry(
FileChunkId,
MoveTemp(ServerRelativePath),
MoveTemp(ClientPath));
}
bool FManifestGenerator::TryAddSingleRelpath(const FString& ClientRoot, const FString& LocalRoot, const FString& RelPathFromRoot)
{
FString LocalFilePath = FPaths::Combine(LocalRoot, RelPathFromRoot);
if (!FPaths::FileExists(LocalFilePath))
{
return false;
}
AddSingleFile(ClientRoot, LocalRoot, LocalFilePath);
return true;
}
void FManifestGenerator::AddFilesFromDirectory(const FString& ClientRoot, const FString& LocalRoot,
bool bIncludeSubDirs, FFileFilter* AdditionalFilter)
{
AddFilesFromDirectory(ClientRoot, LocalRoot, LocalRoot, bIncludeSubDirs, AdditionalFilter);
}
void FManifestGenerator::AddFilesFromDirectory(const FString& ClientRoot, const FString& LocalRoot,
const FString& LocalAddRoot, bool bIncludeSubDirs, FFileFilter* AdditionalFilter)
{
FAddFilesFromDirectoryVisitor Visitor(*this, ClientRoot, LocalRoot, bIncludeSubDirs, AdditionalFilter);
Visitor.DirectoryVisitQueue.Push(LocalAddRoot);
while (!Visitor.DirectoryVisitQueue.IsEmpty())
{
FString DirectoryToVisit = Visitor.DirectoryVisitQueue.Pop(EAllowShrinking::No);
PlatformFile.IterateDirectory(*DirectoryToVisit,
[&Visitor](const TCHAR* InnerFileNameOrDirectory, bool bIsDirectory) -> bool
{
return Visitor.VisitorFunction(InnerFileNameOrDirectory, bIsDirectory);
});
}
};
bool FManifestGenerator::TryAddFilesFromFlexDirectory(const FString& FullOrProjectRelativePath,
bool bRecursive, FFileFilter* AdditionalFilter)
{
FString AbsoluteDirToStage = FPaths::ConvertRelativePathToFull(FPaths::Combine(ProjectDir, TEXT("Content"), FullOrProjectRelativePath));
FPaths::NormalizeDirectoryName(AbsoluteDirToStage);
FString AbsoluteEngineDir = FPaths::ConvertRelativePathToFull(EngineDir);
FString AbsoluteProjectDir = FPaths::ConvertRelativePathToFull(ProjectDir);
FStringView RelativeToKnownRootView;
if (FPathViews::TryMakeChildPathRelativeTo(AbsoluteDirToStage, AbsoluteProjectDir, RelativeToKnownRootView))
{
AddFilesFromDirectory(FPaths::Combine(TEXT("/{project}"), RelativeToKnownRootView.GetData()),
FPaths::Combine(ProjectDir, RelativeToKnownRootView.GetData()),
bRecursive, AdditionalFilter);
return true;
}
else if (FPathViews::TryMakeChildPathRelativeTo(AbsoluteDirToStage, AbsoluteEngineDir, RelativeToKnownRootView))
{
AddFilesFromDirectory(FPaths::Combine(TEXT("/{engine}"), RelativeToKnownRootView.GetData()),
FPaths::Combine(EngineDir, RelativeToKnownRootView.GetData()),
bRecursive, AdditionalFilter);
return true;
}
else
{
UE_LOG(LogZenFileSystemManifest, Warning,
TEXT("Ignoring additional folder to stage that is not relative to the engine or project directory: %s"),
*FullOrProjectRelativePath);
return false;
}
}
void FManifestGenerator::AddFilesFromExtensionDirectories(const TCHAR* ExtensionSubDir, FFileFilter* AdditionalFilter)
{
TArray<FString> ExtensionDirs;
if (IsIncludeEngine())
{
GetExtensionDirs(ExtensionDirs, *EngineDir, ExtensionSubDir, PlatformDirectoryNames);
for (const FString& LocalDir : ExtensionDirs)
{
if (!LocalDir.StartsWith(EngineDir))
{
UE_LOG(LogZenFileSystemManifest, Warning,
TEXT("GetExtensionDirs on root directory '%s' unexpectedly returned folder '%s' that is not a subdirectory of the root directory."),
*EngineDir, *LocalDir);
continue;
}
FString ClientDir = FPaths::Combine(TEXT("/{engine}"), LocalDir.RightChop(EngineDir.Len()));
AddFilesFromDirectory(ClientDir, LocalDir, true, AdditionalFilter);
}
}
if (IsIncludeProject())
{
ExtensionDirs.Reset();
GetExtensionDirs(ExtensionDirs, *ProjectDir, ExtensionSubDir, PlatformDirectoryNames);
for (const FString& LocalDir : ExtensionDirs)
{
if (!LocalDir.StartsWith(ProjectDir))
{
UE_LOG(LogZenFileSystemManifest, Warning,
TEXT("GetExtensionDirs on root directory '%s' unexpectedly returned folder '%s' that is not a subdirectory of the root directory."),
*ProjectDir, *LocalDir);
continue;
}
FString ClientDir = FPaths::Combine(TEXT("/{project}"), LocalDir.RightChop(ProjectDir.Len()));
AddFilesFromDirectory(ClientDir, LocalDir, true, AdditionalFilter);
}
}
};
void FManifestGenerator::GetExtensionDirs(TArray<FString>& OutExtensionDirs, const TCHAR* BaseDir, const TCHAR* SubDir,
const TArray<FString>& PlatformDirectoryNames)
{
auto AddIfDirectoryExists = [&OutExtensionDirs](FString&& Dir)
{
if (FPaths::DirectoryExists(Dir))
{
OutExtensionDirs.Emplace(MoveTemp(Dir));
}
};
AddIfDirectoryExists(FPaths::Combine(BaseDir, SubDir));
FString PlatformExtensionBaseDir = FPaths::Combine(BaseDir, TEXT("Platforms"));
for (const FString& PlatformDirectoryName : PlatformDirectoryNames)
{
AddIfDirectoryExists(FPaths::Combine(PlatformExtensionBaseDir, PlatformDirectoryName, SubDir));
}
FString RestrictedBaseDir = FPaths::Combine(BaseDir, TEXT("Restricted"));
IFileManager::Get().IterateDirectory(*RestrictedBaseDir,
[&OutExtensionDirs, SubDir, &PlatformDirectoryNames]
(const TCHAR* FilenameOrDirectory, bool bIsDirectory) -> bool
{
if (bIsDirectory)
{
GetExtensionDirs(OutExtensionDirs, FilenameOrDirectory, SubDir, PlatformDirectoryNames);
}
return true;
});
}
TArray<TSharedRef<IPlugin>> FManifestGenerator::GetApplicablePlugins()
{
constexpr bool bSkipDisabledPlugins = false;
IPluginManager& PluginManager = IPluginManager::Get();
TArray<TSharedRef<IPlugin>> DiscoveredPlugins = PluginManager.GetDiscoveredPlugins();
DiscoveredPlugins.RemoveAll([this](TSharedRef<IPlugin>& Plugin)
{
const FString& PluginName = Plugin->GetName();
if (bSkipDisabledPlugins && !Plugin->IsEnabled())
{
UE_LOG(LogZenFileSystemManifest, Verbose, TEXT("Skipping plugin '%s' because it is disabled."), *PluginName);
return true;
}
const FPluginDescriptor& Descriptor = Plugin->GetDescriptor();
if (!Descriptor.SupportsTargetPlatform(UBTPlatformString))
{
UE_LOG(LogZenFileSystemManifest, Verbose, TEXT("Skipping plugin '%s' because it is not supported on platform '%s'."),
*PluginName, *Manifest.TargetPlatform.PlatformName());
for (const FString& SupportedTargetPlatform : Descriptor.SupportedTargetPlatforms)
{
UE_LOG(LogZenFileSystemManifest, Verbose, TEXT(" '%s' supports platform '%s'"),
*PluginName, *SupportedTargetPlatform);
}
return true;
}
if (ReferencedPlugins.IsSet())
{
// Skip adding files to the manifest that are under plugins that were not cooked in the current BuildLayer.
if (!Plugin->CanContainContent())
{
// Code-only plugins would not be found by looking for cooked packages, so do not filter them out based
// on their presence in the ReferencedPlugins. Instead, add all code-only plugins from game or engine that
// are enabled on the target, but only if we are cooking game or engine.
if (Plugin->GetLoadedFrom() == EPluginLoadedFrom::Engine && !ReferencedPlugins->bReferencesEngine)
{
UE_LOG(LogZenFileSystemManifest, Verbose,
TEXT("Skipping plugin '%s' because it is a code-only Engine plugin, and we did not cook /Engine."),
*PluginName);
return true;
}
if (Plugin->GetLoadedFrom() == EPluginLoadedFrom::Project && !ReferencedPlugins->bReferencesGame)
{
UE_LOG(LogZenFileSystemManifest, Verbose,
TEXT("Skipping plugin '%s' because it is a code-only Project plugin, and we did not cook /Game."),
*PluginName);
return true;
}
}
else
{
if (!ReferencedPlugins->ReferencedPlugins.Contains(Plugin->GetName()))
{
UE_LOG(LogZenFileSystemManifest, Verbose,
TEXT("Skipping plugin '%s' because we did not cook any content from it."),
*PluginName);
return true;
}
}
}
return false;
});
return DiscoveredPlugins;
}
void FManifestGenerator::GetPluginClientAndLocalRoots(TSharedRef<IPlugin>& Plugin,
FString& OutClientRoot, FString& OutLocalRoot)
{
switch (Plugin->GetLoadedFrom())
{
case EPluginLoadedFrom::Engine:
OutClientRoot = TEXT("/{engine}");
OutLocalRoot = EngineDir;
break;
case EPluginLoadedFrom::Project:
OutClientRoot = TEXT("/{project}");
OutLocalRoot = ProjectDir;
break;
default:
checkNoEntry();
break;
}
}
TArray<FString> FManifestGenerator::GetApplicablePluginExtensionBaseDirs(TSharedRef<IPlugin>& Plugin)
{
TArray<FString> ExtensionBaseDirs = Plugin->GetExtensionBaseDirs();
ExtensionBaseDirs.RemoveAll([this](const FString& ExtensionBaseDir)
{
// Scan the extension path for "Platforms/X" and include this extension if it is not platform specific at all,
// or if X is found and it is a valid target platform
bool bFoundPlatformsComponent = false;
bool bDone = false;
bool bIncludeExtension = true;
FPathViews::IterateComponents(
ExtensionBaseDir,
[this, &bFoundPlatformsComponent, &bDone, &bIncludeExtension](FStringView CurrentPathComponent)
{
if (!bFoundPlatformsComponent)
{
if (CurrentPathComponent == TEXTVIEW("Platforms"))
{
bFoundPlatformsComponent = true;
}
}
else if (!bDone)
{
uint32 CurrentPathComponentHash = GetTypeHash(CurrentPathComponent);
const bool bIsValidPlatform = PlatformDirectoryNameSet.ContainsByHash(CurrentPathComponentHash, CurrentPathComponent);
bIncludeExtension = bIsValidPlatform;
bDone = true;
}
else
{
// Do nothing.
}
});
return !bIncludeExtension;
});
return ExtensionBaseDirs;
}
FString FManifestGenerator::GetInternationalizationPresetPath()
{
const UProjectPackagingSettings* const PackagingSettings = GetDefault<UProjectPackagingSettings>();
FString InternationalizationPresetAsString = UEnum::GetValueAsString(PackagingSettings->InternationalizationPreset);
const TCHAR* InternationalizationPresetPath = FCString::Strrchr(*InternationalizationPresetAsString, ':');
if (InternationalizationPresetPath)
{
++InternationalizationPresetPath;
}
else
{
UE_LOG(LogZenFileSystemManifest, Warning, TEXT("Failed reading internationalization preset setting, defaulting to English"));
InternationalizationPresetPath = TEXT("English");
}
return FString(InternationalizationPresetPath);
}
bool FManifestGenerator::IsSSLCertificatesWillStage()
{
bool bSSLCertificatesWillStage = false;
FConfigCacheIni* TargetPlatformConfig = Manifest.TargetPlatform.GetConfigSystem();
if (TargetPlatformConfig)
{
GConfig->GetBool(TEXT("/Script/Engine.NetworkSettings"), TEXT("n.VerifyPeer"), bSSLCertificatesWillStage, GEngineIni);
}
return bSSLCertificatesWillStage;
}
bool FManifestGenerator::IsIncludeEngine()
{
if (!ReferencedPlugins.IsSet())
{
return true;
}
return ReferencedPlugins->bReferencesEngine;
}
bool FManifestGenerator::IsIncludeProject()
{
if (!ReferencedPlugins.IsSet())
{
return true;
}
return ReferencedPlugins->bReferencesGame;
}
void FManifestGenerator::PopulatePlatformDirectoryNames()
{
FString IniPlatformName = Manifest.TargetPlatform.IniPlatformName();
const FDataDrivenPlatformInfo& PlatformInfo = FDataDrivenPlatformInfoRegistry::GetPlatformInfo(IniPlatformName);
PlatformDirectoryNameSet.Reserve(PlatformInfo.IniParentChain.Num()
+ PlatformInfo.AdditionalRestrictedFolders.Num() + 1);
PlatformDirectoryNameSet.Add(IniPlatformName);
for (const FString& PlatformName : PlatformInfo.AdditionalRestrictedFolders)
{
PlatformDirectoryNameSet.Add(PlatformName);
}
for (const FString& PlatformName : PlatformInfo.IniParentChain)
{
PlatformDirectoryNameSet.Add(PlatformName);
}
PlatformDirectoryNames = PlatformDirectoryNameSet.Array();
UBTPlatformString = PlatformInfo.UBTPlatformString;
}
FFileFilter& FFileFilter::ExcludeDirectoryLeafName(const TCHAR* LeafName)
{
DirectoryExclusionFilter.Emplace(LeafName);
return *this;
}
FFileFilter& FFileFilter::ExcludeExtension(const TCHAR* Extension)
{
ExtensionExclusionFilter.Emplace(Extension);
return *this;
}
FFileFilter& FFileFilter::IncludeExtension(const TCHAR* Extension)
{
ExtensionInclusionFilter.Emplace(Extension);
return *this;
}
bool FFileFilter::IsDirectoryLeafNameKept(FStringView LeafName) const
{
for (const FString& ExcludedDirectory : DirectoryExclusionFilter)
{
if (LeafName == ExcludedDirectory)
{
return false;
}
}
return true;
}
bool FFileFilter::IsFileExtensionKept(FStringView Extension)
{
if (!ExtensionExclusionFilter.IsEmpty())
{
for (const FString& ExcludedExtension : ExtensionExclusionFilter)
{
if (Extension == ExcludedExtension)
{
return false;
}
}
}
if (!ExtensionInclusionFilter.IsEmpty())
{
for (const FString& IncludedExtension : ExtensionInclusionFilter)
{
if (Extension == IncludedExtension)
{
return true;
}
}
return false;
}
return true;
}
FAddFilesFromDirectoryVisitor::FAddFilesFromDirectoryVisitor(
FManifestGenerator& InGenerator, const FString& InClientRoot, const FString& InLocalRoot, bool bInIncludeSubDirs,
FFileFilter* InAdditionalFilter)
: Generator(InGenerator), ClientRoot(InClientRoot), LocalRoot(InLocalRoot)
, AdditionalFilter(InAdditionalFilter), bIncludeSubDirs(bInIncludeSubDirs)
{
}
bool FAddFilesFromDirectoryVisitor::VisitorFunction(const TCHAR* InnerFileNameOrDirectory, bool bIsDirectory)
{
if (bIsDirectory)
{
if (!bIncludeSubDirs)
{
return true;
}
FStringView DirectoryName = FPathViews::GetPathLeaf(InnerFileNameOrDirectory);
if (!Generator.BaseFilter.IsDirectoryLeafNameKept(DirectoryName))
{
return true;
}
if (AdditionalFilter && !AdditionalFilter->IsDirectoryLeafNameKept(DirectoryName))
{
return true;
}
DirectoryVisitQueue.Add(InnerFileNameOrDirectory);
return true;
}
FStringView Extension = FPathViews::GetExtension(InnerFileNameOrDirectory);
if (!Generator.BaseFilter.IsFileExtensionKept(Extension))
{
return true;
}
if (AdditionalFilter && !AdditionalFilter->IsFileExtensionKept(Extension))
{
return true;
}
Generator.AddSingleFile(ClientRoot, LocalRoot, InnerFileNameOrDirectory);
return true;
}
} // namespace UE::ZenFileSystemManifest