blob: ca89640036687378dab813f6114d1c73afcd0f9a [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
16import copy
17import itertools
18import logging
19import os
20import re
21
22import isolateserver
23
24from utils import short_expression_finder
25
26# Files that should be 0-length when mapped.
27KEY_TOUCHED = 'isolate_dependency_touched'
28# Files that should be tracked by the build tool.
29KEY_TRACKED = 'isolate_dependency_tracked'
30# Files that should not be tracked by the build tool.
31KEY_UNTRACKED = 'isolate_dependency_untracked'
32
33# Valid variable name.
34VALID_VARIABLE = '[A-Za-z_][A-Za-z_0-9]*'
35
36
37def determine_root_dir(relative_root, infiles):
38 """For a list of infiles, determines the deepest root directory that is
39 referenced indirectly.
40
41 All arguments must be using os.path.sep.
42 """
43 # The trick used to determine the root directory is to look at "how far" back
44 # up it is looking up.
45 deepest_root = relative_root
46 for i in infiles:
47 x = relative_root
48 while i.startswith('..' + os.path.sep):
49 i = i[3:]
50 assert not i.startswith(os.path.sep)
51 x = os.path.dirname(x)
52 if deepest_root.startswith(x):
53 deepest_root = x
54 logging.debug(
55 'determine_root_dir(%s, %d files) -> %s' % (
56 relative_root, len(infiles), deepest_root))
57 return deepest_root
58
59
60def replace_variable(part, variables):
61 m = re.match(r'<\((' + VALID_VARIABLE + ')\)', part)
62 if m:
63 if m.group(1) not in variables:
64 raise isolateserver.ConfigError(
65 'Variable "%s" was not found in %s.\nDid you forget to specify '
66 '--path-variable?' % (m.group(1), variables))
67 return variables[m.group(1)]
68 return part
69
70
71def eval_variables(item, variables):
72 """Replaces the .isolate variables in a string item.
73
74 Note that the .isolate format is a subset of the .gyp dialect.
75 """
76 return ''.join(
77 replace_variable(p, variables)
78 for p in re.split(r'(<\(' + VALID_VARIABLE + '\))', item))
79
80
81def split_touched(files):
82 """Splits files that are touched vs files that are read."""
83 tracked = []
84 touched = []
85 for f in files:
86 if f.size:
87 tracked.append(f)
88 else:
89 touched.append(f)
90 return tracked, touched
91
92
93def pretty_print(variables, stdout):
94 """Outputs a gyp compatible list from the decoded variables.
95
96 Similar to pprint.print() but with NIH syndrome.
97 """
98 # Order the dictionary keys by these keys in priority.
99 ORDER = (
100 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
101 KEY_TRACKED, KEY_UNTRACKED)
102
103 def sorting_key(x):
104 """Gives priority to 'most important' keys before the others."""
105 if x in ORDER:
106 return str(ORDER.index(x))
107 return x
108
109 def loop_list(indent, items):
110 for item in items:
111 if isinstance(item, basestring):
112 stdout.write('%s\'%s\',\n' % (indent, item))
113 elif isinstance(item, dict):
114 stdout.write('%s{\n' % indent)
115 loop_dict(indent + ' ', item)
116 stdout.write('%s},\n' % indent)
117 elif isinstance(item, list):
118 # A list inside a list will write the first item embedded.
119 stdout.write('%s[' % indent)
120 for index, i in enumerate(item):
121 if isinstance(i, basestring):
122 stdout.write(
123 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
124 elif isinstance(i, dict):
125 stdout.write('{\n')
126 loop_dict(indent + ' ', i)
127 if index != len(item) - 1:
128 x = ', '
129 else:
130 x = ''
131 stdout.write('%s}%s' % (indent, x))
132 else:
133 assert False
134 stdout.write('],\n')
135 else:
136 assert False
137
138 def loop_dict(indent, items):
139 for key in sorted(items, key=sorting_key):
140 item = items[key]
141 stdout.write("%s'%s': " % (indent, key))
142 if isinstance(item, dict):
143 stdout.write('{\n')
144 loop_dict(indent + ' ', item)
145 stdout.write(indent + '},\n')
146 elif isinstance(item, list):
147 stdout.write('[\n')
148 loop_list(indent + ' ', item)
149 stdout.write(indent + '],\n')
150 elif isinstance(item, basestring):
151 stdout.write(
152 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500153 elif isinstance(item, (int, bool)) or item is None:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500154 stdout.write('%s\n' % item)
155 else:
156 assert False, item
157
158 stdout.write('{\n')
159 loop_dict(' ', variables)
160 stdout.write('}\n')
161
162
163def print_all(comment, data, stream):
164 """Prints a complete .isolate file and its top-level file comment into a
165 stream.
166 """
167 if comment:
168 stream.write(comment)
169 pretty_print(data, stream)
170
171
172def union(lhs, rhs):
173 """Merges two compatible datastructures composed of dict/list/set."""
174 assert lhs is not None or rhs is not None
175 if lhs is None:
176 return copy.deepcopy(rhs)
177 if rhs is None:
178 return copy.deepcopy(lhs)
179 assert type(lhs) == type(rhs), (lhs, rhs)
180 if hasattr(lhs, 'union'):
181 # Includes set, ConfigSettings and Configs.
182 return lhs.union(rhs)
183 if isinstance(lhs, dict):
184 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
185 elif isinstance(lhs, list):
186 # Do not go inside the list.
187 return lhs + rhs
188 assert False, type(lhs)
189
190
191def extract_comment(content):
192 """Extracts file level comment."""
193 out = []
194 for line in content.splitlines(True):
195 if line.startswith('#'):
196 out.append(line)
197 else:
198 break
199 return ''.join(out)
200
201
202def eval_content(content):
203 """Evaluates a python file and return the value defined in it.
204
205 Used in practice for .isolate files.
206 """
207 globs = {'__builtins__': None}
208 locs = {}
209 try:
210 value = eval(content, globs, locs)
211 except TypeError as e:
212 e.args = list(e.args) + [content]
213 raise
214 assert locs == {}, locs
215 assert globs == {'__builtins__': None}, globs
216 return value
217
218
219def match_configs(expr, config_variables, all_configs):
220 """Returns the configs from |all_configs| that match the |expr|, where
221 the elements of |all_configs| are tuples of values for the |config_variables|.
222 Example:
223 >>> match_configs(expr = "(foo==1 or foo==2) and bar=='b'",
224 config_variables = ["foo", "bar"],
225 all_configs = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
226 [(1, 'b'), (2, 'b')]
227 """
228 return [
229 config for config in all_configs
230 if eval(expr, dict(zip(config_variables, config)))
231 ]
232
233
234def verify_variables(variables):
235 """Verifies the |variables| dictionary is in the expected format."""
236 VALID_VARIABLES = [
237 KEY_TOUCHED,
238 KEY_TRACKED,
239 KEY_UNTRACKED,
240 'command',
241 'read_only',
242 ]
243 assert isinstance(variables, dict), variables
244 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
245 for name, value in variables.iteritems():
246 if name == 'read_only':
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500247 assert value in (0, 1, 2, None), value
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500248 else:
249 assert isinstance(value, list), value
250 assert all(isinstance(i, basestring) for i in value), value
251
252
253def verify_ast(expr, variables_and_values):
254 """Verifies that |expr| is of the form
255 expr ::= expr ( "or" | "and" ) expr
256 | identifier "==" ( string | int )
257 Also collects the variable identifiers and string/int values in the dict
258 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
259 """
260 assert isinstance(expr, (ast.BoolOp, ast.Compare))
261 if isinstance(expr, ast.BoolOp):
262 assert isinstance(expr.op, (ast.And, ast.Or))
263 for subexpr in expr.values:
264 verify_ast(subexpr, variables_and_values)
265 else:
266 assert isinstance(expr.left.ctx, ast.Load)
267 assert len(expr.ops) == 1
268 assert isinstance(expr.ops[0], ast.Eq)
269 var_values = variables_and_values.setdefault(expr.left.id, set())
270 rhs = expr.comparators[0]
271 assert isinstance(rhs, (ast.Str, ast.Num))
272 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
273
274
275def verify_condition(condition, variables_and_values):
276 """Verifies the |condition| dictionary is in the expected format.
277 See verify_ast() for the meaning of |variables_and_values|.
278 """
279 VALID_INSIDE_CONDITION = ['variables']
280 assert isinstance(condition, list), condition
281 assert len(condition) == 2, condition
282 expr, then = condition
283
284 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
285 verify_ast(test_ast.body, variables_and_values)
286
287 assert isinstance(then, dict), then
288 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
289 if not 'variables' in then:
290 raise isolateserver.ConfigError('Missing \'variables\' in condition %s' %
291 condition)
292 verify_variables(then['variables'])
293
294
295def verify_root(value, variables_and_values):
296 """Verifies that |value| is the parsed form of a valid .isolate file.
297 See verify_ast() for the meaning of |variables_and_values|.
298 """
299 VALID_ROOTS = ['includes', 'conditions']
300 assert isinstance(value, dict), value
301 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
302
303 includes = value.get('includes', [])
304 assert isinstance(includes, list), includes
305 for include in includes:
306 assert isinstance(include, basestring), include
307
308 conditions = value.get('conditions', [])
309 assert isinstance(conditions, list), conditions
310 for condition in conditions:
311 verify_condition(condition, variables_and_values)
312
313
314def remove_weak_dependencies(values, key, item, item_configs):
315 """Removes any configs from this key if the item is already under a
316 strong key.
317 """
318 if key == KEY_TOUCHED:
319 item_configs = set(item_configs)
320 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
321 try:
322 item_configs -= values[stronger_key][item]
323 except KeyError:
324 pass
325
326 return item_configs
327
328
329def remove_repeated_dependencies(folders, key, item, item_configs):
330 """Removes any configs from this key if the item is in a folder that is
331 already included."""
332
333 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
334 item_configs = set(item_configs)
335 for (folder, configs) in folders.iteritems():
336 if folder != item and item.startswith(folder):
337 item_configs -= configs
338
339 return item_configs
340
341
342def get_folders(values_dict):
343 """Returns a dict of all the folders in the given value_dict."""
344 return dict(
345 (item, configs) for (item, configs) in values_dict.iteritems()
346 if item.endswith('/')
347 )
348
349
350def invert_map(variables):
351 """Converts {config: {deptype: list(depvals)}} to
352 {deptype: {depval: set(configs)}}.
353 """
354 KEYS = (
355 KEY_TOUCHED,
356 KEY_TRACKED,
357 KEY_UNTRACKED,
358 'command',
359 'read_only',
360 )
361 out = dict((key, {}) for key in KEYS)
362 for config, values in variables.iteritems():
363 for key in KEYS:
364 if key == 'command':
365 items = [tuple(values[key])] if key in values else []
366 elif key == 'read_only':
367 items = [values[key]] if key in values else []
368 else:
369 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
370 items = values.get(key, [])
371 for item in items:
372 out[key].setdefault(item, set()).add(config)
373 return out
374
375
376def reduce_inputs(values):
377 """Reduces the output of invert_map() to the strictest minimum list.
378
379 Looks at each individual file and directory, maps where they are used and
380 reconstructs the inverse dictionary.
381
382 Returns the minimized dictionary.
383 """
384 KEYS = (
385 KEY_TOUCHED,
386 KEY_TRACKED,
387 KEY_UNTRACKED,
388 'command',
389 'read_only',
390 )
391
392 # Folders can only live in KEY_UNTRACKED.
393 folders = get_folders(values.get(KEY_UNTRACKED, {}))
394
395 out = dict((key, {}) for key in KEYS)
396 for key in KEYS:
397 for item, item_configs in values.get(key, {}).iteritems():
398 item_configs = remove_weak_dependencies(values, key, item, item_configs)
399 item_configs = remove_repeated_dependencies(
400 folders, key, item, item_configs)
401 if item_configs:
402 out[key][item] = item_configs
403 return out
404
405
406def convert_map_to_isolate_dict(values, config_variables):
407 """Regenerates back a .isolate configuration dict from files and dirs
408 mappings generated from reduce_inputs().
409 """
410 # Gather a list of configurations for set inversion later.
411 all_mentioned_configs = set()
412 for configs_by_item in values.itervalues():
413 for configs in configs_by_item.itervalues():
414 all_mentioned_configs.update(configs)
415
416 # Invert the mapping to make it dict first.
417 conditions = {}
418 for key in values:
419 for item, configs in values[key].iteritems():
420 then = conditions.setdefault(frozenset(configs), {})
421 variables = then.setdefault('variables', {})
422
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500423 if isinstance(item, int):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500424 # One-off for read_only.
425 variables[key] = item
426 else:
427 assert item
428 if isinstance(item, tuple):
429 # One-off for command.
430 # Do not merge lists and do not sort!
431 # Note that item is a tuple.
432 assert key not in variables
433 variables[key] = list(item)
434 else:
435 # The list of items (files or dirs). Append the new item and keep
436 # the list sorted.
437 l = variables.setdefault(key, [])
438 l.append(item)
439 l.sort()
440
441 if all_mentioned_configs:
442 config_values = map(set, zip(*all_mentioned_configs))
443 sef = short_expression_finder.ShortExpressionFinder(
444 zip(config_variables, config_values))
445
446 conditions = sorted(
447 [sef.get_expr(configs), then] for configs, then in conditions.iteritems())
448 return {'conditions': conditions}
449
450
451class ConfigSettings(object):
452 """Represents the dependency variables for a single build configuration.
453 The structure is immutable.
454 """
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500455 def __init__(self, values):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500456 verify_variables(values)
457 self.touched = sorted(values.get(KEY_TOUCHED, []))
458 self.tracked = sorted(values.get(KEY_TRACKED, []))
459 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
460 self.command = values.get('command', [])[:]
461 self.read_only = values.get('read_only')
462
463 def union(self, rhs):
464 """Merges two config settings together.
465
466 self has priority over rhs for 'command' variable.
467 """
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500468 var = {
469 KEY_TOUCHED: sorted(self.touched + rhs.touched),
470 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
471 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
472 'command': self.command or rhs.command,
473 'read_only': rhs.read_only if self.read_only is None else self.read_only,
474 }
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500475 return ConfigSettings(var)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500476
477 def flatten(self):
478 out = {}
479 if self.command:
480 out['command'] = self.command
481 if self.touched:
482 out[KEY_TOUCHED] = self.touched
483 if self.tracked:
484 out[KEY_TRACKED] = self.tracked
485 if self.untracked:
486 out[KEY_UNTRACKED] = self.untracked
487 if self.read_only is not None:
488 out['read_only'] = self.read_only
489 return out
490
491
492class Configs(object):
493 """Represents a processed .isolate file.
494
495 Stores the file in a processed way, split by configuration.
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500496
497 At this point, we don't know all the possibilities. So mount a partial view
498 that we have.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500499 """
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500500 def __init__(self, file_comment, config_variables):
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500501 self.file_comment = file_comment
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500502 # Contains the names of the config variables seen while processing
503 # .isolate file(s). The order is important since the same order is used for
504 # keys in self.by_config.
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500505 assert isinstance(config_variables, tuple)
506 self._config_variables = config_variables
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500507 # The keys of by_config are tuples of values for each of the items in
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500508 # self._config_variables. A None item in the list of the key means the value
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500509 # is unbounded.
510 self.by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500511
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500512 @property
513 def config_variables(self):
514 return self._config_variables
515
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500516 def union(self, rhs):
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500517 """Adds variables from rhs (a Configs) to the existing variables."""
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500518 # Takes the first file comment, prefering lhs.
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500519 comment = self.file_comment or rhs.file_comment
520 if not self.config_variables:
521 assert not self.by_config
522 out = Configs(comment, rhs.config_variables)
523 elif not rhs.config_variables:
524 assert not rhs.by_config
525 out = Configs(comment, self.config_variables)
526 elif rhs.config_variables == self.config_variables:
527 out = Configs(comment, self.config_variables)
528 else:
529 # TODO(maruel): Fix this.
530 raise isolateserver.ConfigError(
531 'Variables in merged .isolate files do not match: %r and %r' % (
532 self.config_variables, rhs.config_variables))
533
534 for key in set(self.by_config) | set(rhs.by_config):
535 out.by_config[key] = union(
536 self.by_config.get(key), rhs.by_config.get(key))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500537 return out
538
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500539 def flatten(self):
540 """Returns a flat dictionary representation of the configuration.
541 """
542 return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
543
544 def make_isolate_file(self):
545 """Returns a dictionary suitable for writing to a .isolate file.
546 """
547 dependencies_by_config = self.flatten()
548 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
549 return convert_map_to_isolate_dict(configs_by_dependency,
550 self.config_variables)
551
552
553# TODO(benrg): Remove this function when no old-format files are left.
554def convert_old_to_new_format(value):
555 """Converts from the old .isolate format, which only has one variable (OS),
556 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
557 and allows conditions that depend on the set of all OSes, to the new format,
558 which allows any set of variables, has no hardcoded values, and only allows
559 explicit positive tests of variable values.
560 """
561 conditions = value.get('conditions', [])
562 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
563 return value # Nothing to change
564
565 def parse_condition(cond):
566 m = re.match(r'OS=="(\w+)"\Z', cond[0])
567 if not m:
568 raise isolateserver.ConfigError('Invalid condition: %s' % cond[0])
569 return m.group(1)
570
571 oses = set(map(parse_condition, conditions))
572 default_oses = set(['linux', 'mac', 'win'])
573 oses = sorted(oses | default_oses)
574
575 def if_not_os(not_os, then):
576 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
577 return [expr, then]
578
579 conditions = [
580 cond[:2] for cond in conditions if cond[1]
581 ] + [
582 if_not_os(parse_condition(cond), cond[2])
583 for cond in conditions if len(cond) == 3
584 ]
585
586 if 'variables' in value:
587 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
588 conditions.sort()
589
590 value = value.copy()
591 value['conditions'] = conditions
592 return value
593
594
595def load_isolate_as_config(isolate_dir, value, file_comment):
596 """Parses one .isolate file and returns a Configs() instance.
597
598 Arguments:
599 isolate_dir: only used to load relative includes so it doesn't depend on
600 cwd.
601 value: is the loaded dictionary that was defined in the gyp file.
602 file_comment: comments found at the top of the file so it can be preserved.
603
604 The expected format is strict, anything diverting from the format below will
605 throw an assert:
606 {
607 'includes': [
608 'foo.isolate',
609 ],
610 'conditions': [
611 ['OS=="vms" and foo=42', {
612 'variables': {
613 'command': [
614 ...
615 ],
616 'isolate_dependency_tracked': [
617 ...
618 ],
619 'isolate_dependency_untracked': [
620 ...
621 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500622 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500623 },
624 }],
625 ...
626 ],
627 }
628 """
629 value = convert_old_to_new_format(value)
630
631 variables_and_values = {}
632 verify_root(value, variables_and_values)
633 if variables_and_values:
634 config_variables, config_values = zip(
635 *sorted(variables_and_values.iteritems()))
636 all_configs = list(itertools.product(*config_values))
637 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500638 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500639 all_configs = []
640
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500641 isolate = Configs(file_comment, config_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500642
643 # Add configuration-specific variables.
644 for expr, then in value.get('conditions', []):
645 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500646 new = Configs(None, config_variables)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500647 for config in configs:
648 new.by_config[config] = ConfigSettings(then['variables'])
649 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500650
651 # Load the includes. Process them in reverse so the last one take precedence.
652 for include in reversed(value.get('includes', [])):
653 if os.path.isabs(include):
654 raise isolateserver.ConfigError(
655 'Failed to load configuration; absolute include path \'%s\'' %
656 include)
657 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
658 with open(included_isolate, 'r') as f:
659 included_isolate = load_isolate_as_config(
660 os.path.dirname(included_isolate),
661 eval_content(f.read()),
662 None)
663 isolate = union(isolate, included_isolate)
664
665 return isolate
666
667
668def load_isolate_for_config(isolate_dir, content, config_variables):
669 """Loads the .isolate file and returns the information unprocessed but
670 filtered for the specific OS.
671
672 Returns the command, dependencies and read_only flag. The dependencies are
673 fixed to use os.path.sep.
674 """
675 # Load the .isolate file, process its conditions, retrieve the command and
676 # dependencies.
677 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
678 try:
679 config_name = tuple(
680 config_variables[var] for var in isolate.config_variables)
681 except KeyError:
682 raise isolateserver.ConfigError(
683 'These configuration variables were missing from the command line: %s' %
684 ', '.join(
685 sorted(set(isolate.config_variables) - set(config_variables))))
686 config = isolate.by_config.get(config_name)
687 if not config:
688 raise isolateserver.ConfigError(
689 'Failed to load configuration for variable \'%s\' for config(s) \'%s\''
690 '\nAvailable configs: %s' %
691 (', '.join(isolate.config_variables),
692 ', '.join(config_name),
693 ', '.join(str(s) for s in isolate.by_config)))
694 # Merge tracked and untracked variables, isolate.py doesn't care about the
695 # trackability of the variables, only the build tool does.
696 dependencies = [
697 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
698 ]
699 touched = [f.replace('/', os.path.sep) for f in config.touched]
700 return config.command, dependencies, touched, config.read_only