1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
|
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import plistlib
import os
import re
import subprocess
import sys
import tempfile
import shlex
# Xcode substitutes variables like ${PRODUCT_NAME} when compiling Info.plist.
# It also supports supports modifiers like :identifier or :rfc1034identifier.
# SUBST_RE matches a variable substitution pattern with an optional modifier,
# while IDENT_RE matches all characters that are not valid in an "identifier"
# value (used when applying the modifier).
SUBST_RE = re.compile(r'\$\{(?P<id>[^}]*?)(?P<modifier>:[^}]*)?\}')
IDENT_RE = re.compile(r'[/\s]')
class ArgumentParser(argparse.ArgumentParser):
"""Subclass of argparse.ArgumentParser to work with GN response files.
GN response file writes all the arguments on a single line and assumes
that the python script uses shlext.split() to extract them. Since the
default ArgumentParser expects a single argument per line, we need to
provide a subclass to have the correct support for @{{response_file_name}}.
"""
def convert_arg_line_to_args(self, arg_line):
return shlex.split(arg_line)
def InterpolateList(values, substitutions):
"""Interpolates variable references into |value| using |substitutions|.
Inputs:
values: a list of values
substitutions: a mapping of variable names to values
Returns:
A new list of values with all variables references ${VARIABLE} replaced
by their value in |substitutions| or None if any of the variable has no
subsitution.
"""
result = []
for value in values:
interpolated = InterpolateValue(value, substitutions)
if interpolated is None:
return None
result.append(interpolated)
return result
def InterpolateString(value, substitutions):
"""Interpolates variable references into |value| using |substitutions|.
Inputs:
value: a string
substitutions: a mapping of variable names to values
Returns:
A new string with all variables references ${VARIABLES} replaced by their
value in |substitutions| or None if any of the variable has no substitution.
"""
result = value
for match in reversed(list(SUBST_RE.finditer(value))):
variable = match.group('id')
if variable not in substitutions:
return None
# Some values need to be identifier and thus the variables references may
# contains :modifier attributes to indicate how they should be converted
# to identifiers ("identifier" replaces all invalid characters by '-' and
# "rfc1034identifier" replaces them by "_" to make valid URI too).
modifier = match.group('modifier')
if modifier == 'identifier':
interpolated = IDENT_RE.sub('-', substitutions[variable])
elif modifier == 'rfc1034identifier':
interpolated = IDENT_RE.sub('_', substitutions[variable])
else:
interpolated = substitutions[variable]
result = result[:match.start()] + interpolated + result[match.end():]
return result
def InterpolateValue(value, substitutions):
"""Interpolates variable references into |value| using |substitutions|.
Inputs:
value: a value, can be a dictionary, list, string or other
substitutions: a mapping of variable names to values
Returns:
A new value with all variables references ${VARIABLES} replaced by their
value in |substitutions| or None if any of the variable has no substitution.
"""
if isinstance(value, dict):
return Interpolate(value, substitutions)
if isinstance(value, list):
return InterpolateList(value, substitutions)
if isinstance(value, str):
return InterpolateString(value, substitutions)
return value
def Interpolate(plist, substitutions):
"""Interpolates variable references into |value| using |substitutions|.
Inputs:
plist: a dictionary representing a Property List (.plist) file
substitutions: a mapping of variable names to values
Returns:
A new plist with all variables references ${VARIABLES} replaced by their
value in |substitutions|. All values that contains references with no
substitutions will be removed and the corresponding key will be cleared
from the plist (not recursively).
"""
result = {}
for key in plist:
value = InterpolateValue(plist[key], substitutions)
if value is not None:
result[key] = value
return result
def LoadPList(path):
"""Loads Plist at |path| and returns it as a dictionary."""
fd, name = tempfile.mkstemp()
try:
subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path])
with os.fdopen(fd, 'r') as f:
return plistlib.readPlist(f)
finally:
os.unlink(name)
def SavePList(path, data):
"""Saves |data| as a Plist to |path| in binary1 format."""
fd, name = tempfile.mkstemp()
try:
with os.fdopen(fd, 'w') as f:
plistlib.writePlist(data, f)
subprocess.check_call(['plutil', '-convert', 'binary1', '-o', path, name])
finally:
os.unlink(name)
def MergePList(plist1, plist2):
"""Merges |plist1| with |plist2| recursively.
Creates a new dictionary representing a Property List (.plist) files by
merging the two dictionary |plist1| and |plist2| recursively (only for
dictionary values).
Args:
plist1: a dictionary representing a Property List (.plist) file
plist2: a dictionary representing a Property List (.plist) file
Returns:
A new dictionary representing a Property List (.plist) file by merging
|plist1| with |plist2|. If any value is a dictionary, they are merged
recursively, otherwise |plist2| value is used.
"""
if not isinstance(plist1, dict) or not isinstance(plist2, dict):
if plist2 is not None:
return plist2
else:
return plist1
result = {}
for key in set(plist1) | set(plist2):
if key in plist2:
value = plist2[key]
else:
value = plist1[key]
if isinstance(value, dict):
value = MergePList(plist1.get(key, None), plist2.get(key, None))
result[key] = value
return result
def main():
parser = ArgumentParser(
description='A script to generate iOS application Info.plist.',
fromfile_prefix_chars='@')
parser.add_argument('-o', '--output', required=True,
help='Path to output plist file.')
parser.add_argument('-s', '--subst', action='append', default=[],
help='Substitution rule in the format "key=value".')
parser.add_argument('path', nargs="+", help='Path to input plist files.')
args = parser.parse_args()
substitutions = {}
for subst in args.subst:
key, value = subst.split('=', 1)
substitutions[key] = value
data = {}
for filename in args.path:
data = MergePList(data, LoadPList(filename))
data = Interpolate(data, substitutions)
SavePList(args.output, data)
return 0
if __name__ == '__main__':
sys.exit(main())
|