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 |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 16 | import itertools |
| 17 | import logging |
| 18 | import os |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 19 | import posixpath |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 20 | import re |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 21 | import sys |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 22 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 23 | from utils import short_expression_finder |
| 24 | |
| 25 | # Files that should be 0-length when mapped. |
| 26 | KEY_TOUCHED = 'isolate_dependency_touched' |
| 27 | # Files that should be tracked by the build tool. |
| 28 | KEY_TRACKED = 'isolate_dependency_tracked' |
| 29 | # Files that should not be tracked by the build tool. |
| 30 | KEY_UNTRACKED = 'isolate_dependency_untracked' |
| 31 | |
| 32 | # Valid variable name. |
| 33 | VALID_VARIABLE = '[A-Za-z_][A-Za-z_0-9]*' |
| 34 | |
| 35 | |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 36 | class IsolateError(ValueError): |
| 37 | """Generic failure to load a .isolate file.""" |
| 38 | pass |
| 39 | |
| 40 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 41 | def determine_root_dir(relative_root, infiles): |
| 42 | """For a list of infiles, determines the deepest root directory that is |
| 43 | referenced indirectly. |
| 44 | |
| 45 | All arguments must be using os.path.sep. |
| 46 | """ |
| 47 | # The trick used to determine the root directory is to look at "how far" back |
| 48 | # up it is looking up. |
| 49 | deepest_root = relative_root |
| 50 | for i in infiles: |
| 51 | x = relative_root |
| 52 | while i.startswith('..' + os.path.sep): |
| 53 | i = i[3:] |
| 54 | assert not i.startswith(os.path.sep) |
| 55 | x = os.path.dirname(x) |
| 56 | if deepest_root.startswith(x): |
| 57 | deepest_root = x |
Marc-Antoine Ruel | 4eeada9 | 2014-04-03 13:54:26 -0400 | [diff] [blame] | 58 | logging.info( |
| 59 | 'determine_root_dir(%s, %d files) -> %s', |
| 60 | relative_root, len(infiles), deepest_root) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 61 | return deepest_root |
| 62 | |
| 63 | |
| 64 | def replace_variable(part, variables): |
| 65 | m = re.match(r'<\((' + VALID_VARIABLE + ')\)', part) |
| 66 | if m: |
| 67 | if m.group(1) not in variables: |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 68 | raise IsolateError( |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 69 | 'Variable "%s" was not found in %s.\nDid you forget to specify ' |
| 70 | '--path-variable?' % (m.group(1), variables)) |
John Abd-El-Malek | 37bcce2 | 2014-09-29 11:11:31 -0700 | [diff] [blame^] | 71 | return str(variables[m.group(1)]) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 72 | return part |
| 73 | |
| 74 | |
| 75 | def eval_variables(item, variables): |
| 76 | """Replaces the .isolate variables in a string item. |
| 77 | |
| 78 | Note that the .isolate format is a subset of the .gyp dialect. |
| 79 | """ |
| 80 | return ''.join( |
| 81 | replace_variable(p, variables) |
| 82 | for p in re.split(r'(<\(' + VALID_VARIABLE + '\))', item)) |
| 83 | |
| 84 | |
| 85 | def split_touched(files): |
| 86 | """Splits files that are touched vs files that are read.""" |
| 87 | tracked = [] |
| 88 | touched = [] |
| 89 | for f in files: |
| 90 | if f.size: |
| 91 | tracked.append(f) |
| 92 | else: |
| 93 | touched.append(f) |
| 94 | return tracked, touched |
| 95 | |
| 96 | |
| 97 | def pretty_print(variables, stdout): |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 98 | """Outputs a .isolate file from the decoded variables. |
| 99 | |
| 100 | The .isolate format is GYP compatible. |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 101 | |
| 102 | Similar to pprint.print() but with NIH syndrome. |
| 103 | """ |
| 104 | # Order the dictionary keys by these keys in priority. |
| 105 | ORDER = ( |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 106 | 'variables', 'condition', 'command', 'read_only', |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 107 | KEY_TRACKED, KEY_UNTRACKED) |
| 108 | |
| 109 | def sorting_key(x): |
| 110 | """Gives priority to 'most important' keys before the others.""" |
| 111 | if x in ORDER: |
| 112 | return str(ORDER.index(x)) |
| 113 | return x |
| 114 | |
| 115 | def loop_list(indent, items): |
| 116 | for item in items: |
| 117 | if isinstance(item, basestring): |
| 118 | stdout.write('%s\'%s\',\n' % (indent, item)) |
| 119 | elif isinstance(item, dict): |
| 120 | stdout.write('%s{\n' % indent) |
| 121 | loop_dict(indent + ' ', item) |
| 122 | stdout.write('%s},\n' % indent) |
| 123 | elif isinstance(item, list): |
| 124 | # A list inside a list will write the first item embedded. |
| 125 | stdout.write('%s[' % indent) |
| 126 | for index, i in enumerate(item): |
| 127 | if isinstance(i, basestring): |
| 128 | stdout.write( |
| 129 | '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\'')) |
| 130 | elif isinstance(i, dict): |
| 131 | stdout.write('{\n') |
| 132 | loop_dict(indent + ' ', i) |
| 133 | if index != len(item) - 1: |
| 134 | x = ', ' |
| 135 | else: |
| 136 | x = '' |
| 137 | stdout.write('%s}%s' % (indent, x)) |
| 138 | else: |
| 139 | assert False |
| 140 | stdout.write('],\n') |
| 141 | else: |
| 142 | assert False |
| 143 | |
| 144 | def loop_dict(indent, items): |
| 145 | for key in sorted(items, key=sorting_key): |
| 146 | item = items[key] |
| 147 | stdout.write("%s'%s': " % (indent, key)) |
| 148 | if isinstance(item, dict): |
| 149 | stdout.write('{\n') |
| 150 | loop_dict(indent + ' ', item) |
| 151 | stdout.write(indent + '},\n') |
| 152 | elif isinstance(item, list): |
| 153 | stdout.write('[\n') |
| 154 | loop_list(indent + ' ', item) |
| 155 | stdout.write(indent + '],\n') |
| 156 | elif isinstance(item, basestring): |
| 157 | stdout.write( |
| 158 | '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\'')) |
Marc-Antoine Ruel | 7124e39 | 2014-01-09 11:49:21 -0500 | [diff] [blame] | 159 | elif isinstance(item, (int, bool)) or item is None: |
Marc-Antoine Ruel | fdc9a55 | 2014-03-28 13:52:11 -0400 | [diff] [blame] | 160 | stdout.write('%s,\n' % item) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 161 | else: |
| 162 | assert False, item |
| 163 | |
| 164 | stdout.write('{\n') |
| 165 | loop_dict(' ', variables) |
| 166 | stdout.write('}\n') |
| 167 | |
| 168 | |
| 169 | def print_all(comment, data, stream): |
| 170 | """Prints a complete .isolate file and its top-level file comment into a |
| 171 | stream. |
| 172 | """ |
| 173 | if comment: |
| 174 | stream.write(comment) |
| 175 | pretty_print(data, stream) |
| 176 | |
| 177 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 178 | def extract_comment(content): |
| 179 | """Extracts file level comment.""" |
| 180 | out = [] |
| 181 | for line in content.splitlines(True): |
| 182 | if line.startswith('#'): |
| 183 | out.append(line) |
| 184 | else: |
| 185 | break |
| 186 | return ''.join(out) |
| 187 | |
| 188 | |
| 189 | def eval_content(content): |
| 190 | """Evaluates a python file and return the value defined in it. |
| 191 | |
| 192 | Used in practice for .isolate files. |
| 193 | """ |
| 194 | globs = {'__builtins__': None} |
| 195 | locs = {} |
| 196 | try: |
| 197 | value = eval(content, globs, locs) |
| 198 | except TypeError as e: |
| 199 | e.args = list(e.args) + [content] |
| 200 | raise |
| 201 | assert locs == {}, locs |
| 202 | assert globs == {'__builtins__': None}, globs |
| 203 | return value |
| 204 | |
| 205 | |
| 206 | def match_configs(expr, config_variables, all_configs): |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 207 | """Returns the list of values from |values| that match the condition |expr|. |
| 208 | |
| 209 | Arguments: |
| 210 | expr: string that is evaluatable with eval(). It is a GYP condition. |
| 211 | config_variables: list of the name of the variables. |
| 212 | all_configs: list of the list of possible values. |
| 213 | |
| 214 | If a variable is not referenced at all, it is marked as unbounded (free) with |
| 215 | a value set to None. |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 216 | """ |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 217 | # It is more than just eval'ing the variable, it needs to be double checked to |
| 218 | # see if the variable is referenced at all. If not, the variable is free |
| 219 | # (unbounded). |
| 220 | # TODO(maruel): Use the intelligent way by inspecting expr instead of doing |
| 221 | # trial and error to figure out which variable is bound. |
| 222 | combinations = [] |
| 223 | for bound_variables in itertools.product( |
| 224 | (True, False), repeat=len(config_variables)): |
| 225 | # Add the combination of variables bound. |
| 226 | combinations.append( |
| 227 | ( |
| 228 | [c for c, b in zip(config_variables, bound_variables) if b], |
| 229 | set( |
| 230 | tuple(v if b else None for v, b in zip(line, bound_variables)) |
| 231 | for line in all_configs) |
| 232 | )) |
| 233 | |
| 234 | out = [] |
| 235 | for variables, configs in combinations: |
| 236 | # Strip variables and see if expr can still be evaluated. |
| 237 | for values in configs: |
| 238 | globs = {'__builtins__': None} |
| 239 | globs.update(zip(variables, (v for v in values if v is not None))) |
| 240 | try: |
| 241 | assertion = eval(expr, globs, {}) |
| 242 | except NameError: |
| 243 | continue |
| 244 | if not isinstance(assertion, bool): |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 245 | raise IsolateError('Invalid condition') |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 246 | if assertion: |
| 247 | out.append(values) |
| 248 | return out |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 249 | |
| 250 | |
| 251 | def verify_variables(variables): |
| 252 | """Verifies the |variables| dictionary is in the expected format.""" |
| 253 | VALID_VARIABLES = [ |
| 254 | KEY_TOUCHED, |
| 255 | KEY_TRACKED, |
| 256 | KEY_UNTRACKED, |
| 257 | 'command', |
| 258 | 'read_only', |
| 259 | ] |
| 260 | assert isinstance(variables, dict), variables |
| 261 | assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys() |
| 262 | for name, value in variables.iteritems(): |
| 263 | if name == 'read_only': |
Marc-Antoine Ruel | 7124e39 | 2014-01-09 11:49:21 -0500 | [diff] [blame] | 264 | assert value in (0, 1, 2, None), value |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 265 | else: |
| 266 | assert isinstance(value, list), value |
| 267 | assert all(isinstance(i, basestring) for i in value), value |
| 268 | |
| 269 | |
| 270 | def verify_ast(expr, variables_and_values): |
| 271 | """Verifies that |expr| is of the form |
| 272 | expr ::= expr ( "or" | "and" ) expr |
| 273 | | identifier "==" ( string | int ) |
| 274 | Also collects the variable identifiers and string/int values in the dict |
| 275 | |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}. |
| 276 | """ |
| 277 | assert isinstance(expr, (ast.BoolOp, ast.Compare)) |
| 278 | if isinstance(expr, ast.BoolOp): |
| 279 | assert isinstance(expr.op, (ast.And, ast.Or)) |
| 280 | for subexpr in expr.values: |
| 281 | verify_ast(subexpr, variables_and_values) |
| 282 | else: |
| 283 | assert isinstance(expr.left.ctx, ast.Load) |
| 284 | assert len(expr.ops) == 1 |
| 285 | assert isinstance(expr.ops[0], ast.Eq) |
| 286 | var_values = variables_and_values.setdefault(expr.left.id, set()) |
| 287 | rhs = expr.comparators[0] |
| 288 | assert isinstance(rhs, (ast.Str, ast.Num)) |
| 289 | var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s) |
| 290 | |
| 291 | |
| 292 | def verify_condition(condition, variables_and_values): |
| 293 | """Verifies the |condition| dictionary is in the expected format. |
| 294 | See verify_ast() for the meaning of |variables_and_values|. |
| 295 | """ |
| 296 | VALID_INSIDE_CONDITION = ['variables'] |
| 297 | assert isinstance(condition, list), condition |
| 298 | assert len(condition) == 2, condition |
| 299 | expr, then = condition |
| 300 | |
| 301 | test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST) |
| 302 | verify_ast(test_ast.body, variables_and_values) |
| 303 | |
| 304 | assert isinstance(then, dict), then |
| 305 | assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys() |
| 306 | if not 'variables' in then: |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 307 | raise IsolateError('Missing \'variables\' in condition %s' % condition) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 308 | verify_variables(then['variables']) |
| 309 | |
| 310 | |
| 311 | def verify_root(value, variables_and_values): |
| 312 | """Verifies that |value| is the parsed form of a valid .isolate file. |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 313 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 314 | See verify_ast() for the meaning of |variables_and_values|. |
| 315 | """ |
Marc-Antoine Ruel | d7c032b | 2014-03-13 15:32:16 -0400 | [diff] [blame] | 316 | VALID_ROOTS = ['includes', 'conditions', 'variables'] |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 317 | assert isinstance(value, dict), value |
| 318 | assert set(VALID_ROOTS).issuperset(set(value)), value.keys() |
| 319 | |
| 320 | includes = value.get('includes', []) |
| 321 | assert isinstance(includes, list), includes |
| 322 | for include in includes: |
| 323 | assert isinstance(include, basestring), include |
| 324 | |
| 325 | conditions = value.get('conditions', []) |
| 326 | assert isinstance(conditions, list), conditions |
| 327 | for condition in conditions: |
| 328 | verify_condition(condition, variables_and_values) |
| 329 | |
Marc-Antoine Ruel | d7c032b | 2014-03-13 15:32:16 -0400 | [diff] [blame] | 330 | variables = value.get('variables', {}) |
| 331 | verify_variables(variables) |
| 332 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 333 | |
| 334 | def remove_weak_dependencies(values, key, item, item_configs): |
| 335 | """Removes any configs from this key if the item is already under a |
| 336 | strong key. |
| 337 | """ |
| 338 | if key == KEY_TOUCHED: |
| 339 | item_configs = set(item_configs) |
| 340 | for stronger_key in (KEY_TRACKED, KEY_UNTRACKED): |
| 341 | try: |
| 342 | item_configs -= values[stronger_key][item] |
| 343 | except KeyError: |
| 344 | pass |
| 345 | |
| 346 | return item_configs |
| 347 | |
| 348 | |
| 349 | def remove_repeated_dependencies(folders, key, item, item_configs): |
| 350 | """Removes any configs from this key if the item is in a folder that is |
| 351 | already included.""" |
| 352 | |
| 353 | if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED): |
| 354 | item_configs = set(item_configs) |
| 355 | for (folder, configs) in folders.iteritems(): |
| 356 | if folder != item and item.startswith(folder): |
| 357 | item_configs -= configs |
| 358 | |
| 359 | return item_configs |
| 360 | |
| 361 | |
| 362 | def get_folders(values_dict): |
| 363 | """Returns a dict of all the folders in the given value_dict.""" |
| 364 | return dict( |
| 365 | (item, configs) for (item, configs) in values_dict.iteritems() |
| 366 | if item.endswith('/') |
| 367 | ) |
| 368 | |
| 369 | |
| 370 | def invert_map(variables): |
| 371 | """Converts {config: {deptype: list(depvals)}} to |
| 372 | {deptype: {depval: set(configs)}}. |
| 373 | """ |
| 374 | KEYS = ( |
| 375 | KEY_TOUCHED, |
| 376 | KEY_TRACKED, |
| 377 | KEY_UNTRACKED, |
| 378 | 'command', |
| 379 | 'read_only', |
| 380 | ) |
| 381 | out = dict((key, {}) for key in KEYS) |
| 382 | for config, values in variables.iteritems(): |
| 383 | for key in KEYS: |
| 384 | if key == 'command': |
| 385 | items = [tuple(values[key])] if key in values else [] |
| 386 | elif key == 'read_only': |
| 387 | items = [values[key]] if key in values else [] |
| 388 | else: |
| 389 | assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED) |
| 390 | items = values.get(key, []) |
| 391 | for item in items: |
| 392 | out[key].setdefault(item, set()).add(config) |
| 393 | return out |
| 394 | |
| 395 | |
| 396 | def reduce_inputs(values): |
| 397 | """Reduces the output of invert_map() to the strictest minimum list. |
| 398 | |
| 399 | Looks at each individual file and directory, maps where they are used and |
| 400 | reconstructs the inverse dictionary. |
| 401 | |
| 402 | Returns the minimized dictionary. |
| 403 | """ |
| 404 | KEYS = ( |
| 405 | KEY_TOUCHED, |
| 406 | KEY_TRACKED, |
| 407 | KEY_UNTRACKED, |
| 408 | 'command', |
| 409 | 'read_only', |
| 410 | ) |
| 411 | |
| 412 | # Folders can only live in KEY_UNTRACKED. |
| 413 | folders = get_folders(values.get(KEY_UNTRACKED, {})) |
| 414 | |
| 415 | out = dict((key, {}) for key in KEYS) |
| 416 | for key in KEYS: |
| 417 | for item, item_configs in values.get(key, {}).iteritems(): |
| 418 | item_configs = remove_weak_dependencies(values, key, item, item_configs) |
| 419 | item_configs = remove_repeated_dependencies( |
| 420 | folders, key, item, item_configs) |
| 421 | if item_configs: |
| 422 | out[key][item] = item_configs |
| 423 | return out |
| 424 | |
| 425 | |
| 426 | def convert_map_to_isolate_dict(values, config_variables): |
| 427 | """Regenerates back a .isolate configuration dict from files and dirs |
| 428 | mappings generated from reduce_inputs(). |
| 429 | """ |
| 430 | # Gather a list of configurations for set inversion later. |
| 431 | all_mentioned_configs = set() |
| 432 | for configs_by_item in values.itervalues(): |
| 433 | for configs in configs_by_item.itervalues(): |
| 434 | all_mentioned_configs.update(configs) |
| 435 | |
| 436 | # Invert the mapping to make it dict first. |
| 437 | conditions = {} |
| 438 | for key in values: |
| 439 | for item, configs in values[key].iteritems(): |
| 440 | then = conditions.setdefault(frozenset(configs), {}) |
| 441 | variables = then.setdefault('variables', {}) |
| 442 | |
Marc-Antoine Ruel | bb20b6d | 2014-01-10 18:47:47 -0500 | [diff] [blame] | 443 | if key == 'read_only': |
| 444 | if not isinstance(item, int): |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 445 | raise IsolateError( |
Marc-Antoine Ruel | bb20b6d | 2014-01-10 18:47:47 -0500 | [diff] [blame] | 446 | 'Unexpected entry type %r for key %s' % (item, key)) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 447 | variables[key] = item |
Marc-Antoine Ruel | bb20b6d | 2014-01-10 18:47:47 -0500 | [diff] [blame] | 448 | elif key == 'command': |
| 449 | if not isinstance(item, tuple): |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 450 | raise IsolateError( |
Marc-Antoine Ruel | bb20b6d | 2014-01-10 18:47:47 -0500 | [diff] [blame] | 451 | 'Unexpected entry type %r for key %s' % (item, key)) |
| 452 | if key in variables: |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 453 | raise IsolateError('Unexpected duplicate key %s' % key) |
Marc-Antoine Ruel | bb20b6d | 2014-01-10 18:47:47 -0500 | [diff] [blame] | 454 | if not item: |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 455 | raise IsolateError('Expected non empty entry in %s' % key) |
Marc-Antoine Ruel | bb20b6d | 2014-01-10 18:47:47 -0500 | [diff] [blame] | 456 | variables[key] = list(item) |
| 457 | elif key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED): |
| 458 | if not isinstance(item, basestring): |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 459 | raise IsolateError('Unexpected entry type %r' % item) |
Marc-Antoine Ruel | bb20b6d | 2014-01-10 18:47:47 -0500 | [diff] [blame] | 460 | if not item: |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 461 | raise IsolateError('Expected non empty entry in %s' % key) |
Marc-Antoine Ruel | bb20b6d | 2014-01-10 18:47:47 -0500 | [diff] [blame] | 462 | # The list of items (files or dirs). Append the new item and keep |
| 463 | # the list sorted. |
| 464 | l = variables.setdefault(key, []) |
| 465 | l.append(item) |
| 466 | l.sort() |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 467 | else: |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 468 | raise IsolateError('Unexpected key %s' % key) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 469 | |
| 470 | if all_mentioned_configs: |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 471 | # Change [(1, 2), (3, 4)] to [set(1, 3), set(2, 4)] |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 472 | config_values = map(set, zip(*all_mentioned_configs)) |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 473 | for i in config_values: |
| 474 | i.discard(None) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 475 | sef = short_expression_finder.ShortExpressionFinder( |
| 476 | zip(config_variables, config_values)) |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 477 | conditions = sorted([sef.get_expr(c), v] for c, v in conditions.iteritems()) |
| 478 | else: |
| 479 | conditions = [] |
Marc-Antoine Ruel | d27c663 | 2014-03-13 15:29:36 -0400 | [diff] [blame] | 480 | out = {'conditions': conditions} |
| 481 | for c in conditions: |
| 482 | if c[0] == '': |
| 483 | # Extract the global. |
| 484 | out.update(c[1]) |
| 485 | conditions.remove(c) |
| 486 | break |
| 487 | return out |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 488 | |
| 489 | |
| 490 | class ConfigSettings(object): |
| 491 | """Represents the dependency variables for a single build configuration. |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 492 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 493 | The structure is immutable. |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 494 | |
| 495 | .touch, .tracked and .untracked are the list of dependencies. The items in |
| 496 | these lists use '/' as a path separator. |
| 497 | .command and .isolate_dir describe how to run the command. .isolate_dir uses |
| 498 | the OS' native path separator. It must be an absolute path, it's the path |
| 499 | where to start the command from. |
| 500 | .read_only describe how to map the files. |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 501 | """ |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 502 | def __init__(self, values, isolate_dir): |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 503 | verify_variables(values) |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 504 | if isolate_dir is None: |
| 505 | # It must be an empty object if isolate_dir is None. |
| 506 | assert values == {}, values |
| 507 | else: |
| 508 | # Otherwise, the path must be absolute. |
| 509 | assert os.path.isabs(isolate_dir), isolate_dir |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 510 | self.touched = sorted(values.get(KEY_TOUCHED, [])) |
| 511 | self.tracked = sorted(values.get(KEY_TRACKED, [])) |
| 512 | self.untracked = sorted(values.get(KEY_UNTRACKED, [])) |
| 513 | self.command = values.get('command', [])[:] |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 514 | self.isolate_dir = isolate_dir |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 515 | self.read_only = values.get('read_only') |
| 516 | |
| 517 | def union(self, rhs): |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 518 | """Merges two config settings together into a new instance. |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 519 | |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 520 | A new instance is not created and self or rhs is returned if the other |
| 521 | object is the empty object. |
| 522 | |
| 523 | self has priority over rhs for .command. Use the same .isolate_dir as the |
| 524 | one having a .command. |
| 525 | |
| 526 | Dependencies listed in rhs are patch adjusted ONLY if they don't start with |
| 527 | a path variable, e.g. the characters '<('. |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 528 | """ |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 529 | # When an object has .isolate_dir == None, it means it is the empty object. |
| 530 | if rhs.isolate_dir is None: |
| 531 | return self |
| 532 | if self.isolate_dir is None: |
| 533 | return rhs |
| 534 | |
| 535 | if sys.platform == 'win32': |
| 536 | assert self.isolate_dir[0].lower() == rhs.isolate_dir[0].lower() |
| 537 | |
| 538 | # Takes the difference between the two isolate_dir. Note that while |
| 539 | # isolate_dir is in native path case, all other references are in posix. |
| 540 | l_rel_cwd, r_rel_cwd = self.isolate_dir, rhs.isolate_dir |
Marc-Antoine Ruel | 4eeada9 | 2014-04-03 13:54:26 -0400 | [diff] [blame] | 541 | if self.command or rhs.command: |
| 542 | use_rhs = bool(not self.command and rhs.command) |
| 543 | else: |
| 544 | # If self doesn't define any file, use rhs. |
| 545 | use_rhs = not bool(self.touched or self.tracked or self.untracked) |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 546 | if use_rhs: |
| 547 | # Rebase files in rhs. |
| 548 | l_rel_cwd, r_rel_cwd = r_rel_cwd, l_rel_cwd |
| 549 | |
| 550 | rebase_path = os.path.relpath(r_rel_cwd, l_rel_cwd).replace( |
| 551 | os.path.sep, '/') |
| 552 | def rebase_item(f): |
| 553 | if f.startswith('<(') or rebase_path == '.': |
| 554 | return f |
| 555 | return posixpath.join(rebase_path, f) |
| 556 | |
| 557 | def map_both(l, r): |
| 558 | """Rebase items in either lhs or rhs, as needed.""" |
| 559 | if use_rhs: |
| 560 | l, r = r, l |
| 561 | return sorted(l + map(rebase_item, r)) |
| 562 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 563 | var = { |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 564 | KEY_TOUCHED: map_both(self.touched, rhs.touched), |
| 565 | KEY_TRACKED: map_both(self.tracked, rhs.tracked), |
| 566 | KEY_UNTRACKED: map_both(self.untracked, rhs.untracked), |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 567 | 'command': self.command or rhs.command, |
| 568 | 'read_only': rhs.read_only if self.read_only is None else self.read_only, |
| 569 | } |
Marc-Antoine Ruel | 4eeada9 | 2014-04-03 13:54:26 -0400 | [diff] [blame] | 570 | return ConfigSettings(var, l_rel_cwd) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 571 | |
| 572 | def flatten(self): |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 573 | """Converts the object into a dict.""" |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 574 | out = {} |
| 575 | if self.command: |
| 576 | out['command'] = self.command |
| 577 | if self.touched: |
| 578 | out[KEY_TOUCHED] = self.touched |
| 579 | if self.tracked: |
| 580 | out[KEY_TRACKED] = self.tracked |
| 581 | if self.untracked: |
| 582 | out[KEY_UNTRACKED] = self.untracked |
| 583 | if self.read_only is not None: |
| 584 | out['read_only'] = self.read_only |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 585 | # TODO(maruel): Probably better to not output it if command is None? |
| 586 | if self.isolate_dir is not None: |
| 587 | out['isolate_dir'] = self.isolate_dir |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 588 | return out |
| 589 | |
Marc-Antoine Ruel | 4eeada9 | 2014-04-03 13:54:26 -0400 | [diff] [blame] | 590 | def __str__(self): |
| 591 | """Returns a short representation useful for debugging.""" |
| 592 | files = ''.join( |
| 593 | '\n ' + f for f in (self.touched + self.tracked + self.untracked)) |
| 594 | return 'ConfigSettings(%s, %s, %s, %s)' % ( |
| 595 | self.command, |
| 596 | self.isolate_dir, |
| 597 | self.read_only, |
| 598 | files or '[]') |
| 599 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 600 | |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 601 | def _safe_index(l, k): |
| 602 | try: |
| 603 | return l.index(k) |
| 604 | except ValueError: |
| 605 | return None |
| 606 | |
| 607 | |
| 608 | def _get_map_keys(dest_keys, in_keys): |
| 609 | """Returns a tuple of the indexes of each item in in_keys found in dest_keys. |
| 610 | |
| 611 | For example, if in_keys is ('A', 'C') and dest_keys is ('A', 'B', 'C'), the |
| 612 | return value will be (0, None, 1). |
| 613 | """ |
| 614 | return tuple(_safe_index(in_keys, k) for k in dest_keys) |
| 615 | |
| 616 | |
| 617 | def _map_keys(mapping, items): |
| 618 | """Returns a tuple with items placed at mapping index. |
| 619 | |
| 620 | For example, if mapping is (1, None, 0) and items is ('a', 'b'), it will |
| 621 | return ('b', None, 'c'). |
| 622 | """ |
| 623 | return tuple(items[i] if i != None else None for i in mapping) |
| 624 | |
| 625 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 626 | class Configs(object): |
| 627 | """Represents a processed .isolate file. |
| 628 | |
| 629 | Stores the file in a processed way, split by configuration. |
Marc-Antoine Ruel | 9ac1b91 | 2014-01-10 09:08:42 -0500 | [diff] [blame] | 630 | |
| 631 | At this point, we don't know all the possibilities. So mount a partial view |
| 632 | that we have. |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 633 | |
| 634 | This class doesn't hold isolate_dir, since it is dependent on the final |
| 635 | configuration selected. It is implicitly dependent on which .isolate defines |
| 636 | the 'command' that will take effect. |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 637 | """ |
Marc-Antoine Ruel | 67d3c0a | 2014-01-10 09:12:39 -0500 | [diff] [blame] | 638 | def __init__(self, file_comment, config_variables): |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 639 | self.file_comment = file_comment |
Marc-Antoine Ruel | 9ac1b91 | 2014-01-10 09:08:42 -0500 | [diff] [blame] | 640 | # Contains the names of the config variables seen while processing |
| 641 | # .isolate file(s). The order is important since the same order is used for |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 642 | # keys in self._by_config. |
Marc-Antoine Ruel | 67d3c0a | 2014-01-10 09:12:39 -0500 | [diff] [blame] | 643 | assert isinstance(config_variables, tuple) |
Marc-Antoine Ruel | 8170f49 | 2014-03-13 15:26:56 -0400 | [diff] [blame] | 644 | assert all(isinstance(c, basestring) for c in config_variables), ( |
| 645 | config_variables) |
Marc-Antoine Ruel | f0d0787 | 2014-03-27 16:59:03 -0400 | [diff] [blame] | 646 | config_variables = tuple(config_variables) |
| 647 | assert tuple(sorted(config_variables)) == config_variables, config_variables |
Marc-Antoine Ruel | 67d3c0a | 2014-01-10 09:12:39 -0500 | [diff] [blame] | 648 | self._config_variables = config_variables |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 649 | # The keys of _by_config are tuples of values for each of the items in |
Marc-Antoine Ruel | 67d3c0a | 2014-01-10 09:12:39 -0500 | [diff] [blame] | 650 | # self._config_variables. A None item in the list of the key means the value |
Marc-Antoine Ruel | 9ac1b91 | 2014-01-10 09:08:42 -0500 | [diff] [blame] | 651 | # is unbounded. |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 652 | self._by_config = {} |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 653 | |
Marc-Antoine Ruel | 67d3c0a | 2014-01-10 09:12:39 -0500 | [diff] [blame] | 654 | @property |
| 655 | def config_variables(self): |
| 656 | return self._config_variables |
| 657 | |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 658 | def get_config(self, config): |
| 659 | """Returns all configs that matches this config as a single ConfigSettings. |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 660 | |
| 661 | Returns an empty ConfigSettings if none apply. |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 662 | """ |
Marc-Antoine Ruel | f0d0787 | 2014-03-27 16:59:03 -0400 | [diff] [blame] | 663 | # TODO(maruel): Fix ordering based on the bounded values. The keys are not |
| 664 | # necessarily sorted in the way that makes sense, they are alphabetically |
| 665 | # sorted. It is important because the left-most takes predescence. |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 666 | out = ConfigSettings({}, None) |
Marc-Antoine Ruel | f0d0787 | 2014-03-27 16:59:03 -0400 | [diff] [blame] | 667 | for k, v in sorted(self._by_config.iteritems()): |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 668 | if all(i == j or j is None for i, j in zip(config, k)): |
Marc-Antoine Ruel | 8170f49 | 2014-03-13 15:26:56 -0400 | [diff] [blame] | 669 | out = out.union(v) |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 670 | return out |
| 671 | |
Marc-Antoine Ruel | 8170f49 | 2014-03-13 15:26:56 -0400 | [diff] [blame] | 672 | def set_config(self, key, value): |
| 673 | """Sets the ConfigSettings for this key. |
| 674 | |
| 675 | The key is a tuple of bounded or unbounded variables. The global variable |
| 676 | is the key where all values are unbounded, e.g.: |
| 677 | (None,) * len(self._config_variables) |
| 678 | """ |
| 679 | assert key not in self._by_config, (key, self._by_config.keys()) |
| 680 | assert isinstance(key, tuple) |
| 681 | assert len(key) == len(self._config_variables), ( |
| 682 | key, self._config_variables) |
| 683 | assert isinstance(value, ConfigSettings) |
| 684 | self._by_config[key] = value |
| 685 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 686 | def union(self, rhs): |
Marc-Antoine Ruel | 8170f49 | 2014-03-13 15:26:56 -0400 | [diff] [blame] | 687 | """Returns a new Configs instance, the union of variables from self and rhs. |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 688 | |
Marc-Antoine Ruel | 8170f49 | 2014-03-13 15:26:56 -0400 | [diff] [blame] | 689 | Uses self.file_comment if available, otherwise rhs.file_comment. |
Marc-Antoine Ruel | f0d0787 | 2014-03-27 16:59:03 -0400 | [diff] [blame] | 690 | It keeps config_variables sorted in the output. |
Marc-Antoine Ruel | 8170f49 | 2014-03-13 15:26:56 -0400 | [diff] [blame] | 691 | """ |
| 692 | # Merge the keys of config_variables for each Configs instances. All the new |
| 693 | # variables will become unbounded. This requires realigning the keys. |
| 694 | config_variables = tuple(sorted( |
| 695 | set(self.config_variables) | set(rhs.config_variables))) |
| 696 | out = Configs(self.file_comment or rhs.file_comment, config_variables) |
| 697 | mapping_lhs = _get_map_keys(out.config_variables, self.config_variables) |
| 698 | mapping_rhs = _get_map_keys(out.config_variables, rhs.config_variables) |
| 699 | lhs_config = dict( |
| 700 | (_map_keys(mapping_lhs, k), v) for k, v in self._by_config.iteritems()) |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 701 | # pylint: disable=W0212 |
Marc-Antoine Ruel | 8170f49 | 2014-03-13 15:26:56 -0400 | [diff] [blame] | 702 | rhs_config = dict( |
| 703 | (_map_keys(mapping_rhs, k), v) for k, v in rhs._by_config.iteritems()) |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 704 | |
| 705 | for key in set(lhs_config) | set(rhs_config): |
Marc-Antoine Ruel | bd1b284 | 2014-03-28 13:56:43 -0400 | [diff] [blame] | 706 | l = lhs_config.get(key) |
| 707 | r = rhs_config.get(key) |
| 708 | out.set_config(key, l.union(r) if (l and r) else (l or r)) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 709 | return out |
| 710 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 711 | def flatten(self): |
| 712 | """Returns a flat dictionary representation of the configuration. |
| 713 | """ |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 714 | return dict((k, v.flatten()) for k, v in self._by_config.iteritems()) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 715 | |
| 716 | def make_isolate_file(self): |
| 717 | """Returns a dictionary suitable for writing to a .isolate file. |
| 718 | """ |
| 719 | dependencies_by_config = self.flatten() |
| 720 | configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config)) |
| 721 | return convert_map_to_isolate_dict(configs_by_dependency, |
| 722 | self.config_variables) |
| 723 | |
Marc-Antoine Ruel | 4eeada9 | 2014-04-03 13:54:26 -0400 | [diff] [blame] | 724 | def __str__(self): |
| 725 | return 'Configs(%s,%s)' % ( |
| 726 | self._config_variables, |
| 727 | ''.join('\n %s' % str(f) for f in self._by_config)) |
| 728 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 729 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 730 | def load_isolate_as_config(isolate_dir, value, file_comment): |
| 731 | """Parses one .isolate file and returns a Configs() instance. |
| 732 | |
| 733 | Arguments: |
| 734 | isolate_dir: only used to load relative includes so it doesn't depend on |
| 735 | cwd. |
| 736 | value: is the loaded dictionary that was defined in the gyp file. |
| 737 | file_comment: comments found at the top of the file so it can be preserved. |
| 738 | |
| 739 | The expected format is strict, anything diverting from the format below will |
| 740 | throw an assert: |
| 741 | { |
| 742 | 'includes': [ |
| 743 | 'foo.isolate', |
| 744 | ], |
| 745 | 'conditions': [ |
| 746 | ['OS=="vms" and foo=42', { |
| 747 | 'variables': { |
| 748 | 'command': [ |
| 749 | ... |
| 750 | ], |
| 751 | 'isolate_dependency_tracked': [ |
| 752 | ... |
| 753 | ], |
| 754 | 'isolate_dependency_untracked': [ |
| 755 | ... |
| 756 | ], |
Marc-Antoine Ruel | 7124e39 | 2014-01-09 11:49:21 -0500 | [diff] [blame] | 757 | 'read_only': 0, |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 758 | }, |
| 759 | }], |
| 760 | ... |
| 761 | ], |
Marc-Antoine Ruel | d7c032b | 2014-03-13 15:32:16 -0400 | [diff] [blame] | 762 | 'variables': { |
| 763 | ... |
| 764 | }, |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 765 | } |
| 766 | """ |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 767 | assert os.path.isabs(isolate_dir), isolate_dir |
Marc-Antoine Ruel | d7c032b | 2014-03-13 15:32:16 -0400 | [diff] [blame] | 768 | if any(len(cond) == 3 for cond in value.get('conditions', [])): |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 769 | raise IsolateError('Using \'else\' is not supported anymore.') |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 770 | variables_and_values = {} |
| 771 | verify_root(value, variables_and_values) |
| 772 | if variables_and_values: |
| 773 | config_variables, config_values = zip( |
| 774 | *sorted(variables_and_values.iteritems())) |
| 775 | all_configs = list(itertools.product(*config_values)) |
| 776 | else: |
Marc-Antoine Ruel | 9ac1b91 | 2014-01-10 09:08:42 -0500 | [diff] [blame] | 777 | config_variables = () |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 778 | all_configs = [] |
| 779 | |
Marc-Antoine Ruel | 67d3c0a | 2014-01-10 09:12:39 -0500 | [diff] [blame] | 780 | isolate = Configs(file_comment, config_variables) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 781 | |
Marc-Antoine Ruel | d7c032b | 2014-03-13 15:32:16 -0400 | [diff] [blame] | 782 | # Add global variables. The global variables are on the empty tuple key. |
| 783 | isolate.set_config( |
| 784 | (None,) * len(config_variables), |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 785 | ConfigSettings(value.get('variables', {}), isolate_dir)) |
Marc-Antoine Ruel | d7c032b | 2014-03-13 15:32:16 -0400 | [diff] [blame] | 786 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 787 | # Add configuration-specific variables. |
| 788 | for expr, then in value.get('conditions', []): |
| 789 | configs = match_configs(expr, config_variables, all_configs) |
Marc-Antoine Ruel | 67d3c0a | 2014-01-10 09:12:39 -0500 | [diff] [blame] | 790 | new = Configs(None, config_variables) |
Marc-Antoine Ruel | 9ac1b91 | 2014-01-10 09:08:42 -0500 | [diff] [blame] | 791 | for config in configs: |
Marc-Antoine Ruel | b53d0c1 | 2014-03-28 13:46:27 -0400 | [diff] [blame] | 792 | new.set_config(config, ConfigSettings(then['variables'], isolate_dir)) |
Marc-Antoine Ruel | 9ac1b91 | 2014-01-10 09:08:42 -0500 | [diff] [blame] | 793 | isolate = isolate.union(new) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 794 | |
| 795 | # Load the includes. Process them in reverse so the last one take precedence. |
| 796 | for include in reversed(value.get('includes', [])): |
| 797 | if os.path.isabs(include): |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 798 | raise IsolateError( |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 799 | 'Failed to load configuration; absolute include path \'%s\'' % |
| 800 | include) |
| 801 | included_isolate = os.path.normpath(os.path.join(isolate_dir, include)) |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 802 | if sys.platform == 'win32': |
| 803 | if included_isolate[0].lower() != isolate_dir[0].lower(): |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 804 | raise IsolateError( |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 805 | 'Can\'t reference a .isolate file from another drive') |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 806 | with open(included_isolate, 'r') as f: |
| 807 | included_isolate = load_isolate_as_config( |
| 808 | os.path.dirname(included_isolate), |
| 809 | eval_content(f.read()), |
| 810 | None) |
Marc-Antoine Ruel | bd1b284 | 2014-03-28 13:56:43 -0400 | [diff] [blame] | 811 | isolate = isolate.union(included_isolate) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 812 | |
| 813 | return isolate |
| 814 | |
| 815 | |
| 816 | def load_isolate_for_config(isolate_dir, content, config_variables): |
| 817 | """Loads the .isolate file and returns the information unprocessed but |
| 818 | filtered for the specific OS. |
| 819 | |
Marc-Antoine Ruel | fdc9a55 | 2014-03-28 13:52:11 -0400 | [diff] [blame] | 820 | Returns: |
| 821 | tuple of command, dependencies, touched, read_only flag, isolate_dir. |
| 822 | The dependencies are fixed to use os.path.sep. |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 823 | """ |
| 824 | # Load the .isolate file, process its conditions, retrieve the command and |
| 825 | # dependencies. |
| 826 | isolate = load_isolate_as_config(isolate_dir, eval_content(content), None) |
| 827 | try: |
| 828 | config_name = tuple( |
| 829 | config_variables[var] for var in isolate.config_variables) |
| 830 | except KeyError: |
Marc-Antoine Ruel | e819be4 | 2014-08-28 19:38:20 -0400 | [diff] [blame] | 831 | raise IsolateError( |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 832 | 'These configuration variables were missing from the command line: %s' % |
| 833 | ', '.join( |
| 834 | sorted(set(isolate.config_variables) - set(config_variables)))) |
Marc-Antoine Ruel | 3ae9e6e | 2014-01-13 15:42:16 -0500 | [diff] [blame] | 835 | |
| 836 | # A configuration is to be created with all the combinations of free |
| 837 | # variables. |
| 838 | config = isolate.get_config(config_name) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 839 | # Merge tracked and untracked variables, isolate.py doesn't care about the |
| 840 | # trackability of the variables, only the build tool does. |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 841 | dependencies = sorted( |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 842 | f.replace('/', os.path.sep) for f in config.tracked + config.untracked |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 843 | ) |
| 844 | touched = sorted(f.replace('/', os.path.sep) for f in config.touched) |
Marc-Antoine Ruel | fdc9a55 | 2014-03-28 13:52:11 -0400 | [diff] [blame] | 845 | return ( |
| 846 | config.command, dependencies, touched, config.read_only, |
| 847 | config.isolate_dir) |