blob: fc5eb771c3154aa8d4b9dde51f2d3942beac1351 [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 """
500 def __init__(self, file_comment):
501 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 Ruela5a36222014-01-09 10:35:45 -0500505 self.config_variables = ()
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500506 # The keys of by_config are tuples of values for each of the items in
507 # self.config_variables. A None item in the list of the key means the value
508 # is unbounded.
509 self.by_config = {}
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500510
511 def union(self, rhs):
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500512 """Adds variables from rhs (a Configs) to the existing variables."""
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500513 config_variables = self.config_variables
514 if not config_variables:
515 config_variables = rhs.config_variables
516 else:
517 # We can't proceed if this isn't true since we don't know the correct
518 # default values for extra variables. The variables are sorted so we
519 # don't need to worry about permutations.
520 if rhs.config_variables and rhs.config_variables != config_variables:
521 raise isolateserver.ConfigError(
522 'Variables in merged .isolate files do not match: %r and %r' % (
523 config_variables, rhs.config_variables))
524
525 # Takes the first file comment, prefering lhs.
526 out = Configs(self.file_comment or rhs.file_comment)
527 out.config_variables = config_variables
528 for config in set(self.by_config) | set(rhs.by_config):
529 out.by_config[config] = union(
530 self.by_config.get(config), rhs.by_config.get(config))
531 return out
532
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500533 def flatten(self):
534 """Returns a flat dictionary representation of the configuration.
535 """
536 return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
537
538 def make_isolate_file(self):
539 """Returns a dictionary suitable for writing to a .isolate file.
540 """
541 dependencies_by_config = self.flatten()
542 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
543 return convert_map_to_isolate_dict(configs_by_dependency,
544 self.config_variables)
545
546
547# TODO(benrg): Remove this function when no old-format files are left.
548def convert_old_to_new_format(value):
549 """Converts from the old .isolate format, which only has one variable (OS),
550 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
551 and allows conditions that depend on the set of all OSes, to the new format,
552 which allows any set of variables, has no hardcoded values, and only allows
553 explicit positive tests of variable values.
554 """
555 conditions = value.get('conditions', [])
556 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
557 return value # Nothing to change
558
559 def parse_condition(cond):
560 m = re.match(r'OS=="(\w+)"\Z', cond[0])
561 if not m:
562 raise isolateserver.ConfigError('Invalid condition: %s' % cond[0])
563 return m.group(1)
564
565 oses = set(map(parse_condition, conditions))
566 default_oses = set(['linux', 'mac', 'win'])
567 oses = sorted(oses | default_oses)
568
569 def if_not_os(not_os, then):
570 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
571 return [expr, then]
572
573 conditions = [
574 cond[:2] for cond in conditions if cond[1]
575 ] + [
576 if_not_os(parse_condition(cond), cond[2])
577 for cond in conditions if len(cond) == 3
578 ]
579
580 if 'variables' in value:
581 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
582 conditions.sort()
583
584 value = value.copy()
585 value['conditions'] = conditions
586 return value
587
588
589def load_isolate_as_config(isolate_dir, value, file_comment):
590 """Parses one .isolate file and returns a Configs() instance.
591
592 Arguments:
593 isolate_dir: only used to load relative includes so it doesn't depend on
594 cwd.
595 value: is the loaded dictionary that was defined in the gyp file.
596 file_comment: comments found at the top of the file so it can be preserved.
597
598 The expected format is strict, anything diverting from the format below will
599 throw an assert:
600 {
601 'includes': [
602 'foo.isolate',
603 ],
604 'conditions': [
605 ['OS=="vms" and foo=42', {
606 'variables': {
607 'command': [
608 ...
609 ],
610 'isolate_dependency_tracked': [
611 ...
612 ],
613 'isolate_dependency_untracked': [
614 ...
615 ],
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500616 'read_only': 0,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500617 },
618 }],
619 ...
620 ],
621 }
622 """
623 value = convert_old_to_new_format(value)
624
625 variables_and_values = {}
626 verify_root(value, variables_and_values)
627 if variables_and_values:
628 config_variables, config_values = zip(
629 *sorted(variables_and_values.iteritems()))
630 all_configs = list(itertools.product(*config_values))
631 else:
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500632 config_variables = ()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500633 all_configs = []
634
635 isolate = Configs(file_comment)
636
637 # Add configuration-specific variables.
638 for expr, then in value.get('conditions', []):
639 configs = match_configs(expr, config_variables, all_configs)
Marc-Antoine Ruel9ac1b912014-01-10 09:08:42 -0500640 new = Configs(None)
641 new.config_variables = config_variables
642 for config in configs:
643 new.by_config[config] = ConfigSettings(then['variables'])
644 isolate = isolate.union(new)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500645
646 # Load the includes. Process them in reverse so the last one take precedence.
647 for include in reversed(value.get('includes', [])):
648 if os.path.isabs(include):
649 raise isolateserver.ConfigError(
650 'Failed to load configuration; absolute include path \'%s\'' %
651 include)
652 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
653 with open(included_isolate, 'r') as f:
654 included_isolate = load_isolate_as_config(
655 os.path.dirname(included_isolate),
656 eval_content(f.read()),
657 None)
658 isolate = union(isolate, included_isolate)
659
660 return isolate
661
662
663def load_isolate_for_config(isolate_dir, content, config_variables):
664 """Loads the .isolate file and returns the information unprocessed but
665 filtered for the specific OS.
666
667 Returns the command, dependencies and read_only flag. The dependencies are
668 fixed to use os.path.sep.
669 """
670 # Load the .isolate file, process its conditions, retrieve the command and
671 # dependencies.
672 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
673 try:
674 config_name = tuple(
675 config_variables[var] for var in isolate.config_variables)
676 except KeyError:
677 raise isolateserver.ConfigError(
678 'These configuration variables were missing from the command line: %s' %
679 ', '.join(
680 sorted(set(isolate.config_variables) - set(config_variables))))
681 config = isolate.by_config.get(config_name)
682 if not config:
683 raise isolateserver.ConfigError(
684 'Failed to load configuration for variable \'%s\' for config(s) \'%s\''
685 '\nAvailable configs: %s' %
686 (', '.join(isolate.config_variables),
687 ', '.join(config_name),
688 ', '.join(str(s) for s in isolate.by_config)))
689 # Merge tracked and untracked variables, isolate.py doesn't care about the
690 # trackability of the variables, only the build tool does.
691 dependencies = [
692 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
693 ]
694 touched = [f.replace('/', os.path.sep) for f in config.touched]
695 return config.command, dependencies, touched, config.read_only