| OLD | NEW |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. | 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import argparse | 5 import argparse |
| 6 import plistlib | 6 import plistlib |
| 7 import os | 7 import os |
| 8 import re | 8 import re |
| 9 import subprocess | 9 import subprocess |
| 10 import sys | 10 import sys |
| 11 import tempfile | 11 import tempfile |
| 12 import shlex | 12 import shlex |
| 13 | 13 |
| 14 | 14 |
| 15 # Xcode substitutes variables like ${PRODUCT_NAME} when compiling Info.plist. | 15 # Xcode substitutes variables like ${PRODUCT_NAME} when compiling Info.plist. |
| 16 # It also supports supports modifiers like :identifier or :rfc1034identifier. | 16 # It also supports supports modifiers like :identifier or :rfc1034identifier. |
| 17 # SUBST_RE matches a variable substitution pattern with an optional modifier, | 17 # SUBST_RE matches a variable substitution pattern with an optional modifier, |
| 18 # while IDENT_RE matches all characters that are not valid in an "identifier" | 18 # while IDENT_RE matches all characters that are not valid in an "identifier" |
| 19 # value (used when applying the modifier). | 19 # value (used when applying the modifier). |
| 20 SUBST_RE = re.compile(r'\$\{(?P<id>[^}]*?)(?P<modifier>:[^}]*)?\}') | 20 SUBST_RE = re.compile(r'\$\{(?P<id>[^}]*?)(?P<modifier>:[^}]*)?\}') |
| 21 IDENT_RE = re.compile(r'[_/\s]') | 21 IDENT_RE = re.compile(r'[_/\s]') |
| 22 | 22 |
| 23 | 23 |
| 24 def InterpolateList(values, substitutions): | 24 class SubstitutionError(Exception): |
| 25 """Interpolates variable references into |value| using |substitutions|. | 25 def __init__(self, key): |
| 26 super(SubstitutionError, self).__init__() |
| 27 self.key = key |
| 26 | 28 |
| 27 Inputs: | 29 def __str__(self): |
| 28 values: a list of values | 30 return "SubstitutionError: {}".format(self.key) |
| 29 substitutions: a mapping of variable names to values | |
| 30 | |
| 31 Returns: | |
| 32 A new list of values with all variables references ${VARIABLE} replaced | |
| 33 by their value in |substitutions| or None if any of the variable has no | |
| 34 subsitution. | |
| 35 """ | |
| 36 result = [] | |
| 37 for value in values: | |
| 38 interpolated = InterpolateValue(value, substitutions) | |
| 39 if interpolated is None: | |
| 40 return None | |
| 41 result.append(interpolated) | |
| 42 return result | |
| 43 | 31 |
| 44 | 32 |
| 45 def InterpolateString(value, substitutions): | 33 def InterpolateString(value, substitutions): |
| 46 """Interpolates variable references into |value| using |substitutions|. | 34 """Interpolates variable references into |value| using |substitutions|. |
| 47 | 35 |
| 48 Inputs: | 36 Inputs: |
| 49 value: a string | 37 value: a string |
| 50 substitutions: a mapping of variable names to values | 38 substitutions: a mapping of variable names to values |
| 51 | 39 |
| 52 Returns: | 40 Returns: |
| 53 A new string with all variables references ${VARIABLES} replaced by their | 41 A new string with all variables references ${VARIABLES} replaced by their |
| 54 value in |substitutions| or None if any of the variable has no substitution. | 42 value in |substitutions|. Raises SubstitutionError if a variable has no |
| 43 substitution. |
| 55 """ | 44 """ |
| 56 result = value | 45 def repl(match): |
| 57 for match in reversed(list(SUBST_RE.finditer(value))): | |
| 58 variable = match.group('id') | 46 variable = match.group('id') |
| 59 if variable not in substitutions: | 47 if variable not in substitutions: |
| 60 return None | 48 raise SubstitutionError(variable) |
| 61 # Some values need to be identifier and thus the variables references may | 49 # Some values need to be identifier and thus the variables references may |
| 62 # contains :modifier attributes to indicate how they should be converted | 50 # contains :modifier attributes to indicate how they should be converted |
| 63 # to identifiers ("identifier" replaces all invalid characters by '_' and | 51 # to identifiers ("identifier" replaces all invalid characters by '_' and |
| 64 # "rfc1034identifier" replaces them by "-" to make valid URI too). | 52 # "rfc1034identifier" replaces them by "-" to make valid URI too). |
| 65 modifier = match.group('modifier') | 53 modifier = match.group('modifier') |
| 66 if modifier == ':identifier': | 54 if modifier == ':identifier': |
| 67 interpolated = IDENT_RE.sub('_', substitutions[variable]) | 55 return IDENT_RE.sub('_', substitutions[variable]) |
| 68 elif modifier == ':rfc1034identifier': | 56 elif modifier == ':rfc1034identifier': |
| 69 interpolated = IDENT_RE.sub('-', substitutions[variable]) | 57 return IDENT_RE.sub('-', substitutions[variable]) |
| 70 else: | 58 else: |
| 71 interpolated = substitutions[variable] | 59 return substitutions[variable] |
| 72 result = result[:match.start()] + interpolated + result[match.end():] | 60 return SUBST_RE.sub(repl, value) |
| 73 return result | |
| 74 | 61 |
| 75 | 62 |
| 76 def InterpolateValue(value, substitutions): | 63 def Interpolate(value, substitutions): |
| 77 """Interpolates variable references into |value| using |substitutions|. | 64 """Interpolates variable references into |value| using |substitutions|. |
| 78 | 65 |
| 79 Inputs: | 66 Inputs: |
| 80 value: a value, can be a dictionary, list, string or other | 67 value: a value, can be a dictionary, list, string or other |
| 81 substitutions: a mapping of variable names to values | 68 substitutions: a mapping of variable names to values |
| 82 | 69 |
| 83 Returns: | 70 Returns: |
| 84 A new value with all variables references ${VARIABLES} replaced by their | 71 A new value with all variables references ${VARIABLES} replaced by their |
| 85 value in |substitutions| or None if any of the variable has no substitution. | 72 value in |substitutions|. Raises SubstitutionError if a variable has no |
| 73 substitution. |
| 86 """ | 74 """ |
| 87 if isinstance(value, dict): | 75 if isinstance(value, dict): |
| 88 return Interpolate(value, substitutions) | 76 return {k: Interpolate(v, substitutions) for k, v in value.iteritems()} |
| 89 if isinstance(value, list): | 77 if isinstance(value, list): |
| 90 return InterpolateList(value, substitutions) | 78 return [Interpolate(v, substitutions) for v in value] |
| 91 if isinstance(value, str): | 79 if isinstance(value, str): |
| 92 return InterpolateString(value, substitutions) | 80 return InterpolateString(value, substitutions) |
| 93 return value | 81 return value |
| 94 | 82 |
| 95 | 83 |
| 96 def Interpolate(plist, substitutions): | |
| 97 """Interpolates variable references into |value| using |substitutions|. | |
| 98 | |
| 99 Inputs: | |
| 100 plist: a dictionary representing a Property List (.plist) file | |
| 101 substitutions: a mapping of variable names to values | |
| 102 | |
| 103 Returns: | |
| 104 A new plist with all variables references ${VARIABLES} replaced by their | |
| 105 value in |substitutions|. All values that contains references with no | |
| 106 substitutions will be removed and the corresponding key will be cleared | |
| 107 from the plist (not recursively). | |
| 108 """ | |
| 109 result = {} | |
| 110 for key in plist: | |
| 111 value = InterpolateValue(plist[key], substitutions) | |
| 112 if value is not None: | |
| 113 result[key] = value | |
| 114 return result | |
| 115 | |
| 116 | |
| 117 def LoadPList(path): | 84 def LoadPList(path): |
| 118 """Loads Plist at |path| and returns it as a dictionary.""" | 85 """Loads Plist at |path| and returns it as a dictionary.""" |
| 119 fd, name = tempfile.mkstemp() | 86 fd, name = tempfile.mkstemp() |
| 120 try: | 87 try: |
| 121 subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path]) | 88 subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path]) |
| 122 with os.fdopen(fd, 'r') as f: | 89 with os.fdopen(fd, 'r') as f: |
| 123 return plistlib.readPlist(f) | 90 return plistlib.readPlist(f) |
| 124 finally: | 91 finally: |
| 125 os.unlink(name) | 92 os.unlink(name) |
| 126 | 93 |
| (...skipping 19 matching lines...) Expand all Loading... |
| 146 Args: | 113 Args: |
| 147 plist1: a dictionary representing a Property List (.plist) file | 114 plist1: a dictionary representing a Property List (.plist) file |
| 148 plist2: a dictionary representing a Property List (.plist) file | 115 plist2: a dictionary representing a Property List (.plist) file |
| 149 | 116 |
| 150 Returns: | 117 Returns: |
| 151 A new dictionary representing a Property List (.plist) file by merging | 118 A new dictionary representing a Property List (.plist) file by merging |
| 152 |plist1| with |plist2|. If any value is a dictionary, they are merged | 119 |plist1| with |plist2|. If any value is a dictionary, they are merged |
| 153 recursively, otherwise |plist2| value is used. If values are list, they | 120 recursively, otherwise |plist2| value is used. If values are list, they |
| 154 are concatenated. | 121 are concatenated. |
| 155 """ | 122 """ |
| 156 if not isinstance(plist1, dict) or not isinstance(plist2, dict): | 123 result = plist1.copy() |
| 157 if plist2 is not None: | 124 for key, value in plist2.iteritems(): |
| 158 return plist2 | |
| 159 else: | |
| 160 return plist1 | |
| 161 result = {} | |
| 162 for key in set(plist1) | set(plist2): | |
| 163 if key in plist2: | |
| 164 value = plist2[key] | |
| 165 else: | |
| 166 value = plist1[key] | |
| 167 if isinstance(value, dict): | 125 if isinstance(value, dict): |
| 168 value = MergePList(plist1.get(key, None), plist2.get(key, None)) | 126 old_value = result.get(key) |
| 127 if isinstance(old_value, dict): |
| 128 value = MergePList(old_value, value) |
| 169 if isinstance(value, list): | 129 if isinstance(value, list): |
| 170 value = plist1.get(key, []) + plist2.get(key, []) | 130 value = plist1.get(key, []) + plist2.get(key, []) |
| 171 result[key] = value | 131 result[key] = value |
| 172 return result | 132 return result |
| 173 | 133 |
| 174 | 134 |
| 175 class Action(object): | 135 class Action(object): |
| 176 """Class implementing one action supported by the script.""" | 136 """Class implementing one action supported by the script.""" |
| 177 | 137 |
| 178 @classmethod | 138 @classmethod |
| (...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 245 | 205 |
| 246 for action in [MergeAction, SubstituteAction]: | 206 for action in [MergeAction, SubstituteAction]: |
| 247 action.Register(subparsers) | 207 action.Register(subparsers) |
| 248 | 208 |
| 249 args = parser.parse_args() | 209 args = parser.parse_args() |
| 250 args.func(args) | 210 args.func(args) |
| 251 | 211 |
| 252 | 212 |
| 253 if __name__ == '__main__': | 213 if __name__ == '__main__': |
| 254 sys.exit(Main()) | 214 sys.exit(Main()) |
| OLD | NEW |