// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections; 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.Linq; using System.Xml.Schema; using System.Xml.Serialization; using EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Functions for manipulating the XML config cache /// static class XmlConfig { /// /// An input config file /// public class InputFile { /// /// Location of the file /// public FileReference Location { get; init; } /// /// Which folder to display the config file under in the generated project files /// public string FolderName { get; init; } /// /// Constructor /// /// /// public InputFile(FileReference location, string folderName) { Location = location; FolderName = folderName; } } /// /// The cache file that is being used /// public static FileReference? CacheFile; /// /// Parsed config values /// static XmlConfigData? s_values; /// /// Cached serializer for the XML schema /// static XmlSerializer? s_cachedSchemaSerializer; /// /// Initialize the config system with the given types /// /// Force use of the cached XML config without checking if it's valid (useful for remote builds) /// Read XML configuration with a Project directory /// Logger for output public static void ReadConfigFiles(FileReference? overrideCacheFile, DirectoryReference? projectRootDirectory, ILogger logger) { // Find all the configurable types List configTypes = FindConfigurableTypes(); if (projectRootDirectory == null) { logger.LogDebug("No project directory provided for project scope BuildConfiguration.xml resolution."); } // 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 s_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", $"XmlConfigCache-{Unreal.RootDirectory.FullName.Replace(":", "", StringComparison.OrdinalIgnoreCase).Replace(Path.DirectorySeparatorChar, '+')}.bin"); } } } else { CacheFile = FileReference.Combine(projectRootDirectory, "Intermediate", "Build", "XmlConfigCache.bin"); s_values = null; } // Find all the input files List temporaryInputFileLocations = InputFiles.Select(x => x.Location).ToList(); if (projectRootDirectory != null) { FileReference projectRootConfigLocation = FileReference.Combine(projectRootDirectory, "Saved", "UnrealBuildTool", "BuildConfiguration.xml"); if (!FileReference.Exists(projectRootConfigLocation)) { logger.LogDebug("No project scope config file at {ProjectRootConfigLocation}. Creating default.", projectRootConfigLocation.FullName); CreateDefaultConfigFile(projectRootConfigLocation); } temporaryInputFileLocations.Add(projectRootConfigLocation); } // Write any configuration environment variables to a file so they can be loaded FileReference environmentXml = FileReference.Combine(Unreal.WritableEngineDirectory, "Intermediate", "Build", "UnrealBuildTool.Env.BuildConfiguration.xml"); WriteEnvironmentXml(environmentXml, logger); temporaryInputFileLocations.Add(environmentXml); 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)) { s_values = cachedValues; } } // If that failed, regenerate it if (s_values == null) { // Find all the configurable fields from the given types Dictionary> categoryToFields = new Dictionary>(); 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> typeToValues = new Dictionary>(); foreach (FileReference inputFile in inputFileLocations) { logger.LogDebug("Reading configuration file from: {Location}", inputFile.FullName); 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 s_values = new XmlConfigData(inputFileLocations, typeToValues.ToDictionary( x => x.Key, x => x.Value.Select(x => x.Value).ToArray())); s_values.Write(CacheFile); } else { foreach (FileReference inputFile in s_values.InputFiles) { logger.LogDebug("Reading configuration file from cache ({CacheLocaltion}): {Location}", CacheFile.FullName, inputFile.FullName); } } } // Apply all the static field values foreach (KeyValuePair typeValuesPair in s_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); } } } } /// /// Find all the configurable types in the current assembly /// /// List of configurable types static List FindConfigurableTypes() { List configTypes = new List(); 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; } /// /// Determines whether the given type has a field with an XmlConfigFile attribute /// /// The type to check /// True if the type has a field with the XmlConfigFile attribute 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; } /// /// Find the location of the XML config schema /// /// Optional project root directory /// The location of the schema file 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[]? s_cachedInputFiles; /// /// Initialize the list of input files /// public static InputFile[] InputFiles { get { if (s_cachedInputFiles != null) { return s_cachedInputFiles; } ILogger logger = Log.Logger; // Find all the input file locations List inputFilesFound = new List(5); // InputFile info and if a default file should be created if missing List> configs = new(); // Skip all the config files under the Engine folder if it's an installed build if (!Unreal.IsEngineInstalled()) { // Check for the engine config file under /Engine/Programs/NotForLicensees/UnrealBuildTool configs.Add(new(new InputFile(FileReference.Combine(Unreal.EngineDirectory, "Restricted", "NotForLicensees", "Programs", "UnrealBuildTool", "BuildConfiguration.xml"), "Engine (NotForLicensees)"), false)); // Check for the engine user config file under /Engine/Saved/UnrealBuildTool configs.Add(new(new InputFile(FileReference.Combine(Unreal.EngineDirectory, "Saved", "UnrealBuildTool", "BuildConfiguration.xml"), "Engine (Saved)"), true)); } // Check for the global config file under ProgramData/Unreal Engine/UnrealBuildTool DirectoryReference? commonProgramsFolder = DirectoryReference.FromString(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)); if (commonProgramsFolder != null) { configs.Add(new(new InputFile(FileReference.Combine(commonProgramsFolder, "Unreal Engine", "UnrealBuildTool", "BuildConfiguration.xml"), "Global (ProgramData)"), false)); } // Check for the global config file under AppData/Unreal Engine/UnrealBuildTool (Roaming) DirectoryReference? appDataFolder = DirectoryReference.FromString(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); if (appDataFolder != null) { configs.Add(new(new InputFile(FileReference.Combine(appDataFolder, "Unreal Engine", "UnrealBuildTool", "BuildConfiguration.xml"), "Global (AppData)"), true)); } // Check for the global config file under LocalAppData/Unreal Engine/UnrealBuildTool DirectoryReference? localAppDataFolder = DirectoryReference.FromString(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)); if (localAppDataFolder != null) { configs.Add(new(new InputFile(FileReference.Combine(localAppDataFolder, "Unreal Engine", "UnrealBuildTool", "BuildConfiguration.xml"), "Global (LocalAppData)"), false)); } // Check for the global config file under My Documents/Unreal Engine/UnrealBuildTool DirectoryReference? personalFolder = DirectoryReference.FromString(Environment.GetFolderPath(Environment.SpecialFolder.Personal)); if (personalFolder != null) { configs.Add(new(new InputFile(FileReference.Combine(personalFolder, "Unreal Engine", "UnrealBuildTool", "BuildConfiguration.xml"), "Global (Documents)"), false)); } foreach (KeyValuePair config in configs) { if (config.Value && !FileReference.Exists(config.Key.Location)) { logger.LogDebug("Creating default config file at {ConfigLocation}", config.Key.Location); CreateDefaultConfigFile(config.Key.Location); } if (FileReference.Exists(config.Key.Location)) { inputFilesFound.Add(config.Key); } else { logger.LogDebug("No config file at {ConfigLocation}", config.Key.Location); } } s_cachedInputFiles = inputFilesFound.ToArray(); logger.LogDebug("Configuration will be read from:"); foreach (InputFile inputFile in InputFiles) { logger.LogDebug(" {File}", inputFile.Location.FullName); } return s_cachedInputFiles; } } /// /// Create a default config file at the given location /// /// Location to read from static void CreateDefaultConfigFile(FileReference location) { DirectoryReference.CreateDirectory(location.Directory); using (StreamWriter writer = new StreamWriter(location.FullName)) { writer.WriteLine(""); writer.WriteLine("", XmlConfigFile.SchemaNamespaceURI); writer.WriteLine(""); } } /// /// Applies config values to the given object /// /// The object instance to be configured public static void ApplyTo(object targetObject) { ILogger logger = Log.Logger; for (Type? targetType = targetObject.GetType(); targetType != null; targetType = targetType.BaseType) { XmlConfigData.ValueInfo[]? fieldValues; if (s_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 ?? 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) as MemberInfo ?? targetType.GetRuntimeProperties().FirstOrDefault(x => x.Name == memberName) as MemberInfo; XmlConfigFileAttribute? attribute = memberInfo?.GetCustomAttributes().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; } /// /// Instances a value for assignment to a target object /// /// The value to instance /// The type of value /// New instance of the given value, if necessary static object InstanceValue(object value, Type valueType) { if (valueType == typeof(string[])) { return ((string[])value).Clone(); } else { return value; } } /// /// Gets a config value for a single value, without writing it to an instance of that class /// /// Type to find config values for /// Name of the field to receive /// On success, receives the value of the field /// True if the value was read, false otherwise 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 (!s_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; } /// /// Find all the configurable fields in the given types by searching for XmlConfigFile attributes. /// /// Array of types to search /// Dictionaries populated with category -> name -> field mappings on return static void FindConfigurableFields(IEnumerable configTypes, Dictionary> categoryToFields) { foreach (Type configType in configTypes) { foreach (FieldInfo fieldInfo in configType.GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public | BindingFlags.NonPublic)) { ProcessConfigurableMember(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(configType, propertyInfo, categoryToFields, propertyInfo => new XmlConfigData.TargetProperty(propertyInfo)); } } } private static void ProcessConfigurableMember(Type type, MEMBER memberInfo, Dictionary> categoryToFields, Func createTarget) where MEMBER : System.Reflection.MemberInfo { IEnumerable attributes = memberInfo.GetCustomAttributes(); foreach (XmlConfigFileAttribute attribute in attributes) { string categoryName = attribute.Category ?? type.Name; Dictionary? nameToTarget; if (!categoryToFields.TryGetValue(categoryName, out nameToTarget)) { nameToTarget = new Dictionary(); categoryToFields.Add(categoryName, nameToTarget); } nameToTarget[attribute.Name ?? memberInfo.Name] = createTarget(memberInfo); } } /// /// Creates a schema from attributes in the given types /// /// Lookup for all field settings /// New schema instance static XmlSchema CreateSchema(Dictionary> categoryToFields) { // Create elements for all the categories XmlSchemaAll rootAll = new XmlSchemaAll(); foreach (KeyValuePair> categoryPair in categoryToFields) { string categoryName = categoryPair.Key; XmlSchemaAll categoryAll = new XmlSchemaAll(); foreach (KeyValuePair 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().First(); } /// /// Creates an XML schema element for reading a value of the given type /// /// Name of the field /// Type of the field /// New schema element representing the field 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; } /// /// Writes a schema to the given location. Avoids writing it if the file is identical. /// /// The schema to be written /// Location to write to static void WriteSchema(XmlSchema schema, FileReference location) { XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.IndentChars = "\t"; settings.NewLineChars = Environment.NewLine; settings.OmitXmlDeclaration = true; s_cachedSchemaSerializer ??= XmlSerializer.FromTypes(new Type[] { typeof(XmlSchema) })[0]!; StringBuilder output = new StringBuilder(); output.AppendLine(""); using (XmlWriter writer = XmlWriter.Create(output, settings)) { XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces(); namespaces.Add("", "http://www.w3.org/2001/XMLSchema"); s_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); } } /// /// Tests whether a type is a nullable struct, and extracts the inner type if it is /// 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; } } /// /// Reads an XML config file and merges it to the given cache /// /// Location to read from /// Lookup for configurable fields by category /// Map of types to fields and their associated values /// Schema to validate against /// Logger for output /// True if the file was read successfully static bool TryReadFile(FileReference location, Dictionary> categoryToFields, Dictionary> 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()) { Dictionary? nameToField; if (categoryToFields.TryGetValue(categoryElement.Name, out nameToField)) { foreach (XmlElement keyElement in categoryElement.ChildNodes.OfType()) { if (nameToField.TryGetValue(keyElement.Name, out XmlConfigData.TargetMember? field)) { object value; if (field.Type == typeof(string[])) { value = keyElement.ChildNodes.OfType().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? fieldToValue; if (!typeToValues.TryGetValue(field.MemberInfo.DeclaringType!, out fieldToValue)) { fieldToValue = new Dictionary(); typeToValues.Add(field.MemberInfo.DeclaringType!, fieldToValue); } // Parse the corresponding value XmlConfigData.ValueInfo fieldValue = new XmlConfigData.ValueInfo(field, value, location, field.MemberInfo.GetCustomAttribute()!); fieldToValue[field] = fieldValue; } } } } return true; } /// /// Writes environment variables as a BuildConfiguration xml /// /// Path to write to /// Logger for output /// True if the environment was written successfully static bool WriteEnvironmentXml(FileReference location, ILogger logger) { // Read the environment variables Dictionary> contents = []; foreach (DictionaryEntry EnvironmentVariable in Environment.GetEnvironmentVariables()) { string key = EnvironmentVariable.Key?.ToString() ?? String.Empty; string value = EnvironmentVariable.Value?.ToString() ?? String.Empty; if (!key.StartsWith("UnrealBuildTool_") || !key.Contains("__")) { continue; } string[] split = key.Substring("UnrealBuildTool_".Length).Split("__"); string category = split.First(); string field = split.Last(); if (!contents.TryGetValue(category, out Dictionary? fields)) { fields = new(); contents.Add(category, fields); } fields.Add(field, value); } StringBuilder output = new StringBuilder(); output.AppendLine(""); XmlWriterSettings xmlSettings = new XmlWriterSettings(); xmlSettings.Encoding = new UTF8Encoding(false); xmlSettings.Indent = true; xmlSettings.OmitXmlDeclaration = true; using (XmlWriter writer = XmlWriter.Create(output, xmlSettings)) { XNamespace ns = XNamespace.Get("https://www.unrealengine.com/BuildConfiguration"); XElement root = new XElement(ns + "Configuration"); foreach (KeyValuePair> CategoryItem in contents) { XElement categoryElement = new XElement(ns + CategoryItem.Key); foreach (KeyValuePair FieldItem in CategoryItem.Value) { XElement fieldElement = new XElement(ns + FieldItem.Key); if (FieldItem.Value.Contains(';')) { foreach (string item in FieldItem.Value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { fieldElement.Add(new XElement(ns + "Item", item)); } } else { fieldElement.Add(FieldItem.Value); } categoryElement.Add(fieldElement); } root.Add(categoryElement); } new XDocument(root).Save(writer); } Utils.WriteFileIfChanged(location, output.ToString(), logger); return true; } /// /// Parse the value for a field from its text based representation in an XML file /// /// The type of field being read /// Text to parse /// The object that was parsed 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.OrdinalIgnoreCase)) { return true; } else if (trimmedText == "0" || trimmedText.Equals("false", StringComparison.OrdinalIgnoreCase)) { 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)); } } /// /// 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. /// /// Path to the config cache file /// The expected set of input files in the cache /// True if the cache was valid and could be read, false otherwise. 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; } /// /// Generates documentation files for the available settings, by merging the XML documentation from the compiler. /// /// The documentation file to write /// Logger for output public static void WriteDocumentation(FileReference outputFile, ILogger logger) { // Find all the configurable types List configTypes = FindConfigurableTypes(); // Find all the configurable fields from the given types Dictionary> categoryToFields = new Dictionary>(); 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); } /// /// Writes out documentation in UDN format /// /// The output file /// The XML documentation for this assembly /// Map of string to types to fields /// Logger for output private static void WriteDocumentationUDN(FileReference outputFile, XmlDocument inputDocumentation, Dictionary> 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> categoryPair in categoryToFields) { string categoryName = categoryPair.Key; writer.WriteLine("### {0}", categoryName); writer.WriteLine(); Dictionary fields = categoryPair.Value; foreach (KeyValuePair fieldPair in fields) { // Get the XML comment for this field List? 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("*", StringComparison.OrdinalIgnoreCase) || lines[idx].StartsWith("-", StringComparison.OrdinalIgnoreCase)) { writer.WriteLine(" * {0}", lines[idx].Substring(1).TrimStart()); } else { writer.WriteLine(" * {0}", lines[idx]); } } writer.WriteLine(); } } } } /// /// Writes out documentation in HTML format /// /// The output file /// The XML documentation for this assembly /// Map of string to types to fields /// Logger for output private static void WriteDocumentationHTML(FileReference outputFile, XmlDocument inputDocumentation, Dictionary> categoryToFields, ILogger logger) { using (StreamWriter writer = new StreamWriter(outputFile.FullName)) { writer.WriteLine(""); writer.WriteLine(" "); writer.WriteLine("

BuildConfiguration Properties

"); foreach (KeyValuePair> categoryPair in categoryToFields) { string categoryName = categoryPair.Key; writer.WriteLine("

{0}

", categoryName); writer.WriteLine("
"); Dictionary fields = categoryPair.Value; foreach (KeyValuePair fieldPair in fields) { // Get the XML comment for this field List? 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}
", fieldPair.Key); if (lines.Count == 1) { writer.WriteLine("
{0}
", lines[0]); } else { writer.WriteLine("
"); for (int idx = 0; idx < lines.Count; idx++) { if (lines[idx].StartsWith("*", StringComparison.OrdinalIgnoreCase) || lines[idx].StartsWith("-", StringComparison.OrdinalIgnoreCase)) { writer.WriteLine("
    "); for (; idx < lines.Count && (lines[idx].StartsWith("*", StringComparison.OrdinalIgnoreCase) || lines[idx].StartsWith("-", StringComparison.OrdinalIgnoreCase)); idx++) { writer.WriteLine("
  • {0}
  • ", lines[idx].Substring(1).TrimStart()); } writer.WriteLine("
"); } else { writer.WriteLine(" {0}", lines[idx]); } } writer.WriteLine("
"); } } writer.WriteLine("
"); } writer.WriteLine(" "); writer.WriteLine(""); } } } }