using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; namespace UnrealBuildTool { /// /// Stores information about a compiled rules assembly and the types it contains /// public class RulesAssembly { /// /// The compiled assembly /// private Assembly CompiledAssembly; /// /// All the plugins included in this assembly /// private IReadOnlyList Plugins; /// /// Maps module names to their actual xxx.Module.cs file on disk /// private Dictionary ModuleNameToModuleFile = new Dictionary(StringComparer.InvariantCultureIgnoreCase); /// /// Maps target names to their actual xxx.Target.cs file on disk /// private Dictionary TargetNameToTargetFile = new Dictionary(StringComparer.InvariantCultureIgnoreCase); /// /// Mapping from module file to its plugin info. /// private Dictionary ModuleFileToPluginInfo; /// /// Cache for whether a module has source code /// private Dictionary ModuleHasSource = new Dictionary(); /// /// The parent rules assembly that this assembly inherits. Game assemblies inherit the engine assembly, and the engine assembly inherits nothing. /// private RulesAssembly Parent; /// /// Constructor. Compiles a rules assembly from the given source files. /// /// All the plugins included in this assembly /// List of module files to compile /// List of target files to compile /// Mapping of module file to the plugin that contains it /// The output path for the compiled assembly /// The parent rules assembly public RulesAssembly(IReadOnlyList Plugins, List ModuleFiles, List TargetFiles, Dictionary ModuleFileToPluginInfo, FileReference AssemblyFileName, RulesAssembly Parent) { this.Plugins = Plugins; this.ModuleFileToPluginInfo = ModuleFileToPluginInfo; this.Parent = Parent; // Find all the source files List AssemblySourceFiles = new List(); AssemblySourceFiles.AddRange(ModuleFiles); AssemblySourceFiles.AddRange(TargetFiles); // Compile the assembly if (AssemblySourceFiles.Count > 0) { CompiledAssembly = DynamicCompilation.CompileAndLoadAssembly(AssemblyFileName, AssemblySourceFiles); } // Setup the module map foreach (FileReference ModuleFile in ModuleFiles) { string ModuleName = ModuleFile.GetFileNameWithoutAnyExtensions(); if (!ModuleNameToModuleFile.ContainsKey(ModuleName)) { ModuleNameToModuleFile.Add(ModuleName, ModuleFile); } } // Setup the target map foreach (FileReference TargetFile in TargetFiles) { string TargetName = TargetFile.GetFileNameWithoutAnyExtensions(); if (!TargetNameToTargetFile.ContainsKey(TargetName)) { TargetNameToTargetFile.Add(TargetName, TargetFile); } } // Write any deprecation warnings for methods overriden from a base with the [ObsoleteOverride] attribute. Unlike the [Obsolete] attribute, this ensures the message // is given because the method is implemented, not because it's called. if (CompiledAssembly != null) { foreach (Type CompiledType in CompiledAssembly.GetTypes()) { foreach (MethodInfo Method in CompiledType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)) { ObsoleteOverrideAttribute Attribute = Method.GetCustomAttribute(true); if (Attribute != null) { FileReference Location; if (!TryGetFileNameFromType(CompiledType, out Location)) { Location = new FileReference(CompiledAssembly.Location); } Log.TraceWarning("{0}: warning: {1}", Location, Attribute.Message); } } if(CompiledType.BaseType == typeof(ModuleRules)) { ConstructorInfo Constructor = CompiledType.GetConstructor(new Type[] { typeof(TargetInfo) }); if(Constructor != null) { FileReference Location; if (!TryGetFileNameFromType(CompiledType, out Location)) { Location = new FileReference(CompiledAssembly.Location); } Log.TraceWarning("{0}: warning: Module constructors should take a ReadOnlyTargetRules argument (rather than a TargetInfo argument) and pass it to the base class constructor from 4.15 onwards. Please update the method signature.", Location); } } } } } /// /// Fills a list with all the module names in this assembly (or its parent) /// /// List to receive the module names public void GetAllModuleNames(List ModuleNames) { if (Parent != null) { Parent.GetAllModuleNames(ModuleNames); } ModuleNames.AddRange(ModuleNameToModuleFile.Keys); } /// /// Tries to get the filename that declared the given type /// /// /// /// True if the type was found, false otherwise public bool TryGetFileNameFromType(Type ExistingType, out FileReference File) { if (ExistingType.Assembly == CompiledAssembly) { string Name = ExistingType.Name; if (ModuleNameToModuleFile.TryGetValue(Name, out File)) { return true; } string NameWithoutTarget = Name; if (NameWithoutTarget.EndsWith("Target")) { NameWithoutTarget = NameWithoutTarget.Substring(0, NameWithoutTarget.Length - 6); } if (TargetNameToTargetFile.TryGetValue(NameWithoutTarget, out File)) { return true; } } else { if (Parent != null && Parent.TryGetFileNameFromType(ExistingType, out File)) { return true; } } File = null; return false; } /// /// Gets the source file containing rules for the given module /// /// The name of the module /// The filename containing rules for this module, or an empty string if not found public FileReference GetModuleFileName(string ModuleName) { FileReference ModuleFile; if (ModuleNameToModuleFile.TryGetValue(ModuleName, out ModuleFile)) { return ModuleFile; } else { return (Parent == null) ? null : Parent.GetModuleFileName(ModuleName); } } /// /// Gets the source file containing rules for the given target /// /// The name of the target /// The filename containing rules for this target, or an empty string if not found public FileReference GetTargetFileName(string TargetName) { FileReference TargetFile; if (TargetNameToTargetFile.TryGetValue(TargetName, out TargetFile)) { return TargetFile; } else { return (Parent == null) ? null : Parent.GetTargetFileName(TargetName); } } /// /// Creates an instance of a module rules descriptor object for the specified module name /// /// Name of the module /// Information about the target associated with this module /// Compiled module rule info public ModuleRules CreateModuleRules(string ModuleName, ReadOnlyTargetRules Target) { FileReference ModuleFileName; return CreateModuleRules(ModuleName, Target, out ModuleFileName); } /// /// Creates an instance of a module rules descriptor object for the specified module name /// /// Name of the module /// Information about the target associated with this module /// The original source file name for the Module.cs file for this module /// Compiled module rule info public ModuleRules CreateModuleRules(string ModuleName, ReadOnlyTargetRules Target, out FileReference ModuleFileName) { // Currently, we expect the user's rules object type name to be the same as the module name string ModuleTypeName = ModuleName; // Make sure the module file is known to us if (!ModuleNameToModuleFile.TryGetValue(ModuleName, out ModuleFileName)) { if (Parent == null) { throw new MissingModuleException(ModuleName); } else { return Parent.CreateModuleRules(ModuleName, Target, out ModuleFileName); } } // The build module must define a type named 'Rules' that derives from our 'ModuleRules' type. Type RulesObjectType = CompiledAssembly.GetType(ModuleName); if (RulesObjectType == null) { // Temporary hack to avoid System namespace collisions // @todo projectfiles: Make rules assemblies require namespaces. RulesObjectType = CompiledAssembly.GetType("UnrealBuildTool.Rules." + ModuleName); } if (RulesObjectType == null) { throw new BuildException("Expecting to find a type to be declared in a module rules named '{0}' in {1}. This type must derive from the 'ModuleRules' type defined by Unreal Build Tool.", ModuleTypeName, CompiledAssembly.FullName); } // Create an instance of the module's rules object ModuleRules RulesObject; try { // Create an uninitialized ModuleRules object and initialize some fields on it while we're still supporting the deprecated parameterless constructor. RulesObject = (ModuleRules)FormatterServices.GetUninitializedObject(RulesObjectType); typeof(ModuleRules).GetField("Target").SetValue(RulesObject, Target); // Call the constructor ConstructorInfo Constructor = RulesObjectType.GetConstructor(new Type[] { typeof(ReadOnlyTargetRules) }); if(Constructor != null) { Constructor.Invoke(RulesObject, new object[] { Target }); } else { ConstructorInfo DeprecatedConstructor = RulesObjectType.GetConstructor(new Type[] { typeof(TargetInfo) }); if(DeprecatedConstructor == null) { throw new Exception("No valid constructor found."); } DeprecatedConstructor.Invoke(RulesObject, new object[] { new TargetInfo(Target) }); } } catch (Exception Ex) { throw new BuildException(Ex, "Unable to instantiate instance of '{0}' object type from compiled assembly '{1}'. Unreal Build Tool creates an instance of your module's 'Rules' object in order to find out about your module's requirements. The CLR exception details may provide more information: {2}", ModuleTypeName, CompiledAssembly.FullName, Ex.ToString()); } // Update the run-time dependencies path to remove $(PluginDir) and replace with a full path. When the receipt is saved it'll be converted to a $(ProjectDir) or $(EngineDir) equivalent. foreach (RuntimeDependency Dependency in RulesObject.RuntimeDependencies) { const string PluginDirVariable = "$(PluginDir)"; if (Dependency.Path.StartsWith(PluginDirVariable, StringComparison.InvariantCultureIgnoreCase)) { PluginInfo Plugin; if (ModuleFileToPluginInfo.TryGetValue(ModuleFileName, out Plugin)) { Dependency.Path = Plugin.Directory + Dependency.Path.Substring(PluginDirVariable.Length); } } } return RulesObject; } /// /// Determines whether the given module name is a game module (as opposed to an engine module) /// public bool IsGameModule(string InModuleName) { FileReference ModuleFileName = GetModuleFileName(InModuleName); return (ModuleFileName != null && !ModuleFileName.IsUnderDirectory(UnrealBuildTool.EngineDirectory)); } /// /// Construct an instance of the given target rules /// /// Type name of the target rules /// Target configuration information to pass to the constructor /// Instance of the corresponding TargetRules protected TargetRules CreateTargetRulesInstance(string TypeName, TargetInfo TargetInfo) { // The build module must define a type named 'Target' that derives from our 'TargetRules' type. Type RulesObjectType = CompiledAssembly.GetType(TypeName); if (RulesObjectType == null) { throw new BuildException("Expecting to find a type to be declared in a target rules named '{0}'. This type must derive from the 'TargetRules' type defined by Unreal Build Tool.", TypeName); } // Create an instance of the module's rules object. To avoid breaking backwards compatibility and requiring this information be passed through to the base class // constructor, we construct these objects and call the constructor manually for now. TargetRules RulesObject = (TargetRules)FormatterServices.GetUninitializedObject(RulesObjectType); typeof(TargetRules).GetField("Name").SetValue(RulesObject, TargetInfo.Name); typeof(TargetRules).GetField("Platform").SetValue(RulesObject, TargetInfo.Platform); typeof(TargetRules).GetField("Configuration").SetValue(RulesObject, TargetInfo.Configuration); typeof(TargetRules).GetField("Architecture").SetValue(RulesObject, TargetInfo.Architecture); typeof(TargetRules).GetField("ProjectFile").SetValue(RulesObject, TargetInfo.ProjectFile); // Find the constructor ConstructorInfo Constructor = RulesObjectType.GetConstructor(new Type[] { typeof(TargetInfo) }); if(Constructor == null) { throw new BuildException("No constructor found on {0} which takes an argument of type TargetInfo.", RulesObjectType.Name); } // Invoke the regular constructor try { Constructor.Invoke(RulesObject, new object[] { TargetInfo }); } catch (Exception Ex) { throw new BuildException(Ex, "Unable to instantiate instance of '{0}' object type from compiled assembly '{1}'. Unreal Build Tool creates an instance of your module's 'Rules' object in order to find out about your module's requirements. The CLR exception details may provide more information: {2}", TypeName, Path.GetFileNameWithoutExtension(CompiledAssembly.Location), Ex.ToString()); } RulesObject.SetOverridesForTargetType(); return RulesObject; } /// /// Creates a target rules object for the specified target name. /// /// Name of the target /// The platform that the target is being built for /// The configuration the target is being built for /// The architecture the target is being built for /// The project containing the target being built /// Whether this is an editor recompile, where we need to guess the name of the editor target /// The build target rules for the specified target public TargetRules CreateTargetRules(string TargetName, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, string Architecture, FileReference ProjectFile, bool bInEditorRecompile) { FileReference TargetFileName; return CreateTargetRules(TargetName, Platform, Configuration, Architecture, ProjectFile, bInEditorRecompile, out TargetFileName); } /// /// Creates a target rules object for the specified target name. /// /// Name of the target /// Platform being compiled /// Configuration being compiled /// Architecture being built /// Path to the project file for this target /// Whether this is an editor recompile, where we need to guess the name of the editor target /// The original source file name of the Target.cs file for this target /// The build target rules for the specified target public TargetRules CreateTargetRules(string TargetName, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, string Architecture, FileReference ProjectFile, bool bInEditorRecompile, out FileReference TargetFileName) { // Make sure the target file is known to us bool bFoundTargetName = TargetNameToTargetFile.ContainsKey(TargetName); if (bFoundTargetName == false) { if (Parent == null) { // throw new BuildException("Couldn't find target rules file for target '{0}' in rules assembly '{1}'.", TargetName, RulesAssembly.FullName); string ExceptionMessage = "Couldn't find target rules file for target '"; ExceptionMessage += TargetName; ExceptionMessage += "' in rules assembly '"; ExceptionMessage += CompiledAssembly.FullName; ExceptionMessage += "'." + Environment.NewLine; ExceptionMessage += "Location: " + CompiledAssembly.Location + Environment.NewLine; ExceptionMessage += "Target rules found:" + Environment.NewLine; foreach (KeyValuePair entry in TargetNameToTargetFile) { ExceptionMessage += "\t" + entry.Key + " - " + entry.Value + Environment.NewLine; } throw new BuildException(ExceptionMessage); } else { return Parent.CreateTargetRules(TargetName, Platform, Configuration, Architecture, ProjectFile, bInEditorRecompile, out TargetFileName); } } // Return the target file name to the caller TargetFileName = TargetNameToTargetFile[TargetName]; // Currently, we expect the user's rules object type name to be the same as the module name + 'Target' string TargetTypeName = TargetName + "Target"; // The build module must define a type named 'Target' that derives from our 'TargetRules' type. TargetRules RulesObject = CreateTargetRulesInstance(TargetTypeName, new TargetInfo(TargetName, Platform, Configuration, Architecture, ProjectFile)); if (bInEditorRecompile) { // Make sure this is an editor module. if (RulesObject != null) { if (RulesObject.Type != TargetType.Editor) { // Not the editor... determine the editor project string TargetSourceFolderString = TargetFileName.FullName; Int32 SourceFolderIndex = -1; if (Utils.IsRunningOnMono) { TargetSourceFolderString = TargetSourceFolderString.Replace("\\", "/"); SourceFolderIndex = TargetSourceFolderString.LastIndexOf("/Source/", StringComparison.InvariantCultureIgnoreCase); } else { TargetSourceFolderString = TargetSourceFolderString.Replace("/", "\\"); SourceFolderIndex = TargetSourceFolderString.LastIndexOf("\\Source\\", StringComparison.InvariantCultureIgnoreCase); } if (SourceFolderIndex != -1) { DirectoryReference TargetSourceFolder = new DirectoryReference(TargetSourceFolderString.Substring(0, SourceFolderIndex + 7)); foreach (KeyValuePair CheckEntry in TargetNameToTargetFile) { if (CheckEntry.Value.IsUnderDirectory(TargetSourceFolder)) { if (CheckEntry.Key.Equals(TargetName, StringComparison.InvariantCultureIgnoreCase) == false) { // We have found a target in the same source folder that is not the original target found. // See if it is the editor project string CheckTargetTypeName = CheckEntry.Key + "Target"; TargetRules CheckRulesObject = CreateTargetRulesInstance(CheckTargetTypeName, new TargetInfo(CheckEntry.Key, Platform, Configuration, Architecture, ProjectFile)); if (CheckRulesObject != null) { if (CheckRulesObject.Type == TargetType.Editor) { // Found it // NOTE: This prevents multiple Editor targets from co-existing... RulesObject = CheckRulesObject; break; } } } } } } } } } return RulesObject; } /// /// Enumerates all the plugins that are available /// /// public IEnumerable EnumeratePlugins() { return global::UnrealBuildTool.Plugins.FilterPlugins(EnumeratePluginsInternal()); } /// /// Enumerates all the plugins that are available /// /// protected IEnumerable EnumeratePluginsInternal() { if (Parent == null) { return Plugins; } else { return Plugins.Concat(Parent.EnumeratePluginsInternal()); } } /// /// Tries to find the PluginInfo associated with a given module file /// /// The module to search for /// The matching plugin info, or null. /// True if the module belongs to a plugin public bool TryGetPluginForModule(FileReference ModuleFile, out PluginInfo Plugin) { if (ModuleFileToPluginInfo.TryGetValue(ModuleFile, out Plugin)) { return true; } else { return (Parent == null) ? false : Parent.TryGetPluginForModule(ModuleFile, out Plugin); } } /// /// Determines if a module in this rules assembly has source code. /// /// Name of the module to check /// True if the module has source files, false if the module was not found, or does not have source files. public bool DoesModuleHaveSource(string ModuleName) { FileReference ModuleFile; if (ModuleNameToModuleFile.TryGetValue(ModuleName, out ModuleFile)) { bool HasSource; if (!ModuleHasSource.TryGetValue(ModuleFile, out HasSource)) { foreach (string FileName in Directory.EnumerateFiles(ModuleFile.Directory.FullName, "*.cpp", SearchOption.AllDirectories)) { HasSource = true; break; } ModuleHasSource.Add(ModuleFile, HasSource); } return HasSource; } return (Parent == null) ? false : Parent.DoesModuleHaveSource(ModuleName); } } }