blob: 65090b19a17bbcfc2c824aaac65811be3ff9493f [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
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050084def pretty_print(variables, stdout):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -040085 """Outputs a .isolate file from the decoded variables.
86
87 The .isolate format is GYP compatible.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050088
89 Similar to pprint.print() but with NIH syndrome.
90 """
91 # Order the dictionary keys by these keys in priority.
92 ORDER = (
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -040093 'variables', 'condition', 'command', 'read_only',
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050094 KEY_TRACKED, KEY_UNTRACKED)
95
96 def sorting_key(x):
97 """Gives priority to 'most important' keys before the others."""
98 if x in ORDER:
99 return str(ORDER.index(x))
100 return x
101
102 def loop_list(indent, items):
103 for item in items:
104 if isinstance(item, basestring):
105 stdout.write('%s\'%s\',\n' % (indent, item))
106 elif isinstance(item, dict):
107 stdout.write('%s{\n' % indent)
108 loop_dict(indent + ' ', item)
109 stdout.write('%s},\n' % indent)
110 elif isinstance(item, list):
111 # A list inside a list will write the first item embedded.
112 stdout.write('%s[' % indent)
113 for index, i in enumerate(item):
114 if isinstance(i, basestring):
115 stdout.write(
116 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
117 elif isinstance(i, dict):
118 stdout.write('{\n')
119 loop_dict(indent + ' ', i)
120 if index != len(item) - 1:
121 x = ', '
122 else:
123 x = ''
124 stdout.write('%s}%s' % (indent, x))
125 else:
126 assert False
127 stdout.write('],\n')
128 else:
129 assert False
130
131 def loop_dict(indent, items):
132 for key in sorted(items, key=sorting_key):
133 item = items[key]
134 stdout.write("%s'%s': " % (indent, key))
135 if isinstance(item, dict):
136 stdout.write('{\n')
137 loop_dict(indent + ' ', item)
138 stdout.write(indent + '},\n')
139 elif isinstance(item, list):
140 stdout.write('[\n')
141 loop_list(indent + ' ', item)
142 stdout.write(indent + '],\n')
143 elif isinstance(item, basestring):
144 stdout.write(
145 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500146 elif isinstance(item, (int, bool)) or item is None:
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400147 stdout.write('%s,\n' % item)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500148 else:
149 assert False, item
150
151 stdout.write('{\n')
152 loop_dict(' ', variables)
153 stdout.write('}\n')
154
155
156def print_all(comment, data, stream):
157 """Prints a complete .isolate file and its top-level file comment into a
158 stream.
159 """
160 if comment:
161 stream.write(comment)
162 pretty_print(data, stream)
163
164
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500165def extract_comment(content):
166 """Extracts file level comment."""
167 out = []
168 for line in content.splitlines(True):
169 if line.startswith('#'):
170 out.append(line)
171 else:
172 break
173 return ''.join(out)
174
175
176def eval_content(content):
177 """Evaluates a python file and return the value defined in it.
178
179 Used in practice for .isolate files.
180 """
181 globs = {'__builtins__': None}
182 locs = {}
183 try:
184 value = eval(content, globs, locs)
185 except TypeError as e:
186 e.args = list(e.args) + [content]
187 raise
188 assert locs == {}, locs
189 assert globs == {'__builtins__': None}, globs
190 return value
191
192
193def match_configs(expr, config_variables, all_configs):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500194 """Returns the list of values from |values| that match the condition |expr|.
195
196 Arguments:
197 expr: string that is evaluatable with eval(). It is a GYP condition.
198 config_variables: list of the name of the variables.
199 all_configs: list of the list of possible values.
200
201 If a variable is not referenced at all, it is marked as unbounded (free) with
202 a value set to None.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500203 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500204 # It is more than just eval'ing the variable, it needs to be double checked to
205 # see if the variable is referenced at all. If not, the variable is free
206 # (unbounded).
207 # TODO(maruel): Use the intelligent way by inspecting expr instead of doing
208 # trial and error to figure out which variable is bound.
209 combinations = []
210 for bound_variables in itertools.product(
211 (True, False), repeat=len(config_variables)):
212 # Add the combination of variables bound.
213 combinations.append(
214 (
215 [c for c, b in zip(config_variables, bound_variables) if b],
216 set(
217 tuple(v if b else None for v, b in zip(line, bound_variables))
218 for line in all_configs)
219 ))
220
221 out = []
222 for variables, configs in combinations:
223 # Strip variables and see if expr can still be evaluated.
224 for values in configs:
225 globs = {'__builtins__': None}
226 globs.update(zip(variables, (v for v in values if v is not None)))
227 try:
228 assertion = eval(expr, globs, {})
229 except NameError:
230 continue
231 if not isinstance(assertion, bool):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400232 raise IsolateError('Invalid condition')
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500233 if assertion:
234 out.append(values)
235 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500236
237
238def verify_variables(variables):
239 """Verifies the |variables| dictionary is in the expected format."""
240 VALID_VARIABLES = [
241 KEY_TOUCHED,
242 KEY_TRACKED,
243 KEY_UNTRACKED,
244 'command',
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400245 'files',
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500246 'read_only',
247 ]
248 assert isinstance(variables, dict), variables
249 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
250 for name, value in variables.iteritems():
251 if name == 'read_only':
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500252 assert value in (0, 1, 2, None), value
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500253 else:
254 assert isinstance(value, list), value
255 assert all(isinstance(i, basestring) for i in value), value
256
257
258def verify_ast(expr, variables_and_values):
259 """Verifies that |expr| is of the form
260 expr ::= expr ( "or" | "and" ) expr
261 | identifier "==" ( string | int )
262 Also collects the variable identifiers and string/int values in the dict
263 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
264 """
265 assert isinstance(expr, (ast.BoolOp, ast.Compare))
266 if isinstance(expr, ast.BoolOp):
267 assert isinstance(expr.op, (ast.And, ast.Or))
268 for subexpr in expr.values:
269 verify_ast(subexpr, variables_and_values)
270 else:
271 assert isinstance(expr.left.ctx, ast.Load)
272 assert len(expr.ops) == 1
273 assert isinstance(expr.ops[0], ast.Eq)
274 var_values = variables_and_values.setdefault(expr.left.id, set())
275 rhs = expr.comparators[0]
276 assert isinstance(rhs, (ast.Str, ast.Num))
277 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
278
279
280def verify_condition(condition, variables_and_values):
281 """Verifies the |condition| dictionary is in the expected format.
282 See verify_ast() for the meaning of |variables_and_values|.
283 """
284 VALID_INSIDE_CONDITION = ['variables']
285 assert isinstance(condition, list), condition
286 assert len(condition) == 2, condition
287 expr, then = condition
288
289 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
290 verify_ast(test_ast.body, variables_and_values)
291
292 assert isinstance(then, dict), then
293 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
294 if not 'variables' in then:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400295 raise IsolateError('Missing \'variables\' in condition %s' % condition)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500296 verify_variables(then['variables'])
297
298
299def verify_root(value, variables_and_values):
300 """Verifies that |value| is the parsed form of a valid .isolate file.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400301
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500302 See verify_ast() for the meaning of |variables_and_values|.
303 """
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400304 VALID_ROOTS = ['includes', 'conditions', 'variables']
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500305 assert isinstance(value, dict), value
306 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
307
308 includes = value.get('includes', [])
309 assert isinstance(includes, list), includes
310 for include in includes:
311 assert isinstance(include, basestring), include
312
313 conditions = value.get('conditions', [])
314 assert isinstance(conditions, list), conditions
315 for condition in conditions:
316 verify_condition(condition, variables_and_values)
317
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400318 variables = value.get('variables', {})
319 verify_variables(variables)
320
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500321
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500322def get_folders(values_dict):
323 """Returns a dict of all the folders in the given value_dict."""
324 return dict(
325 (item, configs) for (item, configs) in values_dict.iteritems()
326 if item.endswith('/')
327 )
328
329
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500330class ConfigSettings(object):
331 """Represents the dependency variables for a single build configuration.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400332
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500333 The structure is immutable.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400334
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400335 .command and .isolate_dir describe how to run the command. .isolate_dir uses
336 the OS' native path separator. It must be an absolute path, it's the path
337 where to start the command from.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400338 .files is the list of dependencies. The items use '/' as a path separator.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400339 .read_only describe how to map the files.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500340 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400341 def __init__(self, values, isolate_dir):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500342 verify_variables(values)
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400343 if isolate_dir is None:
344 # It must be an empty object if isolate_dir is None.
345 assert values == {}, values
346 else:
347 # Otherwise, the path must be absolute.
348 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400349
350 self.files = sorted(
351 values.get('files', []) +
352 values.get(KEY_TOUCHED, []) +
353 values.get(KEY_TRACKED, []) +
354 values.get(KEY_UNTRACKED, []))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500355 self.command = values.get('command', [])[:]
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400356 self.isolate_dir = isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500357 self.read_only = values.get('read_only')
358
359 def union(self, rhs):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400360 """Merges two config settings together into a new instance.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500361
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400362 A new instance is not created and self or rhs is returned if the other
363 object is the empty object.
364
365 self has priority over rhs for .command. Use the same .isolate_dir as the
366 one having a .command.
367
368 Dependencies listed in rhs are patch adjusted ONLY if they don't start with
369 a path variable, e.g. the characters '<('.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500370 """
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400371 # When an object has .isolate_dir == None, it means it is the empty object.
372 if rhs.isolate_dir is None:
373 return self
374 if self.isolate_dir is None:
375 return rhs
376
377 if sys.platform == 'win32':
378 assert self.isolate_dir[0].lower() == rhs.isolate_dir[0].lower()
379
380 # Takes the difference between the two isolate_dir. Note that while
381 # isolate_dir is in native path case, all other references are in posix.
382 l_rel_cwd, r_rel_cwd = self.isolate_dir, rhs.isolate_dir
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400383 if self.command or rhs.command:
384 use_rhs = bool(not self.command and rhs.command)
385 else:
386 # If self doesn't define any file, use rhs.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400387 use_rhs = not bool(self.files)
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400388 if use_rhs:
389 # Rebase files in rhs.
390 l_rel_cwd, r_rel_cwd = r_rel_cwd, l_rel_cwd
391
392 rebase_path = os.path.relpath(r_rel_cwd, l_rel_cwd).replace(
393 os.path.sep, '/')
394 def rebase_item(f):
395 if f.startswith('<(') or rebase_path == '.':
396 return f
397 return posixpath.join(rebase_path, f)
398
399 def map_both(l, r):
400 """Rebase items in either lhs or rhs, as needed."""
401 if use_rhs:
402 l, r = r, l
403 return sorted(l + map(rebase_item, r))
404
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500405 var = {
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500406 'command': self.command or rhs.command,
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400407 'files': map_both(self.files, rhs.files),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500408 'read_only': rhs.read_only if self.read_only is None else self.read_only,
409 }
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400410 return ConfigSettings(var, l_rel_cwd)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500411
412 def flatten(self):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400413 """Converts the object into a dict."""
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500414 out = {}
415 if self.command:
416 out['command'] = self.command
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400417 if self.files:
418 out['files'] = self.files
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500419 if self.read_only is not None:
420 out['read_only'] = self.read_only
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400421 # TODO(maruel): Probably better to not output it if command is None?
422 if self.isolate_dir is not None:
423 out['isolate_dir'] = self.isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500424 return out
425
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400426 def __str__(self):
427 """Returns a short representation useful for debugging."""
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400428 files = ''.join('\n ' + f for f in self.files)
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400429 return 'ConfigSettings(%s, %s, %s, %s)' % (
430 self.command,
431 self.isolate_dir,
432 self.read_only,
433 files or '[]')
434
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500435
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500436def _safe_index(l, k):
437 try:
438 return l.index(k)
439 except ValueError:
440 return None
441
442
443def _get_map_keys(dest_keys, in_keys):
444 """Returns a tuple of the indexes of each item in in_keys found in dest_keys.
445
446 For example, if in_keys is ('A', 'C') and dest_keys is ('A', 'B', 'C'), the
447 return value will be (0, None, 1).
448 """
449 return tuple(_safe_index(in_keys, k) for k in dest_keys)
450
451
452def _map_keys(mapping, items):
453 """Returns a tuple with items placed at mapping index.
454
455 For example, if mapping is (1, None, 0) and items is ('a', 'b'), it will
456 return ('b', None, 'c').
457 """
458 return tuple(items[i] if i != None else None for i in mapping)
459
460
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500461class Configs(object):
462 """Represents a processed .isolate file.
463
464 Stores the file in a processed way, split by configuration.
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500465
466 At this point, we don't know all the possibilities. So mount a partial view
467 that we have.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400468
469 This class doesn't hold isolate_dir, since it is dependent on the final
470 configuration selected. It is implicitly dependent on which .isolate defines
471 the 'command' that will take effect.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500472 """
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500473 def __init__(self, file_comment, config_variables):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500474 self.file_comment = file_comment
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500475 # Contains the names of the config variables seen while processing
476 # .isolate file(s). The order is important since the same order is used for
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500477 # keys in self._by_config.
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500478 assert isinstance(config_variables, tuple)
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400479 assert all(isinstance(c, basestring) for c in config_variables), (
480 config_variables)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400481 config_variables = tuple(config_variables)
482 assert tuple(sorted(config_variables)) == config_variables, config_variables
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500483 self._config_variables = config_variables
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500484 # The keys of _by_config are tuples of values for each of the items in
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500485 # self._config_variables. A None item in the list of the key means the value
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500486 # is unbounded.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500487 self._by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500488
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500489 @property
490 def config_variables(self):
491 return self._config_variables
492
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500493 def get_config(self, config):
494 """Returns all configs that matches this config as a single ConfigSettings.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400495
496 Returns an empty ConfigSettings if none apply.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500497 """
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400498 # TODO(maruel): Fix ordering based on the bounded values. The keys are not
499 # necessarily sorted in the way that makes sense, they are alphabetically
500 # sorted. It is important because the left-most takes predescence.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400501 out = ConfigSettings({}, None)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400502 for k, v in sorted(self._by_config.iteritems()):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500503 if all(i == j or j is None for i, j in zip(config, k)):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400504 out = out.union(v)
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500505 return out
506
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400507 def set_config(self, key, value):
508 """Sets the ConfigSettings for this key.
509
510 The key is a tuple of bounded or unbounded variables. The global variable
511 is the key where all values are unbounded, e.g.:
512 (None,) * len(self._config_variables)
513 """
514 assert key not in self._by_config, (key, self._by_config.keys())
515 assert isinstance(key, tuple)
516 assert len(key) == len(self._config_variables), (
517 key, self._config_variables)
518 assert isinstance(value, ConfigSettings)
519 self._by_config[key] = value
520
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500521 def union(self, rhs):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400522 """Returns a new Configs instance, the union of variables from self and rhs.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500523
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400524 Uses self.file_comment if available, otherwise rhs.file_comment.
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400525 It keeps config_variables sorted in the output.
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400526 """
527 # Merge the keys of config_variables for each Configs instances. All the new
528 # variables will become unbounded. This requires realigning the keys.
529 config_variables = tuple(sorted(
530 set(self.config_variables) | set(rhs.config_variables)))
531 out = Configs(self.file_comment or rhs.file_comment, config_variables)
532 mapping_lhs = _get_map_keys(out.config_variables, self.config_variables)
533 mapping_rhs = _get_map_keys(out.config_variables, rhs.config_variables)
534 lhs_config = dict(
535 (_map_keys(mapping_lhs, k), v) for k, v in self._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500536 # pylint: disable=W0212
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400537 rhs_config = dict(
538 (_map_keys(mapping_rhs, k), v) for k, v in rhs._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500539
540 for key in set(lhs_config) | set(rhs_config):
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400541 l = lhs_config.get(key)
542 r = rhs_config.get(key)
543 out.set_config(key, l.union(r) if (l and r) else (l or r))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500544 return out
545
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500546 def flatten(self):
547 """Returns a flat dictionary representation of the configuration.
548 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500549 return dict((k, v.flatten()) for k, v in self._by_config.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500550
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400551 def __str__(self):
552 return 'Configs(%s,%s)' % (
553 self._config_variables,
554 ''.join('\n %s' % str(f) for f in self._by_config))
555
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500556
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500557def load_isolate_as_config(isolate_dir, value, file_comment):
558 """Parses one .isolate file and returns a Configs() instance.
559
560 Arguments:
561 isolate_dir: only used to load relative includes so it doesn't depend on
562 cwd.
563 value: is the loaded dictionary that was defined in the gyp file.
564 file_comment: comments found at the top of the file so it can be preserved.
565
566 The expected format is strict, anything diverting from the format below will
567 throw an assert:
568 {
569 'includes': [
570 'foo.isolate',
571 ],
572 'conditions': [
573 ['OS=="vms" and foo=42', {
574 'variables': {
575 'command': [
576 ...
577 ],
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400578 'files': [
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500579 ...
580 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500581 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500582 },
583 }],
584 ...
585 ],
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400586 'variables': {
587 ...
588 },
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500589 }
590 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400591 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400592 if any(len(cond) == 3 for cond in value.get('conditions', [])):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400593 raise IsolateError('Using \'else\' is not supported anymore.')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500594 variables_and_values = {}
595 verify_root(value, variables_and_values)
596 if variables_and_values:
597 config_variables, config_values = zip(
598 *sorted(variables_and_values.iteritems()))
599 all_configs = list(itertools.product(*config_values))
600 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500601 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500602 all_configs = []
603
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500604 isolate = Configs(file_comment, config_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500605
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400606 # Add global variables. The global variables are on the empty tuple key.
607 isolate.set_config(
608 (None,) * len(config_variables),
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400609 ConfigSettings(value.get('variables', {}), isolate_dir))
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400610
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500611 # Add configuration-specific variables.
612 for expr, then in value.get('conditions', []):
613 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500614 new = Configs(None, config_variables)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500615 for config in configs:
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400616 new.set_config(config, ConfigSettings(then['variables'], isolate_dir))
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500617 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500618
619 # Load the includes. Process them in reverse so the last one take precedence.
620 for include in reversed(value.get('includes', [])):
621 if os.path.isabs(include):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400622 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500623 'Failed to load configuration; absolute include path \'%s\'' %
624 include)
625 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400626 if sys.platform == 'win32':
627 if included_isolate[0].lower() != isolate_dir[0].lower():
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400628 raise IsolateError(
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400629 'Can\'t reference a .isolate file from another drive')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500630 with open(included_isolate, 'r') as f:
631 included_isolate = load_isolate_as_config(
632 os.path.dirname(included_isolate),
633 eval_content(f.read()),
634 None)
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400635 isolate = isolate.union(included_isolate)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500636
637 return isolate
638
639
640def load_isolate_for_config(isolate_dir, content, config_variables):
641 """Loads the .isolate file and returns the information unprocessed but
642 filtered for the specific OS.
643
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400644 Returns:
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400645 tuple of command, dependencies, read_only flag, isolate_dir.
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400646 The dependencies are fixed to use os.path.sep.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500647 """
648 # Load the .isolate file, process its conditions, retrieve the command and
649 # dependencies.
650 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
651 try:
652 config_name = tuple(
653 config_variables[var] for var in isolate.config_variables)
654 except KeyError:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400655 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500656 'These configuration variables were missing from the command line: %s' %
657 ', '.join(
658 sorted(set(isolate.config_variables) - set(config_variables))))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500659
660 # A configuration is to be created with all the combinations of free
661 # variables.
662 config = isolate.get_config(config_name)
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400663 dependencies = [f.replace('/', os.path.sep) for f in config.files]
664 return config.command, dependencies, config.read_only, config.isolate_dir