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 from __future__ import print_function | |
6 | |
5 import argparse | 7 import argparse |
6 import plistlib | 8 import plistlib |
7 import os | 9 import os |
8 import re | 10 import re |
9 import subprocess | 11 import subprocess |
10 import sys | 12 import sys |
11 import tempfile | 13 import tempfile |
12 import shlex | 14 import shlex |
13 | 15 |
14 | 16 |
15 # Xcode substitutes variables like ${PRODUCT_NAME} when compiling Info.plist. | 17 # Xcode substitutes variables like ${PRODUCT_NAME} when compiling Info.plist. |
16 # It also supports supports modifiers like :identifier or :rfc1034identifier. | 18 # It also supports supports modifiers like :identifier or :rfc1034identifier. |
17 # SUBST_RE matches a variable substitution pattern with an optional modifier, | 19 # 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" | 20 # while IDENT_RE matches all characters that are not valid in an "identifier" |
19 # value (used when applying the modifier). | 21 # value (used when applying the modifier). |
20 SUBST_RE = re.compile(r'\$\{(?P<id>[^}]*?)(?P<modifier>:[^}]*)?\}') | 22 SUBST_RE = re.compile(r'\$\{(?P<id>[^}]*?)(?P<modifier>:[^}]*)?\}') |
21 IDENT_RE = re.compile(r'[_/\s]') | 23 IDENT_RE = re.compile(r'[_/\s]') |
22 | 24 |
23 | 25 |
26 class SubstitutionError(Exception): | |
27 def __init__(self, key): | |
28 super(SubstitutionError, self).__init__() | |
29 self.key = key | |
30 | |
31 | |
24 class ArgumentParser(argparse.ArgumentParser): | 32 class ArgumentParser(argparse.ArgumentParser): |
25 """Subclass of argparse.ArgumentParser to work with GN response files. | 33 """Subclass of argparse.ArgumentParser to work with GN response files. |
26 | 34 |
27 GN response file writes all the arguments on a single line and assumes | 35 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 | 36 that the python script uses shlext.split() to extract them. Since the |
29 default ArgumentParser expects a single argument per line, we need to | 37 default ArgumentParser expects a single argument per line, we need to |
30 provide a subclass to have the correct support for @{{response_file_name}}. | 38 provide a subclass to have the correct support for @{{response_file_name}}. |
31 """ | 39 """ |
32 | 40 |
33 def convert_arg_line_to_args(self, arg_line): | 41 def convert_arg_line_to_args(self, arg_line): |
34 return shlex.split(arg_line) | 42 return shlex.split(arg_line) |
35 | 43 |
36 | 44 |
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): | 45 def InterpolateString(value, substitutions): |
59 """Interpolates variable references into |value| using |substitutions|. | 46 """Interpolates variable references into |value| using |substitutions|. |
60 | 47 |
61 Inputs: | 48 Inputs: |
62 value: a string | 49 value: a string |
63 substitutions: a mapping of variable names to values | 50 substitutions: a mapping of variable names to values |
64 | 51 |
65 Returns: | 52 Returns: |
66 A new string with all variables references ${VARIABLES} replaced by their | 53 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. | 54 value in |substitutions|. Raises SubstitutionError if a variable has no |
55 substitution. | |
68 """ | 56 """ |
69 result = value | 57 def repl(match): |
70 for match in reversed(list(SUBST_RE.finditer(value))): | |
71 variable = match.group('id') | 58 variable = match.group('id') |
72 if variable not in substitutions: | 59 if variable not in substitutions: |
73 return None | 60 raise SubstitutionError(variable) |
74 # Some values need to be identifier and thus the variables references may | 61 # Some values need to be identifier and thus the variables references may |
75 # contains :modifier attributes to indicate how they should be converted | 62 # contains :modifier attributes to indicate how they should be converted |
76 # to identifiers ("identifier" replaces all invalid characters by '_' and | 63 # to identifiers ("identifier" replaces all invalid characters by '_' and |
77 # "rfc1034identifier" replaces them by "-" to make valid URI too). | 64 # "rfc1034identifier" replaces them by "-" to make valid URI too). |
78 modifier = match.group('modifier') | 65 modifier = match.group('modifier') |
79 if modifier == ':identifier': | 66 if modifier == ':identifier': |
80 interpolated = IDENT_RE.sub('_', substitutions[variable]) | 67 return IDENT_RE.sub('_', substitutions[variable]) |
81 elif modifier == ':rfc1034identifier': | 68 elif modifier == ':rfc1034identifier': |
82 interpolated = IDENT_RE.sub('-', substitutions[variable]) | 69 return IDENT_RE.sub('-', substitutions[variable]) |
83 else: | 70 else: |
84 interpolated = substitutions[variable] | 71 return substitutions[variable] |
85 result = result[:match.start()] + interpolated + result[match.end():] | 72 return SUBST_RE.sub(repl, value) |
86 return result | |
87 | 73 |
88 | 74 |
89 def InterpolateValue(value, substitutions): | 75 def Interpolate(value, substitutions): |
90 """Interpolates variable references into |value| using |substitutions|. | 76 """Interpolates variable references into |value| using |substitutions|. |
91 | 77 |
92 Inputs: | 78 Inputs: |
93 value: a value, can be a dictionary, list, string or other | 79 value: a value, can be a dictionary, list, string or other |
94 substitutions: a mapping of variable names to values | 80 substitutions: a mapping of variable names to values |
95 | 81 |
96 Returns: | 82 Returns: |
97 A new value with all variables references ${VARIABLES} replaced by their | 83 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. | 84 value in |substitutions|. Raises SubstitutionError if a variable has no |
85 substitution. | |
99 """ | 86 """ |
100 if isinstance(value, dict): | 87 if isinstance(value, dict): |
101 return Interpolate(value, substitutions) | 88 return {k: Interpolate(v, substitutions) for k, v in value.iteritems()} |
102 if isinstance(value, list): | 89 if isinstance(value, list): |
103 return InterpolateList(value, substitutions) | 90 return [Interpolate(v, substitutions) for v in value] |
104 if isinstance(value, str): | 91 if isinstance(value, str): |
105 return InterpolateString(value, substitutions) | 92 return InterpolateString(value, substitutions) |
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() |
Robert Sesek
2016/09/23 20:55:55
Was this condition never hit?
Sidney San Martín
2016/09/26 18:38:25
It could get hit in the old code (:180 can call Me
| |
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.') |
195 parser.add_argument('path', nargs="+", help='Path to input plist files.') | 154 parser.add_argument('path', nargs="+", help='Path to input plist files.') |
196 args = parser.parse_args() | 155 args = parser.parse_args() |
197 substitutions = {} | 156 substitutions = {} |
198 for subst in args.subst: | 157 for subst in args.subst: |
199 key, value = subst.split('=', 1) | 158 key, value = subst.split('=', 1) |
200 substitutions[key] = value | 159 substitutions[key] = value |
201 data = {} | 160 data = {} |
202 for filename in args.path: | 161 for filename in args.path: |
203 data = MergePList(data, LoadPList(filename)) | 162 try: |
204 data = Interpolate(data, substitutions) | 163 plist = LoadPList(filename) |
164 data = MergePList(data, Interpolate(plist, substitutions)) | |
165 except SubstitutionError as e: | |
166 print("SubstitutionError: No substitution found for '{0}' in {1}" | |
Robert Sesek
2016/09/23 20:55:55
More idiomatic Chromium style is:
print >>sys.s
Sidney San Martín
2016/09/26 18:38:25
Done.
| |
167 .format(e.key, filename), file=sys.stderr) | |
168 return 1 | |
169 | |
205 SavePList(args.output, args.format, data) | 170 SavePList(args.output, args.format, data) |
206 return 0 | 171 return 0 |
207 | 172 |
208 if __name__ == '__main__': | 173 if __name__ == '__main__': |
209 sys.exit(main()) | 174 sys.exit(main()) |
OLD | NEW |