blob: 80624dbcdd0a11739d41dd41b1f6c97f146581ca [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
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -040055 logging.info(
56 'determine_root_dir(%s, %d files) -> %s',
57 relative_root, len(infiles), deepest_root)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050058 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
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400541 if self.command or rhs.command:
542 use_rhs = bool(not self.command and rhs.command)
543 else:
544 # If self doesn't define any file, use rhs.
545 use_rhs = not bool(self.touched or self.tracked or self.untracked)
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400546 if use_rhs:
547 # Rebase files in rhs.
548 l_rel_cwd, r_rel_cwd = r_rel_cwd, l_rel_cwd
549
550 rebase_path = os.path.relpath(r_rel_cwd, l_rel_cwd).replace(
551 os.path.sep, '/')
552 def rebase_item(f):
553 if f.startswith('<(') or rebase_path == '.':
554 return f
555 return posixpath.join(rebase_path, f)
556
557 def map_both(l, r):
558 """Rebase items in either lhs or rhs, as needed."""
559 if use_rhs:
560 l, r = r, l
561 return sorted(l + map(rebase_item, r))
562
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500563 var = {
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400564 KEY_TOUCHED: map_both(self.touched, rhs.touched),
565 KEY_TRACKED: map_both(self.tracked, rhs.tracked),
566 KEY_UNTRACKED: map_both(self.untracked, rhs.untracked),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500567 'command': self.command or rhs.command,
568 'read_only': rhs.read_only if self.read_only is None else self.read_only,
569 }
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400570 return ConfigSettings(var, l_rel_cwd)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500571
572 def flatten(self):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400573 """Converts the object into a dict."""
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500574 out = {}
575 if self.command:
576 out['command'] = self.command
577 if self.touched:
578 out[KEY_TOUCHED] = self.touched
579 if self.tracked:
580 out[KEY_TRACKED] = self.tracked
581 if self.untracked:
582 out[KEY_UNTRACKED] = self.untracked
583 if self.read_only is not None:
584 out['read_only'] = self.read_only
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400585 # TODO(maruel): Probably better to not output it if command is None?
586 if self.isolate_dir is not None:
587 out['isolate_dir'] = self.isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500588 return out
589
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400590 def __str__(self):
591 """Returns a short representation useful for debugging."""
592 files = ''.join(
593 '\n ' + f for f in (self.touched + self.tracked + self.untracked))
594 return 'ConfigSettings(%s, %s, %s, %s)' % (
595 self.command,
596 self.isolate_dir,
597 self.read_only,
598 files or '[]')
599
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500600
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500601def _safe_index(l, k):
602 try:
603 return l.index(k)
604 except ValueError:
605 return None
606
607
608def _get_map_keys(dest_keys, in_keys):
609 """Returns a tuple of the indexes of each item in in_keys found in dest_keys.
610
611 For example, if in_keys is ('A', 'C') and dest_keys is ('A', 'B', 'C'), the
612 return value will be (0, None, 1).
613 """
614 return tuple(_safe_index(in_keys, k) for k in dest_keys)
615
616
617def _map_keys(mapping, items):
618 """Returns a tuple with items placed at mapping index.
619
620 For example, if mapping is (1, None, 0) and items is ('a', 'b'), it will
621 return ('b', None, 'c').
622 """
623 return tuple(items[i] if i != None else None for i in mapping)
624
625
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500626class Configs(object):
627 """Represents a processed .isolate file.
628
629 Stores the file in a processed way, split by configuration.
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500630
631 At this point, we don't know all the possibilities. So mount a partial view
632 that we have.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400633
634 This class doesn't hold isolate_dir, since it is dependent on the final
635 configuration selected. It is implicitly dependent on which .isolate defines
636 the 'command' that will take effect.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500637 """
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500638 def __init__(self, file_comment, config_variables):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500639 self.file_comment = file_comment
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500640 # Contains the names of the config variables seen while processing
641 # .isolate file(s). The order is important since the same order is used for
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500642 # keys in self._by_config.
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500643 assert isinstance(config_variables, tuple)
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400644 assert all(isinstance(c, basestring) for c in config_variables), (
645 config_variables)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400646 config_variables = tuple(config_variables)
647 assert tuple(sorted(config_variables)) == config_variables, config_variables
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500648 self._config_variables = config_variables
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500649 # The keys of _by_config are tuples of values for each of the items in
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500650 # self._config_variables. A None item in the list of the key means the value
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500651 # is unbounded.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500652 self._by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500653
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500654 @property
655 def config_variables(self):
656 return self._config_variables
657
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500658 def get_config(self, config):
659 """Returns all configs that matches this config as a single ConfigSettings.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400660
661 Returns an empty ConfigSettings if none apply.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500662 """
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400663 # TODO(maruel): Fix ordering based on the bounded values. The keys are not
664 # necessarily sorted in the way that makes sense, they are alphabetically
665 # sorted. It is important because the left-most takes predescence.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400666 out = ConfigSettings({}, None)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400667 for k, v in sorted(self._by_config.iteritems()):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500668 if all(i == j or j is None for i, j in zip(config, k)):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400669 out = out.union(v)
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500670 return out
671
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400672 def set_config(self, key, value):
673 """Sets the ConfigSettings for this key.
674
675 The key is a tuple of bounded or unbounded variables. The global variable
676 is the key where all values are unbounded, e.g.:
677 (None,) * len(self._config_variables)
678 """
679 assert key not in self._by_config, (key, self._by_config.keys())
680 assert isinstance(key, tuple)
681 assert len(key) == len(self._config_variables), (
682 key, self._config_variables)
683 assert isinstance(value, ConfigSettings)
684 self._by_config[key] = value
685
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500686 def union(self, rhs):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400687 """Returns a new Configs instance, the union of variables from self and rhs.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500688
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400689 Uses self.file_comment if available, otherwise rhs.file_comment.
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400690 It keeps config_variables sorted in the output.
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400691 """
692 # Merge the keys of config_variables for each Configs instances. All the new
693 # variables will become unbounded. This requires realigning the keys.
694 config_variables = tuple(sorted(
695 set(self.config_variables) | set(rhs.config_variables)))
696 out = Configs(self.file_comment or rhs.file_comment, config_variables)
697 mapping_lhs = _get_map_keys(out.config_variables, self.config_variables)
698 mapping_rhs = _get_map_keys(out.config_variables, rhs.config_variables)
699 lhs_config = dict(
700 (_map_keys(mapping_lhs, k), v) for k, v in self._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500701 # pylint: disable=W0212
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400702 rhs_config = dict(
703 (_map_keys(mapping_rhs, k), v) for k, v in rhs._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500704
705 for key in set(lhs_config) | set(rhs_config):
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400706 l = lhs_config.get(key)
707 r = rhs_config.get(key)
708 out.set_config(key, l.union(r) if (l and r) else (l or r))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500709 return out
710
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500711 def flatten(self):
712 """Returns a flat dictionary representation of the configuration.
713 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500714 return dict((k, v.flatten()) for k, v in self._by_config.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500715
716 def make_isolate_file(self):
717 """Returns a dictionary suitable for writing to a .isolate file.
718 """
719 dependencies_by_config = self.flatten()
720 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
721 return convert_map_to_isolate_dict(configs_by_dependency,
722 self.config_variables)
723
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400724 def __str__(self):
725 return 'Configs(%s,%s)' % (
726 self._config_variables,
727 ''.join('\n %s' % str(f) for f in self._by_config))
728
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500729
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500730def load_isolate_as_config(isolate_dir, value, file_comment):
731 """Parses one .isolate file and returns a Configs() instance.
732
733 Arguments:
734 isolate_dir: only used to load relative includes so it doesn't depend on
735 cwd.
736 value: is the loaded dictionary that was defined in the gyp file.
737 file_comment: comments found at the top of the file so it can be preserved.
738
739 The expected format is strict, anything diverting from the format below will
740 throw an assert:
741 {
742 'includes': [
743 'foo.isolate',
744 ],
745 'conditions': [
746 ['OS=="vms" and foo=42', {
747 'variables': {
748 'command': [
749 ...
750 ],
751 'isolate_dependency_tracked': [
752 ...
753 ],
754 'isolate_dependency_untracked': [
755 ...
756 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500757 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500758 },
759 }],
760 ...
761 ],
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400762 'variables': {
763 ...
764 },
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500765 }
766 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400767 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400768 if any(len(cond) == 3 for cond in value.get('conditions', [])):
769 raise isolateserver.ConfigError('Using \'else\' is not supported anymore.')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500770 variables_and_values = {}
771 verify_root(value, variables_and_values)
772 if variables_and_values:
773 config_variables, config_values = zip(
774 *sorted(variables_and_values.iteritems()))
775 all_configs = list(itertools.product(*config_values))
776 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500777 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500778 all_configs = []
779
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500780 isolate = Configs(file_comment, config_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500781
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400782 # Add global variables. The global variables are on the empty tuple key.
783 isolate.set_config(
784 (None,) * len(config_variables),
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400785 ConfigSettings(value.get('variables', {}), isolate_dir))
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400786
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500787 # Add configuration-specific variables.
788 for expr, then in value.get('conditions', []):
789 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500790 new = Configs(None, config_variables)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500791 for config in configs:
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400792 new.set_config(config, ConfigSettings(then['variables'], isolate_dir))
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500793 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500794
795 # Load the includes. Process them in reverse so the last one take precedence.
796 for include in reversed(value.get('includes', [])):
797 if os.path.isabs(include):
798 raise isolateserver.ConfigError(
799 'Failed to load configuration; absolute include path \'%s\'' %
800 include)
801 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400802 if sys.platform == 'win32':
803 if included_isolate[0].lower() != isolate_dir[0].lower():
804 raise isolateserver.ConfigError(
805 'Can\'t reference a .isolate file from another drive')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500806 with open(included_isolate, 'r') as f:
807 included_isolate = load_isolate_as_config(
808 os.path.dirname(included_isolate),
809 eval_content(f.read()),
810 None)
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400811 isolate = isolate.union(included_isolate)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500812
813 return isolate
814
815
816def load_isolate_for_config(isolate_dir, content, config_variables):
817 """Loads the .isolate file and returns the information unprocessed but
818 filtered for the specific OS.
819
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400820 Returns:
821 tuple of command, dependencies, touched, read_only flag, isolate_dir.
822 The dependencies are fixed to use os.path.sep.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500823 """
824 # Load the .isolate file, process its conditions, retrieve the command and
825 # dependencies.
826 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
827 try:
828 config_name = tuple(
829 config_variables[var] for var in isolate.config_variables)
830 except KeyError:
831 raise isolateserver.ConfigError(
832 'These configuration variables were missing from the command line: %s' %
833 ', '.join(
834 sorted(set(isolate.config_variables) - set(config_variables))))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500835
836 # A configuration is to be created with all the combinations of free
837 # variables.
838 config = isolate.get_config(config_name)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500839 # Merge tracked and untracked variables, isolate.py doesn't care about the
840 # trackability of the variables, only the build tool does.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400841 dependencies = sorted(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500842 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400843 )
844 touched = sorted(f.replace('/', os.path.sep) for f in config.touched)
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400845 return (
846 config.command, dependencies, touched, config.read_only,
847 config.isolate_dir)