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

Unified Diff: tools/isolate/isolate.py

Issue 10019014: Convert isolate.py to exclusively use .isolate files. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Do not read as binary Created 8 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « tools/isolate/data/isolate/test_file2.txt ('k') | tools/isolate/isolate_smoke_test.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: tools/isolate/isolate.py
diff --git a/tools/isolate/isolate.py b/tools/isolate/isolate.py
index a2e87fe30936788a613d1d6624b3a952eafed8c6..41320f3066d1c2de9ed209490aaca42993ad022e 100755
--- a/tools/isolate/isolate.py
+++ b/tools/isolate/isolate.py
@@ -30,9 +30,13 @@ import subprocess
import sys
import tempfile
+import merge_isolate
import trace_inputs
import run_test_from_archive
+# Used by process_inputs().
+NO_INFO, STATS_ONLY, WITH_HASH = range(56, 59)
+
def relpath(path, root):
"""os.path.relpath() that keeps trailing slash."""
@@ -45,6 +49,14 @@ def relpath(path, root):
return out
+def normpath(path):
+ """os.path.normpath() that keeps trailing slash."""
+ out = os.path.normpath(path)
+ if path.endswith(('/', os.path.sep)):
+ out += os.path.sep
+ return out
+
+
def to_relative(path, root, relative):
"""Converts any absolute path to a relative path, only if under root."""
if sys.platform == 'win32':
@@ -67,7 +79,7 @@ def expand_directories(indir, infiles, blacklist):
if os.path.isabs(relfile):
raise run_test_from_archive.MappingError(
'Can\'t map absolute path %s' % relfile)
- infile = os.path.normpath(os.path.join(indir, relfile))
+ infile = normpath(os.path.join(indir, relfile))
if not infile.startswith(indir):
raise run_test_from_archive.MappingError(
'Can\'t map file %s outside %s' % (infile, indir))
@@ -75,16 +87,21 @@ def expand_directories(indir, infiles, blacklist):
if relfile.endswith(os.path.sep):
if not os.path.isdir(infile):
raise run_test_from_archive.MappingError(
- 'Input directory %s must have a trailing slash' % infile)
+ '%s is not a directory' % infile)
for dirpath, dirnames, filenames in os.walk(infile):
# Convert the absolute path to subdir + relative subdirectory.
reldirpath = dirpath[len(indir)+1:]
- outfiles.extend(os.path.join(reldirpath, f) for f in filenames)
+ files_to_add = (os.path.join(reldirpath, f) for f in filenames)
+ outfiles.extend(f for f in files_to_add if not blacklist(f))
for index, dirname in enumerate(dirnames):
# Do not process blacklisted directories.
if blacklist(os.path.join(reldirpath, dirname)):
del dirnames[index]
else:
+ # Always add individual files even if they were blacklisted.
+ if os.path.isdir(infile):
+ raise run_test_from_archive.MappingError(
+ 'Input directory %s must have a trailing slash' % infile)
if not os.path.isfile(infile):
raise run_test_from_archive.MappingError(
'Input file %s doesn\'t exist' % infile)
@@ -92,29 +109,84 @@ def expand_directories(indir, infiles, blacklist):
return outfiles
-def process_inputs(indir, infiles, need_hash, read_only):
+def replace_variable(part, variables):
+ m = re.match(r'<\(([A-Z_]+)\)', part)
+ if m:
+ return variables[m.group(1)]
+ return part
+
+
+def eval_variables(item, variables):
+ return ''.join(
+ replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
+
+
+def load_isolate(content, variables, error):
+ """Loads the .isolate file. Returns the command, dependencies and read_only
+ flag.
+ """
+ # Load the .isolate file, process its conditions, retrieve the command and
+ # dependencies.
+ configs = merge_isolate.load_gyp(merge_isolate.eval_content(content))
+ flavor = trace_inputs.get_flavor()
+ config = configs.per_os.get(flavor) or configs.per_os.get(None)
+ if not config:
+ error('Failed to load configuration for \'%s\'' % flavor)
+
+ # Convert the variables and merge tracked and untracked dependencies.
+ # isolate.py doesn't care about the trackability of the dependencies.
+ infiles = [
+ eval_variables(f, variables) for f in config.tracked
+ ] + [
+ eval_variables(f, variables) for f in config.untracked
+ ]
+ command = [eval_variables(i, variables) for i in config.command]
+ return command, infiles, config.read_only
+
+
+def process_inputs(prevdict, indir, infiles, level, read_only):
"""Returns a dictionary of input files, populated with the files' mode and
hash.
+ |prevdict| is the previous dictionary. It is used to retrieve the cached sha-1
+ to skip recalculating the hash.
+
+ |level| determines the amount of information retrieved.
+ 1 loads no information. 2 loads minimal stat() information. 3 calculates the
+ sha-1 of the file's content.
+
The file mode is manipulated if read_only is True. In practice, we only save
one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r).
"""
+ assert level in (NO_INFO, STATS_ONLY, WITH_HASH)
outdict = {}
for infile in infiles:
filepath = os.path.join(indir, infile)
- filemode = stat.S_IMODE(os.stat(filepath).st_mode)
- # Remove write access for non-owner.
- filemode &= ~(stat.S_IWGRP | stat.S_IWOTH)
- if read_only:
- filemode &= ~stat.S_IWUSR
- if filemode & stat.S_IXUSR:
- filemode |= (stat.S_IXGRP | stat.S_IXOTH)
- else:
- filemode &= ~(stat.S_IXGRP | stat.S_IXOTH)
- outdict[infile] = {
- 'mode': filemode,
- }
- if need_hash:
+ outdict[infile] = {}
+ if level >= STATS_ONLY:
+ filestats = os.stat(filepath)
+ filemode = stat.S_IMODE(filestats.st_mode)
+ # Remove write access for non-owner.
+ filemode &= ~(stat.S_IWGRP | stat.S_IWOTH)
+ if read_only:
+ filemode &= ~stat.S_IWUSR
+ if filemode & stat.S_IXUSR:
+ filemode |= (stat.S_IXGRP | stat.S_IXOTH)
+ else:
+ filemode &= ~(stat.S_IXGRP | stat.S_IXOTH)
+ outdict[infile]['mode'] = filemode
+ outdict[infile]['size'] = filestats.st_size
+ # Used to skip recalculating the hash. Use the most recent update time.
+ outdict[infile]['timestamp'] = int(round(
+ max(filestats.st_mtime, filestats.st_ctime)))
+ # If the timestamp wasn't updated, carry on the sha-1.
+ if (prevdict.get(infile, {}).get('timestamp') ==
+ outdict[infile]['timestamp'] and
+ 'sha-1' in prevdict[infile]):
+ # Reuse the previous hash.
+ outdict[infile]['sha-1'] = prevdict[infile]['sha-1']
+
+ if level >= WITH_HASH and not outdict[infile].get('sha-1'):
h = hashlib.sha1()
with open(filepath, 'rb') as f:
h.update(f.read())
@@ -155,87 +227,92 @@ def recreate_tree(outdir, indir, infiles, action):
run_test_from_archive.link_file(outfile, infile, action)
-def separate_inputs_command(args, root, files):
- """Strips off the command line from the inputs.
-
- gyp provides input paths relative to cwd. Convert them to be relative to root.
- OptionParser kindly strips off '--' from sys.argv if it's provided and that's
- the first non-arg value. Manually look up if it was present in sys.argv.
- """
- cmd = []
- if '--' in args:
- i = args.index('--')
- cmd = args[i+1:]
- args = args[:i]
- elif '--' in sys.argv:
- # optparse is messing with us. Fix it manually.
- cmd = args
- args = []
- if files:
- args = [
- i.decode('utf-8') for i in open(files, 'rb').read().splitlines() if i
- ] + args
- cwd = os.getcwd()
- return [relpath(os.path.join(cwd, arg), root) for arg in args], cmd
-
-
-def isolate(outdir, resultfile, indir, infiles, mode, read_only, cmd, no_save):
+def isolate(
+ outdir, indir, infiles, mode, read_only, cmd, relative_cwd, resultfile):
"""Main function to isolate a target with its dependencies.
Arguments:
- outdir: Output directory where the result is stored. Depends on |mode|.
- - resultfile: File to save the json data.
- indir: Root directory to be used as the base directory for infiles.
- infiles: List of files, with relative path, to process.
- mode: Action to do. See file level docstring.
- read_only: Makes the temporary directory read only.
- cmd: Command to execute.
- - no_save: If True, do not touch resultfile.
+ - relative_cwd: Directory relative to the base directory where to start the
+ command from. In general, this path will be the path
+ containing the gyp file where the target was defined. This
+ relative directory may be created implicitely if a file from
+ this directory is needed to run the test. Otherwise it won't
+ be created and the process creation will fail. It's up to the
+ caller to create this directory manually before starting the
+ test.
+ - resultfile: Path where to read and write the metadata.
Some arguments are optional, dependending on |mode|. See the corresponding
MODE<mode> function for the exact behavior.
"""
mode_fn = getattr(sys.modules[__name__], 'MODE' + mode)
assert mode_fn
- assert os.path.isabs(resultfile)
- infiles = expand_directories(
- indir, infiles, lambda x: re.match(r'.*\.(svn|pyc)$', x))
-
- # Note the relative current directory.
- # In general, this path will be the path containing the gyp file where the
- # target was defined. This relative directory may be created implicitely if a
- # file from this directory is needed to run the test. Otherwise it won't be
- # created and the process creation will fail. It's up to the caller to create
- # this directory manually before starting the test.
- cwd = os.getcwd()
- relative_cwd = os.path.relpath(cwd, indir)
+ # Load the previous results as an optimization.
+ prevdict = {}
+ if resultfile and os.path.isfile(resultfile):
+ resultfile = os.path.abspath(resultfile)
+ with open(resultfile, 'rb') as f:
+ prevdict = json.load(f)
+ else:
+ resultfile = os.path.abspath(resultfile)
+ # Works with native os.path.sep but stores as '/'.
+ if 'files' in prevdict and os.path.sep != '/':
+ prevdict['files'] = dict(
+ (k.replace('/', os.path.sep), v)
+ for k, v in prevdict['files'].iteritems())
- # Workaround make behavior of passing absolute paths.
- cmd = [to_relative(i, indir, cwd) for i in cmd]
- if not cmd:
- # Note that it is exactly the reverse of relative_cwd.
- cmd = [os.path.join(os.path.relpath(indir, cwd), infiles[0])]
- if cmd[0].endswith('.py'):
- cmd.insert(0, sys.executable)
+ infiles = expand_directories(
+ indir, infiles, lambda x: re.match(r'.*\.(svn|pyc)$', x))
# Only hashtable mode really needs the sha-1.
- dictfiles = process_inputs(indir, infiles, mode == 'hashtable', read_only)
+ level = {
+ 'check': NO_INFO,
+ 'hashtable': WITH_HASH,
+ 'remap': STATS_ONLY,
+ 'run': STATS_ONLY,
+ 'trace': STATS_ONLY,
+ }
+ dictfiles = process_inputs(
+ prevdict.get('files', {}), indir, infiles, level[mode], read_only)
result = mode_fn(
outdir, indir, dictfiles, read_only, cmd, relative_cwd, resultfile)
+ out = {
+ 'command': cmd,
+ 'relative_cwd': relative_cwd,
+ 'files': dictfiles,
+ # Makes the directories read-only in addition to the files.
+ 'read_only': read_only,
+ }
+
+ # Works with native os.path.sep but stores as '/'.
+ if os.path.sep != '/':
+ out['files'] = dict(
+ (k.replace(os.path.sep, '/'), v) for k, v in out['files'].iteritems())
+
+ f = None
+ try:
+ if resultfile:
+ f = open(resultfile, 'wb')
+ else:
+ f = sys.stdout
+ json.dump(out, f, indent=2, sort_keys=True)
+ f.write('\n')
+ finally:
+ if resultfile and f:
+ f.close()
- if result == 0 and not no_save:
- # Saves the resulting file.
- out = {
- 'command': cmd,
- 'relative_cwd': relative_cwd,
- 'files': dictfiles,
- 'read_only': read_only,
- }
- with open(resultfile, 'wb') as f:
- json.dump(out, f, indent=2, sort_keys=True)
+ total_bytes = sum(i.get('size', 0) for i in out['files'].itervalues())
+ if total_bytes:
+ logging.debug('Total size: %d bytes' % total_bytes)
return result
@@ -247,13 +324,18 @@ def MODEcheck(
def MODEhashtable(
outdir, indir, dictfiles, _read_only, _cmd, _relative_cwd, resultfile):
- outdir = outdir or os.path.dirname(resultfile)
+ outdir = outdir or os.path.join(os.path.dirname(resultfile), 'hashtable')
+ if not os.path.isdir(outdir):
+ os.makedirs(outdir)
for relfile, properties in dictfiles.iteritems():
infile = os.path.join(indir, relfile)
outfile = os.path.join(outdir, properties['sha-1'])
if os.path.isfile(outfile):
- # Just do a quick check that the file size matches.
- if os.stat(infile).st_size == os.stat(outfile).st_size:
+ # Just do a quick check that the file size matches. No need to stat()
+ # again the input file, grab the value from the dict.
+ out_size = os.stat(outfile).st_size
+ in_size = dictfiles.get(infile, {}).get('size') or os.stat(infile).st_size
+ if in_size == out_size:
continue
# Otherwise, an exception will be raised.
run_test_from_archive.link_file(
@@ -287,7 +369,7 @@ def MODErun(
os.makedirs(cwd)
if read_only:
run_test_from_archive.make_writable(outdir, True)
-
+ cmd = trace_inputs.fix_python_path(cmd)
logging.info('Running %s, cwd=%s' % (cmd, cwd))
return subprocess.call(cmd, cwd=cwd)
finally:
@@ -302,11 +384,13 @@ def MODEtrace(
checkout at src/.
"""
logging.info('Running %s, cwd=%s' % (cmd, os.path.join(indir, relative_cwd)))
- try:
+ if resultfile:
# Guesswork here.
- product_dir = os.path.relpath(os.path.dirname(resultfile), indir)
- except ValueError:
- product_dir = ''
+ product_dir = os.path.dirname(resultfile)
+ if product_dir and indir:
+ product_dir = os.path.relpath(product_dir, indir)
+ else:
+ product_dir = None
return trace_inputs.trace_inputs(
'%s.log' % resultfile,
cmd,
@@ -323,36 +407,42 @@ def get_valid_modes():
def main():
+ default_variables = ['OS=%s' % trace_inputs.get_flavor()]
+ if sys.platform in ('win32', 'cygwin'):
+ default_variables.append('EXECUTABLE_SUFFIX=.exe')
+ else:
+ default_variables.append('EXECUTABLE_SUFFIX=')
valid_modes = get_valid_modes()
parser = optparse.OptionParser(
- usage='%prog [options] [inputs] -- [command line]',
+ usage='%prog [options] [.isolate file]',
description=sys.modules[__name__].__doc__)
- parser.allow_interspersed_args = False
parser.format_description = lambda *_: parser.description
parser.add_option(
- '-v', '--verbose', action='count', default=0, help='Use multiple times')
+ '-v', '--verbose',
+ action='count',
+ default=2 if 'ISOLATE_DEBUG' in os.environ else 0,
+ help='Use multiple times')
parser.add_option(
- '--mode', choices=valid_modes,
+ '-m', '--mode',
+ choices=valid_modes,
help='Determines the action to be taken: %s' % ', '.join(valid_modes))
parser.add_option(
- '--result', metavar='FILE',
- help='File containing the json information about inputs')
+ '-r', '--result',
+ metavar='FILE',
+ help='Result file to store the json manifest')
parser.add_option(
- '--root', metavar='DIR', help='Base directory to fetch files, required')
+ '-V', '--variable',
+ action='append',
+ default=default_variables,
+ dest='variables',
+ metavar='FOO=BAR',
+ help='Variables to process in the .isolate file, default: %default')
parser.add_option(
- '--outdir', metavar='DIR',
+ '-o', '--outdir', metavar='DIR',
help='Directory used to recreate the tree or store the hash table. '
- 'For run and remap, uses a /tmp subdirectory. For the other modes, '
- 'defaults to the directory containing --result')
- parser.add_option(
- '--read-only', action='store_true', default=False,
- help='Make the temporary tree read-only')
- parser.add_option(
- '--from-results', action='store_true',
- help='Loads everything from the result file instead of generating it')
- parser.add_option(
- '--files', metavar='FILE',
- help='File to be read containing input files')
+ 'If the environment variable ISOLATE_HASH_TABLE_DIR exists, it will '
+ 'be used. Otherwise, for run and remap, uses a /tmp subdirectory. '
+ 'For the other modes, defaults to the directory containing --result')
options, args = parser.parse_args()
level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
@@ -362,55 +452,68 @@ def main():
if not options.mode:
parser.error('--mode is required')
+ if len(args) != 1:
+ parser.error('Use only one argument which should be a .isolate file')
+ input_file = os.path.abspath(args[0])
+
+ # Extract the variables.
+ variables = dict(i.split('=', 1) for i in options.variables)
+ if not variables.get('DEPTH'):
+ parser.error('--variable DEPTH=<base dir> is required')
+
+ PATH_VARIABLES = ('DEPTH', 'PRODUCT_DIR')
+ # Process path variables as a special case. First normalize it, verifies it
+ # exists, convert it to an absolute path, then calculate relative_dir, and
+ # finally convert it back to a relative value from relative_dir.
+ abs_variables = {}
+ for i in PATH_VARIABLES:
+ if i not in variables:
+ continue
+ abs_variables[i] = os.path.normpath(variables[i])
+ if not os.path.isdir(abs_variables[i]):
+ parser.error('%s is not a directory' % abs_variables[i])
+ abs_variables[i] = os.path.abspath(abs_variables[i])
+
+ # The relative directory is automatically determined by the relative path
+ # between DEPTH and the directory containing the .isolate file.
+ isolate_dir = os.path.dirname(os.path.abspath(input_file))
+ relative_dir = os.path.relpath(isolate_dir, abs_variables['DEPTH'])
+ logging.debug('relative_dir: %s' % relative_dir)
+
+ # Directories are _relative_ to relative_dir.
+ for i in PATH_VARIABLES:
+ if i not in variables:
+ continue
+ variables[i] = os.path.relpath(abs_variables[i], isolate_dir)
- if not options.result:
- parser.error('--result is required.')
- if options.from_results:
- if not options.root:
- options.root = os.getcwd()
- if args:
- parser.error('Arguments cannot be used with --from-result')
- if options.files:
- parser.error('--files cannot be used with --from-result')
- else:
- if not options.root:
- parser.error('--root is required.')
-
- options.result = os.path.abspath(options.result)
-
- # Normalize the root input directory.
- indir = os.path.normpath(options.root)
- if not os.path.isdir(indir):
- parser.error('%s is not a directory' % indir)
-
- # Do not call abspath until it was verified the directory exists.
- indir = os.path.abspath(indir)
-
- logging.info('sys.argv: %s' % sys.argv)
- logging.info('cwd: %s' % os.getcwd())
- logging.info('Args: %s' % args)
- if not options.from_results:
- infiles, cmd = separate_inputs_command(args, indir, options.files)
- if not infiles:
- parser.error('Need at least one input file to map')
- else:
- data = json.load(open(options.result))
- cmd = data['command']
- infiles = data['files'].keys()
- os.chdir(data['relative_cwd'])
-
- logging.info('infiles: %s' % infiles)
+ logging.debug(
+ 'variables: %s' % ', '.join(
+ '%s=%s' % (k, v) for k, v in variables.iteritems()))
+
+ # TODO(maruel): Case insensitive file systems.
+ if not input_file.startswith(abs_variables['DEPTH']):
+ parser.error(
+ '%s must be under %s, as it is used as the relative start directory.' %
+ (args[0], abs_variables['DEPTH']))
+
+ command, infiles, read_only = load_isolate(
+ open(input_file, 'r').read(), variables, parser.error)
+ logging.debug('command: %s' % command)
+ logging.debug('infiles: %s' % infiles)
+ logging.debug('read_only: %s' % read_only)
+ infiles = [normpath(os.path.join(relative_dir, f)) for f in infiles]
+ logging.debug('processed infiles: %s' % infiles)
try:
return isolate(
options.outdir,
- options.result,
- indir,
+ abs_variables['DEPTH'],
infiles,
options.mode,
- options.read_only,
- cmd,
- options.from_results)
+ read_only,
+ command,
+ relative_dir,
+ options.result)
except run_test_from_archive.MappingError, e:
print >> sys.stderr, str(e)
return 1
« no previous file with comments | « tools/isolate/data/isolate/test_file2.txt ('k') | tools/isolate/isolate_smoke_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698