Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(183)

Side by Side Diff: build/config/mac/gen_plist.py

Issue 2367923002: Fail to build with unbound Info.plist substitutions instead of silently dropping them. (Closed)
Patch Set: Add a temporary flag to go back to skipping unbound substitutions Created 4 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « build/config/mac/BuildInfo.plist ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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())
OLDNEW
« no previous file with comments | « build/config/mac/BuildInfo.plist ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698