| Index: tools/isolate/isolate.py
|
| diff --git a/tools/isolate/isolate.py b/tools/isolate/isolate.py
|
| index 2c102c394aedd9afc8ca57d98d2887158c6b06ce..57346eb0e9a6210c469b88d82fe2ac818f6d21f0 100755
|
| --- a/tools/isolate/isolate.py
|
| +++ b/tools/isolate/isolate.py
|
| @@ -25,7 +25,6 @@ import json
|
| import logging
|
| import optparse
|
| import os
|
| -import posixpath
|
| import re
|
| import stat
|
| import subprocess
|
| @@ -36,43 +35,26 @@ import merge_isolate
|
| import trace_inputs
|
| import run_test_from_archive
|
|
|
| -# Used by process_inputs().
|
| +# Used by process_input().
|
| NO_INFO, STATS_ONLY, WITH_HASH = range(56, 59)
|
|
|
|
|
| def relpath(path, root):
|
| - """os.path.relpath() that keeps trailing slash."""
|
| + """os.path.relpath() that keeps trailing os.path.sep."""
|
| out = os.path.relpath(path, root)
|
| if path.endswith(os.path.sep):
|
| out += os.path.sep
|
| - elif sys.platform == 'win32' and path.endswith('/'):
|
| - # TODO(maruel): Temporary.
|
| - out += os.path.sep
|
| return out
|
|
|
|
|
| def normpath(path):
|
| - """os.path.normpath() that keeps trailing slash."""
|
| + """os.path.normpath() that keeps trailing os.path.sep."""
|
| out = os.path.normpath(path)
|
| - if path.endswith(('/', os.path.sep)):
|
| + if path.endswith(os.path.sep):
|
| out += os.path.sep
|
| return out
|
|
|
|
|
| -def to_relative(path, root, relative):
|
| - """Converts any absolute path to a relative path, only if under root."""
|
| - if sys.platform == 'win32':
|
| - path = path.lower()
|
| - root = root.lower()
|
| - relative = relative.lower()
|
| - if path.startswith(root):
|
| - logging.info('%s starts with %s' % (path, root))
|
| - path = os.path.relpath(path, relative)
|
| - else:
|
| - logging.info('%s not under %s' % (path, root))
|
| - return path
|
| -
|
| -
|
| def expand_directories(indir, infiles, blacklist):
|
| """Expands the directories, applies the blacklist and verifies files exist."""
|
| logging.debug('expand_directories(%s, %s, %s)' % (indir, infiles, blacklist))
|
| @@ -126,9 +108,17 @@ def eval_variables(item, variables):
|
| replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
|
|
|
|
|
| +def indent(data, indent_length):
|
| + """Indents text."""
|
| + spacing = ' ' * indent_length
|
| + return ''.join(spacing + l for l in str(data).splitlines(True))
|
| +
|
| +
|
| def load_isolate(content, error):
|
| - """Loads the .isolate file. Returns the command, dependencies and read_only
|
| - flag.
|
| + """Loads the .isolate file and returns the information unprocessed.
|
| +
|
| + Returns the command, dependencies and read_only flag. The dependencies are
|
| + fixed to use os.path.sep.
|
| """
|
| # Load the .isolate file, process its conditions, retrieve the command and
|
| # dependencies.
|
| @@ -139,58 +129,55 @@ def load_isolate(content, error):
|
| error('Failed to load configuration for \'%s\'' % flavor)
|
| # Merge tracked and untracked dependencies, isolate.py doesn't care about the
|
| # trackability of the dependencies, only the build tool does.
|
| - return config.command, config.tracked + config.untracked, config.read_only
|
| -
|
| + dependencies = [
|
| + f.replace('/', os.path.sep) for f in config.tracked + config.untracked
|
| + ]
|
| + return config.command, dependencies, config.read_only
|
|
|
| -def process_inputs(prevdict, indir, infiles, level, read_only):
|
| - """Returns a dictionary of input files, populated with the files' mode and
|
| - hash.
|
|
|
| - |prevdict| is the previous dictionary. It is used to retrieve the cached sha-1
|
| - to skip recalculating the hash.
|
| +def process_input(filepath, prevdict, level, read_only):
|
| + """Processes an input file, a dependency, and return meta data about it.
|
|
|
| - |level| determines the amount of information retrieved.
|
| - 1 loads no information. 2 loads minimal stat() information. 3 calculates the
|
| - sha-1 of the file's content.
|
| -
|
| - The file mode is manipulated if read_only is True. In practice, we only save
|
| - one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On windows, mode
|
| - is not set since all files are 'executable' by default.
|
| + Arguments:
|
| + - filepath: File to act on.
|
| + - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
|
| + to skip recalculating the hash.
|
| + - level: determines the amount of information retrieved.
|
| + - read_only: If True, the file mode is manipulated. In practice, only save
|
| + one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
|
| + windows, mode is not set since all files are 'executable' by
|
| + default.
|
| """
|
| assert level in (NO_INFO, STATS_ONLY, WITH_HASH)
|
| - outdict = {}
|
| - for infile in infiles:
|
| - filepath = os.path.join(indir, infile)
|
| - outdict[infile] = {}
|
| - if level >= STATS_ONLY:
|
| - filestats = os.stat(filepath)
|
| - if trace_inputs.get_flavor() != 'win':
|
| - filemode = stat.S_IMODE(filestats.st_mode)
|
| - # Remove write access for non-owner.
|
| - filemode &= ~(stat.S_IWGRP | stat.S_IWOTH)
|
| - if read_only:
|
| - filemode &= ~stat.S_IWUSR
|
| - if filemode & stat.S_IXUSR:
|
| - filemode |= (stat.S_IXGRP | stat.S_IXOTH)
|
| - else:
|
| - filemode &= ~(stat.S_IXGRP | stat.S_IXOTH)
|
| - outdict[infile]['mode'] = filemode
|
| - outdict[infile]['size'] = filestats.st_size
|
| - # Used to skip recalculating the hash. Use the most recent update time.
|
| - outdict[infile]['timestamp'] = int(round(filestats.st_mtime))
|
| - # If the timestamp wasn't updated, carry on the sha-1.
|
| - if (prevdict.get(infile, {}).get('timestamp') ==
|
| - outdict[infile]['timestamp'] and
|
| - 'sha-1' in prevdict[infile]):
|
| - # Reuse the previous hash.
|
| - outdict[infile]['sha-1'] = prevdict[infile]['sha-1']
|
| -
|
| - if level >= WITH_HASH and not outdict[infile].get('sha-1'):
|
| - h = hashlib.sha1()
|
| - with open(filepath, 'rb') as f:
|
| - h.update(f.read())
|
| - outdict[infile]['sha-1'] = h.hexdigest()
|
| - return outdict
|
| + out = {}
|
| + if level >= STATS_ONLY:
|
| + filestats = os.stat(filepath)
|
| + if trace_inputs.get_flavor() != 'win':
|
| + filemode = stat.S_IMODE(filestats.st_mode)
|
| + # Remove write access for non-owner.
|
| + filemode &= ~(stat.S_IWGRP | stat.S_IWOTH)
|
| + if read_only:
|
| + filemode &= ~stat.S_IWUSR
|
| + if filemode & stat.S_IXUSR:
|
| + filemode |= (stat.S_IXGRP | stat.S_IXOTH)
|
| + else:
|
| + filemode &= ~(stat.S_IXGRP | stat.S_IXOTH)
|
| + out['mode'] = filemode
|
| + out['size'] = filestats.st_size
|
| + # Used to skip recalculating the hash. Use the most recent update time.
|
| + out['timestamp'] = int(round(filestats.st_mtime))
|
| + # If the timestamp wasn't updated, carry on the sha-1.
|
| + if (prevdict.get('timestamp') == out['timestamp'] and
|
| + 'sha-1' in prevdict):
|
| + # Reuse the previous hash.
|
| + out['sha-1'] = prevdict['sha-1']
|
| +
|
| + if level >= WITH_HASH and not out.get('sha-1'):
|
| + h = hashlib.sha1()
|
| + with open(filepath, 'rb') as f:
|
| + h.update(f.read())
|
| + out['sha-1'] = h.hexdigest()
|
| + return out
|
|
|
|
|
| def recreate_tree(outdir, indir, infiles, action):
|
| @@ -226,111 +213,304 @@ def recreate_tree(outdir, indir, infiles, action):
|
| run_test_from_archive.link_file(outfile, infile, action)
|
|
|
|
|
| -def load_results(resultfile):
|
| - """Loads the previous results as an optimization."""
|
| - data = {}
|
| - if resultfile and os.path.isfile(resultfile):
|
| - resultfile = os.path.abspath(resultfile)
|
| - with open(resultfile, 'r') as f:
|
| - data = json.load(f)
|
| - logging.debug('Loaded %s' % resultfile)
|
| - else:
|
| - resultfile = os.path.abspath(resultfile)
|
| - logging.debug('%s was not found' % resultfile)
|
| +def result_to_state(filename):
|
| + """Replaces the file's extension."""
|
| + return filename.rsplit('.', 1)[0] + '.state'
|
|
|
| - # Works with native os.path.sep but stores as '/'.
|
| - if 'files' in data and os.path.sep != '/':
|
| - data['files'] = dict(
|
| - (k.replace('/', os.path.sep), v)
|
| - for k, v in data['files'].iteritems())
|
| - return data
|
|
|
| +def write_json(stream, data):
|
| + """Writes data to a stream as json."""
|
| + json.dump(data, stream, indent=2, sort_keys=True)
|
| + stream.write('\n')
|
|
|
| -def save_results(resultfile, data):
|
| - data = data.copy()
|
|
|
| - # Works with native os.path.sep but stores as '/'.
|
| - if os.path.sep != '/':
|
| - data['files'] = dict(
|
| - (k.replace(os.path.sep, '/'), v) for k, v in data['files'].iteritems())
|
| +def determine_root_dir(relative_root, infiles):
|
| + """For a list of infiles, determines the deepest root directory that is
|
| + referenced indirectly.
|
|
|
| - f = None
|
| - try:
|
| - if resultfile:
|
| - f = open(resultfile, 'wb')
|
| - else:
|
| - f = sys.stdout
|
| - json.dump(data, f, indent=2, sort_keys=True)
|
| - f.write('\n')
|
| - finally:
|
| - if resultfile and f:
|
| - f.close()
|
| + All arguments must be using os.path.sep.
|
| + """
|
| + # The trick used to determine the root directory is to look at "how far" back
|
| + # up it is looking up.
|
| + deepest_root = relative_root
|
| + for i in infiles:
|
| + x = relative_root
|
| + while i.startswith('..' + os.path.sep):
|
| + i = i[3:]
|
| + assert not i.startswith(os.path.sep)
|
| + x = os.path.dirname(x)
|
| + if deepest_root.startswith(x):
|
| + deepest_root = x
|
| + logging.debug(
|
| + 'determine_root_dir(%s, %s) -> %s' % (
|
| + relative_root, infiles, deepest_root))
|
| + return deepest_root
|
|
|
| - total_bytes = sum(i.get('size', 0) for i in data['files'].itervalues())
|
| - if total_bytes:
|
| - logging.debug('Total size: %d bytes' % total_bytes)
|
|
|
| +def process_variables(variables, relative_base_dir, error):
|
| + """Processes path variables as a special case and returns a copy of the dict.
|
|
|
| -def isolate(outdir, mode, indir, infiles, data):
|
| - """Main function to isolate a target with its dependencies.
|
| + For each 'path' varaible: first normalizes it, verifies it exists, converts it
|
| + to an absolute path, then sets it as relative to relative_base_dir.
|
| + """
|
| + variables = variables.copy()
|
| + for i in ('DEPTH', 'PRODUCT_DIR'):
|
| + if i not in variables:
|
| + continue
|
| + variable = os.path.normpath(variables[i])
|
| + if not os.path.isdir(variable):
|
| + error('%s=%s is not a directory' % (i, variable))
|
| + # Variables could contain / or \ on windows. Always normalize to
|
| + # os.path.sep.
|
| + variable = os.path.abspath(variable.replace('/', os.path.sep))
|
| + # All variables are relative to the .isolate file.
|
| + variables[i] = os.path.relpath(variable, relative_base_dir)
|
| + return variables
|
| +
|
| +
|
| +class Flattenable(object):
|
| + """Represents data that can be represented as a json file."""
|
| + MEMBERS = ()
|
| +
|
| + def flatten(self):
|
| + """Returns a json-serializable version of itself."""
|
| + return dict((member, getattr(self, member)) for member in self.MEMBERS)
|
| +
|
| + @classmethod
|
| + def load(cls, data):
|
| + """Loads a flattened version."""
|
| + data = data.copy()
|
| + out = cls()
|
| + for member in out.MEMBERS:
|
| + if member in data:
|
| + value = data.pop(member)
|
| + setattr(out, member, value)
|
| + assert not data, data
|
| + return out
|
| +
|
| + @classmethod
|
| + def load_file(cls, filename):
|
| + """Loads the data from a file or return an empty instance."""
|
| + out = cls()
|
| + try:
|
| + with open(filename, 'r') as f:
|
| + out = cls.load(json.load(f))
|
| + logging.debug('Loaded %s(%s)' % (cls.__name__, filename))
|
| + except IOError:
|
| + pass
|
| + return out
|
|
|
| - Arguments:
|
| - - outdir: Output directory where the result is stored. Depends on |mode|.
|
| - - indir: Root directory to be used as the base directory for infiles.
|
| - - infiles: List of files, with relative path, to process.
|
| - - mode: Action to do. See file level docstring.
|
| - - data: Contains all the command specific meta-data.
|
|
|
| - Some arguments are optional, dependending on |mode|. See the corresponding
|
| - MODE<mode> function for the exact behavior.
|
| +class Result(Flattenable):
|
| + """Describes the content of a .result file.
|
| +
|
| + This file is used by run_test_from_archive.py so its content is strictly only
|
| + what is necessary to run the test outside of a checkout.
|
| + """
|
| + MEMBERS = (
|
| + 'command',
|
| + 'files',
|
| + 'read_only',
|
| + 'relative_cwd',
|
| + )
|
| +
|
| + def __init__(self):
|
| + super(Result, self).__init__()
|
| + self.command = []
|
| + self.files = {}
|
| + self.read_only = None
|
| + self.relative_cwd = None
|
| +
|
| + def update(self, command, infiles, read_only, relative_cwd):
|
| + """Updates the result state with new information."""
|
| + self.command = command
|
| + # Add new files.
|
| + for f in infiles:
|
| + self.files.setdefault(f, {})
|
| + # Prune extraneous files that are not a dependency anymore.
|
| + for f in set(infiles).difference(self.files.keys()):
|
| + del self.files[f]
|
| + if read_only is not None:
|
| + self.read_only = read_only
|
| + self.relative_cwd = relative_cwd
|
| +
|
| + def __str__(self):
|
| + out = '%s(\n' % self.__class__.__name__
|
| + out += ' command: %s\n' % self.command
|
| + out += ' files: %s\n' % ', '.join(sorted(self.files))
|
| + out += ' read_only: %s\n' % self.read_only
|
| + out += ' relative_cwd: %s)' % self.relative_cwd
|
| + return out
|
| +
|
| +
|
| +class SavedState(Flattenable):
|
| + """Describes the content of a .state file.
|
| +
|
| + The items in this file are simply to improve the developer's life and aren't
|
| + used by run_test_from_archive.py. This file can always be safely removed.
|
| +
|
| + isolate_file permits to find back root_dir, variables are used for stateful
|
| + rerun.
|
| """
|
| - modes = {
|
| - 'check': MODEcheck,
|
| - 'hashtable': MODEhashtable,
|
| - 'remap': MODEremap,
|
| - 'run': MODErun,
|
| - 'trace': MODEtrace,
|
| - }
|
| - mode_fn = modes[mode]
|
| -
|
| - infiles = expand_directories(
|
| - indir, infiles, lambda x: re.match(r'.*\.(git|svn|pyc)$', x))
|
| -
|
| - # Only hashtable mode really needs the sha-1.
|
| - level = {
|
| - 'check': NO_INFO,
|
| - 'hashtable': WITH_HASH,
|
| - 'remap': STATS_ONLY,
|
| - 'run': STATS_ONLY,
|
| - 'trace': STATS_ONLY,
|
| - }
|
| - # Regenerate data['files'] from infiles.
|
| - data['files'] = process_inputs(
|
| - data.get('files', {}), indir, infiles, level[mode], data.get('read_only'))
|
| -
|
| - result = mode_fn(outdir, indir, data)
|
| - return result, data
|
| -
|
| -
|
| -def MODEcheck(_outdir, _indir, _data):
|
| + MEMBERS = (
|
| + 'isolate_file',
|
| + 'variables',
|
| + )
|
| +
|
| + def __init__(self):
|
| + super(SavedState, self).__init__()
|
| + self.isolate_file = None
|
| + self.variables = {}
|
| +
|
| + def update(self, isolate_file, variables):
|
| + """Updates the saved state with new information."""
|
| + self.isolate_file = isolate_file
|
| + self.variables.update(variables)
|
| +
|
| + def __str__(self):
|
| + out = '%s(\n' % self.__class__.__name__
|
| + out += ' isolate_file: %s\n' % self.isolate_file
|
| + out += ' variables: %s' % ''.join(
|
| + '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
|
| + out += ')'
|
| + return out
|
| +
|
| +
|
| +class CompleteState(object):
|
| + """Contains all the state to run the task at hand."""
|
| + def __init__(self, result_file, result, saved_state, out_dir):
|
| + super(CompleteState, self).__init__()
|
| + self.result_file = result_file
|
| + # Contains the data that will be used by run_test_from_archive.py
|
| + self.result = result
|
| + # Contains the data to ease developer's use-case but that is not strictly
|
| + # necessary.
|
| + self.saved_state = saved_state
|
| + self.out_dir = out_dir
|
| +
|
| + @classmethod
|
| + def load_files(cls, result_file, out_dir):
|
| + """Loads state from disk."""
|
| + assert os.path.isabs(result_file), result_file
|
| + assert result_file.rsplit('.', 1)[1] == 'result', result_file
|
| + return cls(
|
| + result_file,
|
| + Result.load_file(result_file),
|
| + SavedState.load_file(result_to_state(result_file)),
|
| + out_dir)
|
| +
|
| + def load_isolate(self, isolate_file, variables, error):
|
| + """Updates self.result and self.saved_state with information loaded from a
|
| + .isolate file.
|
| +
|
| + Processes the loaded data, deduce root_dir, relative_cwd.
|
| + """
|
| + # Make sure to not depend on os.getcwd().
|
| + assert os.path.isabs(isolate_file), isolate_file
|
| + logging.info(
|
| + 'CompleteState.load_isolate(%s, %s)' % (isolate_file, variables))
|
| + relative_base_dir = os.path.dirname(isolate_file)
|
| +
|
| + # Processes the variables and update the saved state.
|
| + variables = process_variables(variables, relative_base_dir, error)
|
| + self.saved_state.update(isolate_file, variables)
|
| +
|
| + with open(isolate_file, 'r') as f:
|
| + # At that point, variables are not replaced yet in command and infiles.
|
| + # infiles may contain directory entries and is in posix style.
|
| + command, infiles, read_only = load_isolate(f.read(), error)
|
| + command = [eval_variables(i, variables) for i in command]
|
| + infiles = [eval_variables(f, variables) for f in infiles]
|
| + # root_dir is automatically determined by the deepest root accessed with the
|
| + # form '../../foo/bar'.
|
| + root_dir = determine_root_dir(relative_base_dir, infiles)
|
| + # The relative directory is automatically determined by the relative path
|
| + # between root_dir and the directory containing the .isolate file,
|
| + # isolate_base_dir.
|
| + relative_cwd = os.path.relpath(relative_base_dir, root_dir)
|
| + # Normalize the files based to root_dir. It is important to keep the
|
| + # trailing os.path.sep at that step.
|
| + infiles = [
|
| + relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
|
| + for f in infiles
|
| + ]
|
| + # Expand the directories by listing each file inside. Up to now, trailing
|
| + # os.path.sep must be kept.
|
| + infiles = expand_directories(
|
| + root_dir,
|
| + infiles,
|
| + lambda x: re.match(r'.*\.(git|svn|pyc)$', x))
|
| +
|
| + # Finally, update the new stuff in the foo.result file, the file that is
|
| + # used by run_test_from_archive.py.
|
| + self.result.update(command, infiles, read_only, relative_cwd)
|
| + logging.debug(self)
|
| +
|
| + def process_inputs(self, level):
|
| + """Updates self.result.files with the files' mode and hash.
|
| +
|
| + See process_input() for more information.
|
| + """
|
| + for infile in sorted(self.result.files):
|
| + filepath = os.path.join(self.root_dir, infile)
|
| + self.result.files[infile] = process_input(
|
| + filepath, self.result.files[infile], level, self.result.read_only)
|
| +
|
| + def save_files(self):
|
| + """Saves both self.result and self.saved_state."""
|
| + with open(self.result_file, 'wb') as f:
|
| + write_json(f, self.result.flatten())
|
| + total_bytes = sum(i.get('size', 0) for i in self.result.files.itervalues())
|
| + if total_bytes:
|
| + logging.debug('Total size: %d bytes' % total_bytes)
|
| + with open(result_to_state(self.result_file), 'wb') as f:
|
| + write_json(f, self.saved_state.flatten())
|
| +
|
| + @property
|
| + def root_dir(self):
|
| + """isolate_file is always inside relative_cwd relative to root_dir."""
|
| + isolate_dir = os.path.dirname(self.saved_state.isolate_file)
|
| + # Special case '.'.
|
| + if self.result.relative_cwd == '.':
|
| + return isolate_dir
|
| + assert isolate_dir.endswith(self.result.relative_cwd), (
|
| + isolate_dir, self.result.relative_cwd)
|
| + return isolate_dir[:-len(self.result.relative_cwd)]
|
| +
|
| + @property
|
| + def resultdir(self):
|
| + """Directory containing the results, usually equivalent to the variable
|
| + PRODUCT_DIR.
|
| + """
|
| + return os.path.dirname(self.result_file)
|
| +
|
| + def __str__(self):
|
| + out = '%s(\n' % self.__class__.__name__
|
| + out += ' root_dir: %s\n' % self.root_dir
|
| + out += ' result: %s\n' % indent(self.result, 2)
|
| + out += ' saved_state: %s)' % indent(self.saved_state, 2)
|
| + return out
|
| +
|
| +
|
| +def MODEcheck(_outdir, _state):
|
| """No-op."""
|
| return 0
|
|
|
|
|
| -def MODEhashtable(outdir, indir, data):
|
| +def MODEhashtable(outdir, state):
|
| outdir = (
|
| - outdir or os.path.join(os.path.dirname(data['resultdir']), 'hashtable'))
|
| + outdir or os.path.join(os.path.dirname(state.resultdir), 'hashtable'))
|
| if not os.path.isdir(outdir):
|
| os.makedirs(outdir)
|
| - for relfile, properties in data['files'].iteritems():
|
| - infile = os.path.join(indir, relfile)
|
| + for relfile, properties in state.result.files.iteritems():
|
| + infile = os.path.join(state.root_dir, relfile)
|
| outfile = os.path.join(outdir, properties['sha-1'])
|
| if os.path.isfile(outfile):
|
| # Just do a quick check that the file size matches. No need to stat()
|
| # again the input file, grab the value from the dict.
|
| out_size = os.stat(outfile).st_size
|
| in_size = (
|
| - data.get('files', {}).get(infile, {}).get('size') or
|
| + state.result.files[infile].get('size') or
|
| os.stat(infile).st_size)
|
| if in_size == out_size:
|
| continue
|
| @@ -340,7 +520,7 @@ def MODEhashtable(outdir, indir, data):
|
| return 0
|
|
|
|
|
| -def MODEremap(outdir, indir, data):
|
| +def MODEremap(outdir, state):
|
| if not outdir:
|
| outdir = tempfile.mkdtemp(prefix='isolate')
|
| else:
|
| @@ -351,34 +531,40 @@ def MODEremap(outdir, indir, data):
|
| print 'Can\'t remap in a non-empty directory'
|
| return 1
|
| recreate_tree(
|
| - outdir, indir, data['files'].keys(), run_test_from_archive.HARDLINK)
|
| - if data['read_only']:
|
| + outdir,
|
| + state.root_dir,
|
| + state.result.files.keys(),
|
| + run_test_from_archive.HARDLINK)
|
| + if state.result.read_only:
|
| run_test_from_archive.make_writable(outdir, True)
|
| return 0
|
|
|
|
|
| -def MODErun(_outdir, indir, data):
|
| +def MODErun(_outdir, state):
|
| """Always uses a temporary directory."""
|
| try:
|
| outdir = tempfile.mkdtemp(prefix='isolate')
|
| recreate_tree(
|
| - outdir, indir, data['files'].keys(), run_test_from_archive.HARDLINK)
|
| - cwd = os.path.join(outdir, data['relative_cwd'])
|
| + outdir,
|
| + state.root_dir,
|
| + state.result.files.keys(),
|
| + run_test_from_archive.HARDLINK)
|
| + cwd = os.path.join(outdir, state.result.relative_cwd)
|
| if not os.path.isdir(cwd):
|
| os.makedirs(cwd)
|
| - if data['read_only']:
|
| + if state.result.read_only:
|
| run_test_from_archive.make_writable(outdir, True)
|
| - if not data['command']:
|
| + if not state.result.command:
|
| print 'No command to run'
|
| return 1
|
| - cmd = trace_inputs.fix_python_path(data['command'])
|
| + cmd = trace_inputs.fix_python_path(state.result.command)
|
| logging.info('Running %s, cwd=%s' % (cmd, cwd))
|
| return subprocess.call(cmd, cwd=cwd)
|
| finally:
|
| run_test_from_archive.rmtree(outdir)
|
|
|
|
|
| -def MODEtrace(_outdir, indir, data):
|
| +def MODEtrace(_outdir, state):
|
| """Shortcut to use trace_inputs.py properly.
|
|
|
| It constructs the equivalent of dictfiles. It is hardcoded to base the
|
| @@ -386,140 +572,107 @@ def MODEtrace(_outdir, indir, data):
|
| """
|
| logging.info(
|
| 'Running %s, cwd=%s' % (
|
| - data['command'], os.path.join(indir, data['relative_cwd'])))
|
| + state.result.command,
|
| + os.path.join(state.root_dir, state.result.relative_cwd)))
|
| product_dir = None
|
| - if data['resultdir'] and indir:
|
| + if state.resultdir and state.root_dir:
|
| # Defaults to none if both are the same directory.
|
| try:
|
| - product_dir = os.path.relpath(data['resultdir'], indir) or None
|
| + product_dir = os.path.relpath(state.resultdir, state.root_dir) or None
|
| except ValueError:
|
| - # This happens on Windows if data['resultdir'] is one drive, let's say
|
| - # 'C:\' and indir on another one like 'D:\'.
|
| + # This happens on Windows if state.resultdir is one drive, let's say
|
| + # 'C:\' and state.root_dir on another one like 'D:\'.
|
| product_dir = None
|
| - if not data['command']:
|
| + if not state.result.command:
|
| print 'No command to run'
|
| return 1
|
| return trace_inputs.trace_inputs(
|
| - data['resultfile'] + '.log',
|
| - data['command'],
|
| - indir,
|
| - data['relative_cwd'],
|
| + state.result_file + '.log',
|
| + state.result.command,
|
| + state.root_dir,
|
| + state.result.relative_cwd,
|
| product_dir,
|
| False)
|
|
|
|
|
| -def get_valid_modes():
|
| - """Returns the modes that can be used."""
|
| - return sorted(
|
| - i[4:] for i in dir(sys.modules[__name__]) if i.startswith('MODE'))
|
| +# Must be declared after all the functions.
|
| +VALID_MODES = {
|
| + 'check': MODEcheck,
|
| + 'hashtable': MODEhashtable,
|
| + 'remap': MODEremap,
|
| + 'run': MODErun,
|
| + 'trace': MODEtrace,
|
| +}
|
|
|
|
|
| -def determine_root_dir(relative_root, infiles):
|
| - """For a list of infiles, determines the deepest root directory that is
|
| - referenced indirectly.
|
| +# Only hashtable mode really needs the sha-1.
|
| +LEVELS = {
|
| + 'check': NO_INFO,
|
| + 'hashtable': WITH_HASH,
|
| + 'remap': STATS_ONLY,
|
| + 'run': STATS_ONLY,
|
| + 'trace': STATS_ONLY,
|
| +}
|
|
|
| - All the paths are processed as posix-style but are eventually returned as
|
| - os.path.sep.
|
| - """
|
| - # The trick used to determine the root directory is to look at "how far" back
|
| - # up it is looking up.
|
| - relative_root = relative_root.replace(os.path.sep, '/')
|
| - deepest_root = relative_root
|
| - for i in infiles:
|
| - x = relative_root
|
| - i = i.replace(os.path.sep, '/')
|
| - while i.startswith('../'):
|
| - i = i[3:]
|
| - assert not i.startswith('/')
|
| - x = posixpath.dirname(x)
|
| - if deepest_root.startswith(x):
|
| - deepest_root = x
|
| - deepest_root = deepest_root.replace('/', os.path.sep)
|
| - logging.debug(
|
| - 'determine_root_dir(%s, %s) -> %s' % (
|
| - relative_root, infiles, deepest_root))
|
| - return deepest_root.replace('/', os.path.sep)
|
| -
|
| -
|
| -def process_options(variables, resultfile, input_file, error):
|
| - """Processes the options and loads the input and result files.
|
| -
|
| - Returns a tuple of:
|
| - - The deepest root directory used as a relative path, to be used to determine
|
| - 'indir'.
|
| - - The list of dependency files.
|
| - - The 'data' dictionary. It contains all the processed data from the result
|
| - file if it existed, augmented with current data. This permits keeping the
|
| - state of data['variables'] across runs, simplifying the command line on
|
| - repeated run, e.g. the variables are kept between runs.
|
| - Warning: data['files'] is stale at that point and it only use as a cache for
|
| - the previous hash if the file wasn't touched between two runs, to speed it
|
| - up. 'infiles' must be used as the valid list of dependencies.
|
| +
|
| +assert (
|
| + sorted(i[4:] for i in dir(sys.modules[__name__]) if i.startswith('MODE')) ==
|
| + sorted(VALID_MODES))
|
| +
|
| +
|
| +def isolate(result_file, isolate_file, mode, variables, out_dir, error):
|
| + """Main function to isolate a target with its dependencies.
|
| +
|
| + Arguments:
|
| + - result_file: File to load or save state from.
|
| + - isolate_file: File to load data from. Can be None if result_file contains
|
| + the necessary information.
|
| + - mode: Action to do. See file level docstring.
|
| + - variables: Variables to process, if necessary.
|
| + - out_dir: Output directory where the result is stored. It's use depends on
|
| + |mode|.
|
| +
|
| + Some arguments are optional, dependending on |mode|. See the corresponding
|
| + MODE<mode> function for the exact behavior.
|
| """
|
| - # Constants
|
| - input_file = os.path.abspath(input_file).replace('/', os.path.sep)
|
| - relative_base_dir = os.path.dirname(input_file)
|
| - resultfile = os.path.abspath(resultfile).replace('/', os.path.sep)
|
| - logging.info(
|
| - 'process_options(%s, %s, %s, ...)' % (variables, resultfile, input_file))
|
| + # First, load the previous stuff if it was present. Namely, "foo.result" and
|
| + # "foo.state".
|
| + complete_state = CompleteState.load_files(result_file, out_dir)
|
| + isolate_file = isolate_file or complete_state.saved_state.isolate_file
|
| + if not isolate_file:
|
| + error('A .isolate file is required.')
|
| + if (complete_state.saved_state.isolate_file and
|
| + isolate_file != complete_state.saved_state.isolate_file):
|
| + error(
|
| + '%s and %s do not match.' % (
|
| + isolate_file, complete_state.saved_state.isolate_file))
|
|
|
| - # Process path variables as a special case. First normalize it, verifies it
|
| - # exists, convert it to an absolute path, then set it as relative to
|
| - # relative_base_dir.
|
| - for i in ('DEPTH', 'PRODUCT_DIR'):
|
| - if i not in variables:
|
| - continue
|
| - variable = os.path.normpath(variables[i])
|
| - if not os.path.isdir(variable):
|
| - error('%s=%s is not a directory' % (i, variable))
|
| - variable = os.path.abspath(variable).replace('/', os.path.sep)
|
| - # All variables are relative to the input file.
|
| - variables[i] = os.path.relpath(variable, relative_base_dir)
|
| + try:
|
| + # Then process options and expands directories.
|
| + complete_state.load_isolate(isolate_file, variables, error)
|
|
|
| - # At that point, variables are not replaced yet in command and infiles.
|
| - command, infiles, read_only = load_isolate(
|
| - open(input_file, 'r').read(), error)
|
| -
|
| - # Load the result file and set the values already known about.
|
| - data = load_results(resultfile)
|
| - data['read_only'] = read_only
|
| - data['resultfile'] = resultfile
|
| - data['resultdir'] = os.path.dirname(resultfile)
|
| - # Keep the old variables but override them with the new ones.
|
| - data.setdefault('variables', {}).update(variables)
|
| -
|
| - # Convert the variables.
|
| - data['command'] = [eval_variables(i, data['variables']) for i in command]
|
| - infiles = [eval_variables(f, data['variables']) for f in infiles]
|
| - root_dir = determine_root_dir(relative_base_dir, infiles)
|
| -
|
| - # The relative directory is automatically determined by the relative path
|
| - # between root_dir and the directory containing the .isolate file,
|
| - # isolate_base_dir. Keep relative_cwd posix-style.
|
| - data['relative_cwd'] = os.path.relpath(relative_base_dir, root_dir).replace(
|
| - os.path.sep, '/')
|
| -
|
| - logging.debug('relative_cwd: %s' % data['relative_cwd'])
|
| - logging.debug(
|
| - 'variables: %s' % ', '.join(
|
| - '%s=%s' % (k, data['variables'][k]) for k in sorted(data['variables'])))
|
| - logging.debug('command: %s' % data['command'])
|
| - logging.debug('read_only: %s' % data['read_only'])
|
| + # Regenerate complete_state.result.files.
|
| + complete_state.process_inputs(LEVELS[mode])
|
| +
|
| + # Finally run the mode-specific code.
|
| + result = VALID_MODES[mode](out_dir, complete_state)
|
| + except run_test_from_archive.MappingError, e:
|
| + error(str(e))
|
|
|
| - # Normalize the infiles paths in case some absolute paths got in.
|
| - logging.debug('infiles before normalization: %s' % infiles)
|
| - infiles = [normpath(os.path.join(data['relative_cwd'], f)) for f in infiles]
|
| - logging.debug('processed infiles: %s' % infiles)
|
| - return root_dir, infiles, data
|
| + # Then store the result and state.
|
| + complete_state.save_files()
|
| + return result
|
|
|
|
|
| def main():
|
| + """Handles CLI and normalizes the input arguments to pass them to isolate().
|
| + """
|
| default_variables = [('OS', trace_inputs.get_flavor())]
|
| if sys.platform in ('win32', 'cygwin'):
|
| default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
|
| else:
|
| default_variables.append(('EXECUTABLE_SUFFIX', ''))
|
| - valid_modes = get_valid_modes() + ['noop']
|
| + valid_modes = sorted(VALID_MODES.keys() + ['noop'])
|
| parser = optparse.OptionParser(
|
| usage='%prog [options] [.isolate file]',
|
| description=sys.modules[__name__].__doc__)
|
| @@ -560,7 +713,10 @@ def main():
|
|
|
| if not options.mode:
|
| parser.error('--mode is required')
|
| - if len(args) != 1:
|
| + if not options.result:
|
| + parser.error('--result is required.')
|
| +
|
| + if len(args) > 1:
|
| logging.debug('%s' % sys.argv)
|
| parser.error('Use only one argument which should be a .isolate file')
|
|
|
| @@ -569,21 +725,27 @@ def main():
|
| # have all the test data files checked out. Exit silently.
|
| return 0
|
|
|
| - root_dir, infiles, data = process_options(
|
| - dict(options.variables), options.result, args[0], parser.error)
|
| -
|
| - try:
|
| - resultcode, data = isolate(
|
| - options.outdir,
|
| - options.mode,
|
| - root_dir,
|
| - infiles,
|
| - data)
|
| - except run_test_from_archive.MappingError, e:
|
| - print >> sys.stderr, str(e)
|
| - return 1
|
| - save_results(options.result, data)
|
| - return resultcode
|
| + # Make sure the paths make sense. On Windows, / and \ are often mixed together
|
| + # in a path.
|
| + result_file = os.path.abspath(options.result.replace('/', os.path.sep))
|
| + # input_file may be None.
|
| + input_file = (
|
| + os.path.abspath(args[0].replace('/', os.path.sep)) if args else None)
|
| + # out_dir may be None.
|
| + out_dir = (
|
| + os.path.abspath(options.outdir.replace('/', os.path.sep))
|
| + if options.outdir else None)
|
| + # Fix variables.
|
| + variables = dict(options.variables)
|
| +
|
| + # After basic validation, pass this to isolate().
|
| + return isolate(
|
| + result_file,
|
| + input_file,
|
| + options.mode,
|
| + variables,
|
| + out_dir,
|
| + parser.error)
|
|
|
|
|
| if __name__ == '__main__':
|
|
|