blob: 7387907eea18f1faa0e69c8d3f762dbb375ded43 [file] [log] [blame]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Front end tool to manage .isolate files and corresponding tests.
7
8Run ./isolate.py --help for more detailed information.
9
10See more information at
11http://dev.chromium.org/developers/testing/isolated-testing
12"""
13
benrg@chromium.org609b7982013-02-07 16:44:46 +000014import ast
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000015import copy
16import hashlib
benrg@chromium.org609b7982013-02-07 16:44:46 +000017import itertools
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000018import logging
19import optparse
20import os
21import posixpath
22import re
23import stat
24import subprocess
25import sys
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000026
maruel@chromium.orgc6f90062012-11-07 18:32:22 +000027import isolateserver_archive
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000028import run_isolated
benrg@chromium.org609b7982013-02-07 16:44:46 +000029import short_expression_finder
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000030import trace_inputs
31
32# Import here directly so isolate is easier to use as a library.
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000033from run_isolated import get_flavor
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000034
35
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000036PATH_VARIABLES = ('DEPTH', 'PRODUCT_DIR')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000037
38# Files that should be 0-length when mapped.
39KEY_TOUCHED = 'isolate_dependency_touched'
40# Files that should be tracked by the build tool.
41KEY_TRACKED = 'isolate_dependency_tracked'
42# Files that should not be tracked by the build tool.
43KEY_UNTRACKED = 'isolate_dependency_untracked'
44
45_GIT_PATH = os.path.sep + '.git'
46_SVN_PATH = os.path.sep + '.svn'
47
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000048
49class ExecutionError(Exception):
50 """A generic error occurred."""
51 def __str__(self):
52 return self.args[0]
53
54
55### Path handling code.
56
57
58def relpath(path, root):
59 """os.path.relpath() that keeps trailing os.path.sep."""
60 out = os.path.relpath(path, root)
61 if path.endswith(os.path.sep):
62 out += os.path.sep
63 return out
64
65
66def normpath(path):
67 """os.path.normpath() that keeps trailing os.path.sep."""
68 out = os.path.normpath(path)
69 if path.endswith(os.path.sep):
70 out += os.path.sep
71 return out
72
73
74def posix_relpath(path, root):
75 """posix.relpath() that keeps trailing slash."""
76 out = posixpath.relpath(path, root)
77 if path.endswith('/'):
78 out += '/'
79 return out
80
81
82def cleanup_path(x):
83 """Cleans up a relative path. Converts any os.path.sep to '/' on Windows."""
84 if x:
85 x = x.rstrip(os.path.sep).replace(os.path.sep, '/')
86 if x == '.':
87 x = ''
88 if x:
89 x += '/'
90 return x
91
92
93def default_blacklist(f):
94 """Filters unimportant files normally ignored."""
95 return (
96 f.endswith(('.pyc', '.run_test_cases', 'testserver.log')) or
97 _GIT_PATH in f or
98 _SVN_PATH in f or
99 f in ('.git', '.svn'))
100
101
102def expand_directory_and_symlink(indir, relfile, blacklist):
103 """Expands a single input. It can result in multiple outputs.
104
105 This function is recursive when relfile is a directory or a symlink.
106
107 Note: this code doesn't properly handle recursive symlink like one created
108 with:
109 ln -s .. foo
110 """
111 if os.path.isabs(relfile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000112 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000113 'Can\'t map absolute path %s' % relfile)
114
115 infile = normpath(os.path.join(indir, relfile))
116 if not infile.startswith(indir):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000117 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000118 'Can\'t map file %s outside %s' % (infile, indir))
119
120 if sys.platform != 'win32':
121 # Look if any item in relfile is a symlink.
122 base, symlink, rest = trace_inputs.split_at_symlink(indir, relfile)
123 if symlink:
124 # Append everything pointed by the symlink. If the symlink is recursive,
125 # this code blows up.
126 symlink_relfile = os.path.join(base, symlink)
127 symlink_path = os.path.join(indir, symlink_relfile)
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000128 # readlink doesn't exist on Windows.
129 pointed = os.readlink(symlink_path) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000130 dest_infile = normpath(
131 os.path.join(os.path.dirname(symlink_path), pointed))
132 if rest:
133 dest_infile = trace_inputs.safe_join(dest_infile, rest)
134 if not dest_infile.startswith(indir):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000135 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000136 'Can\'t map symlink reference %s (from %s) ->%s outside of %s' %
137 (symlink_relfile, relfile, dest_infile, indir))
138 if infile.startswith(dest_infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000139 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000140 'Can\'t map recursive symlink reference %s->%s' %
141 (symlink_relfile, dest_infile))
142 dest_relfile = dest_infile[len(indir)+1:]
143 logging.info('Found symlink: %s -> %s' % (symlink_relfile, dest_relfile))
144 out = expand_directory_and_symlink(indir, dest_relfile, blacklist)
145 # Add the symlink itself.
146 out.append(symlink_relfile)
147 return out
148
149 if relfile.endswith(os.path.sep):
150 if not os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000151 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000152 '%s is not a directory but ends with "%s"' % (infile, os.path.sep))
153
154 outfiles = []
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000155 try:
156 for filename in os.listdir(infile):
157 inner_relfile = os.path.join(relfile, filename)
158 if blacklist(inner_relfile):
159 continue
160 if os.path.isdir(os.path.join(indir, inner_relfile)):
161 inner_relfile += os.path.sep
162 outfiles.extend(
163 expand_directory_and_symlink(indir, inner_relfile, blacklist))
164 return outfiles
165 except OSError as e:
166 raise run_isolated.MappingError('Unable to iterate over directories.\n'
167 '%s' % e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000168 else:
169 # Always add individual files even if they were blacklisted.
170 if os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000171 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000172 'Input directory %s must have a trailing slash' % infile)
173
174 if not os.path.isfile(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000175 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000176 'Input file %s doesn\'t exist' % infile)
177
178 return [relfile]
179
180
csharp@chromium.org01856802012-11-12 17:48:13 +0000181def expand_directories_and_symlinks(indir, infiles, blacklist,
182 ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000183 """Expands the directories and the symlinks, applies the blacklist and
184 verifies files exist.
185
186 Files are specified in os native path separator.
187 """
188 outfiles = []
189 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +0000190 try:
191 outfiles.extend(expand_directory_and_symlink(indir, relfile, blacklist))
192 except run_isolated.MappingError as e:
193 if ignore_broken_items:
194 logging.info('warning: %s', e)
195 else:
196 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000197 return outfiles
198
199
200def recreate_tree(outdir, indir, infiles, action, as_sha1):
201 """Creates a new tree with only the input files in it.
202
203 Arguments:
204 outdir: Output directory to create the files in.
205 indir: Root directory the infiles are based in.
206 infiles: dict of files to map from |indir| to |outdir|.
207 action: See assert below.
208 as_sha1: Output filename is the sha1 instead of relfile.
209 """
210 logging.info(
211 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_sha1=%s)' %
212 (outdir, indir, len(infiles), action, as_sha1))
213
214 assert action in (
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000215 run_isolated.HARDLINK,
216 run_isolated.SYMLINK,
217 run_isolated.COPY)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000218 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000219 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000220 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000221 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000222
223 for relfile, metadata in infiles.iteritems():
224 infile = os.path.join(indir, relfile)
225 if as_sha1:
226 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000227 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000228 # Skip links when storing a hashtable.
229 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000230 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000231 if os.path.isfile(outfile):
232 # Just do a quick check that the file size matches. No need to stat()
233 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000234 if not 's' in metadata:
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000235 raise run_isolated.MappingError(
236 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000237 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000238 continue
239 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000240 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000241 os.remove(outfile)
242 else:
243 outfile = os.path.join(outdir, relfile)
244 outsubdir = os.path.dirname(outfile)
245 if not os.path.isdir(outsubdir):
246 os.makedirs(outsubdir)
247
248 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000249 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000250 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000251 if 'l' in metadata:
252 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000253 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000254 # symlink doesn't exist on Windows.
255 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000256 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000257 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000258
259
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000260def process_input(filepath, prevdict, read_only):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000261 """Processes an input file, a dependency, and return meta data about it.
262
263 Arguments:
264 - filepath: File to act on.
265 - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
266 to skip recalculating the hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000267 - read_only: If True, the file mode is manipulated. In practice, only save
268 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
269 windows, mode is not set since all files are 'executable' by
270 default.
271
272 Behaviors:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000273 - Retrieves the file mode, file size, file timestamp, file link
274 destination if it is a file link and calcultate the SHA-1 of the file's
275 content if the path points to a file and not a symlink.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000276 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000277 out = {}
278 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000279 # if prevdict.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000280 # # The file's content is ignored. Skip the time and hard code mode.
281 # if get_flavor() != 'win':
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000282 # out['m'] = stat.S_IRUSR | stat.S_IRGRP
283 # out['s'] = 0
284 # out['h'] = SHA_1_NULL
285 # out['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000286 # return out
287
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000288 # Always check the file stat and check if it is a link. The timestamp is used
289 # to know if the file's content/symlink destination should be looked into.
290 # E.g. only reuse from prevdict if the timestamp hasn't changed.
291 # There is the risk of the file's timestamp being reset to its last value
292 # manually while its content changed. We don't protect against that use case.
293 try:
294 filestats = os.lstat(filepath)
295 except OSError:
296 # The file is not present.
297 raise run_isolated.MappingError('%s is missing' % filepath)
298 is_link = stat.S_ISLNK(filestats.st_mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000299
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000300 if get_flavor() != 'win':
301 # Ignore file mode on Windows since it's not really useful there.
302 filemode = stat.S_IMODE(filestats.st_mode)
303 # Remove write access for group and all access to 'others'.
304 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
305 if read_only:
306 filemode &= ~stat.S_IWUSR
307 if filemode & stat.S_IXUSR:
308 filemode |= stat.S_IXGRP
309 else:
310 filemode &= ~stat.S_IXGRP
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000311 out['m'] = filemode
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000312
313 # Used to skip recalculating the hash or link destination. Use the most recent
314 # update time.
315 # TODO(maruel): Save it in the .state file instead of .isolated so the
316 # .isolated file is deterministic.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000317 out['t'] = int(round(filestats.st_mtime))
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000318
319 if not is_link:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000320 out['s'] = filestats.st_size
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000321 # If the timestamp wasn't updated and the file size is still the same, carry
322 # on the sha-1.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000323 if (prevdict.get('t') == out['t'] and
324 prevdict.get('s') == out['s']):
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000325 # Reuse the previous hash if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000326 out['h'] = prevdict.get('h')
327 if not out.get('h'):
maruel@chromium.org6da38772012-12-11 21:36:37 +0000328 out['h'] = isolateserver_archive.sha1_file(filepath)
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000329 else:
330 # If the timestamp wasn't updated, carry on the link destination.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000331 if prevdict.get('t') == out['t']:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000332 # Reuse the previous link destination if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000333 out['l'] = prevdict.get('l')
334 if out.get('l') is None:
335 out['l'] = os.readlink(filepath) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000336 return out
337
338
339### Variable stuff.
340
341
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000342def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000343 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000344 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000345
346
347def determine_root_dir(relative_root, infiles):
348 """For a list of infiles, determines the deepest root directory that is
349 referenced indirectly.
350
351 All arguments must be using os.path.sep.
352 """
353 # The trick used to determine the root directory is to look at "how far" back
354 # up it is looking up.
355 deepest_root = relative_root
356 for i in infiles:
357 x = relative_root
358 while i.startswith('..' + os.path.sep):
359 i = i[3:]
360 assert not i.startswith(os.path.sep)
361 x = os.path.dirname(x)
362 if deepest_root.startswith(x):
363 deepest_root = x
364 logging.debug(
365 'determine_root_dir(%s, %d files) -> %s' % (
366 relative_root, len(infiles), deepest_root))
367 return deepest_root
368
369
370def replace_variable(part, variables):
371 m = re.match(r'<\(([A-Z_]+)\)', part)
372 if m:
373 if m.group(1) not in variables:
374 raise ExecutionError(
375 'Variable "%s" was not found in %s.\nDid you forget to specify '
376 '--variable?' % (m.group(1), variables))
377 return variables[m.group(1)]
378 return part
379
380
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000381def process_variables(cwd, variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000382 """Processes path variables as a special case and returns a copy of the dict.
383
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000384 For each 'path' variable: first normalizes it based on |cwd|, verifies it
385 exists then sets it as relative to relative_base_dir.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000386 """
387 variables = variables.copy()
388 for i in PATH_VARIABLES:
389 if i not in variables:
390 continue
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000391 # Variables could contain / or \ on windows. Always normalize to
392 # os.path.sep.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000393 variable = variables[i].replace('/', os.path.sep)
394 if os.path.isabs(variable):
395 raise ExecutionError(
csharp@chromium.org837352f2013-01-17 21:17:03 +0000396 'Variable can\'t be absolute: %s: %s' % (i, variables[i]))
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000397
398 variable = os.path.join(cwd, variable)
399 variable = os.path.normpath(variable)
400 if not os.path.isdir(variable):
401 raise ExecutionError('%s=%s is not a directory' % (i, variable))
402
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000403 # All variables are relative to the .isolate file.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000404 variable = os.path.relpath(variable, relative_base_dir)
405 logging.debug(
406 'Translated variable %s from %s to %s', i, variables[i], variable)
407 variables[i] = variable
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000408 return variables
409
410
411def eval_variables(item, variables):
412 """Replaces the .isolate variables in a string item.
413
414 Note that the .isolate format is a subset of the .gyp dialect.
415 """
416 return ''.join(
417 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
418
419
420def classify_files(root_dir, tracked, untracked):
421 """Converts the list of files into a .isolate 'variables' dictionary.
422
423 Arguments:
424 - tracked: list of files names to generate a dictionary out of that should
425 probably be tracked.
426 - untracked: list of files names that must not be tracked.
427 """
428 # These directories are not guaranteed to be always present on every builder.
429 OPTIONAL_DIRECTORIES = (
430 'test/data/plugin',
431 'third_party/WebKit/LayoutTests',
432 )
433
434 new_tracked = []
435 new_untracked = list(untracked)
436
437 def should_be_tracked(filepath):
438 """Returns True if it is a file without whitespace in a non-optional
439 directory that has no symlink in its path.
440 """
441 if filepath.endswith('/'):
442 return False
443 if ' ' in filepath:
444 return False
445 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
446 return False
447 # Look if any element in the path is a symlink.
448 split = filepath.split('/')
449 for i in range(len(split)):
450 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
451 return False
452 return True
453
454 for filepath in sorted(tracked):
455 if should_be_tracked(filepath):
456 new_tracked.append(filepath)
457 else:
458 # Anything else.
459 new_untracked.append(filepath)
460
461 variables = {}
462 if new_tracked:
463 variables[KEY_TRACKED] = sorted(new_tracked)
464 if new_untracked:
465 variables[KEY_UNTRACKED] = sorted(new_untracked)
466 return variables
467
468
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000469def chromium_fix(f, variables):
470 """Fixes an isolate dependnecy with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000471 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
472 # separator.
473 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000474 # Ignored items.
475 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000476 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000477 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000478 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000479 # 'First Run' is not created by the compile, but by the test itself.
480 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000481
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000482 # Blacklist logs and other unimportant files.
483 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
484 logging.debug('Ignoring %s', f)
485 return None
486
maruel@chromium.org7650e422012-11-16 21:56:42 +0000487 EXECUTABLE = re.compile(
488 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
489 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
490 r'$')
491 match = EXECUTABLE.match(f)
492 if match:
493 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
494
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000495 if sys.platform == 'darwin':
496 # On OSX, the name of the output is dependent on gyp define, it can be
497 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
498 # Framework.framework'. Furthermore, they are versioned with a gyp
499 # variable. To lower the complexity of the .isolate file, remove all the
500 # individual entries that show up under any of the 4 entries and replace
501 # them with the directory itself. Overall, this results in a bit more
502 # files than strictly necessary.
503 OSX_BUNDLES = (
504 '<(PRODUCT_DIR)/Chromium Framework.framework/',
505 '<(PRODUCT_DIR)/Chromium.app/',
506 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
507 '<(PRODUCT_DIR)/Google Chrome.app/',
508 )
509 for prefix in OSX_BUNDLES:
510 if f.startswith(prefix):
511 # Note this result in duplicate values, so the a set() must be used to
512 # remove duplicates.
513 return prefix
514 return f
515
516
517def generate_simplified(
518 tracked, untracked, touched, root_dir, variables, relative_cwd):
519 """Generates a clean and complete .isolate 'variables' dictionary.
520
521 Cleans up and extracts only files from within root_dir then processes
522 variables and relative_cwd.
523 """
524 root_dir = os.path.realpath(root_dir)
525 logging.info(
526 'generate_simplified(%d files, %s, %s, %s)' %
527 (len(tracked) + len(untracked) + len(touched),
528 root_dir, variables, relative_cwd))
529
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000530 # Preparation work.
531 relative_cwd = cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000532 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000533 # Creates the right set of variables here. We only care about PATH_VARIABLES.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000534 path_variables = dict(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000535 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
536 for k in PATH_VARIABLES if k in variables)
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000537 variables = variables.copy()
538 variables.update(path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000539
540 # Actual work: Process the files.
541 # TODO(maruel): if all the files in a directory are in part tracked and in
542 # part untracked, the directory will not be extracted. Tracked files should be
543 # 'promoted' to be untracked as needed.
544 tracked = trace_inputs.extract_directories(
545 root_dir, tracked, default_blacklist)
546 untracked = trace_inputs.extract_directories(
547 root_dir, untracked, default_blacklist)
548 # touched is not compressed, otherwise it would result in files to be archived
549 # that we don't need.
550
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000551 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000552 def fix(f):
553 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000554 # Important, GYP stores the files with / and not \.
555 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000556 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000557 # If it's not already a variable.
558 if not f.startswith('<'):
559 # relative_cwd is usually the directory containing the gyp file. It may be
560 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000561 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000562 # Convert the whole thing to / since it's isolate's speak.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000563 f = posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000564 posixpath.join(root_dir_posix, f),
565 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000566
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000567 for variable, root_path in path_variables.iteritems():
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000568 if f.startswith(root_path):
569 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000570 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000571 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000572 return f
573
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000574 def fix_all(items):
575 """Reduces the items to convert variables, removes unneeded items, apply
576 chromium-specific fixes and only return unique items.
577 """
578 variables_converted = (fix(f.path) for f in items)
579 chromium_fixed = (chromium_fix(f, variables) for f in variables_converted)
580 return set(f for f in chromium_fixed if f)
581
582 tracked = fix_all(tracked)
583 untracked = fix_all(untracked)
584 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000585 out = classify_files(root_dir, tracked, untracked)
586 if touched:
587 out[KEY_TOUCHED] = sorted(touched)
588 return out
589
590
benrg@chromium.org609b7982013-02-07 16:44:46 +0000591def chromium_filter_flags(variables):
592 """Filters out build flags used in Chromium that we don't want to treat as
593 configuration variables.
594 """
595 # TODO(benrg): Need a better way to determine this.
596 blacklist = set(PATH_VARIABLES + ('EXECUTABLE_SUFFIX', 'FLAG'))
597 return dict((k, v) for k, v in variables.iteritems() if k not in blacklist)
598
599
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000600def generate_isolate(
601 tracked, untracked, touched, root_dir, variables, relative_cwd):
602 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000603 dependencies = generate_simplified(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000604 tracked, untracked, touched, root_dir, variables, relative_cwd)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000605 config_variables = chromium_filter_flags(variables)
606 config_variable_names, config_values = zip(
607 *sorted(config_variables.iteritems()))
608 out = Configs(None)
609 # The new dependencies apply to just one configuration, namely config_values.
610 out.merge_dependencies(dependencies, config_variable_names, [config_values])
611 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000612
613
614def split_touched(files):
615 """Splits files that are touched vs files that are read."""
616 tracked = []
617 touched = []
618 for f in files:
619 if f.size:
620 tracked.append(f)
621 else:
622 touched.append(f)
623 return tracked, touched
624
625
626def pretty_print(variables, stdout):
627 """Outputs a gyp compatible list from the decoded variables.
628
629 Similar to pprint.print() but with NIH syndrome.
630 """
631 # Order the dictionary keys by these keys in priority.
632 ORDER = (
633 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
634 KEY_TRACKED, KEY_UNTRACKED)
635
636 def sorting_key(x):
637 """Gives priority to 'most important' keys before the others."""
638 if x in ORDER:
639 return str(ORDER.index(x))
640 return x
641
642 def loop_list(indent, items):
643 for item in items:
644 if isinstance(item, basestring):
645 stdout.write('%s\'%s\',\n' % (indent, item))
646 elif isinstance(item, dict):
647 stdout.write('%s{\n' % indent)
648 loop_dict(indent + ' ', item)
649 stdout.write('%s},\n' % indent)
650 elif isinstance(item, list):
651 # A list inside a list will write the first item embedded.
652 stdout.write('%s[' % indent)
653 for index, i in enumerate(item):
654 if isinstance(i, basestring):
655 stdout.write(
656 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
657 elif isinstance(i, dict):
658 stdout.write('{\n')
659 loop_dict(indent + ' ', i)
660 if index != len(item) - 1:
661 x = ', '
662 else:
663 x = ''
664 stdout.write('%s}%s' % (indent, x))
665 else:
666 assert False
667 stdout.write('],\n')
668 else:
669 assert False
670
671 def loop_dict(indent, items):
672 for key in sorted(items, key=sorting_key):
673 item = items[key]
674 stdout.write("%s'%s': " % (indent, key))
675 if isinstance(item, dict):
676 stdout.write('{\n')
677 loop_dict(indent + ' ', item)
678 stdout.write(indent + '},\n')
679 elif isinstance(item, list):
680 stdout.write('[\n')
681 loop_list(indent + ' ', item)
682 stdout.write(indent + '],\n')
683 elif isinstance(item, basestring):
684 stdout.write(
685 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
686 elif item in (True, False, None):
687 stdout.write('%s\n' % item)
688 else:
689 assert False, item
690
691 stdout.write('{\n')
692 loop_dict(' ', variables)
693 stdout.write('}\n')
694
695
696def union(lhs, rhs):
697 """Merges two compatible datastructures composed of dict/list/set."""
698 assert lhs is not None or rhs is not None
699 if lhs is None:
700 return copy.deepcopy(rhs)
701 if rhs is None:
702 return copy.deepcopy(lhs)
703 assert type(lhs) == type(rhs), (lhs, rhs)
704 if hasattr(lhs, 'union'):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000705 # Includes set, ConfigSettings and Configs.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000706 return lhs.union(rhs)
707 if isinstance(lhs, dict):
708 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
709 elif isinstance(lhs, list):
710 # Do not go inside the list.
711 return lhs + rhs
712 assert False, type(lhs)
713
714
715def extract_comment(content):
716 """Extracts file level comment."""
717 out = []
718 for line in content.splitlines(True):
719 if line.startswith('#'):
720 out.append(line)
721 else:
722 break
723 return ''.join(out)
724
725
726def eval_content(content):
727 """Evaluates a python file and return the value defined in it.
728
729 Used in practice for .isolate files.
730 """
731 globs = {'__builtins__': None}
732 locs = {}
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000733 try:
734 value = eval(content, globs, locs)
735 except TypeError as e:
736 e.args = list(e.args) + [content]
737 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000738 assert locs == {}, locs
739 assert globs == {'__builtins__': None}, globs
740 return value
741
742
benrg@chromium.org609b7982013-02-07 16:44:46 +0000743def match_configs(expr, config_variables, all_configs):
744 """Returns the configs from |all_configs| that match the |expr|, where
745 the elements of |all_configs| are tuples of values for the |config_variables|.
746 Example:
747 >>> match_configs(expr = "(foo==1 or foo==2) and bar=='b'",
748 config_variables = ["foo", "bar"],
749 all_configs = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
750 [(1, 'b'), (2, 'b')]
751 """
752 return [
753 config for config in all_configs
754 if eval(expr, dict(zip(config_variables, config)))
755 ]
756
757
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000758def verify_variables(variables):
759 """Verifies the |variables| dictionary is in the expected format."""
760 VALID_VARIABLES = [
761 KEY_TOUCHED,
762 KEY_TRACKED,
763 KEY_UNTRACKED,
764 'command',
765 'read_only',
766 ]
767 assert isinstance(variables, dict), variables
768 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
769 for name, value in variables.iteritems():
770 if name == 'read_only':
771 assert value in (True, False, None), value
772 else:
773 assert isinstance(value, list), value
774 assert all(isinstance(i, basestring) for i in value), value
775
776
benrg@chromium.org609b7982013-02-07 16:44:46 +0000777def verify_ast(expr, variables_and_values):
778 """Verifies that |expr| is of the form
779 expr ::= expr ( "or" | "and" ) expr
780 | identifier "==" ( string | int )
781 Also collects the variable identifiers and string/int values in the dict
782 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
783 """
784 assert isinstance(expr, (ast.BoolOp, ast.Compare))
785 if isinstance(expr, ast.BoolOp):
786 assert isinstance(expr.op, (ast.And, ast.Or))
787 for subexpr in expr.values:
788 verify_ast(subexpr, variables_and_values)
789 else:
790 assert isinstance(expr.left.ctx, ast.Load)
791 assert len(expr.ops) == 1
792 assert isinstance(expr.ops[0], ast.Eq)
793 var_values = variables_and_values.setdefault(expr.left.id, set())
794 rhs = expr.comparators[0]
795 assert isinstance(rhs, (ast.Str, ast.Num))
796 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
797
798
799def verify_condition(condition, variables_and_values):
800 """Verifies the |condition| dictionary is in the expected format.
801 See verify_ast() for the meaning of |variables_and_values|.
802 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000803 VALID_INSIDE_CONDITION = ['variables']
804 assert isinstance(condition, list), condition
benrg@chromium.org609b7982013-02-07 16:44:46 +0000805 assert len(condition) == 2, condition
806 expr, then = condition
807
808 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
809 verify_ast(test_ast.body, variables_and_values)
810
811 assert isinstance(then, dict), then
812 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
813 verify_variables(then['variables'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000814
815
benrg@chromium.org609b7982013-02-07 16:44:46 +0000816def verify_root(value, variables_and_values):
817 """Verifies that |value| is the parsed form of a valid .isolate file.
818 See verify_ast() for the meaning of |variables_and_values|.
819 """
820 VALID_ROOTS = ['includes', 'conditions']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000821 assert isinstance(value, dict), value
822 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000823
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000824 includes = value.get('includes', [])
825 assert isinstance(includes, list), includes
826 for include in includes:
827 assert isinstance(include, basestring), include
828
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000829 conditions = value.get('conditions', [])
830 assert isinstance(conditions, list), conditions
831 for condition in conditions:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000832 verify_condition(condition, variables_and_values)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000833
834
benrg@chromium.org609b7982013-02-07 16:44:46 +0000835def remove_weak_dependencies(values, key, item, item_configs):
836 """Removes any configs from this key if the item is already under a
837 strong key.
838 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000839 if key == KEY_TOUCHED:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000840 item_configs = set(item_configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000841 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000842 try:
843 item_configs -= values[stronger_key][item]
844 except KeyError:
845 pass
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000846
benrg@chromium.org609b7982013-02-07 16:44:46 +0000847 return item_configs
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000848
849
benrg@chromium.org609b7982013-02-07 16:44:46 +0000850def remove_repeated_dependencies(folders, key, item, item_configs):
851 """Removes any configs from this key if the item is in a folder that is
852 already included."""
csharp@chromium.org31176252012-11-02 13:04:40 +0000853
854 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000855 item_configs = set(item_configs)
856 for (folder, configs) in folders.iteritems():
csharp@chromium.org31176252012-11-02 13:04:40 +0000857 if folder != item and item.startswith(folder):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000858 item_configs -= configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000859
benrg@chromium.org609b7982013-02-07 16:44:46 +0000860 return item_configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000861
862
863def get_folders(values_dict):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000864 """Returns a dict of all the folders in the given value_dict."""
865 return dict(
866 (item, configs) for (item, configs) in values_dict.iteritems()
867 if item.endswith('/')
868 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000869
870
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000871def invert_map(variables):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000872 """Converts {config: {deptype: list(depvals)}} to
873 {deptype: {depval: set(configs)}}.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000874 """
875 KEYS = (
876 KEY_TOUCHED,
877 KEY_TRACKED,
878 KEY_UNTRACKED,
879 'command',
880 'read_only',
881 )
882 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000883 for config, values in variables.iteritems():
884 for key in KEYS:
885 if key == 'command':
886 items = [tuple(values[key])] if key in values else []
887 elif key == 'read_only':
888 items = [values[key]] if key in values else []
889 else:
890 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
891 items = values.get(key, [])
892 for item in items:
893 out[key].setdefault(item, set()).add(config)
894 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000895
896
benrg@chromium.org609b7982013-02-07 16:44:46 +0000897def reduce_inputs(values):
898 """Reduces the output of invert_map() to the strictest minimum list.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000899
benrg@chromium.org609b7982013-02-07 16:44:46 +0000900 Looks at each individual file and directory, maps where they are used and
901 reconstructs the inverse dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000902
benrg@chromium.org609b7982013-02-07 16:44:46 +0000903 Returns the minimized dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000904 """
905 KEYS = (
906 KEY_TOUCHED,
907 KEY_TRACKED,
908 KEY_UNTRACKED,
909 'command',
910 'read_only',
911 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000912
913 # Folders can only live in KEY_UNTRACKED.
914 folders = get_folders(values.get(KEY_UNTRACKED, {}))
915
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000916 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000917 for key in KEYS:
918 for item, item_configs in values.get(key, {}).iteritems():
919 item_configs = remove_weak_dependencies(values, key, item, item_configs)
920 item_configs = remove_repeated_dependencies(
921 folders, key, item, item_configs)
922 if item_configs:
923 out[key][item] = item_configs
924 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000925
926
benrg@chromium.org609b7982013-02-07 16:44:46 +0000927def convert_map_to_isolate_dict(values, config_variables):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000928 """Regenerates back a .isolate configuration dict from files and dirs
929 mappings generated from reduce_inputs().
930 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000931 # Gather a list of configurations for set inversion later.
932 all_mentioned_configs = set()
933 for configs_by_item in values.itervalues():
934 for configs in configs_by_item.itervalues():
935 all_mentioned_configs.update(configs)
936
937 # Invert the mapping to make it dict first.
938 conditions = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000939 for key in values:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000940 for item, configs in values[key].iteritems():
941 then = conditions.setdefault(frozenset(configs), {})
942 variables = then.setdefault('variables', {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000943
benrg@chromium.org609b7982013-02-07 16:44:46 +0000944 if item in (True, False):
945 # One-off for read_only.
946 variables[key] = item
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000947 else:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000948 assert item
949 if isinstance(item, tuple):
950 # One-off for command.
951 # Do not merge lists and do not sort!
952 # Note that item is a tuple.
953 assert key not in variables
954 variables[key] = list(item)
955 else:
956 # The list of items (files or dirs). Append the new item and keep
957 # the list sorted.
958 l = variables.setdefault(key, [])
959 l.append(item)
960 l.sort()
961
962 if all_mentioned_configs:
963 config_values = map(set, zip(*all_mentioned_configs))
964 sef = short_expression_finder.ShortExpressionFinder(
965 zip(config_variables, config_values))
966
967 conditions = sorted(
968 [sef.get_expr(configs), then] for configs, then in conditions.iteritems())
969 return {'conditions': conditions}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000970
971
972### Internal state files.
973
974
benrg@chromium.org609b7982013-02-07 16:44:46 +0000975class ConfigSettings(object):
976 """Represents the dependency variables for a single build configuration.
977 The structure is immutable.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000978 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000979 def __init__(self, config, values):
980 self.config = config
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000981 verify_variables(values)
982 self.touched = sorted(values.get(KEY_TOUCHED, []))
983 self.tracked = sorted(values.get(KEY_TRACKED, []))
984 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
985 self.command = values.get('command', [])[:]
986 self.read_only = values.get('read_only')
987
988 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000989 assert not (self.config and rhs.config) or (self.config == rhs.config)
maruel@chromium.org669edcb2012-11-02 19:16:14 +0000990 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000991 var = {
992 KEY_TOUCHED: sorted(self.touched + rhs.touched),
993 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
994 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
995 'command': self.command or rhs.command,
996 'read_only': rhs.read_only if self.read_only is None else self.read_only,
997 }
benrg@chromium.org609b7982013-02-07 16:44:46 +0000998 return ConfigSettings(self.config or rhs.config, var)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000999
1000 def flatten(self):
1001 out = {}
1002 if self.command:
1003 out['command'] = self.command
1004 if self.touched:
1005 out[KEY_TOUCHED] = self.touched
1006 if self.tracked:
1007 out[KEY_TRACKED] = self.tracked
1008 if self.untracked:
1009 out[KEY_UNTRACKED] = self.untracked
1010 if self.read_only is not None:
1011 out['read_only'] = self.read_only
1012 return out
1013
1014
1015class Configs(object):
1016 """Represents a processed .isolate file.
1017
benrg@chromium.org609b7982013-02-07 16:44:46 +00001018 Stores the file in a processed way, split by configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001019 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001020 def __init__(self, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001021 self.file_comment = file_comment
benrg@chromium.org609b7982013-02-07 16:44:46 +00001022 # The keys of by_config are tuples of values for the configuration
1023 # variables. The names of the variables (which must be the same for
1024 # every by_config key) are kept in config_variables. Initially by_config
1025 # is empty and we don't know what configuration variables will be used,
1026 # so config_variables also starts out empty. It will be set by the first
1027 # call to union() or merge_dependencies().
1028 self.by_config = {}
1029 self.config_variables = ()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001030
1031 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001032 """Adds variables from rhs (a Configs) to the existing variables.
1033 """
1034 config_variables = self.config_variables
1035 if not config_variables:
1036 config_variables = rhs.config_variables
1037 else:
1038 # We can't proceed if this isn't true since we don't know the correct
1039 # default values for extra variables. The variables are sorted so we
1040 # don't need to worry about permutations.
1041 if rhs.config_variables and rhs.config_variables != config_variables:
1042 raise ExecutionError(
1043 'Variables in merged .isolate files do not match: %r and %r' % (
1044 config_variables, rhs.config_variables))
1045
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001046 # Takes the first file comment, prefering lhs.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001047 out = Configs(self.file_comment or rhs.file_comment)
1048 out.config_variables = config_variables
1049 for config in set(self.by_config) | set(rhs.by_config):
1050 out.by_config[config] = union(
1051 self.by_config.get(config), rhs.by_config.get(config))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001052 return out
1053
benrg@chromium.org609b7982013-02-07 16:44:46 +00001054 def merge_dependencies(self, values, config_variables, configs):
1055 """Adds new dependencies to this object for the given configurations.
1056 Arguments:
1057 values: A variables dict as found in a .isolate file, e.g.,
1058 {KEY_TOUCHED: [...], 'command': ...}.
1059 config_variables: An ordered list of configuration variables, e.g.,
1060 ["OS", "chromeos"]. If this object already contains any dependencies,
1061 the configuration variables must match.
1062 configs: a list of tuples of values of the configuration variables,
1063 e.g., [("mac", 0), ("linux", 1)]. The dependencies in |values|
1064 are added to all of these configurations, and other configurations
1065 are unchanged.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001066 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001067 if not values:
1068 return
1069
1070 if not self.config_variables:
1071 self.config_variables = config_variables
1072 else:
1073 # See comment in Configs.union().
1074 assert self.config_variables == config_variables
1075
1076 for config in configs:
1077 self.by_config[config] = union(
1078 self.by_config.get(config), ConfigSettings(config, values))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001079
1080 def flatten(self):
1081 """Returns a flat dictionary representation of the configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001082 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001083 return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
1084
1085 def make_isolate_file(self):
1086 """Returns a dictionary suitable for writing to a .isolate file.
1087 """
1088 dependencies_by_config = self.flatten()
1089 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
1090 return convert_map_to_isolate_dict(configs_by_dependency,
1091 self.config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001092
1093
benrg@chromium.org609b7982013-02-07 16:44:46 +00001094# TODO(benrg): Remove this function when no old-format files are left.
1095def convert_old_to_new_format(value):
1096 """Converts from the old .isolate format, which only has one variable (OS),
1097 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
1098 and allows conditions that depend on the set of all OSes, to the new format,
1099 which allows any set of variables, has no hardcoded values, and only allows
1100 explicit positive tests of variable values.
1101 """
1102 conditions = value.setdefault('conditions', [])
1103 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
1104 return value # Nothing to change
1105
1106 def parse_condition(cond):
1107 return re.match(r'OS=="(\w+)"\Z', cond[0]).group(1)
1108
1109 oses = set(map(parse_condition, conditions))
1110 default_oses = set(['linux', 'mac', 'win'])
1111 oses = sorted(oses | default_oses)
1112
1113 def if_not_os(not_os, then):
1114 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
1115 return [expr, then]
1116
1117 conditions += [
1118 if_not_os(parse_condition(cond), cond.pop())
1119 for cond in conditions if len(cond) == 3
1120 ]
1121 if 'variables' in value:
1122 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
1123 conditions.sort()
1124
1125 return value
1126
1127
1128def load_isolate_as_config(isolate_dir, value, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001129 """Parses one .isolate file and returns a Configs() instance.
1130
1131 |value| is the loaded dictionary that was defined in the gyp file.
1132
1133 The expected format is strict, anything diverting from the format below will
1134 throw an assert:
1135 {
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001136 'includes': [
1137 'foo.isolate',
1138 ],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001139 'conditions': [
benrg@chromium.org609b7982013-02-07 16:44:46 +00001140 ['OS=="vms" and foo=42', {
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001141 'variables': {
benrg@chromium.org609b7982013-02-07 16:44:46 +00001142 'command': [
1143 ...
1144 ],
1145 'isolate_dependency_tracked': [
1146 ...
1147 ],
1148 'isolate_dependency_untracked': [
1149 ...
1150 ],
1151 'read_only': False,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001152 },
1153 }],
1154 ...
1155 ],
1156 }
1157 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001158 value = convert_old_to_new_format(value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001159
benrg@chromium.org609b7982013-02-07 16:44:46 +00001160 variables_and_values = {}
1161 verify_root(value, variables_and_values)
1162 if variables_and_values:
1163 config_variables, config_values = zip(
1164 *sorted(variables_and_values.iteritems()))
1165 all_configs = list(itertools.product(*config_values))
1166 else:
1167 config_variables = None
1168 all_configs = []
1169
1170 isolate = Configs(file_comment)
1171
1172 # Add configuration-specific variables.
1173 for expr, then in value.get('conditions', []):
1174 configs = match_configs(expr, config_variables, all_configs)
1175 isolate.merge_dependencies(then['variables'], config_variables, configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001176
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001177 # Load the includes.
1178 for include in value.get('includes', []):
1179 if os.path.isabs(include):
1180 raise ExecutionError(
1181 'Failed to load configuration; absolute include path \'%s\'' %
1182 include)
1183 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
1184 with open(included_isolate, 'r') as f:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001185 included_isolate = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001186 os.path.dirname(included_isolate),
1187 eval_content(f.read()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001188 None)
1189 isolate = union(isolate, included_isolate)
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001190
benrg@chromium.org609b7982013-02-07 16:44:46 +00001191 return isolate
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001192
1193
benrg@chromium.org609b7982013-02-07 16:44:46 +00001194def load_isolate_for_config(isolate_dir, content, variables):
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001195 """Loads the .isolate file and returns the information unprocessed but
1196 filtered for the specific OS.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001197
1198 Returns the command, dependencies and read_only flag. The dependencies are
1199 fixed to use os.path.sep.
1200 """
1201 # Load the .isolate file, process its conditions, retrieve the command and
1202 # dependencies.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001203 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
1204 try:
1205 config = tuple(variables[var] for var in isolate.config_variables)
1206 except KeyError:
1207 raise ExecutionError(
1208 'These configuration variables were missing from the command line: %s' %
1209 ', '.join(sorted(set(isolate.config_variables) - set(variables))))
1210 config = isolate.by_config.get(config)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001211 if not config:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001212 raise ExecutionError('Failed to load configuration for (%s) = (%s)' % (
1213 ', '.join(isolate.config_variables), ', '.join(map(str, config))))
1214 # Merge tracked and untracked variables, isolate.py doesn't care about the
1215 # trackability of the variables, only the build tool does.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001216 dependencies = [
1217 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1218 ]
1219 touched = [f.replace('/', os.path.sep) for f in config.touched]
1220 return config.command, dependencies, touched, config.read_only
1221
1222
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001223def chromium_save_isolated(isolated, data, variables):
1224 """Writes one or many .isolated files.
1225
1226 This slightly increases the cold cache cost but greatly reduce the warm cache
1227 cost by splitting low-churn files off the master .isolated file. It also
1228 reduces overall isolateserver memcache consumption.
1229 """
1230 slaves = []
1231
1232 def extract_into_included_isolated(prefix):
1233 new_slave = {'files': {}, 'os': data['os']}
1234 for f in data['files'].keys():
1235 if f.startswith(prefix):
1236 new_slave['files'][f] = data['files'].pop(f)
1237 if new_slave['files']:
1238 slaves.append(new_slave)
1239
1240 # Split test/data/ in its own .isolated file.
1241 extract_into_included_isolated(os.path.join('test', 'data', ''))
1242
1243 # Split everything out of PRODUCT_DIR in its own .isolated file.
1244 if variables.get('PRODUCT_DIR'):
1245 extract_into_included_isolated(variables['PRODUCT_DIR'])
1246
1247 files = [isolated]
1248 for index, f in enumerate(slaves):
1249 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
1250 trace_inputs.write_json(slavepath, f, True)
1251 data.setdefault('includes', []).append(
1252 isolateserver_archive.sha1_file(slavepath))
1253 files.append(slavepath)
1254
1255 trace_inputs.write_json(isolated, data, True)
1256 return files
1257
1258
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001259class Flattenable(object):
1260 """Represents data that can be represented as a json file."""
1261 MEMBERS = ()
1262
1263 def flatten(self):
1264 """Returns a json-serializable version of itself.
1265
1266 Skips None entries.
1267 """
1268 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1269 return dict((member, value) for member, value in items if value is not None)
1270
1271 @classmethod
1272 def load(cls, data):
1273 """Loads a flattened version."""
1274 data = data.copy()
1275 out = cls()
1276 for member in out.MEMBERS:
1277 if member in data:
1278 # Access to a protected member XXX of a client class
1279 # pylint: disable=W0212
1280 out._load_member(member, data.pop(member))
1281 if data:
1282 raise ValueError(
1283 'Found unexpected entry %s while constructing an object %s' %
1284 (data, cls.__name__), data, cls.__name__)
1285 return out
1286
1287 def _load_member(self, member, value):
1288 """Loads a member into self."""
1289 setattr(self, member, value)
1290
1291 @classmethod
1292 def load_file(cls, filename):
1293 """Loads the data from a file or return an empty instance."""
1294 out = cls()
1295 try:
1296 out = cls.load(trace_inputs.read_json(filename))
1297 logging.debug('Loaded %s(%s)' % (cls.__name__, filename))
1298 except (IOError, ValueError):
1299 logging.warn('Failed to load %s' % filename)
1300 return out
1301
1302
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001303class SavedState(Flattenable):
1304 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001305
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001306 This file caches the items calculated by this script and is used to increase
1307 the performance of the script. This file is not loaded by run_isolated.py.
1308 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001309
1310 It is important to note that the 'files' dict keys are using native OS path
1311 separator instead of '/' used in .isolate file.
1312 """
1313 MEMBERS = (
1314 'command',
1315 'files',
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001316 'isolate_file',
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001317 'isolated_files',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001318 'read_only',
1319 'relative_cwd',
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001320 'variables',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001321 )
1322
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001323 def __init__(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001324 super(SavedState, self).__init__()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001325 self.command = []
1326 self.files = {}
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001327 # Link back to the .isolate file.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001328 self.isolate_file = None
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001329 # Used to support/remember 'slave' .isolated files.
1330 self.isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001331 self.read_only = None
1332 self.relative_cwd = None
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001333 # Variables are saved so a user can use isolate.py after building and the
1334 # GYP variables are still defined.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001335 self.variables = {'OS': get_flavor()}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001336
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001337 def update(self, isolate_file, variables):
1338 """Updates the saved state with new data to keep GYP variables and internal
1339 reference to the original .isolate file.
1340 """
1341 self.isolate_file = isolate_file
1342 self.variables.update(variables)
1343
1344 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
1345 """Updates the saved state with data necessary to generate a .isolated file.
1346 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001347 self.command = command
1348 # Add new files.
1349 for f in infiles:
1350 self.files.setdefault(f, {})
1351 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001352 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001353 # Prune extraneous files that are not a dependency anymore.
1354 for f in set(self.files).difference(set(infiles).union(touched)):
1355 del self.files[f]
1356 if read_only is not None:
1357 self.read_only = read_only
1358 self.relative_cwd = relative_cwd
1359
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001360 def to_isolated(self):
1361 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001362
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001363 http://chromium.org/developers/testing/isolated-testing/design
1364 """
1365 def strip(data):
1366 """Returns a 'files' entry with only the whitelisted keys."""
1367 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
1368
1369 out = {
1370 'files': dict(
1371 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001372 'os': self.variables['OS'],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001373 }
1374 if self.command:
1375 out['command'] = self.command
1376 if self.read_only is not None:
1377 out['read_only'] = self.read_only
1378 if self.relative_cwd:
1379 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001380 return out
1381
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001382 @classmethod
1383 def load(cls, data):
1384 out = super(SavedState, cls).load(data)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001385 if 'os' in data:
1386 out.variables['OS'] = data['os']
1387 if out.variables['OS'] != get_flavor():
1388 raise run_isolated.ConfigError(
1389 'The .isolated.state file was created on another platform')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001390 if out.isolate_file:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001391 out.isolate_file = trace_inputs.get_native_path_case(
1392 unicode(out.isolate_file))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001393 return out
1394
1395 def __str__(self):
1396 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001397 out += ' command: %s\n' % self.command
1398 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001399 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001400 out += ' read_only: %s\n' % self.read_only
1401 out += ' relative_cwd: %s' % self.relative_cwd
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001402 out += ' isolated_files: %s' % self.isolated_files
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001403 out += ' variables: %s' % ''.join(
1404 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1405 out += ')'
1406 return out
1407
1408
1409class CompleteState(object):
1410 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001411 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001412 super(CompleteState, self).__init__()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001413 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001414 # Contains the data to ease developer's use-case but that is not strictly
1415 # necessary.
1416 self.saved_state = saved_state
1417
1418 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001419 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001420 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001421 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001422 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001423 isolated_filepath,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001424 SavedState.load_file(isolatedfile_to_state(isolated_filepath)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001425
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001426 def load_isolate(self, cwd, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001427 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001428 .isolate file.
1429
1430 Processes the loaded data, deduce root_dir, relative_cwd.
1431 """
1432 # Make sure to not depend on os.getcwd().
1433 assert os.path.isabs(isolate_file), isolate_file
1434 logging.info(
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001435 'CompleteState.load_isolate(%s, %s, %s, %s)',
1436 cwd, isolate_file, variables, ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001437 relative_base_dir = os.path.dirname(isolate_file)
1438
1439 # Processes the variables and update the saved state.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001440 variables = process_variables(cwd, variables, relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001441 self.saved_state.update(isolate_file, variables)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001442 variables = self.saved_state.variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001443
1444 with open(isolate_file, 'r') as f:
1445 # At that point, variables are not replaced yet in command and infiles.
1446 # infiles may contain directory entries and is in posix style.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001447 command, infiles, touched, read_only = load_isolate_for_config(
1448 os.path.dirname(isolate_file), f.read(), variables)
1449 command = [eval_variables(i, variables) for i in command]
1450 infiles = [eval_variables(f, variables) for f in infiles]
1451 touched = [eval_variables(f, variables) for f in touched]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001452 # root_dir is automatically determined by the deepest root accessed with the
1453 # form '../../foo/bar'.
1454 root_dir = determine_root_dir(relative_base_dir, infiles + touched)
1455 # The relative directory is automatically determined by the relative path
1456 # between root_dir and the directory containing the .isolate file,
1457 # isolate_base_dir.
1458 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
1459 # Normalize the files based to root_dir. It is important to keep the
1460 # trailing os.path.sep at that step.
1461 infiles = [
1462 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1463 for f in infiles
1464 ]
1465 touched = [
1466 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1467 for f in touched
1468 ]
1469 # Expand the directories by listing each file inside. Up to now, trailing
1470 # os.path.sep must be kept. Do not expand 'touched'.
1471 infiles = expand_directories_and_symlinks(
1472 root_dir,
1473 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001474 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
1475 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001476
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001477 # Finally, update the new data to be able to generate the foo.isolated file,
1478 # the file that is used by run_isolated.py.
1479 self.saved_state.update_isolated(
1480 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001481 logging.debug(self)
1482
maruel@chromium.org9268f042012-10-17 17:36:41 +00001483 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001484 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001485
maruel@chromium.org9268f042012-10-17 17:36:41 +00001486 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1487 file is tainted.
1488
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001489 See process_input() for more information.
1490 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001491 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001492 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001493 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001494 else:
1495 filepath = os.path.join(self.root_dir, infile)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001496 self.saved_state.files[infile] = process_input(
1497 filepath,
1498 self.saved_state.files[infile],
1499 self.saved_state.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001500
1501 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001502 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001503 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001504 self.saved_state.isolated_files = chromium_save_isolated(
1505 self.isolated_filepath,
1506 self.saved_state.to_isolated(),
1507 self.saved_state.variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001508 total_bytes = sum(
1509 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001511 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001512 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001513 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001514 logging.debug('Dumping to %s' % saved_state_file)
1515 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1516
1517 @property
1518 def root_dir(self):
1519 """isolate_file is always inside relative_cwd relative to root_dir."""
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001520 if not self.saved_state.isolate_file:
1521 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001522 isolate_dir = os.path.dirname(self.saved_state.isolate_file)
1523 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001524 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001525 return isolate_dir
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001526 assert isolate_dir.endswith(self.saved_state.relative_cwd), (
1527 isolate_dir, self.saved_state.relative_cwd)
1528 return isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001529
1530 @property
1531 def resultdir(self):
1532 """Directory containing the results, usually equivalent to the variable
1533 PRODUCT_DIR.
1534 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001535 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001536
1537 def __str__(self):
1538 def indent(data, indent_length):
1539 """Indents text."""
1540 spacing = ' ' * indent_length
1541 return ''.join(spacing + l for l in str(data).splitlines(True))
1542
1543 out = '%s(\n' % self.__class__.__name__
1544 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001545 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1546 return out
1547
1548
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001549def load_complete_state(options, cwd, subdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001550 """Loads a CompleteState.
1551
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001552 This includes data from .isolate and .isolated.state files. Never reads the
1553 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001554
1555 Arguments:
1556 options: Options instance generated with OptionParserIsolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001557 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001558 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001559 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001560 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001561 else:
1562 # Constructs a dummy object that cannot be saved. Useful for temporary
1563 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001564 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001565 options.isolate = options.isolate or complete_state.saved_state.isolate_file
1566 if not options.isolate:
1567 raise ExecutionError('A .isolate file is required.')
1568 if (complete_state.saved_state.isolate_file and
1569 options.isolate != complete_state.saved_state.isolate_file):
1570 raise ExecutionError(
1571 '%s and %s do not match.' % (
1572 options.isolate, complete_state.saved_state.isolate_file))
1573
1574 # Then load the .isolate and expands directories.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001575 complete_state.load_isolate(
1576 cwd, options.isolate, options.variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001577
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001578 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001579 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001580 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001581 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1582 subdir = subdir.replace('/', os.path.sep)
1583 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001584 return complete_state
1585
1586
1587def read_trace_as_isolate_dict(complete_state):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001588 """Reads a trace and returns the .isolate dictionary.
1589
1590 Returns exceptions during the log parsing so it can be re-raised.
1591 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001592 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001593 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001594 if not os.path.isfile(logfile):
1595 raise ExecutionError(
1596 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1597 try:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00001598 data = api.parse_log(logfile, default_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001599 exceptions = [i['exception'] for i in data if 'exception' in i]
1600 results = (i['results'] for i in data if 'results' in i)
1601 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1602 files = set(sum((result.existent for result in results_stripped), []))
1603 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001604 value = generate_isolate(
1605 tracked,
1606 [],
1607 touched,
1608 complete_state.root_dir,
1609 complete_state.saved_state.variables,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001610 complete_state.saved_state.relative_cwd)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001611 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001612 except trace_inputs.TracingFailure, e:
1613 raise ExecutionError(
1614 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001615 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001616
1617
1618def print_all(comment, data, stream):
1619 """Prints a complete .isolate file and its top-level file comment into a
1620 stream.
1621 """
1622 if comment:
1623 stream.write(comment)
1624 pretty_print(data, stream)
1625
1626
1627def merge(complete_state):
1628 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001629 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001630
1631 # Now take that data and union it into the original .isolate file.
1632 with open(complete_state.saved_state.isolate_file, 'r') as f:
1633 prev_content = f.read()
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001634 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001635 prev_config = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001636 isolate_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001637 eval_content(prev_content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001638 extract_comment(prev_content))
1639 new_config = load_isolate_as_config(isolate_dir, value, '')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001640 config = union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001641 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001642 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001643 with open(complete_state.saved_state.isolate_file, 'wb') as f:
1644 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001645 if exceptions:
1646 # It got an exception, raise the first one.
1647 raise \
1648 exceptions[0][0], \
1649 exceptions[0][1], \
1650 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001651
1652
1653def CMDcheck(args):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001654 """Checks that all the inputs are present and generates .isolated."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001655 parser = OptionParserIsolate(command='check')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001656 parser.add_option('--subdir', help='Filters to a subdirectory')
1657 options, args = parser.parse_args(args)
1658 if args:
1659 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001660 complete_state = load_complete_state(options, os.getcwd(), options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001661
1662 # Nothing is done specifically. Just store the result and state.
1663 complete_state.save_files()
1664 return 0
1665
1666
1667def CMDhashtable(args):
1668 """Creates a hash table content addressed object store.
1669
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001670 All the files listed in the .isolated file are put in the output directory
1671 with the file name being the sha-1 of the file's content.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001672 """
1673 parser = OptionParserIsolate(command='hashtable')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001674 parser.add_option('--subdir', help='Filters to a subdirectory')
1675 options, args = parser.parse_args(args)
1676 if args:
1677 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001678
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001679 with run_isolated.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001680 success = False
1681 try:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001682 complete_state = load_complete_state(options, os.getcwd(), options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001683 options.outdir = (
1684 options.outdir or os.path.join(complete_state.resultdir, 'hashtable'))
1685 # Make sure that complete_state isn't modified until save_files() is
1686 # called, because any changes made to it here will propagate to the files
1687 # created (which is probably not intended).
1688 complete_state.save_files()
1689
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001690 infiles = complete_state.saved_state.files
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001691 # Add all the .isolated files.
1692 for item in complete_state.saved_state.isolated_files:
1693 item_path = os.path.join(
1694 os.path.dirname(complete_state.isolated_filepath), item)
1695 with open(item_path, 'rb') as f:
1696 content = f.read()
1697 isolated_metadata = {
1698 'h': hashlib.sha1(content).hexdigest(),
1699 's': len(content),
1700 'priority': '0'
1701 }
1702 infiles[item_path] = isolated_metadata
1703
1704 logging.info('Creating content addressed object store with %d item',
1705 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001706
1707 if re.match(r'^https?://.+$', options.outdir):
maruel@chromium.orgc6f90062012-11-07 18:32:22 +00001708 isolateserver_archive.upload_sha1_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001709 base_url=options.outdir,
1710 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00001711 infiles=infiles,
1712 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001713 else:
1714 recreate_tree(
1715 outdir=options.outdir,
1716 indir=complete_state.root_dir,
1717 infiles=infiles,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001718 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001719 as_sha1=True)
1720 success = True
1721 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001722 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001723 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001724 if not success and os.path.isfile(options.isolated):
1725 os.remove(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001726
1727
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001728def CMDmerge(args):
1729 """Reads and merges the data from the trace back into the original .isolate.
1730
1731 Ignores --outdir.
1732 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001733 parser = OptionParserIsolate(command='merge', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001734 options, args = parser.parse_args(args)
1735 if args:
1736 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001737 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001738 merge(complete_state)
1739 return 0
1740
1741
1742def CMDread(args):
1743 """Reads the trace file generated with command 'trace'.
1744
1745 Ignores --outdir.
1746 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001747 parser = OptionParserIsolate(command='read', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001748 options, args = parser.parse_args(args)
1749 if args:
1750 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001751 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001752 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001753 pretty_print(value, sys.stdout)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001754 if exceptions:
1755 # It got an exception, raise the first one.
1756 raise \
1757 exceptions[0][0], \
1758 exceptions[0][1], \
1759 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001760 return 0
1761
1762
1763def CMDremap(args):
1764 """Creates a directory with all the dependencies mapped into it.
1765
1766 Useful to test manually why a test is failing. The target executable is not
1767 run.
1768 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001769 parser = OptionParserIsolate(command='remap', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001770 options, args = parser.parse_args(args)
1771 if args:
1772 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001773 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001774
1775 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001776 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001777 'isolate', complete_state.root_dir)
1778 else:
1779 if not os.path.isdir(options.outdir):
1780 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001781 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001782 if len(os.listdir(options.outdir)):
1783 raise ExecutionError('Can\'t remap in a non-empty directory')
1784 recreate_tree(
1785 outdir=options.outdir,
1786 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001787 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001788 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001789 as_sha1=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001790 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001791 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001792
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001793 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001794 complete_state.save_files()
1795 return 0
1796
1797
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001798def CMDrewrite(args):
1799 """Rewrites a .isolate file into the canonical format."""
1800 parser = OptionParserIsolate(command='rewrite', require_isolated=False)
1801 options, args = parser.parse_args(args)
1802 if args:
1803 parser.error('Unsupported argument: %s' % args)
1804
1805 if options.isolated:
1806 # Load the previous state if it was present. Namely, "foo.isolated.state".
1807 complete_state = CompleteState.load_files(options.isolated)
1808 else:
1809 # Constructs a dummy object that cannot be saved. Useful for temporary
1810 # commands like 'run'.
1811 complete_state = CompleteState(None, SavedState())
1812 isolate = options.isolate or complete_state.saved_state.isolate_file
1813 if not isolate:
1814 raise ExecutionError('A .isolate file is required.')
1815 with open(isolate, 'r') as f:
1816 content = f.read()
1817 config = load_isolate_as_config(
1818 os.path.dirname(os.path.abspath(isolate)),
1819 eval_content(content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001820 extract_comment(content))
1821 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001822 print('Updating %s' % isolate)
1823 with open(isolate, 'wb') as f:
1824 print_all(config.file_comment, data, f)
1825 return 0
1826
1827
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001828def CMDrun(args):
1829 """Runs the test executable in an isolated (temporary) directory.
1830
1831 All the dependencies are mapped into the temporary directory and the
1832 directory is cleaned up after the target exits. Warning: if -outdir is
1833 specified, it is deleted upon exit.
1834
1835 Argument processing stops at the first non-recognized argument and these
1836 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001837 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001838 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001839 parser = OptionParserIsolate(command='run', require_isolated=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001840 parser.enable_interspersed_args()
1841 options, args = parser.parse_args(args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001842 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001843 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001844 if not cmd:
1845 raise ExecutionError('No command to run')
1846 cmd = trace_inputs.fix_python_path(cmd)
1847 try:
1848 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001849 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001850 'isolate', complete_state.root_dir)
1851 else:
1852 if not os.path.isdir(options.outdir):
1853 os.makedirs(options.outdir)
1854 recreate_tree(
1855 outdir=options.outdir,
1856 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001857 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001858 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001859 as_sha1=False)
1860 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001861 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001862 if not os.path.isdir(cwd):
1863 # It can happen when no files are mapped from the directory containing the
1864 # .isolate file. But the directory must exist to be the current working
1865 # directory.
1866 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001867 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001868 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001869 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1870 result = subprocess.call(cmd, cwd=cwd)
1871 finally:
1872 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001873 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001874
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001875 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001876 complete_state.save_files()
1877 return result
1878
1879
1880def CMDtrace(args):
1881 """Traces the target using trace_inputs.py.
1882
1883 It runs the executable without remapping it, and traces all the files it and
1884 its child processes access. Then the 'read' command can be used to generate an
1885 updated .isolate file out of it.
1886
1887 Argument processing stops at the first non-recognized argument and these
1888 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001889 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001890 """
1891 parser = OptionParserIsolate(command='trace')
1892 parser.enable_interspersed_args()
1893 parser.add_option(
1894 '-m', '--merge', action='store_true',
1895 help='After tracing, merge the results back in the .isolate file')
1896 options, args = parser.parse_args(args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001897 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001898 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001899 if not cmd:
1900 raise ExecutionError('No command to run')
1901 cmd = trace_inputs.fix_python_path(cmd)
1902 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001903 unicode(complete_state.root_dir),
1904 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001905 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1906 if not os.path.isfile(cmd[0]):
1907 raise ExecutionError(
1908 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001909 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1910 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001911 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001912 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001913 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001914 try:
1915 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001916 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001917 cmd,
1918 cwd,
1919 'default',
1920 True)
1921 except trace_inputs.TracingFailure, e:
1922 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1923
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001924 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001925 logging.error(
1926 'Tracer exited with %d, which means the tests probably failed so the '
1927 'trace is probably incomplete.', result)
1928 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001929
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001930 complete_state.save_files()
1931
1932 if options.merge:
1933 merge(complete_state)
1934
1935 return result
1936
1937
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001938def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001939 """Adds --isolated and --variable to an OptionParser."""
1940 parser.add_option(
1941 '-s', '--isolated',
1942 metavar='FILE',
1943 help='.isolated file to generate or read')
1944 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001945 parser.add_option(
1946 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001947 dest='isolated',
1948 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001949 default_variables = [('OS', get_flavor())]
1950 if sys.platform in ('win32', 'cygwin'):
1951 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
1952 else:
1953 default_variables.append(('EXECUTABLE_SUFFIX', ''))
1954 parser.add_option(
1955 '-V', '--variable',
1956 nargs=2,
1957 action='append',
1958 default=default_variables,
1959 dest='variables',
1960 metavar='FOO BAR',
1961 help='Variables to process in the .isolate file, default: %default. '
1962 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001963 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001964
1965
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001966def parse_variable_option(parser, options, cwd, require_isolated):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001967 """Processes --isolated and --variable."""
1968 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001969 options.isolated = os.path.normpath(
1970 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001971 if require_isolated and not options.isolated:
csharp@chromium.org707f0452012-11-26 21:50:40 +00001972 parser.error('--isolated is required. Visit http://chromium.org/developers/'
1973 'testing/isolated-testing#TOC-Where-can-I-find-the-.isolated-'
1974 'file- to see how to create the .isolated file.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001975 if options.isolated and not options.isolated.endswith('.isolated'):
1976 parser.error('--isolated value must end with \'.isolated\'')
benrg@chromium.org609b7982013-02-07 16:44:46 +00001977 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
1978 # but it wouldn't be backward compatible.
1979 def try_make_int(s):
1980 try:
1981 return int(s)
1982 except ValueError:
1983 return s
1984 options.variables = dict((k, try_make_int(v)) for k, v in options.variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001985
1986
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001987class OptionParserIsolate(trace_inputs.OptionParserWithNiceDescription):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001988 """Adds automatic --isolate, --isolated, --out and --variable handling."""
1989 def __init__(self, require_isolated=True, **kwargs):
maruel@chromium.org55276902012-10-05 20:56:19 +00001990 trace_inputs.OptionParserWithNiceDescription.__init__(
1991 self,
1992 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1993 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001994 group = optparse.OptionGroup(self, "Common options")
1995 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001996 '-i', '--isolate',
1997 metavar='FILE',
1998 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001999 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002000 group.add_option(
2001 '-o', '--outdir', metavar='DIR',
2002 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002003 'Defaults: run|remap: a /tmp subdirectory, others: '
2004 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00002005 group.add_option(
2006 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002007 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
2008 help='Indicates that invalid entries in the isolated file to be '
2009 'only be logged and not stop processing. Defaults to True if '
2010 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002011 self.add_option_group(group)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002012 self.require_isolated = require_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002013
2014 def parse_args(self, *args, **kwargs):
2015 """Makes sure the paths make sense.
2016
2017 On Windows, / and \ are often mixed together in a path.
2018 """
2019 options, args = trace_inputs.OptionParserWithNiceDescription.parse_args(
2020 self, *args, **kwargs)
2021 if not self.allow_interspersed_args and args:
2022 self.error('Unsupported argument: %s' % args)
2023
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002024 cwd = os.getcwd()
2025 parse_variable_option(self, options, cwd, self.require_isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002026
2027 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002028 # TODO(maruel): Work with non-ASCII.
2029 # The path must be in native path case for tracing purposes.
2030 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
2031 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
2032 options.isolate = trace_inputs.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002033
2034 if options.outdir and not re.match(r'^https?://.+$', options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002035 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
2036 # outdir doesn't need native path case since tracing is never done from
2037 # there.
2038 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002039
2040 return options, args
2041
2042
2043### Glue code to make all the commands works magically.
2044
2045
2046CMDhelp = trace_inputs.CMDhelp
2047
2048
2049def main(argv):
2050 try:
2051 return trace_inputs.main_impl(argv)
2052 except (
2053 ExecutionError,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002054 run_isolated.MappingError,
2055 run_isolated.ConfigError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002056 sys.stderr.write('\nError: ')
2057 sys.stderr.write(str(e))
2058 sys.stderr.write('\n')
2059 return 1
2060
2061
2062if __name__ == '__main__':
2063 sys.exit(main(sys.argv[1:]))