blob: eb9b77ff76ae53979d7dd6b880e63804813750e0 [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 """
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400329 VALID_ROOTS = ['includes', 'conditions', 'variables']
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500330 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
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400343 variables = value.get('variables', {})
344 verify_variables(variables)
345
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500346
347def remove_weak_dependencies(values, key, item, item_configs):
348 """Removes any configs from this key if the item is already under a
349 strong key.
350 """
351 if key == KEY_TOUCHED:
352 item_configs = set(item_configs)
353 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
354 try:
355 item_configs -= values[stronger_key][item]
356 except KeyError:
357 pass
358
359 return item_configs
360
361
362def remove_repeated_dependencies(folders, key, item, item_configs):
363 """Removes any configs from this key if the item is in a folder that is
364 already included."""
365
366 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
367 item_configs = set(item_configs)
368 for (folder, configs) in folders.iteritems():
369 if folder != item and item.startswith(folder):
370 item_configs -= configs
371
372 return item_configs
373
374
375def get_folders(values_dict):
376 """Returns a dict of all the folders in the given value_dict."""
377 return dict(
378 (item, configs) for (item, configs) in values_dict.iteritems()
379 if item.endswith('/')
380 )
381
382
383def invert_map(variables):
384 """Converts {config: {deptype: list(depvals)}} to
385 {deptype: {depval: set(configs)}}.
386 """
387 KEYS = (
388 KEY_TOUCHED,
389 KEY_TRACKED,
390 KEY_UNTRACKED,
391 'command',
392 'read_only',
393 )
394 out = dict((key, {}) for key in KEYS)
395 for config, values in variables.iteritems():
396 for key in KEYS:
397 if key == 'command':
398 items = [tuple(values[key])] if key in values else []
399 elif key == 'read_only':
400 items = [values[key]] if key in values else []
401 else:
402 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
403 items = values.get(key, [])
404 for item in items:
405 out[key].setdefault(item, set()).add(config)
406 return out
407
408
409def reduce_inputs(values):
410 """Reduces the output of invert_map() to the strictest minimum list.
411
412 Looks at each individual file and directory, maps where they are used and
413 reconstructs the inverse dictionary.
414
415 Returns the minimized dictionary.
416 """
417 KEYS = (
418 KEY_TOUCHED,
419 KEY_TRACKED,
420 KEY_UNTRACKED,
421 'command',
422 'read_only',
423 )
424
425 # Folders can only live in KEY_UNTRACKED.
426 folders = get_folders(values.get(KEY_UNTRACKED, {}))
427
428 out = dict((key, {}) for key in KEYS)
429 for key in KEYS:
430 for item, item_configs in values.get(key, {}).iteritems():
431 item_configs = remove_weak_dependencies(values, key, item, item_configs)
432 item_configs = remove_repeated_dependencies(
433 folders, key, item, item_configs)
434 if item_configs:
435 out[key][item] = item_configs
436 return out
437
438
439def convert_map_to_isolate_dict(values, config_variables):
440 """Regenerates back a .isolate configuration dict from files and dirs
441 mappings generated from reduce_inputs().
442 """
443 # Gather a list of configurations for set inversion later.
444 all_mentioned_configs = set()
445 for configs_by_item in values.itervalues():
446 for configs in configs_by_item.itervalues():
447 all_mentioned_configs.update(configs)
448
449 # Invert the mapping to make it dict first.
450 conditions = {}
451 for key in values:
452 for item, configs in values[key].iteritems():
453 then = conditions.setdefault(frozenset(configs), {})
454 variables = then.setdefault('variables', {})
455
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500456 if key == 'read_only':
457 if not isinstance(item, int):
458 raise isolateserver.ConfigError(
459 'Unexpected entry type %r for key %s' % (item, key))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500460 variables[key] = item
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500461 elif key == 'command':
462 if not isinstance(item, tuple):
463 raise isolateserver.ConfigError(
464 'Unexpected entry type %r for key %s' % (item, key))
465 if key in variables:
466 raise isolateserver.ConfigError('Unexpected duplicate key %s' % key)
467 if not item:
468 raise isolateserver.ConfigError(
469 'Expected non empty entry in %s' % key)
470 variables[key] = list(item)
471 elif key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED):
472 if not isinstance(item, basestring):
473 raise isolateserver.ConfigError('Unexpected entry type %r' % item)
474 if not item:
475 raise isolateserver.ConfigError(
476 'Expected non empty entry in %s' % key)
477 # The list of items (files or dirs). Append the new item and keep
478 # the list sorted.
479 l = variables.setdefault(key, [])
480 l.append(item)
481 l.sort()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500482 else:
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500483 raise isolateserver.ConfigError('Unexpected key %s' % key)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500484
485 if all_mentioned_configs:
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500486 # Change [(1, 2), (3, 4)] to [set(1, 3), set(2, 4)]
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500487 config_values = map(set, zip(*all_mentioned_configs))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500488 for i in config_values:
489 i.discard(None)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500490 sef = short_expression_finder.ShortExpressionFinder(
491 zip(config_variables, config_values))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500492 conditions = sorted([sef.get_expr(c), v] for c, v in conditions.iteritems())
493 else:
494 conditions = []
Marc-Antoine Rueld27c6632014-03-13 15:29:36 -0400495 out = {'conditions': conditions}
496 for c in conditions:
497 if c[0] == '':
498 # Extract the global.
499 out.update(c[1])
500 conditions.remove(c)
501 break
502 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500503
504
505class ConfigSettings(object):
506 """Represents the dependency variables for a single build configuration.
507 The structure is immutable.
508 """
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500509 def __init__(self, values):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500510 verify_variables(values)
511 self.touched = sorted(values.get(KEY_TOUCHED, []))
512 self.tracked = sorted(values.get(KEY_TRACKED, []))
513 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
514 self.command = values.get('command', [])[:]
515 self.read_only = values.get('read_only')
516
517 def union(self, rhs):
518 """Merges two config settings together.
519
520 self has priority over rhs for 'command' variable.
521 """
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500522 var = {
523 KEY_TOUCHED: sorted(self.touched + rhs.touched),
524 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
525 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
526 'command': self.command or rhs.command,
527 'read_only': rhs.read_only if self.read_only is None else self.read_only,
528 }
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500529 return ConfigSettings(var)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500530
531 def flatten(self):
532 out = {}
533 if self.command:
534 out['command'] = self.command
535 if self.touched:
536 out[KEY_TOUCHED] = self.touched
537 if self.tracked:
538 out[KEY_TRACKED] = self.tracked
539 if self.untracked:
540 out[KEY_UNTRACKED] = self.untracked
541 if self.read_only is not None:
542 out['read_only'] = self.read_only
543 return out
544
545
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500546def _safe_index(l, k):
547 try:
548 return l.index(k)
549 except ValueError:
550 return None
551
552
553def _get_map_keys(dest_keys, in_keys):
554 """Returns a tuple of the indexes of each item in in_keys found in dest_keys.
555
556 For example, if in_keys is ('A', 'C') and dest_keys is ('A', 'B', 'C'), the
557 return value will be (0, None, 1).
558 """
559 return tuple(_safe_index(in_keys, k) for k in dest_keys)
560
561
562def _map_keys(mapping, items):
563 """Returns a tuple with items placed at mapping index.
564
565 For example, if mapping is (1, None, 0) and items is ('a', 'b'), it will
566 return ('b', None, 'c').
567 """
568 return tuple(items[i] if i != None else None for i in mapping)
569
570
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500571class Configs(object):
572 """Represents a processed .isolate file.
573
574 Stores the file in a processed way, split by configuration.
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500575
576 At this point, we don't know all the possibilities. So mount a partial view
577 that we have.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500578 """
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500579 def __init__(self, file_comment, config_variables):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500580 self.file_comment = file_comment
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500581 # Contains the names of the config variables seen while processing
582 # .isolate file(s). The order is important since the same order is used for
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500583 # keys in self._by_config.
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500584 assert isinstance(config_variables, tuple)
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400585 assert all(isinstance(c, basestring) for c in config_variables), (
586 config_variables)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400587 config_variables = tuple(config_variables)
588 assert tuple(sorted(config_variables)) == config_variables, config_variables
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500589 self._config_variables = config_variables
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500590 # The keys of _by_config are tuples of values for each of the items in
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500591 # self._config_variables. A None item in the list of the key means the value
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500592 # is unbounded.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500593 self._by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500594
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500595 @property
596 def config_variables(self):
597 return self._config_variables
598
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500599 def get_config(self, config):
600 """Returns all configs that matches this config as a single ConfigSettings.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500601 """
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400602 # TODO(maruel): Fix ordering based on the bounded values. The keys are not
603 # necessarily sorted in the way that makes sense, they are alphabetically
604 # sorted. It is important because the left-most takes predescence.
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400605 out = ConfigSettings({})
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400606 for k, v in sorted(self._by_config.iteritems()):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500607 if all(i == j or j is None for i, j in zip(config, k)):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400608 out = out.union(v)
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500609 return out
610
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400611 def set_config(self, key, value):
612 """Sets the ConfigSettings for this key.
613
614 The key is a tuple of bounded or unbounded variables. The global variable
615 is the key where all values are unbounded, e.g.:
616 (None,) * len(self._config_variables)
617 """
618 assert key not in self._by_config, (key, self._by_config.keys())
619 assert isinstance(key, tuple)
620 assert len(key) == len(self._config_variables), (
621 key, self._config_variables)
622 assert isinstance(value, ConfigSettings)
623 self._by_config[key] = value
624
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500625 def union(self, rhs):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400626 """Returns a new Configs instance, the union of variables from self and rhs.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500627
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400628 Uses self.file_comment if available, otherwise rhs.file_comment.
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400629 It keeps config_variables sorted in the output.
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400630 """
631 # Merge the keys of config_variables for each Configs instances. All the new
632 # variables will become unbounded. This requires realigning the keys.
633 config_variables = tuple(sorted(
634 set(self.config_variables) | set(rhs.config_variables)))
635 out = Configs(self.file_comment or rhs.file_comment, config_variables)
636 mapping_lhs = _get_map_keys(out.config_variables, self.config_variables)
637 mapping_rhs = _get_map_keys(out.config_variables, rhs.config_variables)
638 lhs_config = dict(
639 (_map_keys(mapping_lhs, k), v) for k, v in self._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500640 # pylint: disable=W0212
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400641 rhs_config = dict(
642 (_map_keys(mapping_rhs, k), v) for k, v in rhs._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500643
644 for key in set(lhs_config) | set(rhs_config):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400645 out.set_config(key, union(lhs_config.get(key), rhs_config.get(key)))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500646 return out
647
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500648 def flatten(self):
649 """Returns a flat dictionary representation of the configuration.
650 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500651 return dict((k, v.flatten()) for k, v in self._by_config.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500652
653 def make_isolate_file(self):
654 """Returns a dictionary suitable for writing to a .isolate file.
655 """
656 dependencies_by_config = self.flatten()
657 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
658 return convert_map_to_isolate_dict(configs_by_dependency,
659 self.config_variables)
660
661
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500662def load_isolate_as_config(isolate_dir, value, file_comment):
663 """Parses one .isolate file and returns a Configs() instance.
664
665 Arguments:
666 isolate_dir: only used to load relative includes so it doesn't depend on
667 cwd.
668 value: is the loaded dictionary that was defined in the gyp file.
669 file_comment: comments found at the top of the file so it can be preserved.
670
671 The expected format is strict, anything diverting from the format below will
672 throw an assert:
673 {
674 'includes': [
675 'foo.isolate',
676 ],
677 'conditions': [
678 ['OS=="vms" and foo=42', {
679 'variables': {
680 'command': [
681 ...
682 ],
683 'isolate_dependency_tracked': [
684 ...
685 ],
686 'isolate_dependency_untracked': [
687 ...
688 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500689 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500690 },
691 }],
692 ...
693 ],
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400694 'variables': {
695 ...
696 },
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500697 }
698 """
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400699 if any(len(cond) == 3 for cond in value.get('conditions', [])):
700 raise isolateserver.ConfigError('Using \'else\' is not supported anymore.')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500701 variables_and_values = {}
702 verify_root(value, variables_and_values)
703 if variables_and_values:
704 config_variables, config_values = zip(
705 *sorted(variables_and_values.iteritems()))
706 all_configs = list(itertools.product(*config_values))
707 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500708 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500709 all_configs = []
710
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500711 isolate = Configs(file_comment, config_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500712
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400713 # Add global variables. The global variables are on the empty tuple key.
714 isolate.set_config(
715 (None,) * len(config_variables),
716 ConfigSettings(value.get('variables', {})))
717
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500718 # Add configuration-specific variables.
719 for expr, then in value.get('conditions', []):
720 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500721 new = Configs(None, config_variables)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500722 for config in configs:
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400723 new.set_config(config, ConfigSettings(then['variables']))
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500724 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500725
726 # Load the includes. Process them in reverse so the last one take precedence.
727 for include in reversed(value.get('includes', [])):
728 if os.path.isabs(include):
729 raise isolateserver.ConfigError(
730 'Failed to load configuration; absolute include path \'%s\'' %
731 include)
732 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
733 with open(included_isolate, 'r') as f:
734 included_isolate = load_isolate_as_config(
735 os.path.dirname(included_isolate),
736 eval_content(f.read()),
737 None)
738 isolate = union(isolate, included_isolate)
739
740 return isolate
741
742
743def load_isolate_for_config(isolate_dir, content, config_variables):
744 """Loads the .isolate file and returns the information unprocessed but
745 filtered for the specific OS.
746
747 Returns the command, dependencies and read_only flag. The dependencies are
748 fixed to use os.path.sep.
749 """
750 # Load the .isolate file, process its conditions, retrieve the command and
751 # dependencies.
752 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
753 try:
754 config_name = tuple(
755 config_variables[var] for var in isolate.config_variables)
756 except KeyError:
757 raise isolateserver.ConfigError(
758 'These configuration variables were missing from the command line: %s' %
759 ', '.join(
760 sorted(set(isolate.config_variables) - set(config_variables))))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500761
762 # A configuration is to be created with all the combinations of free
763 # variables.
764 config = isolate.get_config(config_name)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500765 # Merge tracked and untracked variables, isolate.py doesn't care about the
766 # trackability of the variables, only the build tool does.
767 dependencies = [
768 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
769 ]
770 touched = [f.replace('/', os.path.sep) for f in config.touched]
771 return config.command, dependencies, touched, config.read_only