"""
import ast
-import copy
import itertools
import logging
import os
+import posixpath
import re
+import sys
import isolateserver
x = os.path.dirname(x)
if deepest_root.startswith(x):
deepest_root = x
- logging.debug(
- 'determine_root_dir(%s, %d files) -> %s' % (
- relative_root, len(infiles), deepest_root))
+ logging.info(
+ 'determine_root_dir(%s, %d files) -> %s',
+ relative_root, len(infiles), deepest_root)
return deepest_root
def pretty_print(variables, stdout):
- """Outputs a gyp compatible list from the decoded variables.
+ """Outputs a .isolate file from the decoded variables.
+
+ The .isolate format is GYP compatible.
Similar to pprint.print() but with NIH syndrome.
"""
# Order the dictionary keys by these keys in priority.
ORDER = (
- 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
+ 'variables', 'condition', 'command', 'read_only',
KEY_TRACKED, KEY_UNTRACKED)
def sorting_key(x):
stdout.write(
'\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
elif isinstance(item, (int, bool)) or item is None:
- stdout.write('%s\n' % item)
+ stdout.write('%s,\n' % item)
else:
assert False, item
pretty_print(data, stream)
-def union(lhs, rhs):
- """Merges two compatible datastructures composed of dict/list/set."""
- assert lhs is not None or rhs is not None
- if lhs is None:
- return copy.deepcopy(rhs)
- if rhs is None:
- return copy.deepcopy(lhs)
- assert type(lhs) == type(rhs), (lhs, rhs)
- if hasattr(lhs, 'union'):
- # Includes set, ConfigSettings 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, list):
- # Do not go inside the list.
- return lhs + rhs
- assert False, type(lhs)
-
-
def extract_comment(content):
"""Extracts file level comment."""
out = []
def verify_root(value, variables_and_values):
"""Verifies that |value| is the parsed form of a valid .isolate file.
+
See verify_ast() for the meaning of |variables_and_values|.
"""
VALID_ROOTS = ['includes', 'conditions', 'variables']
class ConfigSettings(object):
"""Represents the dependency variables for a single build configuration.
+
The structure is immutable.
+
+ .touch, .tracked and .untracked are the list of dependencies. The items in
+ these lists use '/' as a path separator.
+ .command and .isolate_dir describe how to run the command. .isolate_dir uses
+ the OS' native path separator. It must be an absolute path, it's the path
+ where to start the command from.
+ .read_only describe how to map the files.
"""
- def __init__(self, values):
+ def __init__(self, values, isolate_dir):
verify_variables(values)
+ if isolate_dir is None:
+ # It must be an empty object if isolate_dir is None.
+ assert values == {}, values
+ else:
+ # Otherwise, the path must be absolute.
+ assert os.path.isabs(isolate_dir), isolate_dir
self.touched = sorted(values.get(KEY_TOUCHED, []))
self.tracked = sorted(values.get(KEY_TRACKED, []))
self.untracked = sorted(values.get(KEY_UNTRACKED, []))
self.command = values.get('command', [])[:]
+ self.isolate_dir = isolate_dir
self.read_only = values.get('read_only')
def union(self, rhs):
- """Merges two config settings together.
+ """Merges two config settings together into a new instance.
+
+ A new instance is not created and self or rhs is returned if the other
+ object is the empty object.
+
+ self has priority over rhs for .command. Use the same .isolate_dir as the
+ one having a .command.
- self has priority over rhs for 'command' variable.
+ Dependencies listed in rhs are patch adjusted ONLY if they don't start with
+ a path variable, e.g. the characters '<('.
"""
+ # When an object has .isolate_dir == None, it means it is the empty object.
+ if rhs.isolate_dir is None:
+ return self
+ if self.isolate_dir is None:
+ return rhs
+
+ if sys.platform == 'win32':
+ assert self.isolate_dir[0].lower() == rhs.isolate_dir[0].lower()
+
+ # Takes the difference between the two isolate_dir. Note that while
+ # isolate_dir is in native path case, all other references are in posix.
+ l_rel_cwd, r_rel_cwd = self.isolate_dir, rhs.isolate_dir
+ if self.command or rhs.command:
+ use_rhs = bool(not self.command and rhs.command)
+ else:
+ # If self doesn't define any file, use rhs.
+ use_rhs = not bool(self.touched or self.tracked or self.untracked)
+ if use_rhs:
+ # Rebase files in rhs.
+ l_rel_cwd, r_rel_cwd = r_rel_cwd, l_rel_cwd
+
+ rebase_path = os.path.relpath(r_rel_cwd, l_rel_cwd).replace(
+ os.path.sep, '/')
+ def rebase_item(f):
+ if f.startswith('<(') or rebase_path == '.':
+ return f
+ return posixpath.join(rebase_path, f)
+
+ def map_both(l, r):
+ """Rebase items in either lhs or rhs, as needed."""
+ if use_rhs:
+ l, r = r, l
+ return sorted(l + map(rebase_item, r))
+
var = {
- KEY_TOUCHED: sorted(self.touched + rhs.touched),
- KEY_TRACKED: sorted(self.tracked + rhs.tracked),
- KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
+ KEY_TOUCHED: map_both(self.touched, rhs.touched),
+ KEY_TRACKED: map_both(self.tracked, rhs.tracked),
+ KEY_UNTRACKED: map_both(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 ConfigSettings(var)
+ return ConfigSettings(var, l_rel_cwd)
def flatten(self):
+ """Converts the object into a dict."""
out = {}
if self.command:
out['command'] = self.command
out[KEY_UNTRACKED] = self.untracked
if self.read_only is not None:
out['read_only'] = self.read_only
+ # TODO(maruel): Probably better to not output it if command is None?
+ if self.isolate_dir is not None:
+ out['isolate_dir'] = self.isolate_dir
return out
+ def __str__(self):
+ """Returns a short representation useful for debugging."""
+ files = ''.join(
+ '\n ' + f for f in (self.touched + self.tracked + self.untracked))
+ return 'ConfigSettings(%s, %s, %s, %s)' % (
+ self.command,
+ self.isolate_dir,
+ self.read_only,
+ files or '[]')
+
def _safe_index(l, k):
try:
At this point, we don't know all the possibilities. So mount a partial view
that we have.
+
+ This class doesn't hold isolate_dir, since it is dependent on the final
+ configuration selected. It is implicitly dependent on which .isolate defines
+ the 'command' that will take effect.
"""
def __init__(self, file_comment, config_variables):
self.file_comment = file_comment
assert isinstance(config_variables, tuple)
assert all(isinstance(c, basestring) for c in config_variables), (
config_variables)
+ config_variables = tuple(config_variables)
+ assert tuple(sorted(config_variables)) == config_variables, config_variables
self._config_variables = config_variables
# The keys of _by_config are tuples of values for each of the items in
# self._config_variables. A None item in the list of the key means the value
def get_config(self, config):
"""Returns all configs that matches this config as a single ConfigSettings.
+
+ Returns an empty ConfigSettings if none apply.
"""
- out = ConfigSettings({})
- for k, v in self._by_config.iteritems():
+ # TODO(maruel): Fix ordering based on the bounded values. The keys are not
+ # necessarily sorted in the way that makes sense, they are alphabetically
+ # sorted. It is important because the left-most takes predescence.
+ out = ConfigSettings({}, None)
+ for k, v in sorted(self._by_config.iteritems()):
if all(i == j or j is None for i, j in zip(config, k)):
out = out.union(v)
return out
"""Returns a new Configs instance, the union of variables from self and rhs.
Uses self.file_comment if available, otherwise rhs.file_comment.
+ It keeps config_variables sorted in the output.
"""
# Merge the keys of config_variables for each Configs instances. All the new
# variables will become unbounded. This requires realigning the keys.
(_map_keys(mapping_rhs, k), v) for k, v in rhs._by_config.iteritems())
for key in set(lhs_config) | set(rhs_config):
- out.set_config(key, union(lhs_config.get(key), rhs_config.get(key)))
+ l = lhs_config.get(key)
+ r = rhs_config.get(key)
+ out.set_config(key, l.union(r) if (l and r) else (l or r))
return out
def flatten(self):
return convert_map_to_isolate_dict(configs_by_dependency,
self.config_variables)
+ def __str__(self):
+ return 'Configs(%s,%s)' % (
+ self._config_variables,
+ ''.join('\n %s' % str(f) for f in self._by_config))
+
def load_isolate_as_config(isolate_dir, value, file_comment):
"""Parses one .isolate file and returns a Configs() instance.
},
}
"""
+ assert os.path.isabs(isolate_dir), isolate_dir
if any(len(cond) == 3 for cond in value.get('conditions', [])):
raise isolateserver.ConfigError('Using \'else\' is not supported anymore.')
variables_and_values = {}
# Add global variables. The global variables are on the empty tuple key.
isolate.set_config(
(None,) * len(config_variables),
- ConfigSettings(value.get('variables', {})))
+ ConfigSettings(value.get('variables', {}), isolate_dir))
# Add configuration-specific variables.
for expr, then in value.get('conditions', []):
configs = match_configs(expr, config_variables, all_configs)
new = Configs(None, config_variables)
for config in configs:
- new.set_config(config, ConfigSettings(then['variables']))
+ new.set_config(config, ConfigSettings(then['variables'], isolate_dir))
isolate = isolate.union(new)
# Load the includes. Process them in reverse so the last one take precedence.
'Failed to load configuration; absolute include path \'%s\'' %
include)
included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
+ if sys.platform == 'win32':
+ if included_isolate[0].lower() != isolate_dir[0].lower():
+ raise isolateserver.ConfigError(
+ 'Can\'t reference a .isolate file from another drive')
with open(included_isolate, 'r') as f:
included_isolate = load_isolate_as_config(
os.path.dirname(included_isolate),
eval_content(f.read()),
None)
- isolate = union(isolate, included_isolate)
+ isolate = isolate.union(included_isolate)
return isolate
"""Loads the .isolate file and returns the information unprocessed but
filtered for the specific OS.
- Returns the command, dependencies and read_only flag. The dependencies are
- fixed to use os.path.sep.
+ Returns:
+ tuple of command, dependencies, touched, read_only flag, isolate_dir.
+ The dependencies are fixed to use os.path.sep.
"""
# Load the .isolate file, process its conditions, retrieve the command and
# dependencies.
config = isolate.get_config(config_name)
# Merge tracked and untracked variables, isolate.py doesn't care about the
# trackability of the variables, only the build tool does.
- dependencies = [
+ dependencies = sorted(
f.replace('/', os.path.sep) for f in config.tracked + config.untracked
- ]
- touched = [f.replace('/', os.path.sep) for f in config.touched]
- return config.command, dependencies, touched, config.read_only
+ )
+ touched = sorted(f.replace('/', os.path.sep) for f in config.touched)
+ return (
+ config.command, dependencies, touched, config.read_only,
+ config.isolate_dir)