blob: 935943b0dbea6fa462d375751e65d1fc34ccd58a [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
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050024# Valid variable name.
25VALID_VARIABLE = '[A-Za-z_][A-Za-z_0-9]*'
26
27
Marc-Antoine Ruele819be42014-08-28 19:38:20 -040028class IsolateError(ValueError):
29 """Generic failure to load a .isolate file."""
30 pass
31
32
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050033def determine_root_dir(relative_root, infiles):
34 """For a list of infiles, determines the deepest root directory that is
35 referenced indirectly.
36
37 All arguments must be using os.path.sep.
38 """
39 # The trick used to determine the root directory is to look at "how far" back
40 # up it is looking up.
41 deepest_root = relative_root
42 for i in infiles:
43 x = relative_root
44 while i.startswith('..' + os.path.sep):
45 i = i[3:]
46 assert not i.startswith(os.path.sep)
47 x = os.path.dirname(x)
48 if deepest_root.startswith(x):
49 deepest_root = x
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -040050 logging.info(
51 'determine_root_dir(%s, %d files) -> %s',
52 relative_root, len(infiles), deepest_root)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050053 return deepest_root
54
55
56def replace_variable(part, variables):
57 m = re.match(r'<\((' + VALID_VARIABLE + ')\)', part)
58 if m:
59 if m.group(1) not in variables:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -040060 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050061 'Variable "%s" was not found in %s.\nDid you forget to specify '
62 '--path-variable?' % (m.group(1), variables))
John Abd-El-Malek37bcce22014-09-29 11:11:31 -070063 return str(variables[m.group(1)])
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050064 return part
65
66
67def eval_variables(item, variables):
68 """Replaces the .isolate variables in a string item.
69
70 Note that the .isolate format is a subset of the .gyp dialect.
71 """
72 return ''.join(
73 replace_variable(p, variables)
74 for p in re.split(r'(<\(' + VALID_VARIABLE + '\))', item))
75
76
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050077def pretty_print(variables, stdout):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -040078 """Outputs a .isolate file from the decoded variables.
79
80 The .isolate format is GYP compatible.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050081
82 Similar to pprint.print() but with NIH syndrome.
83 """
84 # Order the dictionary keys by these keys in priority.
Marc-Antoine Ruel5b827c92014-11-14 18:40:27 -050085 ORDER = ('variables', 'condition', 'command', 'files', 'read_only')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050086
87 def sorting_key(x):
88 """Gives priority to 'most important' keys before the others."""
89 if x in ORDER:
90 return str(ORDER.index(x))
91 return x
92
93 def loop_list(indent, items):
94 for item in items:
95 if isinstance(item, basestring):
96 stdout.write('%s\'%s\',\n' % (indent, item))
97 elif isinstance(item, dict):
98 stdout.write('%s{\n' % indent)
99 loop_dict(indent + ' ', item)
100 stdout.write('%s},\n' % indent)
101 elif isinstance(item, list):
102 # A list inside a list will write the first item embedded.
103 stdout.write('%s[' % indent)
104 for index, i in enumerate(item):
105 if isinstance(i, basestring):
106 stdout.write(
107 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
108 elif isinstance(i, dict):
109 stdout.write('{\n')
110 loop_dict(indent + ' ', i)
111 if index != len(item) - 1:
112 x = ', '
113 else:
114 x = ''
115 stdout.write('%s}%s' % (indent, x))
116 else:
117 assert False
118 stdout.write('],\n')
119 else:
120 assert False
121
122 def loop_dict(indent, items):
123 for key in sorted(items, key=sorting_key):
124 item = items[key]
125 stdout.write("%s'%s': " % (indent, key))
126 if isinstance(item, dict):
127 stdout.write('{\n')
128 loop_dict(indent + ' ', item)
129 stdout.write(indent + '},\n')
130 elif isinstance(item, list):
131 stdout.write('[\n')
132 loop_list(indent + ' ', item)
133 stdout.write(indent + '],\n')
134 elif isinstance(item, basestring):
135 stdout.write(
136 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500137 elif isinstance(item, (int, bool)) or item is None:
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400138 stdout.write('%s,\n' % item)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500139 else:
140 assert False, item
141
142 stdout.write('{\n')
143 loop_dict(' ', variables)
144 stdout.write('}\n')
145
146
147def print_all(comment, data, stream):
148 """Prints a complete .isolate file and its top-level file comment into a
149 stream.
150 """
151 if comment:
152 stream.write(comment)
153 pretty_print(data, stream)
154
155
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500156def extract_comment(content):
157 """Extracts file level comment."""
158 out = []
159 for line in content.splitlines(True):
160 if line.startswith('#'):
161 out.append(line)
162 else:
163 break
164 return ''.join(out)
165
166
167def eval_content(content):
168 """Evaluates a python file and return the value defined in it.
169
170 Used in practice for .isolate files.
171 """
172 globs = {'__builtins__': None}
173 locs = {}
174 try:
175 value = eval(content, globs, locs)
176 except TypeError as e:
177 e.args = list(e.args) + [content]
178 raise
179 assert locs == {}, locs
180 assert globs == {'__builtins__': None}, globs
181 return value
182
183
184def match_configs(expr, config_variables, all_configs):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500185 """Returns the list of values from |values| that match the condition |expr|.
186
187 Arguments:
188 expr: string that is evaluatable with eval(). It is a GYP condition.
189 config_variables: list of the name of the variables.
190 all_configs: list of the list of possible values.
191
192 If a variable is not referenced at all, it is marked as unbounded (free) with
193 a value set to None.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500194 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500195 # It is more than just eval'ing the variable, it needs to be double checked to
196 # see if the variable is referenced at all. If not, the variable is free
197 # (unbounded).
198 # TODO(maruel): Use the intelligent way by inspecting expr instead of doing
199 # trial and error to figure out which variable is bound.
200 combinations = []
201 for bound_variables in itertools.product(
202 (True, False), repeat=len(config_variables)):
203 # Add the combination of variables bound.
204 combinations.append(
205 (
206 [c for c, b in zip(config_variables, bound_variables) if b],
207 set(
208 tuple(v if b else None for v, b in zip(line, bound_variables))
209 for line in all_configs)
210 ))
211
212 out = []
213 for variables, configs in combinations:
214 # Strip variables and see if expr can still be evaluated.
215 for values in configs:
216 globs = {'__builtins__': None}
217 globs.update(zip(variables, (v for v in values if v is not None)))
218 try:
219 assertion = eval(expr, globs, {})
220 except NameError:
221 continue
222 if not isinstance(assertion, bool):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400223 raise IsolateError('Invalid condition')
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500224 if assertion:
225 out.append(values)
226 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500227
228
229def verify_variables(variables):
230 """Verifies the |variables| dictionary is in the expected format."""
231 VALID_VARIABLES = [
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500232 'command',
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400233 'files',
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500234 'read_only',
235 ]
236 assert isinstance(variables, dict), variables
237 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
238 for name, value in variables.iteritems():
239 if name == 'read_only':
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500240 assert value in (0, 1, 2, None), value
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500241 else:
242 assert isinstance(value, list), value
243 assert all(isinstance(i, basestring) for i in value), value
244
245
246def verify_ast(expr, variables_and_values):
247 """Verifies that |expr| is of the form
248 expr ::= expr ( "or" | "and" ) expr
249 | identifier "==" ( string | int )
250 Also collects the variable identifiers and string/int values in the dict
251 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
252 """
253 assert isinstance(expr, (ast.BoolOp, ast.Compare))
254 if isinstance(expr, ast.BoolOp):
255 assert isinstance(expr.op, (ast.And, ast.Or))
256 for subexpr in expr.values:
257 verify_ast(subexpr, variables_and_values)
258 else:
259 assert isinstance(expr.left.ctx, ast.Load)
260 assert len(expr.ops) == 1
261 assert isinstance(expr.ops[0], ast.Eq)
262 var_values = variables_and_values.setdefault(expr.left.id, set())
263 rhs = expr.comparators[0]
264 assert isinstance(rhs, (ast.Str, ast.Num))
265 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
266
267
268def verify_condition(condition, variables_and_values):
269 """Verifies the |condition| dictionary is in the expected format.
270 See verify_ast() for the meaning of |variables_and_values|.
271 """
272 VALID_INSIDE_CONDITION = ['variables']
273 assert isinstance(condition, list), condition
274 assert len(condition) == 2, condition
275 expr, then = condition
276
277 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
278 verify_ast(test_ast.body, variables_and_values)
279
280 assert isinstance(then, dict), then
281 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
282 if not 'variables' in then:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400283 raise IsolateError('Missing \'variables\' in condition %s' % condition)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500284 verify_variables(then['variables'])
285
286
287def verify_root(value, variables_and_values):
288 """Verifies that |value| is the parsed form of a valid .isolate file.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400289
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500290 See verify_ast() for the meaning of |variables_and_values|.
291 """
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400292 VALID_ROOTS = ['includes', 'conditions', 'variables']
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500293 assert isinstance(value, dict), value
294 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
295
296 includes = value.get('includes', [])
297 assert isinstance(includes, list), includes
298 for include in includes:
299 assert isinstance(include, basestring), include
300
301 conditions = value.get('conditions', [])
302 assert isinstance(conditions, list), conditions
303 for condition in conditions:
304 verify_condition(condition, variables_and_values)
305
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400306 variables = value.get('variables', {})
307 verify_variables(variables)
308
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500309
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500310def get_folders(values_dict):
311 """Returns a dict of all the folders in the given value_dict."""
312 return dict(
313 (item, configs) for (item, configs) in values_dict.iteritems()
314 if item.endswith('/')
315 )
316
317
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500318class ConfigSettings(object):
319 """Represents the dependency variables for a single build configuration.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400320
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500321 The structure is immutable.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400322
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400323 .command and .isolate_dir describe how to run the command. .isolate_dir uses
324 the OS' native path separator. It must be an absolute path, it's the path
325 where to start the command from.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400326 .files is the list of dependencies. The items use '/' as a path separator.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400327 .read_only describe how to map the files.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500328 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400329 def __init__(self, values, isolate_dir):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500330 verify_variables(values)
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400331 if isolate_dir is None:
332 # It must be an empty object if isolate_dir is None.
333 assert values == {}, values
334 else:
335 # Otherwise, the path must be absolute.
336 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400337
Marc-Antoine Ruel5b827c92014-11-14 18:40:27 -0500338 self.files = sorted(values.get('files', []))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500339 self.command = values.get('command', [])[:]
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400340 self.isolate_dir = isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500341 self.read_only = values.get('read_only')
342
343 def union(self, rhs):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400344 """Merges two config settings together into a new instance.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500345
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400346 A new instance is not created and self or rhs is returned if the other
347 object is the empty object.
348
349 self has priority over rhs for .command. Use the same .isolate_dir as the
350 one having a .command.
351
352 Dependencies listed in rhs are patch adjusted ONLY if they don't start with
353 a path variable, e.g. the characters '<('.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500354 """
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400355 # When an object has .isolate_dir == None, it means it is the empty object.
356 if rhs.isolate_dir is None:
357 return self
358 if self.isolate_dir is None:
359 return rhs
360
361 if sys.platform == 'win32':
362 assert self.isolate_dir[0].lower() == rhs.isolate_dir[0].lower()
363
364 # Takes the difference between the two isolate_dir. Note that while
365 # isolate_dir is in native path case, all other references are in posix.
366 l_rel_cwd, r_rel_cwd = self.isolate_dir, rhs.isolate_dir
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400367 if self.command or rhs.command:
368 use_rhs = bool(not self.command and rhs.command)
369 else:
370 # If self doesn't define any file, use rhs.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400371 use_rhs = not bool(self.files)
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400372 if use_rhs:
373 # Rebase files in rhs.
374 l_rel_cwd, r_rel_cwd = r_rel_cwd, l_rel_cwd
375
376 rebase_path = os.path.relpath(r_rel_cwd, l_rel_cwd).replace(
377 os.path.sep, '/')
378 def rebase_item(f):
379 if f.startswith('<(') or rebase_path == '.':
380 return f
381 return posixpath.join(rebase_path, f)
382
383 def map_both(l, r):
384 """Rebase items in either lhs or rhs, as needed."""
385 if use_rhs:
386 l, r = r, l
387 return sorted(l + map(rebase_item, r))
388
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500389 var = {
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500390 'command': self.command or rhs.command,
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400391 'files': map_both(self.files, rhs.files),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500392 'read_only': rhs.read_only if self.read_only is None else self.read_only,
393 }
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400394 return ConfigSettings(var, l_rel_cwd)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500395
396 def flatten(self):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400397 """Converts the object into a dict."""
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500398 out = {}
399 if self.command:
400 out['command'] = self.command
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400401 if self.files:
402 out['files'] = self.files
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500403 if self.read_only is not None:
404 out['read_only'] = self.read_only
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400405 # TODO(maruel): Probably better to not output it if command is None?
406 if self.isolate_dir is not None:
407 out['isolate_dir'] = self.isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500408 return out
409
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400410 def __str__(self):
411 """Returns a short representation useful for debugging."""
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400412 files = ''.join('\n ' + f for f in self.files)
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400413 return 'ConfigSettings(%s, %s, %s, %s)' % (
414 self.command,
415 self.isolate_dir,
416 self.read_only,
417 files or '[]')
418
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500419
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500420def _safe_index(l, k):
421 try:
422 return l.index(k)
423 except ValueError:
424 return None
425
426
427def _get_map_keys(dest_keys, in_keys):
428 """Returns a tuple of the indexes of each item in in_keys found in dest_keys.
429
430 For example, if in_keys is ('A', 'C') and dest_keys is ('A', 'B', 'C'), the
431 return value will be (0, None, 1).
432 """
433 return tuple(_safe_index(in_keys, k) for k in dest_keys)
434
435
436def _map_keys(mapping, items):
437 """Returns a tuple with items placed at mapping index.
438
439 For example, if mapping is (1, None, 0) and items is ('a', 'b'), it will
440 return ('b', None, 'c').
441 """
442 return tuple(items[i] if i != None else None for i in mapping)
443
444
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500445class Configs(object):
446 """Represents a processed .isolate file.
447
448 Stores the file in a processed way, split by configuration.
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500449
450 At this point, we don't know all the possibilities. So mount a partial view
451 that we have.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400452
453 This class doesn't hold isolate_dir, since it is dependent on the final
454 configuration selected. It is implicitly dependent on which .isolate defines
455 the 'command' that will take effect.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500456 """
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500457 def __init__(self, file_comment, config_variables):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500458 self.file_comment = file_comment
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500459 # Contains the names of the config variables seen while processing
460 # .isolate file(s). The order is important since the same order is used for
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500461 # keys in self._by_config.
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500462 assert isinstance(config_variables, tuple)
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400463 assert all(isinstance(c, basestring) for c in config_variables), (
464 config_variables)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400465 config_variables = tuple(config_variables)
466 assert tuple(sorted(config_variables)) == config_variables, config_variables
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500467 self._config_variables = config_variables
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500468 # The keys of _by_config are tuples of values for each of the items in
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500469 # self._config_variables. A None item in the list of the key means the value
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500470 # is unbounded.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500471 self._by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500472
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500473 @property
474 def config_variables(self):
475 return self._config_variables
476
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500477 def get_config(self, config):
478 """Returns all configs that matches this config as a single ConfigSettings.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400479
480 Returns an empty ConfigSettings if none apply.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500481 """
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400482 # TODO(maruel): Fix ordering based on the bounded values. The keys are not
483 # necessarily sorted in the way that makes sense, they are alphabetically
484 # sorted. It is important because the left-most takes predescence.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400485 out = ConfigSettings({}, None)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400486 for k, v in sorted(self._by_config.iteritems()):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500487 if all(i == j or j is None for i, j in zip(config, k)):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400488 out = out.union(v)
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500489 return out
490
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400491 def set_config(self, key, value):
492 """Sets the ConfigSettings for this key.
493
494 The key is a tuple of bounded or unbounded variables. The global variable
495 is the key where all values are unbounded, e.g.:
496 (None,) * len(self._config_variables)
497 """
498 assert key not in self._by_config, (key, self._by_config.keys())
499 assert isinstance(key, tuple)
500 assert len(key) == len(self._config_variables), (
501 key, self._config_variables)
502 assert isinstance(value, ConfigSettings)
503 self._by_config[key] = value
504
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500505 def union(self, rhs):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400506 """Returns a new Configs instance, the union of variables from self and rhs.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500507
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400508 Uses self.file_comment if available, otherwise rhs.file_comment.
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400509 It keeps config_variables sorted in the output.
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400510 """
511 # Merge the keys of config_variables for each Configs instances. All the new
512 # variables will become unbounded. This requires realigning the keys.
513 config_variables = tuple(sorted(
514 set(self.config_variables) | set(rhs.config_variables)))
515 out = Configs(self.file_comment or rhs.file_comment, config_variables)
516 mapping_lhs = _get_map_keys(out.config_variables, self.config_variables)
517 mapping_rhs = _get_map_keys(out.config_variables, rhs.config_variables)
518 lhs_config = dict(
519 (_map_keys(mapping_lhs, k), v) for k, v in self._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500520 # pylint: disable=W0212
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400521 rhs_config = dict(
522 (_map_keys(mapping_rhs, k), v) for k, v in rhs._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500523
524 for key in set(lhs_config) | set(rhs_config):
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400525 l = lhs_config.get(key)
526 r = rhs_config.get(key)
527 out.set_config(key, l.union(r) if (l and r) else (l or r))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500528 return out
529
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500530 def flatten(self):
531 """Returns a flat dictionary representation of the configuration.
532 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500533 return dict((k, v.flatten()) for k, v in self._by_config.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500534
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400535 def __str__(self):
536 return 'Configs(%s,%s)' % (
537 self._config_variables,
538 ''.join('\n %s' % str(f) for f in self._by_config))
539
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500540
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500541def load_isolate_as_config(isolate_dir, value, file_comment):
542 """Parses one .isolate file and returns a Configs() instance.
543
544 Arguments:
545 isolate_dir: only used to load relative includes so it doesn't depend on
546 cwd.
547 value: is the loaded dictionary that was defined in the gyp file.
548 file_comment: comments found at the top of the file so it can be preserved.
549
550 The expected format is strict, anything diverting from the format below will
551 throw an assert:
552 {
553 'includes': [
554 'foo.isolate',
555 ],
556 'conditions': [
557 ['OS=="vms" and foo=42', {
558 'variables': {
559 'command': [
560 ...
561 ],
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400562 'files': [
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500563 ...
564 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500565 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500566 },
567 }],
568 ...
569 ],
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400570 'variables': {
571 ...
572 },
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500573 }
574 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400575 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400576 if any(len(cond) == 3 for cond in value.get('conditions', [])):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400577 raise IsolateError('Using \'else\' is not supported anymore.')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500578 variables_and_values = {}
579 verify_root(value, variables_and_values)
580 if variables_and_values:
581 config_variables, config_values = zip(
582 *sorted(variables_and_values.iteritems()))
583 all_configs = list(itertools.product(*config_values))
584 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500585 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500586 all_configs = []
587
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500588 isolate = Configs(file_comment, config_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500589
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400590 # Add global variables. The global variables are on the empty tuple key.
591 isolate.set_config(
592 (None,) * len(config_variables),
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400593 ConfigSettings(value.get('variables', {}), isolate_dir))
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400594
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500595 # Add configuration-specific variables.
596 for expr, then in value.get('conditions', []):
597 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500598 new = Configs(None, config_variables)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500599 for config in configs:
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400600 new.set_config(config, ConfigSettings(then['variables'], isolate_dir))
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500601 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500602
603 # Load the includes. Process them in reverse so the last one take precedence.
604 for include in reversed(value.get('includes', [])):
605 if os.path.isabs(include):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400606 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500607 'Failed to load configuration; absolute include path \'%s\'' %
608 include)
609 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400610 if sys.platform == 'win32':
611 if included_isolate[0].lower() != isolate_dir[0].lower():
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400612 raise IsolateError(
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400613 'Can\'t reference a .isolate file from another drive')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500614 with open(included_isolate, 'r') as f:
615 included_isolate = load_isolate_as_config(
616 os.path.dirname(included_isolate),
617 eval_content(f.read()),
618 None)
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400619 isolate = isolate.union(included_isolate)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500620
621 return isolate
622
623
624def load_isolate_for_config(isolate_dir, content, config_variables):
625 """Loads the .isolate file and returns the information unprocessed but
626 filtered for the specific OS.
627
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400628 Returns:
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400629 tuple of command, dependencies, read_only flag, isolate_dir.
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400630 The dependencies are fixed to use os.path.sep.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500631 """
632 # Load the .isolate file, process its conditions, retrieve the command and
633 # dependencies.
634 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
635 try:
636 config_name = tuple(
637 config_variables[var] for var in isolate.config_variables)
638 except KeyError:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400639 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500640 'These configuration variables were missing from the command line: %s' %
641 ', '.join(
642 sorted(set(isolate.config_variables) - set(config_variables))))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500643
644 # A configuration is to be created with all the combinations of free
645 # variables.
646 config = isolate.get_config(config_name)
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400647 dependencies = [f.replace('/', os.path.sep) for f in config.files]
648 return config.command, dependencies, config.read_only, config.isolate_dir