// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using System.Linq;
using EpicGames.Core;
using UnrealBuildBase;
namespace UnrealBuildTool
{
///
/// Where a plugin was loaded from
///
public enum PluginLoadedFrom
{
///
/// Plugin is built-in to the engine
///
Engine,
///
/// Project-specific plugin, stored within a game project directory
///
Project
}
///
/// Where a plugin was loaded from. The order of this enum is important; in the case of name collisions, larger-valued types will take precedence. Plugins of the same type may not be duplicated.
///
public enum PluginType
{
///
/// Plugin is built-in to the engine
///
Engine,
///
/// Project-specific plugin, stored within a game project directory
///
Project,
///
/// Plugin found in an external directory (found in an AdditionalPluginDirectory listed in the project file, or referenced on the command line)
///
External,
///
/// Project-specific mod plugin
///
Mod,
}
///
/// Information about a single plugin
///
[DebuggerDisplay("\\{{File}\\}")]
public class PluginInfo
{
///
/// Plugin name
///
public readonly string Name;
///
/// Path to the plugin
///
public readonly FileReference File;
///
/// Path to the plugin's root directory
///
public readonly DirectoryReference Directory;
///
/// Children plugin files that can be added to this plugin (platform extensions)
///
public List ChildFiles = new List();
///
/// The plugin descriptor
///
public PluginDescriptor Descriptor;
///
/// The type of this plugin
///
public PluginType Type;
///
/// Constructs a PluginInfo object
///
/// Path to the plugin descriptor
/// The type of this plugin
public PluginInfo(FileReference InFile, PluginType InType)
{
Name = Path.GetFileNameWithoutExtension(InFile.FullName);
File = InFile;
Directory = File.Directory;
Descriptor = PluginDescriptor.FromFile(File);
Type = InType;
}
///
/// Determines whether the plugin should be enabled by default
///
public bool IsEnabledByDefault(bool bAllowEnginePluginsEnabledByDefault)
{
if (Descriptor.bEnabledByDefault.HasValue)
{
if (Descriptor.bEnabledByDefault.Value)
{
return (LoadedFrom == PluginLoadedFrom.Project ? true : bAllowEnginePluginsEnabledByDefault);
}
else
{
return false;
}
}
else
{
return (LoadedFrom == PluginLoadedFrom.Project);
}
}
///
/// Determines where the plugin was loaded from
///
public PluginLoadedFrom LoadedFrom
{
get
{
if(Type == PluginType.Engine)
{
return PluginLoadedFrom.Engine;
}
else
{
return PluginLoadedFrom.Project;
}
}
}
}
///
/// Class for enumerating plugin metadata
///
public static class Plugins
{
///
/// Cache of plugins under each directory
///
static Dictionary> PluginInfoCache = new Dictionary>();
///
/// Cache of plugin filenames under each directory
///
static Dictionary> PluginFileCache = new Dictionary>();
///
/// Invalidate cached plugin data so that we can pickup new things
/// Warning: Will make subsequent plugin lookups and directory scans slow until the caches are repopulated
///
public static void InvalidateCaches_SLOW()
{
PluginInfoCache = new Dictionary>();
PluginFileCache = new Dictionary>();
DirectoryItem.ResetAllCachedInfo_SLOW();
}
///
/// Returns a filtered list of plugins as a name:plugin dictionary to ensure that any game plugins override engine plugins with the same
/// name, and otherwise that no two plugins with the same name exist.
///
/// List of plugins to filter
/// Filtered Dictionary of plugins
public static Dictionary ToFilteredDictionary(IEnumerable Plugins)
{
Dictionary NameToPluginInfo = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
foreach (PluginInfo Plugin in Plugins)
{
PluginInfo? ExistingPluginInfo;
if (!NameToPluginInfo.TryGetValue(Plugin.Name, out ExistingPluginInfo))
{
NameToPluginInfo.Add(Plugin.Name, Plugin);
}
else if (Plugin.Type > ExistingPluginInfo.Type)
{
NameToPluginInfo[Plugin.Name] = Plugin;
}
else if (Plugin.Type == ExistingPluginInfo.Type)
{
throw new BuildException(String.Format("Found '{0}' plugin in two locations ({1} and {2}). Plugin names must be unique.", Plugin.Name, ExistingPluginInfo.File, Plugin.File));
}
}
return NameToPluginInfo;
}
///
/// Filters the list of plugins to ensure that any game plugins override engine plugins with the same name, and otherwise that no two
/// plugins with the same name exist.
///
/// List of plugins to filter
/// Filtered list of plugins in the original order
public static IEnumerable FilterPlugins(IEnumerable Plugins)
{
Dictionary NameToPluginInfo = ToFilteredDictionary(Plugins);
return Plugins.Where(x => NameToPluginInfo[x.Name] == x);
}
///
/// Read all the plugins available to a given project
///
/// Path to the engine directory
/// Path to the project directory (or null)
/// List of additional directories to scan for available plugins
/// Sequence of PluginInfo objects, one for each discovered plugin
public static List ReadAvailablePlugins(DirectoryReference EngineDir, DirectoryReference ProjectDir, List AdditionalDirectories)
{
List Plugins = new List();
// Read all the engine plugins
Plugins.AddRange(ReadEnginePlugins(EngineDir));
// Read all the project plugins
if (ProjectDir != null)
{
Plugins.AddRange(ReadProjectPlugins(ProjectDir));
}
// Scan for shared plugins in project specified additional directories
if(AdditionalDirectories != null)
{
foreach (DirectoryReference AdditionalDirectory in AdditionalDirectories)
{
Plugins.AddRange(ReadPluginsFromDirectory(AdditionalDirectory, "", PluginType.External));
}
}
return Plugins;
}
///
/// Enumerates all the plugin files available to the given project
///
/// Path to the project file
/// List of project files
public static IEnumerable EnumeratePlugins(FileReference? ProjectFile)
{
List BaseDirs = new List();
BaseDirs.AddRange(UnrealBuildTool.GetExtensionDirs(Unreal.EngineDirectory, "Plugins"));
if(ProjectFile != null)
{
BaseDirs.AddRange(UnrealBuildTool.GetExtensionDirs(ProjectFile.Directory, "Plugins"));
BaseDirs.AddRange(UnrealBuildTool.GetExtensionDirs(ProjectFile.Directory, "Mods"));
}
return BaseDirs.SelectMany(x => EnumeratePlugins(x)).ToList();
}
///
/// Read all the plugin descriptors under the given engine directory
///
/// The parent directory to look in.
/// Sequence of the found PluginInfo object.
public static IReadOnlyList ReadEnginePlugins(DirectoryReference EngineDirectory)
{
return ReadPluginsFromDirectory(EngineDirectory, "Plugins", PluginType.Engine);
}
///
/// Read all the plugin descriptors under the given project directory
///
/// The parent directory to look in.
/// Sequence of the found PluginInfo object.
public static IReadOnlyList ReadProjectPlugins(DirectoryReference ProjectDirectory)
{
List Plugins = new List();
Plugins.AddRange(ReadPluginsFromDirectory(ProjectDirectory, "Plugins", PluginType.Project));
Plugins.AddRange(ReadPluginsFromDirectory(ProjectDirectory, "Mods", PluginType.Mod));
return Plugins.AsReadOnly();
}
///
/// Read all of the plugins found in the project specified additional plugin directories
///
/// The additional directory to scan
/// List of the found PluginInfo objects
public static IReadOnlyList ReadAdditionalPlugins(DirectoryReference AdditionalDirectory)
{
DirectoryReference FullPath = DirectoryReference.Combine(AdditionalDirectory, "");
if (!DirectoryReference.Exists(FullPath))
{
Log.TraceWarning("AdditionalPluginDirectory {0} not found. Path should be relative to the project", FullPath);
}
return ReadPluginsFromDirectory(AdditionalDirectory, "", PluginType.External);
}
///
/// Determines whether the given suffix is valid for a child plugin
///
///
/// Whether the suffix is appopriate
private static bool IsValidChildPluginSuffix(string Suffix)
{
foreach (UnrealPlatformGroup Group in UnrealPlatformGroup.GetValidGroups())
{
if (Group.ToString().Equals(Suffix, StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
}
foreach (UnrealTargetPlatform Platform in UnrealTargetPlatform.GetValidPlatforms())
{
if (Platform.ToString().Equals(Suffix, StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
}
return false;
}
///
/// Attempt to merge a child plugin up into a parent plugin (via file naming scheme). Very little merging happens
/// but it does allow for platform extensions to extend a plugin with module files
///
/// Child plugin that needs to merge to a main, parent plugin
/// Child plugin's filename, used to determine the parent's name
private static void TryMergeWithParent(PluginInfo Child, FileReference Filename)
{
// find the parent
PluginInfo? Parent = null;
string[] Tokens = Filename.GetFileNameWithoutAnyExtensions().Split("_".ToCharArray());
if (Tokens.Length == 2)
{
string ParentPluginName = Tokens[0];
foreach (KeyValuePair> Pair in PluginInfoCache)
{
Parent = Pair.Value.FirstOrDefault(x => x.Name.Equals(ParentPluginName, StringComparison.InvariantCultureIgnoreCase) && x.LoadedFrom == Child.LoadedFrom);
if (Parent != null)
{
break;
}
}
}
else
{
throw new BuildException("Platform extension plugin {0} was named improperly. It must be in the form _.uplugin", Filename);
}
// did we find a parent plugin?
if (Parent == null)
{
throw new BuildException("Unable to find parent plugin {0} for platform extension plugin {1}. Make sure {0}.uplugin exists.", Tokens[0], Filename);
}
// validate child plugin file name
string PlatformName = Tokens[1];
if (!IsValidChildPluginSuffix(PlatformName))
{
Log.TraceWarning("Ignoring child plugin: {0} - Unknown suffix \"{1}\". Expected valid platform or group", Child.File.GetFileName(), PlatformName);
return;
}
// add our uplugin file to the existing plugin to be used to search for modules later
Parent.ChildFiles.Add(Child.File);
// this should cause an error if it's invalid platform name
//UnrealTargetPlatform Platform = UnrealTargetPlatform.Parse(PlatformName);
// merge the supported platforms
if (Child.Descriptor.SupportedTargetPlatforms != null)
{
if (Parent.Descriptor.SupportedTargetPlatforms == null)
{
Parent.Descriptor.SupportedTargetPlatforms = Child.Descriptor.SupportedTargetPlatforms;
}
else
{
Parent.Descriptor.SupportedTargetPlatforms = Parent.Descriptor.SupportedTargetPlatforms.Union(Child.Descriptor.SupportedTargetPlatforms).ToList();
}
}
// make sure we are whitelisted for any modules we list
if (Child.Descriptor.Modules != null)
{
if (Parent.Descriptor.Modules == null)
{
Parent.Descriptor.Modules = Child.Descriptor.Modules;
}
else
{
foreach (ModuleDescriptor ChildModule in Child.Descriptor.Modules)
{
ModuleDescriptor ParentModule = Parent.Descriptor.Modules.FirstOrDefault(x => x.Name.Equals(ChildModule.Name) && x.Type == ChildModule.Type);
if (ParentModule != null)
{
// merge white/blacklists (if the parent had a list, and child didn't specify a list, just add the child platform to the parent list - for white and black!)
if (ChildModule.WhitelistPlatforms != null)
{
if (ParentModule.WhitelistPlatforms == null)
{
ParentModule.WhitelistPlatforms = ChildModule.WhitelistPlatforms;
}
else
{
ParentModule.WhitelistPlatforms = ParentModule.WhitelistPlatforms.Union(ChildModule.WhitelistPlatforms).ToList();
}
}
if (ChildModule.BlacklistPlatforms != null)
{
if (ParentModule.BlacklistPlatforms == null)
{
ParentModule.BlacklistPlatforms = ChildModule.BlacklistPlatforms;
}
else
{
ParentModule.BlacklistPlatforms = ParentModule.BlacklistPlatforms.Union(ChildModule.BlacklistPlatforms).ToList();
}
}
}
else
{
Parent.Descriptor.Modules.Add(ChildModule);
}
}
}
}
// make sure we are whitelisted for any plugins we list
if (Child.Descriptor.Plugins != null)
{
if (Parent.Descriptor.Plugins == null)
{
Parent.Descriptor.Plugins = Child.Descriptor.Plugins;
}
else
{
foreach (PluginReferenceDescriptor ChildPluginReference in Child.Descriptor.Plugins)
{
PluginReferenceDescriptor ParentPluginReference = Parent.Descriptor.Plugins.FirstOrDefault(x => x.Name.Equals(ChildPluginReference.Name));
if (ParentPluginReference != null)
{
// we only need to whitelist the platform if the parent had a whitelist (otherwise, we could mistakenly remove all other platforms)
if (ParentPluginReference.WhitelistPlatforms != null)
{
if (ChildPluginReference.WhitelistPlatforms != null)
{
ParentPluginReference.WhitelistPlatforms = ParentPluginReference.WhitelistPlatforms.Union(ChildPluginReference.WhitelistPlatforms).ToArray();
}
}
// if we want to blacklist a platform, add it even if the parent didn't have a blacklist. this won't cause problems with other platforms
if (ChildPluginReference.BlacklistPlatforms != null)
{
if (ParentPluginReference.BlacklistPlatforms == null)
{
ParentPluginReference.BlacklistPlatforms = ChildPluginReference.BlacklistPlatforms;
}
else
{
ParentPluginReference.BlacklistPlatforms = ParentPluginReference.BlacklistPlatforms.Union(ChildPluginReference.BlacklistPlatforms).ToArray();
}
}
}
else
{
Parent.Descriptor.Plugins.Add(ChildPluginReference);
}
}
}
}
// @todo platplug: what else do we want to support merging?!?
}
///
/// Read all the plugin descriptors under the given directory
///
/// The directory to look in.
/// A subdirectory to look in in RootDirectory and any other Platform directories under Root
/// The plugin type
/// Sequence of the found PluginInfo object.
public static IReadOnlyList ReadPluginsFromDirectory(DirectoryReference RootDirectory, string Subdirectory, PluginType Type)
{
// look for directories in RootDirectory and and extension directories under RootDirectory
List RootDirectories = UnrealBuildTool.GetExtensionDirs(RootDirectory, Subdirectory);
Dictionary ChildPlugins = new Dictionary();
List AllParentPlugins = new List();
foreach (DirectoryReference Dir in RootDirectories)
{
if (!DirectoryReference.Exists(Dir))
{
continue;
}
List? Plugins;
if (!PluginInfoCache.TryGetValue(Dir, out Plugins))
{
Plugins = new List();
foreach (FileReference PluginFileName in EnumeratePlugins(Dir))
{
PluginInfo Plugin = new PluginInfo(PluginFileName, Type);
// is there a parent to merge up into?
if (Plugin.Descriptor.bIsPluginExtension)
{
ChildPlugins.Add(Plugin, PluginFileName);
}
else
{
Plugins.Add(Plugin);
}
}
PluginInfoCache.Add(Dir, Plugins);
}
// gather all of the plugins into one list
AllParentPlugins.AddRange(Plugins);
}
// now that all parent plugins are read in, we can let the children look up the parents
foreach (KeyValuePair Pair in ChildPlugins)
{
TryMergeWithParent(Pair.Key, Pair.Value);
}
return AllParentPlugins;
}
///
/// Find paths to all the plugins under a given parent directory (recursively)
///
/// Parent directory to look in. Plugins will be found in any *subfolders* of this directory.
public static IEnumerable EnumeratePlugins(DirectoryReference ParentDirectory)
{
List? FileNames;
if (!PluginFileCache.TryGetValue(ParentDirectory, out FileNames))
{
FileNames = new List();
DirectoryItem ParentDirectoryItem = DirectoryItem.GetItemByDirectoryReference(ParentDirectory);
if (ParentDirectoryItem.Exists)
{
using(ThreadPoolWorkQueue Queue = new ThreadPoolWorkQueue())
{
EnumeratePluginsInternal(ParentDirectoryItem, FileNames, Queue);
}
}
// Sort the filenames to ensure that the plugin order is deterministic; otherwise response files will change with each build.
FileNames = FileNames.OrderBy(x => x.FullName, StringComparer.OrdinalIgnoreCase).ToList();
PluginFileCache.Add(ParentDirectory, FileNames);
}
return FileNames;
}
///
/// Find paths to all the plugins under a given parent directory (recursively)
///
/// Parent directory to look in. Plugins will be found in any *subfolders* of this directory.
/// List of filenames. Will have all the discovered .uplugin files appended to it.
/// Queue for tasks to be executed
static void EnumeratePluginsInternal(DirectoryItem ParentDirectory, List FileNames, ThreadPoolWorkQueue Queue)
{
foreach (DirectoryItem ChildDirectory in ParentDirectory.EnumerateDirectories())
{
bool bSearchSubDirectories = true;
foreach (FileItem PluginFile in ChildDirectory.EnumerateFiles())
{
if(PluginFile.HasExtension(".uplugin"))
{
lock(FileNames)
{
FileNames.Add(PluginFile.Location);
}
bSearchSubDirectories = false;
}
}
if (bSearchSubDirectories)
{
Queue.Enqueue(() => EnumeratePluginsInternal(ChildDirectory, FileNames, Queue));
}
}
}
///
/// Determine if a plugin is enabled for a given project
///
/// The project to check. May be null.
/// Information about the plugin
/// The target platform
/// The target configuration
/// The type of target being built
/// True if the plugin should be enabled for this project
public static bool IsPluginEnabledForTarget(PluginInfo Plugin, ProjectDescriptor Project, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, TargetType TargetType)
{
if (!Plugin.Descriptor.SupportsTargetPlatform(Platform))
{
return false;
}
bool bAllowEnginePluginsEnabledByDefault = (Project == null ? true : !Project.DisableEnginePluginsByDefault);
bool bEnabled = Plugin.IsEnabledByDefault(bAllowEnginePluginsEnabledByDefault);
if (Project != null && Project.Plugins != null)
{
foreach (PluginReferenceDescriptor PluginReference in Project.Plugins)
{
if (String.Compare(PluginReference.Name, Plugin.Name, true) == 0 && !PluginReference.bOptional)
{
bEnabled = PluginReference.IsEnabledForPlatform(Platform) && PluginReference.IsEnabledForTargetConfiguration(Configuration) && PluginReference.IsEnabledForTarget(TargetType);
}
}
}
return bEnabled;
}
///
/// Determine if a plugin is enabled for a given project
///
/// The project to check. May be null.
/// Information about the plugin
/// The target platform
/// The target configuration
/// The type of target being built
/// Whether the target requires cooked data
/// True if the plugin should be enabled for this project
public static bool IsPluginCompiledForTarget(PluginInfo Plugin, ProjectDescriptor Project, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, TargetType TargetType, bool bRequiresCookedData)
{
bool bCompiledForTarget = false;
if (IsPluginEnabledForTarget(Plugin, Project, Platform, Configuration, TargetType) && Plugin.Descriptor.Modules != null)
{
bool bBuildDeveloperTools = (TargetType == TargetType.Editor || TargetType == TargetType.Program || (Configuration != UnrealTargetConfiguration.Test && Configuration != UnrealTargetConfiguration.Shipping));
foreach (ModuleDescriptor Module in Plugin.Descriptor.Modules)
{
if (Module.IsCompiledInConfiguration(Platform, Configuration, "", TargetType, bBuildDeveloperTools, bRequiresCookedData))
{
bCompiledForTarget = true;
break;
}
}
}
return bCompiledForTarget;
}
}
}