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

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: Do not read as binary 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
« no previous file with comments | « tools/isolate/isolate_test.py ('k') | tools/isolate/merge_isolate_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
94 """ 70
95 assert isinstance(inner, dict), inner 71
96 assert set(['variables']).issuperset(set(inner)), inner.keys() 72 def verify_condition(condition):
97 new_files, new_dirs = process_variables(for_os, inner.get('variables', {})) 73 """Verifies the |condition| dictionary is in the expected format."""
98 if new_files or new_dirs: 74 VALID_INSIDE_CONDITION = ['variables']
99 old_os = old_os.union([for_os.lstrip('!')]) 75 assert isinstance(condition, list), condition
100 return union(old_files, new_files), union(old_dirs, new_dirs), old_os 76 assert 2 <= len(condition) <= 3, condition
101 77 assert re.match(r'OS==\"([a-z]+)\"', condition[0]), condition[0]
102 78 for c in condition[1:]:
103 def parse_gyp_dict(value): 79 assert isinstance(c, dict), c
104 """Parses a gyp dict as returned by eval_content(). 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.
135 """
136 def __init__(self, oses):
137 self.per_os = {
138 None: OSSettings(None, {}),
139 }
140 self.per_os.update(dict((name, OSSettings(name, {})) for name in oses))
141
142 def union(self, rhs):
143 out = Configs(list(set(self.per_os.keys() + rhs.per_os.keys())))
144 for value in self.per_os.itervalues():
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 a flattened view.
178
179 Returns a tuple of:
180 1. dict(deptype, dict(dependency, set(OSes)) for easier processing.
181 2. All the OSes found as a set.
182 """
183 KEYS = (
184 KEY_TRACKED,
185 KEY_UNTRACKED,
186 'command',
187 'read_only',
188 )
189 out = dict((key, {}) for key in KEYS)
190 for os_name, values in variables.iteritems():
191 for key in (KEY_TRACKED, KEY_UNTRACKED):
192 for item in values.get(key, []):
193 out[key].setdefault(item, set()).add(os_name)
194
195 # command needs special handling.
196 command = tuple(values.get('command', []))
197 out['command'].setdefault(command, set()).add(os_name)
198
199 # read_only needs special handling.
200 out['read_only'].setdefault(values.get('read_only'), set()).add(os_name)
201 return out, set(variables)
202
203
204 def reduce_inputs(values, oses):
205 """Reduces the invert_map() output to the strictest minimum list.
206
207 1. Construct the inverse map first.
208 2. Look at each individual file and directory, map where they are used and
209 reconstruct the inverse dictionary.
210 3. Do not convert back to negative if only 2 OSes were merged.
211
212 Returns a tuple of:
213 1. the minimized dictionary
214 2. oses passed through as-is.
215 """
216 KEYS = (
217 KEY_TRACKED,
218 KEY_UNTRACKED,
219 'command',
220 'read_only',
221 )
222 out = dict((key, {}) for key in KEYS)
223 assert all(oses), oses
224 if len(oses) > 2:
225 for key in KEYS:
226 for item, item_oses in values.get(key, {}).iteritems():
227 # Converts all oses.difference('foo') to '!foo'.
228 assert all(item_oses), item_oses
229 missing = oses.difference(item_oses)
230 if len(missing) == 1:
231 # Replace it with a negative.
232 out[key][item] = set(['!' + tuple(missing)[0]])
233 elif not missing:
234 out[key][item] = set([None])
235 else:
236 out[key][item] = set(item_oses)
237 return out, oses
238
239
240 def convert_map_to_gyp(values, oses):
241 """Regenerates back a gyp-like configuration dict from files and dirs
242 mappings generated from reduce_inputs().
243 """
244 # First, inverse the mapping to make it dict first.
245 config = {}
246 for key in values:
247 for item, oses in values[key].iteritems():
248 if item is None:
249 # For read_only default.
250 continue
251 for cond_os in oses:
252 cond_key = None if cond_os is None else cond_os.lstrip('!')
253 # Insert the if/else dicts.
254 condition_values = config.setdefault(cond_key, [{}, {}])
255 # If condition is negative, use index 1, else use index 0.
256 cond_value = condition_values[int((cond_os or '').startswith('!'))]
257 variables = cond_value.setdefault('variables', {})
258
259 if item in (True, False):
260 # One-off for read_only.
261 variables[key] = item
262 else:
263 if isinstance(item, tuple):
264 # One-off for command.
265 # Do not merge lists and do not sort!
266 # Note that item is a tuple.
267 assert key not in variables
268 variables[key] = list(item)
269 else:
270 # The list of items (files or dirs). Append the new item and keep
271 # the list sorted.
272 l = variables.setdefault(key, [])
273 l.append(item)
274 l.sort()
275
276 out = {}
277 for o in sorted(config):
278 d = config[o]
279 if o is None:
280 assert not d[1]
281 out = union(out, d[0])
282 else:
283 c = out.setdefault('conditions', [])
284 if d[1]:
285 c.append(['OS=="%s"' % o] + d)
286 else:
287 c.append(['OS=="%s"' % o] + d[0:1])
288 return out
289
290
291 def load_gyp(value):
292 """Parses one gyp skeleton and returns a Configs() instance.
105 293
106 |value| is the loaded dictionary that was defined in the gyp file. 294 |value| is the loaded dictionary that was defined in the gyp file.
107 295
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 296 The expected format is strict, anything diverting from the format below will
113 fail: 297 throw an assert:
114 { 298 {
115 'variables': { 299 'variables': {
116 'isolate_files': [ 300 'command': [
117 ... 301 ...
118 ], 302 ],
119 'isolate_dirs: [ 303 'isolate_dependency_tracked': [
120 ... 304 ...
121 ], 305 ],
306 'isolate_dependency_untracked': [
307 ...
308 ],
309 'read_only': False,
122 }, 310 },
123 'conditions': [ 311 'conditions': [
124 ['OS=="<os>"', { 312 ['OS=="<os>"', {
125 'variables': { 313 'variables': {
126 ... 314 ...
127 }, 315 },
128 }, { # else 316 }, { # else
129 'variables': { 317 'variables': {
130 ... 318 ...
131 }, 319 },
132 }], 320 }],
133 ... 321 ...
134 ], 322 ],
135 } 323 }
136 """ 324 """
137 assert isinstance(value, dict), value 325 verify_root(value)
138 VALID_ROOTS = ['variables', 'conditions'] 326
139 assert set(VALID_ROOTS).issuperset(set(value)), value.keys() 327 # Scan to get the list of OSes.
328 conditions = value.get('conditions', [])
329 oses = set(re.match(r'OS==\"([a-z]+)\"', c[0]).group(1) for c in conditions)
330 configs = Configs(oses)
140 331
141 # Global level variables. 332 # Global level variables.
142 oses = set() 333 configs.add_globals(value.get('variables', {}))
143 files, dirs = process_variables(None, value.get('variables', {}))
144 334
145 # OS specific variables. 335 # OS specific variables.
146 conditions = value.get('conditions', [])
147 assert isinstance(conditions, list), conditions
148 for condition in conditions: 336 for condition in conditions:
149 assert isinstance(condition, list), condition 337 condition_os = re.match(r'OS==\"([a-z]+)\"', condition[0]).group(1)
150 assert 2 <= len(condition) <= 3, condition 338 configs.add_values(condition_os, condition[1].get('variables', {}))
151 m = re.match(r'OS==\"([a-z]+)\"', condition[0]) 339 if len(condition) > 2:
152 assert m, condition[0] 340 configs.add_negative_values(
153 condition_os = m.group(1) 341 condition_os, condition[2].get('variables', {}))
154 342 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 343
167 344
168 def parse_gyp_dicts(gyps): 345 def load_gyps(items):
169 """Parses each gyp file and returns the merged results. 346 """Parses each gyp file and returns the merged results.
170 347
171 It only loads what parse_gyp_dict() can process. 348 It only loads what load_gyp() can process.
172 349
173 Return values: 350 Return values:
174 files: dict(filename, set(OS where this filename is a dependency)) 351 files: dict(filename, set(OS where this filename is a dependency))
175 dirs: dict(dirame, set(OS where this dirname is a dependency)) 352 dirs: dict(dirame, set(OS where this dirname is a dependency))
176 oses: set(all the OSes referenced) 353 oses: set(all the OSes referenced)
177 """ 354 """
178 files = {} 355 configs = Configs([])
179 dirs = {} 356 for item in items:
180 oses = set() 357 configs = configs.union(load_gyp(eval_content(open(item, 'rb').read())))
181 for gyp in gyps: 358 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 359
190 360
191 def _expand_negative(items, oses): 361 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( 362 parser = optparse.OptionParser(
284 usage='%prog <options> [file1] [file2] ...') 363 usage='%prog <options> [file1] [file2] ...')
285 parser.add_option( 364 parser.add_option(
286 '-v', '--verbose', action='count', default=0, help='Use multiple times') 365 '-v', '--verbose', action='count', default=0, help='Use multiple times')
287 366
288 options, args = parser.parse_args() 367 options, args = parser.parse_args(args)
289 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)] 368 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
290 logging.basicConfig( 369 logging.basicConfig(
291 level=level, 370 level=level,
292 format='%(levelname)5s %(module)15s(%(lineno)3d):%(message)s') 371 format='%(levelname)5s %(module)15s(%(lineno)3d):%(message)s')
293 372
294 trace_inputs.pretty_print( 373 trace_inputs.pretty_print(
295 convert_to_gyp(*reduce_inputs(*parse_gyp_dicts(args))), 374 convert_map_to_gyp(
375 *reduce_inputs(
376 *invert_map(
377 load_gyps(args).flatten()))),
296 sys.stdout) 378 sys.stdout)
297 return 0 379 return 0
298 380
299 381
300 if __name__ == '__main__': 382 if __name__ == '__main__':
301 sys.exit(main()) 383 sys.exit(main())
OLDNEW
« no previous file with comments | « tools/isolate/isolate_test.py ('k') | tools/isolate/merge_isolate_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698