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 |