summaryrefslogtreecommitdiffstats
path: root/tools/isolate/merge_isolate.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/isolate/merge_isolate.py')
-rwxr-xr-xtools/isolate/merge_isolate.py458
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