Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 1 | # 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 | |
| 7 | This module doesn't touch the file system. It's the job of the client code to do |
| 8 | I/O on behalf of this module. |
| 9 | |
| 10 | See more information at |
| 11 | https://code.google.com/p/swarming/wiki/IsolateDesign |
| 12 | https://code.google.com/p/swarming/wiki/IsolateUserGuide |
| 13 | """ |
| 14 | |
| 15 | import ast |
| 16 | import copy |
| 17 | import itertools |
| 18 | import logging |
| 19 | import os |
| 20 | import re |
| 21 | |
| 22 | import isolateserver |
| 23 | |
| 24 | from utils import short_expression_finder |
| 25 | |
| 26 | # Files that should be 0-length when mapped. |
| 27 | KEY_TOUCHED = 'isolate_dependency_touched' |
| 28 | # Files that should be tracked by the build tool. |
| 29 | KEY_TRACKED = 'isolate_dependency_tracked' |
| 30 | # Files that should not be tracked by the build tool. |
| 31 | KEY_UNTRACKED = 'isolate_dependency_untracked' |
| 32 | |
| 33 | # Valid variable name. |
| 34 | VALID_VARIABLE = '[A-Za-z_][A-Za-z_0-9]*' |
| 35 | |
| 36 | |
| 37 | def 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 | |
| 60 | def 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 | |
| 71 | def 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 | |
| 81 | def 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 | |
| 93 | def 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 Ruel | 7124e39 | 2014-01-09 11:49:21 -0500 | [diff] [blame^] | 153 | elif isinstance(item, (int, bool)) or item is None: |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 154 | 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 | |
| 163 | def 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 | |
| 172 | def 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 | |
| 191 | def 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 | |
| 202 | def 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 | |
| 219 | def 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 | |
| 234 | def 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 Ruel | 7124e39 | 2014-01-09 11:49:21 -0500 | [diff] [blame^] | 247 | assert value in (0, 1, 2, None), value |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 248 | else: |
| 249 | assert isinstance(value, list), value |
| 250 | assert all(isinstance(i, basestring) for i in value), value |
| 251 | |
| 252 | |
| 253 | def 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 | |
| 275 | def 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 | |
| 295 | def 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 | |
| 314 | def 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 | |
| 329 | def 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 | |
| 342 | def 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 | |
| 350 | def 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 | |
| 376 | def 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 | |
| 406 | def 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 Ruel | 7124e39 | 2014-01-09 11:49:21 -0500 | [diff] [blame^] | 423 | if isinstance(item, int): |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 424 | # 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 | |
| 451 | class ConfigSettings(object): |
| 452 | """Represents the dependency variables for a single build configuration. |
| 453 | The structure is immutable. |
| 454 | """ |
| 455 | def __init__(self, config, values): |
| 456 | self.config = config |
| 457 | verify_variables(values) |
| 458 | self.touched = sorted(values.get(KEY_TOUCHED, [])) |
| 459 | self.tracked = sorted(values.get(KEY_TRACKED, [])) |
| 460 | self.untracked = sorted(values.get(KEY_UNTRACKED, [])) |
| 461 | self.command = values.get('command', [])[:] |
| 462 | self.read_only = values.get('read_only') |
| 463 | |
| 464 | def union(self, rhs): |
| 465 | """Merges two config settings together. |
| 466 | |
| 467 | self has priority over rhs for 'command' variable. |
| 468 | """ |
| 469 | assert not (self.config and rhs.config) or (self.config == rhs.config) |
| 470 | var = { |
| 471 | KEY_TOUCHED: sorted(self.touched + rhs.touched), |
| 472 | KEY_TRACKED: sorted(self.tracked + rhs.tracked), |
| 473 | KEY_UNTRACKED: sorted(self.untracked + rhs.untracked), |
| 474 | 'command': self.command or rhs.command, |
| 475 | 'read_only': rhs.read_only if self.read_only is None else self.read_only, |
| 476 | } |
| 477 | return ConfigSettings(self.config or rhs.config, var) |
| 478 | |
| 479 | def flatten(self): |
| 480 | out = {} |
| 481 | if self.command: |
| 482 | out['command'] = self.command |
| 483 | if self.touched: |
| 484 | out[KEY_TOUCHED] = self.touched |
| 485 | if self.tracked: |
| 486 | out[KEY_TRACKED] = self.tracked |
| 487 | if self.untracked: |
| 488 | out[KEY_UNTRACKED] = self.untracked |
| 489 | if self.read_only is not None: |
| 490 | out['read_only'] = self.read_only |
| 491 | return out |
| 492 | |
| 493 | |
| 494 | class Configs(object): |
| 495 | """Represents a processed .isolate file. |
| 496 | |
| 497 | Stores the file in a processed way, split by configuration. |
| 498 | """ |
| 499 | def __init__(self, file_comment): |
| 500 | self.file_comment = file_comment |
| 501 | # The keys of by_config are tuples of values for the configuration |
| 502 | # variables. The names of the variables (which must be the same for |
| 503 | # every by_config key) are kept in config_variables. Initially by_config |
| 504 | # is empty and we don't know what configuration variables will be used, |
| 505 | # so config_variables also starts out empty. It will be set by the first |
| 506 | # call to union() or merge_dependencies(). |
| 507 | self.by_config = {} |
| 508 | self.config_variables = () |
| 509 | |
| 510 | def union(self, rhs): |
| 511 | """Adds variables from rhs (a Configs) to the existing variables. |
| 512 | """ |
| 513 | 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 | |
| 533 | def merge_dependencies(self, values, config_variables, configs): |
| 534 | """Adds new dependencies to this object for the given configurations. |
| 535 | Arguments: |
| 536 | values: A variables dict as found in a .isolate file, e.g., |
| 537 | {KEY_TOUCHED: [...], 'command': ...}. |
| 538 | config_variables: An ordered list of configuration variables, e.g., |
| 539 | ["OS", "chromeos"]. If this object already contains any dependencies, |
| 540 | the configuration variables must match. |
| 541 | configs: a list of tuples of values of the configuration variables, |
| 542 | e.g., [("mac", 0), ("linux", 1)]. The dependencies in |values| |
| 543 | are added to all of these configurations, and other configurations |
| 544 | are unchanged. |
| 545 | """ |
| 546 | if not values: |
| 547 | return |
| 548 | |
| 549 | if not self.config_variables: |
| 550 | self.config_variables = config_variables |
| 551 | else: |
| 552 | # See comment in Configs.union(). |
| 553 | assert self.config_variables == config_variables |
| 554 | |
| 555 | for config in configs: |
| 556 | self.by_config[config] = union( |
| 557 | self.by_config.get(config), ConfigSettings(config, values)) |
| 558 | |
| 559 | def flatten(self): |
| 560 | """Returns a flat dictionary representation of the configuration. |
| 561 | """ |
| 562 | return dict((k, v.flatten()) for k, v in self.by_config.iteritems()) |
| 563 | |
| 564 | def make_isolate_file(self): |
| 565 | """Returns a dictionary suitable for writing to a .isolate file. |
| 566 | """ |
| 567 | dependencies_by_config = self.flatten() |
| 568 | configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config)) |
| 569 | return convert_map_to_isolate_dict(configs_by_dependency, |
| 570 | self.config_variables) |
| 571 | |
| 572 | |
| 573 | # TODO(benrg): Remove this function when no old-format files are left. |
| 574 | def convert_old_to_new_format(value): |
| 575 | """Converts from the old .isolate format, which only has one variable (OS), |
| 576 | always includes 'linux', 'mac' and 'win' in the set of valid values for OS, |
| 577 | and allows conditions that depend on the set of all OSes, to the new format, |
| 578 | which allows any set of variables, has no hardcoded values, and only allows |
| 579 | explicit positive tests of variable values. |
| 580 | """ |
| 581 | conditions = value.get('conditions', []) |
| 582 | if 'variables' not in value and all(len(cond) == 2 for cond in conditions): |
| 583 | return value # Nothing to change |
| 584 | |
| 585 | def parse_condition(cond): |
| 586 | m = re.match(r'OS=="(\w+)"\Z', cond[0]) |
| 587 | if not m: |
| 588 | raise isolateserver.ConfigError('Invalid condition: %s' % cond[0]) |
| 589 | return m.group(1) |
| 590 | |
| 591 | oses = set(map(parse_condition, conditions)) |
| 592 | default_oses = set(['linux', 'mac', 'win']) |
| 593 | oses = sorted(oses | default_oses) |
| 594 | |
| 595 | def if_not_os(not_os, then): |
| 596 | expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os) |
| 597 | return [expr, then] |
| 598 | |
| 599 | conditions = [ |
| 600 | cond[:2] for cond in conditions if cond[1] |
| 601 | ] + [ |
| 602 | if_not_os(parse_condition(cond), cond[2]) |
| 603 | for cond in conditions if len(cond) == 3 |
| 604 | ] |
| 605 | |
| 606 | if 'variables' in value: |
| 607 | conditions.append(if_not_os(None, {'variables': value.pop('variables')})) |
| 608 | conditions.sort() |
| 609 | |
| 610 | value = value.copy() |
| 611 | value['conditions'] = conditions |
| 612 | return value |
| 613 | |
| 614 | |
| 615 | def load_isolate_as_config(isolate_dir, value, file_comment): |
| 616 | """Parses one .isolate file and returns a Configs() instance. |
| 617 | |
| 618 | Arguments: |
| 619 | isolate_dir: only used to load relative includes so it doesn't depend on |
| 620 | cwd. |
| 621 | value: is the loaded dictionary that was defined in the gyp file. |
| 622 | file_comment: comments found at the top of the file so it can be preserved. |
| 623 | |
| 624 | The expected format is strict, anything diverting from the format below will |
| 625 | throw an assert: |
| 626 | { |
| 627 | 'includes': [ |
| 628 | 'foo.isolate', |
| 629 | ], |
| 630 | 'conditions': [ |
| 631 | ['OS=="vms" and foo=42', { |
| 632 | 'variables': { |
| 633 | 'command': [ |
| 634 | ... |
| 635 | ], |
| 636 | 'isolate_dependency_tracked': [ |
| 637 | ... |
| 638 | ], |
| 639 | 'isolate_dependency_untracked': [ |
| 640 | ... |
| 641 | ], |
Marc-Antoine Ruel | 7124e39 | 2014-01-09 11:49:21 -0500 | [diff] [blame^] | 642 | 'read_only': 0, |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 643 | }, |
| 644 | }], |
| 645 | ... |
| 646 | ], |
| 647 | } |
| 648 | """ |
| 649 | value = convert_old_to_new_format(value) |
| 650 | |
| 651 | variables_and_values = {} |
| 652 | verify_root(value, variables_and_values) |
| 653 | if variables_and_values: |
| 654 | config_variables, config_values = zip( |
| 655 | *sorted(variables_and_values.iteritems())) |
| 656 | all_configs = list(itertools.product(*config_values)) |
| 657 | else: |
| 658 | config_variables = None |
| 659 | all_configs = [] |
| 660 | |
| 661 | isolate = Configs(file_comment) |
| 662 | |
| 663 | # Add configuration-specific variables. |
| 664 | for expr, then in value.get('conditions', []): |
| 665 | configs = match_configs(expr, config_variables, all_configs) |
| 666 | isolate.merge_dependencies(then['variables'], config_variables, configs) |
| 667 | |
| 668 | # Load the includes. Process them in reverse so the last one take precedence. |
| 669 | for include in reversed(value.get('includes', [])): |
| 670 | if os.path.isabs(include): |
| 671 | raise isolateserver.ConfigError( |
| 672 | 'Failed to load configuration; absolute include path \'%s\'' % |
| 673 | include) |
| 674 | included_isolate = os.path.normpath(os.path.join(isolate_dir, include)) |
| 675 | with open(included_isolate, 'r') as f: |
| 676 | included_isolate = load_isolate_as_config( |
| 677 | os.path.dirname(included_isolate), |
| 678 | eval_content(f.read()), |
| 679 | None) |
| 680 | isolate = union(isolate, included_isolate) |
| 681 | |
| 682 | return isolate |
| 683 | |
| 684 | |
| 685 | def load_isolate_for_config(isolate_dir, content, config_variables): |
| 686 | """Loads the .isolate file and returns the information unprocessed but |
| 687 | filtered for the specific OS. |
| 688 | |
| 689 | Returns the command, dependencies and read_only flag. The dependencies are |
| 690 | fixed to use os.path.sep. |
| 691 | """ |
| 692 | # Load the .isolate file, process its conditions, retrieve the command and |
| 693 | # dependencies. |
| 694 | isolate = load_isolate_as_config(isolate_dir, eval_content(content), None) |
| 695 | try: |
| 696 | config_name = tuple( |
| 697 | config_variables[var] for var in isolate.config_variables) |
| 698 | except KeyError: |
| 699 | raise isolateserver.ConfigError( |
| 700 | 'These configuration variables were missing from the command line: %s' % |
| 701 | ', '.join( |
| 702 | sorted(set(isolate.config_variables) - set(config_variables)))) |
| 703 | config = isolate.by_config.get(config_name) |
| 704 | if not config: |
| 705 | raise isolateserver.ConfigError( |
| 706 | 'Failed to load configuration for variable \'%s\' for config(s) \'%s\'' |
| 707 | '\nAvailable configs: %s' % |
| 708 | (', '.join(isolate.config_variables), |
| 709 | ', '.join(config_name), |
| 710 | ', '.join(str(s) for s in isolate.by_config))) |
| 711 | # Merge tracked and untracked variables, isolate.py doesn't care about the |
| 712 | # trackability of the variables, only the build tool does. |
| 713 | dependencies = [ |
| 714 | f.replace('/', os.path.sep) for f in config.tracked + config.untracked |
| 715 | ] |
| 716 | touched = [f.replace('/', os.path.sep) for f in config.touched] |
| 717 | return config.command, dependencies, touched, config.read_only |