Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / tools / swarming_client / isolate.py
index b5940fa..89ab0ad 100755 (executable)
@@ -13,20 +13,21 @@ See more information at
 """
 # Run ./isolate.py --help for more detailed information.
 
+__version__ = '0.4'
+
 import datetime
 import logging
 import optparse
 import os
-import posixpath
 import re
 import subprocess
 import sys
 
 import auth
 import isolate_format
+import isolated_format
 import isolateserver
 import run_isolated
-import trace_inputs
 
 from third_party import colorama
 from third_party.depot_tools import fix_encoding
@@ -36,9 +37,6 @@ from utils import file_path
 from utils import tools
 
 
-__version__ = '0.3.1'
-
-
 class ExecutionError(Exception):
   """A generic error occurred."""
   def __str__(self):
@@ -48,27 +46,6 @@ class ExecutionError(Exception):
 ### Path handling code.
 
 
-def expand_directories_and_symlinks(indir, infiles, blacklist,
-                                    follow_symlinks, ignore_broken_items):
-  """Expands the directories and the symlinks, applies the blacklist and
-  verifies files exist.
-
-  Files are specified in os native path separator.
-  """
-  outfiles = []
-  for relfile in infiles:
-    try:
-      outfiles.extend(
-          isolateserver.expand_directory_and_symlink(
-              indir, relfile, blacklist, follow_symlinks))
-    except isolateserver.MappingError as e:
-      if ignore_broken_items:
-        logging.info('warning: %s', e)
-      else:
-        raise
-  return outfiles
-
-
 def recreate_tree(outdir, indir, infiles, action, as_hash):
   """Creates a new tree with only the input files in it.
 
@@ -100,7 +77,7 @@ def recreate_tree(outdir, indir, infiles, action, as_hash):
         # Just do a quick check that the file size matches. No need to stat()
         # again the input file, grab the value from the dict.
         if not 's' in metadata:
-          raise isolateserver.MappingError(
+          raise isolated_format.MappingError(
               'Misconfigured item %s: %s' % (relfile, metadata))
         if metadata['s'] == os.stat(outfile).st_size:
           continue
@@ -170,182 +147,6 @@ def isolatedfile_to_state(filename):
   return filename + '.state'
 
 
-def classify_files(root_dir, tracked, untracked):
-  """Converts the list of files into a .isolate 'variables' dictionary.
-
-  Arguments:
-  - tracked: list of files names to generate a dictionary out of that should
-             probably be tracked.
-  - untracked: list of files names that must not be tracked.
-  """
-  new_tracked = []
-  new_untracked = list(untracked)
-
-  def should_be_tracked(filepath):
-    """Returns True if it is a file without whitespace in a non-optional
-    directory that has no symlink in its path.
-    """
-    if filepath.endswith('/'):
-      return False
-    if ' ' in filepath:
-      return False
-    # Look if any element in the path is a symlink.
-    split = filepath.split('/')
-    for i in range(len(split)):
-      if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
-        return False
-    return True
-
-  for filepath in sorted(tracked):
-    if should_be_tracked(filepath):
-      new_tracked.append(filepath)
-    else:
-      # Anything else.
-      new_untracked.append(filepath)
-
-  variables = {}
-  if new_tracked:
-    variables[isolate_format.KEY_TRACKED] = sorted(new_tracked)
-  if new_untracked:
-    variables[isolate_format.KEY_UNTRACKED] = sorted(new_untracked)
-  return variables
-
-
-def chromium_fix(f, variables):
-  """Fixes an isolate dependency with Chromium-specific fixes."""
-  # Blacklist logs and other unimportant files.
-  # - 'First Run' is not created by the compile but by the test itself.
-  # - Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
-  #   separator at this point.
-  if (re.match(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$', f) or
-      f == '<(PRODUCT_DIR)/First Run'):
-    logging.debug('Ignoring %s', f)
-    return None
-
-  EXECUTABLE = re.compile(
-      r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
-      re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
-      r'$')
-  match = EXECUTABLE.match(f)
-  if match:
-    return match.group(1) + '<(EXECUTABLE_SUFFIX)'
-
-  if sys.platform == 'darwin':
-    # On OSX, the name of the output is dependent on gyp define, it can be
-    # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
-    # Framework.framework'. Furthermore, they are versioned with a gyp
-    # variable.  To lower the complexity of the .isolate file, remove all the
-    # individual entries that show up under any of the 4 entries and replace
-    # them with the directory itself. Overall, this results in a bit more
-    # files than strictly necessary.
-    OSX_BUNDLES = (
-      '<(PRODUCT_DIR)/Chromium Framework.framework/',
-      '<(PRODUCT_DIR)/Chromium.app/',
-      '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
-      '<(PRODUCT_DIR)/Google Chrome.app/',
-    )
-    for prefix in OSX_BUNDLES:
-      if f.startswith(prefix):
-        # Note this result in duplicate values, so the a set() must be used to
-        # remove duplicates.
-        return prefix
-  return f
-
-
-def generate_simplified(
-    tracked, untracked, touched, root_dir, path_variables, extra_variables,
-    relative_cwd, trace_blacklist):
-  """Generates a clean and complete .isolate 'variables' dictionary.
-
-  Cleans up and extracts only files from within root_dir then processes
-  variables and relative_cwd.
-  """
-  root_dir = os.path.realpath(root_dir)
-  logging.info(
-      'generate_simplified(%d files, %s, %s, %s, %s)' %
-      (len(tracked) + len(untracked) + len(touched),
-        root_dir, path_variables, extra_variables, relative_cwd))
-
-  # Preparation work.
-  relative_cwd = file_path.cleanup_path(relative_cwd)
-  assert not os.path.isabs(relative_cwd), relative_cwd
-
-  # Normalizes to posix path. .isolate files are using posix paths on all OSes
-  # for coherency.
-  path_variables = dict(
-      (k, v.replace(os.path.sep, '/')) for k, v in path_variables.iteritems())
-  # Contains normalized path_variables plus extra_variables.
-  total_variables = path_variables.copy()
-  total_variables.update(extra_variables)
-
-  # Actual work: Process the files.
-  # TODO(maruel): if all the files in a directory are in part tracked and in
-  # part untracked, the directory will not be extracted. Tracked files should be
-  # 'promoted' to be untracked as needed.
-  tracked = trace_inputs.extract_directories(
-      root_dir, tracked, trace_blacklist)
-  untracked = trace_inputs.extract_directories(
-      root_dir, untracked, trace_blacklist)
-  # touched is not compressed, otherwise it would result in files to be archived
-  # that we don't need.
-
-  root_dir_posix = root_dir.replace(os.path.sep, '/')
-  def fix(f):
-    """Bases the file on the most restrictive variable."""
-    # Important, GYP stores the files with / and not \.
-    f = f.replace(os.path.sep, '/')
-    logging.debug('fix(%s)' % f)
-    # If it's not already a variable.
-    if not f.startswith('<'):
-      # relative_cwd is usually the directory containing the gyp file. It may be
-      # empty if the whole directory containing the gyp file is needed.
-      # Use absolute paths in case cwd_dir is outside of root_dir.
-      # Convert the whole thing to / since it's isolate's speak.
-      f = file_path.posix_relpath(
-          posixpath.join(root_dir_posix, f),
-          posixpath.join(root_dir_posix, relative_cwd)) or './'
-
-      # Use the longest value first.
-      for key, value in sorted(
-          path_variables.iteritems(), key=lambda x: -len(x[1])):
-        if f.startswith(value):
-          f = '<(%s)%s' % (key, f[len(value):])
-          logging.debug('Converted to %s' % f)
-          break
-    return f
-
-  def fix_all(items):
-    """Reduces the items to convert variables, removes unneeded items, apply
-    chromium-specific fixes and only return unique items.
-    """
-    variables_converted = (fix(f.path) for f in items)
-    chromium_fixed = (
-        chromium_fix(f, total_variables) for f in variables_converted)
-    return set(f for f in chromium_fixed if f)
-
-  tracked = fix_all(tracked)
-  untracked = fix_all(untracked)
-  touched = fix_all(touched)
-  out = classify_files(root_dir, tracked, untracked)
-  if touched:
-    out[isolate_format.KEY_TOUCHED] = sorted(touched)
-  return out
-
-
-def generate_isolate(
-    tracked, untracked, touched, root_dir, path_variables, config_variables,
-    extra_variables, relative_cwd, trace_blacklist):
-  """Generates a clean and complete .isolate file."""
-  dependencies = generate_simplified(
-      tracked, untracked, touched, root_dir, path_variables, extra_variables,
-      relative_cwd, trace_blacklist)
-  config_variable_names, config_values = zip(
-      *sorted(config_variables.iteritems()))
-  out = isolate_format.Configs(None, config_variable_names)
-  out.set_config(config_values, isolate_format.ConfigSettings(dependencies))
-  return out.make_isolate_file()
-
-
 def chromium_save_isolated(isolated, data, path_variables, algo):
   """Writes one or many .isolated files.
 
@@ -379,10 +180,10 @@ def chromium_save_isolated(isolated, data, path_variables, algo):
     slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
     tools.write_json(slavepath, f, True)
     data.setdefault('includes', []).append(
-        isolateserver.hash_file(slavepath, algo))
+        isolated_format.hash_file(slavepath, algo))
     files.append(os.path.basename(slavepath))
 
-  files.extend(isolateserver.save_isolated(isolated, data))
+  files.extend(isolated_format.save_isolated(isolated, data))
   return files
 
 
@@ -449,6 +250,9 @@ class SavedState(Flattenable):
     # Algorithm used to generate the hash. The only supported value is at the
     # time of writting 'sha-1'.
     'algo',
+    # List of included .isolated files. Used to support/remember 'slave'
+    # .isolated files. Relative path to isolated_basedir.
+    'child_isolated_files',
     # Cache of the processed command. This value is saved because .isolated
     # files are never loaded by isolate.py so it's the only way to load the
     # command safely.
@@ -463,21 +267,25 @@ class SavedState(Flattenable):
     'files',
     # Path of the original .isolate file. Relative path to isolated_basedir.
     'isolate_file',
-    # List of included .isolated files. Used to support/remember 'slave'
-    # .isolated files. Relative path to isolated_basedir.
-    'child_isolated_files',
+    # GYP variables used to generate the .isolated files paths based on path
+    # variables. Frequent examples are DEPTH and PRODUCT_DIR.
+    'path_variables',
     # If the generated directory tree should be read-only.
     'read_only',
     # Relative cwd to use to start the command.
     'relative_cwd',
-    # GYP variables used to generate the .isolated files paths based on path
-    # variables. Frequent examples are DEPTH and PRODUCT_DIR.
-    'path_variables',
-    # Version of the file format in format 'major.minor'. Any non-breaking
-    # change must update minor. Any breaking change must update major.
+    # Root directory the files are mapped from.
+    'root_dir',
+    # Version of the saved state file format. Any breaking change must update
+    # the value.
     'version',
   )
 
+  # Bump this version whenever the saved state changes. It is also keyed on the
+  # .isolated file version so any change in the generator will invalidate .state
+  # files.
+  EXPECTED_VERSION = isolated_format.ISOLATED_FILE_VERSION + '.2'
+
   def __init__(self, isolated_basedir):
     """Creates an empty SavedState.
 
@@ -492,7 +300,7 @@ class SavedState(Flattenable):
 
     # The default algorithm used.
     self.OS = sys.platform
-    self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
+    self.algo = isolated_format.SUPPORTED_ALGOS['sha-1']
     self.child_isolated_files = []
     self.command = []
     self.config_variables = {}
@@ -502,10 +310,14 @@ class SavedState(Flattenable):
     self.path_variables = {}
     self.read_only = None
     self.relative_cwd = None
-    self.version = isolateserver.ISOLATED_FILE_VERSION
+    self.root_dir = None
+    self.version = self.EXPECTED_VERSION
 
-  def update(
-      self, isolate_file, path_variables, config_variables, extra_variables):
+  def update_config(self, config_variables):
+    """Updates the saved state with only config variables."""
+    self.config_variables.update(config_variables)
+
+  def update(self, isolate_file, path_variables, extra_variables):
     """Updates the saved state with new data to keep GYP variables and internal
     reference to the original .isolate file.
     """
@@ -519,7 +331,6 @@ class SavedState(Flattenable):
     # .isolated.state.
     assert isolate_file == self.isolate_file or not self.isolate_file, (
         isolate_file, self.isolate_file)
-    self.config_variables.update(config_variables)
     self.extra_variables.update(extra_variables)
     self.isolate_file = isolate_file
     self.path_variables.update(path_variables)
@@ -553,10 +364,12 @@ class SavedState(Flattenable):
       return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
 
     out = {
-      'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
+      'algo': isolated_format.SUPPORTED_ALGOS_REVERSE[self.algo],
       'files': dict(
           (filepath, strip(data)) for filepath, data in self.files.iteritems()),
-      'version': self.version,
+      # The version of the .state file is different than the one of the
+      # .isolated file.
+      'version': isolated_format.ISOLATED_FILE_VERSION,
     }
     if self.command:
       out['command'] = self.command
@@ -582,21 +395,19 @@ class SavedState(Flattenable):
     """
     out = super(SavedState, cls).load(data, isolated_basedir)
     if data.get('OS') != sys.platform:
-      raise isolateserver.ConfigError('Unexpected OS %s', data.get('OS'))
+      raise isolated_format.IsolatedError('Unexpected OS %s', data.get('OS'))
 
     # Converts human readable form back into the proper class type.
     algo = data.get('algo')
-    if not algo in isolateserver.SUPPORTED_ALGOS:
-      raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
-    out.algo = isolateserver.SUPPORTED_ALGOS[algo]
+    if not algo in isolated_format.SUPPORTED_ALGOS:
+      raise isolated_format.IsolatedError('Unknown algo \'%s\'' % out.algo)
+    out.algo = isolated_format.SUPPORTED_ALGOS[algo]
 
     # Refuse the load non-exact version, even minor difference. This is unlike
     # isolateserver.load_isolated(). This is because .isolated.state could have
     # changed significantly even in minor version difference.
-    if not re.match(r'^(\d+)\.(\d+)$', out.version):
-      raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
-    if out.version != isolateserver.ISOLATED_FILE_VERSION:
-      raise isolateserver.ConfigError(
+    if out.version != cls.EXPECTED_VERSION:
+      raise isolated_format.IsolatedError(
           'Unsupported version \'%s\'' % out.version)
 
     # The .isolate file must be valid. If it is not present anymore, zap the
@@ -615,7 +426,7 @@ class SavedState(Flattenable):
   def flatten(self):
     """Makes sure 'algo' is in human readable form."""
     out = super(SavedState, self).flatten()
-    out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
+    out['algo'] = isolated_format.SUPPORTED_ALGOS_REVERSE[out['algo']]
     return out
 
   def __str__(self):
@@ -670,24 +481,26 @@ class CompleteState(object):
         'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
         cwd, isolate_file, path_variables, config_variables, extra_variables,
         ignore_broken_items)
-    relative_base_dir = os.path.dirname(isolate_file)
 
-    # Processes the variables.
-    path_variables = normalize_path_variables(
-        cwd, path_variables, relative_base_dir)
-    # Update the saved state.
-    self.saved_state.update(
-        isolate_file, path_variables, config_variables, extra_variables)
-    path_variables = self.saved_state.path_variables
+    # Config variables are not affected by the paths and must be used to
+    # retrieve the paths, so update them first.
+    self.saved_state.update_config(config_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, touched, read_only = (
+      command, infiles, touched, read_only, isolate_cmd_dir = (
           isolate_format.load_isolate_for_config(
               os.path.dirname(isolate_file), f.read(),
               self.saved_state.config_variables))
 
+    # Processes the variables with the new found relative root. Note that 'cwd'
+    # is used when path variables are used.
+    path_variables = normalize_path_variables(
+        cwd, path_variables, isolate_cmd_dir)
+    # Update the rest of the saved state.
+    self.saved_state.update(isolate_file, path_variables, extra_variables)
+
     total_variables = self.saved_state.path_variables.copy()
     total_variables.update(self.saved_state.config_variables)
     total_variables.update(self.saved_state.extra_variables)
@@ -706,38 +519,41 @@ class CompleteState(object):
     # root_dir is automatically determined by the deepest root accessed with the
     # form '../../foo/bar'. Note that path variables must be taken in account
     # too, add them as if they were input files.
-    root_dir = isolate_format.determine_root_dir(
-        relative_base_dir, infiles + touched +
+    self.saved_state.root_dir = isolate_format.determine_root_dir(
+        isolate_cmd_dir, infiles + touched +
         self.saved_state.path_variables.values())
     # 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)
+    relative_cwd = os.path.relpath(isolate_cmd_dir, self.saved_state.root_dir)
     # Now that we know where the root is, check that the path_variables point
     # inside it.
     for k, v in self.saved_state.path_variables.iteritems():
-      if not file_path.path_starts_with(
-          root_dir, os.path.join(relative_base_dir, v)):
-        raise isolateserver.MappingError(
-            'Path variable %s=%r points outside the inferred root directory %s'
-            % (k, v, root_dir))
-    # Normalize the files based to root_dir. It is important to keep the
-    # trailing os.path.sep at that step.
+      dest = os.path.join(isolate_cmd_dir, relative_cwd, v)
+      if not file_path.path_starts_with(self.saved_state.root_dir, dest):
+        raise isolated_format.MappingError(
+            'Path variable %s=%r points outside the inferred root directory '
+            '%s; %s'
+            % (k, v, self.saved_state.root_dir, dest))
+    # Normalize the files based to self.saved_state.root_dir. It is important to
+    # keep the trailing os.path.sep at that step.
     infiles = [
       file_path.relpath(
-          file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
+          file_path.normpath(os.path.join(isolate_cmd_dir, f)),
+          self.saved_state.root_dir)
       for f in infiles
     ]
     touched = [
       file_path.relpath(
-          file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
+          file_path.normpath(os.path.join(isolate_cmd_dir, f)),
+          self.saved_state.root_dir)
       for f in touched
     ]
     follow_symlinks = sys.platform != 'win32'
     # Expand the directories by listing each file inside. Up to now, trailing
     # os.path.sep must be kept. Do not expand 'touched'.
-    infiles = expand_directories_and_symlinks(
-        root_dir,
+    infiles = isolated_format.expand_directories_and_symlinks(
+        self.saved_state.root_dir,
         infiles,
         lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
         follow_symlinks,
@@ -758,20 +574,20 @@ class CompleteState(object):
         command, infiles, touched, read_only, relative_cwd)
     logging.debug(self)
 
-  def process_inputs(self, subdir):
+  def files_to_metadata(self, subdir):
     """Updates self.saved_state.files with the files' mode and hash.
 
     If |subdir| is specified, filters to a subdirectory. The resulting .isolated
     file is tainted.
 
-    See isolateserver.process_input() for more information.
+    See isolated_format.file_to_metadata() for more information.
     """
     for infile in sorted(self.saved_state.files):
       if subdir and not infile.startswith(subdir):
         self.saved_state.files.pop(infile)
       else:
         filepath = os.path.join(self.root_dir, infile)
-        self.saved_state.files[infile] = isolateserver.process_input(
+        self.saved_state.files[infile] = isolated_format.file_to_metadata(
             filepath,
             self.saved_state.files[infile],
             self.saved_state.read_only,
@@ -796,36 +612,7 @@ class CompleteState(object):
 
   @property
   def root_dir(self):
-    """Returns the absolute path of the root_dir to reference the .isolate file
-    via relative_cwd.
-
-    So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
-    to isolate_filepath.
-    """
-    if not self.saved_state.isolate_file:
-      raise ExecutionError('Please specify --isolate')
-    isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
-    # Special case '.'.
-    if self.saved_state.relative_cwd == '.':
-      root_dir = isolate_dir
-    else:
-      if not isolate_dir.endswith(self.saved_state.relative_cwd):
-        raise ExecutionError(
-            ('Make sure the .isolate file is in the directory that will be '
-             'used as the relative directory. It is currently in %s and should '
-             'be in %s') % (isolate_dir, self.saved_state.relative_cwd))
-      # Walk back back to the root directory.
-      root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
-    return file_path.get_native_path_case(root_dir)
-
-  @property
-  def resultdir(self):
-    """Returns the absolute path containing the .isolated file.
-
-    It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
-    path as the value.
-    """
-    return os.path.dirname(self.isolated_filepath)
+    return self.saved_state.root_dir
 
   def __str__(self):
     def indent(data, indent_length):
@@ -913,71 +700,10 @@ def load_complete_state(options, cwd, subdir, skip_update):
     subdir = subdir.replace('/', os.path.sep)
 
   if not skip_update:
-    complete_state.process_inputs(subdir)
+    complete_state.files_to_metadata(subdir)
   return complete_state
 
 
-def read_trace_as_isolate_dict(complete_state, trace_blacklist):
-  """Reads a trace and returns the .isolate dictionary.
-
-  Returns exceptions during the log parsing so it can be re-raised.
-  """
-  api = trace_inputs.get_api()
-  logfile = complete_state.isolated_filepath + '.log'
-  if not os.path.isfile(logfile):
-    raise ExecutionError(
-        'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
-  try:
-    data = api.parse_log(logfile, trace_blacklist, None)
-    exceptions = [i['exception'] for i in data if 'exception' in i]
-    results = (i['results'] for i in data if 'results' in i)
-    results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
-    files = set(sum((result.existent for result in results_stripped), []))
-    tracked, touched = isolate_format.split_touched(files)
-    value = generate_isolate(
-        tracked,
-        [],
-        touched,
-        complete_state.root_dir,
-        complete_state.saved_state.path_variables,
-        complete_state.saved_state.config_variables,
-        complete_state.saved_state.extra_variables,
-        complete_state.saved_state.relative_cwd,
-        trace_blacklist)
-    return value, exceptions
-  except trace_inputs.TracingFailure, e:
-    raise ExecutionError(
-        'Reading traces failed for: %s\n%s' %
-          (' '.join(complete_state.saved_state.command), str(e)))
-
-
-def merge(complete_state, trace_blacklist):
-  """Reads a trace and merges it back into the source .isolate file."""
-  value, exceptions = read_trace_as_isolate_dict(
-      complete_state, trace_blacklist)
-
-  # Now take that data and union it into the original .isolate file.
-  with open(complete_state.saved_state.isolate_filepath, 'r') as f:
-    prev_content = f.read()
-  isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
-  prev_config = isolate_format.load_isolate_as_config(
-      isolate_dir,
-      isolate_format.eval_content(prev_content),
-      isolate_format.extract_comment(prev_content))
-  new_config = isolate_format.load_isolate_as_config(isolate_dir, value, '')
-  config = isolate_format.union(prev_config, new_config)
-  data = config.make_isolate_file()
-  print('Updating %s' % complete_state.saved_state.isolate_file)
-  with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
-    isolate_format.print_all(config.file_comment, data, f)
-  if exceptions:
-    # It got an exception, raise the first one.
-    raise \
-        exceptions[0][0], \
-        exceptions[0][1], \
-        exceptions[0][2]
-
-
 def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
   """Creates a isolated tree usable for test execution.
 
@@ -1026,7 +752,7 @@ def prepare_for_archival(options, cwd):
   for item in isolated_files:
     item_path = os.path.join(
         os.path.dirname(complete_state.isolated_filepath), item)
-    # Do not use isolateserver.hash_file() here because the file is
+    # Do not use isolated_format.hash_file() here because the file is
     # likely smallish (under 500kb) and its file size is needed.
     with open(item_path, 'rb') as f:
       content = f.read()
@@ -1058,6 +784,8 @@ def CMDarchive(parser, args):
   isolateserver.process_isolate_server_options(parser, options)
   if args:
     parser.error('Unsupported argument: %s' % args)
+  if file_path.is_url(options.isolate_server):
+    auth.ensure_logged_in(options.isolate_server)
   cwd = os.getcwd()
   with tools.Profiler('GenerateHashtable'):
     success = False
@@ -1097,92 +825,6 @@ def CMDcheck(parser, args):
   return 0
 
 
-def CMDhashtable(parser, args):
-  """Creates a .isolated file and stores the contains in a directory.
-
-  All the files listed in the .isolated file are put in the directory with their
-  sha-1 as their file name. When using an NFS/CIFS server, the files can then be
-  shared accross slaves without an isolate server.
-  """
-  add_subdir_option(parser)
-  isolateserver.add_outdir_options(parser)
-  add_skip_refresh_option(parser)
-  options, args = parser.parse_args(args)
-  if args:
-    parser.error('Unsupported argument: %s' % args)
-  cwd = os.getcwd()
-  isolateserver.process_outdir_options(parser, options, cwd)
-
-  success = False
-  try:
-    complete_state, infiles, isolated_hash = prepare_for_archival(options, cwd)
-    logging.info('Creating content addressed object store with %d item',
-                  len(infiles))
-    if not os.path.isdir(options.outdir):
-      os.makedirs(options.outdir)
-
-    # TODO(maruel): Make the files read-only?
-    recreate_tree(
-        outdir=options.outdir,
-        indir=complete_state.root_dir,
-        infiles=infiles,
-        action=run_isolated.HARDLINK_WITH_FALLBACK,
-        as_hash=True)
-    success = True
-    print('%s  %s' % (isolated_hash[0], os.path.basename(options.isolated)))
-  finally:
-    # If the command failed, delete the .isolated file if it exists. This is
-    # important so no stale swarm job is executed.
-    if not success and os.path.isfile(options.isolated):
-      os.remove(options.isolated)
-  return int(not success)
-
-
-def CMDmerge(parser, args):
-  """Reads and merges the data from the trace back into the original .isolate.
-  """
-  parser.require_isolated = False
-  add_trace_option(parser)
-  options, args = parser.parse_args(args)
-  if args:
-    parser.error('Unsupported argument: %s' % args)
-
-  complete_state = load_complete_state(options, os.getcwd(), None, False)
-  blacklist = tools.gen_blacklist(options.trace_blacklist)
-  merge(complete_state, blacklist)
-  return 0
-
-
-def CMDread(parser, args):
-  """Reads the trace file generated with command 'trace'."""
-  parser.require_isolated = False
-  add_trace_option(parser)
-  add_skip_refresh_option(parser)
-  parser.add_option(
-      '-m', '--merge', action='store_true',
-      help='merge the results back in the .isolate file instead of printing')
-  options, args = parser.parse_args(args)
-  if args:
-    parser.error('Unsupported argument: %s' % args)
-
-  complete_state = load_complete_state(
-      options, os.getcwd(), None, options.skip_refresh)
-  blacklist = tools.gen_blacklist(options.trace_blacklist)
-  value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
-  if options.merge:
-    merge(complete_state, blacklist)
-  else:
-    isolate_format.pretty_print(value, sys.stdout)
-
-  if exceptions:
-    # It got an exception, raise the first one.
-    raise \
-        exceptions[0][0], \
-        exceptions[0][1], \
-        exceptions[0][2]
-  return 0
-
-
 def CMDremap(parser, args):
   """Creates a directory with all the dependencies mapped into it.
 
@@ -1190,13 +832,13 @@ def CMDremap(parser, args):
   run.
   """
   parser.require_isolated = False
-  isolateserver.add_outdir_options(parser)
+  add_outdir_options(parser)
   add_skip_refresh_option(parser)
   options, args = parser.parse_args(args)
   if args:
     parser.error('Unsupported argument: %s' % args)
   cwd = os.getcwd()
-  isolateserver.process_outdir_options(parser, options, cwd)
+  process_outdir_options(parser, options, cwd)
   complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
 
   if not os.path.isdir(options.outdir):
@@ -1274,8 +916,16 @@ def CMDrun(parser, args):
         outdir, complete_state.root_dir, complete_state.saved_state.files,
         complete_state.saved_state.relative_cwd,
         complete_state.saved_state.read_only)
+    file_path.ensure_command_has_abs_path(cmd, cwd)
     logging.info('Running %s, cwd=%s' % (cmd, cwd))
-    result = subprocess.call(cmd, cwd=cwd)
+    try:
+      result = subprocess.call(cmd, cwd=cwd)
+    except OSError:
+      sys.stderr.write(
+          'Failed to executed the command; executable is missing, maybe you\n'
+          'forgot to map it in the .isolate file?\n  %s\n  in %s\n' %
+          (' '.join(cmd), cwd))
+      result = 1
   finally:
     run_isolated.rmtree(outdir)
 
@@ -1284,69 +934,6 @@ def CMDrun(parser, args):
   return result
 
 
-@subcommand.usage('-- [extra arguments]')
-def CMDtrace(parser, args):
-  """Traces the target using trace_inputs.py.
-
-  It runs the executable without remapping it, and traces all the files it and
-  its child processes access. Then the 'merge' command can be used to generate
-  an updated .isolate file out of it or the 'read' command to print it out to
-  stdout.
-
-  Argument processing stops at -- and these arguments are appended to the
-  command line of the target to run. For example, use:
-    isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
-  """
-  add_trace_option(parser)
-  parser.add_option(
-      '-m', '--merge', action='store_true',
-      help='After tracing, merge the results back in the .isolate file')
-  add_skip_refresh_option(parser)
-  options, args = parser.parse_args(args)
-
-  complete_state = load_complete_state(
-      options, os.getcwd(), None, options.skip_refresh)
-  cmd = complete_state.saved_state.command + args
-  if not cmd:
-    raise ExecutionError('No command to run.')
-  cmd = tools.fix_python_path(cmd)
-  cwd = os.path.normpath(os.path.join(
-      unicode(complete_state.root_dir),
-      complete_state.saved_state.relative_cwd))
-  cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
-  if not os.path.isfile(cmd[0]):
-    raise ExecutionError(
-        'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
-  logging.info('Running %s, cwd=%s' % (cmd, cwd))
-  api = trace_inputs.get_api()
-  logfile = complete_state.isolated_filepath + '.log'
-  api.clean_trace(logfile)
-  out = None
-  try:
-    with api.get_tracer(logfile) as tracer:
-      result, out = tracer.trace(
-          cmd,
-          cwd,
-          'default',
-          True)
-  except trace_inputs.TracingFailure, e:
-    raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
-
-  if result:
-    logging.error(
-        'Tracer exited with %d, which means the tests probably failed so the '
-        'trace is probably incomplete.', result)
-    logging.info(out)
-
-  complete_state.save_files()
-
-  if options.merge:
-    blacklist = tools.gen_blacklist(options.trace_blacklist)
-    merge(complete_state, blacklist)
-
-  return result
-
-
 def _process_variable_arg(option, opt, _value, parser):
   """Called by OptionParser to process a --<foo>-variable argument."""
   if not parser.rargs:
@@ -1427,16 +1014,6 @@ def add_subdir_option(parser):
            '.isolate file. Anything else is keyed on the root directory.')
 
 
-def add_trace_option(parser):
-  """Adds --trace-blacklist to the parser."""
-  parser.add_option(
-      '--trace-blacklist',
-      action='append', default=list(isolateserver.DEFAULT_BLACKLIST),
-      help='List of regexp to use as blacklist filter for files to consider '
-           'important, not to be confused with --blacklist which blacklists '
-           'test case.')
-
-
 def add_skip_refresh_option(parser):
   parser.add_option(
       '--skip-refresh', action='store_true',
@@ -1444,6 +1021,31 @@ def add_skip_refresh_option(parser):
            'dependencies')
 
 
+def add_outdir_options(parser):
+  """Adds --outdir, which is orthogonal to --isolate-server.
+
+  Note: On upload, separate commands are used between 'archive' and 'hashtable'.
+  On 'download', the same command can download from either an isolate server or
+  a file system.
+  """
+  parser.add_option(
+      '-o', '--outdir', metavar='DIR',
+      help='Directory used to recreate the tree.')
+
+
+def process_outdir_options(parser, options, cwd):
+  if not options.outdir:
+    parser.error('--outdir is required.')
+  if file_path.is_url(options.outdir):
+    parser.error('Can\'t use an URL for --outdir.')
+  options.outdir = unicode(options.outdir).replace('/', os.path.sep)
+  # outdir doesn't need native path case since tracing is never done from there.
+  options.outdir = os.path.abspath(
+      os.path.normpath(os.path.join(cwd, options.outdir)))
+  # In theory, we'd create the directory outdir right away. Defer doing it in
+  # case there's errors in the command line.
+
+
 def parse_isolated_option(parser, options, cwd, require_isolated):
   """Processes --isolated."""
   if options.isolated:
@@ -1523,11 +1125,7 @@ class OptionParserIsolate(tools.OptionParserWithLogging):
 
 def main(argv):
   dispatcher = subcommand.CommandDispatcher(__name__)
-  try:
-    return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
-  except Exception as e:
-    tools.report_error(e)
-    return 1
+  return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
 
 
 if __name__ == '__main__':