You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#rnx #preflight 64767efb4b1ead7c7f428c7a [CL 25693857 by joe kirchoff in ue5-main branch]
1051 lines
37 KiB
C#
1051 lines
37 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Xml;
|
|
using System.Xml.Schema;
|
|
using System.Xml.Serialization;
|
|
using EpicGames.Core;
|
|
using Microsoft.Extensions.Logging;
|
|
using UnrealBuildBase;
|
|
|
|
namespace UnrealBuildTool
|
|
{
|
|
/// <summary>
|
|
/// Functions for manipulating the XML config cache
|
|
/// </summary>
|
|
static class XmlConfig
|
|
{
|
|
/// <summary>
|
|
/// An input config file
|
|
/// </summary>
|
|
public class InputFile
|
|
{
|
|
/// <summary>
|
|
/// Location of the file
|
|
/// </summary>
|
|
public FileReference Location;
|
|
|
|
/// <summary>
|
|
/// Which folder to display the config file under in the generated project files
|
|
/// </summary>
|
|
public string FolderName;
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
/// <param name="Location"></param>
|
|
/// <param name="FolderName"></param>
|
|
public InputFile(FileReference Location, string FolderName)
|
|
{
|
|
this.Location = Location;
|
|
this.FolderName = FolderName;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The cache file that is being used
|
|
/// </summary>
|
|
public static FileReference? CacheFile;
|
|
|
|
/// <summary>
|
|
/// Parsed config values
|
|
/// </summary>
|
|
static XmlConfigData? Values;
|
|
|
|
/// <summary>
|
|
/// Cached serializer for the XML schema
|
|
/// </summary>
|
|
static XmlSerializer? CachedSchemaSerializer;
|
|
|
|
/// <summary>
|
|
/// Initialize the config system with the given types
|
|
/// </summary>
|
|
/// <param name="OverrideCacheFile">Force use of the cached XML config without checking if it's valid (useful for remote builds)</param>
|
|
/// <param name="ProjectRootDirectory">Read XML configuration with a Project directory</param>
|
|
/// <param name="Logger">Logger for output</param>
|
|
public static void ReadConfigFiles(FileReference? OverrideCacheFile, DirectoryReference? ProjectRootDirectory, ILogger Logger)
|
|
{
|
|
// Find all the configurable types
|
|
List<Type> ConfigTypes = FindConfigurableTypes();
|
|
|
|
// Update the cache if necessary
|
|
if (OverrideCacheFile != null)
|
|
{
|
|
// Set the cache file to the overriden value
|
|
CacheFile = OverrideCacheFile;
|
|
|
|
// Never rebuild the cache; just try to load it.
|
|
if (!XmlConfigData.TryRead(CacheFile, ConfigTypes, out Values))
|
|
{
|
|
throw new BuildException("Unable to load XML config cache ({0})", CacheFile);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (ProjectRootDirectory == null)
|
|
{
|
|
// Get the default cache file
|
|
CacheFile = FileReference.Combine(Unreal.EngineDirectory, "Intermediate", "Build", "XmlConfigCache.bin");
|
|
if (Unreal.IsEngineInstalled())
|
|
{
|
|
DirectoryReference? UserSettingsDir = Unreal.UserSettingDirectory;
|
|
if (UserSettingsDir != null)
|
|
{
|
|
CacheFile = FileReference.Combine(UserSettingsDir, "UnrealEngine", String.Format("XmlConfigCache-{0}.bin", Unreal.RootDirectory.FullName.Replace(":", "").Replace(Path.DirectorySeparatorChar, '+')));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CacheFile = FileReference.Combine(ProjectRootDirectory, "Intermediate", "Build", "XmlConfigCache.bin");
|
|
Values = null;
|
|
}
|
|
|
|
// Find all the input files
|
|
List<FileReference> TemporaryInputFileLocations = InputFiles.Select(x => x.Location).ToList();
|
|
|
|
if (ProjectRootDirectory != null)
|
|
{
|
|
FileReference ProjectRootConfigLocation = FileReference.Combine(ProjectRootDirectory, "Saved", "UnrealBuildTool", "BuildConfiguration.xml");
|
|
if (!FileReference.Exists(ProjectRootConfigLocation))
|
|
{
|
|
CreateDefaultConfigFile(ProjectRootConfigLocation);
|
|
}
|
|
|
|
TemporaryInputFileLocations.Add(ProjectRootConfigLocation);
|
|
}
|
|
|
|
FileReference[] InputFileLocations = TemporaryInputFileLocations.ToArray();
|
|
|
|
// Get the path to the schema
|
|
FileReference SchemaFile = GetSchemaLocation(ProjectRootDirectory);
|
|
|
|
// Try to read the existing cache from disk
|
|
XmlConfigData? CachedValues;
|
|
if (IsCacheUpToDate(CacheFile, InputFileLocations) && FileReference.Exists(SchemaFile))
|
|
{
|
|
if (XmlConfigData.TryRead(CacheFile, ConfigTypes, out CachedValues) && Enumerable.SequenceEqual(InputFileLocations, CachedValues.InputFiles))
|
|
{
|
|
Values = CachedValues;
|
|
}
|
|
}
|
|
|
|
// If that failed, regenerate it
|
|
if (Values == null)
|
|
{
|
|
// Find all the configurable fields from the given types
|
|
Dictionary<string, Dictionary<string, XmlConfigData.TargetMember>> CategoryToFields = new Dictionary<string, Dictionary<string, XmlConfigData.TargetMember>>();
|
|
FindConfigurableFields(ConfigTypes, CategoryToFields);
|
|
|
|
// Create a schema for the config files
|
|
XmlSchema Schema = CreateSchema(CategoryToFields);
|
|
if (!Unreal.IsEngineInstalled())
|
|
{
|
|
WriteSchema(Schema, SchemaFile);
|
|
}
|
|
|
|
// Read all the XML files and validate them against the schema
|
|
Dictionary<Type, Dictionary<XmlConfigData.TargetMember, XmlConfigData.ValueInfo>> TypeToValues =
|
|
new Dictionary<Type, Dictionary<XmlConfigData.TargetMember, XmlConfigData.ValueInfo>>();
|
|
foreach (FileReference InputFile in InputFileLocations)
|
|
{
|
|
if (!TryReadFile(InputFile, CategoryToFields, TypeToValues, Schema, Logger))
|
|
{
|
|
throw new BuildException("Failed to properly read XML file : {0}", InputFile.FullName);
|
|
}
|
|
}
|
|
|
|
// Make sure the cache directory exists
|
|
DirectoryReference.CreateDirectory(CacheFile.Directory);
|
|
|
|
// Create the new cache
|
|
Values = new XmlConfigData(InputFileLocations, TypeToValues.ToDictionary(
|
|
x => x.Key,
|
|
x => x.Value.Select(x => x.Value).ToArray()));
|
|
Values.Write(CacheFile);
|
|
}
|
|
}
|
|
|
|
// Apply all the static field values
|
|
foreach (KeyValuePair<Type, XmlConfigData.ValueInfo[]> TypeValuesPair in Values.TypeToValues)
|
|
{
|
|
foreach (XmlConfigData.ValueInfo MemberValue in TypeValuesPair.Value)
|
|
{
|
|
if (MemberValue.Target.IsStatic)
|
|
{
|
|
object Value = InstanceValue(MemberValue.Value, MemberValue.Target.Type);
|
|
MemberValue.Target.SetValue(null, Value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find all the configurable types in the current assembly
|
|
/// </summary>
|
|
/// <returns>List of configurable types</returns>
|
|
static List<Type> FindConfigurableTypes()
|
|
{
|
|
List<Type> ConfigTypes = new List<Type>();
|
|
try
|
|
{
|
|
foreach (Type ConfigType in Assembly.GetExecutingAssembly().GetTypes())
|
|
{
|
|
if (HasXmlConfigFileAttribute(ConfigType))
|
|
{
|
|
ConfigTypes.Add(ConfigType);
|
|
}
|
|
}
|
|
}
|
|
catch (ReflectionTypeLoadException Ex)
|
|
{
|
|
Console.WriteLine("TypeLoadException: {0}", String.Join("\n", Ex.LoaderExceptions.Select(x => x?.Message)));
|
|
throw;
|
|
}
|
|
return ConfigTypes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the given type has a field with an XmlConfigFile attribute
|
|
/// </summary>
|
|
/// <param name="Type">The type to check</param>
|
|
/// <returns>True if the type has a field with the XmlConfigFile attribute</returns>
|
|
static bool HasXmlConfigFileAttribute(Type Type)
|
|
{
|
|
foreach (FieldInfo Field in Type.GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
|
|
{
|
|
foreach (CustomAttributeData CustomAttribute in Field.CustomAttributes)
|
|
{
|
|
if (CustomAttribute.AttributeType == typeof(XmlConfigFileAttribute))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
foreach (PropertyInfo Property in Type.GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
|
|
{
|
|
foreach (CustomAttributeData CustomAttribute in Property.CustomAttributes)
|
|
{
|
|
if (CustomAttribute.AttributeType == typeof(XmlConfigFileAttribute))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the location of the XML config schema
|
|
/// </summary>
|
|
/// <param name="ProjectRootDirecory">Optional project root directory</param>
|
|
/// <returns>The location of the schema file</returns>
|
|
public static FileReference GetSchemaLocation(DirectoryReference? ProjectRootDirecory = null)
|
|
{
|
|
if (ProjectRootDirecory != null)
|
|
{
|
|
FileReference ProjectSchema = FileReference.Combine(ProjectRootDirecory, "Saved", "UnrealBuildTool", "BuildConfiguration.Schema.xsd");
|
|
if (FileReference.Exists(ProjectSchema))
|
|
{
|
|
return ProjectSchema;
|
|
}
|
|
}
|
|
return FileReference.Combine(Unreal.EngineDirectory, "Saved", "UnrealBuildTool", "BuildConfiguration.Schema.xsd");
|
|
}
|
|
|
|
static InputFile[]? CachedInputFiles;
|
|
|
|
/// <summary>
|
|
/// Initialize the list of input files
|
|
/// </summary>
|
|
public static InputFile[] InputFiles
|
|
{
|
|
get
|
|
{
|
|
if (CachedInputFiles != null)
|
|
{
|
|
return CachedInputFiles;
|
|
}
|
|
|
|
ILogger Logger = Log.Logger;
|
|
|
|
// Find all the input file locations
|
|
List<InputFile> InputFilesFound = new List<InputFile>(4);
|
|
|
|
// Skip all the config files under the Engine folder if it's an installed build
|
|
if (!Unreal.IsEngineInstalled())
|
|
{
|
|
// Check for the config file under /Engine/Programs/NotForLicensees/UnrealBuildTool
|
|
FileReference NotForLicenseesConfigLocation = FileReference.Combine(Unreal.EngineDirectory, "Restricted", "NotForLicensees", "Programs", "UnrealBuildTool", "BuildConfiguration.xml");
|
|
if (FileReference.Exists(NotForLicenseesConfigLocation))
|
|
{
|
|
InputFilesFound.Add(new InputFile(NotForLicenseesConfigLocation, "NotForLicensees"));
|
|
}
|
|
else
|
|
{
|
|
Logger.LogDebug("No config file at {NotForLicenseesConfigLocation}", NotForLicenseesConfigLocation);
|
|
}
|
|
|
|
// Check for the user config file under /Engine/Saved/UnrealBuildTool
|
|
FileReference UserConfigLocation = FileReference.Combine(Unreal.EngineDirectory, "Saved", "UnrealBuildTool", "BuildConfiguration.xml");
|
|
if (!FileReference.Exists(UserConfigLocation))
|
|
{
|
|
Logger.LogDebug("Creating default config file at {UserConfigLocation}", UserConfigLocation);
|
|
CreateDefaultConfigFile(UserConfigLocation);
|
|
}
|
|
InputFilesFound.Add(new InputFile(UserConfigLocation, "User"));
|
|
}
|
|
|
|
// Check for the global config file under AppData/Unreal Engine/UnrealBuildTool
|
|
string AppDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
|
if (!String.IsNullOrEmpty(AppDataFolder))
|
|
{
|
|
FileReference AppDataConfigLocation = FileReference.Combine(new DirectoryReference(AppDataFolder), "Unreal Engine", "UnrealBuildTool", "BuildConfiguration.xml");
|
|
if (!FileReference.Exists(AppDataConfigLocation))
|
|
{
|
|
Logger.LogDebug("Creating default config file at {AppDataConfigLocation}", AppDataConfigLocation);
|
|
CreateDefaultConfigFile(AppDataConfigLocation);
|
|
}
|
|
InputFilesFound.Add(new InputFile(AppDataConfigLocation, "Global (AppData)"));
|
|
}
|
|
|
|
// Check for the global config file under My Documents/Unreal Engine/UnrealBuildTool
|
|
string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
|
|
if (!String.IsNullOrEmpty(PersonalFolder))
|
|
{
|
|
FileReference PersonalConfigLocation = FileReference.Combine(new DirectoryReference(PersonalFolder), "Unreal Engine", "UnrealBuildTool", "BuildConfiguration.xml");
|
|
if (FileReference.Exists(PersonalConfigLocation))
|
|
{
|
|
InputFilesFound.Add(new InputFile(PersonalConfigLocation, "Global (Documents)"));
|
|
}
|
|
else
|
|
{
|
|
Logger.LogDebug("No config file at {PersonalConfigLocation}", PersonalConfigLocation);
|
|
}
|
|
}
|
|
|
|
CachedInputFiles = InputFilesFound.ToArray();
|
|
|
|
Logger.LogDebug("Configuration will be read from:");
|
|
foreach (InputFile InputFile in InputFiles)
|
|
{
|
|
Logger.LogDebug(" {File}", InputFile.Location.FullName);
|
|
}
|
|
|
|
return CachedInputFiles;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a default config file at the given location
|
|
/// </summary>
|
|
/// <param name="Location">Location to read from</param>
|
|
static void CreateDefaultConfigFile(FileReference Location)
|
|
{
|
|
DirectoryReference.CreateDirectory(Location.Directory);
|
|
using (StreamWriter Writer = new StreamWriter(Location.FullName))
|
|
{
|
|
Writer.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\" ?>");
|
|
Writer.WriteLine("<Configuration xmlns=\"{0}\">", XmlConfigFile.SchemaNamespaceURI);
|
|
Writer.WriteLine("</Configuration>");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies config values to the given object
|
|
/// </summary>
|
|
/// <param name="TargetObject">The object instance to be configured</param>
|
|
public static void ApplyTo(object TargetObject)
|
|
{
|
|
ILogger Logger = Log.Logger;
|
|
for (Type? TargetType = TargetObject.GetType(); TargetType != null; TargetType = TargetType.BaseType)
|
|
{
|
|
XmlConfigData.ValueInfo[]? FieldValues;
|
|
if (Values!.TypeToValues.TryGetValue(TargetType, out FieldValues))
|
|
{
|
|
foreach (XmlConfigData.ValueInfo FieldValue in FieldValues)
|
|
{
|
|
if (!FieldValue.Target.IsStatic)
|
|
{
|
|
XmlConfigData.TargetMember TargetToWrite = FieldValue.Target;
|
|
|
|
// Check if setting has been deprecated
|
|
if (FieldValue.XmlConfigAttribute.Deprecated)
|
|
{
|
|
string CurrentSettingName = FieldValue.XmlConfigAttribute.Name != null ? FieldValue.XmlConfigAttribute.Name : FieldValue.Target.MemberInfo.Name;
|
|
|
|
Logger.LogWarning("Deprecated setting found in \"{SourceFile}\":", FieldValue.SourceFile);
|
|
Logger.LogWarning("The setting \"{Setting}\" is deprecated. Support for this setting will be removed in a future version of Unreal Engine.", CurrentSettingName);
|
|
|
|
if (FieldValue.XmlConfigAttribute.NewAttributeName != null)
|
|
{
|
|
// NewAttributeName is the name of a member in code. However, the log messages below are written from the XML's perspective,
|
|
// so we need to check if the new target member is not exposed under a custom name in the config.
|
|
string NewSettingName = GetMemberConfigAttributeName(TargetType, FieldValue.XmlConfigAttribute.NewAttributeName);
|
|
|
|
Logger.LogWarning("Use \"{NewAttributeName}\" in place of \"{OldAttributeName}\"", NewSettingName, CurrentSettingName);
|
|
Logger.LogInformation("The value provided for deprecated setting \"{OldName}\" will be applied to \"{NewName}\"", CurrentSettingName, NewSettingName);
|
|
|
|
TargetToWrite = GetTargetMember(TargetType, FieldValue.XmlConfigAttribute.NewAttributeName) ?? TargetToWrite;
|
|
}
|
|
}
|
|
|
|
object ValueInstance = InstanceValue(FieldValue.Value, FieldValue.Target.Type);
|
|
TargetToWrite.SetValue(TargetObject, ValueInstance);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string GetMemberConfigAttributeName(Type TargetType, string MemberName)
|
|
{
|
|
MemberInfo? MemberInfo = TargetType.GetRuntimeFields()
|
|
.FirstOrDefault(x => x.Name == MemberName);
|
|
|
|
if (MemberInfo == null)
|
|
{
|
|
MemberInfo = TargetType.GetRuntimeProperties()
|
|
.FirstOrDefault(x => x.Name == MemberName);
|
|
}
|
|
|
|
XmlConfigFileAttribute? Attribute = MemberInfo?.GetCustomAttributes<XmlConfigFileAttribute>().FirstOrDefault();
|
|
|
|
return Attribute?.Name ?? MemberName;
|
|
}
|
|
|
|
private static XmlConfigData.TargetMember? GetTargetMember(Type TargetType, string MemberName)
|
|
{
|
|
// First, try to find the new field to which the setting should be actually applied.
|
|
FieldInfo? FieldInfo = TargetType.GetRuntimeFields()
|
|
.FirstOrDefault(x => x.Name == MemberName);
|
|
|
|
if (FieldInfo != null)
|
|
{
|
|
return new XmlConfigData.TargetField(FieldInfo);
|
|
}
|
|
|
|
// If not found, try to find the new property to which the setting should be actually applied.
|
|
PropertyInfo? PropertyInfo = TargetType.GetRuntimeProperties()
|
|
.FirstOrDefault(x => x.Name == MemberName);
|
|
|
|
if (PropertyInfo != null)
|
|
{
|
|
return new XmlConfigData.TargetProperty(PropertyInfo);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Instances a value for assignment to a target object
|
|
/// </summary>
|
|
/// <param name="Value">The value to instance</param>
|
|
/// <param name="ValueType">The type of value</param>
|
|
/// <returns>New instance of the given value, if necessary</returns>
|
|
static object InstanceValue(object Value, Type ValueType)
|
|
{
|
|
if (ValueType == typeof(string[]))
|
|
{
|
|
return ((string[])Value).Clone();
|
|
}
|
|
else
|
|
{
|
|
return Value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a config value for a single value, without writing it to an instance of that class
|
|
/// </summary>
|
|
/// <param name="TargetType">Type to find config values for</param>
|
|
/// <param name="Name">Name of the field to receive</param>
|
|
/// <param name="Value">On success, receives the value of the field</param>
|
|
/// <returns>True if the value was read, false otherwise</returns>
|
|
public static bool TryGetValue(Type TargetType, string Name, [NotNullWhen(true)] out object? Value)
|
|
{
|
|
// Find all the config values for this type
|
|
XmlConfigData.ValueInfo[]? FieldValues;
|
|
if (!Values!.TypeToValues.TryGetValue(TargetType, out FieldValues))
|
|
{
|
|
Value = null;
|
|
return false;
|
|
}
|
|
|
|
// Find the value with the matching name
|
|
foreach (XmlConfigData.ValueInfo FieldValue in FieldValues)
|
|
{
|
|
if (FieldValue.Target.MemberInfo.Name == Name)
|
|
{
|
|
Value = FieldValue.Value;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Not found
|
|
Value = null;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find all the configurable fields in the given types by searching for XmlConfigFile attributes.
|
|
/// </summary>
|
|
/// <param name="ConfigTypes">Array of types to search</param>
|
|
/// <param name="CategoryToFields">Dictionaries populated with category -> name -> field mappings on return</param>
|
|
static void FindConfigurableFields(IEnumerable<Type> ConfigTypes, Dictionary<string, Dictionary<string, XmlConfigData.TargetMember>> CategoryToFields)
|
|
{
|
|
foreach (Type ConfigType in ConfigTypes)
|
|
{
|
|
foreach (FieldInfo FieldInfo in ConfigType.GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public | BindingFlags.NonPublic))
|
|
{
|
|
ProcessConfigurableMember<FieldInfo>(ConfigType, FieldInfo, CategoryToFields, FieldInfo => new XmlConfigData.TargetField(FieldInfo));
|
|
}
|
|
foreach (PropertyInfo PropertyInfo in ConfigType.GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public | BindingFlags.NonPublic))
|
|
{
|
|
ProcessConfigurableMember<PropertyInfo>(ConfigType, PropertyInfo, CategoryToFields, PropertyInfo => new XmlConfigData.TargetProperty(PropertyInfo));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ProcessConfigurableMember<MEMBER>(Type Type, MEMBER MemberInfo, Dictionary<string, Dictionary<string, XmlConfigData.TargetMember>> CategoryToFields, Func<MEMBER, XmlConfigData.TargetMember> CreateTarget)
|
|
where MEMBER : System.Reflection.MemberInfo
|
|
{
|
|
IEnumerable<XmlConfigFileAttribute> Attributes = MemberInfo.GetCustomAttributes<XmlConfigFileAttribute>();
|
|
foreach (XmlConfigFileAttribute Attribute in Attributes)
|
|
{
|
|
string CategoryName = Attribute.Category ?? Type.Name;
|
|
|
|
Dictionary<string, XmlConfigData.TargetMember>? NameToTarget;
|
|
if (!CategoryToFields.TryGetValue(CategoryName, out NameToTarget))
|
|
{
|
|
NameToTarget = new Dictionary<string, XmlConfigData.TargetMember>();
|
|
CategoryToFields.Add(CategoryName, NameToTarget);
|
|
}
|
|
|
|
NameToTarget[Attribute.Name ?? MemberInfo.Name] = CreateTarget(MemberInfo);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a schema from attributes in the given types
|
|
/// </summary>
|
|
/// <param name="CategoryToFields">Lookup for all field settings</param>
|
|
/// <returns>New schema instance</returns>
|
|
static XmlSchema CreateSchema(Dictionary<string, Dictionary<string, XmlConfigData.TargetMember>> CategoryToFields)
|
|
{
|
|
// Create elements for all the categories
|
|
XmlSchemaAll RootAll = new XmlSchemaAll();
|
|
foreach (KeyValuePair<string, Dictionary<string, XmlConfigData.TargetMember>> CategoryPair in CategoryToFields)
|
|
{
|
|
string CategoryName = CategoryPair.Key;
|
|
|
|
XmlSchemaAll CategoryAll = new XmlSchemaAll();
|
|
foreach (KeyValuePair<string, XmlConfigData.TargetMember> FieldPair in CategoryPair.Value)
|
|
{
|
|
XmlSchemaElement Element = CreateSchemaFieldElement(FieldPair.Key, FieldPair.Value.Type);
|
|
CategoryAll.Items.Add(Element);
|
|
}
|
|
|
|
XmlSchemaComplexType CategoryType = new XmlSchemaComplexType();
|
|
CategoryType.Particle = CategoryAll;
|
|
|
|
XmlSchemaElement CategoryElement = new XmlSchemaElement();
|
|
CategoryElement.Name = CategoryName;
|
|
CategoryElement.SchemaType = CategoryType;
|
|
CategoryElement.MinOccurs = 0;
|
|
CategoryElement.MaxOccurs = 1;
|
|
|
|
RootAll.Items.Add(CategoryElement);
|
|
}
|
|
|
|
// Create the root element and schema object
|
|
XmlSchemaComplexType RootType = new XmlSchemaComplexType();
|
|
RootType.Particle = RootAll;
|
|
|
|
XmlSchemaElement RootElement = new XmlSchemaElement();
|
|
RootElement.Name = XmlConfigFile.RootElementName;
|
|
RootElement.SchemaType = RootType;
|
|
|
|
XmlSchema Schema = new XmlSchema();
|
|
Schema.TargetNamespace = XmlConfigFile.SchemaNamespaceURI;
|
|
Schema.ElementFormDefault = XmlSchemaForm.Qualified;
|
|
Schema.Items.Add(RootElement);
|
|
|
|
// Finally compile it
|
|
XmlSchemaSet SchemaSet = new XmlSchemaSet();
|
|
SchemaSet.Add(Schema);
|
|
SchemaSet.Compile();
|
|
return SchemaSet.Schemas().OfType<XmlSchema>().First();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an XML schema element for reading a value of the given type
|
|
/// </summary>
|
|
/// <param name="Name">Name of the field</param>
|
|
/// <param name="Type">Type of the field</param>
|
|
/// <returns>New schema element representing the field</returns>
|
|
static XmlSchemaElement CreateSchemaFieldElement(string Name, Type Type)
|
|
{
|
|
XmlSchemaElement Element = new XmlSchemaElement();
|
|
Element.Name = Name;
|
|
Element.MinOccurs = 0;
|
|
Element.MaxOccurs = 1;
|
|
|
|
if (TryGetNullableStructType(Type, out Type? InnerType))
|
|
{
|
|
Type = InnerType;
|
|
}
|
|
|
|
if (Type == typeof(string))
|
|
{
|
|
Element.SchemaTypeName = XmlSchemaType.GetBuiltInSimpleType(XmlTypeCode.String).QualifiedName;
|
|
}
|
|
else if (Type == typeof(bool))
|
|
{
|
|
Element.SchemaTypeName = XmlSchemaType.GetBuiltInSimpleType(XmlTypeCode.Boolean).QualifiedName;
|
|
}
|
|
else if (Type == typeof(int))
|
|
{
|
|
Element.SchemaTypeName = XmlSchemaType.GetBuiltInSimpleType(XmlTypeCode.Int).QualifiedName;
|
|
}
|
|
else if (Type == typeof(float))
|
|
{
|
|
Element.SchemaTypeName = XmlSchemaType.GetBuiltInSimpleType(XmlTypeCode.Float).QualifiedName;
|
|
}
|
|
else if (Type == typeof(double))
|
|
{
|
|
Element.SchemaTypeName = XmlSchemaType.GetBuiltInSimpleType(XmlTypeCode.Double).QualifiedName;
|
|
}
|
|
else if (Type == typeof(FileReference))
|
|
{
|
|
Element.SchemaTypeName = XmlSchemaType.GetBuiltInSimpleType(XmlTypeCode.String).QualifiedName;
|
|
}
|
|
else if (Type.IsEnum)
|
|
{
|
|
XmlSchemaSimpleTypeRestriction Restriction = new XmlSchemaSimpleTypeRestriction();
|
|
Restriction.BaseTypeName = XmlSchemaType.GetBuiltInSimpleType(XmlTypeCode.String).QualifiedName;
|
|
|
|
foreach (string EnumName in Enum.GetNames(Type))
|
|
{
|
|
XmlSchemaEnumerationFacet Facet = new XmlSchemaEnumerationFacet();
|
|
Facet.Value = EnumName;
|
|
Restriction.Facets.Add(Facet);
|
|
}
|
|
|
|
XmlSchemaSimpleType EnumType = new XmlSchemaSimpleType();
|
|
EnumType.Content = Restriction;
|
|
Element.SchemaType = EnumType;
|
|
}
|
|
else if (Type == typeof(string[]))
|
|
{
|
|
XmlSchemaElement ItemElement = new XmlSchemaElement();
|
|
ItemElement.Name = "Item";
|
|
ItemElement.SchemaTypeName = XmlSchemaType.GetBuiltInSimpleType(XmlTypeCode.String).QualifiedName;
|
|
ItemElement.MinOccurs = 0;
|
|
ItemElement.MaxOccursString = "unbounded";
|
|
|
|
XmlSchemaSequence Sequence = new XmlSchemaSequence();
|
|
Sequence.Items.Add(ItemElement);
|
|
|
|
XmlSchemaComplexType ArrayType = new XmlSchemaComplexType();
|
|
ArrayType.Particle = Sequence;
|
|
Element.SchemaType = ArrayType;
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("Unsupported field type for XmlConfigFile attribute");
|
|
}
|
|
return Element;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a schema to the given location. Avoids writing it if the file is identical.
|
|
/// </summary>
|
|
/// <param name="Schema">The schema to be written</param>
|
|
/// <param name="Location">Location to write to</param>
|
|
static void WriteSchema(XmlSchema Schema, FileReference Location)
|
|
{
|
|
XmlWriterSettings Settings = new XmlWriterSettings();
|
|
Settings.Indent = true;
|
|
Settings.IndentChars = "\t";
|
|
Settings.NewLineChars = Environment.NewLine;
|
|
Settings.OmitXmlDeclaration = true;
|
|
|
|
if (CachedSchemaSerializer == null)
|
|
{
|
|
CachedSchemaSerializer = XmlSerializer.FromTypes(new Type[] { typeof(XmlSchema) })[0]!;
|
|
}
|
|
|
|
StringBuilder Output = new StringBuilder();
|
|
Output.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
|
|
using (XmlWriter Writer = XmlWriter.Create(Output, Settings))
|
|
{
|
|
XmlSerializerNamespaces Namespaces = new XmlSerializerNamespaces();
|
|
Namespaces.Add("", "http://www.w3.org/2001/XMLSchema");
|
|
CachedSchemaSerializer.Serialize(Writer, Schema, Namespaces);
|
|
}
|
|
|
|
string OutputText = Output.ToString();
|
|
if (!FileReference.Exists(Location) || File.ReadAllText(Location.FullName) != OutputText)
|
|
{
|
|
DirectoryReference.CreateDirectory(Location.Directory);
|
|
File.WriteAllText(Location.FullName, OutputText);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests whether a type is a nullable struct, and extracts the inner type if it is
|
|
/// </summary>
|
|
static bool TryGetNullableStructType(Type Type, [NotNullWhen(true)] out Type? InnerType)
|
|
{
|
|
if (Type.IsGenericType && Type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
|
{
|
|
InnerType = Type.GetGenericArguments()[0];
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
InnerType = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads an XML config file and merges it to the given cache
|
|
/// </summary>
|
|
/// <param name="Location">Location to read from</param>
|
|
/// <param name="CategoryToFields">Lookup for configurable fields by category</param>
|
|
/// <param name="TypeToValues">Map of types to fields and their associated values</param>
|
|
/// <param name="Schema">Schema to validate against</param>
|
|
/// <param name="Logger">Logger for output</param>
|
|
/// <returns>True if the file was read successfully</returns>
|
|
static bool TryReadFile(FileReference Location, Dictionary<string, Dictionary<string, XmlConfigData.TargetMember>> CategoryToFields,
|
|
Dictionary<Type, Dictionary<XmlConfigData.TargetMember, XmlConfigData.ValueInfo>> TypeToValues, XmlSchema Schema, ILogger Logger)
|
|
{
|
|
// Read the XML file, and validate it against the schema
|
|
XmlConfigFile? ConfigFile;
|
|
if (!XmlConfigFile.TryRead(Location, Schema, Logger, out ConfigFile))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Parse the document
|
|
foreach (XmlElement CategoryElement in ConfigFile.DocumentElement!.ChildNodes.OfType<XmlElement>())
|
|
{
|
|
Dictionary<string, XmlConfigData.TargetMember>? NameToField;
|
|
if (CategoryToFields.TryGetValue(CategoryElement.Name, out NameToField))
|
|
{
|
|
foreach (XmlElement KeyElement in CategoryElement.ChildNodes.OfType<XmlElement>())
|
|
{
|
|
XmlConfigData.TargetMember? Field;
|
|
object Value;
|
|
if (NameToField.TryGetValue(KeyElement.Name, out Field))
|
|
{
|
|
if (Field.Type == typeof(string[]))
|
|
{
|
|
Value = KeyElement.ChildNodes.OfType<XmlElement>().Where(x => x.Name == "Item").Select(x => x.InnerText).ToArray();
|
|
}
|
|
else if (TryGetNullableStructType(Field.Type, out Type? StructType))
|
|
{
|
|
Value = ParseValue(StructType, KeyElement.InnerText);
|
|
}
|
|
else
|
|
{
|
|
Value = ParseValue(Field.Type, KeyElement.InnerText);
|
|
}
|
|
|
|
// Add it to the set of values for the type containing this field
|
|
Dictionary<XmlConfigData.TargetMember, XmlConfigData.ValueInfo>? FieldToValue;
|
|
if (!TypeToValues.TryGetValue(Field.MemberInfo.DeclaringType!, out FieldToValue))
|
|
{
|
|
FieldToValue = new Dictionary<XmlConfigData.TargetMember, XmlConfigData.ValueInfo>();
|
|
TypeToValues.Add(Field.MemberInfo.DeclaringType!, FieldToValue);
|
|
}
|
|
|
|
// Parse the corresponding value
|
|
XmlConfigData.ValueInfo FieldValue = new XmlConfigData.ValueInfo(Field, Value, Location,
|
|
Field.MemberInfo.GetCustomAttribute<XmlConfigFileAttribute>()!);
|
|
|
|
FieldToValue[Field] = FieldValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse the value for a field from its text based representation in an XML file
|
|
/// </summary>
|
|
/// <param name="FieldType">The type of field being read</param>
|
|
/// <param name="Text">Text to parse</param>
|
|
/// <returns>The object that was parsed</returns>
|
|
static object ParseValue(Type FieldType, string Text)
|
|
{
|
|
// ignore whitespace in all fields except for Strings which we leave unprocessed
|
|
string TrimmedText = Text.Trim();
|
|
if (FieldType == typeof(string))
|
|
{
|
|
return Text;
|
|
}
|
|
else if (FieldType == typeof(bool) || FieldType == typeof(bool?))
|
|
{
|
|
if (TrimmedText == "1" || TrimmedText.Equals("true", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
else if (TrimmedText == "0" || TrimmedText.Equals("false", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
throw new Exception(String.Format("Unable to convert '{0}' to boolean. 'true/false/0/1' are the supported formats.", Text));
|
|
}
|
|
}
|
|
else if (FieldType == typeof(int))
|
|
{
|
|
return Int32.Parse(TrimmedText);
|
|
}
|
|
else if (FieldType == typeof(float))
|
|
{
|
|
return Single.Parse(TrimmedText, System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
else if (FieldType == typeof(double))
|
|
{
|
|
return Double.Parse(TrimmedText, System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
else if (FieldType.IsEnum)
|
|
{
|
|
return Enum.Parse(FieldType, TrimmedText);
|
|
}
|
|
else if (FieldType == typeof(FileReference))
|
|
{
|
|
return FileReference.FromString(Text);
|
|
}
|
|
else
|
|
{
|
|
throw new Exception(String.Format("Unsupported config type '{0}'", FieldType.Name));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks that the given cache file exists and is newer than the given input files, and attempts to read it. Verifies that the resulting cache was created
|
|
/// from the same input files in the same order.
|
|
/// </summary>
|
|
/// <param name="CacheFile">Path to the config cache file</param>
|
|
/// <param name="InputFiles">The expected set of input files in the cache</param>
|
|
/// <returns>True if the cache was valid and could be read, false otherwise.</returns>
|
|
static bool IsCacheUpToDate(FileReference CacheFile, FileReference[] InputFiles)
|
|
{
|
|
// Always rebuild if the cache doesn't exist
|
|
if (!FileReference.Exists(CacheFile))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Get the timestamp for the cache
|
|
DateTime CacheWriteTime = File.GetLastWriteTimeUtc(CacheFile.FullName);
|
|
|
|
// Always rebuild if this executable is newer
|
|
if (File.GetLastWriteTimeUtc(Assembly.GetExecutingAssembly().Location) > CacheWriteTime)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if any of the input files are newer than the cache
|
|
foreach (FileReference InputFile in InputFiles)
|
|
{
|
|
if (File.GetLastWriteTimeUtc(InputFile.FullName) > CacheWriteTime)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Otherwise, it's up to date
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates documentation files for the available settings, by merging the XML documentation from the compiler.
|
|
/// </summary>
|
|
/// <param name="OutputFile">The documentation file to write</param>
|
|
/// <param name="Logger">Logger for output</param>
|
|
public static void WriteDocumentation(FileReference OutputFile, ILogger Logger)
|
|
{
|
|
// Find all the configurable types
|
|
List<Type> ConfigTypes = FindConfigurableTypes();
|
|
|
|
// Find all the configurable fields from the given types
|
|
Dictionary<string, Dictionary<string, XmlConfigData.TargetMember>> CategoryToFields = new Dictionary<string, Dictionary<string, XmlConfigData.TargetMember>>();
|
|
FindConfigurableFields(ConfigTypes, CategoryToFields);
|
|
CategoryToFields = CategoryToFields.Where(x => x.Value.Count > 0).ToDictionary(x => x.Key, x => x.Value);
|
|
|
|
// Get the path to the XML documentation
|
|
FileReference InputDocumentationFile = new FileReference(Assembly.GetExecutingAssembly().Location).ChangeExtension(".xml");
|
|
if (!FileReference.Exists(InputDocumentationFile))
|
|
{
|
|
throw new BuildException("Generated assembly documentation not found at {0}.", InputDocumentationFile);
|
|
}
|
|
|
|
// Read the documentation
|
|
XmlDocument InputDocumentation = new XmlDocument();
|
|
InputDocumentation.Load(InputDocumentationFile.FullName);
|
|
|
|
// Make sure we can write to the output file
|
|
if (FileReference.Exists(OutputFile))
|
|
{
|
|
FileReference.MakeWriteable(OutputFile);
|
|
}
|
|
else
|
|
{
|
|
DirectoryReference.CreateDirectory(OutputFile.Directory);
|
|
}
|
|
|
|
// Generate the documentation file
|
|
if (OutputFile.HasExtension(".udn"))
|
|
{
|
|
WriteDocumentationUDN(OutputFile, InputDocumentation, CategoryToFields, Logger);
|
|
}
|
|
else if (OutputFile.HasExtension(".html"))
|
|
{
|
|
WriteDocumentationHTML(OutputFile, InputDocumentation, CategoryToFields, Logger);
|
|
}
|
|
else
|
|
{
|
|
throw new BuildException("Unable to detect format from extension of output file ({0})", OutputFile);
|
|
}
|
|
|
|
// Success!
|
|
Logger.LogInformation("Written documentation to {OutputFile}.", OutputFile);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes out documentation in UDN format
|
|
/// </summary>
|
|
/// <param name="OutputFile">The output file</param>
|
|
/// <param name="InputDocumentation">The XML documentation for this assembly</param>
|
|
/// <param name="CategoryToFields">Map of string to types to fields</param>
|
|
/// <param name="Logger">Logger for output</param>
|
|
private static void WriteDocumentationUDN(FileReference OutputFile, XmlDocument InputDocumentation, Dictionary<string, Dictionary<string, XmlConfigData.TargetMember>> CategoryToFields, ILogger Logger)
|
|
{
|
|
using (StreamWriter Writer = new StreamWriter(OutputFile.FullName))
|
|
{
|
|
Writer.WriteLine("Availability: NoPublish");
|
|
Writer.WriteLine("Title: Build Configuration Properties Page");
|
|
Writer.WriteLine("Crumbs:");
|
|
Writer.WriteLine("Description: This is a procedurally generated markdown page.");
|
|
Writer.WriteLine("Version: {0}.{1}", ReadOnlyBuildVersion.Current.MajorVersion, ReadOnlyBuildVersion.Current.MinorVersion);
|
|
Writer.WriteLine("");
|
|
|
|
foreach (KeyValuePair<string, Dictionary<string, XmlConfigData.TargetMember>> CategoryPair in CategoryToFields)
|
|
{
|
|
string CategoryName = CategoryPair.Key;
|
|
Writer.WriteLine("### {0}", CategoryName);
|
|
Writer.WriteLine();
|
|
|
|
Dictionary<string, XmlConfigData.TargetMember> Fields = CategoryPair.Value;
|
|
foreach (KeyValuePair<string, XmlConfigData.TargetMember> FieldPair in Fields)
|
|
{
|
|
// Get the XML comment for this field
|
|
List<string>? Lines;
|
|
if (!RulesDocumentation.TryGetXmlComment(InputDocumentation, FieldPair.Value.MemberInfo, Logger, out Lines) || Lines.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Write the result to the .udn file
|
|
Writer.WriteLine("$ {0} : {1}", FieldPair.Key, Lines[0]);
|
|
for (int Idx = 1; Idx < Lines.Count; Idx++)
|
|
{
|
|
if (Lines[Idx].StartsWith("*") || Lines[Idx].StartsWith("-"))
|
|
{
|
|
Writer.WriteLine(" * {0}", Lines[Idx].Substring(1).TrimStart());
|
|
}
|
|
else
|
|
{
|
|
Writer.WriteLine(" * {0}", Lines[Idx]);
|
|
}
|
|
}
|
|
Writer.WriteLine();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes out documentation in HTML format
|
|
/// </summary>
|
|
/// <param name="OutputFile">The output file</param>
|
|
/// <param name="InputDocumentation">The XML documentation for this assembly</param>
|
|
/// <param name="CategoryToFields">Map of string to types to fields</param>
|
|
/// <param name="Logger">Logger for output</param>
|
|
private static void WriteDocumentationHTML(FileReference OutputFile, XmlDocument InputDocumentation, Dictionary<string, Dictionary<string, XmlConfigData.TargetMember>> CategoryToFields, ILogger Logger)
|
|
{
|
|
using (StreamWriter Writer = new StreamWriter(OutputFile.FullName))
|
|
{
|
|
Writer.WriteLine("<html>");
|
|
Writer.WriteLine(" <body>");
|
|
Writer.WriteLine(" <h2>BuildConfiguration Properties</h2>");
|
|
foreach (KeyValuePair<string, Dictionary<string, XmlConfigData.TargetMember>> CategoryPair in CategoryToFields)
|
|
{
|
|
string CategoryName = CategoryPair.Key;
|
|
Writer.WriteLine(" <h3>{0}</h3>", CategoryName);
|
|
Writer.WriteLine(" <dl>");
|
|
|
|
Dictionary<string, XmlConfigData.TargetMember> Fields = CategoryPair.Value;
|
|
foreach (KeyValuePair<string, XmlConfigData.TargetMember> FieldPair in Fields)
|
|
{
|
|
// Get the XML comment for this field
|
|
List<string>? Lines;
|
|
if (!RulesDocumentation.TryGetXmlComment(InputDocumentation, FieldPair.Value.MemberInfo, Logger, out Lines) || Lines.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Write the result to the .udn file
|
|
Writer.WriteLine(" <dt>{0}</dt>", FieldPair.Key);
|
|
|
|
if (Lines.Count == 1)
|
|
{
|
|
Writer.WriteLine(" <dd>{0}</dd>", Lines[0]);
|
|
}
|
|
else
|
|
{
|
|
Writer.WriteLine(" <dd>");
|
|
for (int Idx = 0; Idx < Lines.Count; Idx++)
|
|
{
|
|
if (Lines[Idx].StartsWith("*") || Lines[Idx].StartsWith("-"))
|
|
{
|
|
Writer.WriteLine(" <ul>");
|
|
for (; Idx < Lines.Count && (Lines[Idx].StartsWith("*") || Lines[Idx].StartsWith("-")); Idx++)
|
|
{
|
|
Writer.WriteLine(" <li>{0}</li>", Lines[Idx].Substring(1).TrimStart());
|
|
}
|
|
Writer.WriteLine(" </ul>");
|
|
}
|
|
else
|
|
{
|
|
Writer.WriteLine(" {0}", Lines[Idx]);
|
|
}
|
|
}
|
|
Writer.WriteLine(" </dd>");
|
|
}
|
|
}
|
|
|
|
Writer.WriteLine(" </dl>");
|
|
}
|
|
Writer.WriteLine(" </body>");
|
|
Writer.WriteLine("</html>");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|