Files
UnrealEngineUWP/Engine/Source/Programs/AutomationTool/BuildGraph/Script.cs
Ben Marsh 3dbefdf14d Copying //UE4/Dev-Build to //UE4/Dev-Main (Source: //UE4/Dev-Build @ 3047776)
#lockdown Nick.Penwarden
#rb none

==========================
MAJOR FEATURES + CHANGES
==========================

Change 3021930 on 2016/06/21 by Ben.Marsh

	BuildGraph: Better diagnostic message if the source directory for copies does not exist.

Change 3022391 on 2016/06/21 by Ben.Marsh

	Rework copy task slightly so that all code paths result in files being tagged.

Change 3026592 on 2016/06/24 by Ben.Marsh

	BuildGraph: Add a ForEach element, which will assign a local property to each of a semicolon separated list of values, and expand the elements within it. Added an example in Properties.xml.

Change 3028708 on 2016/06/27 by Matthew.Griffin

	Converting Engine build process to BuildGraph script
	Added Tag Receipts task to retrieve list of build products/dependencies from *.target files.
	Changed Pak File task so that you can specify an existing response file, rather than creating one from the file list.
	Changed base task so that you can resolve filespec from a list of file patterns if you already have them separated, which was the case with wildcards in runtime dependencies.
	Added EngineMajorVersion, EngineMinorVersion and EnginePatchVersion as default properties available to BuildGraph
	Added FinalizeInstalledBuild command to write out InstalledBuild.txt file and config entries for installed platforms
	Included .exe.config and exe.mdb files to build products of CsCompile task if they exist
	Added TagReferences option to CsCompile so that you can get any external references projects have that need to be included when staging
	Added RunOptions parameter to SpawnTask, so that you can specify these for the exe you want to run
	Added missing Runtime Dependency for ICU on Mac

Change 3030209 on 2016/06/28 by Matthew.Griffin

	Renamed EngineBuild.xml to InstalledEngineBuild.xml to make its purpose more clear.
	Removed reference to xcodeunlock.sh from Mac Installed build dependencies as the file itself has been deleted.
	Added myself to list of notifiers for failures in the UE4 Binary build.

Change 3034068 on 2016/06/30 by Ben.Marsh

	BuildGraph: Change scoping rules for properties. Local properties can no longer shadow global properties with the same name (or vice versa), and local properties are always modified in the scope that they were first declared, rather than being re-declared in a narrower scope.

Change 3034070 on 2016/06/30 by Ben.Marsh

	BuildGraph: Warn when referencing a property which is not defined, and add new attributes to the <Property> element to set the default value for a property if it's not already set, and validating that it's one of a list of valid values if it is (eg. <Property Name="WithWin64" Restrict="true;false" Default="false"/>).

Change 3034110 on 2016/06/30 by Matthew.Griffin

	Updated Installed Build so that properties are consistently named Exceptions and that the right versions are used
	Added Filter and Exception properties for each target platform to add any files that can't be figured out via dependencies
	Added Default values for various properties used across Engine build scripts - IsReleaseBranch, IsPreflight, OutputDir, BuildLabel, WithWin64 etc.
	Tagged Generated Includes from each target so that they can be included in Installed Build
	Added additional Android architectures to Shipping build
	Changed SwarmCoordinator to build for Any CPU
	Removed Local HostPlatform property from DDC nodes
	Changed Installed Build target platforms to use Do blocks so that we only have to check With... property once
	Reordered stripping and signing process so that we use the Exception check in less places

Change 3035499 on 2016/07/01 by Ben.Marsh

	BuildGraph: Remove the <Local> element, and just make all <Property> declarations scoped. Also add an error if a property is later declared in a parent scope, since the earlier assignment won't be visible to the later one.

Change 3035520 on 2016/07/01 by Ben.Marsh

	BuildGraph: Add support for <, <=, >, >= operators in condition expressions.

Change 3035666 on 2016/07/01 by Matthew.Griffin

	Added more parameters to Chunk and Label Build tasks
	Updated all remaining uses of Local to Property in Installed Build script
	Made sure Feature Packs use paths compatible with Mac and also changed the node to use a ForEach element

Change 3037020 on 2016/07/04 by Matthew.Griffin

	Ensured that TempStorageFileList uses forward slashes as its path separators so that it's easily used on Mac and Windows
	Was causing the results of the Make Feature Packs node to be tagged using Windows style paths, meaning they would throw an error if you tried to copy them on Mac

Change 3037052 on 2016/07/04 by Ben.Marsh

	Move FJsonValue::ErrorMessage into cpp file, since it depends on the log class defined in Json.h (which includes it).

Change 3037283 on 2016/07/05 by Matthew.Griffin

	Removed EnterScope and LeaveScope from ReadGraphBody so that included files are treated as being in the same scope (allows use of properties across files)

Change 3037547 on 2016/07/05 by Ben.Marsh

	UAT: Allow CommandUtils.Run() to check directories listed in the PATH environment variable for the executable before failing.

Change 3037552 on 2016/07/05 by Ben.Marsh

	BuildGraph: Add an <Unzip> task, which extracts a zip file to an output directory.

Change 3039109 on 2016/07/06 by Matthew.Griffin

	Moved tagging of UAT build products to the Installed Build step as it's the only thing that needs them
	Moved Strip and Sign filters to the filters file, made sure they're used for all operations and added stripping back to UE4Editor nodes
	Changed BuildPatchTool to be built in shipping mode
	Changed all C# projects to be compiled for AnyCPU as they ended up in different output folders otherwise
	Added all files referenced by C# projects to avoid having to filter them manually
	Changed filters to get files included for Linux closer to the old pattern
	Changed Build DDC command to ignore empty entries in FeaturePacks list, don't want to fail the process if a list begins with a ;
	Changed UE4Game to use shipping PhysX libs for Shipping builds
	Added glut32.dll as a Runtime Dependency for PhysX
	Added libsteam_api.so as a Runtime Dependency for Steamworks on Linux

Change 3039676 on 2016/07/06 by Ben.Marsh

	Core: Move definitions for FORCEINLINE'd FMath functions into UnrealMathUtility. Prevents link errors if including one without the other.

Change 3039681 on 2016/07/06 by Ben.Marsh

	Core: Move implementation of GetTypeHash(FTimespan) into CPP file, to remove implicit dependency on the inline implementation of GetTypeHash(int64) being included.

Change 3039735 on 2016/07/06 by Ben.Marsh

	Core: Move USE_DELEGATE_TRYGETBOUNDFUNCTIONNAME into a separate header, so delegate headers can be included separately.

Change 3039878 on 2016/07/06 by Ben.Marsh

	Core: Move FOperatorFunctionID out of TOperatorJumpTable to allow MSVC to compile it and catch errors before the template is instantiated.

Change 3040156 on 2016/07/06 by Ben.Marsh

	Core: Move FDateTime::GetTypeHash() into cpp file to eliminate dependency on TypeHash.h being included before it.

Change 3041009 on 2016/07/07 by Matthew.Griffin

	Changed UE4Game to only use shipping PhysX libraries on Windows

Change 3041015 on 2016/07/07 by Leigh.Swift

	UBT: Support creating C# programs that will be included in the UE4.sln Programs list.
	To have your program listed, remove the sln file that may have been created for you, and add a file named "UE4CSharp.prog" next to your csproj file.

Change 3041234 on 2016/07/07 by Matthew.Griffin

	Added building of Launcher Samples to BuildGraph system
	Added Command to Build Sample projects, which distills to temp directory, builds DDC if needed and then chunks/posts to MCP

Change 3041244 on 2016/07/07 by Ben.Marsh

	Core: Change PlatformIncludes.h to include all the individual PlatformMemory.h, PlatformTime.h, etc... headers rather than including separate per-platform headers which include them all. Makes it much easier to optimize header file usage, and eliminates redundant typedefs in the individual Platform*.h files. Also fixes some headers that previously didn't compile.

Change 3042518 on 2016/07/08 by Matthew.Griffin

	Added content modifiers to those notified about Sample failures
	Throw exception if RocketPromoteBuild tries to promote all samples
	Throw exceptions for missing parameters in BuildLauncherSample command, corrected EngineDir parameter name.

Change 3042545 on 2016/07/08 by Ben.Marsh

	Core: Push/Pop defines for MAX_uint8, MAX_uint16, MAX_uint32, MAX_int32 around Windows.h includes, so we don't need to be careful about the order in which we include NumericLimits.h.

Change 3042546 on 2016/07/08 by Ben.Marsh

	Core: Put standard CRT includes into their own header, so we can include it without taking all of PlatformIncludes.h (and make any platform-specific additions as needed)

Change 3042548 on 2016/07/08 by Ben.Marsh

	Core: Include PlatformCompilerSetup headers from Platform.h, as well as all the defaults for non-platform overriden defines. Allows including Platform.h to get all the basic types, defines and compile environment set up without having to include a large number of system headers or unnecessary functionality.

Change 3044424 on 2016/07/11 by Ben.Marsh

	Merge fixes for QFE installer (CL 3044412) from 4.11 branch.

Change 3044584 on 2016/07/11 by Ben.Marsh

	Core: Move FMath::FormatIntToHumanReadable() to UnrealMath.cpp, since it's a very large/expensive function to try to inline (and introduce a FString dependency for)

Change 3044603 on 2016/07/11 by Matthew.Griffin

	Added PS4 and XboxOne to installed build as options that will always be disabled by default
	Standardised some of the agent names
	Removed logging from the Installed Build nodes as it takes a huge amount of time to write out the list for little reward

Change 3044608 on 2016/07/11 by Ben.Marsh

	Core: Split out definition of SIMD VectorRegister class into its own header, so it's not forcibly included with UnrealMathUtility.

Change 3044638 on 2016/07/11 by Matthew.Griffin

	Added internal build jobs for all games with compile, cook and package nodes.
	Added Documentation, Localization and NonUnity steps.

Change 3045959 on 2016/07/12 by Matthew.Griffin

	Removed Aggregates from Installed Build script as they weren't used/necessary.

Change 3045961 on 2016/07/12 by Matthew.Griffin

	Fixed various issues with Full Build
	Switch to build non-client/server configurations for some games
	Included PS4 and Xbox game targets in our internal monolithics aggregate
	Added Requirements for steps that need UHT, SCW etc.
	Added list of Packaged Game Nodes that we can build up as they're defined
	Added targets that were previously in the Internal Tools nodes
	Changed APIDocTool to build Release as that's what the solution uses and made use of the path created for it
	Removed -clean from the NonUnity targets as that doesn't actually build anything
	Changed mail notifications so that individual nodes are used for content modifiers, not every preceeding node too

Change 3047068 on 2016/07/12 by Ben.Marsh

	BuildGraph: Reduce the amount of log output when compiling a C# project; use /verbosity:minimal and /nolog, as Visual Studio does.

Change 3047298 on 2016/07/12 by Ben.Marsh

	EC: Add a workspace setting specifying that it should be synced incrementally.

Change 3047626 on 2016/07/13 by Matthew.Griffin

	Added PackageToNetwork property, which will default to false, which determines whether to put staged builds on the P: drive or within the LocalBuilds folder of the root dir
	Also changed WorldExplorers to use P:/Builds/Friday instead of WEX, as no one is now clearing up the WEX folder regularly

Change 3047762 on 2016/07/13 by Matthew.Griffin

	Added -nodebuginfo to all compile tasks with -precompile to reduce the size of libs produced
	Added plugin intermediates to list of files excluded from installed build

[CL 3047809 by Ben Marsh in Main branch]
2016-07-13 09:16:28 -04:00

1446 lines
48 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using UnrealBuildTool;
using AutomationTool;
using System.Reflection;
using System.Diagnostics;
using System.Xml.Schema;
namespace AutomationTool
{
/// <summary>
/// Implementation of XmlDocument which preserves line numbers for its elements
/// </summary>
class ScriptDocument : XmlDocument
{
/// <summary>
/// The file being read
/// </summary>
FileReference File;
/// <summary>
/// Interface to the LineInfo on the active XmlReader
/// </summary>
IXmlLineInfo LineInfo;
/// <summary>
/// Set to true if the reader encounters an error
/// </summary>
bool bHasErrors;
/// <summary>
/// Private constructor. Use ScriptDocument.Load to read an XML document.
/// </summary>
ScriptDocument(FileReference InFile)
{
File = InFile;
}
/// <summary>
/// Overrides XmlDocument.CreateElement() to construct ScriptElements rather than XmlElements
/// </summary>
public override XmlElement CreateElement(string Prefix, string LocalName, string NamespaceUri)
{
return new ScriptElement(File, LineInfo.LineNumber, Prefix, LocalName, NamespaceUri, this);
}
/// <summary>
/// Loads a script document from the given file
/// </summary>
/// <param name="File">The file to load</param>
/// <param name="Schema">The schema to validate against</param>
/// <param name="OutDocument">If successful, the document that was read</param>
/// <returns>True if the document could be read, false otherwise</returns>
public static bool TryRead(FileReference File, ScriptSchema Schema, out ScriptDocument OutDocument)
{
ScriptDocument Document = new ScriptDocument(File);
XmlReaderSettings Settings = new XmlReaderSettings();
Settings.Schemas.Add(Schema.CompiledSchema);
Settings.ValidationType = ValidationType.Schema;
Settings.ValidationEventHandler += Document.ValidationEvent;
using (XmlReader Reader = XmlReader.Create(File.FullName, Settings))
{
// Read the document
Document.LineInfo = (IXmlLineInfo)Reader;
try
{
Document.Load(Reader);
}
catch (XmlException Ex)
{
if (!Document.bHasErrors)
{
CommandUtils.LogError("{0}", Ex.Message);
Document.bHasErrors = true;
}
}
// If we hit any errors while parsing
if (Document.bHasErrors)
{
OutDocument = null;
return false;
}
// Check that the root element is valid. If not, we didn't actually validate against the schema.
if (Document.DocumentElement.Name != ScriptSchema.RootElementName)
{
CommandUtils.LogError("Script does not have a root element called '{0}'", ScriptSchema.RootElementName);
OutDocument = null;
return false;
}
if (Document.DocumentElement.NamespaceURI != ScriptSchema.NamespaceURI)
{
CommandUtils.LogError("Script root element is not in the '{0}' namespace (add the xmlns=\"{0}\" attribute)", ScriptSchema.NamespaceURI);
OutDocument = null;
return false;
}
}
OutDocument = Document;
return true;
}
/// <summary>
/// Callback for validation errors in the document
/// </summary>
/// <param name="Sender">Standard argument for ValidationEventHandler</param>
/// <param name="Args">Standard argument for ValidationEventHandler</param>
void ValidationEvent(object Sender, ValidationEventArgs Args)
{
if (Args.Severity == XmlSeverityType.Warning)
{
CommandUtils.LogWarning("{0}({1}): {2}", File.FullName, Args.Exception.LineNumber, Args.Message);
}
else
{
CommandUtils.LogError("{0}({1}): {2}", File.FullName, Args.Exception.LineNumber, Args.Message);
bHasErrors = true;
}
}
}
/// <summary>
/// Implementation of XmlElement which preserves line numbers
/// </summary>
class ScriptElement : XmlElement
{
/// <summary>
/// The file containing this element
/// </summary>
public readonly FileReference File;
/// <summary>
/// The line number containing this element
/// </summary>
public readonly int LineNumber;
/// <summary>
/// Constructor
/// </summary>
public ScriptElement(FileReference InFile, int InLineNumber, string Prefix, string LocalName, string NamespaceUri, ScriptDocument Document)
: base(Prefix, LocalName, NamespaceUri, Document)
{
File = InFile;
LineNumber = InLineNumber;
}
}
/// <summary>
/// Reader for build graph definitions. Instanced to contain temporary state; public interface is through ScriptReader.TryRead().
/// </summary>
class ScriptReader
{
/// <summary>
/// The current graph
/// </summary>
Graph Graph = new Graph();
/// <summary>
/// List of property name to value lookups. Modifications to properties are scoped to nodes and agents. EnterScope() pushes an empty dictionary onto the end of this list, and LeaveScope() removes one.
/// ExpandProperties() searches from last to first lookup when trying to resolve a property name, and takes the first it finds.
/// </summary>
List<Dictionary<string, string>> ScopedProperties = new List<Dictionary<string, string>>();
/// <summary>
/// When declaring a property in a nested scope, we enter its name into a set for each parent scope which prevents redeclaration in an OUTER scope later. Subsequent NESTED scopes can redeclare it.
/// The former is likely a coding error, since it implies that the scope of the variable was meant to be further out, whereas the latter is common for temporary and loop variables.
/// </summary>
List<HashSet<string>> ShadowProperties = new List<HashSet<string>>();
/// <summary>
/// Schema for the script
/// </summary>
ScriptSchema Schema;
/// <summary>
/// The number of errors encountered during processing so far
/// </summary>
int NumErrors;
/// <summary>
/// Private constructor. Use ScriptReader.TryRead() to read a script file.
/// </summary>
/// <param name="Properties">Predefined property name to value mapping</param>
/// <param name="InSchema">Schema for the script</param>
private ScriptReader(IDictionary<string, string> Properties, ScriptSchema InSchema)
{
EnterScope();
foreach(KeyValuePair<string, string> Pair in Properties)
{
ScopedProperties[ScopedProperties.Count - 1].Add(Pair.Key, Pair.Value);
}
Schema = InSchema;
}
/// <summary>
/// Try to read a script file from the given file.
/// </summary>
/// <param name="File">File to read from</param>
/// <param name="DefaultProperties">Manually defined properties to parse the graph with</param>
/// <param name="InSchema">Schema for the script</param>
/// <param name="Graph">If successful, the graph constructed from the given script</param>
/// <returns>True if the graph was read, false if there were errors</returns>
public static bool TryRead(FileReference File, IDictionary<string, string> DefaultProperties, ScriptSchema Schema, out Graph Graph)
{
// Check the file exists before doing anything.
if (!File.Exists())
{
CommandUtils.LogError("Cannot open '{0}'", File.FullName);
Graph = null;
return false;
}
// Read the file and build the graph
ScriptReader Reader = new ScriptReader(DefaultProperties, Schema);
if (Reader.TryRead(File) && Reader.NumErrors == 0)
{
Graph = Reader.Graph;
return true;
}
else
{
Graph = null;
return false;
}
}
/// <summary>
/// Read the script from the given file
/// </summary>
/// <param name="File">File to read from</param>
bool TryRead(FileReference File)
{
// Read the document and validate it against the schema
ScriptDocument Document;
if (!ScriptDocument.TryRead(File, Schema, out Document))
{
NumErrors++;
return false;
}
// Read the root BuildGraph element
ReadGraphBody(Document.DocumentElement, File.Directory);
return true;
}
/// <summary>
/// Reads the contents of a graph
/// </summary>
/// <param name="Element">The parent element to read from</param>
/// <param name="BaseDirectory">Base directory to resolve includes against</param>
void ReadGraphBody(XmlElement Element, DirectoryReference BaseDirectory)
{
foreach (ScriptElement ChildElement in Element.ChildNodes.OfType<ScriptElement>())
{
switch (ChildElement.Name)
{
case "Include":
ReadInclude(ChildElement, BaseDirectory);
break;
case "Property":
ReadProperty(ChildElement);
break;
case "Agent":
ReadAgent(ChildElement, null);
break;
case "Aggregate":
ReadAggregate(ChildElement);
break;
case "Report":
ReadReport(ChildElement);
break;
case "Badge":
ReadBadge(ChildElement);
break;
case "Notify":
ReadNotifier(ChildElement);
break;
case "Trigger":
ReadTrigger(ChildElement);
break;
case "Warning":
ReadDiagnostic(ChildElement, LogEventType.Warning, null, null, null);
break;
case "Error":
ReadDiagnostic(ChildElement, LogEventType.Error, null, null, null);
break;
case "Do":
ReadBlock(ChildElement, x => ReadGraphBody(x, BaseDirectory));
break;
case "Choose":
ReadChoice(ChildElement, x => ReadGraphBody(x, BaseDirectory));
break;
case "ForEach":
ReadForEach(ChildElement, x => ReadGraphBody(x, BaseDirectory));
break;
default:
LogError(ChildElement, "Invalid element '{0}'", ChildElement.Name);
break;
}
}
}
/// <summary>
/// Handles validation messages from validating the document against its schema
/// </summary>
/// <param name="Sender">The source of the event</param>
/// <param name="Args">Event arguments</param>
void ValidationHandler(object Sender, ValidationEventArgs Args)
{
if (Args.Severity == XmlSeverityType.Warning)
{
CommandUtils.LogWarning("Script: {0}", Args.Message);
}
else
{
CommandUtils.LogError("Script: {0}", Args.Message);
NumErrors++;
}
}
/// <summary>
/// Push a new property scope onto the stack
/// </summary>
void EnterScope()
{
ScopedProperties.Add(new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase));
ShadowProperties.Add(new HashSet<string>(StringComparer.InvariantCultureIgnoreCase));
}
/// <summary>
/// Pop a property scope from the stack
/// </summary>
void LeaveScope()
{
ScopedProperties.RemoveAt(ScopedProperties.Count - 1);
ShadowProperties.RemoveAt(ShadowProperties.Count - 1);
}
/// <summary>
/// Reads the definition for a trigger.
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
void ReadTrigger(ScriptElement Element)
{
string[] QualifiedName;
if (EvaluateCondition(Element) && TryReadQualifiedObjectName(Element, out QualifiedName))
{
// Validate all the parent triggers
ManualTrigger ParentTrigger = null;
for (int Idx = 0; Idx < QualifiedName.Length - 1; Idx++)
{
ManualTrigger NextTrigger;
if (!Graph.NameToTrigger.TryGetValue(QualifiedName[Idx], out NextTrigger))
{
LogError(Element, "Unknown trigger '{0}'", QualifiedName[Idx]);
return;
}
if (NextTrigger.Parent != ParentTrigger)
{
LogError(Element, "Qualified name of trigger '{0}' is '{1}'", NextTrigger.Name, NextTrigger.QualifiedName);
return;
}
ParentTrigger = NextTrigger;
}
// Get the name of the new trigger
string Name = QualifiedName[QualifiedName.Length - 1];
// Create the new trigger
ManualTrigger Trigger;
if (!Graph.NameToTrigger.TryGetValue(Name, out Trigger))
{
Trigger = new ManualTrigger(ParentTrigger, Name);
Graph.NameToTrigger.Add(Name, Trigger);
}
else if (Trigger.Parent != ParentTrigger)
{
LogError(Element, "Conflicting parent for '{0}' - previously declared as '{1}', now '{2}'", Name, Trigger.QualifiedName, new ManualTrigger(ParentTrigger, Name).QualifiedName);
return;
}
// Read the child elements
ReadTriggerBody(Element, Trigger);
}
}
/// <summary>
/// Reads the body of a trigger element
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
void ReadTriggerBody(XmlElement Element, ManualTrigger Trigger)
{
EnterScope();
foreach (ScriptElement ChildElement in Element.ChildNodes.OfType<ScriptElement>())
{
switch (ChildElement.Name)
{
case "Property":
ReadProperty(ChildElement);
break;
case "Agent":
ReadAgent(ChildElement, Trigger);
break;
case "Aggregate":
ReadAggregate(ChildElement);
break;
case "Notifier":
ReadNotifier(ChildElement);
break;
case "Warning":
ReadDiagnostic(ChildElement, LogEventType.Warning, null, null, Trigger);
break;
case "Error":
ReadDiagnostic(ChildElement, LogEventType.Error, null, null, Trigger);
break;
case "Do":
ReadBlock(ChildElement, x => ReadTriggerBody(x, Trigger));
break;
case "Choose":
ReadChoice(ChildElement, x => ReadTriggerBody(x, Trigger));
break;
case "ForEach":
ReadForEach(ChildElement, x => ReadTriggerBody(x, Trigger));
break;
default:
LogError(ChildElement, "Invalid element '{0}'", ChildElement.Name);
break;
}
}
LeaveScope();
}
/// <summary>
/// Read an include directive, and the contents of the target file
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
/// <param name="BaseDir">Base directory to resolve relative include paths from </param>
void ReadInclude(ScriptElement Element, DirectoryReference BaseDir)
{
if (EvaluateCondition(Element))
{
FileReference Script = FileReference.Combine(BaseDir, Element.GetAttribute("Script"));
if (Script.Exists())
{
TryRead(Script);
}
else
{
LogError(Element, "Cannot find included script '{0}'", Script.FullName);
}
}
}
/// <summary>
/// Reads a property assignment.
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
void ReadProperty(ScriptElement Element)
{
if (EvaluateCondition(Element))
{
string Name = ReadAttribute(Element, "Name");
if (ValidateName(Element, Name))
{
// Find the scope containing this property, defaulting to the current scope
int ScopeIdx = 0;
while(ScopeIdx < ScopedProperties.Count - 1 && !ScopedProperties[ScopeIdx].ContainsKey(Name))
{
ScopeIdx++;
}
// Make sure this property name was not already used in a child scope; it likely indicates an error.
if(ShadowProperties[ScopeIdx].Contains(Name))
{
LogError(Element, "Property '{0}' was already used in a child scope. Move this definition before the previous usage if they are intended to share scope, or use a different name.", Name);
}
else
{
// Make sure it's added to the shadow property list for every parent scope
for(int Idx = 0; Idx < ScopeIdx; Idx++)
{
ShadowProperties[Idx].Add(Name);
}
// Assign the property value
if(Element.HasAttribute("Value"))
{
ScopedProperties[ScopeIdx][Name] = ReadAttribute(Element, "Value");
}
if(Element.HasAttribute("Default") && !ScopedProperties[ScopeIdx].ContainsKey(Name))
{
ScopedProperties[ScopeIdx][Name] = ReadAttribute(Element, "Default");
}
if(Element.HasAttribute("Restrict"))
{
// Find the valid values for this property
string[] Values = ReadListAttribute(Element, "Restrict");
// Get the property value, and check it matches
string Value;
if(!ScopedProperties[ScopeIdx].TryGetValue(Name, out Value))
{
LogError(Element, "Property '{0}' is not defined.", Name);
}
else if(!Values.Any(x => String.Compare(x, Value, StringComparison.InvariantCultureIgnoreCase) == 0))
{
LogError(Element, "Property '{0}' is set to an invalid value ({1}). Valid values are {2}.", Name, Value, String.Join(", ", Values));
}
}
}
}
}
}
/// <summary>
/// Reads the definition for an agent.
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
/// <param name="Trigger">The controlling trigger for nodes in this agent</param>
void ReadAgent(ScriptElement Element, ManualTrigger Trigger)
{
string Name;
if (EvaluateCondition(Element) && TryReadObjectName(Element, out Name))
{
// Read the valid agent types. This may be omitted if we're continuing an existing agent.
string[] Types = ReadListAttribute(Element, "Type");
// Create the agent object, or continue an existing one
Agent Agent;
if (Graph.NameToAgent.TryGetValue(Name, out Agent))
{
if (Types.Length > 0 && Agent.PossibleTypes.Length > 0)
{
string[] NewTypes = Agent.PossibleTypes.Intersect(Types, StringComparer.InvariantCultureIgnoreCase).ToArray();
if (NewTypes.Length == 0)
{
LogError(Element, "No common agent types with previous agent definition");
}
Agent.PossibleTypes = NewTypes;
}
}
else
{
if (Types.Length == 0)
{
LogError(Element, "Missing type for agent '{0}'", Name);
}
Agent = new Agent(Name, Types);
Graph.NameToAgent.Add(Name, Agent);
Graph.Agents.Add(Agent);
}
// Process all the child elements.
ReadAgentBody(Element, Agent, Trigger);
}
}
/// <summary>
/// Read the contents of an agent definition
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
/// <param name="ParentAgent">The agent to contain the definition</param>
/// <param name="ControllingTrigger">The enclosing trigger</param>
void ReadAgentBody(ScriptElement Element, Agent ParentAgent, ManualTrigger ControllingTrigger)
{
EnterScope();
foreach (ScriptElement ChildElement in Element.ChildNodes.OfType<ScriptElement>())
{
switch (ChildElement.Name)
{
case "Property":
ReadProperty(ChildElement);
break;
case "Node":
ReadNode(ChildElement, ParentAgent, ControllingTrigger);
break;
case "Aggregate":
ReadAggregate(ChildElement);
break;
case "Warning":
ReadDiagnostic(ChildElement, LogEventType.Warning, null, ParentAgent, ControllingTrigger);
break;
case "Error":
ReadDiagnostic(ChildElement, LogEventType.Error, null, ParentAgent, ControllingTrigger);
break;
case "Do":
ReadBlock(ChildElement, x => ReadAgentBody(x, ParentAgent, ControllingTrigger));
break;
case "Choose":
ReadChoice(ChildElement, x => ReadAgentBody(x, ParentAgent, ControllingTrigger));
break;
case "ForEach":
ReadForEach(ChildElement, x => ReadAgentBody(x, ParentAgent, ControllingTrigger));
break;
default:
LogError(ChildElement, "Unexpected element type '{0}'", ChildElement.Name);
break;
}
}
LeaveScope();
}
/// <summary>
/// Reads the definition for an aggregate
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
void ReadAggregate(ScriptElement Element)
{
string Name;
if (EvaluateCondition(Element) && TryReadObjectName(Element, out Name) && CheckNameIsUnique(Element, Name))
{
string[] RequiredNames = ReadListAttribute(Element, "Requires");
Graph.AggregateNameToNodes.Add(Name, ResolveReferences(Element, RequiredNames).ToArray());
}
}
/// <summary>
/// Reads the definition for a report
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
void ReadReport(ScriptElement Element)
{
string Name;
if (EvaluateCondition(Element) && TryReadObjectName(Element, out Name) && CheckNameIsUnique(Element, Name))
{
string[] RequiredNames = ReadListAttribute(Element, "Requires");
Report NewReport = new Report(Name);
foreach (Node ReferencedNode in ResolveReferences(Element, RequiredNames))
{
NewReport.Nodes.Add(ReferencedNode);
NewReport.Nodes.UnionWith(ReferencedNode.OrderDependencies);
}
Graph.NameToReport.Add(Name, NewReport);
}
}
/// <summary>
/// Reads the definition for a badge
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
void ReadBadge(ScriptElement Element)
{
string Name;
if (EvaluateCondition(Element) && TryReadObjectName(Element, out Name))
{
string[] RequiredNames = ReadListAttribute(Element, "Requires");
string Project = ReadAttribute(Element, "Project");
Badge NewBadge = new Badge(Name, Project);
foreach (Node ReferencedNode in ResolveReferences(Element, RequiredNames))
{
NewBadge.Nodes.Add(ReferencedNode);
NewBadge.Nodes.UnionWith(ReferencedNode.OrderDependencies);
}
Graph.Badges.Add(NewBadge);
}
}
/// <summary>
/// Reads the definition for a node, and adds it to the given agent
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
/// <param name="ParentAgent">Agent for the node to be added to</param>
/// <param name="ControllingTrigger">The controlling trigger for this node</param>
void ReadNode(ScriptElement Element, Agent ParentAgent, ManualTrigger ControllingTrigger)
{
string Name;
if (EvaluateCondition(Element) && TryReadObjectName(Element, out Name))
{
string[] RequiresNames = ReadListAttribute(Element, "Requires");
string[] ProducesNames = ReadListAttribute(Element, "Produces");
string[] AfterNames = ReadListAttribute(Element, "After");
string[] TicketFileNames = ReadListAttribute(Element, "Ticket");
bool bNotifyOnWarnings = ReadBooleanAttribute(Element, "NotifyOnWarnings", true);
// Resolve all the inputs we depend on
HashSet<NodeOutput> Inputs = ResolveInputReferences(Element, RequiresNames);
// Gather up all the input dependencies, and check they're all upstream of the current node
HashSet<Node> InputDependencies = new HashSet<Node>();
foreach (Node InputDependency in Inputs.Select(x => x.ProducingNode).Distinct())
{
if (InputDependency.ControllingTrigger != null && InputDependency.ControllingTrigger != ControllingTrigger && !InputDependency.ControllingTrigger.IsUpstreamFrom(ControllingTrigger))
{
LogError(Element, "'{0}' is dependent on '{1}', which is behind a different controlling trigger ({2})", Name, InputDependency.Name, InputDependency.ControllingTrigger.QualifiedName);
}
else
{
InputDependencies.Add(InputDependency);
}
}
// Remove all the lock names from the list of required names
HashSet<FileReference> RequiredTickets = new HashSet<FileReference>(TicketFileNames.Select(x => FileReference.Combine(CommandUtils.RootDirectory, x)));
// Recursively include all their dependencies too
foreach (Node InputDependency in InputDependencies.ToArray())
{
RequiredTickets.UnionWith(InputDependency.RequiredTickets);
InputDependencies.UnionWith(InputDependency.InputDependencies);
}
// Validate all the outputs
List<string> ValidOutputNames = new List<string>();
foreach (string ProducesName in ProducesNames)
{
NodeOutput ExistingOutput;
if(Graph.TagNameToNodeOutput.TryGetValue(ProducesName, out ExistingOutput))
{
LogError(Element, "Output tag '{0}' is already generated by node '{1}'", ProducesName, ExistingOutput.ProducingNode.Name);
}
else if(Graph.LocalTagNames.Contains(ProducesName))
{
LogError(Element, "Output tag '{0}' is used elsewhere as a local tag name", ProducesName);
}
else if(!ProducesName.StartsWith("#"))
{
LogError(Element, "Output tag names must begin with a '#' character ('{0}')", ProducesName);
}
else
{
ValidOutputNames.Add(ProducesName);
}
}
// Gather up all the order dependencies
HashSet<Node> OrderDependencies = new HashSet<Node>(InputDependencies);
OrderDependencies.UnionWith(ResolveReferences(Element, AfterNames));
// Recursively include all their order dependencies too
foreach (Node OrderDependency in OrderDependencies.ToArray())
{
OrderDependencies.UnionWith(OrderDependency.OrderDependencies);
}
// Check that we're not dependent on anything completing that is declared after the initial declaration of this agent.
int AgentIdx = Graph.Agents.IndexOf(ParentAgent);
for (int Idx = AgentIdx + 1; Idx < Graph.Agents.Count; Idx++)
{
foreach (Node Node in Graph.Agents[Idx].Nodes.Where(x => OrderDependencies.Contains(x)))
{
LogError(Element, "Node '{0}' has a dependency on '{1}', which was declared after the initial definition of '{2}'.", Name, Node.Name, ParentAgent.Name);
}
}
// Construct and register the node
if (CheckNameIsUnique(Element, Name))
{
// Add it to the node lookup
Node NewNode = new Node(Name, Inputs.ToArray(), ValidOutputNames.ToArray(), InputDependencies.ToArray(), OrderDependencies.ToArray(), ControllingTrigger, RequiredTickets.ToArray());
NewNode.bNotifyOnWarnings = bNotifyOnWarnings;
Graph.NameToNode.Add(Name, NewNode);
// Register all the output tags in the global name table.
foreach(NodeOutput Output in NewNode.Outputs)
{
Graph.TagNameToNodeOutput.Add(Output.TagName, Output);
}
// Add all the tasks
ReadNodeBody(Element, NewNode, ParentAgent, ControllingTrigger);
// Add it to the current agent
ParentAgent.Nodes.Add(NewNode);
}
}
}
/// <summary>
/// Reads the contents of a node element
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
/// <param name="NewNode">The new node that has been created</param>
/// <param name="ParentAgent">Agent for the node to be added to</param>
/// <param name="ControllingTrigger">The controlling trigger for this node</param>
void ReadNodeBody(XmlElement Element, Node NewNode, Agent ParentAgent, ManualTrigger ControllingTrigger)
{
EnterScope();
foreach (ScriptElement ChildElement in Element.ChildNodes.OfType<ScriptElement>())
{
switch (ChildElement.Name)
{
case "Property":
ReadProperty(ChildElement);
break;
case "Warning":
ReadDiagnostic(ChildElement, LogEventType.Warning, NewNode, ParentAgent, ControllingTrigger);
break;
case "Error":
ReadDiagnostic(ChildElement, LogEventType.Error, NewNode, ParentAgent, ControllingTrigger);
break;
case "Do":
ReadBlock(ChildElement, x => ReadNodeBody(x, NewNode, ParentAgent, ControllingTrigger));
break;
case "Choose":
ReadChoice(ChildElement, x => ReadNodeBody(x, NewNode, ParentAgent, ControllingTrigger));
break;
case "ForEach":
ReadForEach(ChildElement, x => ReadNodeBody(x, NewNode, ParentAgent, ControllingTrigger));
break;
default:
ReadTask(ChildElement, NewNode);
break;
}
}
LeaveScope();
}
/// <summary>
/// Reads a block element
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
/// <param name="ReadContents">Delegate to read the contents of the element, if the condition evaluates to true</param>
void ReadBlock(ScriptElement Element, Action<ScriptElement> ReadContents)
{
if (EvaluateCondition(Element))
{
ReadContents(Element);
}
}
/// <summary>
/// Reads a "Choose" element
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
/// <param name="ReadContents">Delegate to read the contents of the element, if the condition evaluates to true</param>
void ReadChoice(ScriptElement Element, Action<ScriptElement> ReadContents)
{
foreach (ScriptElement ChildElement in Element.ChildNodes.OfType<ScriptElement>())
{
if (ChildElement.Name == "Otherwise" || EvaluateCondition(ChildElement))
{
ReadContents(ChildElement);
break;
}
}
}
/// <summary>
/// Reads a "ForEach" element
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
/// <param name="ReadContents">Delegate to read the contents of the element, if the condition evaluates to true</param>
void ReadForEach(ScriptElement Element, Action<ScriptElement> ReadContents)
{
EnterScope();
if(EvaluateCondition(Element))
{
string Name = ReadAttribute(Element, "Name");
if (ValidateName(Element, Name))
{
if(ScopedProperties.Any(x => x.ContainsKey(Name)))
{
LogError(Element, "Loop variable '{0}' already exists as a local property in an outer scope", Name);
}
else
{
// Loop through all the values
string[] Values = ReadListAttribute(Element, "Values");
foreach(string Value in Values)
{
ScopedProperties[ScopedProperties.Count - 1][Name] = Value;
ReadContents(Element);
}
}
}
}
LeaveScope();
}
/// <summary>
/// Reads a task definition from the given element, and add it to the given list
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
/// <param name="ParentNode">The node which owns this task</param>
void ReadTask(ScriptElement Element, Node ParentNode)
{
if (EvaluateCondition(Element))
{
// Get the reflection info for this element
ScriptTask Task;
if (!Schema.TryGetTask(Element.Name, out Task))
{
LogError(Element, "Unknown task '{0}'", Element.Name);
return;
}
// Check all the required parameters are present
bool bHasRequiredAttributes = true;
foreach (ScriptTaskParameter Parameter in Task.NameToParameter.Values)
{
if (!Parameter.bOptional && !Element.HasAttribute(Parameter.Name))
{
LogError(Element, "Missing required attribute - {0}", Parameter.Name);
bHasRequiredAttributes = false;
}
}
// Read all the attributes into a parameters object for this task
object ParametersObject = Activator.CreateInstance(Task.ParametersClass);
foreach (XmlAttribute Attribute in Element.Attributes)
{
if (String.Compare(Attribute.Name, "If", StringComparison.InvariantCultureIgnoreCase) != 0)
{
// Get the field that this attribute should be written to in the parameters object
ScriptTaskParameter Parameter;
if (!Task.NameToParameter.TryGetValue(Attribute.Name, out Parameter))
{
LogError(Element, "Unknown attribute '{0}'", Attribute.Name);
continue;
}
// Expand variables in the value
string ExpandedValue = ExpandProperties(Element, Attribute.Value);
// Parse it and assign it to the parameters object
object Value;
if (Parameter.FieldInfo.FieldType.IsEnum)
{
Value = Enum.Parse(Parameter.FieldInfo.FieldType, ExpandedValue);
}
else if (Parameter.FieldInfo.FieldType == typeof(Boolean))
{
Value = Condition.Evaluate(ExpandedValue);
}
else
{
Value = Convert.ChangeType(ExpandedValue, Parameter.FieldInfo.FieldType);
}
Parameter.FieldInfo.SetValue(ParametersObject, Value);
}
}
// Construct the task
if (bHasRequiredAttributes)
{
// Add it to the list
CustomTask NewTask = (CustomTask)Activator.CreateInstance(Task.TaskClass, ParametersObject);
ParentNode.Tasks.Add(NewTask);
// Make sure all the read tags are local or listed as a dependency
foreach(string ReadTagName in NewTask.FindConsumedTagNames())
{
NodeOutput Output;
if(!Graph.TagNameToNodeOutput.TryGetValue(ReadTagName, out Output))
{
Graph.LocalTagNames.Add(ReadTagName);
}
else if(Output != null && Output.ProducingNode != ParentNode && !ParentNode.Inputs.Contains(Output))
{
LogError(Element, "The tag '{0}' is not a dependency of node '{1}'", ReadTagName, ParentNode.Name);
}
}
// Make sure all the written tags are local or listed as an output
foreach(string ModifiedTagName in NewTask.FindProducedTagNames())
{
NodeOutput Output;
if(!Graph.TagNameToNodeOutput.TryGetValue(ModifiedTagName, out Output))
{
Graph.LocalTagNames.Add(ModifiedTagName);
}
else if(Output != null && !ParentNode.Outputs.Contains(Output))
{
LogError(Element, "The tag '{0}' is created by '{1}', and cannot be modified downstream", Output.TagName, Output.ProducingNode.Name);
}
}
}
}
}
/// <summary>
/// Reads the definition for an email notifier
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
void ReadNotifier(ScriptElement Element)
{
if (EvaluateCondition(Element))
{
string[] TargetNames = ReadListAttribute(Element, "Targets");
string[] ExceptNames = ReadListAttribute(Element, "Except");
string[] IndividualNodeNames = ReadListAttribute(Element, "Nodes");
string[] TriggerNames = ReadListAttribute(Element, "Triggers");
string[] ReportNames = ReadListAttribute(Element, "Reports");
string[] Users = ReadListAttribute(Element, "Users");
string[] Submitters = ReadListAttribute(Element, "Submitters");
bool? bWarnings = Element.HasAttribute("Warnings") ? (bool?)ReadBooleanAttribute(Element, "Warnings", true) : null;
// Find the list of targets which are included, and recurse through all their dependencies
HashSet<Node> Nodes = new HashSet<Node>();
if (TargetNames != null)
{
HashSet<Node> TargetNodes = ResolveReferences(Element, TargetNames);
foreach (Node Node in TargetNodes)
{
Nodes.Add(Node);
Nodes.UnionWith(Node.InputDependencies);
}
}
// Add all the individually referenced nodes
if (IndividualNodeNames != null)
{
HashSet<Node> IndividualNodes = ResolveReferences(Element, IndividualNodeNames);
Nodes.UnionWith(IndividualNodes);
}
// Exclude all the exceptions
if (ExceptNames != null)
{
HashSet<Node> ExceptNodes = ResolveReferences(Element, ExceptNames);
Nodes.ExceptWith(ExceptNodes);
}
// Update all the referenced nodes with the settings
foreach (Node Node in Nodes)
{
if (Users != null)
{
Node.NotifyUsers.UnionWith(Users);
}
if (Submitters != null)
{
Node.NotifySubmitters.UnionWith(Submitters);
}
if (bWarnings.HasValue)
{
Node.bNotifyOnWarnings = bWarnings.Value;
}
}
// Add the users to the list of triggers
if (TriggerNames != null)
{
foreach (string TriggerName in TriggerNames)
{
ManualTrigger Trigger;
if (Graph.NameToTrigger.TryGetValue(TriggerName, out Trigger))
{
Trigger.NotifyUsers.UnionWith(Users);
}
else
{
LogError(Element, "Trigger '{0}' has not been defined", TriggerName);
}
}
}
// Add the users to the list of reports
if (ReportNames != null)
{
foreach (string ReportName in ReportNames)
{
Report Report;
if (Graph.NameToReport.TryGetValue(ReportName, out Report))
{
Report.NotifyUsers.UnionWith(Users);
}
else
{
LogError(Element, "Report '{0}' has not been defined", ReportName);
}
}
}
}
}
/// <summary>
/// Reads a warning from the given element, evaluates the condition on it, and writes it to the log if the condition passes.
/// </summary>
/// <param name="Element">Xml element to read the definition from</param>
/// <param name="EventType">The diagnostic event type</param>
/// <param name="EnclosingNode">The node that this diagnostic is declared in, or null</param>
/// <param name="EnclosingAgent">The agent that this diagnostic is declared in, or null</param>
/// <param name="EnclosingTrigger">The trigger that this diagnostic is declared in, or null</param>
void ReadDiagnostic(ScriptElement Element, LogEventType EventType, Node EnclosingNode, Agent EnclosingAgent, ManualTrigger EnclosingTrigger)
{
if (EvaluateCondition(Element))
{
string Message = ReadAttribute(Element, "Message");
GraphDiagnostic Diagnostic = new GraphDiagnostic();
Diagnostic.EventType = EventType;
Diagnostic.Message = String.Format("{0}({1}): {2}", Element.File.FullName, Element.LineNumber, Message);
Diagnostic.EnclosingNode = EnclosingNode;
Diagnostic.EnclosingAgent = EnclosingAgent;
Diagnostic.EnclosingTrigger = EnclosingTrigger;
Graph.Diagnostics.Add(Diagnostic);
}
}
/// <summary>
/// Checks that the given name does not already used to refer to a node, and print an error if it is.
/// </summary>
/// <param name="Element">Xml element to read from</param>
/// <param name="Name">Name of the alias</param>
/// <param name="Nodes">Array of nodes that this name should resolve to</param>
/// <returns>True if the name was registered correctly, false otherwise.</returns>
bool CheckNameIsUnique(ScriptElement Element, string Name)
{
// Get the nodes that it maps to
if (Graph.ContainsName(Name))
{
LogError(Element, "'{0}' is already defined; cannot add a second time", Name);
return false;
}
return true;
}
/// <summary>
/// Resolve a list of references to a set of nodes
/// </summary>
/// <param name="Element">Element used to locate any errors</param>
/// <param name="ReferenceNames">Sequence of names to look up</param>
/// <returns>Hashset of all the nodes included by the given names</returns>
HashSet<Node> ResolveReferences(ScriptElement Element, IEnumerable<string> ReferenceNames)
{
HashSet<Node> Nodes = new HashSet<Node>();
foreach (string ReferenceName in ReferenceNames)
{
Node[] OtherNodes;
if (Graph.TryResolveReference(ReferenceName, out OtherNodes))
{
Nodes.UnionWith(OtherNodes);
}
else if (!ReferenceName.StartsWith("#") && Graph.TagNameToNodeOutput.ContainsKey("#" + ReferenceName))
{
LogError(Element, "Reference to '{0}' cannot be resolved; did you mean '#{0}'?", ReferenceName);
}
else
{
LogError(Element, "Reference to '{0}' cannot be resolved; check it has been defined.", ReferenceName);
}
}
return Nodes;
}
/// <summary>
/// Resolve a list of references to a set of nodes
/// </summary>
/// <param name="Element">Element used to locate any errors</param>
/// <param name="ReferenceNames">Sequence of names to look up</param>
/// <returns>Set of all the nodes included by the given names</returns>
HashSet<NodeOutput> ResolveInputReferences(ScriptElement Element, IEnumerable<string> ReferenceNames)
{
HashSet<NodeOutput> Inputs = new HashSet<NodeOutput>();
foreach (string ReferenceName in ReferenceNames)
{
NodeOutput[] ReferenceInputs;
if (Graph.TryResolveInputReference(ReferenceName, out ReferenceInputs))
{
Inputs.UnionWith(ReferenceInputs);
}
else if (!ReferenceName.StartsWith("#") && Graph.TagNameToNodeOutput.ContainsKey("#" + ReferenceName))
{
LogError(Element, "Reference to '{0}' cannot be resolved; did you mean '#{0}'?", ReferenceName);
}
else
{
LogError(Element, "Reference to '{0}' cannot be resolved; check it has been defined.", ReferenceName);
}
}
return Inputs;
}
/// <summary>
/// Reads an object name from its defining element. Outputs an error if the name is missing.
/// </summary>
/// <param name="Element">Element to read the name for</param>
/// <param name="Name">Output variable to receive the name of the object</param>
/// <returns>True if the object had a valid name (assigned to the Name variable), false if the name was invalid or missing.</returns>
bool TryReadObjectName(ScriptElement Element, out string Name)
{
// Check the name attribute is present
if (!Element.HasAttribute("Name"))
{
LogError(Element, "Missing 'Name' attribute");
Name = null;
return false;
}
// Get the value of it, strip any leading or trailing whitespace, and make sure it's not empty
string Value = ReadAttribute(Element, "Name");
if (!ValidateName(Element, Value))
{
Name = null;
return false;
}
// Return it
Name = Value;
return true;
}
/// <summary>
/// Reads an qualified object name from its defining element. Outputs an error if the name is missing.
/// </summary>
/// <param name="Element">Element to read the name for</param>
/// <param name="QualifiedName">Output variable to receive the name of the object</param>
/// <returns>True if the object had a valid name (assigned to the Name variable), false if the name was invalid or missing.</returns>
bool TryReadQualifiedObjectName(ScriptElement Element, out string[] QualifiedName)
{
// Check the name attribute is present
if (!Element.HasAttribute("Name"))
{
LogError(Element, "Missing 'Name' attribute");
QualifiedName = null;
return false;
}
// Get the value of it, strip any leading or trailing whitespace, and make sure it's not empty
string[] Values = ReadAttribute(Element, "Name").Split('.');
foreach (string Value in Values)
{
if (!ValidateName(Element, Value))
{
QualifiedName = null;
return false;
}
}
// Return it
QualifiedName = Values;
return true;
}
/// <summary>
/// Checks that the given name is valid syntax
/// </summary>
/// <param name="Element">The element that contains the name</param>
/// <param name="Name">The name to check</param>
/// <returns>True if the name is valid</returns>
bool ValidateName(ScriptElement Element, string Name)
{
// Check it's not empty
if (Name.Length == 0)
{
LogError(Element, "Name is empty");
return false;
}
// Check there are no invalid characters
for (int Idx = 0; Idx < Name.Length; Idx++)
{
if (Idx > 0 && Name[Idx] == ' ' && Name[Idx - 1] == ' ')
{
LogError(Element, "Consecutive spaces in object name");
return false;
}
if (!Char.IsLetterOrDigit(Name[Idx]) && Name[Idx] != '_' && Name[Idx] != ' ')
{
LogError(Element, "Invalid character in object name - '{0}'", Name[Idx]);
return false;
}
}
return true;
}
/// <summary>
/// Expands any properties and reads an attribute.
/// </summary>
/// <param name="Element">Element to read the attribute from</param>
/// <param name="Name">Name of the attribute</param>
/// <returns>Array of names, with all leading and trailing whitespace removed</returns>
string ReadAttribute(ScriptElement Element, string Name)
{
return ExpandProperties(Element, Element.GetAttribute(Name));
}
/// <summary>
/// Expands any properties and reads a list of strings from an attribute, separated by semi-colon characters
/// </summary>
/// <param name="Element"></param>
/// <param name="Name"></param>
/// <returns>Array of names, with all leading and trailing whitespace removed</returns>
string[] ReadListAttribute(ScriptElement Element, string Name)
{
string Value = ReadAttribute(Element, Name);
return Value.Split(new char[] { ';' }).Select(x => x.Trim()).Where(x => x.Length > 0).ToArray();
}
/// <summary>
/// Reads an attribute from the given XML element, expands any properties in it, and parses it as a boolean.
/// </summary>
/// <param name="Element">Element to read the attribute from</param>
/// <param name="Name">Name of the attribute</param>
/// <param name="DefaultValue">Default value if the attribute is missing</param>
/// <returns>The value of the attribute field</returns>
bool ReadBooleanAttribute(ScriptElement Element, string Name, bool bDefaultValue)
{
bool bResult = bDefaultValue;
if (Element.HasAttribute(Name))
{
string Value = ReadAttribute(Element, Name).Trim();
if (Value.Equals("true", StringComparison.InvariantCultureIgnoreCase))
{
bResult = true;
}
else if (Value.Equals("false", StringComparison.InvariantCultureIgnoreCase))
{
bResult = false;
}
else
{
LogError(Element, "Invalid boolean value '{0}' - expected 'true' or 'false'", Value);
}
}
return bResult;
}
/// <summary>
/// Reads an attribute from the given XML element, expands any properties in it, and parses it as an enum of the given type.
/// </summary>
/// <typeparam name="T">The enum type to parse the attribute as</typeparam>
/// <param name="Element">Element to read the attribute from</param>
/// <param name="Name">Name of the attribute</param>
/// <param name="DefaultValue">Default value for the enum, if the attribute is missing</param>
/// <returns>The value of the attribute field</returns>
T ReadEnumAttribute<T>(ScriptElement Element, string Name, T DefaultValue) where T : struct
{
T Result = DefaultValue;
if (Element.HasAttribute(Name))
{
string Value = ReadAttribute(Element, Name).Trim();
T EnumValue;
if (Enum.TryParse(Value, true, out EnumValue))
{
Result = EnumValue;
}
else
{
LogError(Element, "Invalid value '{0}' - expected {1}", Value, String.Join("/", Enum.GetNames(typeof(T))));
}
}
return Result;
}
/// <summary>
/// Outputs an error message to the log and increments the number of errors, referencing the file and line number of the element that caused it.
/// </summary>
/// <param name="Element">The script element causing the error</param>
/// <param name="Format">Standard String.Format()-style format string</param>
/// <param name="Args">Optional arguments</param>
void LogError(ScriptElement Element, string Format, params object[] Args)
{
CommandUtils.LogError("{0}({1}): {2}", Element.File.FullName, Element.LineNumber, String.Format(Format, Args));
NumErrors++;
}
/// <summary>
/// Outputs a warning message to the log and increments the number of errors, referencing the file and line number of the element that caused it.
/// </summary>
/// <param name="Element">The script element causing the error</param>
/// <param name="Format">Standard String.Format()-style format string</param>
/// <param name="Args">Optional arguments</param>
void LogWarning(ScriptElement Element, string Format, params object[] Args)
{
CommandUtils.LogWarning("{0}({1}): {2}", Element.File.FullName, Element.LineNumber, String.Format(Format, Args));
}
/// <summary>
/// Evaluates the (optional) conditional expression on a given XML element via the If="..." attribute, and returns true if the element is enabled.
/// </summary>
/// <param name="Element">The element to check</param>
/// <returns>True if the element's condition evaluates to true (or doesn't have a conditional expression), false otherwise</returns>
bool EvaluateCondition(ScriptElement Element)
{
// Check if the element has a conditional attribute
const string AttributeName = "If";
if (!Element.HasAttribute(AttributeName))
{
return true;
}
// If it does, try to evaluate it.
try
{
string Text = ExpandProperties(Element, Element.GetAttribute("If"));
return Condition.Evaluate(Text);
}
catch (ConditionException Ex)
{
LogError(Element, "Error in condition: {0}", Ex.Message);
return false;
}
}
/// <summary>
/// Expand all the property references (of the form $(PropertyName)) in a string.
/// </summary>
/// <param name="Element">The element containing the string. Used for diagnostic messages.</param>
/// <param name="Text">The input string to expand properties in</param>
/// <returns>The expanded string</returns>
string ExpandProperties(ScriptElement Element, string Text)
{
string Result = Text;
for (int Idx = Result.IndexOf("$("); Idx != -1; Idx = Result.IndexOf("$(", Idx))
{
// Find the end of the variable name
int EndIdx = Result.IndexOf(')', Idx + 2);
if (EndIdx == -1)
{
break;
}
// Extract the variable name from the string
string Name = Result.Substring(Idx + 2, EndIdx - (Idx + 2));
// Find the value for it, either from the dictionary or the environment block
string Value = null;
for (int ScopeIdx = ScopedProperties.Count - 1; ScopeIdx >= 0; ScopeIdx--)
{
if (ScopedProperties[ScopeIdx].TryGetValue(Name, out Value))
{
break;
}
}
// Write a warning if the property does not exist
if(Value == null)
{
LogWarning(Element, "Property '{0}' is not defined", Name);
Value = "";
}
// Replace the variable, or skip past it
Result = Result.Substring(0, Idx) + Value + Result.Substring(EndIdx + 1);
// Make sure we skip over the expanded variable; we don't want to recurse on it.
Idx += Value.Length;
}
return Result;
}
}
}