// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; namespace UnrealBuildTool { /// /// Exception when parsing ini files /// public class IniParsingException : Exception { public IniParsingException(string Message) : base(Message) { } public IniParsingException(string Format, params object[] Args) : base(String.Format(Format, Args)) { } } /// /// Equivalent of FConfigCacheIni. Parses ini files. /// public class ConfigCacheIni { // command class for being able to create config caches over and over without needing to read the ini files public class Command { public string TrimmedLine; } class SectionCommand : Command { public FileReference Filename; public int LineIndex; } class KeyValueCommand : Command { public string Key; public string Value; public ParseAction LastAction; } // cached ini files static Dictionary> FileCache = new Dictionary>(); static Dictionary IniCache = new Dictionary(); static Dictionary BaseIniCache = new Dictionary(); static List RequiredSections = new List(){"AppxManifest", "CommonSettings", "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "/Script/AndroidPlatformEditor.AndroidSDKSettings", "/Script/BuildSettings.BuildSettings", "/Script/IOSRuntimeSettings.IOSRuntimeSettings", "/Script/WindowsTargetPlatform.WindowsTargetSettings", "/Script/UnrealEd.ProjectPackagingSettings", "/Script/PS4PlatformEditor.PS4TargetSettings", "/Script/XboxOneTargetPlatform.XboxOneTargetSettings", "/Script/HTML5PlatformEditor.HTML5TargetSettings"}; // static creation functions for ini files public static ConfigCacheIni CreateConfigCacheIni(UnrealTargetPlatform Platform, string BaseIniName, DirectoryReference ProjectDirectory, DirectoryReference EngineDirectory = null) { if (EngineDirectory == null) { EngineDirectory = UnrealBuildTool.EngineDirectory; } // cache base ini for use as the seed for the rest if (!BaseIniCache.ContainsKey(BaseIniName)) { BaseIniCache.Add(BaseIniName, new ConfigCacheIni(UnrealTargetPlatform.Unknown, BaseIniName, null, EngineDirectory, EngineOnly: true)); } // build the new ini and cache it for later re-use ConfigCacheIni BaseCache = BaseIniCache[BaseIniName]; string Key = GetIniPlatformName(Platform) + BaseIniName + EngineDirectory.FullName + (ProjectDirectory != null ? ProjectDirectory.FullName : ""); if (!IniCache.ContainsKey(Key)) { IniCache.Add(Key, new ConfigCacheIni(Platform, BaseIniName, ProjectDirectory, EngineDirectory, BaseCache: BaseCache)); } return IniCache[Key]; } /// /// List of values (or a single value) /// public class IniValues : List { public override string ToString() { return String.Join(",", ToArray()); } } /// /// Ini section (map of keys and values) /// public class IniSection : Dictionary { public IniSection() : base(StringComparer.InvariantCultureIgnoreCase) { } public override string ToString() { return "IniSection"; } } /// /// True if we are loading a hierarchy of config files that should be merged together /// bool bIsMergingConfigs; /// /// All sections parsed from ini file /// Dictionary Sections; /// /// Constructor. Parses a single ini file. No Platform settings, no engine hierarchy. Do not use this with ini files that have hierarchy! /// /// The ini file to load public ConfigCacheIni(FileReference Filename) { Init(Filename); } /// /// Constructor. Parses ini hierarchy for the specified project. No Platform settings. /// /// Project path /// Target platform /// Ini name (Engine, Editor, etc) public ConfigCacheIni(string BaseIniName, string ProjectDirectory, string EngineDirectory = null) { Init(UnrealTargetPlatform.Unknown, BaseIniName, (ProjectDirectory == null) ? null : new DirectoryReference(ProjectDirectory), (EngineDirectory == null) ? null : new DirectoryReference(EngineDirectory)); } /// /// Constructor. Parses ini hierarchy for the specified project. No Platform settings. /// /// Project path /// Target platform /// Ini name (Engine, Editor, etc) public ConfigCacheIni(string BaseIniName, DirectoryReference ProjectDirectory, DirectoryReference EngineDirectory = null) { Init(UnrealTargetPlatform.Unknown, BaseIniName, ProjectDirectory, EngineDirectory); } /// /// Constructor. Parses ini hierarchy for the specified platform and project. /// /// Project path /// Target platform /// Ini name (Engine, Editor, etc) public ConfigCacheIni(UnrealTargetPlatform Platform, string BaseIniName, string ProjectDirectory, string EngineDirectory = null) { Init(Platform, BaseIniName, (ProjectDirectory == null) ? null : new DirectoryReference(ProjectDirectory), (EngineDirectory == null) ? null : new DirectoryReference(EngineDirectory)); } /// /// Constructor. Parses ini hierarchy for the specified platform and project. /// /// Project path /// Target platform /// Ini name (Engine, Editor, etc) public ConfigCacheIni(UnrealTargetPlatform Platform, string BaseIniName, DirectoryReference ProjectDirectory, DirectoryReference EngineDirectory = null, bool EngineOnly = false, ConfigCacheIni BaseCache = null) { Init(Platform, BaseIniName, ProjectDirectory, EngineDirectory, EngineOnly, BaseCache); } private void InitCommon() { Sections = new Dictionary(StringComparer.InvariantCultureIgnoreCase); } private void Init(FileReference IniFileName) { InitCommon(); bIsMergingConfigs = false; ParseIniFile(IniFileName); } private void Init(UnrealTargetPlatform Platform, string BaseIniName, DirectoryReference ProjectDirectory, DirectoryReference EngineDirectory, bool EngineOnly = false, ConfigCacheIni BaseCache = null) { InitCommon(); bIsMergingConfigs = true; if (EngineDirectory == null) { EngineDirectory = UnrealBuildTool.EngineDirectory; } if (BaseCache != null) { foreach (var Pair in BaseCache.Sections) { Sections.Add(Pair.Key, Pair.Value); } } if (EngineOnly) { foreach (var IniFileName in EnumerateEngineIniFileNames(EngineDirectory, BaseIniName)) { if (IniFileName.Exists()) { ParseIniFile(IniFileName); } } } else { foreach (var IniFileName in EnumerateCrossPlatformIniFileNames(ProjectDirectory, EngineDirectory, Platform, BaseIniName, BaseCache != null)) { if (IniFileName.Exists()) { ParseIniFile(IniFileName); } } } } /// /// Finds a section in INI /// /// /// Found section or null public IniSection FindSection(string SectionName) { IniSection Section; Sections.TryGetValue(SectionName, out Section); return Section; } /// /// Finds values associated with the specified key (does not copy the list) /// private bool GetList(string SectionName, string Key, out IniValues Value) { bool Result = false; var Section = FindSection(SectionName); Value = null; if (Section != null) { if (Section.TryGetValue(Key, out Value)) { Result = true; } } return Result; } /// /// Gets all values associated with the specified key /// /// Section where the key is located /// Key name /// Copy of the list containing all values associated with the specified key /// True if the key exists public bool GetArray(string SectionName, string Key, out List Value) { Value = null; IniValues ValueList; bool Result = GetList(SectionName, Key, out ValueList); if (Result) { Value = new List(ValueList); } return Result; } /// /// Gets a single string value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetString(string SectionName, string Key, out string Value) { Value = String.Empty; IniValues ValueList; bool Result = GetList(SectionName, Key, out ValueList); if (Result && ValueList != null && ValueList.Count > 0) { Value = ValueList[0]; Result = true; } else { Result = false; } return Result; } /// /// Gets a single bool value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetBool(string SectionName, string Key, out bool Value) { Value = false; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { // C# Boolean type expects "False" or "True" but since we're not case sensitive, we need to suppor that manually if (String.Compare(TextValue, "true", true) == 0 || String.Compare(TextValue, "1") == 0) { Value = true; } else if (String.Compare(TextValue, "false", true) == 0 || String.Compare(TextValue, "0") == 0) { Value = false; } else { // Failed to parse Result = false; } } return Result; } /// /// Gets a single Int32 value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetInt32(string SectionName, string Key, out int Value) { Value = 0; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { Result = Int32.TryParse(TextValue, out Value); } return Result; } /// /// Gets a single GUID value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetGUID(string SectionName, string Key, out Guid Value) { Value = Guid.Empty; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { string HexString = ""; if (TextValue.Contains("A=") && TextValue.Contains("B=") && TextValue.Contains("C=") && TextValue.Contains("D=")) { char[] Separators = new char[] { '(', ')', '=', ',', ' ', 'A', 'B', 'C', 'D' }; string[] ComponentValues = TextValue.Split(Separators, StringSplitOptions.RemoveEmptyEntries); if (ComponentValues.Length == 4) { for (int ComponentIndex = 0; ComponentIndex < 4; ComponentIndex++) { int IntegerValue; Result &= Int32.TryParse(ComponentValues[ComponentIndex], out IntegerValue); HexString += IntegerValue.ToString("X8"); } } } else { HexString = TextValue; } try { Value = Guid.ParseExact(HexString, "N"); Result = true; } catch (Exception) { Result = false; } } return Result; } /// /// Gets a single float value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetSingle(string SectionName, string Key, out float Value) { Value = 0.0f; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { Result = Single.TryParse(TextValue, out Value); } return Result; } /// /// Gets a single double value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetDouble(string SectionName, string Key, out double Value) { Value = 0.0; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { Result = Double.TryParse(TextValue, out Value); } return Result; } private static bool ExtractPath(string Source, out string Path) { int start = Source.IndexOf('"'); int end = Source.LastIndexOf('"'); if (start != 1 && end != -1 && start < end) { ++start; Path = Source.Substring(start, end - start); return true; } else { Path = ""; } return false; } public bool GetPath(string SectionName, string Key, out string Value) { string temp; if (GetString(SectionName, Key, out temp)) { return ExtractPath(temp, out Value); } else { Value = ""; } return false; } /// /// List of actions that can be performed on a single line from ini file /// enum ParseAction { None, New, Add, Remove } /// /// Checks what action should be performed on a single line from ini file /// private ParseAction GetActionForLine(ref string Line) { if (String.IsNullOrEmpty(Line) || Line.StartsWith(";") || Line.StartsWith("//")) { return ParseAction.None; } else if (Line.StartsWith("-")) { Line = Line.Substring(1).TrimStart(); return ParseAction.Remove; } else if (Line.StartsWith("+")) { Line = Line.Substring(1).TrimStart(); return ParseAction.Add; } else { // We use Add rather than New when we're not merging config files together in order // to mimic the behavior of the C++ config cache when loading a single file return (bIsMergingConfigs) ? ParseAction.New : ParseAction.Add; } } /// /// Loads and parses ini file. /// public void ParseIniFile(FileReference Filename) { String[] IniLines = null; List Commands = null; if (!FileCache.ContainsKey(Filename.FullName)) { try { IniLines = File.ReadAllLines(Filename.FullName); Commands = new List(); FileCache.Add(Filename.FullName, Commands); } catch (Exception ex) { Console.WriteLine("Error reading ini file: " + Filename + " Exception: " + ex.Message); } } else { Commands = FileCache[Filename.FullName]; } if (IniLines != null) { IniSection CurrentSection = null; // Line Index for exceptions var LineIndex = 1; var bMultiLine = false; var SingleValue = ""; var Key = ""; var LastAction = ParseAction.None; // Parse each line foreach (var Line in IniLines) { var TrimmedLine = Line.Trim(); // Multiline value support bool bWasMultiLine = bMultiLine; bMultiLine = TrimmedLine.EndsWith("\\"); if (bMultiLine) { TrimmedLine = TrimmedLine.Substring(0, TrimmedLine.Length - 1).TrimEnd(); } if (!bWasMultiLine) { if (TrimmedLine.StartsWith("[")) { CurrentSection = FindOrAddSection(TrimmedLine, Filename, LineIndex); LastAction = ParseAction.None; if (CurrentSection != null) { SectionCommand Command = new SectionCommand(); Command.Filename = Filename; Command.LineIndex = LineIndex; Command.TrimmedLine = TrimmedLine; Commands.Add(Command); } } else { if (LastAction != ParseAction.None) { throw new IniParsingException("Parsing new key/value pair when the previous one has not yet been processed ({0}, {1}) in {2}, line {3}: {4}", Key, SingleValue, Filename, LineIndex, TrimmedLine); } // Check if the line is empty or a comment, also remove any +/- markers LastAction = GetActionForLine(ref TrimmedLine); if (LastAction != ParseAction.None) { /* if (CurrentSection == null) { throw new IniParsingException("Trying to parse key/value pair that doesn't belong to any section in {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); }*/ ParseKeyValuePair(TrimmedLine, Filename, LineIndex, out Key, out SingleValue); } } } if (bWasMultiLine) { SingleValue += TrimmedLine; } if (!bMultiLine && LastAction != ParseAction.None && CurrentSection != null) { ProcessKeyValuePair(CurrentSection, Key, SingleValue, LastAction); KeyValueCommand Command = new KeyValueCommand(); Command.Key = Key; Command.Value = SingleValue; Command.LastAction = LastAction; Commands.Add(Command); LastAction = ParseAction.None; SingleValue = ""; Key = ""; } else if (CurrentSection == null) { LastAction = ParseAction.None; } LineIndex++; } } else if (Commands != null) { IniSection CurrentSection = null; // run each command for (int Idx = 0; Idx < Commands.Count; ++Idx) { var Command = Commands[Idx]; if (Command is SectionCommand) { CurrentSection = FindOrAddSection((Command as SectionCommand).TrimmedLine, (Command as SectionCommand).Filename, (Command as SectionCommand).LineIndex); } else if (Command is KeyValueCommand) { ProcessKeyValuePair(CurrentSection, (Command as KeyValueCommand).Key, (Command as KeyValueCommand).Value, (Command as KeyValueCommand).LastAction); } } } } /// /// Splits a line into key and value /// private static void ParseKeyValuePair(string TrimmedLine, FileReference Filename, int LineIndex, out string Key, out string Value) { var AssignIndex = TrimmedLine.IndexOf('='); if (AssignIndex < 0) { throw new IniParsingException("Failed to find value when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } Key = TrimmedLine.Substring(0, AssignIndex).Trim(); if (String.IsNullOrEmpty(Key)) { throw new IniParsingException("Empty key when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } Value = TrimmedLine.Substring(AssignIndex + 1).Trim(); if (Value.StartsWith("\"")) { // Remove quotes var QuoteEnd = Value.LastIndexOf('\"'); if (QuoteEnd == 0) { throw new IniParsingException("Mismatched quotes when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } Value = Value.Substring(1, Value.Length - 2); } } /// /// Processes parsed key/value pair /// private static void ProcessKeyValuePair(IniSection CurrentSection, string Key, string SingleValue, ParseAction Action) { switch (Action) { case ParseAction.New: { // New/replace IniValues Value; if (CurrentSection.TryGetValue(Key, out Value) == false) { Value = new IniValues(); CurrentSection.Add(Key, Value); } Value.Clear(); Value.Add(SingleValue); } break; case ParseAction.Add: { IniValues Value; if (CurrentSection.TryGetValue(Key, out Value) == false) { Value = new IniValues(); CurrentSection.Add(Key, Value); } Value.Add(SingleValue); } break; case ParseAction.Remove: { IniValues Value; if (CurrentSection.TryGetValue(Key, out Value)) { var ExistingIndex = Value.FindIndex(X => (String.Compare(SingleValue, X, true) == 0)); if (ExistingIndex >= 0) { Value.RemoveAt(ExistingIndex); } } } break; } } /// /// Finds an existing section or adds a new one /// private IniSection FindOrAddSection(string TrimmedLine, FileReference Filename, int LineIndex) { var SectionEndIndex = TrimmedLine.IndexOf(']'); if (SectionEndIndex != (TrimmedLine.Length - 1)) { throw new IniParsingException("Mismatched brackets when parsing section name in {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } var SectionName = TrimmedLine.Substring(1, TrimmedLine.Length - 2); if (String.IsNullOrEmpty(SectionName)) { throw new IniParsingException("Empty section name when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } else if (RequiredSections.Contains(SectionName)) { IniSection CurrentSection; if (Sections.TryGetValue(SectionName, out CurrentSection) == false) { CurrentSection = new IniSection(); Sections.Add(SectionName, CurrentSection); } return CurrentSection; } return null; } /// /// Returns a list of INI filenames for the engine /// private static IEnumerable EnumerateEngineIniFileNames(DirectoryReference EngineDirectory, string BaseIniName) { // Engine/Config/Base.ini (included in every ini type, required) yield return FileReference.Combine(EngineDirectory, "Config", "Base.ini"); // Engine/Config/Base* ini yield return FileReference.Combine(EngineDirectory, "Config", "Base" + BaseIniName + ".ini"); // Engine/Config/NotForLicensees/Base* ini yield return FileReference.Combine(EngineDirectory, "Config", "NotForLicensees", "Base" + BaseIniName + ".ini"); } /// /// Returns a list of INI filenames for the given project /// private static IEnumerable EnumerateCrossPlatformIniFileNames(DirectoryReference ProjectDirectory, DirectoryReference EngineDirectory, UnrealTargetPlatform Platform, string BaseIniName, bool SkipEngine) { if (!SkipEngine) { // Engine/Config/Base.ini (included in every ini type, required) yield return FileReference.Combine(EngineDirectory, "Config", "Base.ini"); // Engine/Config/Base* ini yield return FileReference.Combine(EngineDirectory, "Config", "Base" + BaseIniName + ".ini"); // Engine/Config/NotForLicensees/Base* ini yield return FileReference.Combine(EngineDirectory, "Config", "NotForLicensees", "Base" + BaseIniName + ".ini"); // NOTE: 4.7: See comment in GetSourceIniHierarchyFilenames() // Engine/Config/NoRedist/Base* ini // yield return Path.Combine(EngineDirectory, "Config", "NoRedist", "Base" + BaseIniName + ".ini"); } if (ProjectDirectory != null) { // Game/Config/Default* ini yield return FileReference.Combine(ProjectDirectory, "Config", "Default" + BaseIniName + ".ini"); // Game/Config/NotForLicensees/Default* ini yield return FileReference.Combine(ProjectDirectory, "Config", "NotForLicensees", "Default" + BaseIniName + ".ini"); // Game/Config/NoRedist/Default* ini yield return FileReference.Combine(ProjectDirectory, "Config", "NoRedist", "Default" + BaseIniName + ".ini"); } string PlatformName = GetIniPlatformName(Platform); if (Platform != UnrealTargetPlatform.Unknown) { // Engine/Config/Platform/Platform* ini yield return FileReference.Combine(EngineDirectory, "Config", PlatformName, PlatformName + BaseIniName + ".ini"); if (ProjectDirectory != null) { // Game/Config/Platform/Platform* ini yield return FileReference.Combine(ProjectDirectory, "Config", PlatformName, PlatformName + BaseIniName + ".ini"); } } DirectoryReference UserSettingsFolder = Utils.GetUserSettingDirectory(); // Match FPlatformProcess::UserSettingsDir() DirectoryReference PersonalFolder = null; // Match FPlatformProcess::UserDir() if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac) { PersonalFolder = new DirectoryReference(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Documents")); } else if (Environment.OSVersion.Platform == PlatformID.Unix) { PersonalFolder = new DirectoryReference(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Documents")); } else { PersonalFolder = new DirectoryReference(Environment.GetFolderPath(Environment.SpecialFolder.Personal)); } // /UE4/EngineConfig/User* ini yield return FileReference.Combine(UserSettingsFolder, "Unreal Engine", "Engine", "Config", "User" + BaseIniName + ".ini"); // /UE4/EngineConfig/User* ini yield return FileReference.Combine(PersonalFolder, "Unreal Engine", "Engine", "Config", "User" + BaseIniName + ".ini"); // Game/Config/User* ini if (ProjectDirectory != null) { yield return FileReference.Combine(ProjectDirectory, "Config", "User" + BaseIniName + ".ini"); } } /// /// Returns the platform name to use as part of platform-specific config files /// private static string GetIniPlatformName(UnrealTargetPlatform TargetPlatform) { if (TargetPlatform == UnrealTargetPlatform.Win32 || TargetPlatform == UnrealTargetPlatform.Win64 || TargetPlatform == UnrealTargetPlatform.UWP) { return "Windows"; } else { return Enum.GetName(typeof(UnrealTargetPlatform), TargetPlatform); } } } }