blob: 6e35a63a3c2df990b930225c3637e0490ebb7c0d [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
maruel12e30012015-10-09 11:55:35 -070023from utils import fs
24
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050025
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050026# Valid variable name.
27VALID_VARIABLE = '[A-Za-z_][A-Za-z_0-9]*'
28
29
Marc-Antoine Ruele819be42014-08-28 19:38:20 -040030class IsolateError(ValueError):
31 """Generic failure to load a .isolate file."""
32 pass
33
34
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050035def determine_root_dir(relative_root, infiles):
36 """For a list of infiles, determines the deepest root directory that is
37 referenced indirectly.
38
39 All arguments must be using os.path.sep.
40 """
41 # The trick used to determine the root directory is to look at "how far" back
42 # up it is looking up.
43 deepest_root = relative_root
44 for i in infiles:
45 x = relative_root
46 while i.startswith('..' + os.path.sep):
47 i = i[3:]
48 assert not i.startswith(os.path.sep)
49 x = os.path.dirname(x)
50 if deepest_root.startswith(x):
51 deepest_root = x
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -040052 logging.info(
53 'determine_root_dir(%s, %d files) -> %s',
54 relative_root, len(infiles), deepest_root)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050055 return deepest_root
56
57
58def replace_variable(part, variables):
59 m = re.match(r'<\((' + VALID_VARIABLE + ')\)', part)
60 if m:
61 if m.group(1) not in variables:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -040062 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050063 'Variable "%s" was not found in %s.\nDid you forget to specify '
64 '--path-variable?' % (m.group(1), variables))
John Abd-El-Malek37bcce22014-09-29 11:11:31 -070065 return str(variables[m.group(1)])
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050066 return part
67
68
69def eval_variables(item, variables):
70 """Replaces the .isolate variables in a string item.
71
72 Note that the .isolate format is a subset of the .gyp dialect.
73 """
74 return ''.join(
75 replace_variable(p, variables)
76 for p in re.split(r'(<\(' + VALID_VARIABLE + '\))', item))
77
78
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050079def pretty_print(variables, stdout):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -040080 """Outputs a .isolate file from the decoded variables.
81
82 The .isolate format is GYP compatible.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050083
84 Similar to pprint.print() but with NIH syndrome.
85 """
86 # Order the dictionary keys by these keys in priority.
Marc-Antoine Ruel5b827c92014-11-14 18:40:27 -050087 ORDER = ('variables', 'condition', 'command', 'files', 'read_only')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050088
89 def sorting_key(x):
90 """Gives priority to 'most important' keys before the others."""
91 if x in ORDER:
92 return str(ORDER.index(x))
93 return x
94
95 def loop_list(indent, items):
96 for item in items:
97 if isinstance(item, basestring):
98 stdout.write('%s\'%s\',\n' % (indent, item))
99 elif isinstance(item, dict):
100 stdout.write('%s{\n' % indent)
101 loop_dict(indent + ' ', item)
102 stdout.write('%s},\n' % indent)
103 elif isinstance(item, list):
104 # A list inside a list will write the first item embedded.
105 stdout.write('%s[' % indent)
106 for index, i in enumerate(item):
107 if isinstance(i, basestring):
108 stdout.write(
109 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
110 elif isinstance(i, dict):
111 stdout.write('{\n')
112 loop_dict(indent + ' ', i)
113 if index != len(item) - 1:
114 x = ', '
115 else:
116 x = ''
117 stdout.write('%s}%s' % (indent, x))
118 else:
119 assert False
120 stdout.write('],\n')
121 else:
122 assert False
123
124 def loop_dict(indent, items):
125 for key in sorted(items, key=sorting_key):
126 item = items[key]
127 stdout.write("%s'%s': " % (indent, key))
128 if isinstance(item, dict):
129 stdout.write('{\n')
130 loop_dict(indent + ' ', item)
131 stdout.write(indent + '},\n')
132 elif isinstance(item, list):
133 stdout.write('[\n')
134 loop_list(indent + ' ', item)
135 stdout.write(indent + '],\n')
136 elif isinstance(item, basestring):
137 stdout.write(
138 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500139 elif isinstance(item, (int, bool)) or item is None:
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400140 stdout.write('%s,\n' % item)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500141 else:
142 assert False, item
143
144 stdout.write('{\n')
145 loop_dict(' ', variables)
146 stdout.write('}\n')
147
148
149def print_all(comment, data, stream):
150 """Prints a complete .isolate file and its top-level file comment into a
151 stream.
152 """
153 if comment:
154 stream.write(comment)
155 pretty_print(data, stream)
156
157
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500158def extract_comment(content):
159 """Extracts file level comment."""
160 out = []
161 for line in content.splitlines(True):
162 if line.startswith('#'):
163 out.append(line)
164 else:
165 break
166 return ''.join(out)
167
168
169def eval_content(content):
170 """Evaluates a python file and return the value defined in it.
171
172 Used in practice for .isolate files.
173 """
174 globs = {'__builtins__': None}
175 locs = {}
176 try:
177 value = eval(content, globs, locs)
178 except TypeError as e:
179 e.args = list(e.args) + [content]
180 raise
181 assert locs == {}, locs
182 assert globs == {'__builtins__': None}, globs
183 return value
184
185
186def match_configs(expr, config_variables, all_configs):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500187 """Returns the list of values from |values| that match the condition |expr|.
188
189 Arguments:
190 expr: string that is evaluatable with eval(). It is a GYP condition.
191 config_variables: list of the name of the variables.
192 all_configs: list of the list of possible values.
193
194 If a variable is not referenced at all, it is marked as unbounded (free) with
195 a value set to None.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500196 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500197 # It is more than just eval'ing the variable, it needs to be double checked to
198 # see if the variable is referenced at all. If not, the variable is free
199 # (unbounded).
200 # TODO(maruel): Use the intelligent way by inspecting expr instead of doing
201 # trial and error to figure out which variable is bound.
202 combinations = []
203 for bound_variables in itertools.product(
204 (True, False), repeat=len(config_variables)):
205 # Add the combination of variables bound.
206 combinations.append(
207 (
208 [c for c, b in zip(config_variables, bound_variables) if b],
209 set(
210 tuple(v if b else None for v, b in zip(line, bound_variables))
211 for line in all_configs)
212 ))
213
214 out = []
215 for variables, configs in combinations:
216 # Strip variables and see if expr can still be evaluated.
217 for values in configs:
218 globs = {'__builtins__': None}
219 globs.update(zip(variables, (v for v in values if v is not None)))
220 try:
221 assertion = eval(expr, globs, {})
222 except NameError:
223 continue
224 if not isinstance(assertion, bool):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400225 raise IsolateError('Invalid condition')
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500226 if assertion:
227 out.append(values)
228 return out
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500229
230
231def verify_variables(variables):
232 """Verifies the |variables| dictionary is in the expected format."""
233 VALID_VARIABLES = [
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500234 'command',
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400235 'files',
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500236 'read_only',
237 ]
238 assert isinstance(variables, dict), variables
239 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
240 for name, value in variables.iteritems():
241 if name == 'read_only':
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500242 assert value in (0, 1, 2, None), value
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500243 else:
244 assert isinstance(value, list), value
245 assert all(isinstance(i, basestring) for i in value), value
246
247
248def verify_ast(expr, variables_and_values):
249 """Verifies that |expr| is of the form
250 expr ::= expr ( "or" | "and" ) expr
251 | identifier "==" ( string | int )
252 Also collects the variable identifiers and string/int values in the dict
253 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
254 """
255 assert isinstance(expr, (ast.BoolOp, ast.Compare))
256 if isinstance(expr, ast.BoolOp):
257 assert isinstance(expr.op, (ast.And, ast.Or))
258 for subexpr in expr.values:
259 verify_ast(subexpr, variables_and_values)
260 else:
261 assert isinstance(expr.left.ctx, ast.Load)
262 assert len(expr.ops) == 1
263 assert isinstance(expr.ops[0], ast.Eq)
264 var_values = variables_and_values.setdefault(expr.left.id, set())
265 rhs = expr.comparators[0]
266 assert isinstance(rhs, (ast.Str, ast.Num))
267 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
268
269
270def verify_condition(condition, variables_and_values):
271 """Verifies the |condition| dictionary is in the expected format.
272 See verify_ast() for the meaning of |variables_and_values|.
273 """
274 VALID_INSIDE_CONDITION = ['variables']
275 assert isinstance(condition, list), condition
276 assert len(condition) == 2, condition
277 expr, then = condition
278
279 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
280 verify_ast(test_ast.body, variables_and_values)
281
282 assert isinstance(then, dict), then
283 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
284 if not 'variables' in then:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400285 raise IsolateError('Missing \'variables\' in condition %s' % condition)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500286 verify_variables(then['variables'])
287
288
289def verify_root(value, variables_and_values):
290 """Verifies that |value| is the parsed form of a valid .isolate file.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400291
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500292 See verify_ast() for the meaning of |variables_and_values|.
293 """
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400294 VALID_ROOTS = ['includes', 'conditions', 'variables']
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500295 assert isinstance(value, dict), value
296 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
297
298 includes = value.get('includes', [])
299 assert isinstance(includes, list), includes
300 for include in includes:
301 assert isinstance(include, basestring), include
302
303 conditions = value.get('conditions', [])
304 assert isinstance(conditions, list), conditions
305 for condition in conditions:
306 verify_condition(condition, variables_and_values)
307
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400308 variables = value.get('variables', {})
309 verify_variables(variables)
310
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500311
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500312def get_folders(values_dict):
313 """Returns a dict of all the folders in the given value_dict."""
314 return dict(
315 (item, configs) for (item, configs) in values_dict.iteritems()
316 if item.endswith('/')
317 )
318
319
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500320class ConfigSettings(object):
321 """Represents the dependency variables for a single build configuration.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400322
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500323 The structure is immutable.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400324
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400325 .command and .isolate_dir describe how to run the command. .isolate_dir uses
326 the OS' native path separator. It must be an absolute path, it's the path
327 where to start the command from.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400328 .files is the list of dependencies. The items use '/' as a path separator.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400329 .read_only describe how to map the files.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500330 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400331 def __init__(self, values, isolate_dir):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500332 verify_variables(values)
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400333 if isolate_dir is None:
334 # It must be an empty object if isolate_dir is None.
335 assert values == {}, values
336 else:
337 # Otherwise, the path must be absolute.
338 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400339
Marc-Antoine Ruel5b827c92014-11-14 18:40:27 -0500340 self.files = sorted(values.get('files', []))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500341 self.command = values.get('command', [])[:]
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400342 self.isolate_dir = isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500343 self.read_only = values.get('read_only')
344
345 def union(self, rhs):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400346 """Merges two config settings together into a new instance.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500347
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400348 A new instance is not created and self or rhs is returned if the other
349 object is the empty object.
350
351 self has priority over rhs for .command. Use the same .isolate_dir as the
352 one having a .command.
353
354 Dependencies listed in rhs are patch adjusted ONLY if they don't start with
355 a path variable, e.g. the characters '<('.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500356 """
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400357 # When an object has .isolate_dir == None, it means it is the empty object.
358 if rhs.isolate_dir is None:
359 return self
360 if self.isolate_dir is None:
361 return rhs
362
363 if sys.platform == 'win32':
364 assert self.isolate_dir[0].lower() == rhs.isolate_dir[0].lower()
365
366 # Takes the difference between the two isolate_dir. Note that while
367 # isolate_dir is in native path case, all other references are in posix.
368 l_rel_cwd, r_rel_cwd = self.isolate_dir, rhs.isolate_dir
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400369 if self.command or rhs.command:
370 use_rhs = bool(not self.command and rhs.command)
371 else:
372 # If self doesn't define any file, use rhs.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400373 use_rhs = not bool(self.files)
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400374 if use_rhs:
375 # Rebase files in rhs.
376 l_rel_cwd, r_rel_cwd = r_rel_cwd, l_rel_cwd
377
378 rebase_path = os.path.relpath(r_rel_cwd, l_rel_cwd).replace(
379 os.path.sep, '/')
380 def rebase_item(f):
381 if f.startswith('<(') or rebase_path == '.':
382 return f
383 return posixpath.join(rebase_path, f)
384
385 def map_both(l, r):
386 """Rebase items in either lhs or rhs, as needed."""
387 if use_rhs:
388 l, r = r, l
389 return sorted(l + map(rebase_item, r))
390
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500391 var = {
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500392 'command': self.command or rhs.command,
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400393 'files': map_both(self.files, rhs.files),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500394 'read_only': rhs.read_only if self.read_only is None else self.read_only,
395 }
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400396 return ConfigSettings(var, l_rel_cwd)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500397
398 def flatten(self):
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400399 """Converts the object into a dict."""
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500400 out = {}
401 if self.command:
402 out['command'] = self.command
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400403 if self.files:
404 out['files'] = self.files
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500405 if self.read_only is not None:
406 out['read_only'] = self.read_only
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400407 # TODO(maruel): Probably better to not output it if command is None?
408 if self.isolate_dir is not None:
409 out['isolate_dir'] = self.isolate_dir
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500410 return out
411
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400412 def __str__(self):
413 """Returns a short representation useful for debugging."""
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400414 files = ''.join('\n ' + f for f in self.files)
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400415 return 'ConfigSettings(%s, %s, %s, %s)' % (
416 self.command,
417 self.isolate_dir,
418 self.read_only,
419 files or '[]')
420
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500421
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500422def _safe_index(l, k):
423 try:
424 return l.index(k)
425 except ValueError:
426 return None
427
428
429def _get_map_keys(dest_keys, in_keys):
430 """Returns a tuple of the indexes of each item in in_keys found in dest_keys.
431
432 For example, if in_keys is ('A', 'C') and dest_keys is ('A', 'B', 'C'), the
433 return value will be (0, None, 1).
434 """
435 return tuple(_safe_index(in_keys, k) for k in dest_keys)
436
437
438def _map_keys(mapping, items):
439 """Returns a tuple with items placed at mapping index.
440
441 For example, if mapping is (1, None, 0) and items is ('a', 'b'), it will
442 return ('b', None, 'c').
443 """
444 return tuple(items[i] if i != None else None for i in mapping)
445
446
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500447class Configs(object):
448 """Represents a processed .isolate file.
449
450 Stores the file in a processed way, split by configuration.
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500451
452 At this point, we don't know all the possibilities. So mount a partial view
453 that we have.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400454
455 This class doesn't hold isolate_dir, since it is dependent on the final
456 configuration selected. It is implicitly dependent on which .isolate defines
457 the 'command' that will take effect.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500458 """
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500459 def __init__(self, file_comment, config_variables):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500460 self.file_comment = file_comment
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500461 # Contains the names of the config variables seen while processing
462 # .isolate file(s). The order is important since the same order is used for
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500463 # keys in self._by_config.
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500464 assert isinstance(config_variables, tuple)
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400465 assert all(isinstance(c, basestring) for c in config_variables), (
466 config_variables)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400467 config_variables = tuple(config_variables)
468 assert tuple(sorted(config_variables)) == config_variables, config_variables
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500469 self._config_variables = config_variables
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500470 # The keys of _by_config are tuples of values for each of the items in
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500471 # self._config_variables. A None item in the list of the key means the value
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500472 # is unbounded.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500473 self._by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500474
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500475 @property
476 def config_variables(self):
477 return self._config_variables
478
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500479 def get_config(self, config):
480 """Returns all configs that matches this config as a single ConfigSettings.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400481
482 Returns an empty ConfigSettings if none apply.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500483 """
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400484 # TODO(maruel): Fix ordering based on the bounded values. The keys are not
485 # necessarily sorted in the way that makes sense, they are alphabetically
486 # sorted. It is important because the left-most takes predescence.
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400487 out = ConfigSettings({}, None)
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400488 for k, v in sorted(self._by_config.iteritems()):
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500489 if all(i == j or j is None for i, j in zip(config, k)):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400490 out = out.union(v)
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500491 return out
492
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400493 def set_config(self, key, value):
494 """Sets the ConfigSettings for this key.
495
496 The key is a tuple of bounded or unbounded variables. The global variable
497 is the key where all values are unbounded, e.g.:
498 (None,) * len(self._config_variables)
499 """
500 assert key not in self._by_config, (key, self._by_config.keys())
501 assert isinstance(key, tuple)
502 assert len(key) == len(self._config_variables), (
503 key, self._config_variables)
504 assert isinstance(value, ConfigSettings)
505 self._by_config[key] = value
506
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500507 def union(self, rhs):
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400508 """Returns a new Configs instance, the union of variables from self and rhs.
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500509
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400510 Uses self.file_comment if available, otherwise rhs.file_comment.
Marc-Antoine Ruelf0d07872014-03-27 16:59:03 -0400511 It keeps config_variables sorted in the output.
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400512 """
513 # Merge the keys of config_variables for each Configs instances. All the new
514 # variables will become unbounded. This requires realigning the keys.
515 config_variables = tuple(sorted(
516 set(self.config_variables) | set(rhs.config_variables)))
517 out = Configs(self.file_comment or rhs.file_comment, config_variables)
518 mapping_lhs = _get_map_keys(out.config_variables, self.config_variables)
519 mapping_rhs = _get_map_keys(out.config_variables, rhs.config_variables)
520 lhs_config = dict(
521 (_map_keys(mapping_lhs, k), v) for k, v in self._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500522 # pylint: disable=W0212
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400523 rhs_config = dict(
524 (_map_keys(mapping_rhs, k), v) for k, v in rhs._by_config.iteritems())
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500525
526 for key in set(lhs_config) | set(rhs_config):
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400527 l = lhs_config.get(key)
528 r = rhs_config.get(key)
529 out.set_config(key, l.union(r) if (l and r) else (l or r))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500530 return out
531
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500532 def flatten(self):
533 """Returns a flat dictionary representation of the configuration.
534 """
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500535 return dict((k, v.flatten()) for k, v in self._by_config.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500536
Marc-Antoine Ruel4eeada92014-04-03 13:54:26 -0400537 def __str__(self):
538 return 'Configs(%s,%s)' % (
539 self._config_variables,
540 ''.join('\n %s' % str(f) for f in self._by_config))
541
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500542
Marc-Antoine Ruelb61a1802015-03-09 15:18:18 -0400543def load_included_isolate(isolate_dir, isolate_path):
544 if os.path.isabs(isolate_path):
545 raise IsolateError(
546 'Failed to load configuration; absolute include path \'%s\'' %
547 isolate_path)
548 included_isolate = os.path.normpath(os.path.join(isolate_dir, isolate_path))
549 if sys.platform == 'win32':
550 if included_isolate[0].lower() != isolate_dir[0].lower():
551 raise IsolateError(
552 'Can\'t reference a .isolate file from another drive')
maruel12e30012015-10-09 11:55:35 -0700553 with fs.open(included_isolate, 'r') as f:
Marc-Antoine Ruelb61a1802015-03-09 15:18:18 -0400554 return load_isolate_as_config(
555 os.path.dirname(included_isolate),
556 eval_content(f.read()),
557 None)
558
559
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500560def load_isolate_as_config(isolate_dir, value, file_comment):
561 """Parses one .isolate file and returns a Configs() instance.
562
563 Arguments:
564 isolate_dir: only used to load relative includes so it doesn't depend on
565 cwd.
566 value: is the loaded dictionary that was defined in the gyp file.
567 file_comment: comments found at the top of the file so it can be preserved.
568
569 The expected format is strict, anything diverting from the format below will
570 throw an assert:
571 {
572 'includes': [
573 'foo.isolate',
574 ],
575 'conditions': [
576 ['OS=="vms" and foo=42', {
577 'variables': {
578 'command': [
579 ...
580 ],
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400581 'files': [
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500582 ...
583 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500584 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500585 },
586 }],
587 ...
588 ],
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400589 'variables': {
590 ...
591 },
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500592 }
593 """
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400594 assert os.path.isabs(isolate_dir), isolate_dir
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400595 if any(len(cond) == 3 for cond in value.get('conditions', [])):
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400596 raise IsolateError('Using \'else\' is not supported anymore.')
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500597 variables_and_values = {}
598 verify_root(value, variables_and_values)
599 if variables_and_values:
600 config_variables, config_values = zip(
601 *sorted(variables_and_values.iteritems()))
602 all_configs = list(itertools.product(*config_values))
603 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500604 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500605 all_configs = []
606
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500607 isolate = Configs(file_comment, config_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500608
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400609 # Add global variables. The global variables are on the empty tuple key.
610 isolate.set_config(
611 (None,) * len(config_variables),
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400612 ConfigSettings(value.get('variables', {}), isolate_dir))
Marc-Antoine Rueld7c032b2014-03-13 15:32:16 -0400613
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500614 # Add configuration-specific variables.
615 for expr, then in value.get('conditions', []):
616 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500617 new = Configs(None, config_variables)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500618 for config in configs:
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400619 new.set_config(config, ConfigSettings(then['variables'], isolate_dir))
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500620 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500621
Marc-Antoine Ruelb61a1802015-03-09 15:18:18 -0400622 # If the .isolate contains command, ignore any command in child .isolate.
623 root_has_command = any(c.command for c in isolate._by_config.itervalues())
624
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500625 # Load the includes. Process them in reverse so the last one take precedence.
626 for include in reversed(value.get('includes', [])):
Marc-Antoine Ruelb61a1802015-03-09 15:18:18 -0400627 included = load_included_isolate(isolate_dir, include)
628 if root_has_command:
629 # Strip any command in the imported isolate. It is because the chosen
630 # command is not related to the one in the top-most .isolate, since the
631 # configuration is flattened.
632 for c in included._by_config.itervalues():
633 c.command = []
634 isolate = isolate.union(included)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500635
636 return isolate
637
638
639def load_isolate_for_config(isolate_dir, content, config_variables):
640 """Loads the .isolate file and returns the information unprocessed but
641 filtered for the specific OS.
642
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400643 Returns:
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400644 tuple of command, dependencies, read_only flag, isolate_dir.
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400645 The dependencies are fixed to use os.path.sep.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500646 """
647 # Load the .isolate file, process its conditions, retrieve the command and
648 # dependencies.
649 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
650 try:
651 config_name = tuple(
652 config_variables[var] for var in isolate.config_variables)
653 except KeyError:
Marc-Antoine Ruele819be42014-08-28 19:38:20 -0400654 raise IsolateError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500655 'These configuration variables were missing from the command line: %s' %
656 ', '.join(
657 sorted(set(isolate.config_variables) - set(config_variables))))
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500658
659 # A configuration is to be created with all the combinations of free
660 # variables.
661 config = isolate.get_config(config_name)
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400662 dependencies = [f.replace('/', os.path.sep) for f in config.files]
663 return config.command, dependencies, config.read_only, config.isolate_dir