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

Side by Side Diff: tools/isolate/merge_isolate.py

Issue 10019014: Convert isolate.py to exclusively use .isolate files. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Reduce copy pasted constants Created 8 years, 8 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 | Annotate | Revision Log
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Merges multiple OS-specific gyp dependency lists into one that works on all 6 """Merges multiple OS-specific gyp dependency lists into one that works on all
7 of them. 7 of them.
8 8
9 9
10 The logic is relatively simple. Takes the current conditions, add more 10 The logic is relatively simple. Takes the current conditions, add more
11 condition, find the strict subset. Done. 11 condition, find the strict subset. Done.
12 """ 12 """
13 13
14 import copy 14 import copy
15 import logging 15 import logging
16 import optparse 16 import optparse
17 import re 17 import re
18 import sys 18 import sys
19 19
20 import trace_inputs 20 import trace_inputs
21 # Create shortcuts.
22 from trace_inputs import KEY_TRACKED, KEY_UNTRACKED
21 23
22 24
23 def union(lhs, rhs): 25 def union(lhs, rhs):
24 """Merges two compatible datastructures composed of dict/list/set.""" 26 """Merges two compatible datastructures composed of dict/list/set."""
25 assert lhs is not None or rhs is not None 27 assert lhs is not None or rhs is not None
26 if lhs is None: 28 if lhs is None:
27 return copy.deepcopy(rhs) 29 return copy.deepcopy(rhs)
28 if rhs is None: 30 if rhs is None:
29 return copy.deepcopy(lhs) 31 return copy.deepcopy(lhs)
30 assert type(lhs) == type(rhs), (lhs, rhs) 32 assert type(lhs) == type(rhs), (lhs, rhs)
33 if hasattr(lhs, 'union'):
34 # Includes set, OSSettings and Configs.
35 return lhs.union(rhs)
31 if isinstance(lhs, dict): 36 if isinstance(lhs, dict):
32 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs)) 37 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
33 elif isinstance(lhs, set):
34 # Do not go inside the set.
35 return lhs.union(rhs)
36 elif isinstance(lhs, list): 38 elif isinstance(lhs, list):
37 # Do not go inside the list. 39 # Do not go inside the list.
38 return lhs + rhs 40 return lhs + rhs
39 assert False, type(lhs) 41 assert False, type(lhs)
40 42
41 43
42 def process_variables(for_os, variables):
43 """Extracts files and dirs from the |variables| dict.
44
45 Returns a list of exactly two items. Each item is a dict that maps a string
46 to a set (of strings).
47
48 In the first item, the keys are file names, and the values are sets of OS
49 names, like "win" or "mac". In the second item, the keys are directory names,
50 and the values are sets of OS names too.
51 """
52 VALID_VARIABLES = ['isolate_files', 'isolate_dirs']
53
54 # Verify strictness.
55 assert isinstance(variables, dict), variables
56 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
57 for items in variables.itervalues():
58 assert isinstance(items, list), items
59 assert all(isinstance(i, basestring) for i in items), items
60
61 # Returns [files, dirs]
62 return [
63 dict((name, set([for_os])) for name in variables.get(var, []))
64 for var in VALID_VARIABLES
65 ]
66
67
68 def eval_content(content): 44 def eval_content(content):
69 """Evaluates a GYP file and return the value defined in it.""" 45 """Evaluates a GYP file and return the value defined in it."""
70 globs = {'__builtins__': None} 46 globs = {'__builtins__': None}
71 locs = {} 47 locs = {}
72 value = eval(content, globs, locs) 48 value = eval(content, globs, locs)
73 assert locs == {}, locs 49 assert locs == {}, locs
74 assert globs == {'__builtins__': None}, globs 50 assert globs == {'__builtins__': None}, globs
75 return value 51 return value
76 52
77 53
78 def _process_inner(for_os, inner, old_files, old_dirs, old_os): 54 def verify_variables(variables):
79 """Processes the variables inside a condition. 55 """Verifies the |variables| dictionary is in the expected format."""
80 56 VALID_VARIABLES = [
81 Only meant to be called by parse_gyp_dict(). 57 KEY_TRACKED,
82 58 KEY_UNTRACKED,
83 Args: 59 'command',
84 - for_os: OS where the references are tracked for. 60 'read_only',
85 - inner: Inner dictionary to process. 61 ]
86 - old_files: Previous list of files to union with. 62 assert isinstance(variables, dict), variables
87 - old_dirs: Previous list of directories to union with. 63 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
88 - old_os: Previous list of OSes referenced to union with. 64 for name, value in variables.iteritems():
89 65 if name == 'read_only':
90 Returns: 66 assert value in (True, False, None), value
91 - A tuple of (files, dirs, os) where each list is a union of the new 67 else:
92 dependencies found for this OS, as referenced by for_os, and the previous 68 assert isinstance(value, list), value
93 list. 69 assert all(isinstance(i, basestring) for i in value), value
70
71
72 def verify_condition(condition):
73 """Verifies the |condition| dictionary is in the expected format."""
74 VALID_INSIDE_CONDITION = ['variables']
75 assert isinstance(condition, list), condition
76 assert 2 <= len(condition) <= 3, condition
77 assert re.match(r'OS==\"([a-z]+)\"', condition[0]), condition[0]
78 for c in condition[1:]:
79 assert isinstance(c, dict), c
80 assert set(VALID_INSIDE_CONDITION).issuperset(set(c)), c.keys()
81 verify_variables(c.get('variables', {}))
82
83
84 def verify_root(value):
85 VALID_ROOTS = ['variables', 'conditions']
86 assert isinstance(value, dict), value
87 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
88 verify_variables(value.get('variables', {}))
89
90 conditions = value.get('conditions', [])
91 assert isinstance(conditions, list), conditions
92 for condition in conditions:
93 verify_condition(condition)
94
95
96 class OSSettings(object):
97 """Represents the dependencies for an OS. The structure is immutable."""
98 def __init__(self, name, values):
99 self.name = name
100 verify_variables(values)
101 self.tracked = sorted(values.get(KEY_TRACKED, []))
102 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
103 self.command = values.get('command', [])[:]
104 self.read_only = values.get('read_only')
105
106 def union(self, rhs):
107 assert self.name == rhs.name
108 assert not (self.command and rhs.command)
109 var = {
110 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
111 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
112 'command': self.command or rhs.command,
113 'read_only': rhs.read_only if self.read_only is None else self.read_only,
114 }
115 return OSSettings(self.name, var)
116
117 def flatten(self):
118 out = {}
119 if self.command:
120 out['command'] = self.command
121 if self.tracked:
122 out[KEY_TRACKED] = self.tracked
123 if self.untracked:
124 out[KEY_UNTRACKED] = self.untracked
125 if self.read_only is not None:
126 out['read_only'] = self.read_only
127 return out
128
129
130 class Configs(object):
131 """Represents all the OS-specific configurations.
132
133 The self.per_os[None] member contains all the 'else' clauses plus the default
134 values. It is not included in the flatten() result.
94 """ 135 """
95 assert isinstance(inner, dict), inner 136 def __init__(self, oses):
96 assert set(['variables']).issuperset(set(inner)), inner.keys() 137 self.per_os = {
97 new_files, new_dirs = process_variables(for_os, inner.get('variables', {})) 138 None: OSSettings(None, {}),
98 if new_files or new_dirs: 139 }
99 old_os = old_os.union([for_os.lstrip('!')]) 140 self.per_os.update(dict((name, OSSettings(name, {})) for name in oses))
100 return union(old_files, new_files), union(old_dirs, new_dirs), old_os 141
101 142 def union(self, rhs):
102 143 out = Configs(list(set(self.per_os.keys() + rhs.per_os.keys())))
103 def parse_gyp_dict(value): 144 for value in self.per_os.itervalues():
104 """Parses a gyp dict as returned by eval_content(). 145 # TODO(maruel): FAIL
146 out = out.union(value)
147 for value in rhs.per_os.itervalues():
148 out = out.union(value)
149 return out
150
151 def add_globals(self, values):
152 for key in self.per_os:
153 self.per_os[key] = self.per_os[key].union(OSSettings(key, values))
154
155 def add_values(self, for_os, values):
156 self.per_os[for_os] = self.per_os[for_os].union(OSSettings(for_os, values))
157
158 def add_negative_values(self, for_os, values):
159 """Includes the variables to all OSes except |for_os|.
160
161 This includes 'None' so unknown OSes gets it too.
162 """
163 for key in self.per_os:
164 if key != for_os:
165 self.per_os[key] = self.per_os[key].union(OSSettings(key, values))
166
167 def flatten(self):
168 """Returns a flat dictionary representation of the configuration.
169
170 Skips None pseudo-OS.
171 """
172 return dict(
173 (k, v.flatten()) for k, v in self.per_os.iteritems() if k is not None)
174
175
176 def invert_map(variables):
177 """Converts a dict(OS, dict(deptype, list(dependencies)) to
178 dict(deptype, dict(dependency, set(OSes)) for easier processing.
Roger Tawa OOO till Jul 10th 2012/04/11 15:15:03 also returns 'OSes' key in dict. maybe the return
179 """
180 KEYS = (
181 KEY_TRACKED,
182 KEY_UNTRACKED,
183 'command',
184 'read_only',
185 )
186 out = dict((key, {}) for key in KEYS)
187 out['OSes'] = set(variables)
188 for os_name, values in variables.iteritems():
189 for key in (KEY_TRACKED, KEY_UNTRACKED):
190 for item in values.get(key, []):
191 out[key].setdefault(item, set()).add(os_name)
192
193 # command needs special handling.
194 command = tuple(values.get('command', []))
195 out['command'].setdefault(command, set()).add(os_name)
196
197 # read_only needs special handling.
198 out['read_only'].setdefault(values.get('read_only'), set()).add(os_name)
199 return out
200
201
202 def reduce_inputs(values):
203 """Reduces the invert_map() output to the strictest minimum list."""
204 # Construct the inverse map first.
205 # Look at each individual file and directory, map where they are used and
206 # reconstruct the inverse dictionary.
207 # Do not convert back to negative if only 2 OSes were merged.
208 KEYS = (
209 KEY_TRACKED,
210 KEY_UNTRACKED,
211 'command',
212 'read_only',
213 )
214 out = dict((key, {}) for key in KEYS)
215 oses = out['OSes'] = set(values['OSes'])
216 assert None not in oses, oses
217 if len(oses) > 2:
218 for key in KEYS:
219 for item, item_oses in values.get(key, {}).iteritems():
220 # Converts all oses.difference('foo') to '!foo'.
221 assert None not in item_oses, item_oses
222 missing = oses.difference(item_oses)
223 if len(missing) == 1:
224 # Replace it with a negative.
225 out[key][item] = set(['!' + tuple(missing)[0]])
226 elif not missing:
227 out[key][item] = set([None])
228 else:
229 out[key][item] = set(item_oses)
230 return out
231
232
233 def convert_map_to_gyp(values):
234 """Regenerates back a gyp-like configuration dict from files and dirs
235 mappings generated from reduce_inputs()."""
236 # First, inverse the mapping to make it dict first.
237 oses = values['OSes']
238 config = {}
239 for key in values:
240 if key == 'OSes':
241 continue
242 for item, oses in values[key].iteritems():
243 if item is None:
244 # For read_only default.
245 continue
246 for cond_os in oses:
247 cond_key = None if cond_os is None else cond_os.lstrip('!')
248 # Insert the if/else dicts.
249 condition_values = config.setdefault(cond_key, [{}, {}])
250 # If condition is negative, use index 1, else use index 0.
251 cond_value = condition_values[int((cond_os or '').startswith('!'))]
252 variables = cond_value.setdefault('variables', {})
253
254 if item in (True, False):
255 # One-off for read_only.
256 variables[key] = item
257 else:
258 if isinstance(item, tuple):
259 # One-off for command.
260 # Do not merge lists and do not sort!
261 # Note that item is a tuple.
262 assert key not in variables
263 variables[key] = list(item)
264 else:
265 # The list of items (files or dirs). Append the new item and keep
266 # the list sorted.
267 l = variables.setdefault(key, [])
268 l.append(item)
269 l.sort()
270
271 out = {}
272 for o in sorted(config):
273 d = config[o]
274 if o is None:
275 assert not d[1]
276 out = union(out, d[0])
277 else:
278 c = out.setdefault('conditions', [])
279 if d[1]:
280 c.append(['OS=="%s"' % o] + d)
281 else:
282 c.append(['OS=="%s"' % o] + d[0:1])
283 return out
284
285
286 def load_gyp(value):
287 """Parses one gyp skeleton and returns a Configs() instance.
105 288
106 |value| is the loaded dictionary that was defined in the gyp file. 289 |value| is the loaded dictionary that was defined in the gyp file.
107 290
108 Returns a 3-tuple, where the first two items are the same as the items
109 returned by process_variable() in the same order, and the last item is a set
110 of strings of all OSs seen in the input dict.
111
112 The expected format is strict, anything diverting from the format below will 291 The expected format is strict, anything diverting from the format below will
113 fail: 292 throw an assert:
114 { 293 {
115 'variables': { 294 'variables': {
116 'isolate_files': [ 295 'command': [
117 ... 296 ...
118 ], 297 ],
119 'isolate_dirs: [ 298 'isolate_dependency_tracked': [
120 ... 299 ...
121 ], 300 ],
301 'isolate_dependency_untracked': [
302 ...
303 ],
304 'read_only': False,
122 }, 305 },
123 'conditions': [ 306 'conditions': [
124 ['OS=="<os>"', { 307 ['OS=="<os>"', {
125 'variables': { 308 'variables': {
126 ... 309 ...
127 }, 310 },
128 }, { # else 311 }, { # else
129 'variables': { 312 'variables': {
130 ... 313 ...
131 }, 314 },
132 }], 315 }],
133 ... 316 ...
134 ], 317 ],
135 } 318 }
136 """ 319 """
137 assert isinstance(value, dict), value 320 verify_root(value)
138 VALID_ROOTS = ['variables', 'conditions'] 321
139 assert set(VALID_ROOTS).issuperset(set(value)), value.keys() 322 # Scan to get the list of OSes.
323 conditions = value.get('conditions', [])
324 oses = set(re.match(r'OS==\"([a-z]+)\"', c[0]).group(1) for c in conditions)
325 configs = Configs(oses)
140 326
141 # Global level variables. 327 # Global level variables.
142 oses = set() 328 configs.add_globals(value.get('variables', {}))
143 files, dirs = process_variables(None, value.get('variables', {}))
144 329
145 # OS specific variables. 330 # OS specific variables.
146 conditions = value.get('conditions', [])
147 assert isinstance(conditions, list), conditions
148 for condition in conditions: 331 for condition in conditions:
149 assert isinstance(condition, list), condition 332 condition_os = re.match(r'OS==\"([a-z]+)\"', condition[0]).group(1)
150 assert 2 <= len(condition) <= 3, condition 333 configs.add_values(condition_os, condition[1].get('variables', {}))
151 m = re.match(r'OS==\"([a-z]+)\"', condition[0]) 334 if len(condition) > 2:
152 assert m, condition[0] 335 configs.add_negative_values(
153 condition_os = m.group(1) 336 condition_os, condition[2].get('variables', {}))
154 337 return configs
155 files, dirs, oses = _process_inner(
156 condition_os, condition[1], files, dirs, oses)
157
158 if len(condition) == 3:
159 files, dirs, oses = _process_inner(
160 '!' + condition_os, condition[2], files, dirs, oses)
161
162 # TODO(maruel): _expand_negative() should be called here, because otherwise
163 # the OSes the negative condition represents is lost once the gyps are merged.
164 # This cause an invalid expansion in reduce_inputs() call.
165 return files, dirs, oses
166 338
167 339
168 def parse_gyp_dicts(gyps): 340 def load_gyps(items):
169 """Parses each gyp file and returns the merged results. 341 """Parses each gyp file and returns the merged results.
170 342
171 It only loads what parse_gyp_dict() can process. 343 It only loads what load_gyp() can process.
172 344
173 Return values: 345 Return values:
174 files: dict(filename, set(OS where this filename is a dependency)) 346 files: dict(filename, set(OS where this filename is a dependency))
175 dirs: dict(dirame, set(OS where this dirname is a dependency)) 347 dirs: dict(dirame, set(OS where this dirname is a dependency))
176 oses: set(all the OSes referenced) 348 oses: set(all the OSes referenced)
177 """ 349 """
178 files = {} 350 configs = Configs([])
179 dirs = {} 351 for item in items:
180 oses = set() 352 configs = configs.union(load_gyp(eval_content(open(item, 'rb').read())))
181 for gyp in gyps: 353 return configs
182 with open(gyp, 'rb') as gyp_file:
183 content = gyp_file.read()
184 gyp_files, gyp_dirs, gyp_oses = parse_gyp_dict(eval_content(content))
185 files = union(gyp_files, files)
186 dirs = union(gyp_dirs, dirs)
187 oses |= gyp_oses
188 return files, dirs, oses
189 354
190 355
191 def _expand_negative(items, oses): 356 def main(args=None):
192 """Converts all '!foo' value in the set by oses.difference('foo')."""
193 assert None not in oses and len(oses) >= 2, oses
194 for name in items:
195 if None in items[name]:
196 # Shortcut any item having None in their set. An item listed in None means
197 # the item is a dependency on all OSes. As such, there is no need to list
198 # any OS.
199 items[name] = set([None])
200 continue
201 for neg in [o for o in items[name] if o.startswith('!')]:
202 # Replace it with the inverse.
203 items[name] = items[name].union(oses.difference([neg[1:]]))
204 items[name].remove(neg)
205 if items[name] == oses:
206 items[name] = set([None])
207
208
209 def _compact_negative(items, oses):
210 """Converts all oses.difference('foo') to '!foo'.
211
212 It is doing the reverse of _expand_negative().
213 """
214 assert None not in oses and len(oses) >= 3, oses
215 for name in items:
216 missing = oses.difference(items[name])
217 if len(missing) == 1:
218 # Replace it with a negative.
219 items[name] = set(['!' + tuple(missing)[0]])
220
221
222 def reduce_inputs(files, dirs, oses):
223 """Reduces the variables to their strictest minimum."""
224 # Construct the inverse map first.
225 # Look at each individual file and directory, map where they are used and
226 # reconstruct the inverse dictionary.
227 # First, expands all '!' builders into the reverse.
228 # TODO(maruel): This is too late to call _expand_negative(). The exact list
229 # negative OSes condition it represents is lost at that point.
230 _expand_negative(files, oses)
231 _expand_negative(dirs, oses)
232
233 # Do not convert back to negative if only 2 OSes were merged. It is easier to
234 # read this way.
235 if len(oses) > 2:
236 _compact_negative(files, oses)
237 _compact_negative(dirs, oses)
238
239 return files, dirs
240
241
242 def convert_to_gyp(files, dirs):
243 """Regenerates back a gyp-like configuration dict from files and dirs
244 mappings.
245
246 Sort the lists.
247 """
248 # First, inverse the mapping to make it dict first.
249 config = {}
250 def to_cond(items, name):
251 for item, oses in items.iteritems():
252 for cond_os in oses:
253 condition_values = config.setdefault(
254 None if cond_os is None else cond_os.lstrip('!'),
255 [{}, {}])
256 # If condition is negative, use index 1, else use index 0.
257 condition_value = condition_values[int((cond_os or '').startswith('!'))]
258 # The list of items (files or dirs). Append the new item and keep the
259 # list sorted.
260 l = condition_value.setdefault('variables', {}).setdefault(name, [])
261 l.append(item)
262 l.sort()
263
264 to_cond(files, 'isolate_files')
265 to_cond(dirs, 'isolate_dirs')
266
267 out = {}
268 for o in sorted(config):
269 d = config[o]
270 if o is None:
271 assert not d[1]
272 out = union(out, d[0])
273 else:
274 c = out.setdefault('conditions', [])
275 if d[1]:
276 c.append(['OS=="%s"' % o] + d)
277 else:
278 c.append(['OS=="%s"' % o] + d[0:1])
279 return out
280
281
282 def main():
283 parser = optparse.OptionParser( 357 parser = optparse.OptionParser(
284 usage='%prog <options> [file1] [file2] ...') 358 usage='%prog <options> [file1] [file2] ...')
285 parser.add_option( 359 parser.add_option(
286 '-v', '--verbose', action='count', default=0, help='Use multiple times') 360 '-v', '--verbose', action='count', default=0, help='Use multiple times')
287 361
288 options, args = parser.parse_args() 362 options, args = parser.parse_args(args)
289 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)] 363 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
290 logging.basicConfig( 364 logging.basicConfig(
291 level=level, 365 level=level,
292 format='%(levelname)5s %(module)15s(%(lineno)3d):%(message)s') 366 format='%(levelname)5s %(module)15s(%(lineno)3d):%(message)s')
293 367
294 trace_inputs.pretty_print( 368 trace_inputs.pretty_print(
295 convert_to_gyp(*reduce_inputs(*parse_gyp_dicts(args))), 369 convert_map_to_gyp(reduce_inputs(invert_map(load_gyps(args).flatten()))),
296 sys.stdout) 370 sys.stdout)
297 return 0 371 return 0
298 372
299 373
300 if __name__ == '__main__': 374 if __name__ == '__main__':
301 sys.exit(main()) 375 sys.exit(main())
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698