using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
using System.Xml.Linq;
namespace UnrealBuildTool
{
///
/// Attribute to annotate classes that can be set using xml configuration system.
///
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class XmlConfigAttribute : Attribute
{
public readonly bool bAllPublicStaticFields;
public XmlConfigAttribute(bool bAllPublicStaticFields = false)
{
this.bAllPublicStaticFields = bAllPublicStaticFields;
}
}
///
/// Attribute to annotate fields in type that can be set using xml configuration system.
///
[AttributeUsage(AttributeTargets.Field)]
public class XmlConfigFieldAttribute : Attribute
{
}
///
/// Loads data from disk XML files and stores it in memory.
///
public static class XmlConfigLoader
{
///
/// Resets given config class.
///
/// The class to reset.
public static void Reset()
{
var Type = typeof(ConfigClass);
InvokeIfExists(Type, "LoadDefaults");
// Load eventual XML configuration files if they exist to override default values.
Load(Type);
InvokeIfExists(Type, "PostReset");
}
///
/// Creates XML element for given config class.
///
/// A type of the config class.
/// XML element representing config class.
private static XElement CreateConfigTypeXMLElement(Type ConfigType)
{
var NS = XNamespace.Get("https://www.unrealengine.com/BuildConfiguration");
return new XElement(NS + ConfigType.Name,
from Field in GetConfigurableFields(ConfigType)
where !(Field.FieldType == typeof(string) && Field.GetValue(null) == null)
select CreateFieldXMLElement(Field));
}
///
/// Gets XML representing current config.
///
/// XML representing current config.
private static XDocument GetConfigXML()
{
var NS = XNamespace.Get("https://www.unrealengine.com/BuildConfiguration");
return new XDocument(
new XElement(NS + "Configuration",
from ConfigType in GetAllConfigurationTypes()
select CreateConfigTypeXMLElement(ConfigType)
)
);
}
///
/// Gets default config XML.
///
/// Default config XML.
public static string GetDefaultXML()
{
foreach(var ConfigType in GetAllConfigurationTypes())
{
InvokeIfExists(ConfigType, "LoadDefaults");
}
var Comment = @"
#########################################################################
# #
# This is an XML with default UBT configuration. If you want to #
# override it, create the same file in the locations c. or d. #
# (see below). DONT'T CHANGE CONTENTS OF THIS FILE! #
# #
#########################################################################
The syntax of this file is:
Value
First itemSecond item
...
Last item
...
...
...
...
There are four possible location for this file:
a. UE4/Engine/Programs/UnrealBuildTool
b. UE4/Engine/Programs/NotForLicensees/UnrealBuildTool
c. UE4/Engine/Saved/UnrealBuildTool
d. My Documents/Unreal Engine/UnrealBuildTool
The UBT is looking for it in all four places in the given order and
overrides already read data with the loaded ones, hence d. has the
priority. Not defined classes and fields are left alone.
";
var DefaultXml = GetConfigXML();
DefaultXml.AddFirst(new XComment(Comment));
return WriteToBuffer(DefaultXml);
}
///
/// Loads data for a given config class.
///
/// A type of a config class.
public static void Load(Type Class)
{
XmlConfigLoaderClassData ClassData;
if(Data.TryGetValue(Class, out ClassData))
{
ClassData.LoadXmlData();
}
}
///
/// Builds xsd for BuildConfiguration.xml files.
///
/// Content of the xsd file.
public static string BuildXSD()
{
// all elements use this namespace
var NS = XNamespace.Get("http://www.w3.org/2001/XMLSchema");
return WriteToBuffer(new XDocument(
// define the root element that declares the schema and target namespace it is validating.
new XElement(NS + "schema",
new XAttribute("targetNamespace", "https://www.unrealengine.com/BuildConfiguration"),
new XAttribute("elementFormDefault", "qualified"),
new XElement(NS + "element", new XAttribute("name", "Configuration"),
new XElement(NS + "complexType",
new XElement(NS + "all",
// loop over all public types in UBT assembly
from ConfigType in GetAllConfigurationTypes()
// find all public static fields of intrinsic types (and a few extra)
let PublicStaticFields = GetConfigurableFields(ConfigType).ToList()
where PublicStaticFields.Count > 0
select
new XElement(NS + "element",
new XAttribute("name", ConfigType.Name),
new XAttribute("minOccurs", "0"),
new XAttribute("maxOccurs", "1"),
new XElement(NS + "complexType",
new XElement(NS + "all",
from Field in PublicStaticFields
select CreateXSDElementForField(Field)
)
)
)
)
)
)
)
));
}
///
/// Initializing data and resets all found classes.
///
public static void Init()
{
OverwriteIfDifferent(GetXSDPath(), BuildXSD());
LoadData();
foreach(var ClassData in Data)
{
ClassData.Value.ResetData();
}
}
///
/// Overwrites file at FilePath with the Content if the content was
/// different. If the file doesn't exist it creates file with the
/// Content.
///
/// File to check, overwrite or create.
/// Content to fill the file with.
/// If file can and should be read-only?
private static void OverwriteIfDifferent(string FilePath, string Content, bool bReadOnlyFile = false)
{
if (FileDifferentThan(FilePath, Content))
{
if(bReadOnlyFile && File.Exists(FilePath))
{
var Attributes = File.GetAttributes(FilePath);
if(Attributes.HasFlag(FileAttributes.ReadOnly))
{
File.SetAttributes(FilePath, Attributes & ~FileAttributes.ReadOnly);
}
}
Directory.CreateDirectory(Path.GetDirectoryName(FilePath));
File.WriteAllText(FilePath, Content);
if(bReadOnlyFile)
{
File.SetAttributes(FilePath, File.GetAttributes(FilePath) | FileAttributes.ReadOnly);
}
}
}
///
/// Tells if file at given path has different content than given.
///
/// Path of the file to check.
/// Content to check.
/// True if file at FilePath has different content than Content. False otherwise.
private static bool FileDifferentThan(string FilePath, string Content)
{
if (!File.Exists(FilePath))
{
return true;
}
return !File.ReadAllText(FilePath).Equals(Content, StringComparison.InvariantCulture);
}
///
/// Cache entry class to store loaded info for given class.
///
class XmlConfigLoaderClassData
{
public XmlConfigLoaderClassData(Type ConfigClass)
{
// Adding empty types array to make sure this is parameterless Reset
// in case of overloading.
DefaultValuesLoader = ConfigClass.GetMethod("Reset", new Type[] { });
}
///
/// Resets previously stored data into class.
///
public void ResetData()
{
bDoneLoading = false;
if(DefaultValuesLoader != null)
{
DefaultValuesLoader.Invoke(null, new object[] { });
}
if (!bDoneLoading)
{
LoadXmlData();
}
}
///
/// Loads previously stored data into class.
///
public void LoadXmlData()
{
foreach (var DataPair in DataMap)
{
DataPair.Key.SetValue(null, DataPair.Value);
}
bDoneLoading = true;
}
///
/// Adds or overrides value in the cache.
///
/// The field info of the class.
/// The value to store.
public void SetValue(FieldInfo Field, object Value)
{
if(DataMap.ContainsKey(Field))
{
DataMap[Field] = Value;
}
else
{
DataMap.Add(Field, Value);
}
}
// A variable to indicate if loading was done during invoking of
// default values loader.
bool bDoneLoading = false;
// Method info loader to invoke before overriding fields with XML files data.
MethodInfo DefaultValuesLoader = null;
// Loaded data map.
Dictionary DataMap = new Dictionary();
}
///
/// Class that stores information about possible BuildConfiguration.xml
/// location and its name that will be displayed in IDE.
///
public class XmlConfigLocation
{
///
/// Returns location of the BuildConfiguration.xml.
///
/// Location of the BuildConfiguration.xml.
private static string GetConfigLocation(IEnumerable PossibleLocations, out bool bExists)
{
if(PossibleLocations.Count() == 0)
{
throw new ArgumentException("Empty possible locations", "PossibleLocations");
}
const string ConfigXmlFileName = "BuildConfiguration.xml";
// Filter out non-existing
var ExistingLocations = new List();
foreach(var PossibleLocation in PossibleLocations)
{
var FilePath = Path.Combine(PossibleLocation, ConfigXmlFileName);
if(File.Exists(FilePath))
{
ExistingLocations.Add(FilePath);
}
}
if(ExistingLocations.Count == 0)
{
bExists = false;
return Path.Combine(PossibleLocations.First(), ConfigXmlFileName);
}
bExists = true;
if(ExistingLocations.Count == 1)
{
return ExistingLocations.First();
}
// Choose most recently used from existing.
return ExistingLocations.OrderBy(Location => File.GetLastWriteTime(Location)).Last();
}
// Possible location of the config file in the file system.
public string FSLocation { get; private set; }
// IDE folder name that will contain this location if file will be found.
public string IDEFolderName { get; private set; }
// Tells if UBT has to create a template config file if it does not exist in the location.
public bool bCreateIfDoesNotExist { get; private set; }
// Tells if config file exists in this location.
public bool bExists { get; protected set; }
public XmlConfigLocation(string[] FSLocations, string IDEFolderName, bool bCreateIfDoesNotExist = false)
{
bool bExists;
this.FSLocation = GetConfigLocation(FSLocations, out bExists);
this.IDEFolderName = IDEFolderName;
this.bCreateIfDoesNotExist = bCreateIfDoesNotExist;
this.bExists = bExists;
}
public XmlConfigLocation(string FSLocation, string IDEFolderName, bool bCreateIfDoesNotExist = false)
: this(new string[] { FSLocation }, IDEFolderName, bCreateIfDoesNotExist)
{
}
///
/// Creates template file in the FS location.
///
public virtual void CreateUserXmlConfigTemplate()
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(FSLocation));
var FilePath = Path.Combine(FSLocation);
const string TemplateContent =
"" +
"\n" +
" \n" +
" \n" +
"\n";
File.WriteAllText(FilePath, TemplateContent);
bExists = true;
}
catch (Exception)
{
// Ignore quietly.
}
}
///
/// Tells if procedure should try to create file for this location.
///
/// True if procedure should try to create file for this location. False otherwise.
public virtual bool IfShouldCreateFile()
{
return bCreateIfDoesNotExist && !bExists;
}
}
///
/// Class that stores information about possible default BuildConfiguration.xml.
///
public class XmlDefaultConfigLocation : XmlConfigLocation
{
public XmlDefaultConfigLocation(string FSLocation)
: base(FSLocation, "Default", true)
{
}
///
/// Creates template file in the FS location.
///
public override void CreateUserXmlConfigTemplate()
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(FSLocation));
var FilePath = Path.Combine(FSLocation);
OverwriteIfDifferent(FilePath, GetDefaultXML(), true);
bExists = true;
}
catch (Exception)
{
// Ignore quietly.
}
}
///
/// Tells if procedure should try to create file for this location.
///
/// True if procedure should try to create file for this location. False otherwise.
public override bool IfShouldCreateFile()
{
return true;
}
}
public static readonly XmlConfigLocation[] ConfigLocationHierarchy;
static XmlConfigLoader()
{
/*
* There are four possible location for this file:
* a. UE4/Engine/Programs/UnrealBuildTool
* b. UE4/Engine/Programs/NotForLicensees/UnrealBuildTool
* c. UE4/Engine/Saved/UnrealBuildTool
* d. /Unreal Engine/UnrealBuildTool -- the location is
* chosen by existence and if both exist most recently used.
*
* The UBT is looking for it in all four places in the given order and
* overrides already read data with the loaded ones, hence d. has the
* priority. Not defined classes and fields are left alone.
*/
var UE4EnginePath = new FileInfo(Path.Combine(Utils.GetExecutingAssemblyDirectory(), "..", "..")).FullName;
ConfigLocationHierarchy = new XmlConfigLocation[]
{
new XmlDefaultConfigLocation(Path.Combine(UE4EnginePath, "Programs", "UnrealBuildTool")),
new XmlConfigLocation(Path.Combine(UE4EnginePath, "Programs", "NotForLicensees", "UnrealBuildTool"), "NotForLicensees"),
new XmlConfigLocation(Path.Combine(UE4EnginePath, "Saved", "UnrealBuildTool"), "User", true),
new XmlConfigLocation(new string[] {
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Unreal Engine", "UnrealBuildTool"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Unreal Engine", "UnrealBuildTool")
}, "Global", true)
};
}
///
/// Loads BuildConfiguration from XML into memory.
///
private static void LoadData()
{
foreach (var PossibleConfigLocation in ConfigLocationHierarchy)
{
if (!PossibleConfigLocation.IfShouldCreateFile())
{
continue;
}
PossibleConfigLocation.CreateUserXmlConfigTemplate();
}
foreach (var PossibleConfigLocation in ConfigLocationHierarchy)
{
if(!PossibleConfigLocation.bExists)
{
continue;
}
Load(PossibleConfigLocation.FSLocation);
}
}
///
/// Sets values of this class with values from given XML file.
///
/// The path to the file with values.
private static void Load(string ConfigurationXmlPath)
{
var ConfigDocument = new XmlDocument();
var NS = new XmlNamespaceManager(ConfigDocument.NameTable);
NS.AddNamespace("ns", "https://www.unrealengine.com/BuildConfiguration");
ConfigDocument.Load(ConfigurationXmlPath);
var XmlClasses = ConfigDocument.DocumentElement.SelectNodes("/ns:Configuration/*", NS);
if(XmlClasses.Count == 0)
{
if(ConfigDocument.DocumentElement.Name == "Configuration")
{
ConfigDocument.DocumentElement.SetAttribute("xmlns", "https://www.unrealengine.com/BuildConfiguration");
var NSDoc = new XmlDocument();
NSDoc.LoadXml(ConfigDocument.OuterXml);
try
{
File.WriteAllText(ConfigurationXmlPath, WriteToBuffer(NSDoc));
}
catch(Exception)
{
// Ignore gently.
}
XmlClasses = NSDoc.DocumentElement.SelectNodes("/ns:Configuration/*", NS);
}
}
foreach (XmlNode XmlClass in XmlClasses)
{
var ClassType = Type.GetType("UnrealBuildTool." + XmlClass.Name);
if(ClassType == null)
{
Log.TraceVerbose("XmlConfig Loading: class '{0}' doesn't exist.", XmlClass.Name);
continue;
}
XmlConfigLoaderClassData ClassData;
bool bAllPublicStaticFields = false;
var Attributes = ClassType.GetCustomAttributes(typeof(XmlConfigAttribute), false);
if (Attributes.Length == 0)
{
Log.TraceVerbose("XmlConfig Loading: class '{0}' is not allowed to be configured using XML system.", XmlClass.Name);
continue;
}
bAllPublicStaticFields = ((XmlConfigAttribute)Attributes.First()).bAllPublicStaticFields;
if (!Data.TryGetValue(ClassType, out ClassData))
{
ClassData = new XmlConfigLoaderClassData(ClassType);
Data.Add(ClassType, ClassData);
}
var XmlFields = XmlClass.SelectNodes("*");
foreach (XmlNode XmlField in XmlFields)
{
FieldInfo Field = ClassType.GetField(XmlField.Name);
if (Field == null || !Field.IsPublic || !Field.IsStatic || (!bAllPublicStaticFields && Field.GetCustomAttributes(typeof(XmlConfigFieldAttribute), false).Length == 0))
{
throw new BuildException("BuildConfiguration Loading: field '{0}' doesn't exist or is either non-public, non-static or not-xml-configurable.", XmlField.Name);
}
if(Field.FieldType.IsArray)
{
// If the type is an array type get items for it.
var XmlItems = XmlField.SelectNodes("ns:Item", NS);
// Get the C# type of the array.
var ItemType = Field.FieldType.GetElementType();
// Create the array according to the ItemType.
var OutputArray = Array.CreateInstance(ItemType, XmlItems.Count);
int Id = 0;
foreach(XmlNode XmlItem in XmlItems)
{
// Append values to the OutputArray.
OutputArray.SetValue(ParseFieldData(ItemType, XmlItem.InnerText), Id++);
}
ClassData.SetValue(Field, OutputArray);
}
else
{
ClassData.SetValue(Field, ParseFieldData(Field.FieldType, XmlField.InnerText));
}
}
}
}
private static object ParseFieldData(Type FieldType, string Text)
{
if (FieldType.Equals(typeof(System.String)))
{
return Text;
}
else
{
// Declaring parameters array used by TryParse method.
// Second parameter is "out", so you have to just
// assign placeholder null to it.
object ParsedValue;
if (!TryParse(FieldType, Text, out ParsedValue))
{
throw new BuildException("BuildConfiguration Loading: Parsing {0} value from \"{1}\" failed.", FieldType.Name, Text);
}
// If Invoke returned true, the second object of the
// parameters array is set to the parsed value.
return ParsedValue;
}
}
///
/// Emulates TryParse behavior on custom type. If the type implements
/// Parse(string, IFormatProvider) or Parse(string) static method uses
/// one of them to parse with preference of the one with format
/// provider (but passes invariant culture).
///
/// Type to parse.
/// String representation of the value.
/// Output parsed value.
/// True if parsing succeeded. False otherwise.
private static bool TryParse(Type ParsingType, string UnparsedValue, out object ParsedValue)
{
// Getting Parse method for FieldType which is required,
// if it doesn't exists for complex type, author should add
// one. The signature should be one of:
// static T Parse(string Input, IFormatProvider Provider) or
// static T Parse(string Input)
// where T is containing type.
// The one with format provider is preferred and invoked with
// InvariantCulture.
bool bWithCulture = true;
var ParseMethod = ParsingType.GetMethod("Parse", new Type[] { typeof(System.String), typeof(IFormatProvider) });
if(ParseMethod == null)
{
ParseMethod = ParsingType.GetMethod("Parse", new Type[] { typeof(System.String) });
bWithCulture = false;
}
if (ParseMethod == null)
{
throw new BuildException("BuildConfiguration Loading: Parsing of the type {0} is not supported.", ParsingType.Name);
}
var ParametersList = new List