xref: /qemu/scripts/xml-preprocess.py (revision 6170d09c)
1#!/usr/bin/env python3
2#
3# Copyright (c) 2017-2019 Tony Su
4# Copyright (c) 2023 Red Hat, Inc.
5#
6# SPDX-License-Identifier: MIT
7#
8# Adapted from https://github.com/peitaosu/XML-Preprocessor
9#
10"""This is a XML Preprocessor which can be used to process your XML file before
11you use it, to process conditional statements, variables, iteration
12statements, error/warning, execute command, etc.
13
14## XML Schema
15
16### Include Files
17```
18<?include path/to/file ?>
19```
20
21### Variables
22```
23$(env.EnvironmentVariable)
24
25$(sys.SystemVariable)
26
27$(var.CustomVariable)
28```
29
30### Conditional Statements
31```
32<?if ?>
33
34<?ifdef ?>
35
36<?ifndef ?>
37
38<?else?>
39
40<?elseif ?>
41
42<?endif?>
43```
44
45### Iteration Statements
46```
47<?foreach VARNAME in 1;2;3?>
48    $(var.VARNAME)
49<?endforeach?>
50```
51
52### Errors and Warnings
53```
54<?error "This is error message!" ?>
55
56<?warning "This is warning message!" ?>
57```
58
59### Commands
60```
61<? cmd "echo hello world" ?>
62```
63"""
64
65import os
66import platform
67import re
68import subprocess
69import sys
70from typing import Optional
71from xml.dom import minidom
72
73
74class Preprocessor():
75    """This class holds the XML preprocessing state"""
76
77    def __init__(self):
78        self.sys_vars = {
79            "ARCH": platform.architecture()[0],
80            "SOURCE": os.path.abspath(__file__),
81            "CURRENT": os.getcwd(),
82        }
83        self.cus_vars = {}
84
85    def _pp_include(self, xml_str: str) -> str:
86        include_regex = r"(<\?include([\w\s\\/.:_-]+)\s*\?>)"
87        matches = re.findall(include_regex, xml_str)
88        for group_inc, group_xml in matches:
89            inc_file_path = group_xml.strip()
90            with open(inc_file_path, "r", encoding="utf-8") as inc_file:
91                inc_file_content = inc_file.read()
92                xml_str = xml_str.replace(group_inc, inc_file_content)
93        return xml_str
94
95    def _pp_env_var(self, xml_str: str) -> str:
96        envvar_regex = r"(\$\(env\.(\w+)\))"
97        matches = re.findall(envvar_regex, xml_str)
98        for group_env, group_var in matches:
99            xml_str = xml_str.replace(group_env, os.environ[group_var])
100        return xml_str
101
102    def _pp_sys_var(self, xml_str: str) -> str:
103        sysvar_regex = r"(\$\(sys\.(\w+)\))"
104        matches = re.findall(sysvar_regex, xml_str)
105        for group_sys, group_var in matches:
106            xml_str = xml_str.replace(group_sys, self.sys_vars[group_var])
107        return xml_str
108
109    def _pp_cus_var(self, xml_str: str) -> str:
110        define_regex = r"(<\?define\s*(\w+)\s*=\s*([\w\s\"]+)\s*\?>)"
111        matches = re.findall(define_regex, xml_str)
112        for group_def, group_name, group_var in matches:
113            group_name = group_name.strip()
114            group_var = group_var.strip().strip("\"")
115            self.cus_vars[group_name] = group_var
116            xml_str = xml_str.replace(group_def, "")
117        cusvar_regex = r"(\$\(var\.(\w+)\))"
118        matches = re.findall(cusvar_regex, xml_str)
119        for group_cus, group_var in matches:
120            xml_str = xml_str.replace(
121                group_cus,
122                self.cus_vars.get(group_var, "")
123            )
124        return xml_str
125
126    def _pp_foreach(self, xml_str: str) -> str:
127        foreach_regex = r"(<\?foreach\s+(\w+)\s+in\s+([\w;]+)\s*\?>(.*)<\?endforeach\?>)"
128        matches = re.findall(foreach_regex, xml_str)
129        for group_for, group_name, group_vars, group_text in matches:
130            group_texts = ""
131            for var in group_vars.split(";"):
132                self.cus_vars[group_name] = var
133                group_texts += self._pp_cus_var(group_text)
134            xml_str = xml_str.replace(group_for, group_texts)
135        return xml_str
136
137    def _pp_error_warning(self, xml_str: str) -> str:
138        error_regex = r"<\?error\s*\"([^\"]+)\"\s*\?>"
139        matches = re.findall(error_regex, xml_str)
140        for group_var in matches:
141            raise RuntimeError("[Error]: " + group_var)
142        warning_regex = r"(<\?warning\s*\"([^\"]+)\"\s*\?>)"
143        matches = re.findall(warning_regex, xml_str)
144        for group_wrn, group_var in matches:
145            print("[Warning]: " + group_var)
146            xml_str = xml_str.replace(group_wrn, "")
147        return xml_str
148
149    def _pp_if_eval(self, xml_str: str) -> str:
150        ifelif_regex = (
151            r"(<\?(if|elseif)\s*([^\"\s=<>!]+)\s*([!=<>]+)\s*\"*([^\"=<>!]+)\"*\s*\?>)"
152        )
153        matches = re.findall(ifelif_regex, xml_str)
154        for ifelif, tag, left, operator, right in matches:
155            if "<" in operator or ">" in operator:
156                result = eval(f"{left} {operator} {right}")
157            else:
158                result = eval(f'"{left}" {operator} "{right}"')
159            xml_str = xml_str.replace(ifelif, f"<?{tag} {result}?>")
160        return xml_str
161
162    def _pp_ifdef_ifndef(self, xml_str: str) -> str:
163        ifndef_regex = r"(<\?(ifdef|ifndef)\s*([\w]+)\s*\?>)"
164        matches = re.findall(ifndef_regex, xml_str)
165        for group_ifndef, group_tag, group_var in matches:
166            if group_tag == "ifdef":
167                result = group_var in self.cus_vars
168            else:
169                result = group_var not in self.cus_vars
170            xml_str = xml_str.replace(group_ifndef, f"<?if {result}?>")
171        return xml_str
172
173    def _pp_if_elseif(self, xml_str: str) -> str:
174        if_elif_else_regex = (
175            r"(<\?if\s(True|False)\?>"
176            r"(.*?)"
177            r"<\?elseif\s(True|False)\?>"
178            r"(.*?)"
179            r"<\?else\?>"
180            r"(.*?)"
181            r"<\?endif\?>)"
182        )
183        if_else_regex = (
184            r"(<\?if\s(True|False)\?>"
185            r"(.*?)"
186            r"<\?else\?>"
187            r"(.*?)"
188            r"<\?endif\?>)"
189        )
190        if_regex = r"(<\?if\s(True|False)\?>(.*?)<\?endif\?>)"
191        matches = re.findall(if_elif_else_regex, xml_str, re.DOTALL)
192        for (group_full, group_if, group_if_elif, group_elif,
193             group_elif_else, group_else) in matches:
194            result = ""
195            if group_if == "True":
196                result = group_if_elif
197            elif group_elif == "True":
198                result = group_elif_else
199            else:
200                result = group_else
201            xml_str = xml_str.replace(group_full, result)
202        matches = re.findall(if_else_regex, xml_str, re.DOTALL)
203        for group_full, group_if, group_if_else, group_else in matches:
204            result = ""
205            if group_if == "True":
206                result = group_if_else
207            else:
208                result = group_else
209            xml_str = xml_str.replace(group_full, result)
210        matches = re.findall(if_regex, xml_str, re.DOTALL)
211        for group_full, group_if, group_text in matches:
212            result = ""
213            if group_if == "True":
214                result = group_text
215            xml_str = xml_str.replace(group_full, result)
216        return xml_str
217
218    def _pp_command(self, xml_str: str) -> str:
219        cmd_regex = r"(<\?cmd\s*\"([^\"]+)\"\s*\?>)"
220        matches = re.findall(cmd_regex, xml_str)
221        for group_cmd, group_exec in matches:
222            output = subprocess.check_output(
223                group_exec, shell=True,
224                text=True, stderr=subprocess.STDOUT
225            )
226            xml_str = xml_str.replace(group_cmd, output)
227        return xml_str
228
229    def _pp_blanks(self, xml_str: str) -> str:
230        right_blank_regex = r">[\n\s\t\r]*"
231        left_blank_regex = r"[\n\s\t\r]*<"
232        xml_str = re.sub(right_blank_regex, ">", xml_str)
233        xml_str = re.sub(left_blank_regex, "<", xml_str)
234        return xml_str
235
236    def preprocess(self, xml_str: str) -> str:
237        fns = [
238            self._pp_blanks,
239            self._pp_include,
240            self._pp_foreach,
241            self._pp_env_var,
242            self._pp_sys_var,
243            self._pp_cus_var,
244            self._pp_if_eval,
245            self._pp_ifdef_ifndef,
246            self._pp_if_elseif,
247            self._pp_command,
248            self._pp_error_warning,
249        ]
250
251        while True:
252            changed = False
253            for func in fns:
254                out_xml = func(xml_str)
255                if not changed and out_xml != xml_str:
256                    changed = True
257                xml_str = out_xml
258            if not changed:
259                break
260
261        return xml_str
262
263
264def preprocess_xml(path: str) -> str:
265    with open(path, "r", encoding="utf-8") as original_file:
266        input_xml = original_file.read()
267
268        proc = Preprocessor()
269        return proc.preprocess(input_xml)
270
271
272def save_xml(xml_str: str, path: Optional[str]):
273    xml = minidom.parseString(xml_str)
274    with open(path, "w", encoding="utf-8") if path else sys.stdout as output_file:
275        output_file.write(xml.toprettyxml())
276
277
278def main():
279    if len(sys.argv) < 2:
280        print("Usage: xml-preprocessor input.xml [output.xml]")
281        sys.exit(1)
282
283    output_file = None
284    if len(sys.argv) == 3:
285        output_file = sys.argv[2]
286
287    input_file = sys.argv[1]
288    output_xml = preprocess_xml(input_file)
289    save_xml(output_xml, output_file)
290
291
292if __name__ == "__main__":
293    main()
294