blob: 05bc1be16fa16eff6c5f090096c338b466a61333 [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)
575 self._config_variables = config_variables
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500576 # The keys of _by_config are tuples of values for each of the items in
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500577 # self._config_variables. A None item in the list of the key means the value
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500578 # is unbounded.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500579 self._by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500580
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500581 @property
582 def config_variables(self):
583 return self._config_variables
584
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500585 def get_config(self, config):
586 """Returns all configs that matches this config as a single ConfigSettings.
587
588 Returns None if no matching configuration is found.
589 """
590 out = None
591 for k, v in self._by_config.iteritems():
592 if all(i == j or j is None for i, j in zip(config, k)):
593 out = out.union(v) if out else v
594 return out
595
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500596 def union(self, rhs):
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500597 """Adds variables from rhs (a Configs) to the existing variables."""
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500598 # Takes the first file comment, prefering lhs.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500599
600 # Default mapping of configs.
601 lhs_config = self._by_config
602 # pylint: disable=W0212
603 rhs_config = rhs._by_config
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500604 comment = self.file_comment or rhs.file_comment
605 if not self.config_variables:
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500606 assert not self._by_config
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500607 out = Configs(comment, rhs.config_variables)
608 elif not rhs.config_variables:
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500609 assert not rhs._by_config
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500610 out = Configs(comment, self.config_variables)
611 elif rhs.config_variables == self.config_variables:
612 out = Configs(comment, self.config_variables)
613 else:
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500614 # At that point, we need to merge the keys. By default, all the new
615 # variables will become unbounded. This requires realigning the keys.
616 config_variables = tuple(sorted(
617 set(self.config_variables) | set(rhs.config_variables)))
618 out = Configs(comment, config_variables)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500619
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500620 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)
624 for k, v in self._by_config.iteritems())
625 rhs_config = dict(
626 (_map_keys(mapping_rhs, k), v)
627 for k, v in rhs._by_config.iteritems())
628
629 for key in set(lhs_config) | set(rhs_config):
630 out._by_config[key] = union(lhs_config.get(key), rhs_config.get(key))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500631 return out
632
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500633 def flatten(self):
634 """Returns a flat dictionary representation of the configuration.
635 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500636 return dict((k, v.flatten()) for k, v in self._by_config.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500637
638 def make_isolate_file(self):
639 """Returns a dictionary suitable for writing to a .isolate file.
640 """
641 dependencies_by_config = self.flatten()
642 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
643 return convert_map_to_isolate_dict(configs_by_dependency,
644 self.config_variables)
645
646
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500647def convert_old_to_new_format(value):
648 """Converts from the old .isolate format, which only has one variable (OS),
649 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
650 and allows conditions that depend on the set of all OSes, to the new format,
651 which allows any set of variables, has no hardcoded values, and only allows
652 explicit positive tests of variable values.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500653
654 TODO(maruel): Formalize support for variables with a config with no variable
655 bound. This is sensible to keep them at the global level and not in a
656 condition.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500657 """
658 conditions = value.get('conditions', [])
659 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
660 return value # Nothing to change
661
662 def parse_condition(cond):
663 m = re.match(r'OS=="(\w+)"\Z', cond[0])
664 if not m:
665 raise isolateserver.ConfigError('Invalid condition: %s' % cond[0])
666 return m.group(1)
667
668 oses = set(map(parse_condition, conditions))
669 default_oses = set(['linux', 'mac', 'win'])
670 oses = sorted(oses | default_oses)
671
672 def if_not_os(not_os, then):
673 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
674 return [expr, then]
675
676 conditions = [
677 cond[:2] for cond in conditions if cond[1]
678 ] + [
679 if_not_os(parse_condition(cond), cond[2])
680 for cond in conditions if len(cond) == 3
681 ]
682
683 if 'variables' in value:
684 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
685 conditions.sort()
686
687 value = value.copy()
688 value['conditions'] = conditions
689 return value
690
691
692def load_isolate_as_config(isolate_dir, value, file_comment):
693 """Parses one .isolate file and returns a Configs() instance.
694
695 Arguments:
696 isolate_dir: only used to load relative includes so it doesn't depend on
697 cwd.
698 value: is the loaded dictionary that was defined in the gyp file.
699 file_comment: comments found at the top of the file so it can be preserved.
700
701 The expected format is strict, anything diverting from the format below will
702 throw an assert:
703 {
704 'includes': [
705 'foo.isolate',
706 ],
707 'conditions': [
708 ['OS=="vms" and foo=42', {
709 'variables': {
710 'command': [
711 ...
712 ],
713 'isolate_dependency_tracked': [
714 ...
715 ],
716 'isolate_dependency_untracked': [
717 ...
718 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500719 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500720 },
721 }],
722 ...
723 ],
724 }
725 """
726 value = convert_old_to_new_format(value)
727
728 variables_and_values = {}
729 verify_root(value, variables_and_values)
730 if variables_and_values:
731 config_variables, config_values = zip(
732 *sorted(variables_and_values.iteritems()))
733 all_configs = list(itertools.product(*config_values))
734 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500735 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500736 all_configs = []
737
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500738 isolate = Configs(file_comment, config_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500739
740 # Add configuration-specific variables.
741 for expr, then in value.get('conditions', []):
742 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500743 new = Configs(None, config_variables)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500744 for config in configs:
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500745 # pylint: disable=W0212
746 new._by_config[config] = ConfigSettings(then['variables'])
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500747 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500748
749 # Load the includes. Process them in reverse so the last one take precedence.
750 for include in reversed(value.get('includes', [])):
751 if os.path.isabs(include):
752 raise isolateserver.ConfigError(
753 'Failed to load configuration; absolute include path \'%s\'' %
754 include)
755 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
756 with open(included_isolate, 'r') as f:
757 included_isolate = load_isolate_as_config(
758 os.path.dirname(included_isolate),
759 eval_content(f.read()),
760 None)
761 isolate = union(isolate, included_isolate)
762
763 return isolate
764
765
766def load_isolate_for_config(isolate_dir, content, config_variables):
767 """Loads the .isolate file and returns the information unprocessed but
768 filtered for the specific OS.
769
770 Returns the command, dependencies and read_only flag. The dependencies are
771 fixed to use os.path.sep.
772 """
773 # Load the .isolate file, process its conditions, retrieve the command and
774 # dependencies.
775 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
776 try:
777 config_name = tuple(
778 config_variables[var] for var in isolate.config_variables)
779 except KeyError:
780 raise isolateserver.ConfigError(
781 'These configuration variables were missing from the command line: %s' %
782 ', '.join(
783 sorted(set(isolate.config_variables) - set(config_variables))))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500784
785 # A configuration is to be created with all the combinations of free
786 # variables.
787 config = isolate.get_config(config_name)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500788 if not config:
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500789 # pylint: disable=W0212
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500790 raise isolateserver.ConfigError(
791 'Failed to load configuration for variable \'%s\' for config(s) \'%s\''
792 '\nAvailable configs: %s' %
793 (', '.join(isolate.config_variables),
794 ', '.join(config_name),
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500795 ', '.join(str(s) for s in isolate._by_config.keys())))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500796 # Merge tracked and untracked variables, isolate.py doesn't care about the
797 # trackability of the variables, only the build tool does.
798 dependencies = [
799 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
800 ]
801 touched = [f.replace('/', os.path.sep) for f in config.touched]
802 return config.command, dependencies, touched, config.read_only