blob: e3d4f4af4804c870a07fb5cb68789f22776ecbd1 [file] [log] [blame]
Ben Murdoch097c5b22016-05-18 11:27:45 +01001# 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
5import argparse
6import plistlib
7import os
8import re
9import subprocess
10import sys
11import tempfile
12import 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).
20SUBST_RE = re.compile(r'\$\{(?P<id>[^}]*?)(?P<modifier>:[^}]*)?\}')
21IDENT_RE = re.compile(r'[/\s]')
22
23
24class 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
37def 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
58def 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
89def 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
109def 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
130def 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
141def SavePList(path, format, data):
142 """Saves |data| as a Plist to |path| in the specified |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', format, '-o', path, name])
148 finally:
149 os.unlink(name)
150
151
152def 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
185def 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('-f', '--format', required=True,
194 help='Plist format (e.g. binary1, xml1) to output.')
195 parser.add_argument('path', nargs="+", help='Path to input plist files.')
196 args = parser.parse_args()
197 substitutions = {}
198 for subst in args.subst:
199 key, value = subst.split('=', 1)
200 substitutions[key] = value
201 data = {}
202 for filename in args.path:
203 data = MergePList(data, LoadPList(filename))
204 data = Interpolate(data, substitutions)
205 SavePList(args.output, args.format, data)
206 return 0
207
208if __name__ == '__main__':
209 sys.exit(main())