blob: bed60de4cf97a4e90d8456138503e53634bbcd31 [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
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050016import itertools
17import logging
18import os
19import re
20
21import isolateserver
22
23from utils import short_expression_finder
24
25# Files that should be 0-length when mapped.
26KEY_TOUCHED = 'isolate_dependency_touched'
27# Files that should be tracked by the build tool.
28KEY_TRACKED = 'isolate_dependency_tracked'
29# Files that should not be tracked by the build tool.
30KEY_UNTRACKED = 'isolate_dependency_untracked'
31
32# Valid variable name.
33VALID_VARIABLE = '[A-Za-z_][A-Za-z_0-9]*'
34
35
36def determine_root_dir(relative_root, infiles):
37 """For a list of infiles, determines the deepest root directory that is
38 referenced indirectly.
39
40 All arguments must be using os.path.sep.
41 """
42 # The trick used to determine the root directory is to look at "how far" back
43 # up it is looking up.
44 deepest_root = relative_root
45 for i in infiles:
46 x = relative_root
47 while i.startswith('..' + os.path.sep):
48 i = i[3:]
49 assert not i.startswith(os.path.sep)
50 x = os.path.dirname(x)
51 if deepest_root.startswith(x):
52 deepest_root = x
53 logging.debug(
54 'determine_root_dir(%s, %d files) -> %s' % (
55 relative_root, len(infiles), deepest_root))
56 return deepest_root
57
58
59def replace_variable(part, variables):
60 m = re.match(r'<\((' + VALID_VARIABLE + ')\)', part)
61 if m:
62 if m.group(1) not in variables:
63 raise isolateserver.ConfigError(
64 'Variable "%s" was not found in %s.\nDid you forget to specify '
65 '--path-variable?' % (m.group(1), variables))
66 return variables[m.group(1)]
67 return part
68
69
70def eval_variables(item, variables):
71 """Replaces the .isolate variables in a string item.
72
73 Note that the .isolate format is a subset of the .gyp dialect.
74 """
75 return ''.join(
76 replace_variable(p, variables)
77 for p in re.split(r'(<\(' + VALID_VARIABLE + '\))', item))
78
79
80def split_touched(files):
81 """Splits files that are touched vs files that are read."""
82 tracked = []
83 touched = []
84 for f in files:
85 if f.size:
86 tracked.append(f)
87 else:
88 touched.append(f)
89 return tracked, touched
90
91
92def pretty_print(variables, stdout):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -040093 """Outputs a .isolate file from the decoded variables.
94
95 The .isolate format is GYP compatible.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050096
97 Similar to pprint.print() but with NIH syndrome.
98 """
99 # Order the dictionary keys by these keys in priority.
100 ORDER = (
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400101 'variables', 'condition', 'command', 'read_only',
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500102 KEY_TRACKED, KEY_UNTRACKED)
103
104 def sorting_key(x):
105 """Gives priority to 'most important' keys before the others."""
106 if x in ORDER:
107 return str(ORDER.index(x))
108 return x
109
110 def loop_list(indent, items):
111 for item in items:
112 if isinstance(item, basestring):
113 stdout.write('%s\'%s\',\n' % (indent, item))
114 elif isinstance(item, dict):
115 stdout.write('%s{\n' % indent)
116 loop_dict(indent + ' ', item)
117 stdout.write('%s},\n' % indent)
118 elif isinstance(item, list):
119 # A list inside a list will write the first item embedded.
120 stdout.write('%s[' % indent)
121 for index, i in enumerate(item):
122 if isinstance(i, basestring):
123 stdout.write(
124 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
125 elif isinstance(i, dict):
126 stdout.write('{\n')
127 loop_dict(indent + ' ', i)
128 if index != len(item) - 1:
129 x = ', '
130 else:
131 x = ''
132 stdout.write('%s}%s' % (indent, x))
133 else:
134 assert False
135 stdout.write('],\n')
136 else:
137 assert False
138
139 def loop_dict(indent, items):
140 for key in sorted(items, key=sorting_key):
141 item = items[key]
142 stdout.write("%s'%s': " % (indent, key))
143 if isinstance(item, dict):
144 stdout.write('{\n')
145 loop_dict(indent + ' ', item)
146 stdout.write(indent + '},\n')
147 elif isinstance(item, list):
148 stdout.write('[\n')
149 loop_list(indent + ' ', item)
150 stdout.write(indent + '],\n')
151 elif isinstance(item, basestring):
152 stdout.write(
153 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500154 elif isinstance(item, (int, bool)) or item is None:
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400155 stdout.write('%s,\n' % item)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500156 else:
157 assert False, item
158
159 stdout.write('{\n')
160 loop_dict(' ', variables)
161 stdout.write('}\n')
162
163
164def print_all(comment, data, stream):
165 """Prints a complete .isolate file and its top-level file comment into a
166 stream.
167 """
168 if comment:
169 stream.write(comment)
170 pretty_print(data, stream)
171
172
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500173def extract_comment(content):
174 """Extracts file level comment."""
175 out = []
176 for line in content.splitlines(True):
177 if line.startswith('#'):
178 out.append(line)
179 else:
180 break
181 return ''.join(out)
182
183
184def eval_content(content):
185 """Evaluates a python file and return the value defined in it.
186
187 Used in practice for .isolate files.
188 """
189 globs = {'__builtins__': None}
190 locs = {}
191 try:
192 value = eval(content, globs, locs)
193 except TypeError as e:
194 e.args = list(e.args) + [content]
195 raise
196 assert locs == {}, locs
197 assert globs == {'__builtins__': None}, globs
198 return value
199
200
201def match_configs(expr, config_variables, all_configs):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500202 """Returns the list of values from |values| that match the condition |expr|.
203
204 Arguments:
205 expr: string that is evaluatable with eval(). It is a GYP condition.
206 config_variables: list of the name of the variables.
207 all_configs: list of the list of possible values.
208
209 If a variable is not referenced at all, it is marked as unbounded (free) with
210 a value set to None.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500211 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500212 # It is more than just eval'ing the variable, it needs to be double checked to
213 # see if the variable is referenced at all. If not, the variable is free
214 # (unbounded).
215 # TODO(maruel): Use the intelligent way by inspecting expr instead of doing
216 # trial and error to figure out which variable is bound.
217 combinations = []
218 for bound_variables in itertools.product(
219 (True, False), repeat=len(config_variables)):
220 # Add the combination of variables bound.
221 combinations.append(
222 (
223 [c for c, b in zip(config_variables, bound_variables) if b],
224 set(
225 tuple(v if b else None for v, b in zip(line, bound_variables))
226 for line in all_configs)
227 ))
228
229 out = []
230 for variables, configs in combinations:
231 # Strip variables and see if expr can still be evaluated.
232 for values in configs:
233 globs = {'__builtins__': None}
234 globs.update(zip(variables, (v for v in values if v is not None)))
235 try:
236 assertion = eval(expr, globs, {})
237 except NameError:
238 continue
239 if not isinstance(assertion, bool):
240 raise isolateserver.ConfigError('Invalid condition')
241 if assertion:
242 out.append(values)
243 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500244
245
246def verify_variables(variables):
247 """Verifies the |variables| dictionary is in the expected format."""
248 VALID_VARIABLES = [
249 KEY_TOUCHED,
250 KEY_TRACKED,
251 KEY_UNTRACKED,
252 'command',
253 'read_only',
254 ]
255 assert isinstance(variables, dict), variables
256 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
257 for name, value in variables.iteritems():
258 if name == 'read_only':
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500259 assert value in (0, 1, 2, None), value
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500260 else:
261 assert isinstance(value, list), value
262 assert all(isinstance(i, basestring) for i in value), value
263
264
265def verify_ast(expr, variables_and_values):
266 """Verifies that |expr| is of the form
267 expr ::= expr ( "or" | "and" ) expr
268 | identifier "==" ( string | int )
269 Also collects the variable identifiers and string/int values in the dict
270 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
271 """
272 assert isinstance(expr, (ast.BoolOp, ast.Compare))
273 if isinstance(expr, ast.BoolOp):
274 assert isinstance(expr.op, (ast.And, ast.Or))
275 for subexpr in expr.values:
276 verify_ast(subexpr, variables_and_values)
277 else:
278 assert isinstance(expr.left.ctx, ast.Load)
279 assert len(expr.ops) == 1
280 assert isinstance(expr.ops[0], ast.Eq)
281 var_values = variables_and_values.setdefault(expr.left.id, set())
282 rhs = expr.comparators[0]
283 assert isinstance(rhs, (ast.Str, ast.Num))
284 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
285
286
287def verify_condition(condition, variables_and_values):
288 """Verifies the |condition| dictionary is in the expected format.
289 See verify_ast() for the meaning of |variables_and_values|.
290 """
291 VALID_INSIDE_CONDITION = ['variables']
292 assert isinstance(condition, list), condition
293 assert len(condition) == 2, condition
294 expr, then = condition
295
296 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
297 verify_ast(test_ast.body, variables_and_values)
298
299 assert isinstance(then, dict), then
300 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
301 if not 'variables' in then:
302 raise isolateserver.ConfigError('Missing \'variables\' in condition %s' %
303 condition)
304 verify_variables(then['variables'])
305
306
307def verify_root(value, variables_and_values):
308 """Verifies that |value| is the parsed form of a valid .isolate file.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400309
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500310 See verify_ast() for the meaning of |variables_and_values|.
311 """
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400312 VALID_ROOTS = ['includes', 'conditions', 'variables']
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500313 assert isinstance(value, dict), value
314 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
315
316 includes = value.get('includes', [])
317 assert isinstance(includes, list), includes
318 for include in includes:
319 assert isinstance(include, basestring), include
320
321 conditions = value.get('conditions', [])
322 assert isinstance(conditions, list), conditions
323 for condition in conditions:
324 verify_condition(condition, variables_and_values)
325
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400326 variables = value.get('variables', {})
327 verify_variables(variables)
328
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500329
330def remove_weak_dependencies(values, key, item, item_configs):
331 """Removes any configs from this key if the item is already under a
332 strong key.
333 """
334 if key == KEY_TOUCHED:
335 item_configs = set(item_configs)
336 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
337 try:
338 item_configs -= values[stronger_key][item]
339 except KeyError:
340 pass
341
342 return item_configs
343
344
345def remove_repeated_dependencies(folders, key, item, item_configs):
346 """Removes any configs from this key if the item is in a folder that is
347 already included."""
348
349 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
350 item_configs = set(item_configs)
351 for (folder, configs) in folders.iteritems():
352 if folder != item and item.startswith(folder):
353 item_configs -= configs
354
355 return item_configs
356
357
358def get_folders(values_dict):
359 """Returns a dict of all the folders in the given value_dict."""
360 return dict(
361 (item, configs) for (item, configs) in values_dict.iteritems()
362 if item.endswith('/')
363 )
364
365
366def invert_map(variables):
367 """Converts {config: {deptype: list(depvals)}} to
368 {deptype: {depval: set(configs)}}.
369 """
370 KEYS = (
371 KEY_TOUCHED,
372 KEY_TRACKED,
373 KEY_UNTRACKED,
374 'command',
375 'read_only',
376 )
377 out = dict((key, {}) for key in KEYS)
378 for config, values in variables.iteritems():
379 for key in KEYS:
380 if key == 'command':
381 items = [tuple(values[key])] if key in values else []
382 elif key == 'read_only':
383 items = [values[key]] if key in values else []
384 else:
385 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
386 items = values.get(key, [])
387 for item in items:
388 out[key].setdefault(item, set()).add(config)
389 return out
390
391
392def reduce_inputs(values):
393 """Reduces the output of invert_map() to the strictest minimum list.
394
395 Looks at each individual file and directory, maps where they are used and
396 reconstructs the inverse dictionary.
397
398 Returns the minimized dictionary.
399 """
400 KEYS = (
401 KEY_TOUCHED,
402 KEY_TRACKED,
403 KEY_UNTRACKED,
404 'command',
405 'read_only',
406 )
407
408 # Folders can only live in KEY_UNTRACKED.
409 folders = get_folders(values.get(KEY_UNTRACKED, {}))
410
411 out = dict((key, {}) for key in KEYS)
412 for key in KEYS:
413 for item, item_configs in values.get(key, {}).iteritems():
414 item_configs = remove_weak_dependencies(values, key, item, item_configs)
415 item_configs = remove_repeated_dependencies(
416 folders, key, item, item_configs)
417 if item_configs:
418 out[key][item] = item_configs
419 return out
420
421
422def convert_map_to_isolate_dict(values, config_variables):
423 """Regenerates back a .isolate configuration dict from files and dirs
424 mappings generated from reduce_inputs().
425 """
426 # Gather a list of configurations for set inversion later.
427 all_mentioned_configs = set()
428 for configs_by_item in values.itervalues():
429 for configs in configs_by_item.itervalues():
430 all_mentioned_configs.update(configs)
431
432 # Invert the mapping to make it dict first.
433 conditions = {}
434 for key in values:
435 for item, configs in values[key].iteritems():
436 then = conditions.setdefault(frozenset(configs), {})
437 variables = then.setdefault('variables', {})
438
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500439 if key == 'read_only':
440 if not isinstance(item, int):
441 raise isolateserver.ConfigError(
442 'Unexpected entry type %r for key %s' % (item, key))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500443 variables[key] = item
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500444 elif key == 'command':
445 if not isinstance(item, tuple):
446 raise isolateserver.ConfigError(
447 'Unexpected entry type %r for key %s' % (item, key))
448 if key in variables:
449 raise isolateserver.ConfigError('Unexpected duplicate key %s' % key)
450 if not item:
451 raise isolateserver.ConfigError(
452 'Expected non empty entry in %s' % key)
453 variables[key] = list(item)
454 elif key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED):
455 if not isinstance(item, basestring):
456 raise isolateserver.ConfigError('Unexpected entry type %r' % item)
457 if not item:
458 raise isolateserver.ConfigError(
459 'Expected non empty entry in %s' % key)
460 # The list of items (files or dirs). Append the new item and keep
461 # the list sorted.
462 l = variables.setdefault(key, [])
463 l.append(item)
464 l.sort()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500465 else:
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500466 raise isolateserver.ConfigError('Unexpected key %s' % key)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500467
468 if all_mentioned_configs:
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500469 # Change [(1, 2), (3, 4)] to [set(1, 3), set(2, 4)]
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500470 config_values = map(set, zip(*all_mentioned_configs))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500471 for i in config_values:
472 i.discard(None)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500473 sef = short_expression_finder.ShortExpressionFinder(
474 zip(config_variables, config_values))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500475 conditions = sorted([sef.get_expr(c), v] for c, v in conditions.iteritems())
476 else:
477 conditions = []
Marc-Antoine Rueld27c6632014-03-13 15:29:36 -0400478 out = {'conditions': conditions}
479 for c in conditions:
480 if c[0] == '':
481 # Extract the global.
482 out.update(c[1])
483 conditions.remove(c)
484 break
485 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500486
487
488class ConfigSettings(object):
489 """Represents the dependency variables for a single build configuration.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400490
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500491 The structure is immutable.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400492
493 .touch, .tracked and .untracked are the list of dependencies. The items in
494 these lists use '/' as a path separator.
495 .command and .isolate_dir describe how to run the command. .isolate_dir uses
496 the OS' native path separator. It must be an absolute path, it's the path
497 where to start the command from.
498 .read_only describe how to map the files.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500499 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400500 def __init__(self, values, isolate_dir):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500501 verify_variables(values)
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400502 assert isolate_dir is None or os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500503 self.touched = sorted(values.get(KEY_TOUCHED, []))
504 self.tracked = sorted(values.get(KEY_TRACKED, []))
505 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
506 self.command = values.get('command', [])[:]
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400507 self.isolate_dir = isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500508 self.read_only = values.get('read_only')
509
510 def union(self, rhs):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400511 """Merges two config settings together into a new instance.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500512
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400513 self has priority over rhs for .command. Use the same
514 .isolate_dir as the one having a .command. Preferring self over rhs.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500515 """
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500516 var = {
517 KEY_TOUCHED: sorted(self.touched + rhs.touched),
518 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
519 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
520 'command': self.command or rhs.command,
521 'read_only': rhs.read_only if self.read_only is None else self.read_only,
522 }
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400523 isolate_dir = self.isolate_dir if self.command else rhs.isolate_dir
524 return ConfigSettings(var, isolate_dir)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500525
526 def flatten(self):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400527 """Converts the object into a dict."""
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500528 out = {}
529 if self.command:
530 out['command'] = self.command
531 if self.touched:
532 out[KEY_TOUCHED] = self.touched
533 if self.tracked:
534 out[KEY_TRACKED] = self.tracked
535 if self.untracked:
536 out[KEY_UNTRACKED] = self.untracked
537 if self.read_only is not None:
538 out['read_only'] = self.read_only
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400539 # TODO(maruel): Probably better to not output it if command is None?
540 if self.isolate_dir is not None:
541 out['isolate_dir'] = self.isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500542 return out
543
544
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500545def _safe_index(l, k):
546 try:
547 return l.index(k)
548 except ValueError:
549 return None
550
551
552def _get_map_keys(dest_keys, in_keys):
553 """Returns a tuple of the indexes of each item in in_keys found in dest_keys.
554
555 For example, if in_keys is ('A', 'C') and dest_keys is ('A', 'B', 'C'), the
556 return value will be (0, None, 1).
557 """
558 return tuple(_safe_index(in_keys, k) for k in dest_keys)
559
560
561def _map_keys(mapping, items):
562 """Returns a tuple with items placed at mapping index.
563
564 For example, if mapping is (1, None, 0) and items is ('a', 'b'), it will
565 return ('b', None, 'c').
566 """
567 return tuple(items[i] if i != None else None for i in mapping)
568
569
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500570class Configs(object):
571 """Represents a processed .isolate file.
572
573 Stores the file in a processed way, split by configuration.
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500574
575 At this point, we don't know all the possibilities. So mount a partial view
576 that we have.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400577
578 This class doesn't hold isolate_dir, since it is dependent on the final
579 configuration selected. It is implicitly dependent on which .isolate defines
580 the 'command' that will take effect.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500581 """
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500582 def __init__(self, file_comment, config_variables):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500583 self.file_comment = file_comment
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500584 # Contains the names of the config variables seen while processing
585 # .isolate file(s). The order is important since the same order is used for
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500586 # keys in self._by_config.
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500587 assert isinstance(config_variables, tuple)
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400588 assert all(isinstance(c, basestring) for c in config_variables), (
589 config_variables)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400590 config_variables = tuple(config_variables)
591 assert tuple(sorted(config_variables)) == config_variables, config_variables
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500592 self._config_variables = config_variables
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500593 # The keys of _by_config are tuples of values for each of the items in
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500594 # self._config_variables. A None item in the list of the key means the value
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500595 # is unbounded.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500596 self._by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500597
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500598 @property
599 def config_variables(self):
600 return self._config_variables
601
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500602 def get_config(self, config):
603 """Returns all configs that matches this config as a single ConfigSettings.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500604 """
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400605 # TODO(maruel): Fix ordering based on the bounded values. The keys are not
606 # necessarily sorted in the way that makes sense, they are alphabetically
607 # sorted. It is important because the left-most takes predescence.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400608 out = ConfigSettings({}, None)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400609 for k, v in sorted(self._by_config.iteritems()):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500610 if all(i == j or j is None for i, j in zip(config, k)):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400611 out = out.union(v)
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500612 return out
613
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400614 def set_config(self, key, value):
615 """Sets the ConfigSettings for this key.
616
617 The key is a tuple of bounded or unbounded variables. The global variable
618 is the key where all values are unbounded, e.g.:
619 (None,) * len(self._config_variables)
620 """
621 assert key not in self._by_config, (key, self._by_config.keys())
622 assert isinstance(key, tuple)
623 assert len(key) == len(self._config_variables), (
624 key, self._config_variables)
625 assert isinstance(value, ConfigSettings)
626 self._by_config[key] = value
627
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500628 def union(self, rhs):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400629 """Returns a new Configs instance, the union of variables from self and rhs.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500630
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400631 Uses self.file_comment if available, otherwise rhs.file_comment.
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400632 It keeps config_variables sorted in the output.
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400633 """
634 # Merge the keys of config_variables for each Configs instances. All the new
635 # variables will become unbounded. This requires realigning the keys.
636 config_variables = tuple(sorted(
637 set(self.config_variables) | set(rhs.config_variables)))
638 out = Configs(self.file_comment or rhs.file_comment, config_variables)
639 mapping_lhs = _get_map_keys(out.config_variables, self.config_variables)
640 mapping_rhs = _get_map_keys(out.config_variables, rhs.config_variables)
641 lhs_config = dict(
642 (_map_keys(mapping_lhs, k), v) for k, v in self._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500643 # pylint: disable=W0212
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400644 rhs_config = dict(
645 (_map_keys(mapping_rhs, k), v) for k, v in rhs._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500646
647 for key in set(lhs_config) | set(rhs_config):
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400648 l = lhs_config.get(key)
649 r = rhs_config.get(key)
650 out.set_config(key, l.union(r) if (l and r) else (l or r))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500651 return out
652
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500653 def flatten(self):
654 """Returns a flat dictionary representation of the configuration.
655 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500656 return dict((k, v.flatten()) for k, v in self._by_config.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500657
658 def make_isolate_file(self):
659 """Returns a dictionary suitable for writing to a .isolate file.
660 """
661 dependencies_by_config = self.flatten()
662 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
663 return convert_map_to_isolate_dict(configs_by_dependency,
664 self.config_variables)
665
666
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500667def load_isolate_as_config(isolate_dir, value, file_comment):
668 """Parses one .isolate file and returns a Configs() instance.
669
670 Arguments:
671 isolate_dir: only used to load relative includes so it doesn't depend on
672 cwd.
673 value: is the loaded dictionary that was defined in the gyp file.
674 file_comment: comments found at the top of the file so it can be preserved.
675
676 The expected format is strict, anything diverting from the format below will
677 throw an assert:
678 {
679 'includes': [
680 'foo.isolate',
681 ],
682 'conditions': [
683 ['OS=="vms" and foo=42', {
684 'variables': {
685 'command': [
686 ...
687 ],
688 'isolate_dependency_tracked': [
689 ...
690 ],
691 'isolate_dependency_untracked': [
692 ...
693 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500694 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500695 },
696 }],
697 ...
698 ],
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400699 'variables': {
700 ...
701 },
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500702 }
703 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400704 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400705 if any(len(cond) == 3 for cond in value.get('conditions', [])):
706 raise isolateserver.ConfigError('Using \'else\' is not supported anymore.')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500707 variables_and_values = {}
708 verify_root(value, variables_and_values)
709 if variables_and_values:
710 config_variables, config_values = zip(
711 *sorted(variables_and_values.iteritems()))
712 all_configs = list(itertools.product(*config_values))
713 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500714 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500715 all_configs = []
716
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500717 isolate = Configs(file_comment, config_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500718
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400719 # Add global variables. The global variables are on the empty tuple key.
720 isolate.set_config(
721 (None,) * len(config_variables),
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400722 ConfigSettings(value.get('variables', {}), isolate_dir))
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400723
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500724 # Add configuration-specific variables.
725 for expr, then in value.get('conditions', []):
726 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500727 new = Configs(None, config_variables)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500728 for config in configs:
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400729 new.set_config(config, ConfigSettings(then['variables'], isolate_dir))
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500730 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500731
732 # Load the includes. Process them in reverse so the last one take precedence.
733 for include in reversed(value.get('includes', [])):
734 if os.path.isabs(include):
735 raise isolateserver.ConfigError(
736 'Failed to load configuration; absolute include path \'%s\'' %
737 include)
738 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
739 with open(included_isolate, 'r') as f:
740 included_isolate = load_isolate_as_config(
741 os.path.dirname(included_isolate),
742 eval_content(f.read()),
743 None)
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400744 isolate = isolate.union(included_isolate)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500745
746 return isolate
747
748
749def load_isolate_for_config(isolate_dir, content, config_variables):
750 """Loads the .isolate file and returns the information unprocessed but
751 filtered for the specific OS.
752
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400753 Returns:
754 tuple of command, dependencies, touched, read_only flag, isolate_dir.
755 The dependencies are fixed to use os.path.sep.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500756 """
757 # Load the .isolate file, process its conditions, retrieve the command and
758 # dependencies.
759 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
760 try:
761 config_name = tuple(
762 config_variables[var] for var in isolate.config_variables)
763 except KeyError:
764 raise isolateserver.ConfigError(
765 'These configuration variables were missing from the command line: %s' %
766 ', '.join(
767 sorted(set(isolate.config_variables) - set(config_variables))))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500768
769 # A configuration is to be created with all the combinations of free
770 # variables.
771 config = isolate.get_config(config_name)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500772 # Merge tracked and untracked variables, isolate.py doesn't care about the
773 # trackability of the variables, only the build tool does.
774 dependencies = [
775 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
776 ]
777 touched = [f.replace('/', os.path.sep) for f in config.touched]
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400778 return (
779 config.command, dependencies, touched, config.read_only,
780 config.isolate_dir)