3553 lines
115 KiB
C#
3553 lines
115 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.Text;
|
|
using System.Xml;
|
|
using System.Xml.Linq;
|
|
using EpicGames.Core;
|
|
using Microsoft.Extensions.Logging;
|
|
using UnrealBuildBase;
|
|
|
|
namespace UnrealBuildTool
|
|
{
|
|
/* UnrealPluginLanguage (UPL) is a simple XML-based language for manipulating XML and returning
|
|
* strings. It contains an <init> section which is evaluated once per architecture before any
|
|
* other sections. The state is maintained and carried forward to the next section evaluated
|
|
* so the order the sections are executed matters.
|
|
*
|
|
* While UPL is a general system for modifying and quering XML it is specifically used to allow
|
|
* plug-ins to affect the global configuration of the package that they are a part of. For
|
|
* example, this allows a plug-in to modify an Android's APK AndroidManfiest.xml file or an
|
|
* IOS IPA's plist file. UBT will also query a plug-in's UPL xml file for strings to be included
|
|
* in files that must be common to the package such as some .java files on Android.
|
|
*
|
|
* If you need to see the instructions executed in your plugin context add the following to
|
|
* enable tracing:
|
|
*
|
|
* <trace enable="true"/>
|
|
*
|
|
* After this instuction all the nodes actually executed in your context will be written to the
|
|
* log until you do a <trace enable="false"/>. You can also get a dump of all the variables in
|
|
* your context with this command:
|
|
*
|
|
* <dumpvars/>
|
|
*
|
|
* Bool, Int, and String variable types are supported. Any attribute may reference a variable
|
|
* and will be replaced with the string equivalent before evaluation using this syntax:
|
|
*
|
|
* $B(name) = boolean variable "name"'s value
|
|
* $I(name) = integer variable "name"'s value
|
|
* $S(name) = string variable "name"'s value
|
|
* $E(name) = element variable "name"'s value
|
|
*
|
|
* The following variables are initialized automatically:
|
|
*
|
|
* $S(Output) = the output returned for evaluating the section (initialized to Input)
|
|
* $S(UnrealArch) = target Unreal arch (arm32, arm64, x86, x64)
|
|
* $S(Architecture) = target architecture (armeabi-armv7a, arm64-v8a, x86, x86_64)
|
|
* $S(PluginDir) = directory the XML file was loaded from
|
|
* $S(EngineDir) = engine directory
|
|
* $S(BuildDir) = project's platform appropriate build directory (within the Intermediate folder)
|
|
* $S(Configuration) = configuration type (Debug, DebugGame, Development, Test, Shipping)
|
|
* $B(Distribution) = true if distribution build
|
|
* $B(IsEmbedded) = true if project compiled for embedded use [Android-only]
|
|
* $I(EngineMajorVersion) = major version of engine (ex. 4)
|
|
* $I(EngineMinorVersion) = minor version of engine (ex. 21)
|
|
* $I(EnginePatchVersion) = patch version of engine (ex. 0)
|
|
* $S(EngineVersion) = engine version string (ex. "4.21.0")
|
|
*
|
|
* Note: with the exception of the above variables, all are in the context of the plugin to
|
|
* prevent namespace collision; trying to set a new value to any of the above, with the
|
|
* exception of Output, will only affect the current context.
|
|
*
|
|
* The following nodes allow manipulation of variables:
|
|
*
|
|
* <setBool result="" value=""/>
|
|
* <setInt result="" value=""/>
|
|
* <setString result="" value=""/>
|
|
* <setElement result="" value=""/>
|
|
* <setElement result="" value="" text=""/>
|
|
* <setElement result="" xml=""/>
|
|
*
|
|
* <setElement> with value creates an empty XML element with the tag set to value.
|
|
* <setElement> with value and text creates an XML element with tag set to value of unparsed text.
|
|
* <setElement> with xml will parse the XML provided. Remember to escape any special characters!
|
|
*
|
|
* Variables may also be set from a property in an ini file:
|
|
*
|
|
* <setBoolFromProperty result="" ini="" section="" property="" default=""/>
|
|
* <setBoolFromPropertyContains result="" ini="" section="" property="" default="" contains=""/>
|
|
* <setIntFromProperty result="" ini="" section="" property="" default=""/>
|
|
* <setIntFromPropertyArrayNum result="" ini="" section="" property=""/>
|
|
* <setStringFromProperty result="" ini="" section="" property="" default=""/>
|
|
* <setStringFromPropertyArray result="" ini="" section="" property="" index="" default=""/>
|
|
*
|
|
* Strings may also be set from an environment variable using the <setStringFromEnvVar> node.
|
|
* The environment variable must be specified as the 'value' attribute and wrapped in a pair
|
|
* of '%' characters.
|
|
*
|
|
* <setStringFromEnvVar result="" value=""/>
|
|
*
|
|
* You can also check if a specific environment variable is defined (again, wrapped in '%' characters):
|
|
*
|
|
* <setBoolEnvVarDefined result="" value=""/>
|
|
*
|
|
* A general example for using environment variable nodes:
|
|
*
|
|
* <setBoolEnvVarDefined result="bHasNDK" value="%NDKROOT%"/>
|
|
* <setStringFromEnvVar result="NDKPath" value="%NDKROOT%"/>
|
|
*
|
|
* Boolean variables may also be set to the result of applying operators:
|
|
*
|
|
* <setBoolNot result="" source=""/>
|
|
* <setBoolAnd result="" arg1="" arg2=""/>
|
|
* <setBoolOr result="" arg1="" arg2=""/>
|
|
* <setBoolIsEqual result="" arg1="" arg2=""/>
|
|
* <setBoolIsLess result="" arg1="" arg2=""/>
|
|
* <setBoolIsLessEqual result="" arg1="" arg2=""/>
|
|
* <setBoolIsGreater result="" arg1="" arg2=""/>
|
|
* <setBoolIsGreaterEqual result="" arg1="" arg2=""/>
|
|
* <setBoolFromPropertyContains result="" ini="" section="" property="" contains=""/>
|
|
*
|
|
* Integer variables may use these arithmetic operations:
|
|
*
|
|
* <setIntAdd result="" arg1="" arg2=""/>
|
|
* <setIntSubtract result="" arg1="" arg2=""/>
|
|
* <setIntMultiply result="" arg1="" arg2=""/>
|
|
* <setIntDivide result="" arg1="" arg2=""/>
|
|
*
|
|
* Strings are manipulated with the following:
|
|
*
|
|
* <setStringAdd result="" arg1="" arg2=""/>
|
|
* <setStringSubstring result="" source="" start="" length=""/>
|
|
* <setStringSubstringAfterFind result="" source="" find="" length="" default=""/>
|
|
* <setStringReplace result="" source="" find="" with=""/>
|
|
* <setStringToLower result="" source=""/>
|
|
* <setStringToUpper result="" source=""/>
|
|
*
|
|
* <setStringSubstringAfterFind> is equivalent to calling setStringSubstring with "start" set to
|
|
* the index of the character immediately following the find string. Passing -1 for length (or
|
|
* omitting it) will return the entire string to the right of the find string.
|
|
*
|
|
* String length may be retrieved with:
|
|
*
|
|
* <setIntLength result="" source=""/>
|
|
*
|
|
* The index of a search string may be found in source with:
|
|
*
|
|
* <setIntFindString result="" source="" find=""/>
|
|
*
|
|
* The following shortcut string comparisons may also be used instead of using <setIntFindString>
|
|
* and checking the result:
|
|
*
|
|
* <setBoolStartsWith result="" source="" find=""/>
|
|
* <setBoolEndsWith result="" source="" find=""/>
|
|
* <setBoolContains result="" source="" find=""/>
|
|
*
|
|
* Messages are written to the log with this node:
|
|
*
|
|
* <log text=""/>
|
|
*
|
|
* Conditional execution uses the following form:
|
|
*
|
|
* <if condition="">
|
|
* <true>
|
|
* <!-- executes if boolean variable in condition is true -->
|
|
* </true>
|
|
* <false>
|
|
* <!-- executes if boolean variable in condition is false -->
|
|
* </false>
|
|
* </if>
|
|
*
|
|
* The <true> and <false> blocks are optional. The condition must be in a boolean variable.
|
|
* The boolean operator nodes may be combined to create a final state for more complex
|
|
* conditions:
|
|
*
|
|
* <setBoolNot result="notDistribution" source="$B(Distribution)/>
|
|
* <setBoolIsEqual result="isX86" arg1="$S(Architecture)" arg2="x86"/>
|
|
* <setBoolIsEqual result="isX86_64" arg2="$S(Architecture)" arg2="x86_64/">
|
|
* <setBoolOr result="isIntel" arg1="$B(isX86)" arg2="$B(isX86_64)"/>
|
|
* <setBoolAnd result="intelAndNotDistribution" arg1="$B(isIntel)" arg2="$B(notDistribution)"/>
|
|
* <if condition="intelAndNotDistribution">
|
|
* <true>
|
|
* <!-- do something for Intel if not a distribution build -->
|
|
* </true>
|
|
* </if>
|
|
*
|
|
* Note the "isIntel" could also be done like this:
|
|
*
|
|
* <setStringSubstring result="subarch" source="$S(Architecture)" start="0" length="3"/>
|
|
* <setBoolIsEqual result="isIntel" arg1="$S(subarch)" arg2="x86"/>
|
|
*
|
|
* Two shortcut nodes are available for conditional execution:
|
|
*
|
|
* <isArch arch="armeabi-armv7">
|
|
* <!-- do stuff -->
|
|
* </isArch>
|
|
*
|
|
* is the equivalent of:
|
|
*
|
|
* <setBoolIsEqual result="temp" arg1="$S(Architecture)" arg2="armeabi-armv7">
|
|
* <if condition="temp">
|
|
* <true>
|
|
* <!-- do stuff -->
|
|
* </true>
|
|
* </if>
|
|
*
|
|
* and
|
|
*
|
|
* <isDistribution>
|
|
* <!-- do stuff -->
|
|
* </isDistribution>
|
|
*
|
|
* is the equivalent of:
|
|
*
|
|
* <if condition="Distribution">
|
|
* <!-- do stuff -->
|
|
* </if>
|
|
*
|
|
* Execution may be stopped with:
|
|
*
|
|
* <return/>
|
|
*
|
|
* Loops may be created using these nodes:
|
|
*
|
|
* <while condition="">
|
|
* <!-- do stuff -->
|
|
* </while>
|
|
*
|
|
* <break/>
|
|
* <continue/>
|
|
*
|
|
* The <while> body will execute until the condition is false or a <break/> is hit. The
|
|
* <continue/> will restart execution of the loop if the condition is still true or exit.
|
|
*
|
|
* Note: <break/> outside a <while> body will act the same as <return/>
|
|
*
|
|
* Here is an example loop which writes 1 to 5 to the log, skipping 3. Note the update of the
|
|
* while condition should be done before the continue otherwise it may not exit.
|
|
*
|
|
* <setInt result="index" value="0"/>
|
|
* <setBool result="loopRun" value="true"/>
|
|
* <while condition="loopRun">
|
|
* <setIntAdd result="index" arg1="$I(index)" arg2="1"/>
|
|
* <setBoolIsLess result="loopRun" arg1="$I(index)" arg2="5"/>
|
|
* <setBoolIsEqual result="indexIs3" arg1="$I(index)" arg2="3"/>
|
|
* <if condition="indexIs3">
|
|
* <true>
|
|
* <continue/>
|
|
* </true>
|
|
* </if>
|
|
* <log text="$I(index)"/>
|
|
* </while>
|
|
*
|
|
* It is possible to use variable replacement in generating the result variable
|
|
* name as well. This makes the creation of arrays in loops possible:
|
|
*
|
|
* <setString result="array_$I(index)" value="element $I(index) in array"/>
|
|
*
|
|
* This may be retrieved using the following (value is treated as the variable
|
|
* name):
|
|
*
|
|
* <setStringFrom result="out" value="array_$I(index)"/>
|
|
*
|
|
* For boolean and integer types, you may use <setBoolFrom/> and <setIntFrom/>.
|
|
*
|
|
* Nodes for inserting text into the section are as follows:
|
|
*
|
|
* <insert> body </insert>
|
|
* <insertNewline/>
|
|
* <insertValue value=""/>
|
|
* <loadLibrary name="" failmsg=""/>
|
|
*
|
|
* The first one will insert either text or nodes into the returned section
|
|
* string. Please note you must use escaped characters for:
|
|
*
|
|
* < = <
|
|
* > = >
|
|
* & = &
|
|
*
|
|
* <insertValue value=""/> evaluates variables in value before insertion. If value contains
|
|
* double quote ("), you must escape it with ".
|
|
*
|
|
* <loadLibrary name="" failmsg=""/> is a shortcut to insert a system.LoadLibrary try/catch
|
|
* block with an optional logged message for failure to load case.
|
|
*
|
|
* You can do a search and replace in the Output with:
|
|
*
|
|
* <replace find="" with=""/>
|
|
*
|
|
* Note you can also manipulate the actual $S(Output) directly, the above are more efficient:
|
|
*
|
|
* <setStringAdd result="Input" arg1="$S(Output)" arg2="sample\n"/>
|
|
* <setStringReplace result="Input" source="$S(Output)" find=".LAUNCH" with=".INFO"/>
|
|
*
|
|
* XML manipulation uses the following nodes:
|
|
*
|
|
* <addElement tag="" name=""/>
|
|
* <addElements tag=""> body </addElements>
|
|
* <removeElement tag=""/>
|
|
* <setStringFromTag result="" tag=""/>
|
|
* <setStringFromAttribute result="" tag="" name=""/>
|
|
* <setStringFromTagText result="" tag=""/>
|
|
* <addAttribute tag="" name="" value=""/>
|
|
* <removeAttribute tag="" name=""/>
|
|
* <loopElements tag=""> instructions </loopElements>
|
|
*
|
|
* The current element is referenced with tag="$". Element variables are referenced with $varname
|
|
* since using $E(varname) will be expanded to the string equivalent of the XML.
|
|
*
|
|
* addElement, addElements, and removeElement by default are applied to all matching tags. An
|
|
* optional once="true" attribute may be added to only apply to first matching tag.
|
|
*
|
|
* <uses-permission>, <uses-feature>, and <uses-library> are updated with:
|
|
*
|
|
* <addPermission android:name="" .. />
|
|
* <addFeature android:name="" .. />
|
|
* <addLibrary android:name="" .. />
|
|
*
|
|
* Any attributes in the above commands are copied to the element added to the manifest so you
|
|
* can do the following, for example:
|
|
*
|
|
* <addFeature android:name="android.hardware.usb.host" android:required="true"/>
|
|
*
|
|
* Finally, these nodes allow copying of files useful for staging jar and so files:
|
|
*
|
|
* <copyFile src="" dst="" force=""/>
|
|
* <copyDir src="" dst="" force=""/>
|
|
*
|
|
* If force is false the file(s) are replaced only if length or timestamp don't match. Default is true.
|
|
*
|
|
* The following should be used as the base for the src and dst paths:
|
|
*
|
|
* $S(PluginDir) = directory the XML file was loaded from
|
|
* $S(EngineDir) = engine directory
|
|
* $S(BuildDir) = project's platform appropriate build directory
|
|
*
|
|
* While it is possible to write outside the APK directory, it is not recommended.
|
|
*
|
|
* If you must remove files (like development-only files from distribution builds) you can
|
|
* use this node:
|
|
*
|
|
* <deleteFiles filespec="" recursive=""/>
|
|
*
|
|
* It is restricted to only removing files from the BuildDir. If recursive is true it will search all subfolders.
|
|
* Here is example usage to remove the Oculus Signature Files (osig) from the assets directory:
|
|
*
|
|
* <deleteFiles filespec="assets/oculussig_*"/>
|
|
*
|
|
* The following sections are evaluated during the packaging or deploy stages:
|
|
*
|
|
* ** For all platforms **
|
|
* <!-- init section is always evaluated once per architecture -->
|
|
* <init> </init>
|
|
*
|
|
* ** Android Specific sections **
|
|
* <!-- optional updates applied to AndroidManifest.xml -->
|
|
* <androidManifestUpdates> </androidManifestUpdates>
|
|
*
|
|
* <!-- optional additions to proguard -->
|
|
* <proguardAdditions> </proguardAdditions>
|
|
*
|
|
* <!-- optional AAR imports additions -->
|
|
* <AARImports> </AARImports>
|
|
*
|
|
* <!-- optional base build.gradle additions -->
|
|
* <baseBuildGradleAdditions> </baseBuildGradleAdditions>
|
|
*
|
|
* <!-- optional base build.gradle buildscript additions -->
|
|
* <buildscriptGradleAdditions> </buildscriptGradleAdditions>
|
|
*
|
|
* <!-- optional app build.gradle additions -->
|
|
* <buildGradleAdditions> </buildGradleAdditions>
|
|
*
|
|
* <!-- optional additions to generated build.xml before ${sdk.dir}/tools/ant/build.xml import -->
|
|
* <buildXmlPropertyAdditions> </buildXmlPropertyAdditions>
|
|
*
|
|
* <!-- optional files or directories to copy or delete from Intermediate/Android/APK before ndk-build -->
|
|
* <prebuildCopies> </prebuildCopies>
|
|
*
|
|
* <!-- optional files or directories to copy or delete from Intermediate/Android/APK after ndk-build -->
|
|
* <resourceCopies> </resourceCopies>
|
|
*
|
|
* <!-- optional files or directories to copy or delete from Intermediate/Android/APK before Gradle -->
|
|
* <gradleCopies> </gradleCopies>
|
|
*
|
|
* <!-- optional properties to add to gradle.properties -->
|
|
* <gradleProperties> </gradleProperties>
|
|
*
|
|
* <!-- optional parameters to add to Gradle commandline (prefix with a space or will run into previous parameter(s)) -->
|
|
* <gradleParameters> </gradleParameters>
|
|
*
|
|
* <!-- optional minimum SDK API level required -->
|
|
* <minimumSDKAPI> </minimumSDKAPI>
|
|
*
|
|
* <!-- optional additions to the GameActivity imports in GameActivity.java -->
|
|
* <gameActivityImportAdditions> </gameActivityImportAdditions>
|
|
*
|
|
* <!-- optional additions to the GameActivity after imports in GameActivity.java -->
|
|
* <gameActivityPostImportAdditions> </gameActivityPostImportAdditions>
|
|
*
|
|
* <!-- optional additions to the GameActivity class implements in GameActivity.java (end each line with a comma) -->
|
|
* <gameActivityImplementsAdditions> </gameActivityImplementsAdditions>
|
|
*
|
|
* <!-- optional additions to the GameActivity class body in GameActivity.java -->
|
|
* <gameActivityClassAdditions> </gameActivityOnClassAdditions>
|
|
*
|
|
* <!-- optional additions to GameActivity onCreate metadata reading in GameActivity.java -->
|
|
* <gameActivityReadMetadata> </gameActivityReadMetadata>
|
|
*
|
|
* <!-- optional additions to the very earliest point in GameActivity onCreate in GameActivity.java -->
|
|
* <gameActivityOnCreateBeginningAdditions> </gameActivityOnCreateBeginningAdditions>
|
|
*
|
|
* <!-- optional additions to GameActivity onCreate in GameActivity.java -->
|
|
* <gameActivityOnCreateAdditions> </gameActivityOnCreateAdditions>
|
|
*
|
|
* <!-- optional additions to GameActivity onDestroy in GameActivity.java -->
|
|
* <gameActivityOnDestroyAdditions> </gameActivityOnDestroyAdditions>
|
|
*
|
|
* <!-- optional additions to GameActivity onConfigurationChanged in GameActivity.java -->
|
|
* <gameActivityonConfigurationChangedAdditions> </gameActivityonConfigurationChangedAdditions>
|
|
*
|
|
* <!-- optional additions to GameActivity onStart in GameActivity.java -->
|
|
* <gameActivityOnStartAdditions> </gameActivityOnStartAdditions>
|
|
*
|
|
* <!-- optional additions to GameActivity onStop in GameActivity.java -->
|
|
* <gameActivityOnStopAdditions> </gameActivityOnStopAdditions>
|
|
*
|
|
* <!-- optional additions to GameActivity onPause in GameActivity.java -->
|
|
* <gameActivityOnPauseAdditions> </gameActivityOnPauseAdditions>
|
|
*
|
|
* <!-- optional additions to GameActivity onResume in GameActivity.java -->
|
|
* <gameActivityOnResumeAdditions> </gameActivityOnResumeAdditions>
|
|
*
|
|
* <!-- optional additions to GameActivity onTopResumeActivity in GameActivity.java -->
|
|
* <gameActivityOnTopResumeActivityAdditions> </gameActivityOnTopResumeActivityAdditions>
|
|
*
|
|
* <!-- optional additions to GameActivity onNewIntent in GameActivity.java -->
|
|
* <gameActivityOnNewIntentAdditions> </gameActivityOnNewIntentAdditions>
|
|
*
|
|
* <!-- optional additions to GameActivity onActivityResult in GameActivity.java -->
|
|
* <gameActivityOnActivityResultAdditions> </gameActivityOnActivityResultAdditions>
|
|
*
|
|
* <!-- optional libraries to load in GameActivity.java before libUnreal.so -->
|
|
* <soLoadLibrary> </soLoadLibrary>
|
|
*
|
|
*
|
|
* Here is the complete list of supported nodes:
|
|
*
|
|
* <isArch arch="">
|
|
* <isDistribution>
|
|
* <if> => <true> / <false>
|
|
* <while condition="">
|
|
* <return/>
|
|
* <break/>
|
|
* <continue/>
|
|
* <log text=""/>
|
|
* <insert> </insert>
|
|
* <insertValue value=""/>
|
|
* <replace find="" with=""/>
|
|
* <copyFile src="" dst=""/>
|
|
* <copyDir src="" dst=""/>
|
|
* <loadLibrary name="" failmsg=""/>
|
|
* <setBool result="" value=""/>
|
|
* <setBoolEnvVarDefined result="" value=""/>
|
|
* <setBoolFrom result="" value=""/>
|
|
* <setBoolFromProperty result="" ini="" section="" property="" default=""/>
|
|
* <setBoolFromPropertyContains result="" ini="" section="" property="" contains=""/>
|
|
* <setBoolNot result="" source=""/>
|
|
* <setBoolAnd result="" arg1="" arg2=""/>
|
|
* <setBoolOr result="" arg1="" arg2=""/>
|
|
* <setBoolIsEqual result="" arg1="" arg2=""/>
|
|
* <setBoolIsLess result="" arg1="" arg2=""/>
|
|
* <setBoolIsLessEqual result="" arg1="" arg2=""/>
|
|
* <setBoolIsGreater result="" arg1="" arg2=""/>
|
|
* <setBoolIsGreaterEqual result="" arg1="" arg2=""/>
|
|
* <setInt result="" value=""/>
|
|
* <setIntFrom result="" value=""/>
|
|
* <setIntFromProperty result="" ini="" section="" property="" default=""/>
|
|
* <setIntFromPropertyArrayNum result="" ini="" section="" property=""/>
|
|
* <setIntAdd result="" arg1="" arg2=""/>
|
|
* <setIntSubtract result="" arg1="" arg2=""/>
|
|
* <setIntMultiply result="" arg1="" arg2=""/>
|
|
* <setIntDivide result="" arg1="" arg2=""/>
|
|
* <setIntLength result="" source=""/>
|
|
* <setIntFindString result="" source="" find=""/>
|
|
* <setString result="" value=""/>
|
|
* <setStringFrom result="" value=""/>
|
|
* <setStringFromEnvVar result="" value=""/>
|
|
* <setStringFromProperty result="" ini="" section="" property="" default=""/>
|
|
* <setStringFromPropertyArray result="" ini="" section="" property="" index="" default=""/>
|
|
* <setStringAdd result="" arg1="" arg2=""/>
|
|
* <setStringSubstring result="" source="" index="" length=""/>
|
|
* <setStringSubstringAfterFind result="" source="" find="" length="" default=""/>
|
|
* <setStringReplace result="" source="" find="" with=""/>
|
|
* <setStringToLower result="" source=""/>
|
|
* <setStringToUpper result="" source=""/>
|
|
*
|
|
*/
|
|
|
|
// Helper struct to initialize aliased namespaces
|
|
struct AliasedXMLNamespace
|
|
{
|
|
public string Alias { get; set; }
|
|
public string Url { get; set; }
|
|
|
|
public override string ToString()
|
|
{
|
|
return String.Format("xmlns:{0}=\"{1}\"", Alias, Url);
|
|
}
|
|
}
|
|
|
|
class UnrealPluginLanguage
|
|
{
|
|
/** The merged XML program to run */
|
|
private XDocument XDoc;
|
|
|
|
/** XML namespace */
|
|
private XNamespace XMLDefaultNameSpace;
|
|
private Dictionary<string, XNamespace> XMLNameSpaceAliases = new Dictionary<string, XNamespace>();
|
|
private string XMLRootDefinition;
|
|
|
|
private UnrealTargetPlatform TargetPlatform;
|
|
|
|
/** Trace flag to enable debugging */
|
|
private static bool bGlobalTrace = false;
|
|
private static bool bGlobalTraceFilename = false;
|
|
|
|
/** Project file reference */
|
|
private FileReference? ProjectFile;
|
|
|
|
private static XDocument XMLDummy = XDocument.Parse("<manifest></manifest>");
|
|
|
|
private class UPLContext
|
|
{
|
|
public string Filename;
|
|
|
|
/** Variable state */
|
|
public Dictionary<string, bool> BoolVariables;
|
|
public Dictionary<string, int> IntVariables;
|
|
public Dictionary<string, string> StringVariables;
|
|
public Dictionary<string, XElement> ElementVariables;
|
|
|
|
/** Local context trace */
|
|
public bool bTrace;
|
|
public bool bTraceFilename;
|
|
|
|
public UPLContext(string inFilename, (string UEArch, string NativeArch) Arch, string PluginDir)
|
|
{
|
|
Filename = inFilename;
|
|
|
|
BoolVariables = new Dictionary<string, bool>();
|
|
IntVariables = new Dictionary<string, int>();
|
|
StringVariables = new Dictionary<string, string>();
|
|
ElementVariables = new Dictionary<string, XElement>();
|
|
|
|
if (PluginDir == null || String.IsNullOrEmpty(PluginDir))
|
|
{
|
|
PluginDir = ".";
|
|
}
|
|
StringVariables["PluginDir"] = PluginDir.Replace("\\", "/");
|
|
StringVariables["AbsPluginDir"] = Path.GetFullPath(PluginDir).Replace("\\", "/");
|
|
|
|
StringVariables["UnrealArch"] = Arch.UEArch;
|
|
StringVariables["Architecture"] = Arch.NativeArch;
|
|
|
|
bTrace = false;
|
|
bTraceFilename = false;
|
|
}
|
|
}
|
|
|
|
private UPLContext GlobalContext;
|
|
private Dictionary<string, UPLContext> Contexts;
|
|
private int ContextIndex;
|
|
private string? LastError;
|
|
private ILogger Logger;
|
|
|
|
// First entry in InXMLNameSpaceAliases will be used as default namespace
|
|
public UnrealPluginLanguage(FileReference? InProjectFile, List<string> InXMLFiles, List<(string UEArch, string NativeArch)> InArchs, List<AliasedXMLNamespace>? InXMLNameSpaceAliases, UnrealTargetPlatform InTargetPlatform, ILogger InLogger)
|
|
{
|
|
ProjectFile = InProjectFile;
|
|
Logger = InLogger;
|
|
|
|
LastError = null;
|
|
|
|
Contexts = new Dictionary<string, UPLContext>();
|
|
GlobalContext = new UPLContext("", ("", ""), "");
|
|
ContextIndex = 0;
|
|
|
|
if (InXMLNameSpaceAliases != null && InXMLNameSpaceAliases.Count > 0)
|
|
{
|
|
XMLDefaultNameSpace = InXMLNameSpaceAliases[0].Url;
|
|
|
|
StringBuilder RootNamespaces = new StringBuilder();
|
|
foreach (AliasedXMLNamespace AliasToNamespace in InXMLNameSpaceAliases)
|
|
{
|
|
XMLNameSpaceAliases[AliasToNamespace.Alias] = AliasToNamespace.Url;
|
|
RootNamespaces.AppendFormat("xmlns:{0}=\"{1}\" ", AliasToNamespace.Alias, AliasToNamespace.Url);
|
|
}
|
|
|
|
XMLRootDefinition = String.Join(" ", RootNamespaces);
|
|
}
|
|
else
|
|
{
|
|
XMLDefaultNameSpace = "";
|
|
XMLRootDefinition = "";
|
|
}
|
|
TargetPlatform = InTargetPlatform;
|
|
|
|
string PathPrefix = Path.GetFileName(Directory.GetCurrentDirectory()).Equals("Source") ? ".." : "Engine";
|
|
|
|
XDoc = XDocument.Parse("<root " + XMLRootDefinition + "></root>");
|
|
foreach (string Basename in InXMLFiles)
|
|
{
|
|
string Filename = Path.Combine(PathPrefix, Basename.Replace("\\", "/"));
|
|
Logger.LogInformation("UPL: {FileName}", Filename);
|
|
if (File.Exists(Filename))
|
|
{
|
|
string PluginDir = Path.GetDirectoryName(Filename)!;
|
|
try
|
|
{
|
|
XDocument MergeDoc = XDocument.Load(Filename, LoadOptions.SetLineInfo);
|
|
MergeXML(MergeDoc, Filename, PluginDir, InArchs);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LastError = String.Format("Unreal Plugin file {0} parsing failed! {1}", Filename, e);
|
|
Logger.LogError("\n{LastError}", LastError);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LastError = String.Format("Unreal Plugin file {0} missing!", Filename);
|
|
Logger.LogError("\n{LastError}", LastError);
|
|
Logger.LogInformation("\nCWD: {Cwd}", Directory.GetCurrentDirectory());
|
|
}
|
|
}
|
|
}
|
|
|
|
public string? GetLastError() { return LastError; }
|
|
|
|
public bool GetTrace() { return bGlobalTrace; }
|
|
public void SetTrace() { bGlobalTrace = true; }
|
|
public void ClearTrace() { bGlobalTrace = false; }
|
|
public bool GetTraceFilename() { return bGlobalTraceFilename; }
|
|
public void SetTraceFilename() { bGlobalTraceFilename = true; }
|
|
public void ClearTraceFilename() { bGlobalTraceFilename = false; }
|
|
|
|
public string GetUPLHash()
|
|
{
|
|
return ContentHash.MD5(XDoc.ToString()).ToString();
|
|
}
|
|
|
|
public bool MergeXML(XDocument MergeDoc, string Filename, string PluginDir, List<(string UEArch, string NativeArch)> Archs)
|
|
{
|
|
if (MergeDoc == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// create a context for each architecture
|
|
ContextIndex++;
|
|
foreach ((string UEArch, string NativeArch) Arch in Archs)
|
|
{
|
|
UPLContext Context = new UPLContext(Filename, Arch, PluginDir);
|
|
Contexts[Arch.NativeArch + "_" + ContextIndex] = Context;
|
|
}
|
|
|
|
// line numbers are lost in the merge so add them as an attribute for later tracing of warnings/errors
|
|
foreach (XElement Element in MergeDoc.Root!.Descendants())
|
|
{
|
|
if (((IXmlLineInfo)Element).HasLineInfo())
|
|
{
|
|
Element.Add(new XAttribute("__line", ((IXmlLineInfo)Element).LineNumber));
|
|
}
|
|
}
|
|
|
|
// merge in the nodes
|
|
foreach (XElement Element in MergeDoc.Root!.Elements())
|
|
{
|
|
XElement? Parent = XDoc.Root!.Element(Element.Name);
|
|
if (Parent != null)
|
|
{
|
|
XElement Entry = new XElement("Context", new XAttribute("index", ContextIndex.ToString()));
|
|
Entry.Add(Element.Elements());
|
|
Parent.Add(Entry);
|
|
}
|
|
else
|
|
{
|
|
XElement Entry = new XElement("Context", new XAttribute("index", ContextIndex.ToString()));
|
|
Entry.Add(Element.Elements());
|
|
XElement Base = new XElement(Element.Name);
|
|
Base.Add(Entry);
|
|
XDoc.Root.Add(Base);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void SaveXML(string Filename)
|
|
{
|
|
XDoc?.Save(Filename);
|
|
}
|
|
|
|
private string DumpContext(UPLContext Context)
|
|
{
|
|
StringBuilder Text = new StringBuilder();
|
|
foreach (KeyValuePair<string, bool> Variable in Context.BoolVariables)
|
|
{
|
|
Text.AppendLine(String.Format("\tbool {0} = {1}", Variable.Key, Variable.Value.ToString().ToLower()));
|
|
}
|
|
foreach (KeyValuePair<string, int> Variable in Context.IntVariables)
|
|
{
|
|
Text.AppendLine(String.Format("\tint {0} = {1}", Variable.Key, Variable.Value));
|
|
}
|
|
foreach (KeyValuePair<string, string> Variable in Context.StringVariables)
|
|
{
|
|
Text.AppendLine(String.Format("\tstring {0} = {1}", Variable.Key, Variable.Value));
|
|
}
|
|
foreach (KeyValuePair<string, XElement> Variable in Context.ElementVariables)
|
|
{
|
|
Text.AppendLine(String.Format("\telement {0} = {1}", Variable.Key, Variable.Value));
|
|
}
|
|
return Text.ToString();
|
|
}
|
|
|
|
public string DumpVariables()
|
|
{
|
|
string Result = "Global Context:\n" + DumpContext(GlobalContext);
|
|
foreach (KeyValuePair<string, UPLContext> Context in Contexts)
|
|
{
|
|
Result += "Context " + Context.Key + ": " + Context.Value.StringVariables["PluginDir"] + "\n" + DumpContext(Context.Value);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
private bool GetCondition(UPLContext Context, XElement Node, string? Condition, out bool Result)
|
|
{
|
|
Result = false;
|
|
|
|
if (Condition == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Context.BoolVariables.TryGetValue(Condition, out Result))
|
|
{
|
|
if (!GlobalContext.BoolVariables.TryGetValue(Condition, out Result))
|
|
{
|
|
Logger.LogWarning("\nMissing condition '{Condition}' in '{Node}' (skipping instruction)", Condition, TraceNodeString(Context, Node));
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private string ExpandVariables(UPLContext Context, string InputString)
|
|
{
|
|
string Result = InputString;
|
|
for (int Idx = Result.IndexOf("$B("); Idx != -1; Idx = Result.IndexOf("$B(", Idx))
|
|
{
|
|
// Find the end of the variable name
|
|
int EndIdx = Result.IndexOf(')', Idx + 3);
|
|
if (EndIdx == -1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Extract the variable name from the string
|
|
string Name = Result.Substring(Idx + 3, EndIdx - (Idx + 3));
|
|
|
|
// Find the value for it, either from the dictionary or the environment block
|
|
bool Value;
|
|
if (!Context.BoolVariables.TryGetValue(Name, out Value))
|
|
{
|
|
if (!GlobalContext.BoolVariables.TryGetValue(Name, out Value))
|
|
{
|
|
Idx = EndIdx + 1;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Replace the variable, or skip past it
|
|
Result = Result.Substring(0, Idx) + Value.ToString().ToLower() + Result.Substring(EndIdx + 1);
|
|
}
|
|
for (int Idx = Result.IndexOf("$I("); Idx != -1; Idx = Result.IndexOf("$I(", Idx))
|
|
{
|
|
// Find the end of the variable name
|
|
int EndIdx = Result.IndexOf(')', Idx + 3);
|
|
if (EndIdx == -1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Extract the variable name from the string
|
|
string Name = Result.Substring(Idx + 3, EndIdx - (Idx + 3));
|
|
|
|
// Find the value for it, either from the dictionary or the environment block
|
|
int Value;
|
|
if (!Context.IntVariables.TryGetValue(Name, out Value))
|
|
{
|
|
if (!GlobalContext.IntVariables.TryGetValue(Name, out Value))
|
|
{
|
|
Idx = EndIdx + 1;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Replace the variable, or skip past it
|
|
Result = Result.Substring(0, Idx) + Value.ToString() + Result.Substring(EndIdx + 1);
|
|
}
|
|
for (int Idx = Result.IndexOf("$S("); Idx != -1; Idx = Result.IndexOf("$S(", Idx))
|
|
{
|
|
// Find the end of the variable name
|
|
int EndIdx = Result.IndexOf(')', Idx + 3);
|
|
if (EndIdx == -1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Extract the variable name from the string
|
|
string Name = Result.Substring(Idx + 3, EndIdx - (Idx + 3));
|
|
|
|
// Find the value for it, either from the dictionary or the environment block
|
|
string? Value;
|
|
if (!Context.StringVariables.TryGetValue(Name, out Value))
|
|
{
|
|
if (!GlobalContext.StringVariables.TryGetValue(Name, out Value))
|
|
{
|
|
Idx = EndIdx + 1;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Replace the variable, or skip past it
|
|
Result = Result.Substring(0, Idx) + Value + Result.Substring(EndIdx + 1);
|
|
}
|
|
for (int Idx = Result.IndexOf("$E("); Idx != -1; Idx = Result.IndexOf("$E(", Idx))
|
|
{
|
|
// Find the end of the variable name
|
|
int EndIdx = Result.IndexOf(')', Idx + 3);
|
|
if (EndIdx == -1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Extract the variable name from the string
|
|
string Name = Result.Substring(Idx + 3, EndIdx - (Idx + 3));
|
|
|
|
// Find the value for it, either from the dictionary or the environment block
|
|
XElement? Value;
|
|
if (!Context.ElementVariables.TryGetValue(Name, out Value))
|
|
{
|
|
if (!GlobalContext.ElementVariables.TryGetValue(Name, out Value))
|
|
{
|
|
Idx = EndIdx + 1;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Replace the variable, or skip past it
|
|
Result = Result.Substring(0, Idx) + Value + Result.Substring(EndIdx + 1);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
private string TraceNodeString(UPLContext Context, XElement Node, bool ShowFilename = true)
|
|
{
|
|
XAttribute? LineAttrib = null;
|
|
string Result = Node.Name.ToString();
|
|
foreach (XAttribute Attrib in Node.Attributes())
|
|
{
|
|
string AttribStr = Attrib.ToString();
|
|
if (AttribStr.StartsWith("__line="))
|
|
{
|
|
LineAttrib = Attrib;
|
|
}
|
|
else
|
|
{
|
|
Result += " " + AttribStr;
|
|
}
|
|
}
|
|
|
|
if (ShowFilename)
|
|
{
|
|
Result += ", File: " + Context.Filename;
|
|
}
|
|
if (LineAttrib != null)
|
|
{
|
|
Result += ", Line: " + LineAttrib.Value;
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
private bool StringToBool(string? Input)
|
|
{
|
|
if (Input == null)
|
|
{
|
|
return false;
|
|
}
|
|
Input = Input.ToLower();
|
|
return !(Input.Equals("0") || Input.Equals("false") || Input.Equals("off") || Input.Equals("no"));
|
|
}
|
|
|
|
private int StringToInt(UPLContext Context, XElement Node, string? Input)
|
|
{
|
|
int Result = 0;
|
|
if (!Int32.TryParse(Input, out Result))
|
|
{
|
|
Logger.LogWarning("\nInvalid integer '{Input}' in '{Node}' (defaulting to 0)", Input, TraceNodeString(Context, Node));
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
[return: NotNullIfNotNull("Fallback")]
|
|
private string? GetAttribute(UPLContext Context, XElement Node, string AttributeName, bool bExpand = true, bool bRequired = true, string? Fallback = null)
|
|
{
|
|
XAttribute? Attribute = Node.Attribute(AttributeName);
|
|
if (Attribute == null)
|
|
{
|
|
if (bRequired)
|
|
{
|
|
Logger.LogWarning("\nMissing attribute '{Attribute}' in '{Node}' (skipping instruction)", AttributeName, TraceNodeString(Context, Node));
|
|
}
|
|
return Fallback;
|
|
}
|
|
string Result = Attribute.Value;
|
|
return bExpand ? ExpandVariables(Context, Result) : Result;
|
|
}
|
|
|
|
[return: NotNullIfNotNull("Fallback")]
|
|
private string? GetAttributeWithNamespace(UPLContext Context, XElement Node, XNamespace Namespace, string AttributeName, bool bExpand = true, bool bRequired = true, string? Fallback = null)
|
|
{
|
|
XAttribute? Attribute = Node.Attribute(Namespace + AttributeName);
|
|
if (Attribute == null)
|
|
{
|
|
if (bRequired)
|
|
{
|
|
Logger.LogWarning("\nMissing attribute '{Attribute}' in '{Node}' (skipping instruction)", AttributeName, TraceNodeString(Context, Node));
|
|
}
|
|
return Fallback;
|
|
}
|
|
string Result = Attribute.Value;
|
|
return bExpand ? ExpandVariables(Context, Result) : Result;
|
|
}
|
|
|
|
private static Dictionary<string, ConfigCacheIni_UPL>? ConfigCache = null;
|
|
|
|
private ConfigCacheIni_UPL GetConfigCacheIni_UPL(string baseIniName)
|
|
{
|
|
ConfigCache ??= new Dictionary<string, ConfigCacheIni_UPL>();
|
|
ConfigCacheIni_UPL? config = null;
|
|
if (!ConfigCache.TryGetValue(baseIniName, out config))
|
|
{
|
|
// note: use our own ConfigCacheIni since EngineConfiguration.cs only parses RequiredSections!
|
|
config = ConfigCacheIni_UPL.CreateConfigCacheIni_UPL(TargetPlatform, baseIniName, DirectoryReference.FromFile(ProjectFile), Logger);
|
|
ConfigCache.Add(baseIniName, config);
|
|
}
|
|
return config;
|
|
}
|
|
|
|
private static bool FilesAreDifferent(string SourceFilename, string DestFilename, ILogger Logger)
|
|
{
|
|
// source must exist
|
|
FileInfo SourceInfo = new FileInfo(SourceFilename);
|
|
if (!SourceInfo.Exists)
|
|
{
|
|
Logger.LogInformation("File {SourceFilename} does not exist", SourceFilename);
|
|
return false;
|
|
}
|
|
|
|
// different if destination doesn't exist
|
|
FileInfo DestInfo = new FileInfo(DestFilename);
|
|
if (!DestInfo.Exists)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// file lengths differ?
|
|
if (SourceInfo.Length != DestInfo.Length)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// validate timestamps
|
|
TimeSpan Diff = DestInfo.LastWriteTimeUtc - SourceInfo.LastWriteTimeUtc;
|
|
if (Diff.TotalSeconds < -1 || Diff.TotalSeconds > 1)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// could check actual bytes just to be sure, but good enough
|
|
return false;
|
|
}
|
|
|
|
private static void CopyFileDirectory(string SourceDir, string DestDir, ILogger Logger, bool bForce = false)
|
|
{
|
|
if (!Directory.Exists(SourceDir))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string[] Files = Directory.GetFiles(SourceDir, "*.*", SearchOption.AllDirectories);
|
|
foreach (string Filename in Files)
|
|
{
|
|
// make the dst filename with the same structure as it was in SourceDir
|
|
string DestFilename = Path.Combine(DestDir, Utils.MakePathRelativeTo(Filename, SourceDir));
|
|
|
|
if (bForce || FilesAreDifferent(Filename, DestFilename, Logger))
|
|
{
|
|
if (File.Exists(DestFilename))
|
|
{
|
|
File.SetAttributes(DestFilename, FileAttributes.Normal);
|
|
File.Delete(DestFilename);
|
|
}
|
|
|
|
// make the subdirectory if needed
|
|
string DestSubdir = Path.GetDirectoryName(DestFilename)!;
|
|
if (!Directory.Exists(DestSubdir))
|
|
{
|
|
Directory.CreateDirectory(DestSubdir);
|
|
}
|
|
|
|
File.Copy(Filename, DestFilename);
|
|
|
|
// remove any read only flags and keep timestamp
|
|
FileInfo DestFileInfo = new FileInfo(DestFilename);
|
|
DestFileInfo.Attributes &= ~FileAttributes.ReadOnly;
|
|
File.SetLastWriteTimeUtc(DestFilename, File.GetLastWriteTimeUtc(Filename));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void DeleteFiles(string Filespec, bool Recursive, ILogger Logger)
|
|
{
|
|
string BaseDir = Path.GetDirectoryName(Filespec)!;
|
|
string Mask = Path.GetFileName(Filespec);
|
|
|
|
if (!Directory.Exists(BaseDir))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string[] Files = Directory.GetFiles(BaseDir, Mask, Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
|
|
foreach (string Filename in Files)
|
|
{
|
|
File.SetAttributes(Filename, FileAttributes.Normal);
|
|
File.Delete(Filename);
|
|
Logger.LogInformation("\nDeleted file {Filename}", Filename);
|
|
}
|
|
}
|
|
|
|
XNamespace? TrimNamespaceAliasFromName(ref string Name)
|
|
{
|
|
int Index = Name.IndexOf(":");
|
|
if (Index >= 0)
|
|
{
|
|
XNamespace? Namespace;
|
|
string NamespaceAlias = Name.Substring(0, Index);
|
|
if (!XMLNameSpaceAliases.TryGetValue(NamespaceAlias, out Namespace))
|
|
{
|
|
Namespace = XMLDefaultNameSpace;
|
|
}
|
|
Name = Name.Substring(Index + 1);
|
|
return Namespace;
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
private void AddAttribute(XElement Element, string Name, string Value)
|
|
{
|
|
XNamespace? XMLNameSpace = TrimNamespaceAliasFromName(ref Name);
|
|
XAttribute? Attribute = Element.Attribute(XMLNameSpace != null? XMLNameSpace + Name : Name);
|
|
|
|
if (Attribute != null)
|
|
{
|
|
Attribute.SetValue(Value);
|
|
}
|
|
else
|
|
{
|
|
Element.Add(new XAttribute(XMLNameSpace != null ? XMLNameSpace + Name : Name, Value));
|
|
}
|
|
}
|
|
|
|
private void RemoveAttribute(XElement Element, string Name)
|
|
{
|
|
XNamespace? XMLNameSpace = TrimNamespaceAliasFromName(ref Name);
|
|
XAttribute? Attribute = Element.Attribute(XMLNameSpace != null ? XMLNameSpace + Name : Name);
|
|
|
|
Attribute?.Remove();
|
|
}
|
|
|
|
private void AddElements(XElement Target, XElement Source)
|
|
{
|
|
if (Source.HasElements)
|
|
{
|
|
foreach (XElement Index in Source.Elements())
|
|
{
|
|
// if (Target.Element(Index.Name) == null)
|
|
{
|
|
Target.Add(Index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public string ProcessPluginNode(string Architecture, string NodeName, string Input)
|
|
{
|
|
return ProcessPluginNode(Architecture, NodeName, Input, ref XMLDummy);
|
|
}
|
|
|
|
public string ProcessPluginNode(string Architecture, string NodeName, string Input, ref XDocument XMLWork)
|
|
{
|
|
// add all instructions to execution list
|
|
Stack<XElement> ExecutionStack = new Stack<XElement>();
|
|
XElement? StartNode = XDoc.Root!.Element(NodeName);
|
|
if (StartNode != null)
|
|
{
|
|
foreach (XElement Instruction in StartNode.Elements().Reverse())
|
|
{
|
|
ExecutionStack.Push(Instruction);
|
|
}
|
|
}
|
|
|
|
if (ExecutionStack.Count == 0)
|
|
{
|
|
return Input;
|
|
}
|
|
|
|
Stack<UPLContext> ContextStack = new Stack<UPLContext>();
|
|
UPLContext CurrentContext = GlobalContext;
|
|
|
|
// update Output in global context
|
|
GlobalContext.StringVariables["Output"] = Input;
|
|
|
|
Stack<XElement> ElementStack = new Stack<XElement>();
|
|
XElement CurrentElement = XMLWork.Elements().First();
|
|
|
|
// run the instructions
|
|
while (ExecutionStack.Count > 0)
|
|
{
|
|
XElement Node = ExecutionStack.Pop();
|
|
if (bGlobalTrace || CurrentContext.bTrace)
|
|
{
|
|
Logger.LogInformation("Execute: '{Node}'", TraceNodeString(CurrentContext, Node, bGlobalTraceFilename || CurrentContext.bTraceFilename));
|
|
}
|
|
switch (Node.Name.ToString())
|
|
{
|
|
case "trace":
|
|
{
|
|
string? TraceFilename = GetAttribute(CurrentContext, Node, "filename");
|
|
if (TraceFilename != null)
|
|
{
|
|
CurrentContext.bTraceFilename = StringToBool(TraceFilename);
|
|
}
|
|
string? Enable = GetAttribute(CurrentContext, Node, "enable");
|
|
if (Enable != null)
|
|
{
|
|
CurrentContext.bTrace = StringToBool(Enable);
|
|
if (!bGlobalTrace && CurrentContext.bTrace)
|
|
{
|
|
Logger.LogInformation("Context: '{Context}' using Architecture='{Arch}', NodeName='{NodeName}', Input='{Input}'", CurrentContext.StringVariables["PluginDir"], Architecture, NodeName, Input);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "dumpvars":
|
|
{
|
|
if (!bGlobalTrace && !CurrentContext.bTrace)
|
|
{
|
|
Logger.LogInformation("Context: '{Context}' using Architecture='{Arch}', NodeName='{NodeName}', Input='{Input}'", CurrentContext.StringVariables["PluginDir"], Architecture, NodeName, Input);
|
|
}
|
|
Logger.LogInformation("Variables:\n{Variables}", DumpContext(CurrentContext));
|
|
}
|
|
break;
|
|
|
|
case "Context":
|
|
{
|
|
ContextStack.Push(CurrentContext);
|
|
string? index = GetAttribute(CurrentContext, Node, "index");
|
|
CurrentContext = Contexts[Architecture + "_" + index];
|
|
ExecutionStack.Push(new XElement("PopContext"));
|
|
foreach (XElement instruction in Node.Elements().Reverse())
|
|
{
|
|
ExecutionStack.Push(instruction);
|
|
}
|
|
|
|
if (bGlobalTrace || CurrentContext.bTrace)
|
|
{
|
|
Logger.LogInformation("Context: '{Context}' using Architecture='{Arch}', NodeName='{NodeName}', Input='{Input}'", CurrentContext.StringVariables["PluginDir"], Architecture, NodeName, Input);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "PopContext":
|
|
{
|
|
CurrentContext = ContextStack.Pop();
|
|
}
|
|
break;
|
|
|
|
case "PopElement":
|
|
{
|
|
CurrentElement = ElementStack.Pop();
|
|
}
|
|
break;
|
|
|
|
case "isArch":
|
|
{
|
|
string? arch = GetAttribute(CurrentContext, Node, "arch");
|
|
if (arch != null && arch.Equals(Architecture))
|
|
{
|
|
foreach (XElement instruction in Node.Elements().Reverse())
|
|
{
|
|
ExecutionStack.Push(instruction);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "isDistribution":
|
|
{
|
|
bool Result = false;
|
|
if (GetCondition(CurrentContext, Node, "Distribution", out Result))
|
|
{
|
|
if (Result)
|
|
{
|
|
foreach (XElement Instruction in Node.Elements().Reverse())
|
|
{
|
|
ExecutionStack.Push(Instruction);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "if":
|
|
{
|
|
bool Result;
|
|
if (GetCondition(CurrentContext, Node, GetAttribute(CurrentContext, Node, "condition"), out Result))
|
|
{
|
|
XElement? ResultNode = Node.Element(Result ? "true" : "false");
|
|
if (ResultNode != null)
|
|
{
|
|
foreach (XElement Instruction in ResultNode.Elements().Reverse())
|
|
{
|
|
ExecutionStack.Push(Instruction);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "while":
|
|
{
|
|
bool Result;
|
|
if (GetCondition(CurrentContext, Node, GetAttribute(CurrentContext, Node, "condition"), out Result))
|
|
{
|
|
if (Result)
|
|
{
|
|
IEnumerable<XElement> ResultNode = Node.Elements();
|
|
if (ResultNode != null)
|
|
{
|
|
ExecutionStack.Push(Node);
|
|
foreach (XElement Instruction in ResultNode.Reverse())
|
|
{
|
|
ExecutionStack.Push(Instruction);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "return":
|
|
{
|
|
while (ExecutionStack.Count > 0)
|
|
{
|
|
ExecutionStack.Pop();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "break":
|
|
{
|
|
// remove up to while (or acts like a return if outside by removing everything)
|
|
while (ExecutionStack.Count > 0)
|
|
{
|
|
Node = ExecutionStack.Pop();
|
|
if (Node.Name.ToString().Equals("while"))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "continue":
|
|
{
|
|
// remove up to while (or acts like a return if outside by removing everything)
|
|
while (ExecutionStack.Count > 0)
|
|
{
|
|
Node = ExecutionStack.Pop();
|
|
if (Node.Name.ToString().Equals("while"))
|
|
{
|
|
ExecutionStack.Push(Node);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "log":
|
|
{
|
|
string? Text = GetAttribute(CurrentContext, Node, "text");
|
|
if (Text != null)
|
|
{
|
|
Logger.LogInformation("{Message}", Text);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "loopElements":
|
|
{
|
|
string? Tag = GetAttribute(CurrentContext, Node, "tag");
|
|
ElementStack.Push(CurrentElement);
|
|
ExecutionStack.Push(new XElement("PopElement"));
|
|
IEnumerable<XElement> WorkList = (Tag == "$") ? CurrentElement.Elements().Reverse() : CurrentElement.Descendants(Tag).Reverse();
|
|
foreach (XElement WorkNode in WorkList)
|
|
{
|
|
foreach (XElement Instruction in Node.Elements().Reverse())
|
|
{
|
|
ExecutionStack.Push(Instruction);
|
|
}
|
|
ElementStack.Push(WorkNode);
|
|
ExecutionStack.Push(new XElement("PopElement"));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "addAttribute":
|
|
{
|
|
string? Tag = GetAttribute(CurrentContext, Node, "tag");
|
|
string? Name = GetAttribute(CurrentContext, Node, "name");
|
|
string? Value = GetAttribute(CurrentContext, Node, "value");
|
|
if (Tag != null && Name != null && Value != null)
|
|
{
|
|
if (Tag.StartsWith("$"))
|
|
{
|
|
XElement? Target = CurrentElement;
|
|
if (Tag.Length > 1)
|
|
{
|
|
if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target))
|
|
{
|
|
if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target))
|
|
{
|
|
Logger.LogWarning("Missing element variable '{Tag}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node));
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
AddAttribute(Target!, Name, Value);
|
|
}
|
|
else
|
|
{
|
|
if (CurrentElement.Name.ToString().Equals(Tag))
|
|
{
|
|
AddAttribute(CurrentElement, Name, Value);
|
|
}
|
|
foreach (XElement WorkNode in CurrentElement.Descendants(Tag))
|
|
{
|
|
AddAttribute(WorkNode, Name, Value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "removeAttribute":
|
|
{
|
|
string? Tag = GetAttribute(CurrentContext, Node, "tag");
|
|
string? Name = GetAttribute(CurrentContext, Node, "name");
|
|
if (Tag != null && Name != null)
|
|
{
|
|
if (Tag.StartsWith("$"))
|
|
{
|
|
XElement? Target = CurrentElement;
|
|
if (Tag.Length > 1)
|
|
{
|
|
if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target))
|
|
{
|
|
if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target))
|
|
{
|
|
Logger.LogInformation("\nMissing element variable '{Tag}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node));
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
RemoveAttribute(Target, Name);
|
|
}
|
|
else
|
|
{
|
|
if (CurrentElement.Name.ToString().Equals(Tag))
|
|
{
|
|
RemoveAttribute(CurrentElement, Name);
|
|
}
|
|
foreach (XElement WorkNode in CurrentElement.Descendants(Tag))
|
|
{
|
|
RemoveAttribute(WorkNode, Name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "addPermission":
|
|
{
|
|
string? Name = GetAttributeWithNamespace(CurrentContext, Node, XMLDefaultNameSpace, "name");
|
|
if (Name != null)
|
|
{
|
|
// make sure it isn't already added
|
|
bool bFound = false;
|
|
foreach (XElement Element in XMLWork.Descendants("uses-permission"))
|
|
{
|
|
XAttribute? Attribute = Element.Attribute(XMLDefaultNameSpace + "name");
|
|
if (Attribute != null)
|
|
{
|
|
if (Attribute.Value == Name)
|
|
{
|
|
bFound = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// add it if not found
|
|
if (!bFound)
|
|
{
|
|
// Get the attributes and apply any variable expansion needed
|
|
List<XAttribute> AttributeList = Node.Attributes().ToList();
|
|
foreach (XAttribute Attribute in AttributeList)
|
|
{
|
|
string NewValue = ExpandVariables(CurrentContext, Attribute.Value);
|
|
Attribute.SetValue(NewValue);
|
|
}
|
|
|
|
XMLWork.Element("manifest")?.Add(new XElement("uses-permission", AttributeList));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "removePermission":
|
|
{
|
|
string? Name = GetAttributeWithNamespace(CurrentContext, Node, XMLDefaultNameSpace, "name");
|
|
if (Name != null)
|
|
{
|
|
foreach (XElement Element in XMLWork.Descendants("uses-permission"))
|
|
{
|
|
XAttribute? Attribute = Element.Attribute(XMLDefaultNameSpace + "name");
|
|
if (Attribute != null)
|
|
{
|
|
if (Attribute.Value == Name)
|
|
{
|
|
Element.Remove();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "addFeature":
|
|
{
|
|
string? Name = GetAttributeWithNamespace(CurrentContext, Node, XMLDefaultNameSpace, "name");
|
|
if (Name != null)
|
|
{
|
|
// make sure it isn't already added
|
|
bool bFound = false;
|
|
foreach (XElement Element in XMLWork.Descendants("uses-feature"))
|
|
{
|
|
XAttribute? Attribute = Element.Attribute(XMLDefaultNameSpace + "name");
|
|
if (Attribute != null)
|
|
{
|
|
if (Attribute.Value == Name)
|
|
{
|
|
bFound = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// add it if not found
|
|
if (!bFound)
|
|
{
|
|
// Get the attributes and apply any variable expansion needed
|
|
List<XAttribute> AttributeList = Node.Attributes().ToList();
|
|
foreach (XAttribute Attribute in AttributeList)
|
|
{
|
|
string NewValue = ExpandVariables(CurrentContext, Attribute.Value);
|
|
Attribute.SetValue(NewValue);
|
|
}
|
|
|
|
XMLWork.Element("manifest")?.Add(new XElement("uses-feature", AttributeList));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "removeFeature":
|
|
{
|
|
string? Name = GetAttributeWithNamespace(CurrentContext, Node, XMLDefaultNameSpace, "name");
|
|
if (Name != null)
|
|
{
|
|
foreach (XElement Element in XMLWork.Descendants("uses-feature"))
|
|
{
|
|
XAttribute? Attribute = Element.Attribute(XMLDefaultNameSpace + "name");
|
|
if (Attribute != null)
|
|
{
|
|
if (Attribute.Value == Name)
|
|
{
|
|
Element.Remove();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "addLibrary":
|
|
{
|
|
string? Name = GetAttributeWithNamespace(CurrentContext, Node, XMLDefaultNameSpace, "name");
|
|
if (Name != null)
|
|
{
|
|
// make sure it isn't already added
|
|
bool bFound = false;
|
|
foreach (XElement Element in XMLWork.Descendants("uses-library"))
|
|
{
|
|
XAttribute? Attribute = Element.Attribute(XMLDefaultNameSpace + "name");
|
|
if (Attribute != null)
|
|
{
|
|
if (Attribute.Value == Name)
|
|
{
|
|
bFound = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// add it if not found
|
|
if (!bFound)
|
|
{
|
|
// Get the attributes and apply any variable expansion needed
|
|
List<XAttribute> AttributeList = Node.Attributes().ToList();
|
|
foreach (XAttribute Attribute in AttributeList)
|
|
{
|
|
string NewValue = ExpandVariables(CurrentContext, Attribute.Value);
|
|
Attribute.SetValue(NewValue);
|
|
}
|
|
|
|
XMLWork.Element("manifest")?.Element("application")?.Add(new XElement("uses-library", AttributeList));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "removeLibrary":
|
|
{
|
|
string? Name = GetAttributeWithNamespace(CurrentContext, Node, XMLDefaultNameSpace, "name");
|
|
if (Name != null)
|
|
{
|
|
foreach (XElement Element in XMLWork.Descendants("uses-library"))
|
|
{
|
|
XAttribute? Attribute = Element.Attribute(XMLDefaultNameSpace + "name");
|
|
if (Attribute != null)
|
|
{
|
|
if (Attribute.Value == Name)
|
|
{
|
|
Element.Remove();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "removeElement":
|
|
{
|
|
string? Tag = GetAttribute(CurrentContext, Node, "tag");
|
|
bool bOnce = StringToBool(GetAttribute(CurrentContext, Node, "once", true, false));
|
|
if (Tag != null)
|
|
{
|
|
if (Tag == "$")
|
|
{
|
|
XElement Parent = CurrentElement.Parent!;
|
|
CurrentElement.Remove();
|
|
CurrentElement = Parent;
|
|
}
|
|
else
|
|
{
|
|
// use a list since Remove() may modify it
|
|
foreach (XElement Element in XMLWork.Descendants(Tag).ToList())
|
|
{
|
|
Element.Remove();
|
|
if (bOnce)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "addElement":
|
|
{
|
|
string? Tag = GetAttribute(CurrentContext, Node, "tag");
|
|
string? Name = GetAttribute(CurrentContext, Node, "name");
|
|
bool bOnce = StringToBool(GetAttribute(CurrentContext, Node, "once", true, false));
|
|
if (Tag != null && Name != null)
|
|
{
|
|
XElement? Element;
|
|
if (!CurrentContext.ElementVariables.TryGetValue(Name, out Element))
|
|
{
|
|
if (!GlobalContext.ElementVariables.TryGetValue(Name, out Element))
|
|
{
|
|
Logger.LogWarning("Missing element variable '{Name}' in '{Node}' (skipping instruction)", Name, TraceNodeString(CurrentContext, Node));
|
|
continue;
|
|
}
|
|
}
|
|
if (Tag.StartsWith("$"))
|
|
{
|
|
XElement? Target = CurrentElement;
|
|
if (Tag.Length > 1)
|
|
{
|
|
if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target))
|
|
{
|
|
if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target))
|
|
{
|
|
Logger.LogWarning("Missing element variable '{Name}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node));
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
Target.Add(new XElement(Element));
|
|
}
|
|
else
|
|
{
|
|
if (CurrentElement.Name.ToString().Equals(Tag))
|
|
{
|
|
CurrentElement.Add(new XElement(Element));
|
|
}
|
|
|
|
// make sure we don't recurse forever if Tag is in Element
|
|
List<XElement> AddSet = new List<XElement>();
|
|
foreach (XElement WorkNode in CurrentElement.Descendants(Tag))
|
|
{
|
|
AddSet.Add(WorkNode);
|
|
if (bOnce)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
foreach (XElement WorkNode in AddSet)
|
|
{
|
|
WorkNode.Add(new XElement(Element));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "addElements":
|
|
{
|
|
string? Tag = GetAttribute(CurrentContext, Node, "tag");
|
|
bool bOnce = StringToBool(GetAttribute(CurrentContext, Node, "once", true, false));
|
|
if (Tag != null)
|
|
{
|
|
if (Tag.StartsWith("$"))
|
|
{
|
|
XElement? Target = CurrentElement;
|
|
if (Tag.Length > 1)
|
|
{
|
|
if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target))
|
|
{
|
|
if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target))
|
|
{
|
|
Logger.LogWarning("Missing element variable '{Tag}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node));
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
AddElements(Target, Node);
|
|
}
|
|
else
|
|
{
|
|
if (CurrentElement.Name.ToString().Equals(Tag))
|
|
{
|
|
AddElements(CurrentElement, Node);
|
|
}
|
|
|
|
// make sure we don't recurse forever if Tag is in Node
|
|
List<XElement> AddSet = new List<XElement>();
|
|
foreach (XElement WorkNode in CurrentElement.Descendants(Tag))
|
|
{
|
|
AddSet.Add(WorkNode);
|
|
if (bOnce)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
foreach (XElement WorkNode in AddSet)
|
|
{
|
|
AddElements(WorkNode, Node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "insert":
|
|
{
|
|
if (Node.HasElements)
|
|
{
|
|
foreach (XElement Element in Node.Elements())
|
|
{
|
|
string Value = Element.ToString().Replace(" " + XMLRootDefinition + " ", "");
|
|
GlobalContext.StringVariables["Output"] += Value + "\n";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string Value = Node.Value.ToString();
|
|
|
|
// trim trailing tabs
|
|
int Index = Value.Length;
|
|
while (Index > 0 && Value[Index - 1] == '\t')
|
|
{
|
|
Index--;
|
|
}
|
|
if (Index < Value.Length)
|
|
{
|
|
Value = Value.Substring(0, Index);
|
|
}
|
|
|
|
// trim leading newlines
|
|
Index = 0;
|
|
while (Index < Value.Length && Value[Index] == '\n')
|
|
{
|
|
Index++;
|
|
}
|
|
|
|
if (Index < Value.Length)
|
|
{
|
|
GlobalContext.StringVariables["Output"] += Value.Substring(Index);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "insertNewline":
|
|
GlobalContext.StringVariables["Output"] += "\n";
|
|
break;
|
|
|
|
case "insertValue":
|
|
{
|
|
string? Value = GetAttribute(CurrentContext, Node, "value");
|
|
if (Value != null)
|
|
{
|
|
GlobalContext.StringVariables["Output"] += Value;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "replace":
|
|
{
|
|
string? Find = GetAttribute(CurrentContext, Node, "find");
|
|
string? With = GetAttribute(CurrentContext, Node, "with");
|
|
if (Find != null && With != null)
|
|
{
|
|
GlobalContext.StringVariables["Output"] = GlobalContext.StringVariables["Output"].Replace(Find, With);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "copyFile":
|
|
{
|
|
string? Src = GetAttribute(CurrentContext, Node, "src");
|
|
string? Dst = GetAttribute(CurrentContext, Node, "dst");
|
|
bool bForce = StringToBool(GetAttribute(CurrentContext, Node, "force", true, false, "true"));
|
|
if (Src != null && Dst != null)
|
|
{
|
|
if (File.Exists(Src))
|
|
{
|
|
// check to see if newer than last time we copied
|
|
if (bForce || FilesAreDifferent(Src, Dst, Logger))
|
|
{
|
|
if (File.Exists(Dst))
|
|
{
|
|
File.SetAttributes(Dst, FileAttributes.Normal);
|
|
File.Delete(Dst);
|
|
}
|
|
Directory.CreateDirectory(Path.GetDirectoryName(Dst)!);
|
|
File.Copy(Src, Dst, true);
|
|
Logger.LogInformation("File {Src} copied to {Dst}", Src, Dst);
|
|
|
|
// remove any read only flags and keep timestamp
|
|
FileInfo DestFileInfo = new FileInfo(Dst);
|
|
DestFileInfo.Attributes &= ~FileAttributes.ReadOnly;
|
|
File.SetLastWriteTimeUtc(Dst, File.GetLastWriteTimeUtc(Src));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("File {Src} does not exist, not copied!", Src);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "copyDir":
|
|
{
|
|
string? Src = GetAttribute(CurrentContext, Node, "src");
|
|
string? Dst = GetAttribute(CurrentContext, Node, "dst");
|
|
bool bForce = StringToBool(GetAttribute(CurrentContext, Node, "force", true, false, "true"));
|
|
if (Src != null && Dst != null)
|
|
{
|
|
CopyFileDirectory(Src, Dst, Logger, bForce);
|
|
Logger.LogInformation("\nDirectory {Src} copied to {Dst} ({Force})", Src, Dst, bForce);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "deleteFiles":
|
|
{
|
|
string? Filespec = GetAttribute(CurrentContext, Node, "filespec");
|
|
bool bRecursive = StringToBool(GetAttribute(CurrentContext, Node, "recursive", true, false, "false"));
|
|
if (Filespec != null)
|
|
{
|
|
if (Filespec.Contains(':') || Filespec.Contains(".."))
|
|
{
|
|
Logger.LogInformation("\nFilespec {FileSpec} not allowed; ignored.", Filespec);
|
|
}
|
|
else
|
|
{
|
|
// force relative to BuildDir (and only from global context so someone doesn't try to be clever)
|
|
DeleteFiles(Path.Combine(GlobalContext.StringVariables["BuildDir"], Filespec), bRecursive, Logger);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "loadLibrary":
|
|
{
|
|
string? Name = GetAttribute(CurrentContext, Node, "name");
|
|
string? FailMsg = GetAttribute(CurrentContext, Node, "failmsg", true, false);
|
|
if (Name != null)
|
|
{
|
|
string Work = "\t\ttry\n" +
|
|
"\t\t{\n" +
|
|
"\t\t\tSystem.loadLibrary(\"" + Name + "\");\n" +
|
|
"\t\t}\n" +
|
|
"\t\tcatch (java.lang.UnsatisfiedLinkError e)\n" +
|
|
"\t\t{\n";
|
|
if (FailMsg != null)
|
|
{
|
|
Work += "\t\t\tLog.debug(e.toString());\n";
|
|
Work += "\t\t\tLog.debug(\"" + FailMsg + "\");\n";
|
|
}
|
|
GlobalContext.StringVariables["Output"] += Work + "\t\t}\n";
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBool":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Value = GetAttribute(CurrentContext, Node, "value", true, false, "false");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = StringToBool(Value);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolEnvVarDefined":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Value = GetAttribute(CurrentContext, Node, "value");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = (Value != null && Environment.ExpandEnvironmentVariables(Value).Length > 0);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolFrom":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Value = GetAttribute(CurrentContext, Node, "value", true, false, "false");
|
|
if (Result != null)
|
|
{
|
|
Value = ExpandVariables(CurrentContext, "$B(" + Value + ")");
|
|
CurrentContext.BoolVariables[Result] = StringToBool(Value);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolFromProperty":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Ini = GetAttribute(CurrentContext, Node, "ini");
|
|
string? Section = GetAttribute(CurrentContext, Node, "section");
|
|
string? Property = GetAttribute(CurrentContext, Node, "property");
|
|
string DefaultVal = GetAttribute(CurrentContext, Node, "default", true, false, "false");
|
|
if (Result != null && Ini != null && Section != null && Property != null)
|
|
{
|
|
bool Value = StringToBool(DefaultVal);
|
|
|
|
ConfigCacheIni_UPL ConfigIni = GetConfigCacheIni_UPL(Ini);
|
|
if (ConfigIni != null)
|
|
{
|
|
if (!ConfigIni.GetBool(Section, Property, out Value))
|
|
{
|
|
Value = StringToBool(DefaultVal);
|
|
}
|
|
}
|
|
CurrentContext.BoolVariables[Result] = Value;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolFromPropertyContains":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Ini = GetAttribute(CurrentContext, Node, "ini");
|
|
string? Section = GetAttribute(CurrentContext, Node, "section");
|
|
string? Property = GetAttribute(CurrentContext, Node, "property");
|
|
string DefaultVal = GetAttribute(CurrentContext, Node, "default", true, false, "false");
|
|
string Contains = GetAttribute(CurrentContext, Node, "contains", true, true, "");
|
|
if (Result != null && Ini != null && Section != null && Property != null)
|
|
{
|
|
bool Value = StringToBool(DefaultVal);
|
|
|
|
ConfigCacheIni_UPL ConfigIni = GetConfigCacheIni_UPL(Ini);
|
|
if (ConfigIni != null)
|
|
{
|
|
List<string>? StringList;
|
|
if (ConfigIni.GetArray(Section, Property, out StringList))
|
|
{
|
|
Value = false;
|
|
foreach (string Entry in StringList)
|
|
{
|
|
if (Entry.Equals(Contains))
|
|
{
|
|
Value = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CurrentContext.BoolVariables[Result] = Value;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolContains":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Source = GetAttribute(CurrentContext, Node, "source", true, false, "");
|
|
string Find = GetAttribute(CurrentContext, Node, "find", true, false, "");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = Source.Contains(Find);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolStartsWith":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Source = GetAttribute(CurrentContext, Node, "source", true, false, "");
|
|
string Find = GetAttribute(CurrentContext, Node, "find", true, false, "");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = Source.StartsWith(Find);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolEndsWith":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Source = GetAttribute(CurrentContext, Node, "source", true, false, "");
|
|
string Find = GetAttribute(CurrentContext, Node, "find", true, false, "");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = Source.EndsWith(Find);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolNot":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Source = GetAttribute(CurrentContext, Node, "source", true, false, "false");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = !StringToBool(Source);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolAnd":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "false");
|
|
string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "false");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = StringToBool(Arg1) && StringToBool(Arg2);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolOr":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "false");
|
|
string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "false");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = StringToBool(Arg1) || StringToBool(Arg2);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolIsEqual":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "");
|
|
string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = Arg1.Equals(Arg2);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolIsLess":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false);
|
|
string? Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false);
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = (StringToInt(CurrentContext, Node, Arg1) < StringToInt(CurrentContext, Node, Arg2));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolIsLessEqual":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false);
|
|
string? Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false);
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = (StringToInt(CurrentContext, Node, Arg1) <= StringToInt(CurrentContext, Node, Arg2));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolIsGreater":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false);
|
|
string? Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false);
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = (StringToInt(CurrentContext, Node, Arg1) > StringToInt(CurrentContext, Node, Arg2));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolIsGreaterEqual":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false);
|
|
string? Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false);
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = (StringToInt(CurrentContext, Node, Arg1) >= StringToInt(CurrentContext, Node, Arg2));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setBoolFileExists":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? FilePath = GetAttribute(CurrentContext, Node, "file");
|
|
if (Result != null && FilePath != null)
|
|
{
|
|
CurrentContext.BoolVariables[Result] = File.Exists(FilePath);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setInt":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Value = GetAttribute(CurrentContext, Node, "value", true, false, "0");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Value);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setIntFrom":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Value = GetAttribute(CurrentContext, Node, "value", true, false, "0");
|
|
if (Result != null)
|
|
{
|
|
Value = ExpandVariables(CurrentContext, "$I(" + Value + ")");
|
|
CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Value);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setIntFromProperty":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Ini = GetAttribute(CurrentContext, Node, "ini");
|
|
string? Section = GetAttribute(CurrentContext, Node, "section");
|
|
string? Property = GetAttribute(CurrentContext, Node, "property");
|
|
string DefaultVal = GetAttribute(CurrentContext, Node, "default", true, false, "0");
|
|
if (Result != null && Ini != null && Section != null && Property != null)
|
|
{
|
|
int DefaultInt = StringToInt(CurrentContext, Node, DefaultVal);
|
|
int Value = DefaultInt;
|
|
|
|
ConfigCacheIni_UPL ConfigIni = GetConfigCacheIni_UPL(Ini);
|
|
if (ConfigIni != null)
|
|
{
|
|
if (!ConfigIni.GetInt32(Section, Property, out Value))
|
|
{
|
|
Value = DefaultInt;
|
|
}
|
|
}
|
|
CurrentContext.IntVariables[Result] = Value;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setIntFromPropertyArrayNum":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Ini = GetAttribute(CurrentContext, Node, "ini");
|
|
string? Section = GetAttribute(CurrentContext, Node, "section");
|
|
string? Property = GetAttribute(CurrentContext, Node, "property");
|
|
if (Result != null && Ini != null && Section != null && Property != null)
|
|
{
|
|
int Value = 0;
|
|
|
|
ConfigCacheIni_UPL ConfigIni = GetConfigCacheIni_UPL(Ini);
|
|
if (ConfigIni != null)
|
|
{
|
|
List<string>? StringList;
|
|
if (ConfigIni.GetArray(Section, Property, out StringList))
|
|
{
|
|
Value = StringList.Count;
|
|
}
|
|
}
|
|
CurrentContext.IntVariables[Result] = Value;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setIntAdd":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "0");
|
|
string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "0");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Arg1) + StringToInt(CurrentContext, Node, Arg2);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setIntSubtract":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "0");
|
|
string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "0");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Arg1) - StringToInt(CurrentContext, Node, Arg2);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setIntMultiply":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "1");
|
|
string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "1");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Arg1) * StringToInt(CurrentContext, Node, Arg2);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setIntDivide":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "1");
|
|
string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "1");
|
|
if (Result != null)
|
|
{
|
|
int Denominator = StringToInt(CurrentContext, Node, Arg2);
|
|
if (Denominator == 0)
|
|
{
|
|
CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Arg1);
|
|
}
|
|
else
|
|
{
|
|
CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Arg1) / Denominator;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setIntLength":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Source = GetAttribute(CurrentContext, Node, "source", true, false, "");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.IntVariables[Result] = Source.Length;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setIntFindString":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Source = GetAttribute(CurrentContext, Node, "source", true, false, "");
|
|
string Find = GetAttribute(CurrentContext, Node, "find", true, false, "");
|
|
if (Result != null)
|
|
{
|
|
CurrentContext.IntVariables[Result] = Source.IndexOf(Find);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setString":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Value = GetAttribute(CurrentContext, Node, "value", true, false, "");
|
|
if (Result != null)
|
|
{
|
|
if (Result == "Output")
|
|
{
|
|
GlobalContext.StringVariables["Output"] = Value;
|
|
}
|
|
else
|
|
{
|
|
CurrentContext.StringVariables[Result] = Value;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringFrom":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Value = GetAttribute(CurrentContext, Node, "value", true, false, "0");
|
|
if (Result != null)
|
|
{
|
|
Value = ExpandVariables(CurrentContext, "$S(" + Value + ")");
|
|
CurrentContext.StringVariables[Result] = Value;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringFromEnvVar":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Value = GetAttribute(CurrentContext, Node, "value");
|
|
if (Result != null && Value != null)
|
|
{
|
|
CurrentContext.StringVariables[Result] = Environment.ExpandEnvironmentVariables(Value);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringFromTag":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Tag = GetAttribute(CurrentContext, Node, "tag", true, false, "$");
|
|
if (Result != null)
|
|
{
|
|
XElement? Element = CurrentElement;
|
|
if (Tag.StartsWith("$"))
|
|
{
|
|
if (Tag.Length > 1)
|
|
{
|
|
if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Element))
|
|
{
|
|
if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Element))
|
|
{
|
|
Logger.LogWarning("Missing element variable '{Tag}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node));
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CurrentContext.StringVariables[Result] = Element.Name.ToString();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringFromAttribute":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Tag = GetAttribute(CurrentContext, Node, "tag");
|
|
string? Name = GetAttribute(CurrentContext, Node, "name");
|
|
if (Result != null && Tag != null && Name != null)
|
|
{
|
|
XElement? Element = CurrentElement;
|
|
if (Tag.StartsWith("$"))
|
|
{
|
|
if (Tag.Length > 1)
|
|
{
|
|
if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Element))
|
|
{
|
|
if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Element))
|
|
{
|
|
Logger.LogWarning("Missing element variable '{Tag}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node));
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
XNamespace? XMLNameSpace = TrimNamespaceAliasFromName(ref Name);
|
|
XAttribute? Attribute = Element.Attribute(XMLNameSpace != null ? XMLNameSpace + Name : Name);
|
|
|
|
CurrentContext.StringVariables[Result] = (Attribute != null) ? Attribute.Value : "";
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringFromTagText":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Tag = GetAttribute(CurrentContext, Node, "tag", true, false, "$");
|
|
if (Result != null)
|
|
{
|
|
XElement? Element = CurrentElement;
|
|
if (Tag.StartsWith("$"))
|
|
{
|
|
if (Tag.Length > 1)
|
|
{
|
|
if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Element))
|
|
{
|
|
if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Element))
|
|
{
|
|
Logger.LogWarning("Missing element variable '{Tag}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node));
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Element.Value == null)
|
|
{
|
|
Logger.LogWarning("Expected text in element '{Element}' in '{Node}' but found none (skipping instruction)", Element.Name.ToString(), TraceNodeString(CurrentContext, Node));
|
|
continue;
|
|
}
|
|
|
|
CurrentContext.StringVariables[Result] = Element.Value;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringFromProperty":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Ini = GetAttribute(CurrentContext, Node, "ini");
|
|
string? Section = GetAttribute(CurrentContext, Node, "section");
|
|
string? Property = GetAttribute(CurrentContext, Node, "property");
|
|
string DefaultVal = GetAttribute(CurrentContext, Node, "default", true, false, "");
|
|
if (Result != null && Ini != null && Section != null && Property != null)
|
|
{
|
|
string Value = DefaultVal;
|
|
|
|
ConfigCacheIni_UPL ConfigIni = GetConfigCacheIni_UPL(Ini);
|
|
if (ConfigIni != null)
|
|
{
|
|
if (!ConfigIni.GetString(Section, Property, out Value))
|
|
{
|
|
// If the string was not found in the config, Value will have been set to an empty string
|
|
// Set it back to the DefaultVal
|
|
Value = DefaultVal;
|
|
}
|
|
}
|
|
if (Result == "Output")
|
|
{
|
|
GlobalContext.StringVariables["Output"] = Value;
|
|
}
|
|
else
|
|
{
|
|
CurrentContext.StringVariables[Result] = Value;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringFromPropertyArray":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Ini = GetAttribute(CurrentContext, Node, "ini");
|
|
string? Section = GetAttribute(CurrentContext, Node, "section");
|
|
string? Property = GetAttribute(CurrentContext, Node, "property");
|
|
string? IndexStr = GetAttribute(CurrentContext, Node, "index", true);
|
|
string DefaultVal = GetAttribute(CurrentContext, Node, "default", true, false, "");
|
|
if (Result != null && Ini != null && Section != null && Property != null && IndexStr != null)
|
|
{
|
|
string Value = DefaultVal;
|
|
|
|
ConfigCacheIni_UPL ConfigIni = GetConfigCacheIni_UPL(Ini);
|
|
if (ConfigIni != null)
|
|
{
|
|
List<string>? StringList;
|
|
if (ConfigIni.GetArray(Section, Property, out StringList))
|
|
{
|
|
int Index = StringToInt(CurrentContext, Node, IndexStr);
|
|
if (Index >= 0 && Index < StringList.Count)
|
|
{
|
|
Value = StringList.ElementAt(Index);
|
|
}
|
|
}
|
|
}
|
|
if (Result == "Output")
|
|
{
|
|
GlobalContext.StringVariables["Output"] = Value;
|
|
}
|
|
else
|
|
{
|
|
CurrentContext.StringVariables[Result] = Value;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringAdd":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "");
|
|
string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "");
|
|
if (Result != null)
|
|
{
|
|
string Value = Arg1 + Arg2;
|
|
if (Result == "Output")
|
|
{
|
|
GlobalContext.StringVariables["Output"] = Value;
|
|
}
|
|
else
|
|
{
|
|
CurrentContext.StringVariables[Result] = Value;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringSubstring":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Source = GetAttribute(CurrentContext, Node, "source", true, false, "");
|
|
string Start = GetAttribute(CurrentContext, Node, "start", true, false, "0");
|
|
string Length = GetAttribute(CurrentContext, Node, "length", true, false, "0");
|
|
if (Result != null && Source != null)
|
|
{
|
|
int Index = StringToInt(CurrentContext, Node, Start);
|
|
int Count = StringToInt(CurrentContext, Node, Length);
|
|
Index = (Index < 0) ? 0 : (Index > Source.Length) ? Source.Length : Index;
|
|
Count = (Index + Count > Source.Length) ? Source.Length - Index : Count;
|
|
string Value = Source.Substring(Index, Count);
|
|
if (Result == "Output")
|
|
{
|
|
GlobalContext.StringVariables["Output"] = Value;
|
|
}
|
|
else
|
|
{
|
|
CurrentContext.StringVariables[Result] = Value;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringSubstringAfterFind":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Source = GetAttribute(CurrentContext, Node, "source", true, false, "");
|
|
string Find = GetAttribute(CurrentContext, Node, "find", true, false, "");
|
|
string Length = GetAttribute(CurrentContext, Node, "length", true, false, "-1");
|
|
string DefaultVal = GetAttribute(CurrentContext, Node, "default", true, false, "");
|
|
if (Result != null)
|
|
{
|
|
string Value = DefaultVal;
|
|
int Index = Source.IndexOf(Find);
|
|
if (Index != -1)
|
|
{
|
|
Index += Find.Length;
|
|
int Count = StringToInt(CurrentContext, Node, Length);
|
|
Count = Count == -1 || (Index + Count > Source.Length) ? Source.Length - Index : Count;
|
|
Value = Source.Substring(Index, Count);
|
|
}
|
|
if (Result == "Output")
|
|
{
|
|
GlobalContext.StringVariables["Output"] = Value;
|
|
}
|
|
else
|
|
{
|
|
CurrentContext.StringVariables[Result] = Value;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringReplace":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Source = GetAttribute(CurrentContext, Node, "source", true, false, "");
|
|
string? Find = GetAttribute(CurrentContext, Node, "find");
|
|
string With = GetAttribute(CurrentContext, Node, "with", true, false, "");
|
|
if (Result != null && Find != null)
|
|
{
|
|
string Value = Source.Replace(Find, With);
|
|
if (Result == "Output")
|
|
{
|
|
GlobalContext.StringVariables["Output"] = Value;
|
|
}
|
|
else
|
|
{
|
|
CurrentContext.StringVariables[Result] = Value;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringToLower":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Source = GetAttribute(CurrentContext, Node, "source", true, false, "");
|
|
if (Result != null)
|
|
{
|
|
string Value = Source.ToLower();
|
|
if (Result == "Output")
|
|
{
|
|
GlobalContext.StringVariables["Output"] = Value;
|
|
}
|
|
else
|
|
{
|
|
CurrentContext.StringVariables[Result] = Value;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setStringToUpper":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string Source = GetAttribute(CurrentContext, Node, "source", true, false, "");
|
|
if (Result != null)
|
|
{
|
|
string Value = Source.ToUpper();
|
|
if (Result == "Output")
|
|
{
|
|
GlobalContext.StringVariables["Output"] = Value;
|
|
}
|
|
else
|
|
{
|
|
CurrentContext.StringVariables[Result] = Value;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "setElement":
|
|
{
|
|
string? Result = GetAttribute(CurrentContext, Node, "result");
|
|
string? Value = GetAttribute(CurrentContext, Node, "value", true, false);
|
|
string? Text = GetAttribute(CurrentContext, Node, "text", true, false);
|
|
string? Parse = GetAttribute(CurrentContext, Node, "xml", true, false);
|
|
if (Result != null)
|
|
{
|
|
if (Value != null)
|
|
{
|
|
XNamespace? XMLNameSpace = TrimNamespaceAliasFromName(ref Value);
|
|
XElement NewElement = new XElement(XMLNameSpace != null ? XMLNameSpace + Value : Value);
|
|
if (Text != null)
|
|
{
|
|
NewElement.Value = Text;
|
|
}
|
|
CurrentContext.ElementVariables[Result] = NewElement;
|
|
}
|
|
else if (Parse != null)
|
|
{
|
|
try
|
|
{
|
|
CurrentContext.ElementVariables[Result] = XElement.Parse(Parse);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.LogError(e, "XML parsing {Parse} failed! {Ex} (skipping instruction)", Parse, e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
Logger.LogWarning("Unknown command: {Name}", Node.Name);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return GlobalContext.StringVariables["Output"];
|
|
}
|
|
|
|
public void Init(IEnumerable<string> Architectures, bool bDistribution, string EngineDirectory, string BuildDirectory, string ProjectDirectory, string Configuration, bool bIsEmbedded,
|
|
bool bPerArchBuildDir = false, Dictionary<string, string>? ArchRemapping = null)
|
|
{
|
|
GlobalContext.BoolVariables["Distribution"] = bDistribution;
|
|
GlobalContext.BoolVariables["IsEmbedded"] = bIsEmbedded;
|
|
GlobalContext.StringVariables["Configuration"] = Configuration;
|
|
|
|
GlobalContext.StringVariables["EngineDir"] = EngineDirectory.Replace("\\", "/");
|
|
GlobalContext.StringVariables["BuildDir"] = BuildDirectory.Replace("\\", "/");
|
|
GlobalContext.StringVariables["ProjectDir"] = ProjectDirectory.Replace("\\", "/");
|
|
|
|
ReadOnlyBuildVersion Version = ReadOnlyBuildVersion.Current;
|
|
GlobalContext.IntVariables["EngineMajorVersion"] = Version.MajorVersion;
|
|
GlobalContext.IntVariables["EngineMinorVersion"] = Version.MinorVersion;
|
|
GlobalContext.IntVariables["EnginePatchVersion"] = Version.PatchVersion;
|
|
GlobalContext.IntVariables["EngineChangelist"] = Version.Changelist;
|
|
GlobalContext.StringVariables["EngineVersion"] = Version.MajorVersion.ToString() + "." + Version.MinorVersion.ToString() + "." + Version.PatchVersion.ToString();
|
|
GlobalContext.StringVariables["EngineBranchName"] = Version.BranchName ?? "";
|
|
|
|
if (GlobalContext.StringVariables["EngineDir"].Length < 1)
|
|
{
|
|
GlobalContext.StringVariables["EngineDir"] = "./";
|
|
}
|
|
if (GlobalContext.StringVariables["BuildDir"].Length < 1)
|
|
{
|
|
GlobalContext.StringVariables["BuildDir"] = "./";
|
|
}
|
|
if (GlobalContext.StringVariables["ProjectDir"].Length < 1)
|
|
{
|
|
GlobalContext.StringVariables["ProjectDir"] = "./";
|
|
}
|
|
|
|
GlobalContext.StringVariables["AbsEngineDir"] = Path.GetFullPath(GlobalContext.StringVariables["EngineDir"]).Replace("\\", "/");
|
|
GlobalContext.StringVariables["AbsBuildDir"] = Path.GetFullPath(GlobalContext.StringVariables["BuildDir"]).Replace("\\", "/");
|
|
GlobalContext.StringVariables["AbsProjectDir"] = Path.GetFullPath(GlobalContext.StringVariables["ProjectDir"]).Replace("\\", "/");
|
|
|
|
foreach (string Arch in Architectures)
|
|
{
|
|
if (bPerArchBuildDir)
|
|
{
|
|
string ActiveArch = Arch;
|
|
if (ArchRemapping != null && ArchRemapping.ContainsKey(ActiveArch))
|
|
{
|
|
ActiveArch = ArchRemapping[ActiveArch];
|
|
}
|
|
string ArchBuildDirectory = Path.Combine(GlobalContext.StringVariables["BuildDir"], ActiveArch.Replace("-", "_"));
|
|
string ArchBuildDir = ArchBuildDirectory.Replace("\\", "/");
|
|
string ArchAbsBuildDir = Path.GetFullPath(ArchBuildDir).Replace("\\", "/");
|
|
|
|
// add it to all the architecture contexts (overrides global context)
|
|
for (int Index = 1; Index <= ContextIndex; Index++)
|
|
{
|
|
UPLContext ArchContext = Contexts[Arch + "_" + Index];
|
|
ArchContext.StringVariables["BuildDir"] = ArchBuildDir;
|
|
ArchContext.StringVariables["AbsBuildDir"] = ArchAbsBuildDir;
|
|
}
|
|
}
|
|
|
|
Logger.LogInformation("UPL Init: {Arch}", Arch);
|
|
ProcessPluginNode(Arch, "init", "");
|
|
}
|
|
|
|
if (bGlobalTrace)
|
|
{
|
|
Logger.LogInformation("\nVariables:\n{Variables}", DumpVariables());
|
|
}
|
|
}
|
|
|
|
public void SetGlobalContextVariable(string VariableName, bool Value)
|
|
{
|
|
if (VariableName != null)
|
|
{
|
|
GlobalContext.BoolVariables[VariableName] = Value;
|
|
}
|
|
}
|
|
|
|
public void SetGlobalContextVariable(string VariableName, int Value)
|
|
{
|
|
if (VariableName != null)
|
|
{
|
|
GlobalContext.IntVariables[VariableName] = Value;
|
|
}
|
|
}
|
|
|
|
public void SetGlobalContextVariable(string VariableName, string Value)
|
|
{
|
|
if (VariableName != null && Value != null)
|
|
{
|
|
GlobalContext.StringVariables[VariableName] = Value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Equivalent of FConfigCacheIni_UPL. Parses ini files. This version reads ALL sections since ConfigCacheIni_UPL does NOT
|
|
/// </summary>
|
|
class ConfigCacheIni_UPL
|
|
{
|
|
/// <summary>
|
|
/// Exception when parsing ini files
|
|
/// </summary>
|
|
public class IniParsingException : Exception
|
|
{
|
|
public IniParsingException(string Message)
|
|
: base(Message)
|
|
{ }
|
|
public IniParsingException(string Format, params object[] Args)
|
|
: base(String.Format(Format, Args))
|
|
{ }
|
|
}
|
|
|
|
/// <summary>
|
|
/// command class for being able to create config caches over and over without needing to read the ini files
|
|
/// </summary>
|
|
public class Command
|
|
{
|
|
public string? TrimmedLine;
|
|
}
|
|
|
|
class SectionCommand : Command
|
|
{
|
|
public FileReference? Filename;
|
|
public int LineIndex;
|
|
}
|
|
|
|
class KeyValueCommand : Command
|
|
{
|
|
public string? Key;
|
|
public string? Value;
|
|
public ParseAction LastAction;
|
|
}
|
|
|
|
// cached ini files
|
|
static Dictionary<string, List<Command>> FileCache = new Dictionary<string, List<Command>>();
|
|
static Dictionary<string, ConfigCacheIni_UPL> IniCache = new Dictionary<string, ConfigCacheIni_UPL>();
|
|
static Dictionary<string, ConfigCacheIni_UPL> BaseIniCache = new Dictionary<string, ConfigCacheIni_UPL>();
|
|
|
|
// static creation functions for ini files
|
|
public static ConfigCacheIni_UPL CreateConfigCacheIni_UPL(UnrealTargetPlatform Platform, string BaseIniName, DirectoryReference? ProjectDirectory, ILogger Logger, DirectoryReference? EngineDirectory = null)
|
|
{
|
|
if (EngineDirectory == null)
|
|
{
|
|
EngineDirectory = Unreal.EngineDirectory;
|
|
}
|
|
|
|
// cache base ini for use as the seed for the rest
|
|
if (!BaseIniCache.ContainsKey(BaseIniName))
|
|
{
|
|
BaseIniCache.Add(BaseIniName, new ConfigCacheIni_UPL(BuildHostPlatform.Current.Platform, BaseIniName, null, Logger, EngineDirectory, EngineOnly: true));
|
|
}
|
|
|
|
// build the new ini and cache it for later re-use
|
|
ConfigCacheIni_UPL BaseCache = BaseIniCache[BaseIniName];
|
|
string Key = GetIniPlatformName(Platform) + BaseIniName + EngineDirectory.FullName + (ProjectDirectory != null ? ProjectDirectory.FullName : "");
|
|
if (!IniCache.ContainsKey(Key))
|
|
{
|
|
IniCache.Add(Key, new ConfigCacheIni_UPL(Platform, BaseIniName, ProjectDirectory, Logger, EngineDirectory, BaseCache: BaseCache));
|
|
}
|
|
return IniCache[Key];
|
|
}
|
|
|
|
/// <summary>
|
|
/// List of values (or a single value)
|
|
/// </summary>
|
|
public class IniValues : List<string>
|
|
{
|
|
public IniValues()
|
|
{
|
|
}
|
|
public IniValues(IniValues Other)
|
|
: base(Other)
|
|
{
|
|
}
|
|
public override string ToString()
|
|
{
|
|
return String.Join(",", ToArray());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ini section (map of keys and values)
|
|
/// </summary>
|
|
public class IniSection : Dictionary<string, IniValues>
|
|
{
|
|
public IniSection()
|
|
: base(StringComparer.InvariantCultureIgnoreCase)
|
|
{ }
|
|
public IniSection(IniSection Other)
|
|
: this()
|
|
{
|
|
foreach (KeyValuePair<string, IniValues> Pair in Other)
|
|
{
|
|
Add(Pair.Key, new IniValues(Pair.Value));
|
|
}
|
|
}
|
|
public override string ToString()
|
|
{
|
|
return "IniSection";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// True if we are loading a hierarchy of config files that should be merged together
|
|
/// </summary>
|
|
bool bIsMergingConfigs;
|
|
|
|
/// <summary>
|
|
/// All sections parsed from ini file
|
|
/// </summary>
|
|
Dictionary<string, IniSection> Sections;
|
|
|
|
private ConfigCacheIni_UPL()
|
|
{
|
|
Sections = new Dictionary<string, IniSection>(StringComparer.InvariantCultureIgnoreCase);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor. Parses a single ini file. No Platform settings, no engine hierarchy. Do not use this with ini files that have hierarchy!
|
|
/// </summary>
|
|
/// <param name="Filename">The ini file to load</param>
|
|
/// <param name="Logger"></param>
|
|
public ConfigCacheIni_UPL(FileReference Filename, ILogger Logger) : this()
|
|
{
|
|
bIsMergingConfigs = false;
|
|
ParseIniFile(Filename, Logger);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor. Parses ini hierarchy for the specified project. No Platform settings.
|
|
/// </summary>
|
|
/// <param name="BaseIniName">Ini name (Engine, Editor, etc)</param>
|
|
/// <param name="ProjectDirectory">Project path</param>
|
|
/// <param name="Logger">Logger for </param>
|
|
/// <param name="EngineDirectory"></param>
|
|
public ConfigCacheIni_UPL(string BaseIniName, string? ProjectDirectory, ILogger Logger, string? EngineDirectory = null)
|
|
: this(BuildHostPlatform.Current.Platform, BaseIniName, ProjectDirectory, Logger, EngineDirectory)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor. Parses ini hierarchy for the specified project. No Platform settings.
|
|
/// </summary>
|
|
/// <param name="BaseIniName">Ini name (Engine, Editor, etc)</param>
|
|
/// <param name="ProjectDirectory">Project path</param>
|
|
/// <param name="Logger">Logger for output</param>
|
|
/// <param name="EngineDirectory"></param>
|
|
public ConfigCacheIni_UPL(string BaseIniName, DirectoryReference ProjectDirectory, ILogger Logger, DirectoryReference? EngineDirectory = null)
|
|
: this(BuildHostPlatform.Current.Platform, BaseIniName, ProjectDirectory, Logger, EngineDirectory)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor. Parses ini hierarchy for the specified platform and project.
|
|
/// </summary>
|
|
/// <param name="ProjectDirectory">Project path</param>
|
|
/// <param name="Platform">Target platform</param>
|
|
/// <param name="BaseIniName">Ini name (Engine, Editor, etc)</param>
|
|
/// <param name="Logger"></param>
|
|
/// <param name="EngineDirectory"></param>
|
|
public ConfigCacheIni_UPL(UnrealTargetPlatform Platform, string BaseIniName, string? ProjectDirectory, ILogger Logger, string? EngineDirectory = null)
|
|
: this(Platform, BaseIniName,
|
|
(ProjectDirectory == null) ? null : new DirectoryReference(ProjectDirectory),
|
|
Logger,
|
|
(EngineDirectory == null) ? null : new DirectoryReference(EngineDirectory))
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor. Parses ini hierarchy for the specified platform and project.
|
|
/// </summary>
|
|
/// <param name="ProjectDirectory">Project path</param>
|
|
/// <param name="Platform">Target platform</param>
|
|
/// <param name="BaseIniName">Ini name (Engine, Editor, etc)</param>
|
|
/// <param name="Logger"></param>
|
|
/// <param name="EngineDirectory"></param>
|
|
/// <param name="EngineOnly"></param>
|
|
/// <param name="BaseCache"></param>
|
|
public ConfigCacheIni_UPL(UnrealTargetPlatform Platform, string BaseIniName, DirectoryReference? ProjectDirectory, ILogger Logger, DirectoryReference? EngineDirectory = null, bool EngineOnly = false, ConfigCacheIni_UPL? BaseCache = null) : this()
|
|
{
|
|
bIsMergingConfigs = true;
|
|
if (EngineDirectory == null)
|
|
{
|
|
EngineDirectory = Unreal.EngineDirectory;
|
|
}
|
|
|
|
if (BaseCache != null)
|
|
{
|
|
foreach (KeyValuePair<string, IniSection> Pair in BaseCache.Sections)
|
|
{
|
|
Sections.Add(Pair.Key, new IniSection(Pair.Value));
|
|
}
|
|
}
|
|
if (EngineOnly)
|
|
{
|
|
foreach (FileReference IniFileName in EnumerateEngineIniFileNames(EngineDirectory, BaseIniName))
|
|
{
|
|
if (FileReference.Exists(IniFileName))
|
|
{
|
|
ParseIniFile(IniFileName, Logger);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (FileReference IniFileName in EnumerateCrossPlatformIniFileNames(ProjectDirectory, EngineDirectory, Platform, BaseIniName, BaseCache != null))
|
|
{
|
|
if (FileReference.Exists(IniFileName))
|
|
{
|
|
ParseIniFile(IniFileName, Logger);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds a section in INI
|
|
/// </summary>
|
|
/// <param name="SectionName"></param>
|
|
/// <returns>Found section or null</returns>
|
|
public IniSection? FindSection(string SectionName)
|
|
{
|
|
IniSection? Section;
|
|
Sections.TryGetValue(SectionName, out Section);
|
|
return Section;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds values associated with the specified key (does not copy the list)
|
|
/// </summary>
|
|
private bool GetList(string SectionName, string Key, [NotNullWhen(true)] out IniValues? Value)
|
|
{
|
|
Value = null;
|
|
|
|
IniSection? Section = FindSection(SectionName);
|
|
if (Section == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Section.TryGetValue(Key, out Value) && Value != null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all values associated with the specified key
|
|
/// </summary>
|
|
/// <param name="SectionName">Section where the key is located</param>
|
|
/// <param name="Key">Key name</param>
|
|
/// <param name="Value">Copy of the list containing all values associated with the specified key</param>
|
|
/// <returns>True if the key exists</returns>
|
|
public bool GetArray(string SectionName, string Key, [NotNullWhen(true)] out List<string>? Value)
|
|
{
|
|
Value = null;
|
|
if (GetList(SectionName, Key, out IniValues? ValueList) && ValueList != null)
|
|
{
|
|
Value = new List<string>(ValueList);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a single string value associated with the specified key.
|
|
/// </summary>
|
|
/// <param name="SectionName">Section name</param>
|
|
/// <param name="Key">Key name</param>
|
|
/// <param name="Value">Value associated with the specified key. If the key has more than one value, only the first one is returned</param>
|
|
/// <returns>True if the key exists</returns>
|
|
public bool GetString(string SectionName, string Key, out string Value)
|
|
{
|
|
Value = String.Empty;
|
|
IniValues? ValueList;
|
|
bool Result = GetList(SectionName, Key, out ValueList);
|
|
if (Result && ValueList != null && ValueList.Count > 0)
|
|
{
|
|
Value = ValueList[0];
|
|
Result = true;
|
|
}
|
|
else
|
|
{
|
|
Result = false;
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a single bool value associated with the specified key.
|
|
/// </summary>
|
|
/// <param name="SectionName">Section name</param>
|
|
/// <param name="Key">Key name</param>
|
|
/// <param name="Value">Value associated with the specified key. If the key has more than one value, only the first one is returned</param>
|
|
/// <returns>True if the key exists</returns>
|
|
public bool GetBool(string SectionName, string Key, out bool Value)
|
|
{
|
|
Value = false;
|
|
string TextValue;
|
|
bool Result = GetString(SectionName, Key, out TextValue);
|
|
if (Result)
|
|
{
|
|
// C# Boolean type expects "False" or "True" but since we're not case sensitive, we need to suppor that manually
|
|
if (String.Equals(TextValue, "true", StringComparison.CurrentCultureIgnoreCase) || String.Equals(TextValue, "1"))
|
|
{
|
|
Value = true;
|
|
}
|
|
else if (String.Equals(TextValue, "false", StringComparison.CurrentCultureIgnoreCase) || String.Equals(TextValue, "0"))
|
|
{
|
|
Value = false;
|
|
}
|
|
else
|
|
{
|
|
// Failed to parse
|
|
Result = false;
|
|
}
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a single Int32 value associated with the specified key.
|
|
/// </summary>
|
|
/// <param name="SectionName">Section name</param>
|
|
/// <param name="Key">Key name</param>
|
|
/// <param name="Value">Value associated with the specified key. If the key has more than one value, only the first one is returned</param>
|
|
/// <returns>True if the key exists</returns>
|
|
public bool GetInt32(string SectionName, string Key, out int Value)
|
|
{
|
|
Value = 0;
|
|
string TextValue;
|
|
bool Result = GetString(SectionName, Key, out TextValue);
|
|
if (Result)
|
|
{
|
|
Result = Int32.TryParse(TextValue, out Value);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a single GUID value associated with the specified key.
|
|
/// </summary>
|
|
/// <param name="SectionName">Section name</param>
|
|
/// <param name="Key">Key name</param>
|
|
/// <param name="Value">Value associated with the specified key. If the key has more than one value, only the first one is returned</param>
|
|
/// <returns>True if the key exists</returns>
|
|
public bool GetGUID(string SectionName, string Key, out Guid Value)
|
|
{
|
|
Value = Guid.Empty;
|
|
string TextValue;
|
|
bool Result = GetString(SectionName, Key, out TextValue);
|
|
if (Result)
|
|
{
|
|
string HexString = "";
|
|
if (TextValue.Contains("A=") && TextValue.Contains("B=") && TextValue.Contains("C=") && TextValue.Contains("D="))
|
|
{
|
|
char[] Separators = new char[] { '(', ')', '=', ',', ' ', 'A', 'B', 'C', 'D' };
|
|
string[] ComponentValues = TextValue.Split(Separators, StringSplitOptions.RemoveEmptyEntries);
|
|
if (ComponentValues.Length == 4)
|
|
{
|
|
for (int ComponentIndex = 0; ComponentIndex < 4; ComponentIndex++)
|
|
{
|
|
int IntegerValue;
|
|
Result &= Int32.TryParse(ComponentValues[ComponentIndex], out IntegerValue);
|
|
HexString += IntegerValue.ToString("X8");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
HexString = TextValue;
|
|
}
|
|
|
|
try
|
|
{
|
|
Value = Guid.ParseExact(HexString, "N");
|
|
Result = true;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
Result = false;
|
|
}
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a single float value associated with the specified key.
|
|
/// </summary>
|
|
/// <param name="SectionName">Section name</param>
|
|
/// <param name="Key">Key name</param>
|
|
/// <param name="Value">Value associated with the specified key. If the key has more than one value, only the first one is returned</param>
|
|
/// <returns>True if the key exists</returns>
|
|
public bool GetSingle(string SectionName, string Key, out float Value)
|
|
{
|
|
Value = 0.0f;
|
|
string TextValue;
|
|
bool Result = GetString(SectionName, Key, out TextValue);
|
|
if (Result)
|
|
{
|
|
Result = Single.TryParse(TextValue, out Value);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a single double value associated with the specified key.
|
|
/// </summary>
|
|
/// <param name="SectionName">Section name</param>
|
|
/// <param name="Key">Key name</param>
|
|
/// <param name="Value">Value associated with the specified key. If the key has more than one value, only the first one is returned</param>
|
|
/// <returns>True if the key exists</returns>
|
|
public bool GetDouble(string SectionName, string Key, out double Value)
|
|
{
|
|
Value = 0.0;
|
|
string TextValue;
|
|
bool Result = GetString(SectionName, Key, out TextValue);
|
|
if (Result)
|
|
{
|
|
Result = Double.TryParse(TextValue, out Value);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
private static bool ExtractPath(string Source, out string Path)
|
|
{
|
|
int start = Source.IndexOf('"');
|
|
int end = Source.LastIndexOf('"');
|
|
if (start != 1 && end != -1 && start < end)
|
|
{
|
|
++start;
|
|
Path = Source.Substring(start, end - start);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
Path = "";
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool GetPath(string SectionName, string Key, out string Value)
|
|
{
|
|
string temp;
|
|
if (GetString(SectionName, Key, out temp))
|
|
{
|
|
return ExtractPath(temp, out Value);
|
|
}
|
|
else
|
|
{
|
|
Value = "";
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// List of actions that can be performed on a single line from ini file
|
|
/// </summary>
|
|
enum ParseAction
|
|
{
|
|
None,
|
|
New,
|
|
Add,
|
|
Remove
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks what action should be performed on a single line from ini file
|
|
/// </summary>
|
|
private ParseAction GetActionForLine(ref string Line)
|
|
{
|
|
if (String.IsNullOrEmpty(Line) || Line.StartsWith(";") || Line.StartsWith("//"))
|
|
{
|
|
return ParseAction.None;
|
|
}
|
|
else if (Line.StartsWith("-"))
|
|
{
|
|
Line = Line.Substring(1).TrimStart();
|
|
return ParseAction.Remove;
|
|
}
|
|
else if (Line.StartsWith("+"))
|
|
{
|
|
Line = Line.Substring(1).TrimStart();
|
|
return ParseAction.Add;
|
|
}
|
|
else
|
|
{
|
|
// We use Add rather than New when we're not merging config files together in order
|
|
// to mimic the behavior of the C++ config cache when loading a single file
|
|
return (bIsMergingConfigs) ? ParseAction.New : ParseAction.Add;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads and parses ini file.
|
|
/// </summary>
|
|
public void ParseIniFile(FileReference Filename, ILogger Logger)
|
|
{
|
|
string[]? IniLines = null;
|
|
List<Command>? Commands = null;
|
|
if (!FileCache.ContainsKey(Filename.FullName))
|
|
{
|
|
try
|
|
{
|
|
IniLines = File.ReadAllLines(Filename.FullName);
|
|
Commands = new List<Command>();
|
|
FileCache.Add(Filename.FullName, Commands);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogInformation("Error reading ini file: {Filename} Exception: {Ex}", Filename, ex.Message);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Commands = FileCache[Filename.FullName];
|
|
}
|
|
if (IniLines != null && Commands != null)
|
|
{
|
|
IniSection? CurrentSection = null;
|
|
|
|
// Line Index for exceptions
|
|
int LineIndex = 1;
|
|
bool bMultiLine = false;
|
|
string SingleValue = "";
|
|
string Key = "";
|
|
ParseAction LastAction = ParseAction.None;
|
|
|
|
// Parse each line
|
|
foreach (string Line in IniLines)
|
|
{
|
|
string TrimmedLine = Line.Trim();
|
|
// Multiline value support
|
|
bool bWasMultiLine = bMultiLine;
|
|
bMultiLine = TrimmedLine.EndsWith("\\");
|
|
if (bMultiLine)
|
|
{
|
|
TrimmedLine = TrimmedLine.Substring(0, TrimmedLine.Length - 1).TrimEnd();
|
|
}
|
|
if (!bWasMultiLine)
|
|
{
|
|
if (TrimmedLine.StartsWith("["))
|
|
{
|
|
CurrentSection = FindOrAddSection(TrimmedLine, Filename, LineIndex);
|
|
LastAction = ParseAction.None;
|
|
if (CurrentSection != null)
|
|
{
|
|
SectionCommand Command = new SectionCommand();
|
|
Command.Filename = Filename;
|
|
Command.LineIndex = LineIndex;
|
|
Command.TrimmedLine = TrimmedLine;
|
|
Commands.Add(Command);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (LastAction != ParseAction.None)
|
|
{
|
|
throw new IniParsingException("Parsing new key/value pair when the previous one has not yet been processed ({0}, {1}) in {2}, line {3}: {4}", Key, SingleValue, Filename, LineIndex, TrimmedLine);
|
|
}
|
|
// Check if the line is empty or a comment, also remove any +/- markers
|
|
LastAction = GetActionForLine(ref TrimmedLine);
|
|
if (LastAction != ParseAction.None)
|
|
{
|
|
/* if (CurrentSection == null)
|
|
{
|
|
throw new IniParsingException("Trying to parse key/value pair that doesn't belong to any section in {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine);
|
|
}*/
|
|
ParseKeyValuePair(TrimmedLine, Filename, LineIndex, out Key, out SingleValue);
|
|
}
|
|
}
|
|
}
|
|
if (bWasMultiLine)
|
|
{
|
|
SingleValue += TrimmedLine;
|
|
}
|
|
if (!bMultiLine && LastAction != ParseAction.None && CurrentSection != null)
|
|
{
|
|
ProcessKeyValuePair(CurrentSection, Key, SingleValue, LastAction);
|
|
KeyValueCommand Command = new KeyValueCommand();
|
|
Command.Key = Key;
|
|
Command.Value = SingleValue;
|
|
Command.LastAction = LastAction;
|
|
Commands.Add(Command);
|
|
LastAction = ParseAction.None;
|
|
SingleValue = "";
|
|
Key = "";
|
|
}
|
|
else if (CurrentSection == null)
|
|
{
|
|
LastAction = ParseAction.None;
|
|
}
|
|
LineIndex++;
|
|
}
|
|
}
|
|
else if (Commands != null)
|
|
{
|
|
IniSection? CurrentSection = null;
|
|
|
|
// run each command
|
|
for (int Idx = 0; Idx < Commands.Count; ++Idx)
|
|
{
|
|
Command Command = Commands[Idx];
|
|
if (Command is SectionCommand SectionCommand)
|
|
{
|
|
CurrentSection = FindOrAddSection(SectionCommand.TrimmedLine!, SectionCommand.Filename!, SectionCommand.LineIndex);
|
|
}
|
|
else if (Command is KeyValueCommand KeyValueCommand)
|
|
{
|
|
ProcessKeyValuePair(CurrentSection!, KeyValueCommand.Key!, KeyValueCommand.Value!, KeyValueCommand.LastAction);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Splits a line into key and value
|
|
/// </summary>
|
|
private static void ParseKeyValuePair(string TrimmedLine, FileReference Filename, int LineIndex, out string Key, out string Value)
|
|
{
|
|
int AssignIndex = TrimmedLine.IndexOf('=');
|
|
if (AssignIndex < 0)
|
|
{
|
|
throw new IniParsingException("Failed to find value when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine);
|
|
}
|
|
Key = TrimmedLine.Substring(0, AssignIndex).Trim();
|
|
if (String.IsNullOrEmpty(Key))
|
|
{
|
|
throw new IniParsingException("Empty key when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine);
|
|
}
|
|
Value = TrimmedLine.Substring(AssignIndex + 1).Trim();
|
|
if (Value.StartsWith("\""))
|
|
{
|
|
// Remove quotes
|
|
int QuoteEnd = Value.LastIndexOf('\"');
|
|
if (QuoteEnd == 0)
|
|
{
|
|
throw new IniParsingException("Mismatched quotes when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine);
|
|
}
|
|
Value = Value.Substring(1, Value.Length - 2);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes parsed key/value pair
|
|
/// </summary>
|
|
private static void ProcessKeyValuePair(IniSection CurrentSection, string Key, string SingleValue, ParseAction Action)
|
|
{
|
|
switch (Action)
|
|
{
|
|
case ParseAction.New:
|
|
{
|
|
// New/replace
|
|
IniValues? Value;
|
|
if (CurrentSection.TryGetValue(Key, out Value) == false)
|
|
{
|
|
Value = new IniValues();
|
|
CurrentSection.Add(Key, Value);
|
|
}
|
|
Value.Clear();
|
|
Value.Add(SingleValue);
|
|
}
|
|
break;
|
|
case ParseAction.Add:
|
|
{
|
|
IniValues? Value;
|
|
if (CurrentSection.TryGetValue(Key, out Value) == false)
|
|
{
|
|
Value = new IniValues();
|
|
CurrentSection.Add(Key, Value);
|
|
}
|
|
Value.Add(SingleValue);
|
|
}
|
|
break;
|
|
case ParseAction.Remove:
|
|
{
|
|
IniValues? Value;
|
|
if (CurrentSection.TryGetValue(Key, out Value))
|
|
{
|
|
int ExistingIndex = Value.FindIndex(X => (String.Equals(SingleValue, X, StringComparison.CurrentCultureIgnoreCase)));
|
|
if (ExistingIndex >= 0)
|
|
{
|
|
Value.RemoveAt(ExistingIndex);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds an existing section or adds a new one
|
|
/// </summary>
|
|
private IniSection FindOrAddSection(string TrimmedLine, FileReference Filename, int LineIndex)
|
|
{
|
|
int SectionEndIndex = TrimmedLine.IndexOf(']');
|
|
if (SectionEndIndex < 1)
|
|
{
|
|
throw new IniParsingException("Mismatched brackets when parsing section name in {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine);
|
|
}
|
|
|
|
// comment could follow the ] but will just be trimmed out
|
|
string SectionName = TrimmedLine.Substring(1, SectionEndIndex - 1);
|
|
if (String.IsNullOrEmpty(SectionName))
|
|
{
|
|
throw new IniParsingException("Empty section name when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine);
|
|
}
|
|
{
|
|
IniSection? CurrentSection;
|
|
if (Sections.TryGetValue(SectionName, out CurrentSection) == false)
|
|
{
|
|
CurrentSection = new IniSection();
|
|
Sections.Add(SectionName, CurrentSection);
|
|
}
|
|
return CurrentSection;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a list of INI filenames for the engine
|
|
/// </summary>
|
|
private static IEnumerable<FileReference> EnumerateEngineIniFileNames(DirectoryReference EngineDirectory, string BaseIniName)
|
|
{
|
|
// Engine/Config/Base.ini (included in every ini type, required)
|
|
yield return FileReference.Combine(EngineDirectory, "Config", "Base.ini");
|
|
|
|
// Engine/Config/Base* ini
|
|
yield return FileReference.Combine(EngineDirectory, "Config", "Base" + BaseIniName + ".ini");
|
|
|
|
// Engine/Config/NotForLicensees/Base* ini
|
|
yield return FileReference.Combine(EngineDirectory, "Restricted", "NotForLicensees", "Config", "Base" + BaseIniName + ".ini");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a list of INI filenames for the given project
|
|
/// </summary>
|
|
private static IEnumerable<FileReference> EnumerateCrossPlatformIniFileNames(DirectoryReference? ProjectDirectory, DirectoryReference EngineDirectory, UnrealTargetPlatform Platform, string BaseIniName, bool SkipEngine)
|
|
{
|
|
if (!SkipEngine)
|
|
{
|
|
// Engine/Config/Base.ini (included in every ini type, required)
|
|
yield return FileReference.Combine(EngineDirectory, "Config", "Base.ini");
|
|
|
|
// Engine/Config/Base* ini
|
|
yield return FileReference.Combine(EngineDirectory, "Config", "Base" + BaseIniName + ".ini");
|
|
|
|
// Engine/Config/NotForLicensees/Base* ini
|
|
yield return FileReference.Combine(EngineDirectory, "Restricted", "NotForLicensees", "Config", "Base" + BaseIniName + ".ini");
|
|
|
|
// Engine/Config/NoRedist/Base* ini
|
|
yield return FileReference.Combine(EngineDirectory, "Restricted", "NoRedist", "Config", "Base" + BaseIniName + ".ini");
|
|
}
|
|
|
|
if (ProjectDirectory != null)
|
|
{
|
|
// Game/Config/Default* ini
|
|
yield return FileReference.Combine(ProjectDirectory, "Config", "Default" + BaseIniName + ".ini");
|
|
|
|
// Game/Config/NotForLicensees/Default* ini
|
|
yield return FileReference.Combine(ProjectDirectory, "Restricted", "NotForLicensees", "Config", "Default" + BaseIniName + ".ini");
|
|
|
|
// Game/Config/NoRedist/Default* ini
|
|
yield return FileReference.Combine(ProjectDirectory, "Restricted", "NoRedist", "Config", "Default" + BaseIniName + ".ini");
|
|
}
|
|
|
|
string PlatformName = GetIniPlatformName(Platform);
|
|
// Engine/Config/Platform/Platform* ini
|
|
yield return FileReference.Combine(EngineDirectory, "Config", PlatformName, PlatformName + BaseIniName + ".ini");
|
|
|
|
if (ProjectDirectory != null)
|
|
{
|
|
// Game/Config/Platform/Platform* ini
|
|
yield return FileReference.Combine(ProjectDirectory, "Config", PlatformName, PlatformName + BaseIniName + ".ini");
|
|
}
|
|
|
|
DirectoryReference? UserSettingsFolder = Unreal.UserSettingDirectory; // Match FPlatformProcess::UserSettingsDir()
|
|
DirectoryReference? PersonalFolder = Unreal.UserDirectory; // Match FPlatformProcess::UserDir()
|
|
|
|
if (UserSettingsFolder != null)
|
|
{
|
|
// <AppData>/Unreal/EngineConfig/User* ini
|
|
yield return FileReference.Combine(UserSettingsFolder, "Unreal Engine", "Engine", "Config", "User" + BaseIniName + ".ini");
|
|
}
|
|
|
|
if (PersonalFolder != null)
|
|
{
|
|
// <Documents>/Unreal/EngineConfig/User* ini
|
|
yield return FileReference.Combine(PersonalFolder, "Unreal Engine", "Engine", "Config", "User" + BaseIniName + ".ini");
|
|
}
|
|
|
|
// Game/Config/User* ini
|
|
if (ProjectDirectory != null)
|
|
{
|
|
yield return FileReference.Combine(ProjectDirectory, "Config", "User" + BaseIniName + ".ini");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the platform name to use as part of platform-specific config files
|
|
/// </summary>
|
|
private static string GetIniPlatformName(UnrealTargetPlatform TargetPlatform)
|
|
{
|
|
if (TargetPlatform == UnrealTargetPlatform.Win64)
|
|
{
|
|
return "Windows";
|
|
}
|
|
else
|
|
{
|
|
return TargetPlatform.ToString();
|
|
}
|
|
}
|
|
}
|
|
}
|