// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
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;
using System.Xml.Schema;
using Tools.DotNETCommon;
namespace UnrealBuildTool
{
///
/// Attribute to annotate fields in type that can be set using XML configuration system.
///
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class XmlConfigAttribute : 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()
{
Load(typeof(ConfigClass));
}
///
/// 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()
{
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)
{
InvokeIfExists(Class, "LoadDefaults");
InvokeIfExists(Class, "Reset");
XmlConfigLoaderClassData ClassData;
if (Data.TryGetValue(Class, out ClassData))
{
ClassData.LoadXmlData();
}
InvokeIfExists(Class, "PostReset");
}
///
/// 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()
{
// No one should try to refereence configuration values until the config files are loaded.
// The XmlConfig system itself will SET config values below, then anyone can read them.
// But our static constructor checks can't differentiate this, so we just allow reads starting now,
// right before the XML files are loaded.
UnrealBuildTool.bIsSafeToReferenceConfigurationValues = true;
OverwriteIfDifferent(GetXSDPath(), BuildXSD());
LoadDefaults();
CreateUserXmlConfigTemplate();
LoadData();
foreach (var ConfClass in Data.Keys)
{
Load(ConfClass);
}
}
///
/// 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, Encoding.UTF8);
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, Encoding.UTF8).Equals(Content, StringComparison.InvariantCulture);
}
///
/// Loads default values for all configuration classes in assembly.
///
private static void LoadDefaults()
{
foreach (var ConfigType in GetAllConfigurationTypes())
{
InvokeIfExists(ConfigType, "LoadDefaults");
}
}
///
/// Cache entry class to store loaded info for given class.
///
class XmlConfigLoaderClassData
{
///
/// Loads previously stored data into class.
///
public void LoadXmlData()
{
foreach (var DataPair in DataMap)
{
DataPair.Key.SetValue(null, DataPair.Value);
}
foreach (var PropertyPair in PropertyMap)
{
PropertyPair.Key.SetValue(null, PropertyPair.Value, null);
}
}
///
/// 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);
}
}
///
/// Adds or overrides value in the cache.
///
/// The property info of the class.
/// The value to store.
public void SetValue(PropertyInfo Property, object Value)
{
if (PropertyMap.ContainsKey(Property))
{
PropertyMap[Property] = Value;
}
else
{
PropertyMap.Add(Property, Value);
}
}
// Loaded data map.
Dictionary DataMap = new Dictionary();
// Loaded data map.
Dictionary PropertyMap = 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(Path.GetDirectoryName(Assembly.GetEntryAssembly().GetOriginalLocation()), "..", "..")).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.bExists)
{
continue;
}
try
{
LoadDataFromFile(PossibleConfigLocation.FSLocation);
}
catch (Exception Ex)
{
Console.WriteLine("Problem parsing {0}:\n {1}", PossibleConfigLocation.FSLocation, Ex.ToString());
}
}
}
///
/// Creates XML files for all known XmlConfigLocations which return IfShouldCreateFile()==true. Files
/// will be filled with default content chosen by the XmlConfigLocation implementation.
///
private static void CreateUserXmlConfigTemplate()
{
foreach (var PossibleConfigLocation in ConfigLocationHierarchy)
{
if (!PossibleConfigLocation.IfShouldCreateFile())
{
continue;
}
PossibleConfigLocation.CreateUserXmlConfigTemplate();
}
}
///
/// Reads config schema from XSD file.
///
private static XmlSchema ReadConfigSchema()
{
var Settings = new XmlReaderSettings();
Settings.ValidationType = ValidationType.DTD;
using (var SR = new StringReader(File.ReadAllText(GetXSDPath(), Encoding.UTF8)))
{
using (var XR = XmlReader.Create(SR, Settings))
{
return XmlSchema.Read(XR, (object Sender, System.Xml.Schema.ValidationEventArgs EventArgs) =>
{
throw new BuildException("XmlConfigLoader: Reading config XSD failed:\n{0}({1}): {2}",
new Uri(EventArgs.Exception.SourceUri).LocalPath, EventArgs.Exception.LineNumber,
EventArgs.Message);
});
}
}
}
// Stores read XSD schema for config files.
private static XmlSchema ConfigSchemaCache = null;
///
/// Gets config XSD schema.
///
private static XmlSchema GetConfigSchema()
{
if (ConfigSchemaCache == null)
{
ConfigSchemaCache = ReadConfigSchema();
}
return ConfigSchemaCache;
}
///
/// Sets values of this class with values from given XML file.
///
/// The path to the file with values.
private static void LoadDataFromFile(string ConfigurationXmlPath)
{
var ConfigDocument = new XmlDocument();
var NS = new XmlNamespaceManager(ConfigDocument.NameTable);
NS.AddNamespace("ns", "https://www.unrealengine.com/BuildConfiguration");
var ReaderSettings = new XmlReaderSettings();
ReaderSettings.ValidationEventHandler += (object Sender, System.Xml.Schema.ValidationEventArgs EventArgs) =>
{
throw new BuildException("XmlConfigLoader: Reading config XML failed:\n{0}({1}): {2}",
ConfigurationXmlPath, EventArgs.Exception.LineNumber,
EventArgs.Message);
};
ReaderSettings.ValidationType = ValidationType.Schema;
ReaderSettings.Schemas.Add(GetConfigSchema());
using (var SR = new StringReader(File.ReadAllText(ConfigurationXmlPath, Encoding.UTF8)))
{
var Reader = XmlReader.Create(SR, ReaderSettings);
ConfigDocument.Load(Reader);
}
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;
}
if (!IsConfigurableClass(ClassType))
{
Log.TraceVerbose("XmlConfig Loading: class '{0}' is not allowed to be configured using XML system.", XmlClass.Name);
continue;
}
XmlConfigLoaderClassData ClassData;
if (!Data.TryGetValue(ClassType, out ClassData))
{
ClassData = new XmlConfigLoaderClassData();
Data.Add(ClassType, ClassData);
}
var XmlFields = XmlClass.SelectNodes("*");
foreach (XmlNode XmlField in XmlFields)
{
FieldInfo Field = ClassType.GetField(XmlField.Name);
// allow settings in the .xml that don't exist, as another branch may have it, and can share this file from Documents
if (Field == null)
{
PropertyInfo Property = ClassType.GetProperty(XmlField.Name);
if (Property != null)
{
if (!IsConfigurable(Property))
{
throw new BuildException("BuildConfiguration Loading: property '{0}' is either non-public, non-static or not-xml-configurable.", XmlField.Name);
}
ClassData.SetValue(Property, ParseFieldData(Property.PropertyType, XmlField.InnerText));
}
continue;
}
if (!IsConfigurableField(Field))
{
throw new BuildException("BuildConfiguration Loading: field '{0}' 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