blob: b7198c6c499d03563d5c5053fe95e523ea515008 [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
Marc-Antoine Rueledf28952014-03-31 19:55:47 -040019import posixpath
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050020import re
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -040021import sys
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050022
23import isolateserver
24
25from utils import short_expression_finder
26
27# Files that should be 0-length when mapped.
28KEY_TOUCHED = 'isolate_dependency_touched'
29# Files that should be tracked by the build tool.
30KEY_TRACKED = 'isolate_dependency_tracked'
31# Files that should not be tracked by the build tool.
32KEY_UNTRACKED = 'isolate_dependency_untracked'
33
34# Valid variable name.
35VALID_VARIABLE = '[A-Za-z_][A-Za-z_0-9]*'
36
37
38def determine_root_dir(relative_root, infiles):
39 """For a list of infiles, determines the deepest root directory that is
40 referenced indirectly.
41
42 All arguments must be using os.path.sep.
43 """
44 # The trick used to determine the root directory is to look at "how far" back
45 # up it is looking up.
46 deepest_root = relative_root
47 for i in infiles:
48 x = relative_root
49 while i.startswith('..' + os.path.sep):
50 i = i[3:]
51 assert not i.startswith(os.path.sep)
52 x = os.path.dirname(x)
53 if deepest_root.startswith(x):
54 deepest_root = x
55 logging.debug(
56 'determine_root_dir(%s, %d files) -> %s' % (
57 relative_root, len(infiles), deepest_root))
58 return deepest_root
59
60
61def replace_variable(part, variables):
62 m = re.match(r'<\((' + VALID_VARIABLE + ')\)', part)
63 if m:
64 if m.group(1) not in variables:
65 raise isolateserver.ConfigError(
66 'Variable "%s" was not found in %s.\nDid you forget to specify '
67 '--path-variable?' % (m.group(1), variables))
68 return variables[m.group(1)]
69 return part
70
71
72def eval_variables(item, variables):
73 """Replaces the .isolate variables in a string item.
74
75 Note that the .isolate format is a subset of the .gyp dialect.
76 """
77 return ''.join(
78 replace_variable(p, variables)
79 for p in re.split(r'(<\(' + VALID_VARIABLE + '\))', item))
80
81
82def split_touched(files):
83 """Splits files that are touched vs files that are read."""
84 tracked = []
85 touched = []
86 for f in files:
87 if f.size:
88 tracked.append(f)
89 else:
90 touched.append(f)
91 return tracked, touched
92
93
94def pretty_print(variables, stdout):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -040095 """Outputs a .isolate file from the decoded variables.
96
97 The .isolate format is GYP compatible.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050098
99 Similar to pprint.print() but with NIH syndrome.
100 """
101 # Order the dictionary keys by these keys in priority.
102 ORDER = (
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400103 'variables', 'condition', 'command', 'read_only',
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500104 KEY_TRACKED, KEY_UNTRACKED)
105
106 def sorting_key(x):
107 """Gives priority to 'most important' keys before the others."""
108 if x in ORDER:
109 return str(ORDER.index(x))
110 return x
111
112 def loop_list(indent, items):
113 for item in items:
114 if isinstance(item, basestring):
115 stdout.write('%s\'%s\',\n' % (indent, item))
116 elif isinstance(item, dict):
117 stdout.write('%s{\n' % indent)
118 loop_dict(indent + ' ', item)
119 stdout.write('%s},\n' % indent)
120 elif isinstance(item, list):
121 # A list inside a list will write the first item embedded.
122 stdout.write('%s[' % indent)
123 for index, i in enumerate(item):
124 if isinstance(i, basestring):
125 stdout.write(
126 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
127 elif isinstance(i, dict):
128 stdout.write('{\n')
129 loop_dict(indent + ' ', i)
130 if index != len(item) - 1:
131 x = ', '
132 else:
133 x = ''
134 stdout.write('%s}%s' % (indent, x))
135 else:
136 assert False
137 stdout.write('],\n')
138 else:
139 assert False
140
141 def loop_dict(indent, items):
142 for key in sorted(items, key=sorting_key):
143 item = items[key]
144 stdout.write("%s'%s': " % (indent, key))
145 if isinstance(item, dict):
146 stdout.write('{\n')
147 loop_dict(indent + ' ', item)
148 stdout.write(indent + '},\n')
149 elif isinstance(item, list):
150 stdout.write('[\n')
151 loop_list(indent + ' ', item)
152 stdout.write(indent + '],\n')
153 elif isinstance(item, basestring):
154 stdout.write(
155 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500156 elif isinstance(item, (int, bool)) or item is None:
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400157 stdout.write('%s,\n' % item)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500158 else:
159 assert False, item
160
161 stdout.write('{\n')
162 loop_dict(' ', variables)
163 stdout.write('}\n')
164
165
166def print_all(comment, data, stream):
167 """Prints a complete .isolate file and its top-level file comment into a
168 stream.
169 """
170 if comment:
171 stream.write(comment)
172 pretty_print(data, stream)
173
174
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500175def extract_comment(content):
176 """Extracts file level comment."""
177 out = []
178 for line in content.splitlines(True):
179 if line.startswith('#'):
180 out.append(line)
181 else:
182 break
183 return ''.join(out)
184
185
186def eval_content(content):
187 """Evaluates a python file and return the value defined in it.
188
189 Used in practice for .isolate files.
190 """
191 globs = {'__builtins__': None}
192 locs = {}
193 try:
194 value = eval(content, globs, locs)
195 except TypeError as e:
196 e.args = list(e.args) + [content]
197 raise
198 assert locs == {}, locs
199 assert globs == {'__builtins__': None}, globs
200 return value
201
202
203def match_configs(expr, config_variables, all_configs):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500204 """Returns the list of values from |values| that match the condition |expr|.
205
206 Arguments:
207 expr: string that is evaluatable with eval(). It is a GYP condition.
208 config_variables: list of the name of the variables.
209 all_configs: list of the list of possible values.
210
211 If a variable is not referenced at all, it is marked as unbounded (free) with
212 a value set to None.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500213 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500214 # It is more than just eval'ing the variable, it needs to be double checked to
215 # see if the variable is referenced at all. If not, the variable is free
216 # (unbounded).
217 # TODO(maruel): Use the intelligent way by inspecting expr instead of doing
218 # trial and error to figure out which variable is bound.
219 combinations = []
220 for bound_variables in itertools.product(
221 (True, False), repeat=len(config_variables)):
222 # Add the combination of variables bound.
223 combinations.append(
224 (
225 [c for c, b in zip(config_variables, bound_variables) if b],
226 set(
227 tuple(v if b else None for v, b in zip(line, bound_variables))
228 for line in all_configs)
229 ))
230
231 out = []
232 for variables, configs in combinations:
233 # Strip variables and see if expr can still be evaluated.
234 for values in configs:
235 globs = {'__builtins__': None}
236 globs.update(zip(variables, (v for v in values if v is not None)))
237 try:
238 assertion = eval(expr, globs, {})
239 except NameError:
240 continue
241 if not isinstance(assertion, bool):
242 raise isolateserver.ConfigError('Invalid condition')
243 if assertion:
244 out.append(values)
245 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500246
247
248def verify_variables(variables):
249 """Verifies the |variables| dictionary is in the expected format."""
250 VALID_VARIABLES = [
251 KEY_TOUCHED,
252 KEY_TRACKED,
253 KEY_UNTRACKED,
254 'command',
255 'read_only',
256 ]
257 assert isinstance(variables, dict), variables
258 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
259 for name, value in variables.iteritems():
260 if name == 'read_only':
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500261 assert value in (0, 1, 2, None), value
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500262 else:
263 assert isinstance(value, list), value
264 assert all(isinstance(i, basestring) for i in value), value
265
266
267def verify_ast(expr, variables_and_values):
268 """Verifies that |expr| is of the form
269 expr ::= expr ( "or" | "and" ) expr
270 | identifier "==" ( string | int )
271 Also collects the variable identifiers and string/int values in the dict
272 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
273 """
274 assert isinstance(expr, (ast.BoolOp, ast.Compare))
275 if isinstance(expr, ast.BoolOp):
276 assert isinstance(expr.op, (ast.And, ast.Or))
277 for subexpr in expr.values:
278 verify_ast(subexpr, variables_and_values)
279 else:
280 assert isinstance(expr.left.ctx, ast.Load)
281 assert len(expr.ops) == 1
282 assert isinstance(expr.ops[0], ast.Eq)
283 var_values = variables_and_values.setdefault(expr.left.id, set())
284 rhs = expr.comparators[0]
285 assert isinstance(rhs, (ast.Str, ast.Num))
286 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
287
288
289def verify_condition(condition, variables_and_values):
290 """Verifies the |condition| dictionary is in the expected format.
291 See verify_ast() for the meaning of |variables_and_values|.
292 """
293 VALID_INSIDE_CONDITION = ['variables']
294 assert isinstance(condition, list), condition
295 assert len(condition) == 2, condition
296 expr, then = condition
297
298 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
299 verify_ast(test_ast.body, variables_and_values)
300
301 assert isinstance(then, dict), then
302 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
303 if not 'variables' in then:
304 raise isolateserver.ConfigError('Missing \'variables\' in condition %s' %
305 condition)
306 verify_variables(then['variables'])
307
308
309def verify_root(value, variables_and_values):
310 """Verifies that |value| is the parsed form of a valid .isolate file.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400311
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500312 See verify_ast() for the meaning of |variables_and_values|.
313 """
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400314 VALID_ROOTS = ['includes', 'conditions', 'variables']
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500315 assert isinstance(value, dict), value
316 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
317
318 includes = value.get('includes', [])
319 assert isinstance(includes, list), includes
320 for include in includes:
321 assert isinstance(include, basestring), include
322
323 conditions = value.get('conditions', [])
324 assert isinstance(conditions, list), conditions
325 for condition in conditions:
326 verify_condition(condition, variables_and_values)
327
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400328 variables = value.get('variables', {})
329 verify_variables(variables)
330
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500331
332def remove_weak_dependencies(values, key, item, item_configs):
333 """Removes any configs from this key if the item is already under a
334 strong key.
335 """
336 if key == KEY_TOUCHED:
337 item_configs = set(item_configs)
338 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
339 try:
340 item_configs -= values[stronger_key][item]
341 except KeyError:
342 pass
343
344 return item_configs
345
346
347def remove_repeated_dependencies(folders, key, item, item_configs):
348 """Removes any configs from this key if the item is in a folder that is
349 already included."""
350
351 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
352 item_configs = set(item_configs)
353 for (folder, configs) in folders.iteritems():
354 if folder != item and item.startswith(folder):
355 item_configs -= configs
356
357 return item_configs
358
359
360def get_folders(values_dict):
361 """Returns a dict of all the folders in the given value_dict."""
362 return dict(
363 (item, configs) for (item, configs) in values_dict.iteritems()
364 if item.endswith('/')
365 )
366
367
368def invert_map(variables):
369 """Converts {config: {deptype: list(depvals)}} to
370 {deptype: {depval: set(configs)}}.
371 """
372 KEYS = (
373 KEY_TOUCHED,
374 KEY_TRACKED,
375 KEY_UNTRACKED,
376 'command',
377 'read_only',
378 )
379 out = dict((key, {}) for key in KEYS)
380 for config, values in variables.iteritems():
381 for key in KEYS:
382 if key == 'command':
383 items = [tuple(values[key])] if key in values else []
384 elif key == 'read_only':
385 items = [values[key]] if key in values else []
386 else:
387 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
388 items = values.get(key, [])
389 for item in items:
390 out[key].setdefault(item, set()).add(config)
391 return out
392
393
394def reduce_inputs(values):
395 """Reduces the output of invert_map() to the strictest minimum list.
396
397 Looks at each individual file and directory, maps where they are used and
398 reconstructs the inverse dictionary.
399
400 Returns the minimized dictionary.
401 """
402 KEYS = (
403 KEY_TOUCHED,
404 KEY_TRACKED,
405 KEY_UNTRACKED,
406 'command',
407 'read_only',
408 )
409
410 # Folders can only live in KEY_UNTRACKED.
411 folders = get_folders(values.get(KEY_UNTRACKED, {}))
412
413 out = dict((key, {}) for key in KEYS)
414 for key in KEYS:
415 for item, item_configs in values.get(key, {}).iteritems():
416 item_configs = remove_weak_dependencies(values, key, item, item_configs)
417 item_configs = remove_repeated_dependencies(
418 folders, key, item, item_configs)
419 if item_configs:
420 out[key][item] = item_configs
421 return out
422
423
424def convert_map_to_isolate_dict(values, config_variables):
425 """Regenerates back a .isolate configuration dict from files and dirs
426 mappings generated from reduce_inputs().
427 """
428 # Gather a list of configurations for set inversion later.
429 all_mentioned_configs = set()
430 for configs_by_item in values.itervalues():
431 for configs in configs_by_item.itervalues():
432 all_mentioned_configs.update(configs)
433
434 # Invert the mapping to make it dict first.
435 conditions = {}
436 for key in values:
437 for item, configs in values[key].iteritems():
438 then = conditions.setdefault(frozenset(configs), {})
439 variables = then.setdefault('variables', {})
440
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500441 if key == 'read_only':
442 if not isinstance(item, int):
443 raise isolateserver.ConfigError(
444 'Unexpected entry type %r for key %s' % (item, key))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500445 variables[key] = item
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500446 elif key == 'command':
447 if not isinstance(item, tuple):
448 raise isolateserver.ConfigError(
449 'Unexpected entry type %r for key %s' % (item, key))
450 if key in variables:
451 raise isolateserver.ConfigError('Unexpected duplicate key %s' % key)
452 if not item:
453 raise isolateserver.ConfigError(
454 'Expected non empty entry in %s' % key)
455 variables[key] = list(item)
456 elif key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED):
457 if not isinstance(item, basestring):
458 raise isolateserver.ConfigError('Unexpected entry type %r' % item)
459 if not item:
460 raise isolateserver.ConfigError(
461 'Expected non empty entry in %s' % key)
462 # The list of items (files or dirs). Append the new item and keep
463 # the list sorted.
464 l = variables.setdefault(key, [])
465 l.append(item)
466 l.sort()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500467 else:
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500468 raise isolateserver.ConfigError('Unexpected key %s' % key)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500469
470 if all_mentioned_configs:
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500471 # Change [(1, 2), (3, 4)] to [set(1, 3), set(2, 4)]
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500472 config_values = map(set, zip(*all_mentioned_configs))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500473 for i in config_values:
474 i.discard(None)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500475 sef = short_expression_finder.ShortExpressionFinder(
476 zip(config_variables, config_values))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500477 conditions = sorted([sef.get_expr(c), v] for c, v in conditions.iteritems())
478 else:
479 conditions = []
Marc-Antoine Rueld27c6632014-03-13 15:29:36 -0400480 out = {'conditions': conditions}
481 for c in conditions:
482 if c[0] == '':
483 # Extract the global.
484 out.update(c[1])
485 conditions.remove(c)
486 break
487 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500488
489
490class ConfigSettings(object):
491 """Represents the dependency variables for a single build configuration.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400492
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500493 The structure is immutable.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400494
495 .touch, .tracked and .untracked are the list of dependencies. The items in
496 these lists use '/' as a path separator.
497 .command and .isolate_dir describe how to run the command. .isolate_dir uses
498 the OS' native path separator. It must be an absolute path, it's the path
499 where to start the command from.
500 .read_only describe how to map the files.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500501 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400502 def __init__(self, values, isolate_dir):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500503 verify_variables(values)
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400504 if isolate_dir is None:
505 # It must be an empty object if isolate_dir is None.
506 assert values == {}, values
507 else:
508 # Otherwise, the path must be absolute.
509 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500510 self.touched = sorted(values.get(KEY_TOUCHED, []))
511 self.tracked = sorted(values.get(KEY_TRACKED, []))
512 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
513 self.command = values.get('command', [])[:]
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400514 self.isolate_dir = isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500515 self.read_only = values.get('read_only')
516
517 def union(self, rhs):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400518 """Merges two config settings together into a new instance.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500519
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400520 A new instance is not created and self or rhs is returned if the other
521 object is the empty object.
522
523 self has priority over rhs for .command. Use the same .isolate_dir as the
524 one having a .command.
525
526 Dependencies listed in rhs are patch adjusted ONLY if they don't start with
527 a path variable, e.g. the characters '<('.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500528 """
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400529 # When an object has .isolate_dir == None, it means it is the empty object.
530 if rhs.isolate_dir is None:
531 return self
532 if self.isolate_dir is None:
533 return rhs
534
535 if sys.platform == 'win32':
536 assert self.isolate_dir[0].lower() == rhs.isolate_dir[0].lower()
537
538 # Takes the difference between the two isolate_dir. Note that while
539 # isolate_dir is in native path case, all other references are in posix.
540 l_rel_cwd, r_rel_cwd = self.isolate_dir, rhs.isolate_dir
541 use_rhs = bool(not self.command and rhs.command)
542 if use_rhs:
543 # Rebase files in rhs.
544 l_rel_cwd, r_rel_cwd = r_rel_cwd, l_rel_cwd
545
546 rebase_path = os.path.relpath(r_rel_cwd, l_rel_cwd).replace(
547 os.path.sep, '/')
548 def rebase_item(f):
549 if f.startswith('<(') or rebase_path == '.':
550 return f
551 return posixpath.join(rebase_path, f)
552
553 def map_both(l, r):
554 """Rebase items in either lhs or rhs, as needed."""
555 if use_rhs:
556 l, r = r, l
557 return sorted(l + map(rebase_item, r))
558
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500559 var = {
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400560 KEY_TOUCHED: map_both(self.touched, rhs.touched),
561 KEY_TRACKED: map_both(self.tracked, rhs.tracked),
562 KEY_UNTRACKED: map_both(self.untracked, rhs.untracked),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500563 'command': self.command or rhs.command,
564 'read_only': rhs.read_only if self.read_only is None else self.read_only,
565 }
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400566 isolate_dir = self.isolate_dir if self.command else rhs.isolate_dir
567 return ConfigSettings(var, isolate_dir)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500568
569 def flatten(self):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400570 """Converts the object into a dict."""
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500571 out = {}
572 if self.command:
573 out['command'] = self.command
574 if self.touched:
575 out[KEY_TOUCHED] = self.touched
576 if self.tracked:
577 out[KEY_TRACKED] = self.tracked
578 if self.untracked:
579 out[KEY_UNTRACKED] = self.untracked
580 if self.read_only is not None:
581 out['read_only'] = self.read_only
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400582 # TODO(maruel): Probably better to not output it if command is None?
583 if self.isolate_dir is not None:
584 out['isolate_dir'] = self.isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500585 return out
586
587
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500588def _safe_index(l, k):
589 try:
590 return l.index(k)
591 except ValueError:
592 return None
593
594
595def _get_map_keys(dest_keys, in_keys):
596 """Returns a tuple of the indexes of each item in in_keys found in dest_keys.
597
598 For example, if in_keys is ('A', 'C') and dest_keys is ('A', 'B', 'C'), the
599 return value will be (0, None, 1).
600 """
601 return tuple(_safe_index(in_keys, k) for k in dest_keys)
602
603
604def _map_keys(mapping, items):
605 """Returns a tuple with items placed at mapping index.
606
607 For example, if mapping is (1, None, 0) and items is ('a', 'b'), it will
608 return ('b', None, 'c').
609 """
610 return tuple(items[i] if i != None else None for i in mapping)
611
612
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500613class Configs(object):
614 """Represents a processed .isolate file.
615
616 Stores the file in a processed way, split by configuration.
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500617
618 At this point, we don't know all the possibilities. So mount a partial view
619 that we have.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400620
621 This class doesn't hold isolate_dir, since it is dependent on the final
622 configuration selected. It is implicitly dependent on which .isolate defines
623 the 'command' that will take effect.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500624 """
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500625 def __init__(self, file_comment, config_variables):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500626 self.file_comment = file_comment
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500627 # Contains the names of the config variables seen while processing
628 # .isolate file(s). The order is important since the same order is used for
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500629 # keys in self._by_config.
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500630 assert isinstance(config_variables, tuple)
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400631 assert all(isinstance(c, basestring) for c in config_variables), (
632 config_variables)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400633 config_variables = tuple(config_variables)
634 assert tuple(sorted(config_variables)) == config_variables, config_variables
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500635 self._config_variables = config_variables
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500636 # The keys of _by_config are tuples of values for each of the items in
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500637 # self._config_variables. A None item in the list of the key means the value
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500638 # is unbounded.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500639 self._by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500640
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500641 @property
642 def config_variables(self):
643 return self._config_variables
644
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500645 def get_config(self, config):
646 """Returns all configs that matches this config as a single ConfigSettings.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400647
648 Returns an empty ConfigSettings if none apply.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500649 """
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400650 # TODO(maruel): Fix ordering based on the bounded values. The keys are not
651 # necessarily sorted in the way that makes sense, they are alphabetically
652 # sorted. It is important because the left-most takes predescence.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400653 out = ConfigSettings({}, None)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400654 for k, v in sorted(self._by_config.iteritems()):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500655 if all(i == j or j is None for i, j in zip(config, k)):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400656 out = out.union(v)
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500657 return out
658
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400659 def set_config(self, key, value):
660 """Sets the ConfigSettings for this key.
661
662 The key is a tuple of bounded or unbounded variables. The global variable
663 is the key where all values are unbounded, e.g.:
664 (None,) * len(self._config_variables)
665 """
666 assert key not in self._by_config, (key, self._by_config.keys())
667 assert isinstance(key, tuple)
668 assert len(key) == len(self._config_variables), (
669 key, self._config_variables)
670 assert isinstance(value, ConfigSettings)
671 self._by_config[key] = value
672
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500673 def union(self, rhs):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400674 """Returns a new Configs instance, the union of variables from self and rhs.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500675
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400676 Uses self.file_comment if available, otherwise rhs.file_comment.
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400677 It keeps config_variables sorted in the output.
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400678 """
679 # Merge the keys of config_variables for each Configs instances. All the new
680 # variables will become unbounded. This requires realigning the keys.
681 config_variables = tuple(sorted(
682 set(self.config_variables) | set(rhs.config_variables)))
683 out = Configs(self.file_comment or rhs.file_comment, config_variables)
684 mapping_lhs = _get_map_keys(out.config_variables, self.config_variables)
685 mapping_rhs = _get_map_keys(out.config_variables, rhs.config_variables)
686 lhs_config = dict(
687 (_map_keys(mapping_lhs, k), v) for k, v in self._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500688 # pylint: disable=W0212
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400689 rhs_config = dict(
690 (_map_keys(mapping_rhs, k), v) for k, v in rhs._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500691
692 for key in set(lhs_config) | set(rhs_config):
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400693 l = lhs_config.get(key)
694 r = rhs_config.get(key)
695 out.set_config(key, l.union(r) if (l and r) else (l or r))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500696 return out
697
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500698 def flatten(self):
699 """Returns a flat dictionary representation of the configuration.
700 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500701 return dict((k, v.flatten()) for k, v in self._by_config.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500702
703 def make_isolate_file(self):
704 """Returns a dictionary suitable for writing to a .isolate file.
705 """
706 dependencies_by_config = self.flatten()
707 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
708 return convert_map_to_isolate_dict(configs_by_dependency,
709 self.config_variables)
710
711
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500712def load_isolate_as_config(isolate_dir, value, file_comment):
713 """Parses one .isolate file and returns a Configs() instance.
714
715 Arguments:
716 isolate_dir: only used to load relative includes so it doesn't depend on
717 cwd.
718 value: is the loaded dictionary that was defined in the gyp file.
719 file_comment: comments found at the top of the file so it can be preserved.
720
721 The expected format is strict, anything diverting from the format below will
722 throw an assert:
723 {
724 'includes': [
725 'foo.isolate',
726 ],
727 'conditions': [
728 ['OS=="vms" and foo=42', {
729 'variables': {
730 'command': [
731 ...
732 ],
733 'isolate_dependency_tracked': [
734 ...
735 ],
736 'isolate_dependency_untracked': [
737 ...
738 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500739 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500740 },
741 }],
742 ...
743 ],
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400744 'variables': {
745 ...
746 },
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500747 }
748 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400749 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400750 if any(len(cond) == 3 for cond in value.get('conditions', [])):
751 raise isolateserver.ConfigError('Using \'else\' is not supported anymore.')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500752 variables_and_values = {}
753 verify_root(value, variables_and_values)
754 if variables_and_values:
755 config_variables, config_values = zip(
756 *sorted(variables_and_values.iteritems()))
757 all_configs = list(itertools.product(*config_values))
758 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500759 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500760 all_configs = []
761
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500762 isolate = Configs(file_comment, config_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500763
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400764 # Add global variables. The global variables are on the empty tuple key.
765 isolate.set_config(
766 (None,) * len(config_variables),
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400767 ConfigSettings(value.get('variables', {}), isolate_dir))
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400768
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500769 # Add configuration-specific variables.
770 for expr, then in value.get('conditions', []):
771 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500772 new = Configs(None, config_variables)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500773 for config in configs:
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400774 new.set_config(config, ConfigSettings(then['variables'], isolate_dir))
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500775 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500776
777 # Load the includes. Process them in reverse so the last one take precedence.
778 for include in reversed(value.get('includes', [])):
779 if os.path.isabs(include):
780 raise isolateserver.ConfigError(
781 'Failed to load configuration; absolute include path \'%s\'' %
782 include)
783 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400784 if sys.platform == 'win32':
785 if included_isolate[0].lower() != isolate_dir[0].lower():
786 raise isolateserver.ConfigError(
787 'Can\'t reference a .isolate file from another drive')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500788 with open(included_isolate, 'r') as f:
789 included_isolate = load_isolate_as_config(
790 os.path.dirname(included_isolate),
791 eval_content(f.read()),
792 None)
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400793 isolate = isolate.union(included_isolate)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500794
795 return isolate
796
797
798def load_isolate_for_config(isolate_dir, content, config_variables):
799 """Loads the .isolate file and returns the information unprocessed but
800 filtered for the specific OS.
801
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400802 Returns:
803 tuple of command, dependencies, touched, read_only flag, isolate_dir.
804 The dependencies are fixed to use os.path.sep.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500805 """
806 # Load the .isolate file, process its conditions, retrieve the command and
807 # dependencies.
808 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
809 try:
810 config_name = tuple(
811 config_variables[var] for var in isolate.config_variables)
812 except KeyError:
813 raise isolateserver.ConfigError(
814 'These configuration variables were missing from the command line: %s' %
815 ', '.join(
816 sorted(set(isolate.config_variables) - set(config_variables))))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500817
818 # A configuration is to be created with all the combinations of free
819 # variables.
820 config = isolate.get_config(config_name)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500821 # Merge tracked and untracked variables, isolate.py doesn't care about the
822 # trackability of the variables, only the build tool does.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400823 dependencies = sorted(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500824 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400825 )
826 touched = sorted(f.replace('/', os.path.sep) for f in config.touched)
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400827 return (
828 config.command, dependencies, touched, config.read_only,
829 config.isolate_dir)