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 class SubstitutionError(Exception): | |
25 def __init__(self, key): | |
26 super(SubstitutionError, self).__init__() | |
27 self.key = key | |
28 | |
29 | |
24 class ArgumentParser(argparse.ArgumentParser): | 30 class ArgumentParser(argparse.ArgumentParser): |
25 """Subclass of argparse.ArgumentParser to work with GN response files. | 31 """Subclass of argparse.ArgumentParser to work with GN response files. |
26 | 32 |
27 GN response file writes all the arguments on a single line and assumes | 33 GN response file writes all the arguments on a single line and assumes |
28 that the python script uses shlext.split() to extract them. Since the | 34 that the python script uses shlext.split() to extract them. Since the |
29 default ArgumentParser expects a single argument per line, we need to | 35 default ArgumentParser expects a single argument per line, we need to |
30 provide a subclass to have the correct support for @{{response_file_name}}. | 36 provide a subclass to have the correct support for @{{response_file_name}}. |
31 """ | 37 """ |
32 | 38 |
33 def convert_arg_line_to_args(self, arg_line): | 39 def convert_arg_line_to_args(self, arg_line): |
34 return shlex.split(arg_line) | 40 return shlex.split(arg_line) |
35 | 41 |
36 | 42 |
37 def InterpolateList(values, substitutions): | |
38 """Interpolates variable references into |value| using |substitutions|. | |
39 | |
40 Inputs: | |
41 values: a list of values | |
42 substitutions: a mapping of variable names to values | |
43 | |
44 Returns: | |
45 A new list of values with all variables references ${VARIABLE} replaced | |
46 by their value in |substitutions| or None if any of the variable has no | |
47 subsitution. | |
48 """ | |
49 result = [] | |
50 for value in values: | |
51 interpolated = InterpolateValue(value, substitutions) | |
52 if interpolated is None: | |
53 return None | |
54 result.append(interpolated) | |
55 return result | |
56 | |
57 | |
58 def InterpolateString(value, substitutions): | 43 def InterpolateString(value, substitutions): |
59 """Interpolates variable references into |value| using |substitutions|. | 44 """Interpolates variable references into |value| using |substitutions|. |
60 | 45 |
61 Inputs: | 46 Inputs: |
62 value: a string | 47 value: a string |
63 substitutions: a mapping of variable names to values | 48 substitutions: a mapping of variable names to values |
64 | 49 |
65 Returns: | 50 Returns: |
66 A new string with all variables references ${VARIABLES} replaced by their | 51 A new string with all variables references ${VARIABLES} replaced by their |
67 value in |substitutions| or None if any of the variable has no substitution. | 52 value in |substitutions|. Raises SubstitutionError if a variable has no |
53 substitution. | |
68 """ | 54 """ |
69 result = value | 55 def repl(match): |
70 for match in reversed(list(SUBST_RE.finditer(value))): | |
71 variable = match.group('id') | 56 variable = match.group('id') |
72 if variable not in substitutions: | 57 if variable not in substitutions: |
73 return None | 58 raise SubstitutionError(variable) |
74 # Some values need to be identifier and thus the variables references may | 59 # Some values need to be identifier and thus the variables references may |
75 # contains :modifier attributes to indicate how they should be converted | 60 # contains :modifier attributes to indicate how they should be converted |
76 # to identifiers ("identifier" replaces all invalid characters by '_' and | 61 # to identifiers ("identifier" replaces all invalid characters by '_' and |
77 # "rfc1034identifier" replaces them by "-" to make valid URI too). | 62 # "rfc1034identifier" replaces them by "-" to make valid URI too). |
78 modifier = match.group('modifier') | 63 modifier = match.group('modifier') |
79 if modifier == ':identifier': | 64 if modifier == ':identifier': |
80 interpolated = IDENT_RE.sub('_', substitutions[variable]) | 65 return IDENT_RE.sub('_', substitutions[variable]) |
81 elif modifier == ':rfc1034identifier': | 66 elif modifier == ':rfc1034identifier': |
82 interpolated = IDENT_RE.sub('-', substitutions[variable]) | 67 return IDENT_RE.sub('-', substitutions[variable]) |
83 else: | 68 else: |
84 interpolated = substitutions[variable] | 69 return substitutions[variable] |
85 result = result[:match.start()] + interpolated + result[match.end():] | 70 return SUBST_RE.sub(repl, value) |
86 return result | |
87 | 71 |
88 | 72 |
89 def InterpolateValue(value, substitutions): | 73 def Interpolate(value, substitutions, map_fn=map): |
90 """Interpolates variable references into |value| using |substitutions|. | 74 """Interpolates variable references into |value| using |substitutions|. |
91 | 75 |
92 Inputs: | 76 Inputs: |
93 value: a value, can be a dictionary, list, string or other | 77 value: a value, can be a dictionary, list, string or other |
94 substitutions: a mapping of variable names to values | 78 substitutions: a mapping of variable names to values |
95 | 79 |
96 Returns: | 80 Returns: |
97 A new value with all variables references ${VARIABLES} replaced by their | 81 A new value with all variables references ${VARIABLES} replaced by their |
98 value in |substitutions| or None if any of the variable has no substitution. | 82 value in |substitutions|. Raises SubstitutionError if a variable has no |
83 substitution. | |
99 """ | 84 """ |
100 if isinstance(value, dict): | 85 if isinstance(value, dict): |
101 return Interpolate(value, substitutions) | 86 return dict(map_fn(lambda (k, v): (k, Interpolate(v, substitutions, |
87 map_fn=map_fn)), value.iteritems())) | |
102 if isinstance(value, list): | 88 if isinstance(value, list): |
103 return InterpolateList(value, substitutions) | 89 return list(map_fn(lambda v: Interpolate(v, substitutions, map_fn=map_fn), |
90 value.iteritems())) | |
104 if isinstance(value, str): | 91 if isinstance(value, str): |
105 return InterpolateString(value, substitutions) | 92 return InterpolateString(value, substitutions) |
sdefresne
2016/09/27 08:26:00
I think call may lead to uncaught SubstitutionErro
Sidney San Martín
2016/09/27 20:15:22
I think the current behavior might be correct, but
| |
106 return value | 93 return value |
107 | 94 |
108 | 95 |
109 def Interpolate(plist, substitutions): | |
110 """Interpolates variable references into |value| using |substitutions|. | |
111 | |
112 Inputs: | |
113 plist: a dictionary representing a Property List (.plist) file | |
114 substitutions: a mapping of variable names to values | |
115 | |
116 Returns: | |
117 A new plist with all variables references ${VARIABLES} replaced by their | |
118 value in |substitutions|. All values that contains references with no | |
119 substitutions will be removed and the corresponding key will be cleared | |
120 from the plist (not recursively). | |
121 """ | |
122 result = {} | |
123 for key in plist: | |
124 value = InterpolateValue(plist[key], substitutions) | |
125 if value is not None: | |
126 result[key] = value | |
127 return result | |
128 | |
129 | |
130 def LoadPList(path): | 96 def LoadPList(path): |
131 """Loads Plist at |path| and returns it as a dictionary.""" | 97 """Loads Plist at |path| and returns it as a dictionary.""" |
132 fd, name = tempfile.mkstemp() | 98 fd, name = tempfile.mkstemp() |
133 try: | 99 try: |
134 subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path]) | 100 subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path]) |
135 with os.fdopen(fd, 'r') as f: | 101 with os.fdopen(fd, 'r') as f: |
136 return plistlib.readPlist(f) | 102 return plistlib.readPlist(f) |
137 finally: | 103 finally: |
138 os.unlink(name) | 104 os.unlink(name) |
139 | 105 |
(...skipping 18 matching lines...) Expand all Loading... | |
158 | 124 |
159 Args: | 125 Args: |
160 plist1: a dictionary representing a Property List (.plist) file | 126 plist1: a dictionary representing a Property List (.plist) file |
161 plist2: a dictionary representing a Property List (.plist) file | 127 plist2: a dictionary representing a Property List (.plist) file |
162 | 128 |
163 Returns: | 129 Returns: |
164 A new dictionary representing a Property List (.plist) file by merging | 130 A new dictionary representing a Property List (.plist) file by merging |
165 |plist1| with |plist2|. If any value is a dictionary, they are merged | 131 |plist1| with |plist2|. If any value is a dictionary, they are merged |
166 recursively, otherwise |plist2| value is used. | 132 recursively, otherwise |plist2| value is used. |
167 """ | 133 """ |
168 if not isinstance(plist1, dict) or not isinstance(plist2, dict): | 134 result = plist1.copy() |
169 if plist2 is not None: | 135 for key, value in plist2.iteritems(): |
170 return plist2 | |
171 else: | |
172 return plist1 | |
173 result = {} | |
174 for key in set(plist1) | set(plist2): | |
175 if key in plist2: | |
176 value = plist2[key] | |
177 else: | |
178 value = plist1[key] | |
179 if isinstance(value, dict): | 136 if isinstance(value, dict): |
180 value = MergePList(plist1.get(key, None), plist2.get(key, None)) | 137 old_value = result.get(key) |
138 if isinstance(old_value, dict): | |
139 value = MergePList(old_value, value) | |
181 result[key] = value | 140 result[key] = value |
182 return result | 141 return result |
183 | 142 |
184 | 143 |
185 def main(): | 144 def main(): |
186 parser = ArgumentParser( | 145 parser = ArgumentParser( |
187 description='A script to generate iOS application Info.plist.', | 146 description='A script to generate iOS application Info.plist.', |
188 fromfile_prefix_chars='@') | 147 fromfile_prefix_chars='@') |
189 parser.add_argument('-o', '--output', required=True, | 148 parser.add_argument('-o', '--output', required=True, |
190 help='Path to output plist file.') | 149 help='Path to output plist file.') |
191 parser.add_argument('-s', '--subst', action='append', default=[], | 150 parser.add_argument('-s', '--subst', action='append', default=[], |
192 help='Substitution rule in the format "key=value".') | 151 help='Substitution rule in the format "key=value".') |
193 parser.add_argument('-f', '--format', required=True, | 152 parser.add_argument('-f', '--format', required=True, |
194 help='Plist format (e.g. binary1, xml1) to output.') | 153 help='Plist format (e.g. binary1, xml1) to output.') |
154 parser.add_argument('--skip-unbound-variables', action='store_true', | |
155 help="""When an item uses an unbound variable, skip it | |
156 instead of raising an error. | |
157 """) | |
195 parser.add_argument('path', nargs="+", help='Path to input plist files.') | 158 parser.add_argument('path', nargs="+", help='Path to input plist files.') |
196 args = parser.parse_args() | 159 args = parser.parse_args() |
197 substitutions = {} | 160 substitutions = {} |
198 for subst in args.subst: | 161 for subst in args.subst: |
199 key, value = subst.split('=', 1) | 162 key, value = subst.split('=', 1) |
200 substitutions[key] = value | 163 substitutions[key] = value |
201 data = {} | 164 data = {} |
165 if args.skip_unbound_variables: | |
166 def interpolate_fn(*args): | |
167 def map_fn(f, iterable): | |
168 for v in iterable: | |
169 try: | |
170 yield f(v) | |
171 except SubstitutionError: | |
172 pass | |
173 return Interpolate(*args, map_fn=map_fn) | |
174 else: | |
175 interpolate_fn = Interpolate | |
176 | |
202 for filename in args.path: | 177 for filename in args.path: |
203 data = MergePList(data, LoadPList(filename)) | 178 plist = LoadPList(filename) |
204 data = Interpolate(data, substitutions) | 179 try: |
180 data = MergePList(data, interpolate_fn(plist, substitutions)) | |
181 except SubstitutionError as e: | |
182 print >>sys.stderr, ( | |
183 "SubstitutionError: No substitution found for '{0}' in {1}" | |
184 .format(e.key, filename)) | |
185 return 1 | |
186 | |
205 SavePList(args.output, args.format, data) | 187 SavePList(args.output, args.format, data) |
206 return 0 | 188 return 0 |
207 | 189 |
208 if __name__ == '__main__': | 190 if __name__ == '__main__': |
209 sys.exit(main()) | 191 sys.exit(main()) |
OLD | NEW |