// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml; using System.Xml.Linq; using System.Diagnostics; using System.IO; namespace UnrealBuildTool { /* AndroidPluginLanguage (APL) is a simple XML-based language for manipulating XML and returning * strings. It contains an 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. * * If you need to see the instructions executed in your plugin context add the following to * enable tracing: * * * * After this instuction all the nodes actually executed in your context will be written to the * log until you do a . You can also get a dump of all the variables in * your context with this command: * * * * 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(Architecture) = target architecture (armeabi-armv7, armeabi-armv8, x86, x86_64) * $S(PluginDir) = directory the XML file was loaded from * $S(EngineDir) = engine directory * $S(BuildDir) = project's Intermediate/Android/APK directory * $B(Distribution) = true if distribution build * * 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: * * * * * * * * with value creates an empty XML element with the tag set to value. * 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: * * * * * * Boolean variables may also be set to the result of applying operators: * * * * * * * * * * * Integer variables may use these arithmetic operations: * * * * * * * Strings are manipulated with the following: * * * * * * String length may be retrieved with: * * * * The index of a search string may be found in source with: * * * * The following shortcut string comparisons may also be used instead of using * and checking the result: * * * * * * Messages are written to the log with this node: * * * * Conditional execution uses the following form: * * * * * * * * * * * The and 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: * * * * * * * * * * * * Note the "isIntel" could also be done like this: * * * * * Two shortcut nodes are available for conditional execution: * * * * * * is the equivalent of: * * * * * * * * * and * * * * * * is the equivalent of: * * * * * * Execution may be stopped with: * * * * Loops may be created using these nodes: * * * * * * * * * The body will execute until the condition is false or a is hit. The * will restart execution of the loop if the condition is still true or exit. * * Note: outside a body will act the same as * * 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. * * * * * * * * * * * * * * * * It is possible to use variable replacement in generating the result variable * name as well. This makes the creation of arrays in loops possible: * * * * This may be retrieved using the following (value is treated as the variable * name): * * * * For boolean and integer types, you may use and . * * Nodes for inserting text into the section are as follows: * * body * * * * The first one will insert either text or nodes into the returned section * string. Please note you must use escaped characters for: * * < = < * > = > * & = & * * evaluates variables in value before insertion. If value contains * double quote ("), you must escape it with ". * * 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: * * * * Note you can also manipulate the actual $S(Output) directly, the above are more efficient: * * * * * XML manipulation uses the following nodes: * * * body * * * * * * instructions * * 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. * * and are updated with: * * * * * Any attributes in the above commands are copied to the element added to the manifest so you * can do the following, for example: * * * * Finally, these nodes allow copying of files useful for staging jar and so files: * * * * * 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 Intermediate/Android/APK 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: * * * * It is restricted to only removing files from the BuildDir. Here is example usage to remove * the Oculus Signature Files (osig) from the assets directory: * * * * The following sections are evaluated during the packaging or deploy stages: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Here is the complete list of supported nodes: * * * * => / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ public class AndroidPluginLanguage { /** The merged XML program to run */ private XDocument XDoc; /** Android namespace */ private XNamespace AndroidNameSpace; /** Trace flag to enable debugging */ static private bool bGlobalTrace = false; /** Project file reference */ private FileReference ProjectFile; static private XDocument XMLDummy = XDocument.Parse(""); private class APLContext { /** Variable state */ public Dictionary BoolVariables; public Dictionary IntVariables; public Dictionary StringVariables; public Dictionary ElementVariables; /** Local context trace */ public bool bTrace; public APLContext(string Architecture, string PluginDir) { BoolVariables = new Dictionary(); IntVariables = new Dictionary(); StringVariables = new Dictionary(); ElementVariables = new Dictionary(); StringVariables["Architecture"] = Architecture; StringVariables["PluginDir"] = PluginDir; bTrace = false; } } private APLContext GlobalContext; private Dictionary Contexts; private int ContextIndex; public AndroidPluginLanguage(FileReference InProjectFile, List XMLFiles, List Architectures) { ProjectFile = InProjectFile; Contexts = new Dictionary(); GlobalContext = new APLContext("", ""); ContextIndex = 0; AndroidNameSpace = "http://schemas.android.com/apk/res/android"; string PathPrefix = Path.GetFileName(Directory.GetCurrentDirectory()).Equals("Source") ? ".." : "Engine"; XDoc = XDocument.Parse(""); foreach (string Basename in XMLFiles) { string Filename = Path.Combine(PathPrefix, Basename.Replace("\\", "/")); Log.TraceInformation("\nAPL: {0}", Filename); if (File.Exists(Filename)) { string PluginDir = Path.GetDirectoryName(Filename); try { XDocument MergeDoc = XDocument.Load(Filename); MergeXML(MergeDoc, PluginDir, Architectures); } catch (Exception e) { Log.TraceError("\nAndroid Plugin file {0} parsing failed! {1}", Filename, e); } } else { Log.TraceError("\nAndroid Plugin file {0} missing!", Filename); Log.TraceInformation("\nCWD: {0}", Directory.GetCurrentDirectory()); } } } public bool GetTrace() { return bGlobalTrace; } public void SetTrace() { bGlobalTrace = true; } public void ClearTrace() { bGlobalTrace = false; } public bool MergeXML(XDocument MergeDoc, string PluginDir, List Architectures) { if (MergeDoc == null) { return false; } // create a context for each architecture ContextIndex++; foreach (string Architecture in Architectures) { APLContext Context = new APLContext(Architecture, PluginDir); Contexts[Architecture + "_" + ContextIndex] = Context; } // merge in the nodes foreach (var Element in MergeDoc.Root.Elements()) { var Parent = XDoc.Root.Element(Element.Name); if (Parent != null) { var Entry = new XElement("Context", new XAttribute("index", ContextIndex.ToString())); Entry.Add(Element.Elements()); Parent.Add(Entry); } else { var Entry = new XElement("Context", new XAttribute("index", ContextIndex.ToString())); Entry.Add(Element.Elements()); var Base = new XElement(Element.Name); Base.Add(Entry); XDoc.Root.Add(Base); } } return true; } public void SaveXML(string Filename) { if (XDoc != null) { XDoc.Save(Filename); } } private string DumpContext(APLContext Context) { StringBuilder Text = new StringBuilder(); foreach (var Variable in Context.BoolVariables) { Text.AppendLine(string.Format("\tbool {0} = {1}", Variable.Key, Variable.Value.ToString().ToLower())); } foreach (var Variable in Context.IntVariables) { Text.AppendLine(string.Format("\tint {0} = {1}", Variable.Key, Variable.Value)); } foreach (var Variable in Context.StringVariables) { Text.AppendLine(string.Format("\tstring {0} = {1}", Variable.Key, Variable.Value)); } foreach (var 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 (var Context in Contexts) { Result += "Context " + Context.Key + ": " + Context.Value.StringVariables["PluginDir"] + "\n" + DumpContext(Context.Value); } return Result; } private bool GetCondition(APLContext Context, XElement Node, string Condition, out bool Result) { Result = false; if (!Context.BoolVariables.TryGetValue(Condition, out Result)) { if (!GlobalContext.BoolVariables.TryGetValue(Condition, out Result)) { Log.TraceWarning("\nMissing condition '{0}' in '{1}' (skipping instruction)", Condition, TraceNodeString(Node)); return false; } } return true; } private string ExpandVariables(APLContext 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(XElement Node) { string Result = Node.Name.ToString(); foreach (var Attrib in Node.Attributes()) { Result += " " + Attrib.ToString(); } 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(string Input, XElement Node) { int Result = 0; if (!int.TryParse(Input, out Result)) { Log.TraceWarning("\nInvalid integer '{0}' in '{1}' (defaulting to 0)", Input, TraceNodeString(Node)); } return Result; } private string GetAttribute(APLContext Context, XElement Node, string AttributeName, bool bExpand = true, bool bRequired = true, string Fallback = null) { XAttribute Attribute = Node.Attribute(AttributeName); if (Attribute == null) { if (bRequired) { Log.TraceWarning("\nMissing attribute '{0}' in '{1}' (skipping instruction)", AttributeName, TraceNodeString(Node)); } return Fallback; } string Result = Attribute.Value; return bExpand ? ExpandVariables(Context, Result) : Result; } private string GetAttributeWithNamespace(APLContext 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) { Log.TraceWarning("\nMissing attribute '{0}' in '{1}' (skipping instruction)", AttributeName, TraceNodeString(Node)); } return Fallback; } string Result = Attribute.Value; return bExpand ? ExpandVariables(Context, Result) : Result; } static private Dictionary ConfigCache = null; private ConfigCacheIni GetConfigCacheIni(string baseIniName) { if (ConfigCache == null) { ConfigCache = new Dictionary(); } ConfigCacheIni config = null; if (!ConfigCache.TryGetValue(baseIniName, out config)) { config = ConfigCacheIni.CreateConfigCacheIni(UnrealTargetPlatform.Android, "Engine", DirectoryReference.FromFile(ProjectFile)); ConfigCache.Add(baseIniName, config); } return config; } private static void CopyFileDirectory(string SourceDir, string DestDir) { 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 (File.Exists(DestFilename)) { 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 FileInfo DestFileInfo = new FileInfo(DestFilename); DestFileInfo.Attributes = DestFileInfo.Attributes & ~FileAttributes.ReadOnly; } } private static void DeleteFiles(string Filespec) { string BaseDir = Path.GetDirectoryName(Filespec); string Mask = Path.GetFileName(Filespec); if (!Directory.Exists(BaseDir)) { return; } string[] Files = Directory.GetFiles(BaseDir, Mask, SearchOption.TopDirectoryOnly); foreach (string Filename in Files) { File.Delete(Filename); Log.TraceInformation("\nDeleted file {0}", Filename); } } private void AddAttribute(XElement Element, string Name, string Value) { XAttribute Attribute; int Index = Name.IndexOf(":"); if (Index >= 0) { Name = Name.Substring(Index + 1); Attribute = Element.Attribute(AndroidNameSpace + Name); } else { Attribute = Element.Attribute(Name); } if (Attribute != null) { Attribute.SetValue(Value); } else { if (Index >= 0) { Element.Add(new XAttribute(AndroidNameSpace + Name, Value)); } else { Element.Add(new XAttribute(Name, Value)); } } } private void RemoveAttribute(XElement Element, string Name) { XAttribute Attribute; int Index = Name.IndexOf(":"); if (Index >= 0) { Name = Name.Substring(Index + 1); Attribute = Element.Attribute(AndroidNameSpace + Name); } else { Attribute = Element.Attribute(Name); } if (Attribute != null) { Attribute.Remove(); } } private void AddElements(XElement Target, XElement Source) { if (Source.HasElements) { foreach (var 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 var ExecutionStack = new Stack(); var StartNode = XDoc.Root.Element(NodeName); if (StartNode != null) { foreach (var Instruction in StartNode.Elements().Reverse()) { ExecutionStack.Push(Instruction); } } if (ExecutionStack.Count == 0) { return Input; } var ContextStack = new Stack(); APLContext CurrentContext = GlobalContext; // update Output in global context GlobalContext.StringVariables["Output"] = Input; var ElementStack = new Stack(); XElement CurrentElement = XMLWork.Elements().First(); // run the instructions while (ExecutionStack.Count > 0) { var Node = ExecutionStack.Pop(); if (bGlobalTrace || CurrentContext.bTrace) { Log.TraceInformation("Execute: '{0}'", TraceNodeString(Node)); } switch (Node.Name.ToString()) { case "trace": { string Enable = GetAttribute(CurrentContext, Node, "enable"); if (Enable != null) { CurrentContext.bTrace = StringToBool(Enable); if (!bGlobalTrace && CurrentContext.bTrace) { Log.TraceInformation("Context: '{0}' using Architecture='{1}', NodeName='{2}', Input='{3}'", CurrentContext.StringVariables["PluginDir"], Architecture, NodeName, Input); } } } break; case "dumpvars": { if (!bGlobalTrace && !CurrentContext.bTrace) { Log.TraceInformation("Context: '{0}' using Architecture='{1}', NodeName='{2}', Input='{3}'", CurrentContext.StringVariables["PluginDir"], Architecture, NodeName, Input); } Log.TraceInformation("Variables:\n{0}", DumpContext(CurrentContext)); } break; case "Context": { ContextStack.Push(CurrentContext); string index = GetAttribute(CurrentContext, Node, "index"); CurrentContext = Contexts[Architecture + "_" + index]; ExecutionStack.Push(new XElement("PopContext")); foreach (var instruction in Node.Elements().Reverse()) { ExecutionStack.Push(instruction); } if (bGlobalTrace || CurrentContext.bTrace) { Log.TraceInformation("Context: '{0}' using Architecture='{1}', NodeName='{2}', Input='{3}'", 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 (var instruction in Node.Elements().Reverse()) { ExecutionStack.Push(instruction); } } } break; case "isDistribution": { bool Result = false; if (GetCondition(CurrentContext, Node, "Distribution", out Result)) { if (Result) { foreach (var Instruction in Node.Elements().Reverse()) { ExecutionStack.Push(Instruction); } } } } break; case "if": { bool Result; if (GetCondition(CurrentContext, Node, GetAttribute(CurrentContext, Node, "condition"), out Result)) { var ResultNode = Node.Element(Result ? "true" : "false"); if (ResultNode != null) { foreach (var 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) { var ResultNode = Node.Elements(); if (ResultNode != null) { ExecutionStack.Push(Node); foreach (var 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) { Log.TraceInformation("{0}", Text); } } break; case "loopElements": { string Tag = GetAttribute(CurrentContext, Node, "tag"); ElementStack.Push(CurrentElement); ExecutionStack.Push(new XElement("PopElement")); var WorkList = (Tag == "$") ? CurrentElement.Elements().Reverse() : CurrentElement.Descendants(Tag).Reverse(); foreach (var WorkNode in WorkList) { foreach (var 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)) { Log.TraceWarning("\nMissing element variable '{0}' in '{1}' (skipping instruction)", Tag, TraceNodeString(Node)); continue; } } } AddAttribute(Target, Name, Value); } else { if (CurrentElement.Name.ToString().Equals(Tag)) { AddAttribute(CurrentElement, Name, Value); } foreach (var 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)) { Log.TraceWarning("\nMissing element variable '{0}' in '{1}' (skipping instruction)", Tag, TraceNodeString(Node)); continue; } } } RemoveAttribute(Target, Name); } else { if (CurrentElement.Name.ToString().Equals(Tag)) { RemoveAttribute(CurrentElement, Name); } foreach (var WorkNode in CurrentElement.Descendants(Tag)) { RemoveAttribute(WorkNode, Name); } } } } break; case "addPermission": { string Name = GetAttributeWithNamespace(CurrentContext, Node, AndroidNameSpace, "name"); if (Name != null) { // make sure it isn't already added bool bFound = false; foreach (var Element in XMLWork.Descendants("uses-permission")) { XAttribute Attribute = Element.Attribute(AndroidNameSpace + "name"); if (Attribute != null) { if (Attribute.Value == Name) { bFound = true; break; } } } // add it if not found if (!bFound) { XMLWork.Element("manifest").Add(new XElement("uses-permission", Node.Attributes())); } } } break; case "removePermission": { string Name = GetAttributeWithNamespace(CurrentContext, Node, AndroidNameSpace, "name"); if (Name != null) { foreach (var Element in XMLWork.Descendants("uses-permission")) { XAttribute Attribute = Element.Attribute(AndroidNameSpace + "name"); if (Attribute != null) { if (Attribute.Value == Name) { Element.Remove(); break; } } } } } break; case "addFeature": { string Name = GetAttributeWithNamespace(CurrentContext, Node, AndroidNameSpace, "name"); if (Name != null) { // make sure it isn't already added bool bFound = false; foreach (var Element in XMLWork.Descendants("uses-feature")) { XAttribute Attribute = Element.Attribute(AndroidNameSpace + "name"); if (Attribute != null) { if (Attribute.Value == Name) { bFound = true; break; } } } // add it if not found if (!bFound) { XMLWork.Element("manifest").Add(new XElement("uses-feature", Node.Attributes())); } } } break; case "removeFeature": { string Name = GetAttributeWithNamespace(CurrentContext, Node, AndroidNameSpace, "name"); if (Name != null) { foreach (var Element in XMLWork.Descendants("uses-feature")) { XAttribute Attribute = Element.Attribute(AndroidNameSpace + "name"); if (Attribute != null) { if (Attribute.Value == Name) { Element.Remove(); break; } } } } } break; case "removeElement": { string Tag = GetAttribute(CurrentContext, Node, "tag"); if (Tag != null) { if (Tag == "$") { XElement Parent = CurrentElement.Parent; CurrentElement.Remove(); CurrentElement = Parent; } else { // use a list since Remove() may modify it foreach (var Element in XMLWork.Descendants(Tag).ToList()) { Element.Remove(); } } } } break; case "addElement": { string Tag = GetAttribute(CurrentContext, Node, "tag"); string Name = GetAttribute(CurrentContext, Node, "name"); if (Tag != null && Name != null) { XElement Element; if (!CurrentContext.ElementVariables.TryGetValue(Name, out Element)) { if (!GlobalContext.ElementVariables.TryGetValue(Name, out Element)) { Log.TraceWarning("\nMissing element variable '{0}' in '{1}' (skipping instruction)", Tag, TraceNodeString(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)) { Log.TraceWarning("\nMissing element variable '{0}' in '{1}' (skipping instruction)", Tag, TraceNodeString(Node)); continue; } } } Target.Add(new XElement(Element)); } else { if (CurrentElement.Name.ToString().Equals(Tag)) { CurrentElement.Add(new XElement(Element)); } foreach (var WorkNode in CurrentElement.Descendants(Tag)) { WorkNode.Add(new XElement(Element)); } } } } break; case "addElements": { string Tag = GetAttribute(CurrentContext, Node, "tag"); 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)) { Log.TraceWarning("\nMissing element variable '{0}' in '{1}' (skipping instruction)", Tag, TraceNodeString(Node)); continue; } } } AddElements(Target, Node); } else { if (CurrentElement.Name.ToString().Equals(Tag)) { AddElements(CurrentElement, Node); } foreach (var WorkNode in CurrentElement.Descendants(Tag)) { AddElements(WorkNode, Node); } } } } break; case "insert": { if (Node.HasElements) { foreach (var Element in Node.Elements()) { string Value = Element.ToString().Replace(" xmlns:android=\"http://schemas.android.com/apk/res/android\" ", ""); 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 "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"); if (Src != null && Dst != null) { if (File.Exists(Src)) { // check to see if newer than last time we copied bool bFileExists = File.Exists(Dst); TimeSpan Diff = File.GetLastWriteTimeUtc(Dst) - File.GetLastWriteTimeUtc(Src); if (!bFileExists || Diff.TotalSeconds < -1 || Diff.TotalSeconds > 1) { if (bFileExists) { File.Delete(Dst); } Directory.CreateDirectory(Path.GetDirectoryName(Dst)); File.Copy(Src, Dst, true); Log.TraceInformation("\nFile {0} copied to {1}", Src, Dst); } } } } break; case "copyDir": { string Src = GetAttribute(CurrentContext, Node, "src"); string Dst = GetAttribute(CurrentContext, Node, "dst"); if (Src != null && Dst != null) { CopyFileDirectory(Src, Dst); Log.TraceInformation("\nDirectory {0} copied to {1}", Src, Dst); } } break; case "deleteFiles": { string Filespec = GetAttribute(CurrentContext, Node, "filespec"); if (Filespec != null) { if (Filespec.Contains(":") || Filespec.Contains("..")) { Log.TraceInformation("\nFilespec {0} 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)); } } } 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(\"" + 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 "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 ConfigIni = GetConfigCacheIni(Ini); if (ConfigIni != null) { ConfigIni.GetBool(Section, Property, out Value); } 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(Arg1, Node) < StringToInt(Arg2, Node)); } } 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(Arg1, Node) <= StringToInt(Arg2, Node)); } } 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(Arg1, Node) > StringToInt(Arg2, Node)); } } 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(Arg1, Node) >= StringToInt(Arg2, Node)); } } 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(Value, Node); } } 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(Value, Node); } } 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 Value = StringToInt(DefaultVal, Node); ConfigCacheIni ConfigIni = GetConfigCacheIni(Ini); if (ConfigIni != null) { ConfigIni.GetInt32(Section, Property, out Value); } 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(Arg1, Node) + StringToInt(Arg2, Node); } } 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(Arg1, Node) - StringToInt(Arg2, Node); } } 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(Arg1, Node) * StringToInt(Arg2, Node); } } 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(Arg2, Node); if (Denominator == 0) { CurrentContext.IntVariables[Result] = StringToInt(Arg1, Node); } else { CurrentContext.IntVariables[Result] = StringToInt(Arg1, Node) / 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 "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)) { Log.TraceWarning("\nMissing element variable '{0}' in '{1}' (skipping instruction)", Tag, TraceNodeString(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)) { Log.TraceWarning("\nMissing element variable '{0}' in '{1}' (skipping instruction)", Tag, TraceNodeString(Node)); continue; } } } } XAttribute Attribute; int Index = Name.IndexOf(":"); if (Index >= 0) { Name = Name.Substring(Index + 1); Attribute = Element.Attribute(AndroidNameSpace + Name); } else { Attribute = Element.Attribute(Name); } CurrentContext.StringVariables[Result] = (Attribute != null) ? Attribute.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 ConfigIni = GetConfigCacheIni(Ini); if (ConfigIni != null) { ConfigIni.GetString(Section, Property, out Value); } 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(Start, Node); int Count = StringToInt(Length, Node); 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 "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 "setElement": { string Result = GetAttribute(CurrentContext, Node, "result"); string Value = GetAttribute(CurrentContext, Node, "value", true, false); string Parse = GetAttribute(CurrentContext, Node, "xml", true, false); if (Result != null) { if (Value != null) { CurrentContext.ElementVariables[Result] = new XElement(Value); } else if (Parse != null) { try { CurrentContext.ElementVariables[Result] = XElement.Parse(Parse); } catch (Exception e) { Log.TraceError("\nXML parsing {0} failed! {1} (skipping instruction)", Parse, e); } } } } break; default: Log.TraceWarning("\nUnknown command: {0}", Node.Name); break; } } return GlobalContext.StringVariables["Output"]; } public void Init(List Architectures, bool bDistribution, string EngineDirectory, string BuildDirectory) { GlobalContext.BoolVariables["Distribution"] = bDistribution; GlobalContext.StringVariables["EngineDir"] = EngineDirectory; GlobalContext.StringVariables["BuildDir"] = BuildDirectory; foreach (string Arch in Architectures) { Log.TraceInformation("APL Init: {0}", Arch); ProcessPluginNode(Arch, "init", ""); } if (bGlobalTrace) { Log.TraceInformation("\nVariables:\n{0}", DumpVariables()); } } } }