| Index: swarm_client/isolate.py
|
| ===================================================================
|
| --- swarm_client/isolate.py (revision 235167)
|
| +++ swarm_client/isolate.py (working copy)
|
| @@ -1,2461 +0,0 @@
|
| -#!/usr/bin/env python
|
| -# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -"""Front end tool to operate on .isolate files.
|
| -
|
| -This includes creating, merging or compiling them to generate a .isolated file.
|
| -
|
| -See more information at
|
| - https://code.google.com/p/swarming/wiki/IsolateDesign
|
| - https://code.google.com/p/swarming/wiki/IsolateUserGuide
|
| -"""
|
| -# Run ./isolate.py --help for more detailed information.
|
| -
|
| -import ast
|
| -import copy
|
| -import itertools
|
| -import logging
|
| -import optparse
|
| -import os
|
| -import posixpath
|
| -import re
|
| -import stat
|
| -import subprocess
|
| -import sys
|
| -
|
| -import isolateserver
|
| -import run_isolated
|
| -import trace_inputs
|
| -
|
| -# Import here directly so isolate is easier to use as a library.
|
| -from run_isolated import get_flavor
|
| -
|
| -from third_party import colorama
|
| -from third_party.depot_tools import fix_encoding
|
| -from third_party.depot_tools import subcommand
|
| -
|
| -from utils import file_path
|
| -from utils import tools
|
| -from utils import short_expression_finder
|
| -
|
| -
|
| -__version__ = '0.1.1'
|
| -
|
| -
|
| -PATH_VARIABLES = ('DEPTH', 'PRODUCT_DIR')
|
| -
|
| -# Files that should be 0-length when mapped.
|
| -KEY_TOUCHED = 'isolate_dependency_touched'
|
| -# Files that should be tracked by the build tool.
|
| -KEY_TRACKED = 'isolate_dependency_tracked'
|
| -# Files that should not be tracked by the build tool.
|
| -KEY_UNTRACKED = 'isolate_dependency_untracked'
|
| -
|
| -
|
| -class ExecutionError(Exception):
|
| - """A generic error occurred."""
|
| - def __str__(self):
|
| - return self.args[0]
|
| -
|
| -
|
| -### Path handling code.
|
| -
|
| -
|
| -DEFAULT_BLACKLIST = (
|
| - # Temporary vim or python files.
|
| - r'^.+\.(?:pyc|swp)$',
|
| - # .git or .svn directory.
|
| - r'^(?:.+' + re.escape(os.path.sep) + r'|)\.(?:git|svn)$',
|
| -)
|
| -
|
| -
|
| -# Chromium-specific.
|
| -DEFAULT_BLACKLIST += (
|
| - r'^.+\.(?:run_test_cases)$',
|
| - r'^(?:.+' + re.escape(os.path.sep) + r'|)testserver\.log$',
|
| -)
|
| -
|
| -
|
| -def relpath(path, root):
|
| - """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
|
| - return out
|
| -
|
| -
|
| -def safe_relpath(filepath, basepath):
|
| - """Do not throw on Windows when filepath and basepath are on different drives.
|
| -
|
| - Different than relpath() above since this one doesn't keep the trailing
|
| - os.path.sep and it swallows exceptions on Windows and return the original
|
| - absolute path in the case of different drives.
|
| - """
|
| - try:
|
| - return os.path.relpath(filepath, basepath)
|
| - except ValueError:
|
| - assert sys.platform == 'win32'
|
| - return filepath
|
| -
|
| -
|
| -def normpath(path):
|
| - """os.path.normpath() that keeps trailing os.path.sep."""
|
| - out = os.path.normpath(path)
|
| - if path.endswith(os.path.sep):
|
| - out += os.path.sep
|
| - return out
|
| -
|
| -
|
| -def posix_relpath(path, root):
|
| - """posix.relpath() that keeps trailing slash."""
|
| - out = posixpath.relpath(path, root)
|
| - if path.endswith('/'):
|
| - out += '/'
|
| - return out
|
| -
|
| -
|
| -def cleanup_path(x):
|
| - """Cleans up a relative path. Converts any os.path.sep to '/' on Windows."""
|
| - if x:
|
| - x = x.rstrip(os.path.sep).replace(os.path.sep, '/')
|
| - if x == '.':
|
| - x = ''
|
| - if x:
|
| - x += '/'
|
| - return x
|
| -
|
| -
|
| -def is_url(path):
|
| - return bool(re.match(r'^https?://.+$', path))
|
| -
|
| -
|
| -def path_starts_with(prefix, path):
|
| - """Returns true if the components of the path |prefix| are the same as the
|
| - initial components of |path| (or all of the components of |path|). The paths
|
| - must be absolute.
|
| - """
|
| - assert os.path.isabs(prefix) and os.path.isabs(path)
|
| - prefix = os.path.normpath(prefix)
|
| - path = os.path.normpath(path)
|
| - assert prefix == file_path.get_native_path_case(prefix), prefix
|
| - assert path == file_path.get_native_path_case(path), path
|
| - prefix = prefix.rstrip(os.path.sep) + os.path.sep
|
| - path = path.rstrip(os.path.sep) + os.path.sep
|
| - return path.startswith(prefix)
|
| -
|
| -
|
| -def fix_native_path_case(root, path):
|
| - """Ensures that each component of |path| has the proper native case by
|
| - iterating slowly over the directory elements of |path|."""
|
| - native_case_path = root
|
| - for raw_part in path.split(os.sep):
|
| - if not raw_part or raw_part == '.':
|
| - break
|
| -
|
| - part = file_path.find_item_native_case(native_case_path, raw_part)
|
| - if not part:
|
| - raise isolateserver.MappingError(
|
| - 'Input file %s doesn\'t exist' %
|
| - os.path.join(native_case_path, raw_part))
|
| - native_case_path = os.path.join(native_case_path, part)
|
| -
|
| - return os.path.normpath(native_case_path)
|
| -
|
| -
|
| -def expand_symlinks(indir, relfile):
|
| - """Follows symlinks in |relfile|, but treating symlinks that point outside the
|
| - build tree as if they were ordinary directories/files. Returns the final
|
| - symlink-free target and a list of paths to symlinks encountered in the
|
| - process.
|
| -
|
| - The rule about symlinks outside the build tree is for the benefit of the
|
| - Chromium OS ebuild, which symlinks the output directory to an unrelated path
|
| - in the chroot.
|
| -
|
| - Fails when a directory loop is detected, although in theory we could support
|
| - that case.
|
| - """
|
| - is_directory = relfile.endswith(os.path.sep)
|
| - done = indir
|
| - todo = relfile.strip(os.path.sep)
|
| - symlinks = []
|
| -
|
| - while todo:
|
| - pre_symlink, symlink, post_symlink = file_path.split_at_symlink(
|
| - done, todo)
|
| - if not symlink:
|
| - todo = fix_native_path_case(done, todo)
|
| - done = os.path.join(done, todo)
|
| - break
|
| - symlink_path = os.path.join(done, pre_symlink, symlink)
|
| - post_symlink = post_symlink.lstrip(os.path.sep)
|
| - # readlink doesn't exist on Windows.
|
| - # pylint: disable=E1101
|
| - target = os.path.normpath(os.path.join(done, pre_symlink))
|
| - symlink_target = os.readlink(symlink_path)
|
| - if os.path.isabs(symlink_target):
|
| - # Absolute path are considered a normal directories. The use case is
|
| - # generally someone who puts the output directory on a separate drive.
|
| - target = symlink_target
|
| - else:
|
| - # The symlink itself could be using the wrong path case.
|
| - target = fix_native_path_case(target, symlink_target)
|
| -
|
| - if not os.path.exists(target):
|
| - raise isolateserver.MappingError(
|
| - 'Symlink target doesn\'t exist: %s -> %s' % (symlink_path, target))
|
| - target = file_path.get_native_path_case(target)
|
| - if not path_starts_with(indir, target):
|
| - done = symlink_path
|
| - todo = post_symlink
|
| - continue
|
| - if path_starts_with(target, symlink_path):
|
| - raise isolateserver.MappingError(
|
| - 'Can\'t map recursive symlink reference %s -> %s' %
|
| - (symlink_path, target))
|
| - logging.info('Found symlink: %s -> %s', symlink_path, target)
|
| - symlinks.append(os.path.relpath(symlink_path, indir))
|
| - # Treat the common prefix of the old and new paths as done, and start
|
| - # scanning again.
|
| - target = target.split(os.path.sep)
|
| - symlink_path = symlink_path.split(os.path.sep)
|
| - prefix_length = 0
|
| - for target_piece, symlink_path_piece in zip(target, symlink_path):
|
| - if target_piece == symlink_path_piece:
|
| - prefix_length += 1
|
| - else:
|
| - break
|
| - done = os.path.sep.join(target[:prefix_length])
|
| - todo = os.path.join(
|
| - os.path.sep.join(target[prefix_length:]), post_symlink)
|
| -
|
| - relfile = os.path.relpath(done, indir)
|
| - relfile = relfile.rstrip(os.path.sep) + is_directory * os.path.sep
|
| - return relfile, symlinks
|
| -
|
| -
|
| -def expand_directory_and_symlink(indir, relfile, blacklist, follow_symlinks):
|
| - """Expands a single input. It can result in multiple outputs.
|
| -
|
| - This function is recursive when relfile is a directory.
|
| -
|
| - Note: this code doesn't properly handle recursive symlink like one created
|
| - with:
|
| - ln -s .. foo
|
| - """
|
| - if os.path.isabs(relfile):
|
| - raise isolateserver.MappingError(
|
| - 'Can\'t map absolute path %s' % relfile)
|
| -
|
| - infile = normpath(os.path.join(indir, relfile))
|
| - if not infile.startswith(indir):
|
| - raise isolateserver.MappingError(
|
| - 'Can\'t map file %s outside %s' % (infile, indir))
|
| -
|
| - filepath = os.path.join(indir, relfile)
|
| - native_filepath = file_path.get_native_path_case(filepath)
|
| - if filepath != native_filepath:
|
| - # Special case './'.
|
| - if filepath != native_filepath + '.' + os.path.sep:
|
| - # Give up enforcing strict path case on OSX. Really, it's that sad. The
|
| - # case where it happens is very specific and hard to reproduce:
|
| - # get_native_path_case(
|
| - # u'Foo.framework/Versions/A/Resources/Something.nib') will return
|
| - # u'Foo.framework/Versions/A/resources/Something.nib', e.g. lowercase 'r'.
|
| - #
|
| - # Note that this is really something deep in OSX because running
|
| - # ls Foo.framework/Versions/A
|
| - # will print out 'Resources', while file_path.get_native_path_case()
|
| - # returns a lower case 'r'.
|
| - #
|
| - # So *something* is happening under the hood resulting in the command 'ls'
|
| - # and Carbon.File.FSPathMakeRef('path').FSRefMakePath() to disagree. We
|
| - # have no idea why.
|
| - if sys.platform != 'darwin':
|
| - raise isolateserver.MappingError(
|
| - 'File path doesn\'t equal native file path\n%s != %s' %
|
| - (filepath, native_filepath))
|
| -
|
| - symlinks = []
|
| - if follow_symlinks:
|
| - relfile, symlinks = expand_symlinks(indir, relfile)
|
| -
|
| - if relfile.endswith(os.path.sep):
|
| - if not os.path.isdir(infile):
|
| - raise isolateserver.MappingError(
|
| - '%s is not a directory but ends with "%s"' % (infile, os.path.sep))
|
| -
|
| - # Special case './'.
|
| - if relfile.startswith('.' + os.path.sep):
|
| - relfile = relfile[2:]
|
| - outfiles = symlinks
|
| - try:
|
| - for filename in os.listdir(infile):
|
| - inner_relfile = os.path.join(relfile, filename)
|
| - if blacklist(inner_relfile):
|
| - continue
|
| - if os.path.isdir(os.path.join(indir, inner_relfile)):
|
| - inner_relfile += os.path.sep
|
| - outfiles.extend(
|
| - expand_directory_and_symlink(indir, inner_relfile, blacklist,
|
| - follow_symlinks))
|
| - return outfiles
|
| - except OSError as e:
|
| - raise isolateserver.MappingError(
|
| - 'Unable to iterate over directory %s.\n%s' % (infile, e))
|
| - else:
|
| - # Always add individual files even if they were blacklisted.
|
| - if os.path.isdir(infile):
|
| - raise isolateserver.MappingError(
|
| - 'Input directory %s must have a trailing slash' % infile)
|
| -
|
| - if not os.path.isfile(infile):
|
| - raise isolateserver.MappingError(
|
| - 'Input file %s doesn\'t exist' % infile)
|
| -
|
| - return symlinks + [relfile]
|
| -
|
| -
|
| -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(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.
|
| -
|
| - Arguments:
|
| - outdir: Output directory to create the files in.
|
| - indir: Root directory the infiles are based in.
|
| - infiles: dict of files to map from |indir| to |outdir|.
|
| - action: One of accepted action of run_isolated.link_file().
|
| - as_hash: Output filename is the hash instead of relfile.
|
| - """
|
| - logging.info(
|
| - 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
|
| - (outdir, indir, len(infiles), action, as_hash))
|
| -
|
| - assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
|
| - if not os.path.isdir(outdir):
|
| - logging.info('Creating %s' % outdir)
|
| - os.makedirs(outdir)
|
| -
|
| - for relfile, metadata in infiles.iteritems():
|
| - infile = os.path.join(indir, relfile)
|
| - if as_hash:
|
| - # Do the hashtable specific checks.
|
| - if 'l' in metadata:
|
| - # Skip links when storing a hashtable.
|
| - continue
|
| - outfile = os.path.join(outdir, metadata['h'])
|
| - 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.
|
| - if not 's' in metadata:
|
| - raise isolateserver.MappingError(
|
| - 'Misconfigured item %s: %s' % (relfile, metadata))
|
| - if metadata['s'] == os.stat(outfile).st_size:
|
| - continue
|
| - else:
|
| - logging.warn('Overwritting %s' % metadata['h'])
|
| - os.remove(outfile)
|
| - else:
|
| - outfile = os.path.join(outdir, relfile)
|
| - outsubdir = os.path.dirname(outfile)
|
| - if not os.path.isdir(outsubdir):
|
| - os.makedirs(outsubdir)
|
| -
|
| - # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
|
| - # if metadata.get('T') == True:
|
| - # open(outfile, 'ab').close()
|
| - if 'l' in metadata:
|
| - pointed = metadata['l']
|
| - logging.debug('Symlink: %s -> %s' % (outfile, pointed))
|
| - # symlink doesn't exist on Windows.
|
| - os.symlink(pointed, outfile) # pylint: disable=E1101
|
| - else:
|
| - run_isolated.link_file(outfile, infile, action)
|
| -
|
| -
|
| -def process_input(filepath, prevdict, read_only, flavor, algo):
|
| - """Processes an input file, a dependency, and return meta data about it.
|
| -
|
| - Arguments:
|
| - - filepath: File to act on.
|
| - - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
|
| - to skip recalculating the hash.
|
| - - 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.
|
| - - algo: Hashing algorithm used.
|
| -
|
| - Behaviors:
|
| - - Retrieves the file mode, file size, file timestamp, file link
|
| - destination if it is a file link and calcultate the SHA-1 of the file's
|
| - content if the path points to a file and not a symlink.
|
| - """
|
| - out = {}
|
| - # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
|
| - # if prevdict.get('T') == True:
|
| - # # The file's content is ignored. Skip the time and hard code mode.
|
| - # if get_flavor() != 'win':
|
| - # out['m'] = stat.S_IRUSR | stat.S_IRGRP
|
| - # out['s'] = 0
|
| - # out['h'] = algo().hexdigest()
|
| - # out['T'] = True
|
| - # return out
|
| -
|
| - # Always check the file stat and check if it is a link. The timestamp is used
|
| - # to know if the file's content/symlink destination should be looked into.
|
| - # E.g. only reuse from prevdict if the timestamp hasn't changed.
|
| - # There is the risk of the file's timestamp being reset to its last value
|
| - # manually while its content changed. We don't protect against that use case.
|
| - try:
|
| - filestats = os.lstat(filepath)
|
| - except OSError:
|
| - # The file is not present.
|
| - raise isolateserver.MappingError('%s is missing' % filepath)
|
| - is_link = stat.S_ISLNK(filestats.st_mode)
|
| -
|
| - if flavor != 'win':
|
| - # Ignore file mode on Windows since it's not really useful there.
|
| - filemode = stat.S_IMODE(filestats.st_mode)
|
| - # Remove write access for group and all access to 'others'.
|
| - filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
|
| - if read_only:
|
| - filemode &= ~stat.S_IWUSR
|
| - if filemode & stat.S_IXUSR:
|
| - filemode |= stat.S_IXGRP
|
| - else:
|
| - filemode &= ~stat.S_IXGRP
|
| - if not is_link:
|
| - out['m'] = filemode
|
| -
|
| - # Used to skip recalculating the hash or link destination. Use the most recent
|
| - # update time.
|
| - # TODO(maruel): Save it in the .state file instead of .isolated so the
|
| - # .isolated file is deterministic.
|
| - out['t'] = int(round(filestats.st_mtime))
|
| -
|
| - if not is_link:
|
| - out['s'] = filestats.st_size
|
| - # If the timestamp wasn't updated and the file size is still the same, carry
|
| - # on the sha-1.
|
| - if (prevdict.get('t') == out['t'] and
|
| - prevdict.get('s') == out['s']):
|
| - # Reuse the previous hash if available.
|
| - out['h'] = prevdict.get('h')
|
| - if not out.get('h'):
|
| - out['h'] = isolateserver.hash_file(filepath, algo)
|
| - else:
|
| - # If the timestamp wasn't updated, carry on the link destination.
|
| - if prevdict.get('t') == out['t']:
|
| - # Reuse the previous link destination if available.
|
| - out['l'] = prevdict.get('l')
|
| - if out.get('l') is None:
|
| - # The link could be in an incorrect path case. In practice, this only
|
| - # happen on OSX on case insensitive HFS.
|
| - # TODO(maruel): It'd be better if it was only done once, in
|
| - # expand_directory_and_symlink(), so it would not be necessary to do again
|
| - # here.
|
| - symlink_value = os.readlink(filepath) # pylint: disable=E1101
|
| - filedir = file_path.get_native_path_case(os.path.dirname(filepath))
|
| - native_dest = fix_native_path_case(filedir, symlink_value)
|
| - out['l'] = os.path.relpath(native_dest, filedir)
|
| - return out
|
| -
|
| -
|
| -### Variable stuff.
|
| -
|
| -
|
| -def isolatedfile_to_state(filename):
|
| - """Replaces the file's extension."""
|
| - return filename + '.state'
|
| -
|
| -
|
| -def determine_root_dir(relative_root, infiles):
|
| - """For a list of infiles, determines the deepest root directory that is
|
| - referenced indirectly.
|
| -
|
| - 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, %d files) -> %s' % (
|
| - relative_root, len(infiles), deepest_root))
|
| - return deepest_root
|
| -
|
| -
|
| -def replace_variable(part, variables):
|
| - m = re.match(r'<\(([A-Z_]+)\)', part)
|
| - if m:
|
| - if m.group(1) not in variables:
|
| - raise ExecutionError(
|
| - 'Variable "%s" was not found in %s.\nDid you forget to specify '
|
| - '--variable?' % (m.group(1), variables))
|
| - return variables[m.group(1)]
|
| - return part
|
| -
|
| -
|
| -def process_variables(cwd, variables, relative_base_dir):
|
| - """Processes path variables as a special case and returns a copy of the dict.
|
| -
|
| - For each 'path' variable: first normalizes it based on |cwd|, verifies it
|
| - exists then sets it as relative to relative_base_dir.
|
| - """
|
| - relative_base_dir = file_path.get_native_path_case(relative_base_dir)
|
| - variables = variables.copy()
|
| - for i in PATH_VARIABLES:
|
| - if i not in variables:
|
| - continue
|
| - variable = variables[i].strip()
|
| - # Variables could contain / or \ on windows. Always normalize to
|
| - # os.path.sep.
|
| - variable = variable.replace('/', os.path.sep)
|
| - variable = os.path.join(cwd, variable)
|
| - variable = os.path.normpath(variable)
|
| - variable = file_path.get_native_path_case(variable)
|
| - if not os.path.isdir(variable):
|
| - raise ExecutionError('%s=%s is not a directory' % (i, variable))
|
| -
|
| - # All variables are relative to the .isolate file.
|
| - variable = os.path.relpath(variable, relative_base_dir)
|
| - logging.debug(
|
| - 'Translated variable %s from %s to %s', i, variables[i], variable)
|
| - variables[i] = variable
|
| - return variables
|
| -
|
| -
|
| -def eval_variables(item, variables):
|
| - """Replaces the .isolate variables in a string item.
|
| -
|
| - Note that the .isolate format is a subset of the .gyp dialect.
|
| - """
|
| - return ''.join(
|
| - replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
|
| -
|
| -
|
| -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.
|
| - """
|
| - # These directories are not guaranteed to be always present on every builder.
|
| - OPTIONAL_DIRECTORIES = (
|
| - 'test/data/plugin',
|
| - 'third_party/WebKit/LayoutTests',
|
| - )
|
| -
|
| - 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
|
| - if any(i in filepath for i in OPTIONAL_DIRECTORIES):
|
| - 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[KEY_TRACKED] = sorted(new_tracked)
|
| - if new_untracked:
|
| - variables[KEY_UNTRACKED] = sorted(new_untracked)
|
| - return variables
|
| -
|
| -
|
| -def chromium_fix(f, variables):
|
| - """Fixes an isolate dependnecy with Chromium-specific fixes."""
|
| - # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
|
| - # separator.
|
| - LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
|
| - # Ignored items.
|
| - IGNORED_ITEMS = (
|
| - # http://crbug.com/160539, on Windows, it's in chrome/.
|
| - 'Media Cache/',
|
| - 'chrome/Media Cache/',
|
| - # 'First Run' is not created by the compile, but by the test itself.
|
| - '<(PRODUCT_DIR)/First Run')
|
| -
|
| - # Blacklist logs and other unimportant files.
|
| - if LOG_FILE.match(f) or f in IGNORED_ITEMS:
|
| - 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, 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)' %
|
| - (len(tracked) + len(untracked) + len(touched),
|
| - root_dir, variables, relative_cwd))
|
| -
|
| - # Preparation work.
|
| - relative_cwd = cleanup_path(relative_cwd)
|
| - assert not os.path.isabs(relative_cwd), relative_cwd
|
| - # Creates the right set of variables here. We only care about PATH_VARIABLES.
|
| - path_variables = dict(
|
| - ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
|
| - for k in PATH_VARIABLES if k in variables)
|
| - variables = variables.copy()
|
| - variables.update(path_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 = posix_relpath(
|
| - posixpath.join(root_dir_posix, f),
|
| - posixpath.join(root_dir_posix, relative_cwd)) or './'
|
| -
|
| - for variable, root_path in path_variables.iteritems():
|
| - if f.startswith(root_path):
|
| - f = variable + f[len(root_path):]
|
| - 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, 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[KEY_TOUCHED] = sorted(touched)
|
| - return out
|
| -
|
| -
|
| -def chromium_filter_flags(variables):
|
| - """Filters out build flags used in Chromium that we don't want to treat as
|
| - configuration variables.
|
| - """
|
| - # TODO(benrg): Need a better way to determine this.
|
| - blacklist = set(PATH_VARIABLES + ('EXECUTABLE_SUFFIX', 'FLAG'))
|
| - return dict((k, v) for k, v in variables.iteritems() if k not in blacklist)
|
| -
|
| -
|
| -def generate_isolate(
|
| - tracked, untracked, touched, root_dir, variables, relative_cwd,
|
| - trace_blacklist):
|
| - """Generates a clean and complete .isolate file."""
|
| - dependencies = generate_simplified(
|
| - tracked, untracked, touched, root_dir, variables, relative_cwd,
|
| - trace_blacklist)
|
| - config_variables = chromium_filter_flags(variables)
|
| - config_variable_names, config_values = zip(
|
| - *sorted(config_variables.iteritems()))
|
| - out = Configs(None)
|
| - # The new dependencies apply to just one configuration, namely config_values.
|
| - out.merge_dependencies(dependencies, config_variable_names, [config_values])
|
| - return out.make_isolate_file()
|
| -
|
| -
|
| -def split_touched(files):
|
| - """Splits files that are touched vs files that are read."""
|
| - tracked = []
|
| - touched = []
|
| - for f in files:
|
| - if f.size:
|
| - tracked.append(f)
|
| - else:
|
| - touched.append(f)
|
| - return tracked, touched
|
| -
|
| -
|
| -def pretty_print(variables, stdout):
|
| - """Outputs a gyp compatible list from the decoded variables.
|
| -
|
| - Similar to pprint.print() but with NIH syndrome.
|
| - """
|
| - # Order the dictionary keys by these keys in priority.
|
| - ORDER = (
|
| - 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
|
| - KEY_TRACKED, KEY_UNTRACKED)
|
| -
|
| - def sorting_key(x):
|
| - """Gives priority to 'most important' keys before the others."""
|
| - if x in ORDER:
|
| - return str(ORDER.index(x))
|
| - return x
|
| -
|
| - def loop_list(indent, items):
|
| - for item in items:
|
| - if isinstance(item, basestring):
|
| - stdout.write('%s\'%s\',\n' % (indent, item))
|
| - elif isinstance(item, dict):
|
| - stdout.write('%s{\n' % indent)
|
| - loop_dict(indent + ' ', item)
|
| - stdout.write('%s},\n' % indent)
|
| - elif isinstance(item, list):
|
| - # A list inside a list will write the first item embedded.
|
| - stdout.write('%s[' % indent)
|
| - for index, i in enumerate(item):
|
| - if isinstance(i, basestring):
|
| - stdout.write(
|
| - '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
|
| - elif isinstance(i, dict):
|
| - stdout.write('{\n')
|
| - loop_dict(indent + ' ', i)
|
| - if index != len(item) - 1:
|
| - x = ', '
|
| - else:
|
| - x = ''
|
| - stdout.write('%s}%s' % (indent, x))
|
| - else:
|
| - assert False
|
| - stdout.write('],\n')
|
| - else:
|
| - assert False
|
| -
|
| - def loop_dict(indent, items):
|
| - for key in sorted(items, key=sorting_key):
|
| - item = items[key]
|
| - stdout.write("%s'%s': " % (indent, key))
|
| - if isinstance(item, dict):
|
| - stdout.write('{\n')
|
| - loop_dict(indent + ' ', item)
|
| - stdout.write(indent + '},\n')
|
| - elif isinstance(item, list):
|
| - stdout.write('[\n')
|
| - loop_list(indent + ' ', item)
|
| - stdout.write(indent + '],\n')
|
| - elif isinstance(item, basestring):
|
| - stdout.write(
|
| - '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
|
| - elif item in (True, False, None):
|
| - stdout.write('%s\n' % item)
|
| - else:
|
| - assert False, item
|
| -
|
| - stdout.write('{\n')
|
| - loop_dict(' ', variables)
|
| - stdout.write('}\n')
|
| -
|
| -
|
| -def union(lhs, rhs):
|
| - """Merges two compatible datastructures composed of dict/list/set."""
|
| - assert lhs is not None or rhs is not None
|
| - if lhs is None:
|
| - return copy.deepcopy(rhs)
|
| - if rhs is None:
|
| - return copy.deepcopy(lhs)
|
| - assert type(lhs) == type(rhs), (lhs, rhs)
|
| - if hasattr(lhs, 'union'):
|
| - # Includes set, ConfigSettings and Configs.
|
| - return lhs.union(rhs)
|
| - if isinstance(lhs, dict):
|
| - return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
|
| - elif isinstance(lhs, list):
|
| - # Do not go inside the list.
|
| - return lhs + rhs
|
| - assert False, type(lhs)
|
| -
|
| -
|
| -def extract_comment(content):
|
| - """Extracts file level comment."""
|
| - out = []
|
| - for line in content.splitlines(True):
|
| - if line.startswith('#'):
|
| - out.append(line)
|
| - else:
|
| - break
|
| - return ''.join(out)
|
| -
|
| -
|
| -def eval_content(content):
|
| - """Evaluates a python file and return the value defined in it.
|
| -
|
| - Used in practice for .isolate files.
|
| - """
|
| - globs = {'__builtins__': None}
|
| - locs = {}
|
| - try:
|
| - value = eval(content, globs, locs)
|
| - except TypeError as e:
|
| - e.args = list(e.args) + [content]
|
| - raise
|
| - assert locs == {}, locs
|
| - assert globs == {'__builtins__': None}, globs
|
| - return value
|
| -
|
| -
|
| -def match_configs(expr, config_variables, all_configs):
|
| - """Returns the configs from |all_configs| that match the |expr|, where
|
| - the elements of |all_configs| are tuples of values for the |config_variables|.
|
| - Example:
|
| - >>> match_configs(expr = "(foo==1 or foo==2) and bar=='b'",
|
| - config_variables = ["foo", "bar"],
|
| - all_configs = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
|
| - [(1, 'b'), (2, 'b')]
|
| - """
|
| - return [
|
| - config for config in all_configs
|
| - if eval(expr, dict(zip(config_variables, config)))
|
| - ]
|
| -
|
| -
|
| -def verify_variables(variables):
|
| - """Verifies the |variables| dictionary is in the expected format."""
|
| - VALID_VARIABLES = [
|
| - KEY_TOUCHED,
|
| - KEY_TRACKED,
|
| - KEY_UNTRACKED,
|
| - 'command',
|
| - 'read_only',
|
| - ]
|
| - assert isinstance(variables, dict), variables
|
| - assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
|
| - for name, value in variables.iteritems():
|
| - if name == 'read_only':
|
| - assert value in (True, False, None), value
|
| - else:
|
| - assert isinstance(value, list), value
|
| - assert all(isinstance(i, basestring) for i in value), value
|
| -
|
| -
|
| -def verify_ast(expr, variables_and_values):
|
| - """Verifies that |expr| is of the form
|
| - expr ::= expr ( "or" | "and" ) expr
|
| - | identifier "==" ( string | int )
|
| - Also collects the variable identifiers and string/int values in the dict
|
| - |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
|
| - """
|
| - assert isinstance(expr, (ast.BoolOp, ast.Compare))
|
| - if isinstance(expr, ast.BoolOp):
|
| - assert isinstance(expr.op, (ast.And, ast.Or))
|
| - for subexpr in expr.values:
|
| - verify_ast(subexpr, variables_and_values)
|
| - else:
|
| - assert isinstance(expr.left.ctx, ast.Load)
|
| - assert len(expr.ops) == 1
|
| - assert isinstance(expr.ops[0], ast.Eq)
|
| - var_values = variables_and_values.setdefault(expr.left.id, set())
|
| - rhs = expr.comparators[0]
|
| - assert isinstance(rhs, (ast.Str, ast.Num))
|
| - var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
|
| -
|
| -
|
| -def verify_condition(condition, variables_and_values):
|
| - """Verifies the |condition| dictionary is in the expected format.
|
| - See verify_ast() for the meaning of |variables_and_values|.
|
| - """
|
| - VALID_INSIDE_CONDITION = ['variables']
|
| - assert isinstance(condition, list), condition
|
| - assert len(condition) == 2, condition
|
| - expr, then = condition
|
| -
|
| - test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
|
| - verify_ast(test_ast.body, variables_and_values)
|
| -
|
| - assert isinstance(then, dict), then
|
| - assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
|
| - verify_variables(then['variables'])
|
| -
|
| -
|
| -def verify_root(value, variables_and_values):
|
| - """Verifies that |value| is the parsed form of a valid .isolate file.
|
| - See verify_ast() for the meaning of |variables_and_values|.
|
| - """
|
| - VALID_ROOTS = ['includes', 'conditions']
|
| - assert isinstance(value, dict), value
|
| - assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
|
| -
|
| - includes = value.get('includes', [])
|
| - assert isinstance(includes, list), includes
|
| - for include in includes:
|
| - assert isinstance(include, basestring), include
|
| -
|
| - conditions = value.get('conditions', [])
|
| - assert isinstance(conditions, list), conditions
|
| - for condition in conditions:
|
| - verify_condition(condition, variables_and_values)
|
| -
|
| -
|
| -def remove_weak_dependencies(values, key, item, item_configs):
|
| - """Removes any configs from this key if the item is already under a
|
| - strong key.
|
| - """
|
| - if key == KEY_TOUCHED:
|
| - item_configs = set(item_configs)
|
| - for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
|
| - try:
|
| - item_configs -= values[stronger_key][item]
|
| - except KeyError:
|
| - pass
|
| -
|
| - return item_configs
|
| -
|
| -
|
| -def remove_repeated_dependencies(folders, key, item, item_configs):
|
| - """Removes any configs from this key if the item is in a folder that is
|
| - already included."""
|
| -
|
| - if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
|
| - item_configs = set(item_configs)
|
| - for (folder, configs) in folders.iteritems():
|
| - if folder != item and item.startswith(folder):
|
| - item_configs -= configs
|
| -
|
| - return item_configs
|
| -
|
| -
|
| -def get_folders(values_dict):
|
| - """Returns a dict of all the folders in the given value_dict."""
|
| - return dict(
|
| - (item, configs) for (item, configs) in values_dict.iteritems()
|
| - if item.endswith('/')
|
| - )
|
| -
|
| -
|
| -def invert_map(variables):
|
| - """Converts {config: {deptype: list(depvals)}} to
|
| - {deptype: {depval: set(configs)}}.
|
| - """
|
| - KEYS = (
|
| - KEY_TOUCHED,
|
| - KEY_TRACKED,
|
| - KEY_UNTRACKED,
|
| - 'command',
|
| - 'read_only',
|
| - )
|
| - out = dict((key, {}) for key in KEYS)
|
| - for config, values in variables.iteritems():
|
| - for key in KEYS:
|
| - if key == 'command':
|
| - items = [tuple(values[key])] if key in values else []
|
| - elif key == 'read_only':
|
| - items = [values[key]] if key in values else []
|
| - else:
|
| - assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
|
| - items = values.get(key, [])
|
| - for item in items:
|
| - out[key].setdefault(item, set()).add(config)
|
| - return out
|
| -
|
| -
|
| -def reduce_inputs(values):
|
| - """Reduces the output of invert_map() to the strictest minimum list.
|
| -
|
| - Looks at each individual file and directory, maps where they are used and
|
| - reconstructs the inverse dictionary.
|
| -
|
| - Returns the minimized dictionary.
|
| - """
|
| - KEYS = (
|
| - KEY_TOUCHED,
|
| - KEY_TRACKED,
|
| - KEY_UNTRACKED,
|
| - 'command',
|
| - 'read_only',
|
| - )
|
| -
|
| - # Folders can only live in KEY_UNTRACKED.
|
| - folders = get_folders(values.get(KEY_UNTRACKED, {}))
|
| -
|
| - out = dict((key, {}) for key in KEYS)
|
| - for key in KEYS:
|
| - for item, item_configs in values.get(key, {}).iteritems():
|
| - item_configs = remove_weak_dependencies(values, key, item, item_configs)
|
| - item_configs = remove_repeated_dependencies(
|
| - folders, key, item, item_configs)
|
| - if item_configs:
|
| - out[key][item] = item_configs
|
| - return out
|
| -
|
| -
|
| -def convert_map_to_isolate_dict(values, config_variables):
|
| - """Regenerates back a .isolate configuration dict from files and dirs
|
| - mappings generated from reduce_inputs().
|
| - """
|
| - # Gather a list of configurations for set inversion later.
|
| - all_mentioned_configs = set()
|
| - for configs_by_item in values.itervalues():
|
| - for configs in configs_by_item.itervalues():
|
| - all_mentioned_configs.update(configs)
|
| -
|
| - # Invert the mapping to make it dict first.
|
| - conditions = {}
|
| - for key in values:
|
| - for item, configs in values[key].iteritems():
|
| - then = conditions.setdefault(frozenset(configs), {})
|
| - variables = then.setdefault('variables', {})
|
| -
|
| - if item in (True, False):
|
| - # One-off for read_only.
|
| - variables[key] = item
|
| - else:
|
| - assert item
|
| - if isinstance(item, tuple):
|
| - # One-off for command.
|
| - # Do not merge lists and do not sort!
|
| - # Note that item is a tuple.
|
| - assert key not in variables
|
| - variables[key] = list(item)
|
| - else:
|
| - # The list of items (files or dirs). Append the new item and keep
|
| - # the list sorted.
|
| - l = variables.setdefault(key, [])
|
| - l.append(item)
|
| - l.sort()
|
| -
|
| - if all_mentioned_configs:
|
| - config_values = map(set, zip(*all_mentioned_configs))
|
| - sef = short_expression_finder.ShortExpressionFinder(
|
| - zip(config_variables, config_values))
|
| -
|
| - conditions = sorted(
|
| - [sef.get_expr(configs), then] for configs, then in conditions.iteritems())
|
| - return {'conditions': conditions}
|
| -
|
| -
|
| -### Internal state files.
|
| -
|
| -
|
| -class ConfigSettings(object):
|
| - """Represents the dependency variables for a single build configuration.
|
| - The structure is immutable.
|
| - """
|
| - def __init__(self, config, values):
|
| - self.config = config
|
| - verify_variables(values)
|
| - self.touched = sorted(values.get(KEY_TOUCHED, []))
|
| - self.tracked = sorted(values.get(KEY_TRACKED, []))
|
| - self.untracked = sorted(values.get(KEY_UNTRACKED, []))
|
| - self.command = values.get('command', [])[:]
|
| - self.read_only = values.get('read_only')
|
| -
|
| - def union(self, rhs):
|
| - assert not (self.config and rhs.config) or (self.config == rhs.config)
|
| - assert not (self.command and rhs.command) or (self.command == rhs.command)
|
| - var = {
|
| - KEY_TOUCHED: sorted(self.touched + rhs.touched),
|
| - KEY_TRACKED: sorted(self.tracked + rhs.tracked),
|
| - KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
|
| - 'command': self.command or rhs.command,
|
| - 'read_only': rhs.read_only if self.read_only is None else self.read_only,
|
| - }
|
| - return ConfigSettings(self.config or rhs.config, var)
|
| -
|
| - def flatten(self):
|
| - out = {}
|
| - if self.command:
|
| - out['command'] = self.command
|
| - if self.touched:
|
| - out[KEY_TOUCHED] = self.touched
|
| - if self.tracked:
|
| - out[KEY_TRACKED] = self.tracked
|
| - if self.untracked:
|
| - out[KEY_UNTRACKED] = self.untracked
|
| - if self.read_only is not None:
|
| - out['read_only'] = self.read_only
|
| - return out
|
| -
|
| -
|
| -class Configs(object):
|
| - """Represents a processed .isolate file.
|
| -
|
| - Stores the file in a processed way, split by configuration.
|
| - """
|
| - def __init__(self, file_comment):
|
| - self.file_comment = file_comment
|
| - # The keys of by_config are tuples of values for the configuration
|
| - # variables. The names of the variables (which must be the same for
|
| - # every by_config key) are kept in config_variables. Initially by_config
|
| - # is empty and we don't know what configuration variables will be used,
|
| - # so config_variables also starts out empty. It will be set by the first
|
| - # call to union() or merge_dependencies().
|
| - self.by_config = {}
|
| - self.config_variables = ()
|
| -
|
| - def union(self, rhs):
|
| - """Adds variables from rhs (a Configs) to the existing variables.
|
| - """
|
| - config_variables = self.config_variables
|
| - if not config_variables:
|
| - config_variables = rhs.config_variables
|
| - else:
|
| - # We can't proceed if this isn't true since we don't know the correct
|
| - # default values for extra variables. The variables are sorted so we
|
| - # don't need to worry about permutations.
|
| - if rhs.config_variables and rhs.config_variables != config_variables:
|
| - raise ExecutionError(
|
| - 'Variables in merged .isolate files do not match: %r and %r' % (
|
| - config_variables, rhs.config_variables))
|
| -
|
| - # Takes the first file comment, prefering lhs.
|
| - out = Configs(self.file_comment or rhs.file_comment)
|
| - out.config_variables = config_variables
|
| - for config in set(self.by_config) | set(rhs.by_config):
|
| - out.by_config[config] = union(
|
| - self.by_config.get(config), rhs.by_config.get(config))
|
| - return out
|
| -
|
| - def merge_dependencies(self, values, config_variables, configs):
|
| - """Adds new dependencies to this object for the given configurations.
|
| - Arguments:
|
| - values: A variables dict as found in a .isolate file, e.g.,
|
| - {KEY_TOUCHED: [...], 'command': ...}.
|
| - config_variables: An ordered list of configuration variables, e.g.,
|
| - ["OS", "chromeos"]. If this object already contains any dependencies,
|
| - the configuration variables must match.
|
| - configs: a list of tuples of values of the configuration variables,
|
| - e.g., [("mac", 0), ("linux", 1)]. The dependencies in |values|
|
| - are added to all of these configurations, and other configurations
|
| - are unchanged.
|
| - """
|
| - if not values:
|
| - return
|
| -
|
| - if not self.config_variables:
|
| - self.config_variables = config_variables
|
| - else:
|
| - # See comment in Configs.union().
|
| - assert self.config_variables == config_variables
|
| -
|
| - for config in configs:
|
| - self.by_config[config] = union(
|
| - self.by_config.get(config), ConfigSettings(config, values))
|
| -
|
| - def flatten(self):
|
| - """Returns a flat dictionary representation of the configuration.
|
| - """
|
| - return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
|
| -
|
| - def make_isolate_file(self):
|
| - """Returns a dictionary suitable for writing to a .isolate file.
|
| - """
|
| - dependencies_by_config = self.flatten()
|
| - configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
|
| - return convert_map_to_isolate_dict(configs_by_dependency,
|
| - self.config_variables)
|
| -
|
| -
|
| -# TODO(benrg): Remove this function when no old-format files are left.
|
| -def convert_old_to_new_format(value):
|
| - """Converts from the old .isolate format, which only has one variable (OS),
|
| - always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
|
| - and allows conditions that depend on the set of all OSes, to the new format,
|
| - which allows any set of variables, has no hardcoded values, and only allows
|
| - explicit positive tests of variable values.
|
| - """
|
| - conditions = value.get('conditions', [])
|
| - if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
|
| - return value # Nothing to change
|
| -
|
| - def parse_condition(cond):
|
| - return re.match(r'OS=="(\w+)"\Z', cond[0]).group(1)
|
| -
|
| - oses = set(map(parse_condition, conditions))
|
| - default_oses = set(['linux', 'mac', 'win'])
|
| - oses = sorted(oses | default_oses)
|
| -
|
| - def if_not_os(not_os, then):
|
| - expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
|
| - return [expr, then]
|
| -
|
| - conditions = [
|
| - cond[:2] for cond in conditions if cond[1]
|
| - ] + [
|
| - if_not_os(parse_condition(cond), cond[2])
|
| - for cond in conditions if len(cond) == 3
|
| - ]
|
| -
|
| - if 'variables' in value:
|
| - conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
|
| - conditions.sort()
|
| -
|
| - value = value.copy()
|
| - value['conditions'] = conditions
|
| - return value
|
| -
|
| -
|
| -def load_isolate_as_config(isolate_dir, value, file_comment):
|
| - """Parses one .isolate file and returns a Configs() instance.
|
| -
|
| - |value| is the loaded dictionary that was defined in the gyp file.
|
| -
|
| - The expected format is strict, anything diverting from the format below will
|
| - throw an assert:
|
| - {
|
| - 'includes': [
|
| - 'foo.isolate',
|
| - ],
|
| - 'conditions': [
|
| - ['OS=="vms" and foo=42', {
|
| - 'variables': {
|
| - 'command': [
|
| - ...
|
| - ],
|
| - 'isolate_dependency_tracked': [
|
| - ...
|
| - ],
|
| - 'isolate_dependency_untracked': [
|
| - ...
|
| - ],
|
| - 'read_only': False,
|
| - },
|
| - }],
|
| - ...
|
| - ],
|
| - }
|
| - """
|
| - value = convert_old_to_new_format(value)
|
| -
|
| - variables_and_values = {}
|
| - verify_root(value, variables_and_values)
|
| - if variables_and_values:
|
| - config_variables, config_values = zip(
|
| - *sorted(variables_and_values.iteritems()))
|
| - all_configs = list(itertools.product(*config_values))
|
| - else:
|
| - config_variables = None
|
| - all_configs = []
|
| -
|
| - isolate = Configs(file_comment)
|
| -
|
| - # Add configuration-specific variables.
|
| - for expr, then in value.get('conditions', []):
|
| - configs = match_configs(expr, config_variables, all_configs)
|
| - isolate.merge_dependencies(then['variables'], config_variables, configs)
|
| -
|
| - # Load the includes.
|
| - for include in value.get('includes', []):
|
| - if os.path.isabs(include):
|
| - raise ExecutionError(
|
| - 'Failed to load configuration; absolute include path \'%s\'' %
|
| - include)
|
| - included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
|
| - with open(included_isolate, 'r') as f:
|
| - included_isolate = load_isolate_as_config(
|
| - os.path.dirname(included_isolate),
|
| - eval_content(f.read()),
|
| - None)
|
| - isolate = union(isolate, included_isolate)
|
| -
|
| - return isolate
|
| -
|
| -
|
| -def load_isolate_for_config(isolate_dir, content, variables):
|
| - """Loads the .isolate file and returns the information unprocessed but
|
| - filtered for the specific OS.
|
| -
|
| - Returns the command, dependencies and read_only flag. The dependencies are
|
| - fixed to use os.path.sep.
|
| - """
|
| - # Load the .isolate file, process its conditions, retrieve the command and
|
| - # dependencies.
|
| - isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
|
| - try:
|
| - config_name = tuple(variables[var] for var in isolate.config_variables)
|
| - except KeyError:
|
| - raise ExecutionError(
|
| - 'These configuration variables were missing from the command line: %s' %
|
| - ', '.join(sorted(set(isolate.config_variables) - set(variables))))
|
| - config = isolate.by_config.get(config_name)
|
| - if not config:
|
| - raise ExecutionError(
|
| - 'Failed to load configuration for variable \'%s\' for config(s) \'%s\''
|
| - '\nAvailable configs: %s' %
|
| - (', '.join(isolate.config_variables),
|
| - ', '.join(config_name),
|
| - ', '.join(str(s) for s in isolate.by_config)))
|
| - # Merge tracked and untracked variables, isolate.py doesn't care about the
|
| - # trackability of the variables, only the build tool does.
|
| - dependencies = [
|
| - f.replace('/', os.path.sep) for f in config.tracked + config.untracked
|
| - ]
|
| - touched = [f.replace('/', os.path.sep) for f in config.touched]
|
| - return config.command, dependencies, touched, config.read_only
|
| -
|
| -
|
| -def save_isolated(isolated, data):
|
| - """Writes one or multiple .isolated files.
|
| -
|
| - Note: this reference implementation does not create child .isolated file so it
|
| - always returns an empty list.
|
| -
|
| - Returns the list of child isolated files that are included by |isolated|.
|
| - """
|
| - trace_inputs.write_json(isolated, data, True)
|
| - return []
|
| -
|
| -
|
| -def chromium_save_isolated(isolated, data, variables, algo):
|
| - """Writes one or many .isolated files.
|
| -
|
| - This slightly increases the cold cache cost but greatly reduce the warm cache
|
| - cost by splitting low-churn files off the master .isolated file. It also
|
| - reduces overall isolateserver memcache consumption.
|
| - """
|
| - slaves = []
|
| -
|
| - def extract_into_included_isolated(prefix):
|
| - new_slave = {
|
| - 'algo': data['algo'],
|
| - 'files': {},
|
| - 'os': data['os'],
|
| - 'version': data['version'],
|
| - }
|
| - for f in data['files'].keys():
|
| - if f.startswith(prefix):
|
| - new_slave['files'][f] = data['files'].pop(f)
|
| - if new_slave['files']:
|
| - slaves.append(new_slave)
|
| -
|
| - # Split test/data/ in its own .isolated file.
|
| - extract_into_included_isolated(os.path.join('test', 'data', ''))
|
| -
|
| - # Split everything out of PRODUCT_DIR in its own .isolated file.
|
| - if variables.get('PRODUCT_DIR'):
|
| - extract_into_included_isolated(variables['PRODUCT_DIR'])
|
| -
|
| - files = []
|
| - for index, f in enumerate(slaves):
|
| - slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
|
| - trace_inputs.write_json(slavepath, f, True)
|
| - data.setdefault('includes', []).append(
|
| - isolateserver.hash_file(slavepath, algo))
|
| - files.append(os.path.basename(slavepath))
|
| -
|
| - files.extend(save_isolated(isolated, data))
|
| - return files
|
| -
|
| -
|
| -class Flattenable(object):
|
| - """Represents data that can be represented as a json file."""
|
| - MEMBERS = ()
|
| -
|
| - def flatten(self):
|
| - """Returns a json-serializable version of itself.
|
| -
|
| - Skips None entries.
|
| - """
|
| - items = ((member, getattr(self, member)) for member in self.MEMBERS)
|
| - return dict((member, value) for member, value in items if value is not None)
|
| -
|
| - @classmethod
|
| - def load(cls, data, *args, **kwargs):
|
| - """Loads a flattened version."""
|
| - data = data.copy()
|
| - out = cls(*args, **kwargs)
|
| - for member in out.MEMBERS:
|
| - if member in data:
|
| - # Access to a protected member XXX of a client class
|
| - # pylint: disable=W0212
|
| - out._load_member(member, data.pop(member))
|
| - if data:
|
| - raise ValueError(
|
| - 'Found unexpected entry %s while constructing an object %s' %
|
| - (data, cls.__name__), data, cls.__name__)
|
| - return out
|
| -
|
| - def _load_member(self, member, value):
|
| - """Loads a member into self."""
|
| - setattr(self, member, value)
|
| -
|
| - @classmethod
|
| - def load_file(cls, filename, *args, **kwargs):
|
| - """Loads the data from a file or return an empty instance."""
|
| - try:
|
| - out = cls.load(trace_inputs.read_json(filename), *args, **kwargs)
|
| - logging.debug('Loaded %s(%s)', cls.__name__, filename)
|
| - except (IOError, ValueError) as e:
|
| - # On failure, loads the default instance.
|
| - out = cls(*args, **kwargs)
|
| - logging.warn('Failed to load %s: %s', filename, e)
|
| - return out
|
| -
|
| -
|
| -class SavedState(Flattenable):
|
| - """Describes the content of a .state file.
|
| -
|
| - This file caches the items calculated by this script and is used to increase
|
| - the performance of the script. This file is not loaded by run_isolated.py.
|
| - This file can always be safely removed.
|
| -
|
| - It is important to note that the 'files' dict keys are using native OS path
|
| - separator instead of '/' used in .isolate file.
|
| - """
|
| - MEMBERS = (
|
| - # Algorithm used to generate the hash. The only supported value is at the
|
| - # time of writting 'sha-1'.
|
| - 'algo',
|
| - # 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.
|
| - 'command',
|
| - # Cache of the files found so the next run can skip hash calculation.
|
| - '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',
|
| - # 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 file. Variables are saved so
|
| - # a user can use isolate.py after building and the GYP variables are still
|
| - # defined.
|
| - 'variables',
|
| - # Version of the file format in format 'major.minor'. Any non-breaking
|
| - # change must update minor. Any breaking change must update major.
|
| - 'version',
|
| - )
|
| -
|
| - def __init__(self, isolated_basedir):
|
| - """Creates an empty SavedState.
|
| -
|
| - |isolated_basedir| is the directory where the .isolated and .isolated.state
|
| - files are saved.
|
| - """
|
| - super(SavedState, self).__init__()
|
| - assert os.path.isabs(isolated_basedir), isolated_basedir
|
| - assert os.path.isdir(isolated_basedir), isolated_basedir
|
| - self.isolated_basedir = isolated_basedir
|
| -
|
| - # The default algorithm used.
|
| - self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
|
| - self.command = []
|
| - self.files = {}
|
| - self.isolate_file = None
|
| - self.child_isolated_files = []
|
| - self.read_only = None
|
| - self.relative_cwd = None
|
| - self.variables = {'OS': get_flavor()}
|
| - # The current version.
|
| - self.version = '1.0'
|
| -
|
| - def update(self, isolate_file, variables):
|
| - """Updates the saved state with new data to keep GYP variables and internal
|
| - reference to the original .isolate file.
|
| - """
|
| - assert os.path.isabs(isolate_file)
|
| - # Convert back to a relative path. On Windows, if the isolate and
|
| - # isolated files are on different drives, isolate_file will stay an absolute
|
| - # path.
|
| - isolate_file = safe_relpath(isolate_file, self.isolated_basedir)
|
| -
|
| - # The same .isolate file should always be used to generate the .isolated and
|
| - # .isolated.state.
|
| - assert isolate_file == self.isolate_file or not self.isolate_file, (
|
| - isolate_file, self.isolate_file)
|
| - self.isolate_file = isolate_file
|
| - self.variables.update(variables)
|
| -
|
| - def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
|
| - """Updates the saved state with data necessary to generate a .isolated file.
|
| -
|
| - The new files in |infiles| are added to self.files dict but their hash is
|
| - not calculated here.
|
| - """
|
| - self.command = command
|
| - # Add new files.
|
| - for f in infiles:
|
| - self.files.setdefault(f, {})
|
| - for f in touched:
|
| - self.files.setdefault(f, {})['T'] = True
|
| - # Prune extraneous files that are not a dependency anymore.
|
| - for f in set(self.files).difference(set(infiles).union(touched)):
|
| - del self.files[f]
|
| - if read_only is not None:
|
| - self.read_only = read_only
|
| - self.relative_cwd = relative_cwd
|
| -
|
| - def to_isolated(self):
|
| - """Creates a .isolated dictionary out of the saved state.
|
| -
|
| - https://code.google.com/p/swarming/wiki/IsolatedDesign
|
| - """
|
| - def strip(data):
|
| - """Returns a 'files' entry with only the whitelisted keys."""
|
| - return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
|
| -
|
| - out = {
|
| - 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
|
| - 'files': dict(
|
| - (filepath, strip(data)) for filepath, data in self.files.iteritems()),
|
| - 'os': self.variables['OS'],
|
| - 'version': self.version,
|
| - }
|
| - if self.command:
|
| - out['command'] = self.command
|
| - if self.read_only is not None:
|
| - out['read_only'] = self.read_only
|
| - if self.relative_cwd:
|
| - out['relative_cwd'] = self.relative_cwd
|
| - return out
|
| -
|
| - @property
|
| - def isolate_filepath(self):
|
| - """Returns the absolute path of self.isolate_file."""
|
| - return os.path.normpath(
|
| - os.path.join(self.isolated_basedir, self.isolate_file))
|
| -
|
| - # Arguments number differs from overridden method
|
| - @classmethod
|
| - def load(cls, data, isolated_basedir): # pylint: disable=W0221
|
| - """Special case loading to disallow different OS.
|
| -
|
| - It is not possible to load a .isolated.state files from a different OS, this
|
| - file is saved in OS-specific format.
|
| - """
|
| - out = super(SavedState, cls).load(data, isolated_basedir)
|
| - if 'os' in data:
|
| - out.variables['OS'] = data['os']
|
| -
|
| - # Converts human readable form back into the proper class type.
|
| - algo = data.get('algo', 'sha-1')
|
| - if not algo in isolateserver.SUPPORTED_ALGOS:
|
| - raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
|
| - out.algo = isolateserver.SUPPORTED_ALGOS[algo]
|
| -
|
| - # For example, 1.1 is guaranteed to be backward compatible with 1.0 code.
|
| - if not re.match(r'^(\d+)\.(\d+)$', out.version):
|
| - raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
|
| - if out.version.split('.', 1)[0] != '1':
|
| - raise isolateserver.ConfigError(
|
| - 'Unsupported version \'%s\'' % out.version)
|
| -
|
| - # The .isolate file must be valid. It could be absolute on Windows if the
|
| - # drive containing the .isolate and the drive containing the .isolated files
|
| - # differ.
|
| - assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
|
| - assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
|
| - return out
|
| -
|
| - def flatten(self):
|
| - """Makes sure 'algo' is in human readable form."""
|
| - out = super(SavedState, self).flatten()
|
| - out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
|
| - return out
|
| -
|
| - def __str__(self):
|
| - out = '%s(\n' % self.__class__.__name__
|
| - out += ' command: %s\n' % self.command
|
| - out += ' files: %d\n' % len(self.files)
|
| - out += ' isolate_file: %s\n' % self.isolate_file
|
| - out += ' read_only: %s\n' % self.read_only
|
| - out += ' relative_cwd: %s\n' % self.relative_cwd
|
| - out += ' child_isolated_files: %s\n' % self.child_isolated_files
|
| - 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, isolated_filepath, saved_state):
|
| - super(CompleteState, self).__init__()
|
| - assert isolated_filepath is None or os.path.isabs(isolated_filepath)
|
| - self.isolated_filepath = isolated_filepath
|
| - # Contains the data to ease developer's use-case but that is not strictly
|
| - # necessary.
|
| - self.saved_state = saved_state
|
| -
|
| - @classmethod
|
| - def load_files(cls, isolated_filepath):
|
| - """Loads state from disk."""
|
| - assert os.path.isabs(isolated_filepath), isolated_filepath
|
| - isolated_basedir = os.path.dirname(isolated_filepath)
|
| - return cls(
|
| - isolated_filepath,
|
| - SavedState.load_file(
|
| - isolatedfile_to_state(isolated_filepath), isolated_basedir))
|
| -
|
| - def load_isolate(self, cwd, isolate_file, variables, ignore_broken_items):
|
| - """Updates self.isolated 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
|
| - isolate_file = file_path.get_native_path_case(isolate_file)
|
| - logging.info(
|
| - 'CompleteState.load_isolate(%s, %s, %s, %s)',
|
| - cwd, isolate_file, variables, ignore_broken_items)
|
| - relative_base_dir = os.path.dirname(isolate_file)
|
| -
|
| - # Processes the variables and update the saved state.
|
| - variables = process_variables(cwd, variables, relative_base_dir)
|
| - self.saved_state.update(isolate_file, variables)
|
| - variables = self.saved_state.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 = load_isolate_for_config(
|
| - os.path.dirname(isolate_file), f.read(), variables)
|
| - command = [eval_variables(i, variables) for i in command]
|
| - infiles = [eval_variables(f, variables) for f in infiles]
|
| - touched = [eval_variables(f, variables) for f in touched]
|
| - # 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.
|
| - path_variables = [variables[v] for v in PATH_VARIABLES if v in variables]
|
| - root_dir = determine_root_dir(
|
| - relative_base_dir, infiles + touched + path_variables)
|
| - # 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)
|
| - # Now that we know where the root is, check that the PATH_VARIABLES point
|
| - # inside it.
|
| - for i in PATH_VARIABLES:
|
| - if i in variables:
|
| - if not path_starts_with(
|
| - root_dir, os.path.join(relative_base_dir, variables[i])):
|
| - raise isolateserver.MappingError(
|
| - 'Path variable %s=%r points outside the inferred root directory'
|
| - ' %s' % (i, variables[i], 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
|
| - ]
|
| - touched = [
|
| - relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
|
| - for f in touched
|
| - ]
|
| - follow_symlinks = variables['OS'] != 'win'
|
| - # 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,
|
| - lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
|
| - follow_symlinks,
|
| - ignore_broken_items)
|
| -
|
| - # If we ignore broken items then remove any missing touched items.
|
| - if ignore_broken_items:
|
| - original_touched_count = len(touched)
|
| - touched = [touch for touch in touched if os.path.exists(touch)]
|
| -
|
| - if len(touched) != original_touched_count:
|
| - logging.info('Removed %d invalid touched entries',
|
| - len(touched) - original_touched_count)
|
| -
|
| - # Finally, update the new data to be able to generate the foo.isolated file,
|
| - # the file that is used by run_isolated.py.
|
| - self.saved_state.update_isolated(
|
| - command, infiles, touched, read_only, relative_cwd)
|
| - logging.debug(self)
|
| -
|
| - def process_inputs(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 process_input() 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] = process_input(
|
| - filepath,
|
| - self.saved_state.files[infile],
|
| - self.saved_state.read_only,
|
| - self.saved_state.variables['OS'],
|
| - self.saved_state.algo)
|
| -
|
| - def save_files(self):
|
| - """Saves self.saved_state and creates a .isolated file."""
|
| - logging.debug('Dumping to %s' % self.isolated_filepath)
|
| - self.saved_state.child_isolated_files = chromium_save_isolated(
|
| - self.isolated_filepath,
|
| - self.saved_state.to_isolated(),
|
| - self.saved_state.variables,
|
| - self.saved_state.algo)
|
| - total_bytes = sum(
|
| - i.get('s', 0) for i in self.saved_state.files.itervalues())
|
| - if total_bytes:
|
| - # TODO(maruel): Stats are missing the .isolated files.
|
| - logging.debug('Total size: %d bytes' % total_bytes)
|
| - saved_state_file = isolatedfile_to_state(self.isolated_filepath)
|
| - logging.debug('Dumping to %s' % saved_state_file)
|
| - trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
|
| -
|
| - @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)
|
| -
|
| - def __str__(self):
|
| - def indent(data, indent_length):
|
| - """Indents text."""
|
| - spacing = ' ' * indent_length
|
| - return ''.join(spacing + l for l in str(data).splitlines(True))
|
| -
|
| - out = '%s(\n' % self.__class__.__name__
|
| - out += ' root_dir: %s\n' % self.root_dir
|
| - out += ' saved_state: %s)' % indent(self.saved_state, 2)
|
| - return out
|
| -
|
| -
|
| -def load_complete_state(options, cwd, subdir, skip_update):
|
| - """Loads a CompleteState.
|
| -
|
| - This includes data from .isolate and .isolated.state files. Never reads the
|
| - .isolated file.
|
| -
|
| - Arguments:
|
| - options: Options instance generated with OptionParserIsolate. For either
|
| - options.isolate and options.isolated, if the value is set, it is an
|
| - absolute path.
|
| - cwd: base directory to be used when loading the .isolate file.
|
| - subdir: optional argument to only process file in the subdirectory, relative
|
| - to CompleteState.root_dir.
|
| - skip_update: Skip trying to load the .isolate file and processing the
|
| - dependencies. It is useful when not needed, like when tracing.
|
| - """
|
| - assert not options.isolate or os.path.isabs(options.isolate)
|
| - assert not options.isolated or os.path.isabs(options.isolated)
|
| - cwd = file_path.get_native_path_case(unicode(cwd))
|
| - if options.isolated:
|
| - # Load the previous state if it was present. Namely, "foo.isolated.state".
|
| - # Note: this call doesn't load the .isolate file.
|
| - complete_state = CompleteState.load_files(options.isolated)
|
| - else:
|
| - # Constructs a dummy object that cannot be saved. Useful for temporary
|
| - # commands like 'run'.
|
| - complete_state = CompleteState(None, SavedState())
|
| -
|
| - if not options.isolate:
|
| - if not complete_state.saved_state.isolate_file:
|
| - if not skip_update:
|
| - raise ExecutionError('A .isolate file is required.')
|
| - isolate = None
|
| - else:
|
| - isolate = complete_state.saved_state.isolate_filepath
|
| - else:
|
| - isolate = options.isolate
|
| - if complete_state.saved_state.isolate_file:
|
| - rel_isolate = safe_relpath(
|
| - options.isolate, complete_state.saved_state.isolated_basedir)
|
| - if rel_isolate != complete_state.saved_state.isolate_file:
|
| - raise ExecutionError(
|
| - '%s and %s do not match.' % (
|
| - options.isolate, complete_state.saved_state.isolate_file))
|
| -
|
| - if not skip_update:
|
| - # Then load the .isolate and expands directories.
|
| - complete_state.load_isolate(
|
| - cwd, isolate, options.variables, options.ignore_broken_items)
|
| -
|
| - # Regenerate complete_state.saved_state.files.
|
| - if subdir:
|
| - subdir = unicode(subdir)
|
| - subdir = eval_variables(subdir, complete_state.saved_state.variables)
|
| - subdir = subdir.replace('/', os.path.sep)
|
| -
|
| - if not skip_update:
|
| - complete_state.process_inputs(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 = split_touched(files)
|
| - value = generate_isolate(
|
| - tracked,
|
| - [],
|
| - touched,
|
| - complete_state.root_dir,
|
| - complete_state.saved_state.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 print_all(comment, data, stream):
|
| - """Prints a complete .isolate file and its top-level file comment into a
|
| - stream.
|
| - """
|
| - if comment:
|
| - stream.write(comment)
|
| - pretty_print(data, stream)
|
| -
|
| -
|
| -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 = load_isolate_as_config(
|
| - isolate_dir,
|
| - eval_content(prev_content),
|
| - extract_comment(prev_content))
|
| - new_config = load_isolate_as_config(isolate_dir, value, '')
|
| - config = 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:
|
| - 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]
|
| -
|
| -
|
| -### Commands.
|
| -
|
| -
|
| -def CMDarchive(parser, args):
|
| - """Creates a .isolated file and uploads the tree to an isolate server.
|
| -
|
| - All the files listed in the .isolated file are put in the isolate server
|
| - cache via isolateserver.py.
|
| - """
|
| - parser.add_option('--subdir', help='Filters to a subdirectory')
|
| - options, args = parser.parse_args(args)
|
| - if args:
|
| - parser.error('Unsupported argument: %s' % args)
|
| -
|
| - with tools.Profiler('GenerateHashtable'):
|
| - success = False
|
| - try:
|
| - complete_state = load_complete_state(
|
| - options, os.getcwd(), options.subdir, False)
|
| - if not options.outdir:
|
| - options.outdir = os.path.join(
|
| - os.path.dirname(complete_state.isolated_filepath), 'hashtable')
|
| - # Make sure that complete_state isn't modified until save_files() is
|
| - # called, because any changes made to it here will propagate to the files
|
| - # created (which is probably not intended).
|
| - complete_state.save_files()
|
| -
|
| - infiles = complete_state.saved_state.files
|
| - # Add all the .isolated files.
|
| - isolated_hash = []
|
| - isolated_files = [
|
| - options.isolated,
|
| - ] + complete_state.saved_state.child_isolated_files
|
| - 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
|
| - # likely smallish (under 500kb) and its file size is needed.
|
| - with open(item_path, 'rb') as f:
|
| - content = f.read()
|
| - isolated_hash.append(
|
| - complete_state.saved_state.algo(content).hexdigest())
|
| - isolated_metadata = {
|
| - 'h': isolated_hash[-1],
|
| - 's': len(content),
|
| - 'priority': '0'
|
| - }
|
| - infiles[item_path] = isolated_metadata
|
| -
|
| - logging.info('Creating content addressed object store with %d item',
|
| - len(infiles))
|
| -
|
| - if is_url(options.outdir):
|
| - isolateserver.upload_tree(
|
| - base_url=options.outdir,
|
| - indir=complete_state.root_dir,
|
| - infiles=infiles,
|
| - namespace='default-gzip')
|
| - else:
|
| - 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 not success
|
| -
|
| -
|
| -def CMDcheck(parser, args):
|
| - """Checks that all the inputs are present and generates .isolated."""
|
| - parser.add_option('--subdir', help='Filters to a subdirectory')
|
| - options, args = parser.parse_args(args)
|
| - if args:
|
| - parser.error('Unsupported argument: %s' % args)
|
| -
|
| - complete_state = load_complete_state(
|
| - options, os.getcwd(), options.subdir, False)
|
| -
|
| - # Nothing is done specifically. Just store the result and state.
|
| - complete_state.save_files()
|
| - return 0
|
| -
|
| -
|
| -CMDhashtable = CMDarchive
|
| -
|
| -
|
| -def CMDmerge(parser, args):
|
| - """Reads and merges the data from the trace back into the original .isolate.
|
| -
|
| - Ignores --outdir.
|
| - """
|
| - 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 = trace_inputs.gen_blacklist(options.trace_blacklist)
|
| - merge(complete_state, blacklist)
|
| - return 0
|
| -
|
| -
|
| -def CMDread(parser, args):
|
| - """Reads the trace file generated with command 'trace'.
|
| -
|
| - Ignores --outdir.
|
| - """
|
| - parser.require_isolated = False
|
| - add_trace_option(parser)
|
| - parser.add_option(
|
| - '--skip-refresh', action='store_true',
|
| - help='Skip reading .isolate file and do not refresh the hash of '
|
| - 'dependencies')
|
| - 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 = trace_inputs.gen_blacklist(options.trace_blacklist)
|
| - value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
|
| - if options.merge:
|
| - merge(complete_state, blacklist)
|
| - else:
|
| - 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.
|
| -
|
| - Useful to test manually why a test is failing. The target executable is not
|
| - run.
|
| - """
|
| - parser.require_isolated = False
|
| - options, args = parser.parse_args(args)
|
| - if args:
|
| - parser.error('Unsupported argument: %s' % args)
|
| - complete_state = load_complete_state(options, os.getcwd(), None, False)
|
| -
|
| - if not options.outdir:
|
| - options.outdir = run_isolated.make_temp_dir(
|
| - 'isolate', complete_state.root_dir)
|
| - else:
|
| - if is_url(options.outdir):
|
| - parser.error('Can\'t use url for --outdir with mode remap.')
|
| - if not os.path.isdir(options.outdir):
|
| - os.makedirs(options.outdir)
|
| - print('Remapping into %s' % options.outdir)
|
| - if len(os.listdir(options.outdir)):
|
| - raise ExecutionError('Can\'t remap in a non-empty directory')
|
| - recreate_tree(
|
| - outdir=options.outdir,
|
| - indir=complete_state.root_dir,
|
| - infiles=complete_state.saved_state.files,
|
| - action=run_isolated.HARDLINK_WITH_FALLBACK,
|
| - as_hash=False)
|
| - if complete_state.saved_state.read_only:
|
| - run_isolated.make_writable(options.outdir, True)
|
| -
|
| - if complete_state.isolated_filepath:
|
| - complete_state.save_files()
|
| - return 0
|
| -
|
| -
|
| -def CMDrewrite(parser, args):
|
| - """Rewrites a .isolate file into the canonical format."""
|
| - parser.require_isolated = False
|
| - options, args = parser.parse_args(args)
|
| - if args:
|
| - parser.error('Unsupported argument: %s' % args)
|
| -
|
| - if options.isolated:
|
| - # Load the previous state if it was present. Namely, "foo.isolated.state".
|
| - complete_state = CompleteState.load_files(options.isolated)
|
| - isolate = options.isolate or complete_state.saved_state.isolate_filepath
|
| - else:
|
| - isolate = options.isolate
|
| - if not isolate:
|
| - parser.error('--isolate is required.')
|
| -
|
| - with open(isolate, 'r') as f:
|
| - content = f.read()
|
| - config = load_isolate_as_config(
|
| - os.path.dirname(os.path.abspath(isolate)),
|
| - eval_content(content),
|
| - extract_comment(content))
|
| - data = config.make_isolate_file()
|
| - print('Updating %s' % isolate)
|
| - with open(isolate, 'wb') as f:
|
| - print_all(config.file_comment, data, f)
|
| - return 0
|
| -
|
| -
|
| -@subcommand.usage('-- [extra arguments]')
|
| -def CMDrun(parser, args):
|
| - """Runs the test executable in an isolated (temporary) directory.
|
| -
|
| - All the dependencies are mapped into the temporary directory and the
|
| - directory is cleaned up after the target exits. Warning: if --outdir is
|
| - specified, it is deleted upon exit.
|
| -
|
| - Argument processing stops at -- and these arguments are appended to the
|
| - command line of the target to run. For example, use:
|
| - isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
|
| - """
|
| - parser.require_isolated = False
|
| - parser.add_option(
|
| - '--skip-refresh', action='store_true',
|
| - help='Skip reading .isolate file and do not refresh the hash of '
|
| - 'dependencies')
|
| - options, args = parser.parse_args(args)
|
| - if options.outdir and is_url(options.outdir):
|
| - parser.error('Can\'t use url for --outdir with mode run.')
|
| -
|
| - 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)
|
| - try:
|
| - root_dir = complete_state.root_dir
|
| - if not options.outdir:
|
| - if not os.path.isabs(root_dir):
|
| - root_dir = os.path.join(os.path.dirname(options.isolated), root_dir)
|
| - options.outdir = run_isolated.make_temp_dir('isolate', root_dir)
|
| - else:
|
| - if not os.path.isdir(options.outdir):
|
| - os.makedirs(options.outdir)
|
| - recreate_tree(
|
| - outdir=options.outdir,
|
| - indir=root_dir,
|
| - infiles=complete_state.saved_state.files,
|
| - action=run_isolated.HARDLINK_WITH_FALLBACK,
|
| - as_hash=False)
|
| - cwd = os.path.normpath(
|
| - os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
|
| - if not os.path.isdir(cwd):
|
| - # It can happen when no files are mapped from the directory containing the
|
| - # .isolate file. But the directory must exist to be the current working
|
| - # directory.
|
| - os.makedirs(cwd)
|
| - if complete_state.saved_state.read_only:
|
| - run_isolated.make_writable(options.outdir, True)
|
| - logging.info('Running %s, cwd=%s' % (cmd, cwd))
|
| - result = subprocess.call(cmd, cwd=cwd)
|
| - finally:
|
| - if options.outdir:
|
| - run_isolated.rmtree(options.outdir)
|
| -
|
| - if complete_state.isolated_filepath:
|
| - complete_state.save_files()
|
| - 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')
|
| - parser.add_option(
|
| - '--skip-refresh', action='store_true',
|
| - help='Skip reading .isolate file and do not refresh the hash of '
|
| - 'dependencies')
|
| - 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 = trace_inputs.gen_blacklist(options.trace_blacklist)
|
| - merge(complete_state, blacklist)
|
| -
|
| - return result
|
| -
|
| -
|
| -def _process_variable_arg(_option, _opt, _value, parser):
|
| - if not parser.rargs:
|
| - raise optparse.OptionValueError(
|
| - 'Please use --variable FOO=BAR or --variable FOO BAR')
|
| - k = parser.rargs.pop(0)
|
| - if '=' in k:
|
| - parser.values.variables.append(tuple(k.split('=', 1)))
|
| - else:
|
| - if not parser.rargs:
|
| - raise optparse.OptionValueError(
|
| - 'Please use --variable FOO=BAR or --variable FOO BAR')
|
| - v = parser.rargs.pop(0)
|
| - parser.values.variables.append((k, v))
|
| -
|
| -
|
| -def add_variable_option(parser):
|
| - """Adds --isolated and --variable to an OptionParser."""
|
| - parser.add_option(
|
| - '-s', '--isolated',
|
| - metavar='FILE',
|
| - help='.isolated file to generate or read')
|
| - # Keep for compatibility. TODO(maruel): Remove once not used anymore.
|
| - parser.add_option(
|
| - '-r', '--result',
|
| - dest='isolated',
|
| - help=optparse.SUPPRESS_HELP)
|
| - default_variables = [('OS', get_flavor())]
|
| - if sys.platform in ('win32', 'cygwin'):
|
| - default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
|
| - else:
|
| - default_variables.append(('EXECUTABLE_SUFFIX', ''))
|
| - parser.add_option(
|
| - '-V', '--variable',
|
| - action='callback',
|
| - callback=_process_variable_arg,
|
| - default=default_variables,
|
| - dest='variables',
|
| - metavar='FOO BAR',
|
| - help='Variables to process in the .isolate file, default: %default. '
|
| - 'Variables are persistent accross calls, they are saved inside '
|
| - '<.isolated>.state')
|
| -
|
| -
|
| -def add_trace_option(parser):
|
| - """Adds --trace-blacklist to the parser."""
|
| - parser.add_option(
|
| - '--trace-blacklist',
|
| - action='append', default=list(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 parse_isolated_option(parser, options, cwd, require_isolated):
|
| - """Processes --isolated."""
|
| - if options.isolated:
|
| - options.isolated = os.path.normpath(
|
| - os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
|
| - if require_isolated and not options.isolated:
|
| - parser.error('--isolated is required.')
|
| - if options.isolated and not options.isolated.endswith('.isolated'):
|
| - parser.error('--isolated value must end with \'.isolated\'')
|
| -
|
| -
|
| -def parse_variable_option(options):
|
| - """Processes --variable."""
|
| - # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
|
| - # but it wouldn't be backward compatible.
|
| - def try_make_int(s):
|
| - """Converts a value to int if possible, converts to unicode otherwise."""
|
| - try:
|
| - return int(s)
|
| - except ValueError:
|
| - return s.decode('utf-8')
|
| - options.variables = dict((k, try_make_int(v)) for k, v in options.variables)
|
| -
|
| -
|
| -class OptionParserIsolate(tools.OptionParserWithLogging):
|
| - """Adds automatic --isolate, --isolated, --out and --variable handling."""
|
| - # Set it to False if it is not required, e.g. it can be passed on but do not
|
| - # fail if not given.
|
| - require_isolated = True
|
| -
|
| - def __init__(self, **kwargs):
|
| - tools.OptionParserWithLogging.__init__(
|
| - self,
|
| - verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
|
| - **kwargs)
|
| - group = optparse.OptionGroup(self, "Common options")
|
| - group.add_option(
|
| - '-i', '--isolate',
|
| - metavar='FILE',
|
| - help='.isolate file to load the dependency data from')
|
| - add_variable_option(group)
|
| - group.add_option(
|
| - '-o', '--outdir', metavar='DIR',
|
| - help='Directory used to recreate the tree or store the hash table. '
|
| - 'Defaults: run|remap: a /tmp subdirectory, others: '
|
| - 'defaults to the directory containing --isolated')
|
| - group.add_option(
|
| - '--ignore_broken_items', action='store_true',
|
| - default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
|
| - help='Indicates that invalid entries in the isolated file to be '
|
| - 'only be logged and not stop processing. Defaults to True if '
|
| - 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
|
| - self.add_option_group(group)
|
| -
|
| - def parse_args(self, *args, **kwargs):
|
| - """Makes sure the paths make sense.
|
| -
|
| - On Windows, / and \ are often mixed together in a path.
|
| - """
|
| - options, args = tools.OptionParserWithLogging.parse_args(
|
| - self, *args, **kwargs)
|
| - if not self.allow_interspersed_args and args:
|
| - self.error('Unsupported argument: %s' % args)
|
| -
|
| - cwd = file_path.get_native_path_case(unicode(os.getcwd()))
|
| - parse_isolated_option(self, options, cwd, self.require_isolated)
|
| - parse_variable_option(options)
|
| -
|
| - if options.isolate:
|
| - # TODO(maruel): Work with non-ASCII.
|
| - # The path must be in native path case for tracing purposes.
|
| - options.isolate = unicode(options.isolate).replace('/', os.path.sep)
|
| - options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
|
| - options.isolate = file_path.get_native_path_case(options.isolate)
|
| -
|
| - if options.outdir and not is_url(options.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.normpath(os.path.join(cwd, options.outdir))
|
| -
|
| - return options, args
|
| -
|
| -
|
| -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
|
| -
|
| -
|
| -if __name__ == '__main__':
|
| - fix_encoding.fix_encoding()
|
| - tools.disable_buffering()
|
| - colorama.init()
|
| - sys.exit(main(sys.argv[1:]))
|
|
|