resolve cyclic dependency with zstd
[platform/upstream/cmake.git] / Source / cmConvertMSBuildXMLToJSON.py
1 # Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2 # file Copyright.txt or https://cmake.org/licensing for details.
3
4 import argparse
5 import codecs
6 import copy
7 import logging
8 import json
9 import os
10
11 from collections import OrderedDict
12 from xml.dom.minidom import parse, parseString, Element
13
14
15 class VSFlags:
16     """Flags corresponding to cmIDEFlagTable."""
17     UserValue = "UserValue"  # (1 << 0)
18     UserIgnored = "UserIgnored"  # (1 << 1)
19     UserRequired = "UserRequired"  # (1 << 2)
20     Continue = "Continue"  #(1 << 3)
21     SemicolonAppendable = "SemicolonAppendable"  # (1 << 4)
22     UserFollowing = "UserFollowing"  # (1 << 5)
23     CaseInsensitive = "CaseInsensitive"  # (1 << 6)
24     UserValueIgnored = [UserValue, UserIgnored]
25     UserValueRequired = [UserValue, UserRequired]
26
27
28 def vsflags(*args):
29     """Combines the flags."""
30     values = []
31
32     for arg in args:
33         __append_list(values, arg)
34
35     return values
36
37
38 def read_msbuild_xml(path, values=None):
39     """Reads the MS Build XML file at the path and returns its contents.
40
41     Keyword arguments:
42     values -- The map to append the contents to (default {})
43     """
44     if values is None:
45         values = {}
46
47     # Attempt to read the file contents
48     try:
49         document = parse(path)
50     except Exception as e:
51         logging.exception('Could not read MS Build XML file at %s', path)
52         return values
53
54     # Convert the XML to JSON format
55     logging.info('Processing MS Build XML file at %s', path)
56
57     # Get the rule node
58     rule = document.getElementsByTagName('Rule')[0]
59
60     rule_name = rule.attributes['Name'].value
61
62     logging.info('Found rules for %s', rule_name)
63
64     # Proprocess Argument values
65     __preprocess_arguments(rule)
66
67     # Get all the values
68     converted_values = []
69     __convert(rule, 'EnumProperty', converted_values, __convert_enum)
70     __convert(rule, 'BoolProperty', converted_values, __convert_bool)
71     __convert(rule, 'StringListProperty', converted_values,
72               __convert_string_list)
73     __convert(rule, 'StringProperty', converted_values, __convert_string)
74     __convert(rule, 'IntProperty', converted_values, __convert_string)
75
76     values[rule_name] = converted_values
77
78     return values
79
80
81 def read_msbuild_json(path, values=None):
82     """Reads the MS Build JSON file at the path and returns its contents.
83
84     Keyword arguments:
85     values -- The list to append the contents to (default [])
86     """
87     if values is None:
88         values = []
89
90     if not os.path.exists(path):
91         logging.info('Could not find MS Build JSON file at %s', path)
92         return values
93
94     try:
95         values.extend(__read_json_file(path))
96     except Exception as e:
97         logging.exception('Could not read MS Build JSON file at %s', path)
98         return values
99
100     logging.info('Processing MS Build JSON file at %s', path)
101
102     return values
103
104 def main():
105     """Script entrypoint."""
106     # Parse the arguments
107     parser = argparse.ArgumentParser(
108         description='Convert MSBuild XML to JSON format')
109
110     parser.add_argument(
111         '-t', '--toolchain', help='The name of the toolchain', required=True)
112     parser.add_argument(
113         '-o', '--output', help='The output directory', default='')
114     parser.add_argument(
115         '-r',
116         '--overwrite',
117         help='Whether previously output should be overwritten',
118         dest='overwrite',
119         action='store_true')
120     parser.set_defaults(overwrite=False)
121     parser.add_argument(
122         '-d',
123         '--debug',
124         help="Debug tool output",
125         action="store_const",
126         dest="loglevel",
127         const=logging.DEBUG,
128         default=logging.WARNING)
129     parser.add_argument(
130         '-v',
131         '--verbose',
132         help="Verbose output",
133         action="store_const",
134         dest="loglevel",
135         const=logging.INFO)
136     parser.add_argument('input', help='The input files', nargs='+')
137
138     args = parser.parse_args()
139
140     toolchain = args.toolchain
141
142     logging.basicConfig(level=args.loglevel)
143     logging.info('Creating %s toolchain files', toolchain)
144
145     values = {}
146
147     # Iterate through the inputs
148     for input in args.input:
149         input = __get_path(input)
150
151         read_msbuild_xml(input, values)
152
153     # Determine if the output directory needs to be created
154     output_dir = __get_path(args.output)
155
156     if not os.path.exists(output_dir):
157         os.mkdir(output_dir)
158         logging.info('Created output directory %s', output_dir)
159
160     for key, value in values.items():
161         output_path = __output_path(toolchain, key, output_dir)
162
163         if os.path.exists(output_path) and not args.overwrite:
164             logging.info('Comparing previous output to current')
165
166             __merge_json_values(value, read_msbuild_json(output_path))
167         else:
168             logging.info('Original output will be overwritten')
169
170         logging.info('Writing MS Build JSON file at %s', output_path)
171
172         __write_json_file(output_path, value)
173
174
175 ###########################################################################################
176 # private joining functions
177 def __merge_json_values(current, previous):
178     """Merges the values between the current and previous run of the script."""
179     for value in current:
180         name = value['name']
181
182         # Find the previous value
183         previous_value = __find_and_remove_value(previous, value)
184
185         if previous_value is not None:
186             flags = value['flags']
187             previous_flags = previous_value['flags']
188
189             if flags != previous_flags:
190                 logging.warning(
191                     'Flags for %s are different. Using previous value.', name)
192
193                 value['flags'] = previous_flags
194         else:
195             logging.warning('Value %s is a new value', name)
196
197     for value in previous:
198         name = value['name']
199         logging.warning(
200             'Value %s not present in current run. Appending value.', name)
201
202         current.append(value)
203
204
205 def __find_and_remove_value(list, compare):
206     """Finds the value in the list that corresponds with the value of compare."""
207     # next throws if there are no matches
208     try:
209         found = next(value for value in list
210                      if value['name'] == compare['name'] and value['switch'] ==
211                      compare['switch'])
212     except:
213         return None
214
215     list.remove(found)
216
217     return found
218
219
220 def __normalize_switch(switch, separator):
221     new = switch
222     if switch.startswith("/") or switch.startswith("-"):
223       new = switch[1:]
224     if new and separator:
225       new = new + separator
226     return new
227
228 ###########################################################################################
229 # private xml functions
230 def __convert(root, tag, values, func):
231     """Converts the tag type found in the root and converts them using the func
232     and appends them to the values.
233     """
234     elements = root.getElementsByTagName(tag)
235
236     for element in elements:
237         converted = func(element)
238
239         # Append to the list
240         __append_list(values, converted)
241
242
243 def __convert_enum(node):
244     """Converts an EnumProperty node to JSON format."""
245     name = __get_attribute(node, 'Name')
246     logging.debug('Found EnumProperty named %s', name)
247
248     converted_values = []
249
250     for value in node.getElementsByTagName('EnumValue'):
251         converted = __convert_node(value)
252
253         converted['value'] = converted['name']
254         converted['name'] = name
255
256         # Modify flags when there is an argument child
257         __with_argument(value, converted)
258
259         converted_values.append(converted)
260
261     return converted_values
262
263
264 def __convert_bool(node):
265     """Converts an BoolProperty node to JSON format."""
266     converted = __convert_node(node, default_value='true')
267
268     # Check for a switch for reversing the value
269     reverse_switch = __get_attribute(node, 'ReverseSwitch')
270
271     if reverse_switch:
272         __with_argument(node, converted)
273
274         converted_reverse = copy.deepcopy(converted)
275
276         converted_reverse['switch'] = reverse_switch
277         converted_reverse['value'] = 'false'
278
279         return [converted_reverse, converted]
280
281     # Modify flags when there is an argument child
282     __with_argument(node, converted)
283
284     return __check_for_flag(converted)
285
286
287 def __convert_string_list(node):
288     """Converts a StringListProperty node to JSON format."""
289     converted = __convert_node(node)
290
291     # Determine flags for the string list
292     flags = vsflags(VSFlags.UserValue)
293
294     # Check for a separator to determine if it is semicolon appendable
295     # If not present assume the value should be ;
296     separator = __get_attribute(node, 'Separator', default_value=';')
297
298     if separator == ';':
299         flags = vsflags(flags, VSFlags.SemicolonAppendable)
300
301     converted['flags'] = flags
302
303     return __check_for_flag(converted)
304
305
306 def __convert_string(node):
307     """Converts a StringProperty node to JSON format."""
308     converted = __convert_node(node, default_flags=vsflags(VSFlags.UserValue))
309
310     return __check_for_flag(converted)
311
312
313 def __convert_node(node, default_value='', default_flags=vsflags()):
314     """Converts a XML node to a JSON equivalent."""
315     name = __get_attribute(node, 'Name')
316     logging.debug('Found %s named %s', node.tagName, name)
317
318     converted = {}
319     converted['name'] = name
320
321     switch = __get_attribute(node, 'Switch')
322     separator = __get_attribute(node, 'Separator')
323     converted['switch'] = __normalize_switch(switch, separator)
324
325     converted['comment'] = __get_attribute(node, 'DisplayName')
326     converted['value'] = default_value
327
328     # Check for the Flags attribute in case it was created during preprocessing
329     flags = __get_attribute(node, 'Flags')
330
331     if flags:
332         flags = flags.split(',')
333     else:
334         flags = default_flags
335
336     converted['flags'] = flags
337
338     return converted
339
340
341 def __check_for_flag(value):
342     """Checks whether the value has a switch value.
343
344     If not then returns None as it should not be added.
345     """
346     if value['switch']:
347         return value
348     else:
349         logging.warning('Skipping %s which has no command line switch',
350                         value['name'])
351         return None
352
353
354 def __with_argument(node, value):
355     """Modifies the flags in value if the node contains an Argument."""
356     arguments = node.getElementsByTagName('Argument')
357
358     if arguments:
359         logging.debug('Found argument within %s', value['name'])
360         value['flags'] = vsflags(VSFlags.UserValueIgnored, VSFlags.Continue)
361
362
363 def __preprocess_arguments(root):
364     """Preprocesses occurrences of Argument within the root.
365
366     Argument XML values reference other values within the document by name. The
367     referenced value does not contain a switch. This function will add the
368     switch associated with the argument.
369     """
370     # Set the flags to require a value
371     flags = ','.join(vsflags(VSFlags.UserValueRequired))
372
373     # Search through the arguments
374     arguments = root.getElementsByTagName('Argument')
375
376     for argument in arguments:
377         reference = __get_attribute(argument, 'Property')
378         found = None
379
380         # Look for the argument within the root's children
381         for child in root.childNodes:
382             # Ignore Text nodes
383             if isinstance(child, Element):
384                 name = __get_attribute(child, 'Name')
385
386                 if name == reference:
387                     found = child
388                     break
389
390         if found is not None:
391             logging.info('Found property named %s', reference)
392             # Get the associated switch
393             switch = __get_attribute(argument.parentNode, 'Switch')
394
395             # See if there is already a switch associated with the element.
396             if __get_attribute(found, 'Switch'):
397                 logging.debug('Copying node %s', reference)
398                 clone = found.cloneNode(True)
399                 root.insertBefore(clone, found)
400                 found = clone
401
402             found.setAttribute('Switch', switch)
403             found.setAttribute('Flags', flags)
404         else:
405             logging.warning('Could not find property named %s', reference)
406
407
408 def __get_attribute(node, name, default_value=''):
409     """Retrieves the attribute of the given name from the node.
410
411     If not present then the default_value is used.
412     """
413     if node.hasAttribute(name):
414         return node.attributes[name].value.strip()
415     else:
416         return default_value
417
418
419 ###########################################################################################
420 # private path functions
421 def __get_path(path):
422     """Gets the path to the file."""
423     if not os.path.isabs(path):
424         path = os.path.join(os.getcwd(), path)
425
426     return os.path.normpath(path)
427
428
429 def __output_path(toolchain, rule, output_dir):
430     """Gets the output path for a file given the toolchain, rule and output_dir"""
431     filename = '%s_%s.json' % (toolchain, rule)
432     return os.path.join(output_dir, filename)
433
434
435 ###########################################################################################
436 # private JSON file functions
437 def __read_json_file(path):
438     """Reads a JSON file at the path."""
439     with open(path, 'r') as f:
440         return json.load(f)
441
442
443 def __write_json_file(path, values):
444     """Writes a JSON file at the path with the values provided."""
445     # Sort the keys to ensure ordering
446     sort_order = ['name', 'switch', 'comment', 'value', 'flags']
447     sorted_values = [
448         OrderedDict(
449             sorted(
450                 value.items(), key=lambda value: sort_order.index(value[0])))
451         for value in values
452     ]
453
454     with open(path, 'w') as f:
455         json.dump(sorted_values, f, indent=2, separators=(',', ': '))
456         f.write("\n")
457
458 ###########################################################################################
459 # private list helpers
460 def __append_list(append_to, value):
461     """Appends the value to the list."""
462     if value is not None:
463         if isinstance(value, list):
464             append_to.extend(value)
465         else:
466             append_to.append(value)
467
468 ###########################################################################################
469 # main entry point
470 if __name__ == "__main__":
471     main()