chr
2026-04-05 fe750b791d5b517cc4e9bc8e99a9a75139a0cfba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
using System;
using System.IO;
using System.Linq;
using System.Xml.Linq;
 
namespace OpenTap.Package
{
    /// <summary>
    /// The <see cref="PackageXmlPreprocessor"/> class can expand variables of the form $(VarName) -> VarNameValue
    /// in an XML document. It reads the current environment variables, and optionally document-local variables set in
    /// a <Variables/> element as a child of the root element. A variable will be expanded exactly once either if it is
    /// an XMLText element, or if it appears as text in an attribute.
    /// (e.g. <SomeElement Attr1="$(abc)">abc $(def) ghi</SomeElement> will expand $(abc) and $(def))
    /// A variable will only be expanded once. If $(abc) -> "$(def)", then the expansion of $(abc) will not be expanded.
    /// Additionally, 'Conditions' are supported. Conditions are attributes on elements. If the condition evaluates to
    /// false, the element containing the condition is removed from the document. If the condition is true, the
    /// condition itself is removed. A condition takes the form Condition="$(abc)" or Condition="$(abc) == $(def)" or
    /// Condition="$(abc) != $(def)". A condition is true if it has the value '1' or 'true', or if the comparison operator evaluates to true.
    /// </summary>
    internal class PackageXmlPreprocessor
    {
        /// <summary>
        /// Initializes a new instance of <see cref="PackageXmlPreprocessor"/> from an <see cref="XElement"/> object.
        /// </summary>
        /// <param name="root"></param>
        /// <param name="projectPath"></param>
        public PackageXmlPreprocessor(XElement root, string projectPath = null)
        {
            // Create a deep copy of the source element
            Root = new XElement(root);
            InitExpander(projectPath);
        }
 
        /// <summary>
        /// Initializes a new instance of <see cref="PackageXmlPreprocessor"/> from a file path.
        /// </summary>
        /// <param name="path"></param>
        /// <param name="projectPath"></param>
        public PackageXmlPreprocessor(string path, string projectPath = null)
        {
            if (File.Exists(path) == false) throw new FileNotFoundException($"The file '{path}' does not exist.");
            try
            {
                Root = XElement.Load(path, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
            }
            catch (Exception)
            {
                log.Debug($"Error loading XML Document from file '{path}'.");
                throw;
            }
            InitExpander(projectPath);
        }
 
        private readonly DefaultVariableExpander _defaultExpander = new DefaultVariableExpander();
        private void InitExpander(string projectPath = null)
        {
            // The project directory is required to compute the gitversion. If it is not set, GitVersion will not be computed.
            Expander.AddProvider(new GitVersionExpander(projectPath));
            Expander.AddProvider(_defaultExpander);
            Expander.AddProvider(new EnvironmentVariableExpander());
            Expander.AddProvider(new ConditionExpander());
 
 
            // Evaluate all '<PropertyGroup/> elements and remove them from the document
            var variables = Root.Elements(Root.GetDefaultNamespace().GetName("Variables")).ToArray();
 
            var variableExpander = new VariableExpander(Expander);
            Expander.AddProvider(variableExpander);
            variableExpander.InitVariables(variables);
 
            foreach (var propElem in variables)
            {
                // The property could have been removed due to a Condition.
                if (propElem.Parent != null)
                    propElem.Remove();
            }
        }
 
        private ElementExpander Expander { get; } = new ElementExpander();
 
        /// <summary>
        /// Evaluate all variables of the form $(VarName) -> VarNameValue in the <see cref="Root"/> document.
        /// Evaluate all Conditions of the form 'Condition="Some-Condition-Expression"'
        /// Removes the node which contains the condition if it evaluates to false. Otherwise it removes the condition itself.
        /// </summary>
        public XElement Evaluate()
        {
            ExpandNodeRecursive(Root);
 
            // OpenTAP only supports a single <Files/>, <Dependencies/> and <PackageActionExtensions/> element,
            // but with conditions it makes sense to specify these elements multiple times with different conditions.
            // Their children should be merged in a single parent element so the XML is still valid according to the schema.
            MergeDuplicateElements(Root);
            
            if (_defaultExpander.UndefinedVariables.Any())
                throw new Exception(
                    $"The following required variables are not defined: {string.Join(", ", _defaultExpander.UndefinedVariables)}");
 
            return Root;
        }
 
        static TraceSource log = Log.CreateSource(nameof(PackageXmlPreprocessor));
        XElement Root { get; }
 
        /// <summary>
        /// Merge all top-level elements in <see cref="XElement"/> elem by adding all of the children of duplicate elements to
        /// the first element
        /// </summary>
        /// <param name="elem"></param>
        static void MergeDuplicateElements(XElement elem)
        {
            var remaining = elem.Elements().GroupBy(e => e.Name.LocalName);
            foreach (var rem in remaining)
            {
                var main = rem.First();
                var rest = rem.Skip(1).ToArray();
 
                foreach (var dup in rest)
                {
                    foreach (var child in dup.Elements().ToArray())
                    {
                        main.Add(new XElement(child));
                    }
                    dup.Remove();
                }
            }
        }
 
        /// <summary>
        /// Recursively expand the children of ele until a terminal node is reached.
        /// </summary>
        /// <param name="ele"></param>
        void ExpandNodeRecursive(XElement ele)
        {
            Expander.ExpandElement(ele);
            foreach (var desc in ele.Elements().ToArray())
            {
                ExpandNodeRecursive(desc);
            }
        }
    }
}