blob: e2028ebe4d4b42f2da9ddd0d760c6bcd3a6fecf5 [file] [log] [blame]
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001# Copyright 2014 The Swarming Authors. All rights reserved.
2# Use of this source code is governed under the Apache License, Version 2.0 that
3# can be found in the LICENSE file.
4
5"""Contains logic to parse .isolate files.
6
7This module doesn't touch the file system. It's the job of the client code to do
8I/O on behalf of this module.
9
10See more information at
11 https://code.google.com/p/swarming/wiki/IsolateDesign
12 https://code.google.com/p/swarming/wiki/IsolateUserGuide
13"""
14
15import ast
16import copy
17import itertools
18import logging
19import os
20import re
21
22import isolateserver
23
24from utils import short_expression_finder
25
26# Files that should be 0-length when mapped.
27KEY_TOUCHED = 'isolate_dependency_touched'
28# Files that should be tracked by the build tool.
29KEY_TRACKED = 'isolate_dependency_tracked'
30# Files that should not be tracked by the build tool.
31KEY_UNTRACKED = 'isolate_dependency_untracked'
32
33# Valid variable name.
34VALID_VARIABLE = '[A-Za-z_][A-Za-z_0-9]*'
35
36
37def determine_root_dir(relative_root, infiles):
38 """For a list of infiles, determines the deepest root directory that is
39 referenced indirectly.
40
41 All arguments must be using os.path.sep.
42 """
43 # The trick used to determine the root directory is to look at "how far" back
44 # up it is looking up.
45 deepest_root = relative_root
46 for i in infiles:
47 x = relative_root
48 while i.startswith('..' + os.path.sep):
49 i = i[3:]
50 assert not i.startswith(os.path.sep)
51 x = os.path.dirname(x)
52 if deepest_root.startswith(x):
53 deepest_root = x
54 logging.debug(
55 'determine_root_dir(%s, %d files) -> %s' % (
56 relative_root, len(infiles), deepest_root))
57 return deepest_root
58
59
60def replace_variable(part, variables):
61 m = re.match(r'<\((' + VALID_VARIABLE + ')\)', part)
62 if m:
63 if m.group(1) not in variables:
64 raise isolateserver.ConfigError(
65 'Variable "%s" was not found in %s.\nDid you forget to specify '
66 '--path-variable?' % (m.group(1), variables))
67 return variables[m.group(1)]
68 return part
69
70
71def eval_variables(item, variables):
72 """Replaces the .isolate variables in a string item.
73
74 Note that the .isolate format is a subset of the .gyp dialect.
75 """
76 return ''.join(
77 replace_variable(p, variables)
78 for p in re.split(r'(<\(' + VALID_VARIABLE + '\))', item))
79
80
81def split_touched(files):
82 """Splits files that are touched vs files that are read."""
83 tracked = []
84 touched = []
85 for f in files:
86 if f.size:
87 tracked.append(f)
88 else:
89 touched.append(f)
90 return tracked, touched
91
92
93def pretty_print(variables, stdout):
94 """Outputs a gyp compatible list from the decoded variables.
95
96 Similar to pprint.print() but with NIH syndrome.
97 """
98 # Order the dictionary keys by these keys in priority.
99 ORDER = (
100 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
101 KEY_TRACKED, KEY_UNTRACKED)
102
103 def sorting_key(x):
104 """Gives priority to 'most important' keys before the others."""
105 if x in ORDER:
106 return str(ORDER.index(x))
107 return x
108
109 def loop_list(indent, items):
110 for item in items:
111 if isinstance(item, basestring):
112 stdout.write('%s\'%s\',\n' % (indent, item))
113 elif isinstance(item, dict):
114 stdout.write('%s{\n' % indent)
115 loop_dict(indent + ' ', item)
116 stdout.write('%s},\n' % indent)
117 elif isinstance(item, list):
118 # A list inside a list will write the first item embedded.
119 stdout.write('%s[' % indent)
120 for index, i in enumerate(item):
121 if isinstance(i, basestring):
122 stdout.write(
123 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
124 elif isinstance(i, dict):
125 stdout.write('{\n')
126 loop_dict(indent + ' ', i)
127 if index != len(item) - 1:
128 x = ', '
129 else:
130 x = ''
131 stdout.write('%s}%s' % (indent, x))
132 else:
133 assert False
134 stdout.write('],\n')
135 else:
136 assert False
137
138 def loop_dict(indent, items):
139 for key in sorted(items, key=sorting_key):
140 item = items[key]
141 stdout.write("%s'%s': " % (indent, key))
142 if isinstance(item, dict):
143 stdout.write('{\n')
144 loop_dict(indent + ' ', item)
145 stdout.write(indent + '},\n')
146 elif isinstance(item, list):
147 stdout.write('[\n')
148 loop_list(indent + ' ', item)
149 stdout.write(indent + '],\n')
150 elif isinstance(item, basestring):
151 stdout.write(
152 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500153 elif isinstance(item, (int, bool)) or item is None:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500154 stdout.write('%s\n' % item)
155 else:
156 assert False, item
157
158 stdout.write('{\n')
159 loop_dict(' ', variables)
160 stdout.write('}\n')
161
162
163def print_all(comment, data, stream):
164 """Prints a complete .isolate file and its top-level file comment into a
165 stream.
166 """
167 if comment:
168 stream.write(comment)
169 pretty_print(data, stream)
170
171
172def union(lhs, rhs):
173 """Merges two compatible datastructures composed of dict/list/set."""
174 assert lhs is not None or rhs is not None
175 if lhs is None:
176 return copy.deepcopy(rhs)
177 if rhs is None:
178 return copy.deepcopy(lhs)
179 assert type(lhs) == type(rhs), (lhs, rhs)
180 if hasattr(lhs, 'union'):
181 # Includes set, ConfigSettings and Configs.
182 return lhs.union(rhs)
183 if isinstance(lhs, dict):
184 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
185 elif isinstance(lhs, list):
186 # Do not go inside the list.
187 return lhs + rhs
188 assert False, type(lhs)
189
190
191def extract_comment(content):
192 """Extracts file level comment."""
193 out = []
194 for line in content.splitlines(True):
195 if line.startswith('#'):
196 out.append(line)
197 else:
198 break
199 return ''.join(out)
200
201
202def eval_content(content):
203 """Evaluates a python file and return the value defined in it.
204
205 Used in practice for .isolate files.
206 """
207 globs = {'__builtins__': None}
208 locs = {}
209 try:
210 value = eval(content, globs, locs)
211 except TypeError as e:
212 e.args = list(e.args) + [content]
213 raise
214 assert locs == {}, locs
215 assert globs == {'__builtins__': None}, globs
216 return value
217
218
219def match_configs(expr, config_variables, all_configs):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500220 """Returns the list of values from |values| that match the condition |expr|.
221
222 Arguments:
223 expr: string that is evaluatable with eval(). It is a GYP condition.
224 config_variables: list of the name of the variables.
225 all_configs: list of the list of possible values.
226
227 If a variable is not referenced at all, it is marked as unbounded (free) with
228 a value set to None.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500229 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500230 # It is more than just eval'ing the variable, it needs to be double checked to
231 # see if the variable is referenced at all. If not, the variable is free
232 # (unbounded).
233 # TODO(maruel): Use the intelligent way by inspecting expr instead of doing
234 # trial and error to figure out which variable is bound.
235 combinations = []
236 for bound_variables in itertools.product(
237 (True, False), repeat=len(config_variables)):
238 # Add the combination of variables bound.
239 combinations.append(
240 (
241 [c for c, b in zip(config_variables, bound_variables) if b],
242 set(
243 tuple(v if b else None for v, b in zip(line, bound_variables))
244 for line in all_configs)
245 ))
246
247 out = []
248 for variables, configs in combinations:
249 # Strip variables and see if expr can still be evaluated.
250 for values in configs:
251 globs = {'__builtins__': None}
252 globs.update(zip(variables, (v for v in values if v is not None)))
253 try:
254 assertion = eval(expr, globs, {})
255 except NameError:
256 continue
257 if not isinstance(assertion, bool):
258 raise isolateserver.ConfigError('Invalid condition')
259 if assertion:
260 out.append(values)
261 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500262
263
264def verify_variables(variables):
265 """Verifies the |variables| dictionary is in the expected format."""
266 VALID_VARIABLES = [
267 KEY_TOUCHED,
268 KEY_TRACKED,
269 KEY_UNTRACKED,
270 'command',
271 'read_only',
272 ]
273 assert isinstance(variables, dict), variables
274 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
275 for name, value in variables.iteritems():
276 if name == 'read_only':
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500277 assert value in (0, 1, 2, None), value
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500278 else:
279 assert isinstance(value, list), value
280 assert all(isinstance(i, basestring) for i in value), value
281
282
283def verify_ast(expr, variables_and_values):
284 """Verifies that |expr| is of the form
285 expr ::= expr ( "or" | "and" ) expr
286 | identifier "==" ( string | int )
287 Also collects the variable identifiers and string/int values in the dict
288 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
289 """
290 assert isinstance(expr, (ast.BoolOp, ast.Compare))
291 if isinstance(expr, ast.BoolOp):
292 assert isinstance(expr.op, (ast.And, ast.Or))
293 for subexpr in expr.values:
294 verify_ast(subexpr, variables_and_values)
295 else:
296 assert isinstance(expr.left.ctx, ast.Load)
297 assert len(expr.ops) == 1
298 assert isinstance(expr.ops[0], ast.Eq)
299 var_values = variables_and_values.setdefault(expr.left.id, set())
300 rhs = expr.comparators[0]
301 assert isinstance(rhs, (ast.Str, ast.Num))
302 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
303
304
305def verify_condition(condition, variables_and_values):
306 """Verifies the |condition| dictionary is in the expected format.
307 See verify_ast() for the meaning of |variables_and_values|.
308 """
309 VALID_INSIDE_CONDITION = ['variables']
310 assert isinstance(condition, list), condition
311 assert len(condition) == 2, condition
312 expr, then = condition
313
314 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
315 verify_ast(test_ast.body, variables_and_values)
316
317 assert isinstance(then, dict), then
318 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
319 if not 'variables' in then:
320 raise isolateserver.ConfigError('Missing \'variables\' in condition %s' %
321 condition)
322 verify_variables(then['variables'])
323
324
325def verify_root(value, variables_and_values):
326 """Verifies that |value| is the parsed form of a valid .isolate file.
327 See verify_ast() for the meaning of |variables_and_values|.
328 """
329 VALID_ROOTS = ['includes', 'conditions']
330 assert isinstance(value, dict), value
331 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
332
333 includes = value.get('includes', [])
334 assert isinstance(includes, list), includes
335 for include in includes:
336 assert isinstance(include, basestring), include
337
338 conditions = value.get('conditions', [])
339 assert isinstance(conditions, list), conditions
340 for condition in conditions:
341 verify_condition(condition, variables_and_values)
342
343
344def remove_weak_dependencies(values, key, item, item_configs):
345 """Removes any configs from this key if the item is already under a
346 strong key.
347 """
348 if key == KEY_TOUCHED:
349 item_configs = set(item_configs)
350 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
351 try:
352 item_configs -= values[stronger_key][item]
353 except KeyError:
354 pass
355
356 return item_configs
357
358
359def remove_repeated_dependencies(folders, key, item, item_configs):
360 """Removes any configs from this key if the item is in a folder that is
361 already included."""
362
363 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
364 item_configs = set(item_configs)
365 for (folder, configs) in folders.iteritems():
366 if folder != item and item.startswith(folder):
367 item_configs -= configs
368
369 return item_configs
370
371
372def get_folders(values_dict):
373 """Returns a dict of all the folders in the given value_dict."""
374 return dict(
375 (item, configs) for (item, configs) in values_dict.iteritems()
376 if item.endswith('/')
377 )
378
379
380def invert_map(variables):
381 """Converts {config: {deptype: list(depvals)}} to
382 {deptype: {depval: set(configs)}}.
383 """
384 KEYS = (
385 KEY_TOUCHED,
386 KEY_TRACKED,
387 KEY_UNTRACKED,
388 'command',
389 'read_only',
390 )
391 out = dict((key, {}) for key in KEYS)
392 for config, values in variables.iteritems():
393 for key in KEYS:
394 if key == 'command':
395 items = [tuple(values[key])] if key in values else []
396 elif key == 'read_only':
397 items = [values[key]] if key in values else []
398 else:
399 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
400 items = values.get(key, [])
401 for item in items:
402 out[key].setdefault(item, set()).add(config)
403 return out
404
405
406def reduce_inputs(values):
407 """Reduces the output of invert_map() to the strictest minimum list.
408
409 Looks at each individual file and directory, maps where they are used and
410 reconstructs the inverse dictionary.
411
412 Returns the minimized dictionary.
413 """
414 KEYS = (
415 KEY_TOUCHED,
416 KEY_TRACKED,
417 KEY_UNTRACKED,
418 'command',
419 'read_only',
420 )
421
422 # Folders can only live in KEY_UNTRACKED.
423 folders = get_folders(values.get(KEY_UNTRACKED, {}))
424
425 out = dict((key, {}) for key in KEYS)
426 for key in KEYS:
427 for item, item_configs in values.get(key, {}).iteritems():
428 item_configs = remove_weak_dependencies(values, key, item, item_configs)
429 item_configs = remove_repeated_dependencies(
430 folders, key, item, item_configs)
431 if item_configs:
432 out[key][item] = item_configs
433 return out
434
435
436def convert_map_to_isolate_dict(values, config_variables):
437 """Regenerates back a .isolate configuration dict from files and dirs
438 mappings generated from reduce_inputs().
439 """
440 # Gather a list of configurations for set inversion later.
441 all_mentioned_configs = set()
442 for configs_by_item in values.itervalues():
443 for configs in configs_by_item.itervalues():
444 all_mentioned_configs.update(configs)
445
446 # Invert the mapping to make it dict first.
447 conditions = {}
448 for key in values:
449 for item, configs in values[key].iteritems():
450 then = conditions.setdefault(frozenset(configs), {})
451 variables = then.setdefault('variables', {})
452
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500453 if key == 'read_only':
454 if not isinstance(item, int):
455 raise isolateserver.ConfigError(
456 'Unexpected entry type %r for key %s' % (item, key))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500457 variables[key] = item
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500458 elif key == 'command':
459 if not isinstance(item, tuple):
460 raise isolateserver.ConfigError(
461 'Unexpected entry type %r for key %s' % (item, key))
462 if key in variables:
463 raise isolateserver.ConfigError('Unexpected duplicate key %s' % key)
464 if not item:
465 raise isolateserver.ConfigError(
466 'Expected non empty entry in %s' % key)
467 variables[key] = list(item)
468 elif key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED):
469 if not isinstance(item, basestring):
470 raise isolateserver.ConfigError('Unexpected entry type %r' % item)
471 if not item:
472 raise isolateserver.ConfigError(
473 'Expected non empty entry in %s' % key)
474 # The list of items (files or dirs). Append the new item and keep
475 # the list sorted.
476 l = variables.setdefault(key, [])
477 l.append(item)
478 l.sort()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500479 else:
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500480 raise isolateserver.ConfigError('Unexpected key %s' % key)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500481
482 if all_mentioned_configs:
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500483 # Change [(1, 2), (3, 4)] to [set(1, 3), set(2, 4)]
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500484 config_values = map(set, zip(*all_mentioned_configs))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500485 for i in config_values:
486 i.discard(None)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500487 sef = short_expression_finder.ShortExpressionFinder(
488 zip(config_variables, config_values))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500489 conditions = sorted([sef.get_expr(c), v] for c, v in conditions.iteritems())
490 else:
491 conditions = []
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500492 return {'conditions': conditions}
493
494
495class ConfigSettings(object):
496 """Represents the dependency variables for a single build configuration.
497 The structure is immutable.
498 """
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500499 def __init__(self, values):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500500 verify_variables(values)
501 self.touched = sorted(values.get(KEY_TOUCHED, []))
502 self.tracked = sorted(values.get(KEY_TRACKED, []))
503 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
504 self.command = values.get('command', [])[:]
505 self.read_only = values.get('read_only')
506
507 def union(self, rhs):
508 """Merges two config settings together.
509
510 self has priority over rhs for 'command' variable.
511 """
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500512 var = {
513 KEY_TOUCHED: sorted(self.touched + rhs.touched),
514 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
515 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
516 'command': self.command or rhs.command,
517 'read_only': rhs.read_only if self.read_only is None else self.read_only,
518 }
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500519 return ConfigSettings(var)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500520
521 def flatten(self):
522 out = {}
523 if self.command:
524 out['command'] = self.command
525 if self.touched:
526 out[KEY_TOUCHED] = self.touched
527 if self.tracked:
528 out[KEY_TRACKED] = self.tracked
529 if self.untracked:
530 out[KEY_UNTRACKED] = self.untracked
531 if self.read_only is not None:
532 out['read_only'] = self.read_only
533 return out
534
535
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500536def _safe_index(l, k):
537 try:
538 return l.index(k)
539 except ValueError:
540 return None
541
542
543def _get_map_keys(dest_keys, in_keys):
544 """Returns a tuple of the indexes of each item in in_keys found in dest_keys.
545
546 For example, if in_keys is ('A', 'C') and dest_keys is ('A', 'B', 'C'), the
547 return value will be (0, None, 1).
548 """
549 return tuple(_safe_index(in_keys, k) for k in dest_keys)
550
551
552def _map_keys(mapping, items):
553 """Returns a tuple with items placed at mapping index.
554
555 For example, if mapping is (1, None, 0) and items is ('a', 'b'), it will
556 return ('b', None, 'c').
557 """
558 return tuple(items[i] if i != None else None for i in mapping)
559
560
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500561class Configs(object):
562 """Represents a processed .isolate file.
563
564 Stores the file in a processed way, split by configuration.
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500565
566 At this point, we don't know all the possibilities. So mount a partial view
567 that we have.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500568 """
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500569 def __init__(self, file_comment, config_variables):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500570 self.file_comment = file_comment
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500571 # Contains the names of the config variables seen while processing
572 # .isolate file(s). The order is important since the same order is used for
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500573 # keys in self._by_config.
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500574 assert isinstance(config_variables, tuple)
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400575 assert all(isinstance(c, basestring) for c in config_variables), (
576 config_variables)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500577 self._config_variables = config_variables
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500578 # The keys of _by_config are tuples of values for each of the items in
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500579 # self._config_variables. A None item in the list of the key means the value
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500580 # is unbounded.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500581 self._by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500582
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500583 @property
584 def config_variables(self):
585 return self._config_variables
586
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500587 def get_config(self, config):
588 """Returns all configs that matches this config as a single ConfigSettings.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500589 """
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400590 out = ConfigSettings({})
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500591 for k, v in self._by_config.iteritems():
592 if all(i == j or j is None for i, j in zip(config, k)):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400593 out = out.union(v)
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500594 return out
595
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400596 def set_config(self, key, value):
597 """Sets the ConfigSettings for this key.
598
599 The key is a tuple of bounded or unbounded variables. The global variable
600 is the key where all values are unbounded, e.g.:
601 (None,) * len(self._config_variables)
602 """
603 assert key not in self._by_config, (key, self._by_config.keys())
604 assert isinstance(key, tuple)
605 assert len(key) == len(self._config_variables), (
606 key, self._config_variables)
607 assert isinstance(value, ConfigSettings)
608 self._by_config[key] = value
609
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500610 def union(self, rhs):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400611 """Returns a new Configs instance, the union of variables from self and rhs.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500612
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400613 Uses self.file_comment if available, otherwise rhs.file_comment.
614 """
615 # Merge the keys of config_variables for each Configs instances. All the new
616 # variables will become unbounded. This requires realigning the keys.
617 config_variables = tuple(sorted(
618 set(self.config_variables) | set(rhs.config_variables)))
619 out = Configs(self.file_comment or rhs.file_comment, config_variables)
620 mapping_lhs = _get_map_keys(out.config_variables, self.config_variables)
621 mapping_rhs = _get_map_keys(out.config_variables, rhs.config_variables)
622 lhs_config = dict(
623 (_map_keys(mapping_lhs, k), v) for k, v in self._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500624 # pylint: disable=W0212
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400625 rhs_config = dict(
626 (_map_keys(mapping_rhs, k), v) for k, v in rhs._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500627
628 for key in set(lhs_config) | set(rhs_config):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400629 out.set_config(key, union(lhs_config.get(key), rhs_config.get(key)))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500630 return out
631
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500632 def flatten(self):
633 """Returns a flat dictionary representation of the configuration.
634 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500635 return dict((k, v.flatten()) for k, v in self._by_config.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500636
637 def make_isolate_file(self):
638 """Returns a dictionary suitable for writing to a .isolate file.
639 """
640 dependencies_by_config = self.flatten()
641 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
642 return convert_map_to_isolate_dict(configs_by_dependency,
643 self.config_variables)
644
645
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500646def convert_old_to_new_format(value):
647 """Converts from the old .isolate format, which only has one variable (OS),
648 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
649 and allows conditions that depend on the set of all OSes, to the new format,
650 which allows any set of variables, has no hardcoded values, and only allows
651 explicit positive tests of variable values.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500652
653 TODO(maruel): Formalize support for variables with a config with no variable
654 bound. This is sensible to keep them at the global level and not in a
655 condition.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500656 """
657 conditions = value.get('conditions', [])
658 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
659 return value # Nothing to change
660
661 def parse_condition(cond):
662 m = re.match(r'OS=="(\w+)"\Z', cond[0])
663 if not m:
664 raise isolateserver.ConfigError('Invalid condition: %s' % cond[0])
665 return m.group(1)
666
667 oses = set(map(parse_condition, conditions))
668 default_oses = set(['linux', 'mac', 'win'])
669 oses = sorted(oses | default_oses)
670
671 def if_not_os(not_os, then):
672 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
673 return [expr, then]
674
675 conditions = [
676 cond[:2] for cond in conditions if cond[1]
677 ] + [
678 if_not_os(parse_condition(cond), cond[2])
679 for cond in conditions if len(cond) == 3
680 ]
681
682 if 'variables' in value:
683 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
684 conditions.sort()
685
686 value = value.copy()
687 value['conditions'] = conditions
688 return value
689
690
691def load_isolate_as_config(isolate_dir, value, file_comment):
692 """Parses one .isolate file and returns a Configs() instance.
693
694 Arguments:
695 isolate_dir: only used to load relative includes so it doesn't depend on
696 cwd.
697 value: is the loaded dictionary that was defined in the gyp file.
698 file_comment: comments found at the top of the file so it can be preserved.
699
700 The expected format is strict, anything diverting from the format below will
701 throw an assert:
702 {
703 'includes': [
704 'foo.isolate',
705 ],
706 'conditions': [
707 ['OS=="vms" and foo=42', {
708 'variables': {
709 'command': [
710 ...
711 ],
712 'isolate_dependency_tracked': [
713 ...
714 ],
715 'isolate_dependency_untracked': [
716 ...
717 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500718 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500719 },
720 }],
721 ...
722 ],
723 }
724 """
725 value = convert_old_to_new_format(value)
726
727 variables_and_values = {}
728 verify_root(value, variables_and_values)
729 if variables_and_values:
730 config_variables, config_values = zip(
731 *sorted(variables_and_values.iteritems()))
732 all_configs = list(itertools.product(*config_values))
733 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500734 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500735 all_configs = []
736
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500737 isolate = Configs(file_comment, config_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500738
739 # Add configuration-specific variables.
740 for expr, then in value.get('conditions', []):
741 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500742 new = Configs(None, config_variables)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500743 for config in configs:
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400744 new.set_config(config, ConfigSettings(then['variables']))
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500745 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500746
747 # Load the includes. Process them in reverse so the last one take precedence.
748 for include in reversed(value.get('includes', [])):
749 if os.path.isabs(include):
750 raise isolateserver.ConfigError(
751 'Failed to load configuration; absolute include path \'%s\'' %
752 include)
753 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
754 with open(included_isolate, 'r') as f:
755 included_isolate = load_isolate_as_config(
756 os.path.dirname(included_isolate),
757 eval_content(f.read()),
758 None)
759 isolate = union(isolate, included_isolate)
760
761 return isolate
762
763
764def load_isolate_for_config(isolate_dir, content, config_variables):
765 """Loads the .isolate file and returns the information unprocessed but
766 filtered for the specific OS.
767
768 Returns the command, dependencies and read_only flag. The dependencies are
769 fixed to use os.path.sep.
770 """
771 # Load the .isolate file, process its conditions, retrieve the command and
772 # dependencies.
773 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
774 try:
775 config_name = tuple(
776 config_variables[var] for var in isolate.config_variables)
777 except KeyError:
778 raise isolateserver.ConfigError(
779 'These configuration variables were missing from the command line: %s' %
780 ', '.join(
781 sorted(set(isolate.config_variables) - set(config_variables))))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500782
783 # A configuration is to be created with all the combinations of free
784 # variables.
785 config = isolate.get_config(config_name)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500786 # Merge tracked and untracked variables, isolate.py doesn't care about the
787 # trackability of the variables, only the build tool does.
788 dependencies = [
789 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
790 ]
791 touched = [f.replace('/', os.path.sep) for f in config.touched]
792 return config.command, dependencies, touched, config.read_only