blob: 8f35f9bb36b306bdd851afac71f35192faf19507 [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):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -040094 """Outputs a .isolate file from the decoded variables.
95
96 The .isolate format is GYP compatible.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050097
98 Similar to pprint.print() but with NIH syndrome.
99 """
100 # Order the dictionary keys by these keys in priority.
101 ORDER = (
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400102 'variables', 'condition', 'command', 'read_only',
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500103 KEY_TRACKED, KEY_UNTRACKED)
104
105 def sorting_key(x):
106 """Gives priority to 'most important' keys before the others."""
107 if x in ORDER:
108 return str(ORDER.index(x))
109 return x
110
111 def loop_list(indent, items):
112 for item in items:
113 if isinstance(item, basestring):
114 stdout.write('%s\'%s\',\n' % (indent, item))
115 elif isinstance(item, dict):
116 stdout.write('%s{\n' % indent)
117 loop_dict(indent + ' ', item)
118 stdout.write('%s},\n' % indent)
119 elif isinstance(item, list):
120 # A list inside a list will write the first item embedded.
121 stdout.write('%s[' % indent)
122 for index, i in enumerate(item):
123 if isinstance(i, basestring):
124 stdout.write(
125 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
126 elif isinstance(i, dict):
127 stdout.write('{\n')
128 loop_dict(indent + ' ', i)
129 if index != len(item) - 1:
130 x = ', '
131 else:
132 x = ''
133 stdout.write('%s}%s' % (indent, x))
134 else:
135 assert False
136 stdout.write('],\n')
137 else:
138 assert False
139
140 def loop_dict(indent, items):
141 for key in sorted(items, key=sorting_key):
142 item = items[key]
143 stdout.write("%s'%s': " % (indent, key))
144 if isinstance(item, dict):
145 stdout.write('{\n')
146 loop_dict(indent + ' ', item)
147 stdout.write(indent + '},\n')
148 elif isinstance(item, list):
149 stdout.write('[\n')
150 loop_list(indent + ' ', item)
151 stdout.write(indent + '],\n')
152 elif isinstance(item, basestring):
153 stdout.write(
154 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500155 elif isinstance(item, (int, bool)) or item is None:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500156 stdout.write('%s\n' % item)
157 else:
158 assert False, item
159
160 stdout.write('{\n')
161 loop_dict(' ', variables)
162 stdout.write('}\n')
163
164
165def print_all(comment, data, stream):
166 """Prints a complete .isolate file and its top-level file comment into a
167 stream.
168 """
169 if comment:
170 stream.write(comment)
171 pretty_print(data, stream)
172
173
174def union(lhs, rhs):
175 """Merges two compatible datastructures composed of dict/list/set."""
176 assert lhs is not None or rhs is not None
177 if lhs is None:
178 return copy.deepcopy(rhs)
179 if rhs is None:
180 return copy.deepcopy(lhs)
181 assert type(lhs) == type(rhs), (lhs, rhs)
182 if hasattr(lhs, 'union'):
183 # Includes set, ConfigSettings and Configs.
184 return lhs.union(rhs)
185 if isinstance(lhs, dict):
186 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
187 elif isinstance(lhs, list):
188 # Do not go inside the list.
189 return lhs + rhs
190 assert False, type(lhs)
191
192
193def extract_comment(content):
194 """Extracts file level comment."""
195 out = []
196 for line in content.splitlines(True):
197 if line.startswith('#'):
198 out.append(line)
199 else:
200 break
201 return ''.join(out)
202
203
204def eval_content(content):
205 """Evaluates a python file and return the value defined in it.
206
207 Used in practice for .isolate files.
208 """
209 globs = {'__builtins__': None}
210 locs = {}
211 try:
212 value = eval(content, globs, locs)
213 except TypeError as e:
214 e.args = list(e.args) + [content]
215 raise
216 assert locs == {}, locs
217 assert globs == {'__builtins__': None}, globs
218 return value
219
220
221def match_configs(expr, config_variables, all_configs):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500222 """Returns the list of values from |values| that match the condition |expr|.
223
224 Arguments:
225 expr: string that is evaluatable with eval(). It is a GYP condition.
226 config_variables: list of the name of the variables.
227 all_configs: list of the list of possible values.
228
229 If a variable is not referenced at all, it is marked as unbounded (free) with
230 a value set to None.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500231 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500232 # It is more than just eval'ing the variable, it needs to be double checked to
233 # see if the variable is referenced at all. If not, the variable is free
234 # (unbounded).
235 # TODO(maruel): Use the intelligent way by inspecting expr instead of doing
236 # trial and error to figure out which variable is bound.
237 combinations = []
238 for bound_variables in itertools.product(
239 (True, False), repeat=len(config_variables)):
240 # Add the combination of variables bound.
241 combinations.append(
242 (
243 [c for c, b in zip(config_variables, bound_variables) if b],
244 set(
245 tuple(v if b else None for v, b in zip(line, bound_variables))
246 for line in all_configs)
247 ))
248
249 out = []
250 for variables, configs in combinations:
251 # Strip variables and see if expr can still be evaluated.
252 for values in configs:
253 globs = {'__builtins__': None}
254 globs.update(zip(variables, (v for v in values if v is not None)))
255 try:
256 assertion = eval(expr, globs, {})
257 except NameError:
258 continue
259 if not isinstance(assertion, bool):
260 raise isolateserver.ConfigError('Invalid condition')
261 if assertion:
262 out.append(values)
263 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500264
265
266def verify_variables(variables):
267 """Verifies the |variables| dictionary is in the expected format."""
268 VALID_VARIABLES = [
269 KEY_TOUCHED,
270 KEY_TRACKED,
271 KEY_UNTRACKED,
272 'command',
273 'read_only',
274 ]
275 assert isinstance(variables, dict), variables
276 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
277 for name, value in variables.iteritems():
278 if name == 'read_only':
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500279 assert value in (0, 1, 2, None), value
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500280 else:
281 assert isinstance(value, list), value
282 assert all(isinstance(i, basestring) for i in value), value
283
284
285def verify_ast(expr, variables_and_values):
286 """Verifies that |expr| is of the form
287 expr ::= expr ( "or" | "and" ) expr
288 | identifier "==" ( string | int )
289 Also collects the variable identifiers and string/int values in the dict
290 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
291 """
292 assert isinstance(expr, (ast.BoolOp, ast.Compare))
293 if isinstance(expr, ast.BoolOp):
294 assert isinstance(expr.op, (ast.And, ast.Or))
295 for subexpr in expr.values:
296 verify_ast(subexpr, variables_and_values)
297 else:
298 assert isinstance(expr.left.ctx, ast.Load)
299 assert len(expr.ops) == 1
300 assert isinstance(expr.ops[0], ast.Eq)
301 var_values = variables_and_values.setdefault(expr.left.id, set())
302 rhs = expr.comparators[0]
303 assert isinstance(rhs, (ast.Str, ast.Num))
304 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
305
306
307def verify_condition(condition, variables_and_values):
308 """Verifies the |condition| dictionary is in the expected format.
309 See verify_ast() for the meaning of |variables_and_values|.
310 """
311 VALID_INSIDE_CONDITION = ['variables']
312 assert isinstance(condition, list), condition
313 assert len(condition) == 2, condition
314 expr, then = condition
315
316 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
317 verify_ast(test_ast.body, variables_and_values)
318
319 assert isinstance(then, dict), then
320 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
321 if not 'variables' in then:
322 raise isolateserver.ConfigError('Missing \'variables\' in condition %s' %
323 condition)
324 verify_variables(then['variables'])
325
326
327def verify_root(value, variables_and_values):
328 """Verifies that |value| is the parsed form of a valid .isolate file.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400329
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500330 See verify_ast() for the meaning of |variables_and_values|.
331 """
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400332 VALID_ROOTS = ['includes', 'conditions', 'variables']
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500333 assert isinstance(value, dict), value
334 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
335
336 includes = value.get('includes', [])
337 assert isinstance(includes, list), includes
338 for include in includes:
339 assert isinstance(include, basestring), include
340
341 conditions = value.get('conditions', [])
342 assert isinstance(conditions, list), conditions
343 for condition in conditions:
344 verify_condition(condition, variables_and_values)
345
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400346 variables = value.get('variables', {})
347 verify_variables(variables)
348
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500349
350def remove_weak_dependencies(values, key, item, item_configs):
351 """Removes any configs from this key if the item is already under a
352 strong key.
353 """
354 if key == KEY_TOUCHED:
355 item_configs = set(item_configs)
356 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
357 try:
358 item_configs -= values[stronger_key][item]
359 except KeyError:
360 pass
361
362 return item_configs
363
364
365def remove_repeated_dependencies(folders, key, item, item_configs):
366 """Removes any configs from this key if the item is in a folder that is
367 already included."""
368
369 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
370 item_configs = set(item_configs)
371 for (folder, configs) in folders.iteritems():
372 if folder != item and item.startswith(folder):
373 item_configs -= configs
374
375 return item_configs
376
377
378def get_folders(values_dict):
379 """Returns a dict of all the folders in the given value_dict."""
380 return dict(
381 (item, configs) for (item, configs) in values_dict.iteritems()
382 if item.endswith('/')
383 )
384
385
386def invert_map(variables):
387 """Converts {config: {deptype: list(depvals)}} to
388 {deptype: {depval: set(configs)}}.
389 """
390 KEYS = (
391 KEY_TOUCHED,
392 KEY_TRACKED,
393 KEY_UNTRACKED,
394 'command',
395 'read_only',
396 )
397 out = dict((key, {}) for key in KEYS)
398 for config, values in variables.iteritems():
399 for key in KEYS:
400 if key == 'command':
401 items = [tuple(values[key])] if key in values else []
402 elif key == 'read_only':
403 items = [values[key]] if key in values else []
404 else:
405 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
406 items = values.get(key, [])
407 for item in items:
408 out[key].setdefault(item, set()).add(config)
409 return out
410
411
412def reduce_inputs(values):
413 """Reduces the output of invert_map() to the strictest minimum list.
414
415 Looks at each individual file and directory, maps where they are used and
416 reconstructs the inverse dictionary.
417
418 Returns the minimized dictionary.
419 """
420 KEYS = (
421 KEY_TOUCHED,
422 KEY_TRACKED,
423 KEY_UNTRACKED,
424 'command',
425 'read_only',
426 )
427
428 # Folders can only live in KEY_UNTRACKED.
429 folders = get_folders(values.get(KEY_UNTRACKED, {}))
430
431 out = dict((key, {}) for key in KEYS)
432 for key in KEYS:
433 for item, item_configs in values.get(key, {}).iteritems():
434 item_configs = remove_weak_dependencies(values, key, item, item_configs)
435 item_configs = remove_repeated_dependencies(
436 folders, key, item, item_configs)
437 if item_configs:
438 out[key][item] = item_configs
439 return out
440
441
442def convert_map_to_isolate_dict(values, config_variables):
443 """Regenerates back a .isolate configuration dict from files and dirs
444 mappings generated from reduce_inputs().
445 """
446 # Gather a list of configurations for set inversion later.
447 all_mentioned_configs = set()
448 for configs_by_item in values.itervalues():
449 for configs in configs_by_item.itervalues():
450 all_mentioned_configs.update(configs)
451
452 # Invert the mapping to make it dict first.
453 conditions = {}
454 for key in values:
455 for item, configs in values[key].iteritems():
456 then = conditions.setdefault(frozenset(configs), {})
457 variables = then.setdefault('variables', {})
458
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500459 if key == 'read_only':
460 if not isinstance(item, int):
461 raise isolateserver.ConfigError(
462 'Unexpected entry type %r for key %s' % (item, key))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500463 variables[key] = item
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500464 elif key == 'command':
465 if not isinstance(item, tuple):
466 raise isolateserver.ConfigError(
467 'Unexpected entry type %r for key %s' % (item, key))
468 if key in variables:
469 raise isolateserver.ConfigError('Unexpected duplicate key %s' % key)
470 if not item:
471 raise isolateserver.ConfigError(
472 'Expected non empty entry in %s' % key)
473 variables[key] = list(item)
474 elif key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED):
475 if not isinstance(item, basestring):
476 raise isolateserver.ConfigError('Unexpected entry type %r' % item)
477 if not item:
478 raise isolateserver.ConfigError(
479 'Expected non empty entry in %s' % key)
480 # The list of items (files or dirs). Append the new item and keep
481 # the list sorted.
482 l = variables.setdefault(key, [])
483 l.append(item)
484 l.sort()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500485 else:
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500486 raise isolateserver.ConfigError('Unexpected key %s' % key)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500487
488 if all_mentioned_configs:
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500489 # Change [(1, 2), (3, 4)] to [set(1, 3), set(2, 4)]
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500490 config_values = map(set, zip(*all_mentioned_configs))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500491 for i in config_values:
492 i.discard(None)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500493 sef = short_expression_finder.ShortExpressionFinder(
494 zip(config_variables, config_values))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500495 conditions = sorted([sef.get_expr(c), v] for c, v in conditions.iteritems())
496 else:
497 conditions = []
Marc-Antoine Rueld27c6632014-03-13 15:29:36 -0400498 out = {'conditions': conditions}
499 for c in conditions:
500 if c[0] == '':
501 # Extract the global.
502 out.update(c[1])
503 conditions.remove(c)
504 break
505 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500506
507
508class ConfigSettings(object):
509 """Represents the dependency variables for a single build configuration.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400510
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500511 The structure is immutable.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400512
513 .touch, .tracked and .untracked are the list of dependencies. The items in
514 these lists use '/' as a path separator.
515 .command and .isolate_dir describe how to run the command. .isolate_dir uses
516 the OS' native path separator. It must be an absolute path, it's the path
517 where to start the command from.
518 .read_only describe how to map the files.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500519 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400520 def __init__(self, values, isolate_dir):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500521 verify_variables(values)
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400522 assert isolate_dir is None or os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500523 self.touched = sorted(values.get(KEY_TOUCHED, []))
524 self.tracked = sorted(values.get(KEY_TRACKED, []))
525 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
526 self.command = values.get('command', [])[:]
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400527 self.isolate_dir = isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500528 self.read_only = values.get('read_only')
529
530 def union(self, rhs):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400531 """Merges two config settings together into a new instance.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500532
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400533 self has priority over rhs for .command. Use the same
534 .isolate_dir as the one having a .command. Preferring self over rhs.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500535 """
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500536 var = {
537 KEY_TOUCHED: sorted(self.touched + rhs.touched),
538 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
539 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
540 'command': self.command or rhs.command,
541 'read_only': rhs.read_only if self.read_only is None else self.read_only,
542 }
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400543 isolate_dir = self.isolate_dir if self.command else rhs.isolate_dir
544 return ConfigSettings(var, isolate_dir)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500545
546 def flatten(self):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400547 """Converts the object into a dict."""
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500548 out = {}
549 if self.command:
550 out['command'] = self.command
551 if self.touched:
552 out[KEY_TOUCHED] = self.touched
553 if self.tracked:
554 out[KEY_TRACKED] = self.tracked
555 if self.untracked:
556 out[KEY_UNTRACKED] = self.untracked
557 if self.read_only is not None:
558 out['read_only'] = self.read_only
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400559 # TODO(maruel): Probably better to not output it if command is None?
560 if self.isolate_dir is not None:
561 out['isolate_dir'] = self.isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500562 return out
563
564
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500565def _safe_index(l, k):
566 try:
567 return l.index(k)
568 except ValueError:
569 return None
570
571
572def _get_map_keys(dest_keys, in_keys):
573 """Returns a tuple of the indexes of each item in in_keys found in dest_keys.
574
575 For example, if in_keys is ('A', 'C') and dest_keys is ('A', 'B', 'C'), the
576 return value will be (0, None, 1).
577 """
578 return tuple(_safe_index(in_keys, k) for k in dest_keys)
579
580
581def _map_keys(mapping, items):
582 """Returns a tuple with items placed at mapping index.
583
584 For example, if mapping is (1, None, 0) and items is ('a', 'b'), it will
585 return ('b', None, 'c').
586 """
587 return tuple(items[i] if i != None else None for i in mapping)
588
589
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500590class Configs(object):
591 """Represents a processed .isolate file.
592
593 Stores the file in a processed way, split by configuration.
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500594
595 At this point, we don't know all the possibilities. So mount a partial view
596 that we have.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400597
598 This class doesn't hold isolate_dir, since it is dependent on the final
599 configuration selected. It is implicitly dependent on which .isolate defines
600 the 'command' that will take effect.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500601 """
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500602 def __init__(self, file_comment, config_variables):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500603 self.file_comment = file_comment
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500604 # Contains the names of the config variables seen while processing
605 # .isolate file(s). The order is important since the same order is used for
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500606 # keys in self._by_config.
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500607 assert isinstance(config_variables, tuple)
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400608 assert all(isinstance(c, basestring) for c in config_variables), (
609 config_variables)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400610 config_variables = tuple(config_variables)
611 assert tuple(sorted(config_variables)) == config_variables, config_variables
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500612 self._config_variables = config_variables
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500613 # The keys of _by_config are tuples of values for each of the items in
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500614 # self._config_variables. A None item in the list of the key means the value
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500615 # is unbounded.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500616 self._by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500617
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500618 @property
619 def config_variables(self):
620 return self._config_variables
621
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500622 def get_config(self, config):
623 """Returns all configs that matches this config as a single ConfigSettings.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500624 """
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400625 # TODO(maruel): Fix ordering based on the bounded values. The keys are not
626 # necessarily sorted in the way that makes sense, they are alphabetically
627 # sorted. It is important because the left-most takes predescence.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400628 out = ConfigSettings({}, None)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400629 for k, v in sorted(self._by_config.iteritems()):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500630 if all(i == j or j is None for i, j in zip(config, k)):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400631 out = out.union(v)
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500632 return out
633
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400634 def set_config(self, key, value):
635 """Sets the ConfigSettings for this key.
636
637 The key is a tuple of bounded or unbounded variables. The global variable
638 is the key where all values are unbounded, e.g.:
639 (None,) * len(self._config_variables)
640 """
641 assert key not in self._by_config, (key, self._by_config.keys())
642 assert isinstance(key, tuple)
643 assert len(key) == len(self._config_variables), (
644 key, self._config_variables)
645 assert isinstance(value, ConfigSettings)
646 self._by_config[key] = value
647
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500648 def union(self, rhs):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400649 """Returns a new Configs instance, the union of variables from self and rhs.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500650
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400651 Uses self.file_comment if available, otherwise rhs.file_comment.
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400652 It keeps config_variables sorted in the output.
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400653 """
654 # Merge the keys of config_variables for each Configs instances. All the new
655 # variables will become unbounded. This requires realigning the keys.
656 config_variables = tuple(sorted(
657 set(self.config_variables) | set(rhs.config_variables)))
658 out = Configs(self.file_comment or rhs.file_comment, config_variables)
659 mapping_lhs = _get_map_keys(out.config_variables, self.config_variables)
660 mapping_rhs = _get_map_keys(out.config_variables, rhs.config_variables)
661 lhs_config = dict(
662 (_map_keys(mapping_lhs, k), v) for k, v in self._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500663 # pylint: disable=W0212
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400664 rhs_config = dict(
665 (_map_keys(mapping_rhs, k), v) for k, v in rhs._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500666
667 for key in set(lhs_config) | set(rhs_config):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400668 out.set_config(key, union(lhs_config.get(key), rhs_config.get(key)))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500669 return out
670
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500671 def flatten(self):
672 """Returns a flat dictionary representation of the configuration.
673 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500674 return dict((k, v.flatten()) for k, v in self._by_config.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500675
676 def make_isolate_file(self):
677 """Returns a dictionary suitable for writing to a .isolate file.
678 """
679 dependencies_by_config = self.flatten()
680 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
681 return convert_map_to_isolate_dict(configs_by_dependency,
682 self.config_variables)
683
684
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500685def load_isolate_as_config(isolate_dir, value, file_comment):
686 """Parses one .isolate file and returns a Configs() instance.
687
688 Arguments:
689 isolate_dir: only used to load relative includes so it doesn't depend on
690 cwd.
691 value: is the loaded dictionary that was defined in the gyp file.
692 file_comment: comments found at the top of the file so it can be preserved.
693
694 The expected format is strict, anything diverting from the format below will
695 throw an assert:
696 {
697 'includes': [
698 'foo.isolate',
699 ],
700 'conditions': [
701 ['OS=="vms" and foo=42', {
702 'variables': {
703 'command': [
704 ...
705 ],
706 'isolate_dependency_tracked': [
707 ...
708 ],
709 'isolate_dependency_untracked': [
710 ...
711 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500712 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500713 },
714 }],
715 ...
716 ],
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400717 'variables': {
718 ...
719 },
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500720 }
721 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400722 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400723 if any(len(cond) == 3 for cond in value.get('conditions', [])):
724 raise isolateserver.ConfigError('Using \'else\' is not supported anymore.')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500725 variables_and_values = {}
726 verify_root(value, variables_and_values)
727 if variables_and_values:
728 config_variables, config_values = zip(
729 *sorted(variables_and_values.iteritems()))
730 all_configs = list(itertools.product(*config_values))
731 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500732 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500733 all_configs = []
734
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500735 isolate = Configs(file_comment, config_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500736
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400737 # Add global variables. The global variables are on the empty tuple key.
738 isolate.set_config(
739 (None,) * len(config_variables),
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400740 ConfigSettings(value.get('variables', {}), isolate_dir))
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400741
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500742 # Add configuration-specific variables.
743 for expr, then in value.get('conditions', []):
744 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500745 new = Configs(None, config_variables)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500746 for config in configs:
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400747 new.set_config(config, ConfigSettings(then['variables'], isolate_dir))
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500748 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500749
750 # Load the includes. Process them in reverse so the last one take precedence.
751 for include in reversed(value.get('includes', [])):
752 if os.path.isabs(include):
753 raise isolateserver.ConfigError(
754 'Failed to load configuration; absolute include path \'%s\'' %
755 include)
756 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
757 with open(included_isolate, 'r') as f:
758 included_isolate = load_isolate_as_config(
759 os.path.dirname(included_isolate),
760 eval_content(f.read()),
761 None)
762 isolate = union(isolate, included_isolate)
763
764 return isolate
765
766
767def load_isolate_for_config(isolate_dir, content, config_variables):
768 """Loads the .isolate file and returns the information unprocessed but
769 filtered for the specific OS.
770
771 Returns the command, dependencies and read_only flag. The dependencies are
772 fixed to use os.path.sep.
773 """
774 # Load the .isolate file, process its conditions, retrieve the command and
775 # dependencies.
776 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
777 try:
778 config_name = tuple(
779 config_variables[var] for var in isolate.config_variables)
780 except KeyError:
781 raise isolateserver.ConfigError(
782 'These configuration variables were missing from the command line: %s' %
783 ', '.join(
784 sorted(set(isolate.config_variables) - set(config_variables))))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500785
786 # A configuration is to be created with all the combinations of free
787 # variables.
788 config = isolate.get_config(config_name)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500789 # Merge tracked and untracked variables, isolate.py doesn't care about the
790 # trackability of the variables, only the build tool does.
791 dependencies = [
792 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
793 ]
794 touched = [f.replace('/', os.path.sep) for f in config.touched]
795 return config.command, dependencies, touched, config.read_only