diff options
Diffstat (limited to 'tools/isolate/merge_isolate.py')
-rwxr-xr-x | tools/isolate/merge_isolate.py | 458 |
1 files changed, 270 insertions, 188 deletions
diff --git a/tools/isolate/merge_isolate.py b/tools/isolate/merge_isolate.py index fd4a0d3..6170c7f 100755 --- a/tools/isolate/merge_isolate.py +++ b/tools/isolate/merge_isolate.py @@ -18,6 +18,8 @@ import re import sys import trace_inputs +# Create shortcuts. +from trace_inputs import KEY_TRACKED, KEY_UNTRACKED def union(lhs, rhs): @@ -28,43 +30,17 @@ def union(lhs, rhs): if rhs is None: return copy.deepcopy(lhs) assert type(lhs) == type(rhs), (lhs, rhs) + if hasattr(lhs, 'union'): + # Includes set, OSSettings and Configs. + return lhs.union(rhs) if isinstance(lhs, dict): return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs)) - elif isinstance(lhs, set): - # Do not go inside the set. - return lhs.union(rhs) elif isinstance(lhs, list): # Do not go inside the list. return lhs + rhs assert False, type(lhs) -def process_variables(for_os, variables): - """Extracts files and dirs from the |variables| dict. - - Returns a list of exactly two items. Each item is a dict that maps a string - to a set (of strings). - - In the first item, the keys are file names, and the values are sets of OS - names, like "win" or "mac". In the second item, the keys are directory names, - and the values are sets of OS names too. - """ - VALID_VARIABLES = ['isolate_files', 'isolate_dirs'] - - # Verify strictness. - assert isinstance(variables, dict), variables - assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys() - for items in variables.itervalues(): - assert isinstance(items, list), items - assert all(isinstance(i, basestring) for i in items), items - - # Returns [files, dirs] - return [ - dict((name, set([for_os])) for name in variables.get(var, [])) - for var in VALID_VARIABLES - ] - - def eval_content(content): """Evaluates a GYP file and return the value defined in it.""" globs = {'__builtins__': None} @@ -75,50 +51,262 @@ def eval_content(content): return value -def _process_inner(for_os, inner, old_files, old_dirs, old_os): - """Processes the variables inside a condition. +def verify_variables(variables): + """Verifies the |variables| dictionary is in the expected format.""" + VALID_VARIABLES = [ + KEY_TRACKED, + KEY_UNTRACKED, + 'command', + 'read_only', + ] + assert isinstance(variables, dict), variables + assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys() + for name, value in variables.iteritems(): + if name == 'read_only': + assert value in (True, False, None), value + else: + assert isinstance(value, list), value + assert all(isinstance(i, basestring) for i in value), value + + +def verify_condition(condition): + """Verifies the |condition| dictionary is in the expected format.""" + VALID_INSIDE_CONDITION = ['variables'] + assert isinstance(condition, list), condition + assert 2 <= len(condition) <= 3, condition + assert re.match(r'OS==\"([a-z]+)\"', condition[0]), condition[0] + for c in condition[1:]: + assert isinstance(c, dict), c + assert set(VALID_INSIDE_CONDITION).issuperset(set(c)), c.keys() + verify_variables(c.get('variables', {})) - Only meant to be called by parse_gyp_dict(). - Args: - - for_os: OS where the references are tracked for. - - inner: Inner dictionary to process. - - old_files: Previous list of files to union with. - - old_dirs: Previous list of directories to union with. - - old_os: Previous list of OSes referenced to union with. +def verify_root(value): + VALID_ROOTS = ['variables', 'conditions'] + assert isinstance(value, dict), value + assert set(VALID_ROOTS).issuperset(set(value)), value.keys() + verify_variables(value.get('variables', {})) - Returns: - - A tuple of (files, dirs, os) where each list is a union of the new - dependencies found for this OS, as referenced by for_os, and the previous - list. + conditions = value.get('conditions', []) + assert isinstance(conditions, list), conditions + for condition in conditions: + verify_condition(condition) + + +class OSSettings(object): + """Represents the dependencies for an OS. The structure is immutable.""" + def __init__(self, name, values): + self.name = name + verify_variables(values) + self.tracked = sorted(values.get(KEY_TRACKED, [])) + self.untracked = sorted(values.get(KEY_UNTRACKED, [])) + self.command = values.get('command', [])[:] + self.read_only = values.get('read_only') + + def union(self, rhs): + assert self.name == rhs.name + assert not (self.command and rhs.command) + var = { + KEY_TRACKED: sorted(self.tracked + rhs.tracked), + KEY_UNTRACKED: sorted(self.untracked + rhs.untracked), + 'command': self.command or rhs.command, + 'read_only': rhs.read_only if self.read_only is None else self.read_only, + } + return OSSettings(self.name, var) + + def flatten(self): + out = {} + if self.command: + out['command'] = self.command + if self.tracked: + out[KEY_TRACKED] = self.tracked + if self.untracked: + out[KEY_UNTRACKED] = self.untracked + if self.read_only is not None: + out['read_only'] = self.read_only + return out + + +class Configs(object): + """Represents all the OS-specific configurations. + + The self.per_os[None] member contains all the 'else' clauses plus the default + values. It is not included in the flatten() result. """ - assert isinstance(inner, dict), inner - assert set(['variables']).issuperset(set(inner)), inner.keys() - new_files, new_dirs = process_variables(for_os, inner.get('variables', {})) - if new_files or new_dirs: - old_os = old_os.union([for_os.lstrip('!')]) - return union(old_files, new_files), union(old_dirs, new_dirs), old_os + def __init__(self, oses): + self.per_os = { + None: OSSettings(None, {}), + } + self.per_os.update(dict((name, OSSettings(name, {})) for name in oses)) + + def union(self, rhs): + out = Configs(list(set(self.per_os.keys() + rhs.per_os.keys()))) + for value in self.per_os.itervalues(): + # TODO(maruel): FAIL + out = out.union(value) + for value in rhs.per_os.itervalues(): + out = out.union(value) + return out + + def add_globals(self, values): + for key in self.per_os: + self.per_os[key] = self.per_os[key].union(OSSettings(key, values)) + + def add_values(self, for_os, values): + self.per_os[for_os] = self.per_os[for_os].union(OSSettings(for_os, values)) + + def add_negative_values(self, for_os, values): + """Includes the variables to all OSes except |for_os|. + + This includes 'None' so unknown OSes gets it too. + """ + for key in self.per_os: + if key != for_os: + self.per_os[key] = self.per_os[key].union(OSSettings(key, values)) + def flatten(self): + """Returns a flat dictionary representation of the configuration. -def parse_gyp_dict(value): - """Parses a gyp dict as returned by eval_content(). + Skips None pseudo-OS. + """ + return dict( + (k, v.flatten()) for k, v in self.per_os.iteritems() if k is not None) - |value| is the loaded dictionary that was defined in the gyp file. - Returns a 3-tuple, where the first two items are the same as the items - returned by process_variable() in the same order, and the last item is a set - of strings of all OSs seen in the input dict. +def invert_map(variables): + """Converts a dict(OS, dict(deptype, list(dependencies)) to a flattened view. + + Returns a tuple of: + 1. dict(deptype, dict(dependency, set(OSes)) for easier processing. + 2. All the OSes found as a set. + """ + KEYS = ( + KEY_TRACKED, + KEY_UNTRACKED, + 'command', + 'read_only', + ) + out = dict((key, {}) for key in KEYS) + for os_name, values in variables.iteritems(): + for key in (KEY_TRACKED, KEY_UNTRACKED): + for item in values.get(key, []): + out[key].setdefault(item, set()).add(os_name) + + # command needs special handling. + command = tuple(values.get('command', [])) + out['command'].setdefault(command, set()).add(os_name) + + # read_only needs special handling. + out['read_only'].setdefault(values.get('read_only'), set()).add(os_name) + return out, set(variables) + + +def reduce_inputs(values, oses): + """Reduces the invert_map() output to the strictest minimum list. + + 1. Construct the inverse map first. + 2. Look at each individual file and directory, map where they are used and + reconstruct the inverse dictionary. + 3. Do not convert back to negative if only 2 OSes were merged. + + Returns a tuple of: + 1. the minimized dictionary + 2. oses passed through as-is. + """ + KEYS = ( + KEY_TRACKED, + KEY_UNTRACKED, + 'command', + 'read_only', + ) + out = dict((key, {}) for key in KEYS) + assert all(oses), oses + if len(oses) > 2: + for key in KEYS: + for item, item_oses in values.get(key, {}).iteritems(): + # Converts all oses.difference('foo') to '!foo'. + assert all(item_oses), item_oses + missing = oses.difference(item_oses) + if len(missing) == 1: + # Replace it with a negative. + out[key][item] = set(['!' + tuple(missing)[0]]) + elif not missing: + out[key][item] = set([None]) + else: + out[key][item] = set(item_oses) + return out, oses + + +def convert_map_to_gyp(values, oses): + """Regenerates back a gyp-like configuration dict from files and dirs + mappings generated from reduce_inputs(). + """ + # First, inverse the mapping to make it dict first. + config = {} + for key in values: + for item, oses in values[key].iteritems(): + if item is None: + # For read_only default. + continue + for cond_os in oses: + cond_key = None if cond_os is None else cond_os.lstrip('!') + # Insert the if/else dicts. + condition_values = config.setdefault(cond_key, [{}, {}]) + # If condition is negative, use index 1, else use index 0. + cond_value = condition_values[int((cond_os or '').startswith('!'))] + variables = cond_value.setdefault('variables', {}) + + if item in (True, False): + # One-off for read_only. + variables[key] = item + else: + if isinstance(item, tuple): + # One-off for command. + # Do not merge lists and do not sort! + # Note that item is a tuple. + assert key not in variables + variables[key] = list(item) + else: + # The list of items (files or dirs). Append the new item and keep + # the list sorted. + l = variables.setdefault(key, []) + l.append(item) + l.sort() + + out = {} + for o in sorted(config): + d = config[o] + if o is None: + assert not d[1] + out = union(out, d[0]) + else: + c = out.setdefault('conditions', []) + if d[1]: + c.append(['OS=="%s"' % o] + d) + else: + c.append(['OS=="%s"' % o] + d[0:1]) + return out + + +def load_gyp(value): + """Parses one gyp skeleton and returns a Configs() instance. + + |value| is the loaded dictionary that was defined in the gyp file. The expected format is strict, anything diverting from the format below will - fail: + throw an assert: { 'variables': { - 'isolate_files': [ + 'command': [ + ... + ], + 'isolate_dependency_tracked': [ ... ], - 'isolate_dirs: [ + 'isolate_dependency_untracked': [ ... ], + 'read_only': False, }, 'conditions': [ ['OS=="<os>"', { @@ -134,165 +322,59 @@ def parse_gyp_dict(value): ], } """ - assert isinstance(value, dict), value - VALID_ROOTS = ['variables', 'conditions'] - assert set(VALID_ROOTS).issuperset(set(value)), value.keys() + verify_root(value) + + # Scan to get the list of OSes. + conditions = value.get('conditions', []) + oses = set(re.match(r'OS==\"([a-z]+)\"', c[0]).group(1) for c in conditions) + configs = Configs(oses) # Global level variables. - oses = set() - files, dirs = process_variables(None, value.get('variables', {})) + configs.add_globals(value.get('variables', {})) # OS specific variables. - conditions = value.get('conditions', []) - assert isinstance(conditions, list), conditions for condition in conditions: - assert isinstance(condition, list), condition - assert 2 <= len(condition) <= 3, condition - m = re.match(r'OS==\"([a-z]+)\"', condition[0]) - assert m, condition[0] - condition_os = m.group(1) - - files, dirs, oses = _process_inner( - condition_os, condition[1], files, dirs, oses) - - if len(condition) == 3: - files, dirs, oses = _process_inner( - '!' + condition_os, condition[2], files, dirs, oses) + condition_os = re.match(r'OS==\"([a-z]+)\"', condition[0]).group(1) + configs.add_values(condition_os, condition[1].get('variables', {})) + if len(condition) > 2: + configs.add_negative_values( + condition_os, condition[2].get('variables', {})) + return configs - # TODO(maruel): _expand_negative() should be called here, because otherwise - # the OSes the negative condition represents is lost once the gyps are merged. - # This cause an invalid expansion in reduce_inputs() call. - return files, dirs, oses - -def parse_gyp_dicts(gyps): +def load_gyps(items): """Parses each gyp file and returns the merged results. - It only loads what parse_gyp_dict() can process. + It only loads what load_gyp() can process. Return values: files: dict(filename, set(OS where this filename is a dependency)) dirs: dict(dirame, set(OS where this dirname is a dependency)) oses: set(all the OSes referenced) """ - files = {} - dirs = {} - oses = set() - for gyp in gyps: - with open(gyp, 'rb') as gyp_file: - content = gyp_file.read() - gyp_files, gyp_dirs, gyp_oses = parse_gyp_dict(eval_content(content)) - files = union(gyp_files, files) - dirs = union(gyp_dirs, dirs) - oses |= gyp_oses - return files, dirs, oses - - -def _expand_negative(items, oses): - """Converts all '!foo' value in the set by oses.difference('foo').""" - assert None not in oses and len(oses) >= 2, oses - for name in items: - if None in items[name]: - # Shortcut any item having None in their set. An item listed in None means - # the item is a dependency on all OSes. As such, there is no need to list - # any OS. - items[name] = set([None]) - continue - for neg in [o for o in items[name] if o.startswith('!')]: - # Replace it with the inverse. - items[name] = items[name].union(oses.difference([neg[1:]])) - items[name].remove(neg) - if items[name] == oses: - items[name] = set([None]) - - -def _compact_negative(items, oses): - """Converts all oses.difference('foo') to '!foo'. - - It is doing the reverse of _expand_negative(). - """ - assert None not in oses and len(oses) >= 3, oses - for name in items: - missing = oses.difference(items[name]) - if len(missing) == 1: - # Replace it with a negative. - items[name] = set(['!' + tuple(missing)[0]]) - - -def reduce_inputs(files, dirs, oses): - """Reduces the variables to their strictest minimum.""" - # Construct the inverse map first. - # Look at each individual file and directory, map where they are used and - # reconstruct the inverse dictionary. - # First, expands all '!' builders into the reverse. - # TODO(maruel): This is too late to call _expand_negative(). The exact list - # negative OSes condition it represents is lost at that point. - _expand_negative(files, oses) - _expand_negative(dirs, oses) - - # Do not convert back to negative if only 2 OSes were merged. It is easier to - # read this way. - if len(oses) > 2: - _compact_negative(files, oses) - _compact_negative(dirs, oses) - - return files, dirs - - -def convert_to_gyp(files, dirs): - """Regenerates back a gyp-like configuration dict from files and dirs - mappings. - - Sort the lists. - """ - # First, inverse the mapping to make it dict first. - config = {} - def to_cond(items, name): - for item, oses in items.iteritems(): - for cond_os in oses: - condition_values = config.setdefault( - None if cond_os is None else cond_os.lstrip('!'), - [{}, {}]) - # If condition is negative, use index 1, else use index 0. - condition_value = condition_values[int((cond_os or '').startswith('!'))] - # The list of items (files or dirs). Append the new item and keep the - # list sorted. - l = condition_value.setdefault('variables', {}).setdefault(name, []) - l.append(item) - l.sort() - - to_cond(files, 'isolate_files') - to_cond(dirs, 'isolate_dirs') - - out = {} - for o in sorted(config): - d = config[o] - if o is None: - assert not d[1] - out = union(out, d[0]) - else: - c = out.setdefault('conditions', []) - if d[1]: - c.append(['OS=="%s"' % o] + d) - else: - c.append(['OS=="%s"' % o] + d[0:1]) - return out + configs = Configs([]) + for item in items: + configs = configs.union(load_gyp(eval_content(open(item, 'rb').read()))) + return configs -def main(): +def main(args=None): parser = optparse.OptionParser( usage='%prog <options> [file1] [file2] ...') parser.add_option( '-v', '--verbose', action='count', default=0, help='Use multiple times') - options, args = parser.parse_args() + options, args = parser.parse_args(args) level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)] logging.basicConfig( level=level, format='%(levelname)5s %(module)15s(%(lineno)3d):%(message)s') trace_inputs.pretty_print( - convert_to_gyp(*reduce_inputs(*parse_gyp_dicts(args))), + convert_map_to_gyp( + *reduce_inputs( + *invert_map( + load_gyps(args).flatten()))), sys.stdout) return 0 |