blob: 5711ac0ef53b1a97ee5029aad6f53cda76a0017e [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 -050023
24# Files that should be 0-length when mapped.
25KEY_TOUCHED = 'isolate_dependency_touched'
26# Files that should be tracked by the build tool.
27KEY_TRACKED = 'isolate_dependency_tracked'
28# Files that should not be tracked by the build tool.
29KEY_UNTRACKED = 'isolate_dependency_untracked'
30
31# Valid variable name.
32VALID_VARIABLE = '[A-Za-z_][A-Za-z_0-9]*'
33
34
Marc-Antoine Ruele819be42014-08-28 19:38:20 -040035class IsolateError(ValueError):
36 """Generic failure to load a .isolate file."""
37 pass
38
39
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050040def determine_root_dir(relative_root, infiles):
41 """For a list of infiles, determines the deepest root directory that is
42 referenced indirectly.
43
44 All arguments must be using os.path.sep.
45 """
46 # The trick used to determine the root directory is to look at "how far" back
47 # up it is looking up.
48 deepest_root = relative_root
49 for i in infiles:
50 x = relative_root
51 while i.startswith('..' + os.path.sep):
52 i = i[3:]
53 assert not i.startswith(os.path.sep)
54 x = os.path.dirname(x)
55 if deepest_root.startswith(x):
56 deepest_root = x
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -040057 logging.info(
58 'determine_root_dir(%s, %d files) -> %s',
59 relative_root, len(infiles), deepest_root)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050060 return deepest_root
61
62
63def replace_variable(part, variables):
64 m = re.match(r'<\((' + VALID_VARIABLE + ')\)', part)
65 if m:
66 if m.group(1) not in variables:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -040067 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050068 'Variable "%s" was not found in %s.\nDid you forget to specify '
69 '--path-variable?' % (m.group(1), variables))
John Abd-El-Malek37bcce22014-09-29 11:11:31 -070070 return str(variables[m.group(1)])
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050071 return part
72
73
74def eval_variables(item, variables):
75 """Replaces the .isolate variables in a string item.
76
77 Note that the .isolate format is a subset of the .gyp dialect.
78 """
79 return ''.join(
80 replace_variable(p, variables)
81 for p in re.split(r'(<\(' + VALID_VARIABLE + '\))', item))
82
83
84def split_touched(files):
85 """Splits files that are touched vs files that are read."""
86 tracked = []
87 touched = []
88 for f in files:
89 if f.size:
90 tracked.append(f)
91 else:
92 touched.append(f)
93 return tracked, touched
94
95
96def pretty_print(variables, stdout):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -040097 """Outputs a .isolate file from the decoded variables.
98
99 The .isolate format is GYP compatible.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500100
101 Similar to pprint.print() but with NIH syndrome.
102 """
103 # Order the dictionary keys by these keys in priority.
104 ORDER = (
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400105 'variables', 'condition', 'command', 'read_only',
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500106 KEY_TRACKED, KEY_UNTRACKED)
107
108 def sorting_key(x):
109 """Gives priority to 'most important' keys before the others."""
110 if x in ORDER:
111 return str(ORDER.index(x))
112 return x
113
114 def loop_list(indent, items):
115 for item in items:
116 if isinstance(item, basestring):
117 stdout.write('%s\'%s\',\n' % (indent, item))
118 elif isinstance(item, dict):
119 stdout.write('%s{\n' % indent)
120 loop_dict(indent + ' ', item)
121 stdout.write('%s},\n' % indent)
122 elif isinstance(item, list):
123 # A list inside a list will write the first item embedded.
124 stdout.write('%s[' % indent)
125 for index, i in enumerate(item):
126 if isinstance(i, basestring):
127 stdout.write(
128 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
129 elif isinstance(i, dict):
130 stdout.write('{\n')
131 loop_dict(indent + ' ', i)
132 if index != len(item) - 1:
133 x = ', '
134 else:
135 x = ''
136 stdout.write('%s}%s' % (indent, x))
137 else:
138 assert False
139 stdout.write('],\n')
140 else:
141 assert False
142
143 def loop_dict(indent, items):
144 for key in sorted(items, key=sorting_key):
145 item = items[key]
146 stdout.write("%s'%s': " % (indent, key))
147 if isinstance(item, dict):
148 stdout.write('{\n')
149 loop_dict(indent + ' ', item)
150 stdout.write(indent + '},\n')
151 elif isinstance(item, list):
152 stdout.write('[\n')
153 loop_list(indent + ' ', item)
154 stdout.write(indent + '],\n')
155 elif isinstance(item, basestring):
156 stdout.write(
157 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500158 elif isinstance(item, (int, bool)) or item is None:
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400159 stdout.write('%s,\n' % item)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500160 else:
161 assert False, item
162
163 stdout.write('{\n')
164 loop_dict(' ', variables)
165 stdout.write('}\n')
166
167
168def print_all(comment, data, stream):
169 """Prints a complete .isolate file and its top-level file comment into a
170 stream.
171 """
172 if comment:
173 stream.write(comment)
174 pretty_print(data, stream)
175
176
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500177def extract_comment(content):
178 """Extracts file level comment."""
179 out = []
180 for line in content.splitlines(True):
181 if line.startswith('#'):
182 out.append(line)
183 else:
184 break
185 return ''.join(out)
186
187
188def eval_content(content):
189 """Evaluates a python file and return the value defined in it.
190
191 Used in practice for .isolate files.
192 """
193 globs = {'__builtins__': None}
194 locs = {}
195 try:
196 value = eval(content, globs, locs)
197 except TypeError as e:
198 e.args = list(e.args) + [content]
199 raise
200 assert locs == {}, locs
201 assert globs == {'__builtins__': None}, globs
202 return value
203
204
205def match_configs(expr, config_variables, all_configs):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500206 """Returns the list of values from |values| that match the condition |expr|.
207
208 Arguments:
209 expr: string that is evaluatable with eval(). It is a GYP condition.
210 config_variables: list of the name of the variables.
211 all_configs: list of the list of possible values.
212
213 If a variable is not referenced at all, it is marked as unbounded (free) with
214 a value set to None.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500215 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500216 # It is more than just eval'ing the variable, it needs to be double checked to
217 # see if the variable is referenced at all. If not, the variable is free
218 # (unbounded).
219 # TODO(maruel): Use the intelligent way by inspecting expr instead of doing
220 # trial and error to figure out which variable is bound.
221 combinations = []
222 for bound_variables in itertools.product(
223 (True, False), repeat=len(config_variables)):
224 # Add the combination of variables bound.
225 combinations.append(
226 (
227 [c for c, b in zip(config_variables, bound_variables) if b],
228 set(
229 tuple(v if b else None for v, b in zip(line, bound_variables))
230 for line in all_configs)
231 ))
232
233 out = []
234 for variables, configs in combinations:
235 # Strip variables and see if expr can still be evaluated.
236 for values in configs:
237 globs = {'__builtins__': None}
238 globs.update(zip(variables, (v for v in values if v is not None)))
239 try:
240 assertion = eval(expr, globs, {})
241 except NameError:
242 continue
243 if not isinstance(assertion, bool):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400244 raise IsolateError('Invalid condition')
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500245 if assertion:
246 out.append(values)
247 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500248
249
250def verify_variables(variables):
251 """Verifies the |variables| dictionary is in the expected format."""
252 VALID_VARIABLES = [
253 KEY_TOUCHED,
254 KEY_TRACKED,
255 KEY_UNTRACKED,
256 'command',
257 'read_only',
258 ]
259 assert isinstance(variables, dict), variables
260 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
261 for name, value in variables.iteritems():
262 if name == 'read_only':
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500263 assert value in (0, 1, 2, None), value
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500264 else:
265 assert isinstance(value, list), value
266 assert all(isinstance(i, basestring) for i in value), value
267
268
269def verify_ast(expr, variables_and_values):
270 """Verifies that |expr| is of the form
271 expr ::= expr ( "or" | "and" ) expr
272 | identifier "==" ( string | int )
273 Also collects the variable identifiers and string/int values in the dict
274 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
275 """
276 assert isinstance(expr, (ast.BoolOp, ast.Compare))
277 if isinstance(expr, ast.BoolOp):
278 assert isinstance(expr.op, (ast.And, ast.Or))
279 for subexpr in expr.values:
280 verify_ast(subexpr, variables_and_values)
281 else:
282 assert isinstance(expr.left.ctx, ast.Load)
283 assert len(expr.ops) == 1
284 assert isinstance(expr.ops[0], ast.Eq)
285 var_values = variables_and_values.setdefault(expr.left.id, set())
286 rhs = expr.comparators[0]
287 assert isinstance(rhs, (ast.Str, ast.Num))
288 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
289
290
291def verify_condition(condition, variables_and_values):
292 """Verifies the |condition| dictionary is in the expected format.
293 See verify_ast() for the meaning of |variables_and_values|.
294 """
295 VALID_INSIDE_CONDITION = ['variables']
296 assert isinstance(condition, list), condition
297 assert len(condition) == 2, condition
298 expr, then = condition
299
300 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
301 verify_ast(test_ast.body, variables_and_values)
302
303 assert isinstance(then, dict), then
304 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
305 if not 'variables' in then:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400306 raise IsolateError('Missing \'variables\' in condition %s' % condition)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500307 verify_variables(then['variables'])
308
309
310def verify_root(value, variables_and_values):
311 """Verifies that |value| is the parsed form of a valid .isolate file.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400312
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500313 See verify_ast() for the meaning of |variables_and_values|.
314 """
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400315 VALID_ROOTS = ['includes', 'conditions', 'variables']
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500316 assert isinstance(value, dict), value
317 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
318
319 includes = value.get('includes', [])
320 assert isinstance(includes, list), includes
321 for include in includes:
322 assert isinstance(include, basestring), include
323
324 conditions = value.get('conditions', [])
325 assert isinstance(conditions, list), conditions
326 for condition in conditions:
327 verify_condition(condition, variables_and_values)
328
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400329 variables = value.get('variables', {})
330 verify_variables(variables)
331
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500332
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500333def get_folders(values_dict):
334 """Returns a dict of all the folders in the given value_dict."""
335 return dict(
336 (item, configs) for (item, configs) in values_dict.iteritems()
337 if item.endswith('/')
338 )
339
340
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500341class ConfigSettings(object):
342 """Represents the dependency variables for a single build configuration.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400343
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500344 The structure is immutable.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400345
346 .touch, .tracked and .untracked are the list of dependencies. The items in
347 these lists use '/' as a path separator.
348 .command and .isolate_dir describe how to run the command. .isolate_dir uses
349 the OS' native path separator. It must be an absolute path, it's the path
350 where to start the command from.
351 .read_only describe how to map the files.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500352 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400353 def __init__(self, values, isolate_dir):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500354 verify_variables(values)
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400355 if isolate_dir is None:
356 # It must be an empty object if isolate_dir is None.
357 assert values == {}, values
358 else:
359 # Otherwise, the path must be absolute.
360 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500361 self.touched = sorted(values.get(KEY_TOUCHED, []))
362 self.tracked = sorted(values.get(KEY_TRACKED, []))
363 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
364 self.command = values.get('command', [])[:]
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400365 self.isolate_dir = isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500366 self.read_only = values.get('read_only')
367
368 def union(self, rhs):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400369 """Merges two config settings together into a new instance.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500370
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400371 A new instance is not created and self or rhs is returned if the other
372 object is the empty object.
373
374 self has priority over rhs for .command. Use the same .isolate_dir as the
375 one having a .command.
376
377 Dependencies listed in rhs are patch adjusted ONLY if they don't start with
378 a path variable, e.g. the characters '<('.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500379 """
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400380 # When an object has .isolate_dir == None, it means it is the empty object.
381 if rhs.isolate_dir is None:
382 return self
383 if self.isolate_dir is None:
384 return rhs
385
386 if sys.platform == 'win32':
387 assert self.isolate_dir[0].lower() == rhs.isolate_dir[0].lower()
388
389 # Takes the difference between the two isolate_dir. Note that while
390 # isolate_dir is in native path case, all other references are in posix.
391 l_rel_cwd, r_rel_cwd = self.isolate_dir, rhs.isolate_dir
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400392 if self.command or rhs.command:
393 use_rhs = bool(not self.command and rhs.command)
394 else:
395 # If self doesn't define any file, use rhs.
396 use_rhs = not bool(self.touched or self.tracked or self.untracked)
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400397 if use_rhs:
398 # Rebase files in rhs.
399 l_rel_cwd, r_rel_cwd = r_rel_cwd, l_rel_cwd
400
401 rebase_path = os.path.relpath(r_rel_cwd, l_rel_cwd).replace(
402 os.path.sep, '/')
403 def rebase_item(f):
404 if f.startswith('<(') or rebase_path == '.':
405 return f
406 return posixpath.join(rebase_path, f)
407
408 def map_both(l, r):
409 """Rebase items in either lhs or rhs, as needed."""
410 if use_rhs:
411 l, r = r, l
412 return sorted(l + map(rebase_item, r))
413
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500414 var = {
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400415 KEY_TOUCHED: map_both(self.touched, rhs.touched),
416 KEY_TRACKED: map_both(self.tracked, rhs.tracked),
417 KEY_UNTRACKED: map_both(self.untracked, rhs.untracked),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500418 'command': self.command or rhs.command,
419 'read_only': rhs.read_only if self.read_only is None else self.read_only,
420 }
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400421 return ConfigSettings(var, l_rel_cwd)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500422
423 def flatten(self):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400424 """Converts the object into a dict."""
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500425 out = {}
426 if self.command:
427 out['command'] = self.command
428 if self.touched:
429 out[KEY_TOUCHED] = self.touched
430 if self.tracked:
431 out[KEY_TRACKED] = self.tracked
432 if self.untracked:
433 out[KEY_UNTRACKED] = self.untracked
434 if self.read_only is not None:
435 out['read_only'] = self.read_only
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400436 # TODO(maruel): Probably better to not output it if command is None?
437 if self.isolate_dir is not None:
438 out['isolate_dir'] = self.isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500439 return out
440
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400441 def __str__(self):
442 """Returns a short representation useful for debugging."""
443 files = ''.join(
444 '\n ' + f for f in (self.touched + self.tracked + self.untracked))
445 return 'ConfigSettings(%s, %s, %s, %s)' % (
446 self.command,
447 self.isolate_dir,
448 self.read_only,
449 files or '[]')
450
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500451
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500452def _safe_index(l, k):
453 try:
454 return l.index(k)
455 except ValueError:
456 return None
457
458
459def _get_map_keys(dest_keys, in_keys):
460 """Returns a tuple of the indexes of each item in in_keys found in dest_keys.
461
462 For example, if in_keys is ('A', 'C') and dest_keys is ('A', 'B', 'C'), the
463 return value will be (0, None, 1).
464 """
465 return tuple(_safe_index(in_keys, k) for k in dest_keys)
466
467
468def _map_keys(mapping, items):
469 """Returns a tuple with items placed at mapping index.
470
471 For example, if mapping is (1, None, 0) and items is ('a', 'b'), it will
472 return ('b', None, 'c').
473 """
474 return tuple(items[i] if i != None else None for i in mapping)
475
476
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500477class Configs(object):
478 """Represents a processed .isolate file.
479
480 Stores the file in a processed way, split by configuration.
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500481
482 At this point, we don't know all the possibilities. So mount a partial view
483 that we have.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400484
485 This class doesn't hold isolate_dir, since it is dependent on the final
486 configuration selected. It is implicitly dependent on which .isolate defines
487 the 'command' that will take effect.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500488 """
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500489 def __init__(self, file_comment, config_variables):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500490 self.file_comment = file_comment
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500491 # Contains the names of the config variables seen while processing
492 # .isolate file(s). The order is important since the same order is used for
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500493 # keys in self._by_config.
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500494 assert isinstance(config_variables, tuple)
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400495 assert all(isinstance(c, basestring) for c in config_variables), (
496 config_variables)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400497 config_variables = tuple(config_variables)
498 assert tuple(sorted(config_variables)) == config_variables, config_variables
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500499 self._config_variables = config_variables
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500500 # The keys of _by_config are tuples of values for each of the items in
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500501 # self._config_variables. A None item in the list of the key means the value
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500502 # is unbounded.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500503 self._by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500504
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500505 @property
506 def config_variables(self):
507 return self._config_variables
508
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500509 def get_config(self, config):
510 """Returns all configs that matches this config as a single ConfigSettings.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400511
512 Returns an empty ConfigSettings if none apply.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500513 """
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400514 # TODO(maruel): Fix ordering based on the bounded values. The keys are not
515 # necessarily sorted in the way that makes sense, they are alphabetically
516 # sorted. It is important because the left-most takes predescence.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400517 out = ConfigSettings({}, None)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400518 for k, v in sorted(self._by_config.iteritems()):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500519 if all(i == j or j is None for i, j in zip(config, k)):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400520 out = out.union(v)
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500521 return out
522
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400523 def set_config(self, key, value):
524 """Sets the ConfigSettings for this key.
525
526 The key is a tuple of bounded or unbounded variables. The global variable
527 is the key where all values are unbounded, e.g.:
528 (None,) * len(self._config_variables)
529 """
530 assert key not in self._by_config, (key, self._by_config.keys())
531 assert isinstance(key, tuple)
532 assert len(key) == len(self._config_variables), (
533 key, self._config_variables)
534 assert isinstance(value, ConfigSettings)
535 self._by_config[key] = value
536
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500537 def union(self, rhs):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400538 """Returns a new Configs instance, the union of variables from self and rhs.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500539
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400540 Uses self.file_comment if available, otherwise rhs.file_comment.
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400541 It keeps config_variables sorted in the output.
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400542 """
543 # Merge the keys of config_variables for each Configs instances. All the new
544 # variables will become unbounded. This requires realigning the keys.
545 config_variables = tuple(sorted(
546 set(self.config_variables) | set(rhs.config_variables)))
547 out = Configs(self.file_comment or rhs.file_comment, config_variables)
548 mapping_lhs = _get_map_keys(out.config_variables, self.config_variables)
549 mapping_rhs = _get_map_keys(out.config_variables, rhs.config_variables)
550 lhs_config = dict(
551 (_map_keys(mapping_lhs, k), v) for k, v in self._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500552 # pylint: disable=W0212
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400553 rhs_config = dict(
554 (_map_keys(mapping_rhs, k), v) for k, v in rhs._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500555
556 for key in set(lhs_config) | set(rhs_config):
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400557 l = lhs_config.get(key)
558 r = rhs_config.get(key)
559 out.set_config(key, l.union(r) if (l and r) else (l or r))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500560 return out
561
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500562 def flatten(self):
563 """Returns a flat dictionary representation of the configuration.
564 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500565 return dict((k, v.flatten()) for k, v in self._by_config.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500566
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400567 def __str__(self):
568 return 'Configs(%s,%s)' % (
569 self._config_variables,
570 ''.join('\n %s' % str(f) for f in self._by_config))
571
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500572
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500573def load_isolate_as_config(isolate_dir, value, file_comment):
574 """Parses one .isolate file and returns a Configs() instance.
575
576 Arguments:
577 isolate_dir: only used to load relative includes so it doesn't depend on
578 cwd.
579 value: is the loaded dictionary that was defined in the gyp file.
580 file_comment: comments found at the top of the file so it can be preserved.
581
582 The expected format is strict, anything diverting from the format below will
583 throw an assert:
584 {
585 'includes': [
586 'foo.isolate',
587 ],
588 'conditions': [
589 ['OS=="vms" and foo=42', {
590 'variables': {
591 'command': [
592 ...
593 ],
594 'isolate_dependency_tracked': [
595 ...
596 ],
597 'isolate_dependency_untracked': [
598 ...
599 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500600 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500601 },
602 }],
603 ...
604 ],
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400605 'variables': {
606 ...
607 },
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500608 }
609 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400610 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400611 if any(len(cond) == 3 for cond in value.get('conditions', [])):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400612 raise IsolateError('Using \'else\' is not supported anymore.')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500613 variables_and_values = {}
614 verify_root(value, variables_and_values)
615 if variables_and_values:
616 config_variables, config_values = zip(
617 *sorted(variables_and_values.iteritems()))
618 all_configs = list(itertools.product(*config_values))
619 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500620 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500621 all_configs = []
622
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500623 isolate = Configs(file_comment, config_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500624
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400625 # Add global variables. The global variables are on the empty tuple key.
626 isolate.set_config(
627 (None,) * len(config_variables),
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400628 ConfigSettings(value.get('variables', {}), isolate_dir))
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400629
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500630 # Add configuration-specific variables.
631 for expr, then in value.get('conditions', []):
632 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500633 new = Configs(None, config_variables)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500634 for config in configs:
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400635 new.set_config(config, ConfigSettings(then['variables'], isolate_dir))
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500636 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500637
638 # Load the includes. Process them in reverse so the last one take precedence.
639 for include in reversed(value.get('includes', [])):
640 if os.path.isabs(include):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400641 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500642 'Failed to load configuration; absolute include path \'%s\'' %
643 include)
644 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400645 if sys.platform == 'win32':
646 if included_isolate[0].lower() != isolate_dir[0].lower():
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400647 raise IsolateError(
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400648 'Can\'t reference a .isolate file from another drive')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500649 with open(included_isolate, 'r') as f:
650 included_isolate = load_isolate_as_config(
651 os.path.dirname(included_isolate),
652 eval_content(f.read()),
653 None)
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400654 isolate = isolate.union(included_isolate)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500655
656 return isolate
657
658
659def load_isolate_for_config(isolate_dir, content, config_variables):
660 """Loads the .isolate file and returns the information unprocessed but
661 filtered for the specific OS.
662
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400663 Returns:
664 tuple of command, dependencies, touched, read_only flag, isolate_dir.
665 The dependencies are fixed to use os.path.sep.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500666 """
667 # Load the .isolate file, process its conditions, retrieve the command and
668 # dependencies.
669 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
670 try:
671 config_name = tuple(
672 config_variables[var] for var in isolate.config_variables)
673 except KeyError:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400674 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500675 'These configuration variables were missing from the command line: %s' %
676 ', '.join(
677 sorted(set(isolate.config_variables) - set(config_variables))))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500678
679 # A configuration is to be created with all the combinations of free
680 # variables.
681 config = isolate.get_config(config_name)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500682 # Merge tracked and untracked variables, isolate.py doesn't care about the
683 # trackability of the variables, only the build tool does.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400684 dependencies = sorted(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500685 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400686 )
687 touched = sorted(f.replace('/', os.path.sep) for f in config.touched)
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400688 return (
689 config.command, dependencies, touched, config.read_only,
690 config.isolate_dir)