blob: 3dccccf838480774a3782c0035b24e66d4e57b5a [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
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050023from 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
Marc-Antoine Ruele819be42014-08-28 19:38:20 -040036class IsolateError(ValueError):
37 """Generic failure to load a .isolate file."""
38 pass
39
40
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050041def determine_root_dir(relative_root, infiles):
42 """For a list of infiles, determines the deepest root directory that is
43 referenced indirectly.
44
45 All arguments must be using os.path.sep.
46 """
47 # The trick used to determine the root directory is to look at "how far" back
48 # up it is looking up.
49 deepest_root = relative_root
50 for i in infiles:
51 x = relative_root
52 while i.startswith('..' + os.path.sep):
53 i = i[3:]
54 assert not i.startswith(os.path.sep)
55 x = os.path.dirname(x)
56 if deepest_root.startswith(x):
57 deepest_root = x
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -040058 logging.info(
59 'determine_root_dir(%s, %d files) -> %s',
60 relative_root, len(infiles), deepest_root)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050061 return deepest_root
62
63
64def replace_variable(part, variables):
65 m = re.match(r'<\((' + VALID_VARIABLE + ')\)', part)
66 if m:
67 if m.group(1) not in variables:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -040068 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050069 'Variable "%s" was not found in %s.\nDid you forget to specify '
70 '--path-variable?' % (m.group(1), variables))
John Abd-El-Malek37bcce22014-09-29 11:11:31 -070071 return str(variables[m.group(1)])
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050072 return part
73
74
75def eval_variables(item, variables):
76 """Replaces the .isolate variables in a string item.
77
78 Note that the .isolate format is a subset of the .gyp dialect.
79 """
80 return ''.join(
81 replace_variable(p, variables)
82 for p in re.split(r'(<\(' + VALID_VARIABLE + '\))', item))
83
84
85def split_touched(files):
86 """Splits files that are touched vs files that are read."""
87 tracked = []
88 touched = []
89 for f in files:
90 if f.size:
91 tracked.append(f)
92 else:
93 touched.append(f)
94 return tracked, touched
95
96
97def pretty_print(variables, stdout):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -040098 """Outputs a .isolate file from the decoded variables.
99
100 The .isolate format is GYP compatible.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500101
102 Similar to pprint.print() but with NIH syndrome.
103 """
104 # Order the dictionary keys by these keys in priority.
105 ORDER = (
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400106 'variables', 'condition', 'command', 'read_only',
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500107 KEY_TRACKED, KEY_UNTRACKED)
108
109 def sorting_key(x):
110 """Gives priority to 'most important' keys before the others."""
111 if x in ORDER:
112 return str(ORDER.index(x))
113 return x
114
115 def loop_list(indent, items):
116 for item in items:
117 if isinstance(item, basestring):
118 stdout.write('%s\'%s\',\n' % (indent, item))
119 elif isinstance(item, dict):
120 stdout.write('%s{\n' % indent)
121 loop_dict(indent + ' ', item)
122 stdout.write('%s},\n' % indent)
123 elif isinstance(item, list):
124 # A list inside a list will write the first item embedded.
125 stdout.write('%s[' % indent)
126 for index, i in enumerate(item):
127 if isinstance(i, basestring):
128 stdout.write(
129 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
130 elif isinstance(i, dict):
131 stdout.write('{\n')
132 loop_dict(indent + ' ', i)
133 if index != len(item) - 1:
134 x = ', '
135 else:
136 x = ''
137 stdout.write('%s}%s' % (indent, x))
138 else:
139 assert False
140 stdout.write('],\n')
141 else:
142 assert False
143
144 def loop_dict(indent, items):
145 for key in sorted(items, key=sorting_key):
146 item = items[key]
147 stdout.write("%s'%s': " % (indent, key))
148 if isinstance(item, dict):
149 stdout.write('{\n')
150 loop_dict(indent + ' ', item)
151 stdout.write(indent + '},\n')
152 elif isinstance(item, list):
153 stdout.write('[\n')
154 loop_list(indent + ' ', item)
155 stdout.write(indent + '],\n')
156 elif isinstance(item, basestring):
157 stdout.write(
158 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500159 elif isinstance(item, (int, bool)) or item is None:
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400160 stdout.write('%s,\n' % item)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500161 else:
162 assert False, item
163
164 stdout.write('{\n')
165 loop_dict(' ', variables)
166 stdout.write('}\n')
167
168
169def print_all(comment, data, stream):
170 """Prints a complete .isolate file and its top-level file comment into a
171 stream.
172 """
173 if comment:
174 stream.write(comment)
175 pretty_print(data, stream)
176
177
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500178def extract_comment(content):
179 """Extracts file level comment."""
180 out = []
181 for line in content.splitlines(True):
182 if line.startswith('#'):
183 out.append(line)
184 else:
185 break
186 return ''.join(out)
187
188
189def eval_content(content):
190 """Evaluates a python file and return the value defined in it.
191
192 Used in practice for .isolate files.
193 """
194 globs = {'__builtins__': None}
195 locs = {}
196 try:
197 value = eval(content, globs, locs)
198 except TypeError as e:
199 e.args = list(e.args) + [content]
200 raise
201 assert locs == {}, locs
202 assert globs == {'__builtins__': None}, globs
203 return value
204
205
206def match_configs(expr, config_variables, all_configs):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500207 """Returns the list of values from |values| that match the condition |expr|.
208
209 Arguments:
210 expr: string that is evaluatable with eval(). It is a GYP condition.
211 config_variables: list of the name of the variables.
212 all_configs: list of the list of possible values.
213
214 If a variable is not referenced at all, it is marked as unbounded (free) with
215 a value set to None.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500216 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500217 # It is more than just eval'ing the variable, it needs to be double checked to
218 # see if the variable is referenced at all. If not, the variable is free
219 # (unbounded).
220 # TODO(maruel): Use the intelligent way by inspecting expr instead of doing
221 # trial and error to figure out which variable is bound.
222 combinations = []
223 for bound_variables in itertools.product(
224 (True, False), repeat=len(config_variables)):
225 # Add the combination of variables bound.
226 combinations.append(
227 (
228 [c for c, b in zip(config_variables, bound_variables) if b],
229 set(
230 tuple(v if b else None for v, b in zip(line, bound_variables))
231 for line in all_configs)
232 ))
233
234 out = []
235 for variables, configs in combinations:
236 # Strip variables and see if expr can still be evaluated.
237 for values in configs:
238 globs = {'__builtins__': None}
239 globs.update(zip(variables, (v for v in values if v is not None)))
240 try:
241 assertion = eval(expr, globs, {})
242 except NameError:
243 continue
244 if not isinstance(assertion, bool):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400245 raise IsolateError('Invalid condition')
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500246 if assertion:
247 out.append(values)
248 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500249
250
251def verify_variables(variables):
252 """Verifies the |variables| dictionary is in the expected format."""
253 VALID_VARIABLES = [
254 KEY_TOUCHED,
255 KEY_TRACKED,
256 KEY_UNTRACKED,
257 'command',
258 'read_only',
259 ]
260 assert isinstance(variables, dict), variables
261 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
262 for name, value in variables.iteritems():
263 if name == 'read_only':
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500264 assert value in (0, 1, 2, None), value
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500265 else:
266 assert isinstance(value, list), value
267 assert all(isinstance(i, basestring) for i in value), value
268
269
270def verify_ast(expr, variables_and_values):
271 """Verifies that |expr| is of the form
272 expr ::= expr ( "or" | "and" ) expr
273 | identifier "==" ( string | int )
274 Also collects the variable identifiers and string/int values in the dict
275 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
276 """
277 assert isinstance(expr, (ast.BoolOp, ast.Compare))
278 if isinstance(expr, ast.BoolOp):
279 assert isinstance(expr.op, (ast.And, ast.Or))
280 for subexpr in expr.values:
281 verify_ast(subexpr, variables_and_values)
282 else:
283 assert isinstance(expr.left.ctx, ast.Load)
284 assert len(expr.ops) == 1
285 assert isinstance(expr.ops[0], ast.Eq)
286 var_values = variables_and_values.setdefault(expr.left.id, set())
287 rhs = expr.comparators[0]
288 assert isinstance(rhs, (ast.Str, ast.Num))
289 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
290
291
292def verify_condition(condition, variables_and_values):
293 """Verifies the |condition| dictionary is in the expected format.
294 See verify_ast() for the meaning of |variables_and_values|.
295 """
296 VALID_INSIDE_CONDITION = ['variables']
297 assert isinstance(condition, list), condition
298 assert len(condition) == 2, condition
299 expr, then = condition
300
301 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
302 verify_ast(test_ast.body, variables_and_values)
303
304 assert isinstance(then, dict), then
305 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
306 if not 'variables' in then:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400307 raise IsolateError('Missing \'variables\' in condition %s' % condition)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500308 verify_variables(then['variables'])
309
310
311def verify_root(value, variables_and_values):
312 """Verifies that |value| is the parsed form of a valid .isolate file.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400313
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500314 See verify_ast() for the meaning of |variables_and_values|.
315 """
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400316 VALID_ROOTS = ['includes', 'conditions', 'variables']
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500317 assert isinstance(value, dict), value
318 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
319
320 includes = value.get('includes', [])
321 assert isinstance(includes, list), includes
322 for include in includes:
323 assert isinstance(include, basestring), include
324
325 conditions = value.get('conditions', [])
326 assert isinstance(conditions, list), conditions
327 for condition in conditions:
328 verify_condition(condition, variables_and_values)
329
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400330 variables = value.get('variables', {})
331 verify_variables(variables)
332
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500333
334def remove_weak_dependencies(values, key, item, item_configs):
335 """Removes any configs from this key if the item is already under a
336 strong key.
337 """
338 if key == KEY_TOUCHED:
339 item_configs = set(item_configs)
340 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
341 try:
342 item_configs -= values[stronger_key][item]
343 except KeyError:
344 pass
345
346 return item_configs
347
348
349def remove_repeated_dependencies(folders, key, item, item_configs):
350 """Removes any configs from this key if the item is in a folder that is
351 already included."""
352
353 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
354 item_configs = set(item_configs)
355 for (folder, configs) in folders.iteritems():
356 if folder != item and item.startswith(folder):
357 item_configs -= configs
358
359 return item_configs
360
361
362def get_folders(values_dict):
363 """Returns a dict of all the folders in the given value_dict."""
364 return dict(
365 (item, configs) for (item, configs) in values_dict.iteritems()
366 if item.endswith('/')
367 )
368
369
370def invert_map(variables):
371 """Converts {config: {deptype: list(depvals)}} to
372 {deptype: {depval: set(configs)}}.
373 """
374 KEYS = (
375 KEY_TOUCHED,
376 KEY_TRACKED,
377 KEY_UNTRACKED,
378 'command',
379 'read_only',
380 )
381 out = dict((key, {}) for key in KEYS)
382 for config, values in variables.iteritems():
383 for key in KEYS:
384 if key == 'command':
385 items = [tuple(values[key])] if key in values else []
386 elif key == 'read_only':
387 items = [values[key]] if key in values else []
388 else:
389 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
390 items = values.get(key, [])
391 for item in items:
392 out[key].setdefault(item, set()).add(config)
393 return out
394
395
396def reduce_inputs(values):
397 """Reduces the output of invert_map() to the strictest minimum list.
398
399 Looks at each individual file and directory, maps where they are used and
400 reconstructs the inverse dictionary.
401
402 Returns the minimized dictionary.
403 """
404 KEYS = (
405 KEY_TOUCHED,
406 KEY_TRACKED,
407 KEY_UNTRACKED,
408 'command',
409 'read_only',
410 )
411
412 # Folders can only live in KEY_UNTRACKED.
413 folders = get_folders(values.get(KEY_UNTRACKED, {}))
414
415 out = dict((key, {}) for key in KEYS)
416 for key in KEYS:
417 for item, item_configs in values.get(key, {}).iteritems():
418 item_configs = remove_weak_dependencies(values, key, item, item_configs)
419 item_configs = remove_repeated_dependencies(
420 folders, key, item, item_configs)
421 if item_configs:
422 out[key][item] = item_configs
423 return out
424
425
426def convert_map_to_isolate_dict(values, config_variables):
427 """Regenerates back a .isolate configuration dict from files and dirs
428 mappings generated from reduce_inputs().
429 """
430 # Gather a list of configurations for set inversion later.
431 all_mentioned_configs = set()
432 for configs_by_item in values.itervalues():
433 for configs in configs_by_item.itervalues():
434 all_mentioned_configs.update(configs)
435
436 # Invert the mapping to make it dict first.
437 conditions = {}
438 for key in values:
439 for item, configs in values[key].iteritems():
440 then = conditions.setdefault(frozenset(configs), {})
441 variables = then.setdefault('variables', {})
442
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500443 if key == 'read_only':
444 if not isinstance(item, int):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400445 raise IsolateError(
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500446 'Unexpected entry type %r for key %s' % (item, key))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500447 variables[key] = item
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500448 elif key == 'command':
449 if not isinstance(item, tuple):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400450 raise IsolateError(
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500451 'Unexpected entry type %r for key %s' % (item, key))
452 if key in variables:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400453 raise IsolateError('Unexpected duplicate key %s' % key)
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500454 if not item:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400455 raise IsolateError('Expected non empty entry in %s' % key)
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500456 variables[key] = list(item)
457 elif key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED):
458 if not isinstance(item, basestring):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400459 raise IsolateError('Unexpected entry type %r' % item)
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500460 if not item:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400461 raise IsolateError('Expected non empty entry in %s' % key)
Marc-Antoine Ruelbb20b6d2014-01-10 18:47:47 -0500462 # 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 Ruele819be42014-08-28 19:38:20 -0400468 raise IsolateError('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', [])):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400769 raise IsolateError('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):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400798 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500799 '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():
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400804 raise IsolateError(
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400805 '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:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400831 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500832 '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)