Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1118)

Unified Diff: tools/isolate/isolate.py

Issue 10387037: Complete rewrite of isolate.py to be more modular. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 8 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | tools/isolate/isolate_smoke_test.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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__':
« no previous file with comments | « no previous file | tools/isolate/isolate_smoke_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698