// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using Tools.DotNETCommon; using UnrealBuildTool; namespace UnrealBuildTool { /// /// Caches config files and config file hierarchies /// public static class ConfigCache { /// /// Delegate to add a value to an ICollection in a target object /// /// The object containing the field to be modified /// The value to add delegate void AddElementDelegate(object TargetObject, object ValueObject); /// /// Caches information about a field with a [ConfigFile] attribute in a type /// class ConfigField { /// /// The field with the config attribute /// public FieldInfo FieldInfo; /// /// The attribute instance /// public ConfigFileAttribute Attribute; /// /// For fields implementing ICollection, specifies the element type /// public Type ElementType; /// /// For fields implementing ICollection, a callback to add an element type. /// public AddElementDelegate AddElement; } /// /// Stores information identifying a unique config hierarchy /// class ConfigHierarchyKey { /// /// The hierarchy type /// public ConfigHierarchyType Type; /// /// The project directory to read from /// public DirectoryReference ProjectDir; /// /// Which platform-specific files to read /// public UnrealTargetPlatform Platform; /// /// Constructor /// /// The hierarchy type /// The project directory to read from /// Which platform-specific files to read public ConfigHierarchyKey(ConfigHierarchyType Type, DirectoryReference ProjectDir, UnrealTargetPlatform Platform) { this.Type = Type; this.ProjectDir = ProjectDir; this.Platform = Platform; } /// /// Test whether this key is equal to another object /// /// The object to compare against /// True if the objects match, false otherwise public override bool Equals(object Other) { ConfigHierarchyKey OtherKey = Other as ConfigHierarchyKey; return (OtherKey != null && OtherKey.Type == Type && OtherKey.ProjectDir == ProjectDir && OtherKey.Platform == Platform); } /// /// Returns a stable hash of this object /// /// Hash value for this object public override int GetHashCode() { return ((ProjectDir != null)? ProjectDir.GetHashCode() : 0) + ((int)Type * 123) + ((int)Platform * 345); } } /// /// Stores the number of config file types /// static readonly int NumConfigFileTypes = (int)((ConfigHierarchyType[])Enum.GetValues(typeof(ConfigHierarchyType))).Last() + 1; /// /// Cache of individual config files /// static Dictionary LocationToConfigFile = new Dictionary(); /// /// Cache of config hierarchies by project /// static Dictionary HierarchyKeyToHierarchy = new Dictionary(); /// /// Cache of config fields by type /// static Dictionary> TypeToConfigFields = new Dictionary>(); /// /// Attempts to read a config file (or retrieve it from the cache) /// /// Location of the file to read /// On success, receives the parsed config file /// True if the file exists and was read, false otherwise internal static bool TryReadFile(FileReference Location, out ConfigFile ConfigFile) { lock (LocationToConfigFile) { if (!LocationToConfigFile.TryGetValue(Location, out ConfigFile)) { if (FileReference.Exists(Location)) { ConfigFile = new ConfigFile(Location); } if (ConfigFile != null) { LocationToConfigFile.Add(Location, ConfigFile); } } } return ConfigFile != null; } /// /// Reads a config hierarchy (or retrieve it from the cache) /// /// The type of hierarchy to read /// The project directory to read the hierarchy for /// Which platform to read platform-specific config files for /// The requested config hierarchy public static ConfigHierarchy ReadHierarchy(ConfigHierarchyType Type, DirectoryReference ProjectDir, UnrealTargetPlatform Platform) { // Get the key to use for the cache. It cannot be null, so we use the engine directory if a project directory is not given. ConfigHierarchyKey Key = new ConfigHierarchyKey(Type, ProjectDir, Platform); // Try to get the cached hierarchy with this key ConfigHierarchy Hierarchy; lock (HierarchyKeyToHierarchy) { if (!HierarchyKeyToHierarchy.TryGetValue(Key, out Hierarchy)) { // Find all the input files List Files = new List(); foreach (FileReference IniFileName in ConfigHierarchy.EnumerateConfigFileLocations(Type, ProjectDir, Platform)) { ConfigFile File; if (TryReadFile(IniFileName, out File)) { Files.Add(File); } } // Handle command line overrides string[] CmdLine = Environment.GetCommandLineArgs(); string IniConfigArgPrefix = "-ini:" + Enum.GetName(typeof(ConfigHierarchyType), Type) + ":"; foreach (string CmdLineArg in CmdLine) { if (CmdLineArg.StartsWith(IniConfigArgPrefix)) { ConfigFile OverrideFile = new ConfigFile(CmdLineArg.Substring(IniConfigArgPrefix.Length)); Files.Add(OverrideFile); } } // Create the hierarchy Hierarchy = new ConfigHierarchy(Files); HierarchyKeyToHierarchy.Add(Key, Hierarchy); } } return Hierarchy; } /// /// Gets a list of ConfigFields for the given type /// /// Type to get configurable fields for /// List of config fields for the given type static List FindConfigFieldsForType(Type TargetObjectType) { List Fields; lock(TypeToConfigFields) { if (!TypeToConfigFields.TryGetValue(TargetObjectType, out Fields)) { Fields = new List(); if(TargetObjectType.BaseType != null) { Fields.AddRange(FindConfigFieldsForType(TargetObjectType.BaseType)); } foreach (FieldInfo FieldInfo in TargetObjectType.GetFields(BindingFlags.Instance | BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) { IEnumerable Attributes = FieldInfo.GetCustomAttributes(); foreach (ConfigFileAttribute Attribute in Attributes) { // Copy the field ConfigField Setter = new ConfigField(); Setter.FieldInfo = FieldInfo; Setter.Attribute = Attribute; // Check if the field type implements ICollection<>. If so, we can take multiple values. foreach (Type InterfaceType in FieldInfo.FieldType.GetInterfaces()) { if (InterfaceType.IsGenericType && InterfaceType.GetGenericTypeDefinition() == typeof(ICollection<>)) { MethodInfo MethodInfo = InterfaceType.GetRuntimeMethod("Add", new Type[] { InterfaceType.GenericTypeArguments[0] }); Setter.AddElement = (Target, Value) => { MethodInfo.Invoke(Setter.FieldInfo.GetValue(Target), new object[] { Value }); }; Setter.ElementType = InterfaceType.GenericTypeArguments[0]; break; } } // Add it to the output list Fields.Add(Setter); } } TypeToConfigFields.Add(TargetObjectType, Fields); } } return Fields; } /// /// Read config settings for the given object /// /// Path to the project directory /// The platform being built /// Object to receive the settings public static void ReadSettings(DirectoryReference ProjectDir, UnrealTargetPlatform Platform, object TargetObject) { List Fields = FindConfigFieldsForType(TargetObject.GetType()); foreach(ConfigField Field in Fields) { // Read the hierarchy listed ConfigHierarchy Hierarchy = ReadHierarchy(Field.Attribute.ConfigType, ProjectDir, Platform); // Parse the values from the config files and update the target object if(Field.AddElement == null) { string Text; if(Hierarchy.TryGetValue(Field.Attribute.SectionName, Field.Attribute.KeyName ?? Field.FieldInfo.Name, out Text)) { object Value; if(TryParseValue(Text, Field.FieldInfo.FieldType, out Value)) { Field.FieldInfo.SetValue(TargetObject, Value); } } } else { IEnumerable Items; if(Hierarchy.TryGetValues(Field.Attribute.SectionName, Field.Attribute.KeyName ?? Field.FieldInfo.Name, out Items)) { foreach(string Item in Items) { object Value; if(TryParseValue(Item, Field.ElementType, out Value)) { Field.AddElement(TargetObject, Value); } } } } } } /// /// Attempts to parse the given text into an object which matches a specific field type /// /// The text to parse /// The type of field to parse /// If successful, a value of type 'FieldType' /// True if the value could be parsed, false otherwise public static bool TryParseValue(string Text, Type FieldType, out object Value) { if(FieldType == typeof(string)) { Value = Text; return true; } else if(FieldType == typeof(bool)) { bool BoolValue; if(ConfigHierarchy.TryParse(Text, out BoolValue)) { Value = BoolValue; return true; } else { Value = null; return false; } } else if(FieldType == typeof(int)) { int IntValue; if(ConfigHierarchy.TryParse(Text, out IntValue)) { Value = IntValue; return true; } else { Value = null; return false; } } else if(FieldType == typeof(float)) { float FloatValue; if(ConfigHierarchy.TryParse(Text, out FloatValue)) { Value = FloatValue; return true; } else { Value = null; return false; } } else if(FieldType == typeof(double)) { double DoubleValue; if(ConfigHierarchy.TryParse(Text, out DoubleValue)) { Value = DoubleValue; return true; } else { Value = null; return false; } } else if(FieldType == typeof(Guid)) { Guid GuidValue; if(ConfigHierarchy.TryParse(Text, out GuidValue)) { Value = GuidValue; return true; } else { Value = null; return false; } } else if(FieldType.IsEnum) { try { Value = Enum.Parse(FieldType, Text); return true; } catch { Value = null; return false; } } else if(FieldType.GetGenericTypeDefinition() == typeof(Nullable<>)) { return TryParseValue(Text, FieldType.GetGenericArguments()[0], out Value); } else { throw new Exception("Unsupported type for [ConfigFile] attribute"); } } } }