OLD | NEW |
| (Empty) |
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 | |
3 # found in the LICENSE file. | |
4 | |
5 import argparse | |
6 import plistlib | |
7 import os | |
8 import re | |
9 import subprocess | |
10 import sys | |
11 import tempfile | |
12 import shlex | |
13 | |
14 | |
15 # Xcode substitutes variables like ${PRODUCT_NAME} when compiling Info.plist. | |
16 # It also supports supports modifiers like :identifier or :rfc1034identifier. | |
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" | |
19 # value (used when applying the modifier). | |
20 SUBST_RE = re.compile(r'\$\{(?P<id>[^}]*?)(?P<modifier>:[^}]*)?\}') | |
21 IDENT_RE = re.compile(r'[/\s]') | |
22 | |
23 | |
24 class ArgumentParser(argparse.ArgumentParser): | |
25 """Subclass of argparse.ArgumentParser to work with GN response files. | |
26 | |
27 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 | |
29 default ArgumentParser expects a single argument per line, we need to | |
30 provide a subclass to have the correct support for @{{response_file_name}}. | |
31 """ | |
32 | |
33 def convert_arg_line_to_args(self, arg_line): | |
34 return shlex.split(arg_line) | |
35 | |
36 | |
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): | |
59 """Interpolates variable references into |value| using |substitutions|. | |
60 | |
61 Inputs: | |
62 value: a string | |
63 substitutions: a mapping of variable names to values | |
64 | |
65 Returns: | |
66 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. | |
68 """ | |
69 result = value | |
70 for match in reversed(list(SUBST_RE.finditer(value))): | |
71 variable = match.group('id') | |
72 if variable not in substitutions: | |
73 return None | |
74 # Some values need to be identifier and thus the variables references may | |
75 # contains :modifier attributes to indicate how they should be converted | |
76 # to identifiers ("identifier" replaces all invalid characters by '-' and | |
77 # "rfc1034identifier" replaces them by "_" to make valid URI too). | |
78 modifier = match.group('modifier') | |
79 if modifier == 'identifier': | |
80 interpolated = IDENT_RE.sub('-', substitutions[variable]) | |
81 elif modifier == 'rfc1034identifier': | |
82 interpolated = IDENT_RE.sub('_', substitutions[variable]) | |
83 else: | |
84 interpolated = substitutions[variable] | |
85 result = result[:match.start()] + interpolated + result[match.end():] | |
86 return result | |
87 | |
88 | |
89 def InterpolateValue(value, substitutions): | |
90 """Interpolates variable references into |value| using |substitutions|. | |
91 | |
92 Inputs: | |
93 value: a value, can be a dictionary, list, string or other | |
94 substitutions: a mapping of variable names to values | |
95 | |
96 Returns: | |
97 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. | |
99 """ | |
100 if isinstance(value, dict): | |
101 return Interpolate(value, substitutions) | |
102 if isinstance(value, list): | |
103 return InterpolateList(value, substitutions) | |
104 if isinstance(value, str): | |
105 return InterpolateString(value, substitutions) | |
106 return value | |
107 | |
108 | |
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): | |
131 """Loads Plist at |path| and returns it as a dictionary.""" | |
132 fd, name = tempfile.mkstemp() | |
133 try: | |
134 subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path]) | |
135 with os.fdopen(fd, 'r') as f: | |
136 return plistlib.readPlist(f) | |
137 finally: | |
138 os.unlink(name) | |
139 | |
140 | |
141 def SavePList(path, data): | |
142 """Saves |data| as a Plist to |path| in binary1 format.""" | |
143 fd, name = tempfile.mkstemp() | |
144 try: | |
145 with os.fdopen(fd, 'w') as f: | |
146 plistlib.writePlist(data, f) | |
147 subprocess.check_call(['plutil', '-convert', 'binary1', '-o', path, name]) | |
148 finally: | |
149 os.unlink(name) | |
150 | |
151 | |
152 def MergePList(plist1, plist2): | |
153 """Merges |plist1| with |plist2| recursively. | |
154 | |
155 Creates a new dictionary representing a Property List (.plist) files by | |
156 merging the two dictionary |plist1| and |plist2| recursively (only for | |
157 dictionary values). | |
158 | |
159 Args: | |
160 plist1: a dictionary representing a Property List (.plist) file | |
161 plist2: a dictionary representing a Property List (.plist) file | |
162 | |
163 Returns: | |
164 A new dictionary representing a Property List (.plist) file by merging | |
165 |plist1| with |plist2|. If any value is a dictionary, they are merged | |
166 recursively, otherwise |plist2| value is used. | |
167 """ | |
168 if not isinstance(plist1, dict) or not isinstance(plist2, dict): | |
169 if plist2 is not None: | |
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): | |
180 value = MergePList(plist1.get(key, None), plist2.get(key, None)) | |
181 result[key] = value | |
182 return result | |
183 | |
184 | |
185 def main(): | |
186 parser = ArgumentParser( | |
187 description='A script to generate iOS application Info.plist.', | |
188 fromfile_prefix_chars='@') | |
189 parser.add_argument('-o', '--output', required=True, | |
190 help='Path to output plist file.') | |
191 parser.add_argument('-s', '--subst', action='append', default=[], | |
192 help='Substitution rule in the format "key=value".') | |
193 parser.add_argument('path', nargs="+", help='Path to input plist files.') | |
194 args = parser.parse_args() | |
195 substitutions = {} | |
196 for subst in args.subst: | |
197 key, value = subst.split('=', 1) | |
198 substitutions[key] = value | |
199 data = {} | |
200 for filename in args.path: | |
201 data = MergePList(data, LoadPList(filename)) | |
202 data = Interpolate(data, substitutions) | |
203 SavePList(args.output, data) | |
204 return 0 | |
205 | |
206 if __name__ == '__main__': | |
207 sys.exit(main()) | |
OLD | NEW |