// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using EpicGames.Core; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; 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 member with a [ConfigFile] attribute in a type /// abstract class ConfigMember { /// /// 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; /// /// Constructor /// /// public ConfigMember(ConfigFileAttribute Attribute) { this.Attribute = Attribute; } /// /// Returns Reflection.MemberInfo describing the target class member. /// public abstract MemberInfo MemberInfo { get; } /// /// Returns Reflection.Type of the target class member. /// public abstract Type Type { get; } /// /// Returns the value setter of the target class member. /// public abstract Action SetValue { get; } /// /// Returns the value getter of the target class member. /// public abstract Func GetValue { get; } } /// /// Caches information about a field with a [ConfigFile] attribute in a type /// class ConfigField : ConfigMember { /// /// Reflection description of the field with the config attribute. /// private FieldInfo FieldInfo; /// /// Constructor /// /// /// public ConfigField(FieldInfo FieldInfo, ConfigFileAttribute Attribute) : base(Attribute) { this.FieldInfo = FieldInfo; } public override MemberInfo MemberInfo => FieldInfo; public override Type Type => FieldInfo.FieldType; public override Action SetValue => FieldInfo.SetValue; public override Func GetValue => FieldInfo.GetValue; } /// /// Caches information about a property with a [ConfigFile] attribute in a type /// class ConfigProperty : ConfigMember { /// /// Reflection description of the property with the config attribute. /// private PropertyInfo PropertyInfo; /// /// Constructor /// /// /// public ConfigProperty(PropertyInfo PropertyInfo, ConfigFileAttribute Attribute) : base(Attribute) { this.PropertyInfo = PropertyInfo; } public override MemberInfo MemberInfo => PropertyInfo; public override Type Type => PropertyInfo.PropertyType; public override Action SetValue => PropertyInfo.SetValue; public override Func GetValue => PropertyInfo.GetValue; } /// /// Allowed modification types allowed for default config files /// public enum ConfigDefaultUpdateType { /// /// Used for non-array types, this will replace a setting, or it will add a setting if it doesn't exist /// SetValue, /// /// Used to add an array entry to the end of any existing array entries, or will add to the end of the section /// AddArrayEntry, } /// /// 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; /// /// Custom config subdirectory to load /// public string CustomConfig; /// /// Custom override strings /// public List OverrideStrings; /// /// A hotfix directory where modifications/additions to config files can be read from /// public DirectoryReference? HotfixDir; /// /// If specified, this hierarchy will include plugins enabled for the given target type /// public TargetType? IncludePluginsForTargetType; /// /// Constructor /// /// The hierarchy type /// The project directory to read from /// Which platform-specific files to read /// Custom config subdirectory to load /// Custom override strings /// A hotfix directory where modifications/additions to config files can be read from /// If specified, this hierarchy will unclude plugins enabled for the given target type public ConfigHierarchyKey(ConfigHierarchyType Type, DirectoryReference? ProjectDir, UnrealTargetPlatform Platform, string CustomConfig, List OverrideStrings, DirectoryReference? HotfixDir, TargetType? IncludePluginsForTargetType) { this.Type = Type; this.ProjectDir = ProjectDir; this.Platform = Platform; this.CustomConfig = CustomConfig; this.OverrideStrings = OverrideStrings; this.HotfixDir = HotfixDir; this.IncludePluginsForTargetType = IncludePluginsForTargetType; } /// /// 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 && OtherKey.CustomConfig == CustomConfig && OtherKey.OverrideStrings.SequenceEqual(OverrideStrings) && OtherKey.HotfixDir == HotfixDir && OtherKey.IncludePluginsForTargetType == IncludePluginsForTargetType; } /// /// Returns a stable hash of this object /// /// Hash value for this object public override int GetHashCode() { int Hash = 17; Hash = (Hash * 31) + Type.GetHashCode(); Hash = (Hash * 31) + ((ProjectDir == null) ? 0 : ProjectDir.GetHashCode()); Hash = (Hash * 31) + Platform.GetHashCode(); Hash = (Hash * 31) + CustomConfig.GetHashCode(); foreach (string OverrideString in OverrideStrings) { Hash = (Hash * 31) + OverrideString.GetHashCode(); } Hash = (Hash * 31) + ((HotfixDir == null) ? 0 : HotfixDir.GetHashCode()); Hash = (Hash * 31) + ((IncludePluginsForTargetType == null) ? 0 : IncludePluginsForTargetType.GetHashCode()); return Hash; } } /// /// Cache of individual config files /// static ConcurrentDictionary LocationToConfigFile = new ConcurrentDictionary(); /// /// Cache of config hierarchies by project /// static ConcurrentDictionary HierarchyKeyToHierarchy = new ConcurrentDictionary(); /// /// Cache of config fields by type /// static ConcurrentDictionary> TypeToConfigMembers = new ConcurrentDictionary>(); /// /// 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, [NotNullWhen(true)] out ConfigFile? ConfigFile) { ConfigFile = LocationToConfigFile.GetOrAdd(Location, _ => { if (FileReference.Exists(Location)) { return new ConfigFile(Location); } // Note: it's important to keep the null value here so we don't keep checking for files that don't exist return null; }); 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 /// Optional override config directory to search, for support of multiple target types /// Optional list of command line arguments added to the existing command line arguments /// A hotfix directory where modifications/additions to config files can be read from /// If specified, this hierarchy will unclude plugins enabled for the given target type /// The requested config hierarchy public static ConfigHierarchy ReadHierarchy(ConfigHierarchyType Type, DirectoryReference? ProjectDir, UnrealTargetPlatform Platform, string CustomConfig = "", string[]? CustomArgs = null, DirectoryReference? HotfixDir = null, TargetType? IncludePluginsForTargetType = null) { CommandLineArguments CombinedArgs; if (CustomArgs != null) { List CommandLineArgs = new List(Environment.GetCommandLineArgs()); foreach (string CustomArg in CustomArgs) { if (CustomArg.StartsWith("-", StringComparison.InvariantCultureIgnoreCase)) { CommandLineArgs.Add(CustomArg); } else { CommandLineArgs.Add("-" + CustomArg); } } CombinedArgs = new CommandLineArguments(CommandLineArgs.ToArray()); } else { CombinedArgs = new CommandLineArguments(Environment.GetCommandLineArgs()); } return ReadHierarchy(Type, ProjectDir, Platform, CustomConfig, CombinedArgs, HotfixDir, IncludePluginsForTargetType); } /// /// 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 /// Optional override config directory to search, for support of multiple target types /// The command line arguments to parse /// A hotfix directory where modifications/additions to config files can be read from /// If specified, this hierarchy will unclude plugins enabled for the given target type /// The requested config hierarchy public static ConfigHierarchy ReadHierarchy(ConfigHierarchyType Type, DirectoryReference? ProjectDir, UnrealTargetPlatform Platform, string CustomConfig, CommandLineArguments CmdLineArgs, DirectoryReference? HotfixDir = null, TargetType? IncludePluginsForTargetType = null) { // Handle command line overrides List OverrideStrings = new List(); string IniConfigArgPrefix = "-ini:" + Enum.GetName(typeof(ConfigHierarchyType), Type) + ":"; string CustomConfigPrefix = "-CustomConfig="; bool bDumpIniLoads = false; foreach (string CmdLineArg in CmdLineArgs) { if (CmdLineArg.StartsWith(IniConfigArgPrefix, StringComparison.InvariantCultureIgnoreCase)) { OverrideStrings.Add(CmdLineArg.Substring(IniConfigArgPrefix.Length)); } if (CmdLineArg.StartsWith(CustomConfigPrefix, StringComparison.InvariantCultureIgnoreCase)) { CustomConfig = CmdLineArg.Substring(CustomConfigPrefix.Length); } if (CmdLineArg.Equals("-dumpiniloads", StringComparison.InvariantCultureIgnoreCase)) { bDumpIniLoads = true; } } CustomConfig ??= String.Empty; // 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, CustomConfig, OverrideStrings, HotfixDir, IncludePluginsForTargetType); ILogger Logger = NullLogger.Instance; if (bDumpIniLoads) { Logger = Log.Logger; } // Try to get the cached hierarchy with this key Logger.LogInformation($"Requested Hierarchy: {Type},{ProjectDir},{Platform},{CustomConfig}"); return HierarchyKeyToHierarchy.GetOrAdd(Key, _ => { // Find all the input files List Files = new List(); foreach (FileReference IniFileName in ConfigHierarchy.EnumerateConfigFileLocations(Type, ProjectDir, Platform, CustomConfig, IncludePluginsForTargetType)) { ConfigFile? File; Logger.LogInformation($"Trying to load: {IniFileName}"); if (TryReadFile(IniFileName, out File)) { Logger.LogInformation($" Found!"); Files.Add(File); } if (HotfixDir != null) { FileReference HotfixFile = FileReference.Combine(HotfixDir, IniFileName.GetFileName()); if (TryReadFile(HotfixFile, out File)) { Logger.LogInformation($" Hotfixed with '{HotfixFile}'"); Files.Add(File); } } } foreach (string OverrideString in OverrideStrings) { ConfigFile OverrideFile = new ConfigFile(OverrideString); Files.Add(OverrideFile); Logger.LogInformation($"Trying to load override: {OverrideFile}"); } // Create the hierarchy return new ConfigHierarchy(Files); }); } /// /// 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 FindConfigMembersForType(Type TargetObjectType) { return TypeToConfigMembers.GetOrAdd(TargetObjectType, _ => { List Members = new List(); if (TargetObjectType.BaseType != null) { Members.AddRange(FindConfigMembersForType(TargetObjectType.BaseType)); } foreach (FieldInfo FieldInfo in TargetObjectType.GetFields(BindingFlags.Instance | BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) { ProcessConfigTypeMember(TargetObjectType, FieldInfo, Members, (FieldInfo, Attribute) => new ConfigField(FieldInfo, Attribute)); } foreach (PropertyInfo PropertyInfo in TargetObjectType.GetProperties(BindingFlags.Instance | BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) { ProcessConfigTypeMember(TargetObjectType, PropertyInfo, Members, (PropertyInfo, Attribute) => new ConfigProperty(PropertyInfo, Attribute)); } return Members; }); } static void ProcessConfigTypeMember(Type TargetType, MEMBER MemberInfo, List Members, Func CreateConfigMember) where MEMBER : System.Reflection.MemberInfo { IEnumerable Attributes = MemberInfo.GetCustomAttributes(); foreach (ConfigFileAttribute Attribute in Attributes) { // Copy the field ConfigMember Setter = CreateConfigMember(MemberInfo, Attribute); // Check if the field type implements ICollection<>. If so, we can take multiple values. foreach (Type InterfaceType in Setter.Type.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.GetValue(Target), new object?[] { Value }); }; Setter.ElementType = InterfaceType.GenericTypeArguments[0]; break; } } // Add it to the output list Members.Add(Setter); } } /// /// Read config settings for the given object /// /// Path to the project directory /// The platform being built /// Object to receive the settings /// Optional override config directory to search, for support of multiple target types public static void ReadSettings(DirectoryReference? ProjectDir, UnrealTargetPlatform Platform, object TargetObject, string CustomConfig = "") { ReadSettings(ProjectDir, Platform, TargetObject, null, null, CustomConfig); } /// /// Read config settings for the given object /// /// Path to the project directory /// The platform being built /// Object to receive the settings /// Will be populated with config values that were retrieved. May be null. /// Command line arguments, if null the application's command line arguments will be used /// Optional override config directory to search, for support of multiple target types internal static void ReadSettings(DirectoryReference? ProjectDir, UnrealTargetPlatform Platform, object TargetObject, Dictionary?>? ConfigValues, CommandLineArguments? CmdLineArgs, string CustomConfig = "") { List Members = FindConfigMembersForType(TargetObject.GetType()); foreach (ConfigMember Member in Members) { // Read the hierarchy listed ConfigHierarchy Hierarchy; if (CmdLineArgs == null) { Hierarchy = ReadHierarchy(Member.Attribute.ConfigType, ProjectDir, Platform, CustomConfig); } else { Hierarchy = ReadHierarchy(Member.Attribute.ConfigType, ProjectDir, Platform, CustomConfig, CmdLineArgs); } // Get the key name string KeyName = Member.Attribute.KeyName ?? Member.MemberInfo.Name; // Get the value(s) associated with this key IReadOnlyList? Values; Hierarchy.TryGetValues(Member.Attribute.SectionName, KeyName, out Values); // Parse the values from the config files and update the target object if (Member.AddElement == null) { if (Values != null && Values.Count == 1) { object? Value; if (TryParseValue(Values[0], Member.Type, out Value)) { Member.SetValue(TargetObject, Value); } } } else { if (Values != null) { foreach (string Item in Values) { object? Value; if (TryParseValue(Item, Member.ElementType!, out Value)) { Member.AddElement(TargetObject, Value); } } } } // Save the dependency if (ConfigValues != null) { ConfigDependencyKey Key = new ConfigDependencyKey(Member.Attribute.ConfigType, ProjectDir, Platform, Member.Attribute.SectionName, KeyName); ConfigValues[Key] = Values; } } } /// /// 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"); } } #region Updating Default Config file support /// /// Calculates the path to where the project's Default config of the type given (ie DefaultEngine.ini) /// /// Game, Engine, etc /// Project directory, used to find Config/Default[Type].ini public static FileReference GetDefaultConfigFileReference(ConfigHierarchyType ConfigType, DirectoryReference ProjectDir) { return FileReference.Combine(ProjectDir, "Config", $"Default{ConfigType}.ini"); } /// /// Calculates the path to where the project's platform specific config of the type given (ie DefaultEngine.ini) /// /// Game, Engine, etc /// Project directory, used to find Config/Default[Type].ini /// Platform name public static FileReference GetPlatformConfigFileReference(ConfigHierarchyType ConfigType, DirectoryReference ProjectDir, string Platform) { return FileReference.Combine(ProjectDir, "Platforms", Platform, "Config", $"{Platform}{ConfigType}.ini"); } /// /// Updates a section in a Default***.ini, and will write it out. If the file is not writable, p4 can attempt to check it out. /// /// Game, Engine, etc /// Project directory, used to find Config/Default[Type].ini /// How to modify the secion /// Name of the section with the Key in it /// Key to update /// Value to write for te Key /// Logger for output /// public static bool WriteSettingToDefaultConfig(ConfigHierarchyType ConfigType, DirectoryReference ProjectDir, ConfigDefaultUpdateType UpdateType, string Section, string Key, string Value, ILogger Logger) { FileReference DefaultConfigFile = GetDefaultConfigFileReference(ConfigType, ProjectDir); return WriteSettingToConfigFile(DefaultConfigFile, UpdateType, Section, Key, Value, Logger); } /// /// Updates a section in a Default***.ini, and will write it out. If the file is not writable, p4 can attempt to check it out. /// /// Game, Engine, etc /// Project directory, used to find Config/Default[Type].ini /// Platform name. Must have a directory under projects Platforms subdirectory /// How to modify the secion /// Name of the section with the Key in it /// Key to update /// Value to write for te Key /// Logger for output /// public static bool WriteSettingToPlatformConfigFile(ConfigHierarchyType ConfigType, DirectoryReference ProjectDir, string Platform, ConfigDefaultUpdateType UpdateType, string Section, string Key, string Value, ILogger Logger) { FileReference PlatformConfigFile = GetPlatformConfigFileReference(ConfigType, ProjectDir, Platform); return WriteSettingToConfigFile(PlatformConfigFile, UpdateType, Section, Key, Value, Logger); } /// /// Updates a section in config file, and will write it out. If the file is not writable, p4 can attempt to check it out. /// /// Config file (.ini) name /// How to modify the secion /// Name of the section with the Key in it /// Key to update /// Value to write for te Key /// Logger for output /// public static bool WriteSettingToConfigFile(FileReference ConfigFile, ConfigDefaultUpdateType UpdateType, string Section, string Key, string Value, ILogger Logger) { if (!FileReference.Exists(ConfigFile)) { Logger.LogWarning("Failed to find config file '{DefaultConfigFile}' to update", ConfigFile); return false; } if (File.GetAttributes(ConfigFile.FullName).HasFlag(FileAttributes.ReadOnly)) { Logger.LogWarning("Config file '{ConfigFile}' is read-only, unable to write setting {Key}", ConfigFile.FullName, Key); return false; } // generate the section header string SectionString = $"[{Section}]"; // genrate the line we are going to write to the config string KeyWithEquals = $"{Key}="; string LineToWrite = (UpdateType == ConfigDefaultUpdateType.AddArrayEntry ? "+" : ""); LineToWrite += KeyWithEquals + Value; // read in all the lines so we can insert or replace one List Lines = File.ReadAllLines(ConfigFile.FullName).ToList(); // look for the section int SectionIndex = -1; for (int Index = 0; Index < Lines.Count; Index++) { if (Lines[Index].Trim().Equals(SectionString, StringComparison.InvariantCultureIgnoreCase)) { SectionIndex = Index; break; } } // if section not found, just append to the end if (SectionIndex == -1) { Lines.Add(SectionString); Lines.Add(LineToWrite); File.WriteAllLines(ConfigFile.FullName, Lines); return true; } // find the last line in the section with the prefix int LastIndexOfPrefix = -1; int NextSectionIndex = -1; for (int Index = SectionIndex + 1; Index < Lines.Count; Index++) { string Line = Lines[Index]; if (Line.StartsWith('+') || Line.StartsWith('-') || Line.StartsWith('.') || Line.StartsWith('!')) { Line = Line.Substring(1); } // look for last array entry in case of multiples (or the only line for non-array type) if (Line.StartsWith(KeyWithEquals, StringComparison.InvariantCultureIgnoreCase)) { LastIndexOfPrefix = Index; } else if (Lines[Index].StartsWith("[")) { NextSectionIndex = Index; // we found another section, so break out break; } } // now we know enough to either insert or replace a line // if we never found the key, we will insert at the end of the section if (LastIndexOfPrefix == -1) { // if we didn't find a next section, thjen we will insert at the end of the file if (NextSectionIndex == -1) { NextSectionIndex = Lines.Count; } // move past blank lines between sections while (String.IsNullOrWhiteSpace(Lines[NextSectionIndex - 1])) { NextSectionIndex--; } // insert before the next section (or end of file) Lines.Insert(NextSectionIndex, LineToWrite); } // otherwise, insert after, or replace, a line, depending on type else { if (UpdateType == ConfigDefaultUpdateType.AddArrayEntry) { Lines.Insert(LastIndexOfPrefix + 1, LineToWrite); } else { Lines[LastIndexOfPrefix] = LineToWrite; } } // now the lines are updated, we can overwrite the file File.WriteAllLines(ConfigFile.FullName, Lines); return true; } /// /// Invalidates the hierarchy internal caches so that next call to ReadHierarchy will re-read from disk /// but existing ones will still be valid with old values /// public static void InvalidateCaches() { lock (LocationToConfigFile) { LocationToConfigFile.Clear(); } lock (HierarchyKeyToHierarchy) { HierarchyKeyToHierarchy.Clear(); } } #endregion } }