blob: 290ffbcf42128dededaacda08b1d86429f2f7984 [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
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000014import copy
15import hashlib
16import logging
17import optparse
18import os
19import posixpath
20import re
21import stat
22import subprocess
23import sys
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000024
maruel@chromium.orgc6f90062012-11-07 18:32:22 +000025import isolateserver_archive
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000026import run_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000027import trace_inputs
28
29# Import here directly so isolate is easier to use as a library.
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000030from run_isolated import get_flavor
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000031
32
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000033PATH_VARIABLES = ('DEPTH', 'PRODUCT_DIR')
34DEFAULT_OSES = ('linux', 'mac', 'win')
35
36# Files that should be 0-length when mapped.
37KEY_TOUCHED = 'isolate_dependency_touched'
38# Files that should be tracked by the build tool.
39KEY_TRACKED = 'isolate_dependency_tracked'
40# Files that should not be tracked by the build tool.
41KEY_UNTRACKED = 'isolate_dependency_untracked'
42
43_GIT_PATH = os.path.sep + '.git'
44_SVN_PATH = os.path.sep + '.svn'
45
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000046
47class ExecutionError(Exception):
48 """A generic error occurred."""
49 def __str__(self):
50 return self.args[0]
51
52
53### Path handling code.
54
55
56def relpath(path, root):
57 """os.path.relpath() that keeps trailing os.path.sep."""
58 out = os.path.relpath(path, root)
59 if path.endswith(os.path.sep):
60 out += os.path.sep
61 return out
62
63
64def normpath(path):
65 """os.path.normpath() that keeps trailing os.path.sep."""
66 out = os.path.normpath(path)
67 if path.endswith(os.path.sep):
68 out += os.path.sep
69 return out
70
71
72def posix_relpath(path, root):
73 """posix.relpath() that keeps trailing slash."""
74 out = posixpath.relpath(path, root)
75 if path.endswith('/'):
76 out += '/'
77 return out
78
79
80def cleanup_path(x):
81 """Cleans up a relative path. Converts any os.path.sep to '/' on Windows."""
82 if x:
83 x = x.rstrip(os.path.sep).replace(os.path.sep, '/')
84 if x == '.':
85 x = ''
86 if x:
87 x += '/'
88 return x
89
90
91def default_blacklist(f):
92 """Filters unimportant files normally ignored."""
93 return (
94 f.endswith(('.pyc', '.run_test_cases', 'testserver.log')) or
95 _GIT_PATH in f or
96 _SVN_PATH in f or
97 f in ('.git', '.svn'))
98
99
100def expand_directory_and_symlink(indir, relfile, blacklist):
101 """Expands a single input. It can result in multiple outputs.
102
103 This function is recursive when relfile is a directory or a symlink.
104
105 Note: this code doesn't properly handle recursive symlink like one created
106 with:
107 ln -s .. foo
108 """
109 if os.path.isabs(relfile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000110 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000111 'Can\'t map absolute path %s' % relfile)
112
113 infile = normpath(os.path.join(indir, relfile))
114 if not infile.startswith(indir):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000115 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000116 'Can\'t map file %s outside %s' % (infile, indir))
117
118 if sys.platform != 'win32':
119 # Look if any item in relfile is a symlink.
120 base, symlink, rest = trace_inputs.split_at_symlink(indir, relfile)
121 if symlink:
122 # Append everything pointed by the symlink. If the symlink is recursive,
123 # this code blows up.
124 symlink_relfile = os.path.join(base, symlink)
125 symlink_path = os.path.join(indir, symlink_relfile)
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000126 # readlink doesn't exist on Windows.
127 pointed = os.readlink(symlink_path) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000128 dest_infile = normpath(
129 os.path.join(os.path.dirname(symlink_path), pointed))
130 if rest:
131 dest_infile = trace_inputs.safe_join(dest_infile, rest)
132 if not dest_infile.startswith(indir):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000133 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000134 'Can\'t map symlink reference %s (from %s) ->%s outside of %s' %
135 (symlink_relfile, relfile, dest_infile, indir))
136 if infile.startswith(dest_infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000137 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000138 'Can\'t map recursive symlink reference %s->%s' %
139 (symlink_relfile, dest_infile))
140 dest_relfile = dest_infile[len(indir)+1:]
141 logging.info('Found symlink: %s -> %s' % (symlink_relfile, dest_relfile))
142 out = expand_directory_and_symlink(indir, dest_relfile, blacklist)
143 # Add the symlink itself.
144 out.append(symlink_relfile)
145 return out
146
147 if relfile.endswith(os.path.sep):
148 if not os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000149 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000150 '%s is not a directory but ends with "%s"' % (infile, os.path.sep))
151
152 outfiles = []
153 for filename in os.listdir(infile):
154 inner_relfile = os.path.join(relfile, filename)
155 if blacklist(inner_relfile):
156 continue
157 if os.path.isdir(os.path.join(indir, inner_relfile)):
158 inner_relfile += os.path.sep
159 outfiles.extend(
160 expand_directory_and_symlink(indir, inner_relfile, blacklist))
161 return outfiles
162 else:
163 # Always add individual files even if they were blacklisted.
164 if os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000165 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000166 'Input directory %s must have a trailing slash' % infile)
167
168 if not os.path.isfile(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000169 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000170 'Input file %s doesn\'t exist' % infile)
171
172 return [relfile]
173
174
csharp@chromium.org01856802012-11-12 17:48:13 +0000175def expand_directories_and_symlinks(indir, infiles, blacklist,
176 ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000177 """Expands the directories and the symlinks, applies the blacklist and
178 verifies files exist.
179
180 Files are specified in os native path separator.
181 """
182 outfiles = []
183 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +0000184 try:
185 outfiles.extend(expand_directory_and_symlink(indir, relfile, blacklist))
186 except run_isolated.MappingError as e:
187 if ignore_broken_items:
188 logging.info('warning: %s', e)
189 else:
190 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000191 return outfiles
192
193
194def recreate_tree(outdir, indir, infiles, action, as_sha1):
195 """Creates a new tree with only the input files in it.
196
197 Arguments:
198 outdir: Output directory to create the files in.
199 indir: Root directory the infiles are based in.
200 infiles: dict of files to map from |indir| to |outdir|.
201 action: See assert below.
202 as_sha1: Output filename is the sha1 instead of relfile.
203 """
204 logging.info(
205 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_sha1=%s)' %
206 (outdir, indir, len(infiles), action, as_sha1))
207
208 assert action in (
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000209 run_isolated.HARDLINK,
210 run_isolated.SYMLINK,
211 run_isolated.COPY)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000212 outdir = os.path.normpath(outdir)
213 if not os.path.isdir(outdir):
214 logging.info ('Creating %s' % outdir)
215 os.makedirs(outdir)
216 # Do not call abspath until the directory exists.
217 outdir = os.path.abspath(outdir)
218
219 for relfile, metadata in infiles.iteritems():
220 infile = os.path.join(indir, relfile)
221 if as_sha1:
222 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000223 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000224 # Skip links when storing a hashtable.
225 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000226 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000227 if os.path.isfile(outfile):
228 # Just do a quick check that the file size matches. No need to stat()
229 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000230 if not 's' in metadata:
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000231 raise run_isolated.MappingError(
232 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000233 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000234 continue
235 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000236 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000237 os.remove(outfile)
238 else:
239 outfile = os.path.join(outdir, relfile)
240 outsubdir = os.path.dirname(outfile)
241 if not os.path.isdir(outsubdir):
242 os.makedirs(outsubdir)
243
244 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000245 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000246 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000247 if 'l' in metadata:
248 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000249 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000250 # symlink doesn't exist on Windows.
251 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000252 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000253 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000254
255
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000256def process_input(filepath, prevdict, read_only):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000257 """Processes an input file, a dependency, and return meta data about it.
258
259 Arguments:
260 - filepath: File to act on.
261 - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
262 to skip recalculating the hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000263 - read_only: If True, the file mode is manipulated. In practice, only save
264 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
265 windows, mode is not set since all files are 'executable' by
266 default.
267
268 Behaviors:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000269 - Retrieves the file mode, file size, file timestamp, file link
270 destination if it is a file link and calcultate the SHA-1 of the file's
271 content if the path points to a file and not a symlink.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000272 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000273 out = {}
274 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000275 # if prevdict.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000276 # # The file's content is ignored. Skip the time and hard code mode.
277 # if get_flavor() != 'win':
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000278 # out['m'] = stat.S_IRUSR | stat.S_IRGRP
279 # out['s'] = 0
280 # out['h'] = SHA_1_NULL
281 # out['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000282 # return out
283
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000284 # Always check the file stat and check if it is a link. The timestamp is used
285 # to know if the file's content/symlink destination should be looked into.
286 # E.g. only reuse from prevdict if the timestamp hasn't changed.
287 # There is the risk of the file's timestamp being reset to its last value
288 # manually while its content changed. We don't protect against that use case.
289 try:
290 filestats = os.lstat(filepath)
291 except OSError:
292 # The file is not present.
293 raise run_isolated.MappingError('%s is missing' % filepath)
294 is_link = stat.S_ISLNK(filestats.st_mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000295
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000296 if get_flavor() != 'win':
297 # Ignore file mode on Windows since it's not really useful there.
298 filemode = stat.S_IMODE(filestats.st_mode)
299 # Remove write access for group and all access to 'others'.
300 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
301 if read_only:
302 filemode &= ~stat.S_IWUSR
303 if filemode & stat.S_IXUSR:
304 filemode |= stat.S_IXGRP
305 else:
306 filemode &= ~stat.S_IXGRP
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000307 out['m'] = filemode
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000308
309 # Used to skip recalculating the hash or link destination. Use the most recent
310 # update time.
311 # TODO(maruel): Save it in the .state file instead of .isolated so the
312 # .isolated file is deterministic.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000313 out['t'] = int(round(filestats.st_mtime))
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000314
315 if not is_link:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000316 out['s'] = filestats.st_size
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000317 # If the timestamp wasn't updated and the file size is still the same, carry
318 # on the sha-1.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000319 if (prevdict.get('t') == out['t'] and
320 prevdict.get('s') == out['s']):
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000321 # Reuse the previous hash if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000322 out['h'] = prevdict.get('h')
323 if not out.get('h'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000324 with open(filepath, 'rb') as f:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000325 out['h'] = hashlib.sha1(f.read()).hexdigest()
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000326 else:
327 # If the timestamp wasn't updated, carry on the link destination.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000328 if prevdict.get('t') == out['t']:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000329 # Reuse the previous link destination if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000330 out['l'] = prevdict.get('l')
331 if out.get('l') is None:
332 out['l'] = os.readlink(filepath) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000333 return out
334
335
336### Variable stuff.
337
338
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000339def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000340 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000341 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000342
343
344def determine_root_dir(relative_root, infiles):
345 """For a list of infiles, determines the deepest root directory that is
346 referenced indirectly.
347
348 All arguments must be using os.path.sep.
349 """
350 # The trick used to determine the root directory is to look at "how far" back
351 # up it is looking up.
352 deepest_root = relative_root
353 for i in infiles:
354 x = relative_root
355 while i.startswith('..' + os.path.sep):
356 i = i[3:]
357 assert not i.startswith(os.path.sep)
358 x = os.path.dirname(x)
359 if deepest_root.startswith(x):
360 deepest_root = x
361 logging.debug(
362 'determine_root_dir(%s, %d files) -> %s' % (
363 relative_root, len(infiles), deepest_root))
364 return deepest_root
365
366
367def replace_variable(part, variables):
368 m = re.match(r'<\(([A-Z_]+)\)', part)
369 if m:
370 if m.group(1) not in variables:
371 raise ExecutionError(
372 'Variable "%s" was not found in %s.\nDid you forget to specify '
373 '--variable?' % (m.group(1), variables))
374 return variables[m.group(1)]
375 return part
376
377
378def process_variables(variables, relative_base_dir):
379 """Processes path variables as a special case and returns a copy of the dict.
380
381 For each 'path' variable: first normalizes it, verifies it exists, converts it
382 to an absolute path, then sets it as relative to relative_base_dir.
383 """
384 variables = variables.copy()
385 for i in PATH_VARIABLES:
386 if i not in variables:
387 continue
388 variable = os.path.normpath(variables[i])
389 if not os.path.isdir(variable):
390 raise ExecutionError('%s=%s is not a directory' % (i, variable))
391 # Variables could contain / or \ on windows. Always normalize to
392 # os.path.sep.
393 variable = os.path.abspath(variable.replace('/', os.path.sep))
394 # All variables are relative to the .isolate file.
395 variables[i] = os.path.relpath(variable, relative_base_dir)
396 return variables
397
398
399def eval_variables(item, variables):
400 """Replaces the .isolate variables in a string item.
401
402 Note that the .isolate format is a subset of the .gyp dialect.
403 """
404 return ''.join(
405 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
406
407
408def classify_files(root_dir, tracked, untracked):
409 """Converts the list of files into a .isolate 'variables' dictionary.
410
411 Arguments:
412 - tracked: list of files names to generate a dictionary out of that should
413 probably be tracked.
414 - untracked: list of files names that must not be tracked.
415 """
416 # These directories are not guaranteed to be always present on every builder.
417 OPTIONAL_DIRECTORIES = (
418 'test/data/plugin',
419 'third_party/WebKit/LayoutTests',
420 )
421
422 new_tracked = []
423 new_untracked = list(untracked)
424
425 def should_be_tracked(filepath):
426 """Returns True if it is a file without whitespace in a non-optional
427 directory that has no symlink in its path.
428 """
429 if filepath.endswith('/'):
430 return False
431 if ' ' in filepath:
432 return False
433 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
434 return False
435 # Look if any element in the path is a symlink.
436 split = filepath.split('/')
437 for i in range(len(split)):
438 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
439 return False
440 return True
441
442 for filepath in sorted(tracked):
443 if should_be_tracked(filepath):
444 new_tracked.append(filepath)
445 else:
446 # Anything else.
447 new_untracked.append(filepath)
448
449 variables = {}
450 if new_tracked:
451 variables[KEY_TRACKED] = sorted(new_tracked)
452 if new_untracked:
453 variables[KEY_UNTRACKED] = sorted(new_untracked)
454 return variables
455
456
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000457def chromium_fix(f, variables):
458 """Fixes an isolate dependnecy with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000459 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
460 # separator.
461 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000462 # Ignored items.
463 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000464 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000465 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000466 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000467 # The fr.pak is occuring when tracing on a system with a French locale.
468 '<(PRODUCT_DIR)/locales/fr.pak',
469 # 'First Run' is not created by the compile, but by the test itself.
470 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000471
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000472 # Blacklist logs and other unimportant files.
473 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
474 logging.debug('Ignoring %s', f)
475 return None
476
maruel@chromium.org7650e422012-11-16 21:56:42 +0000477 EXECUTABLE = re.compile(
478 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
479 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
480 r'$')
481 match = EXECUTABLE.match(f)
482 if match:
483 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
484
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000485 if sys.platform == 'darwin':
486 # On OSX, the name of the output is dependent on gyp define, it can be
487 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
488 # Framework.framework'. Furthermore, they are versioned with a gyp
489 # variable. To lower the complexity of the .isolate file, remove all the
490 # individual entries that show up under any of the 4 entries and replace
491 # them with the directory itself. Overall, this results in a bit more
492 # files than strictly necessary.
493 OSX_BUNDLES = (
494 '<(PRODUCT_DIR)/Chromium Framework.framework/',
495 '<(PRODUCT_DIR)/Chromium.app/',
496 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
497 '<(PRODUCT_DIR)/Google Chrome.app/',
498 )
499 for prefix in OSX_BUNDLES:
500 if f.startswith(prefix):
501 # Note this result in duplicate values, so the a set() must be used to
502 # remove duplicates.
503 return prefix
504 return f
505
506
507def generate_simplified(
508 tracked, untracked, touched, root_dir, variables, relative_cwd):
509 """Generates a clean and complete .isolate 'variables' dictionary.
510
511 Cleans up and extracts only files from within root_dir then processes
512 variables and relative_cwd.
513 """
514 root_dir = os.path.realpath(root_dir)
515 logging.info(
516 'generate_simplified(%d files, %s, %s, %s)' %
517 (len(tracked) + len(untracked) + len(touched),
518 root_dir, variables, relative_cwd))
519
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000520 # Preparation work.
521 relative_cwd = cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000522 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000523 # Creates the right set of variables here. We only care about PATH_VARIABLES.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000524 path_variables = dict(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000525 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
526 for k in PATH_VARIABLES if k in variables)
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000527 variables = variables.copy()
528 variables.update(path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000529
530 # Actual work: Process the files.
531 # TODO(maruel): if all the files in a directory are in part tracked and in
532 # part untracked, the directory will not be extracted. Tracked files should be
533 # 'promoted' to be untracked as needed.
534 tracked = trace_inputs.extract_directories(
535 root_dir, tracked, default_blacklist)
536 untracked = trace_inputs.extract_directories(
537 root_dir, untracked, default_blacklist)
538 # touched is not compressed, otherwise it would result in files to be archived
539 # that we don't need.
540
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000541 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000542 def fix(f):
543 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000544 # Important, GYP stores the files with / and not \.
545 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000546 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000547 # If it's not already a variable.
548 if not f.startswith('<'):
549 # relative_cwd is usually the directory containing the gyp file. It may be
550 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000551 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000552 # Convert the whole thing to / since it's isolate's speak.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000553 f = posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000554 posixpath.join(root_dir_posix, f),
555 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000556
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000557 for variable, root_path in path_variables.iteritems():
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000558 if f.startswith(root_path):
559 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000560 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000561 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000562 return f
563
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000564 def fix_all(items):
565 """Reduces the items to convert variables, removes unneeded items, apply
566 chromium-specific fixes and only return unique items.
567 """
568 variables_converted = (fix(f.path) for f in items)
569 chromium_fixed = (chromium_fix(f, variables) for f in variables_converted)
570 return set(f for f in chromium_fixed if f)
571
572 tracked = fix_all(tracked)
573 untracked = fix_all(untracked)
574 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000575 out = classify_files(root_dir, tracked, untracked)
576 if touched:
577 out[KEY_TOUCHED] = sorted(touched)
578 return out
579
580
581def generate_isolate(
582 tracked, untracked, touched, root_dir, variables, relative_cwd):
583 """Generates a clean and complete .isolate file."""
584 result = generate_simplified(
585 tracked, untracked, touched, root_dir, variables, relative_cwd)
586 return {
587 'conditions': [
588 ['OS=="%s"' % get_flavor(), {
589 'variables': result,
590 }],
591 ],
592 }
593
594
595def split_touched(files):
596 """Splits files that are touched vs files that are read."""
597 tracked = []
598 touched = []
599 for f in files:
600 if f.size:
601 tracked.append(f)
602 else:
603 touched.append(f)
604 return tracked, touched
605
606
607def pretty_print(variables, stdout):
608 """Outputs a gyp compatible list from the decoded variables.
609
610 Similar to pprint.print() but with NIH syndrome.
611 """
612 # Order the dictionary keys by these keys in priority.
613 ORDER = (
614 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
615 KEY_TRACKED, KEY_UNTRACKED)
616
617 def sorting_key(x):
618 """Gives priority to 'most important' keys before the others."""
619 if x in ORDER:
620 return str(ORDER.index(x))
621 return x
622
623 def loop_list(indent, items):
624 for item in items:
625 if isinstance(item, basestring):
626 stdout.write('%s\'%s\',\n' % (indent, item))
627 elif isinstance(item, dict):
628 stdout.write('%s{\n' % indent)
629 loop_dict(indent + ' ', item)
630 stdout.write('%s},\n' % indent)
631 elif isinstance(item, list):
632 # A list inside a list will write the first item embedded.
633 stdout.write('%s[' % indent)
634 for index, i in enumerate(item):
635 if isinstance(i, basestring):
636 stdout.write(
637 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
638 elif isinstance(i, dict):
639 stdout.write('{\n')
640 loop_dict(indent + ' ', i)
641 if index != len(item) - 1:
642 x = ', '
643 else:
644 x = ''
645 stdout.write('%s}%s' % (indent, x))
646 else:
647 assert False
648 stdout.write('],\n')
649 else:
650 assert False
651
652 def loop_dict(indent, items):
653 for key in sorted(items, key=sorting_key):
654 item = items[key]
655 stdout.write("%s'%s': " % (indent, key))
656 if isinstance(item, dict):
657 stdout.write('{\n')
658 loop_dict(indent + ' ', item)
659 stdout.write(indent + '},\n')
660 elif isinstance(item, list):
661 stdout.write('[\n')
662 loop_list(indent + ' ', item)
663 stdout.write(indent + '],\n')
664 elif isinstance(item, basestring):
665 stdout.write(
666 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
667 elif item in (True, False, None):
668 stdout.write('%s\n' % item)
669 else:
670 assert False, item
671
672 stdout.write('{\n')
673 loop_dict(' ', variables)
674 stdout.write('}\n')
675
676
677def union(lhs, rhs):
678 """Merges two compatible datastructures composed of dict/list/set."""
679 assert lhs is not None or rhs is not None
680 if lhs is None:
681 return copy.deepcopy(rhs)
682 if rhs is None:
683 return copy.deepcopy(lhs)
684 assert type(lhs) == type(rhs), (lhs, rhs)
685 if hasattr(lhs, 'union'):
686 # Includes set, OSSettings and Configs.
687 return lhs.union(rhs)
688 if isinstance(lhs, dict):
689 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
690 elif isinstance(lhs, list):
691 # Do not go inside the list.
692 return lhs + rhs
693 assert False, type(lhs)
694
695
696def extract_comment(content):
697 """Extracts file level comment."""
698 out = []
699 for line in content.splitlines(True):
700 if line.startswith('#'):
701 out.append(line)
702 else:
703 break
704 return ''.join(out)
705
706
707def eval_content(content):
708 """Evaluates a python file and return the value defined in it.
709
710 Used in practice for .isolate files.
711 """
712 globs = {'__builtins__': None}
713 locs = {}
714 value = eval(content, globs, locs)
715 assert locs == {}, locs
716 assert globs == {'__builtins__': None}, globs
717 return value
718
719
720def verify_variables(variables):
721 """Verifies the |variables| dictionary is in the expected format."""
722 VALID_VARIABLES = [
723 KEY_TOUCHED,
724 KEY_TRACKED,
725 KEY_UNTRACKED,
726 'command',
727 'read_only',
728 ]
729 assert isinstance(variables, dict), variables
730 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
731 for name, value in variables.iteritems():
732 if name == 'read_only':
733 assert value in (True, False, None), value
734 else:
735 assert isinstance(value, list), value
736 assert all(isinstance(i, basestring) for i in value), value
737
738
739def verify_condition(condition):
740 """Verifies the |condition| dictionary is in the expected format."""
741 VALID_INSIDE_CONDITION = ['variables']
742 assert isinstance(condition, list), condition
743 assert 2 <= len(condition) <= 3, condition
744 assert re.match(r'OS==\"([a-z]+)\"', condition[0]), condition[0]
745 for c in condition[1:]:
746 assert isinstance(c, dict), c
747 assert set(VALID_INSIDE_CONDITION).issuperset(set(c)), c.keys()
748 verify_variables(c.get('variables', {}))
749
750
751def verify_root(value):
752 VALID_ROOTS = ['variables', 'conditions']
753 assert isinstance(value, dict), value
754 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
755 verify_variables(value.get('variables', {}))
756
757 conditions = value.get('conditions', [])
758 assert isinstance(conditions, list), conditions
759 for condition in conditions:
760 verify_condition(condition)
761
762
763def remove_weak_dependencies(values, key, item, item_oses):
764 """Remove any oses from this key if the item is already under a strong key."""
765 if key == KEY_TOUCHED:
766 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
767 oses = values.get(stronger_key, {}).get(item, None)
768 if oses:
769 item_oses -= oses
770
771 return item_oses
772
773
csharp@chromium.org31176252012-11-02 13:04:40 +0000774def remove_repeated_dependencies(folders, key, item, item_oses):
775 """Remove any OSes from this key if the item is in a folder that is already
776 included."""
777
778 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
779 for (folder, oses) in folders.iteritems():
780 if folder != item and item.startswith(folder):
781 item_oses -= oses
782
783 return item_oses
784
785
786def get_folders(values_dict):
787 """Return a dict of all the folders in the given value_dict."""
788 return dict((item, oses) for (item, oses) in values_dict.iteritems()
789 if item.endswith('/'))
790
791
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000792def invert_map(variables):
793 """Converts a dict(OS, dict(deptype, list(dependencies)) to a flattened view.
794
795 Returns a tuple of:
796 1. dict(deptype, dict(dependency, set(OSes)) for easier processing.
797 2. All the OSes found as a set.
798 """
799 KEYS = (
800 KEY_TOUCHED,
801 KEY_TRACKED,
802 KEY_UNTRACKED,
803 'command',
804 'read_only',
805 )
806 out = dict((key, {}) for key in KEYS)
807 for os_name, values in variables.iteritems():
808 for key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED):
809 for item in values.get(key, []):
810 out[key].setdefault(item, set()).add(os_name)
811
812 # command needs special handling.
813 command = tuple(values.get('command', []))
814 out['command'].setdefault(command, set()).add(os_name)
815
816 # read_only needs special handling.
817 out['read_only'].setdefault(values.get('read_only'), set()).add(os_name)
818 return out, set(variables)
819
820
821def reduce_inputs(values, oses):
822 """Reduces the invert_map() output to the strictest minimum list.
823
824 1. Construct the inverse map first.
825 2. Look at each individual file and directory, map where they are used and
826 reconstruct the inverse dictionary.
827 3. Do not convert back to negative if only 2 OSes were merged.
828
829 Returns a tuple of:
830 1. the minimized dictionary
831 2. oses passed through as-is.
832 """
833 KEYS = (
834 KEY_TOUCHED,
835 KEY_TRACKED,
836 KEY_UNTRACKED,
837 'command',
838 'read_only',
839 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000840
841 # Folders can only live in KEY_UNTRACKED.
842 folders = get_folders(values.get(KEY_UNTRACKED, {}))
843
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000844 out = dict((key, {}) for key in KEYS)
845 assert all(oses), oses
846 if len(oses) > 2:
847 for key in KEYS:
848 for item, item_oses in values.get(key, {}).iteritems():
849 item_oses = remove_weak_dependencies(values, key, item, item_oses)
csharp@chromium.org31176252012-11-02 13:04:40 +0000850 item_oses = remove_repeated_dependencies(folders, key, item, item_oses)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000851 if not item_oses:
852 continue
853
854 # Converts all oses.difference('foo') to '!foo'.
855 assert all(item_oses), item_oses
856 missing = oses.difference(item_oses)
857 if len(missing) == 1:
858 # Replace it with a negative.
859 out[key][item] = set(['!' + tuple(missing)[0]])
860 elif not missing:
861 out[key][item] = set([None])
862 else:
863 out[key][item] = set(item_oses)
864 else:
865 for key in KEYS:
866 for item, item_oses in values.get(key, {}).iteritems():
867 item_oses = remove_weak_dependencies(values, key, item, item_oses)
868 if not item_oses:
869 continue
870
871 # Converts all oses.difference('foo') to '!foo'.
872 assert None not in item_oses, item_oses
873 out[key][item] = set(item_oses)
874 return out, oses
875
876
877def convert_map_to_isolate_dict(values, oses):
878 """Regenerates back a .isolate configuration dict from files and dirs
879 mappings generated from reduce_inputs().
880 """
881 # First, inverse the mapping to make it dict first.
882 config = {}
883 for key in values:
884 for item, oses in values[key].iteritems():
885 if item is None:
886 # For read_only default.
887 continue
888 for cond_os in oses:
889 cond_key = None if cond_os is None else cond_os.lstrip('!')
890 # Insert the if/else dicts.
891 condition_values = config.setdefault(cond_key, [{}, {}])
892 # If condition is negative, use index 1, else use index 0.
893 cond_value = condition_values[int((cond_os or '').startswith('!'))]
894 variables = cond_value.setdefault('variables', {})
895
896 if item in (True, False):
897 # One-off for read_only.
898 variables[key] = item
899 else:
900 if isinstance(item, tuple) and item:
901 # One-off for command.
902 # Do not merge lists and do not sort!
903 # Note that item is a tuple.
904 assert key not in variables
905 variables[key] = list(item)
906 elif item:
907 # The list of items (files or dirs). Append the new item and keep
908 # the list sorted.
909 l = variables.setdefault(key, [])
910 l.append(item)
911 l.sort()
912
913 out = {}
914 for o in sorted(config):
915 d = config[o]
916 if o is None:
917 assert not d[1]
918 out = union(out, d[0])
919 else:
920 c = out.setdefault('conditions', [])
921 if d[1]:
922 c.append(['OS=="%s"' % o] + d)
923 else:
924 c.append(['OS=="%s"' % o] + d[0:1])
925 return out
926
927
928### Internal state files.
929
930
931class OSSettings(object):
932 """Represents the dependencies for an OS. The structure is immutable.
933
934 It's the .isolate settings for a specific file.
935 """
936 def __init__(self, name, values):
937 self.name = name
938 verify_variables(values)
939 self.touched = sorted(values.get(KEY_TOUCHED, []))
940 self.tracked = sorted(values.get(KEY_TRACKED, []))
941 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
942 self.command = values.get('command', [])[:]
943 self.read_only = values.get('read_only')
944
945 def union(self, rhs):
946 assert self.name == rhs.name
maruel@chromium.org669edcb2012-11-02 19:16:14 +0000947 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000948 var = {
949 KEY_TOUCHED: sorted(self.touched + rhs.touched),
950 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
951 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
952 'command': self.command or rhs.command,
953 'read_only': rhs.read_only if self.read_only is None else self.read_only,
954 }
955 return OSSettings(self.name, var)
956
957 def flatten(self):
958 out = {}
959 if self.command:
960 out['command'] = self.command
961 if self.touched:
962 out[KEY_TOUCHED] = self.touched
963 if self.tracked:
964 out[KEY_TRACKED] = self.tracked
965 if self.untracked:
966 out[KEY_UNTRACKED] = self.untracked
967 if self.read_only is not None:
968 out['read_only'] = self.read_only
969 return out
970
971
972class Configs(object):
973 """Represents a processed .isolate file.
974
975 Stores the file in a processed way, split by each the OS-specific
976 configurations.
977
978 The self.per_os[None] member contains all the 'else' clauses plus the default
979 values. It is not included in the flatten() result.
980 """
981 def __init__(self, oses, file_comment):
982 self.file_comment = file_comment
983 self.per_os = {
984 None: OSSettings(None, {}),
985 }
986 self.per_os.update(dict((name, OSSettings(name, {})) for name in oses))
987
988 def union(self, rhs):
989 items = list(set(self.per_os.keys() + rhs.per_os.keys()))
990 # Takes the first file comment, prefering lhs.
991 out = Configs(items, self.file_comment or rhs.file_comment)
992 for key in items:
993 out.per_os[key] = union(self.per_os.get(key), rhs.per_os.get(key))
994 return out
995
996 def add_globals(self, values):
997 for key in self.per_os:
998 self.per_os[key] = self.per_os[key].union(OSSettings(key, values))
999
1000 def add_values(self, for_os, values):
1001 self.per_os[for_os] = self.per_os[for_os].union(OSSettings(for_os, values))
1002
1003 def add_negative_values(self, for_os, values):
1004 """Includes the variables to all OSes except |for_os|.
1005
1006 This includes 'None' so unknown OSes gets it too.
1007 """
1008 for key in self.per_os:
1009 if key != for_os:
1010 self.per_os[key] = self.per_os[key].union(OSSettings(key, values))
1011
1012 def flatten(self):
1013 """Returns a flat dictionary representation of the configuration.
1014
1015 Skips None pseudo-OS.
1016 """
1017 return dict(
1018 (k, v.flatten()) for k, v in self.per_os.iteritems() if k is not None)
1019
1020
1021def load_isolate_as_config(value, file_comment, default_oses):
1022 """Parses one .isolate file and returns a Configs() instance.
1023
1024 |value| is the loaded dictionary that was defined in the gyp file.
1025
1026 The expected format is strict, anything diverting from the format below will
1027 throw an assert:
1028 {
1029 'variables': {
1030 'command': [
1031 ...
1032 ],
1033 'isolate_dependency_tracked': [
1034 ...
1035 ],
1036 'isolate_dependency_untracked': [
1037 ...
1038 ],
1039 'read_only': False,
1040 },
1041 'conditions': [
1042 ['OS=="<os>"', {
1043 'variables': {
1044 ...
1045 },
1046 }, { # else
1047 'variables': {
1048 ...
1049 },
1050 }],
1051 ...
1052 ],
1053 }
1054 """
1055 verify_root(value)
1056
1057 # Scan to get the list of OSes.
1058 conditions = value.get('conditions', [])
1059 oses = set(re.match(r'OS==\"([a-z]+)\"', c[0]).group(1) for c in conditions)
1060 oses = oses.union(default_oses)
1061 configs = Configs(oses, file_comment)
1062
1063 # Global level variables.
1064 configs.add_globals(value.get('variables', {}))
1065
1066 # OS specific variables.
1067 for condition in conditions:
1068 condition_os = re.match(r'OS==\"([a-z]+)\"', condition[0]).group(1)
1069 configs.add_values(condition_os, condition[1].get('variables', {}))
1070 if len(condition) > 2:
1071 configs.add_negative_values(
1072 condition_os, condition[2].get('variables', {}))
1073 return configs
1074
1075
1076def load_isolate_for_flavor(content, flavor):
1077 """Loads the .isolate file and returns the information unprocessed.
1078
1079 Returns the command, dependencies and read_only flag. The dependencies are
1080 fixed to use os.path.sep.
1081 """
1082 # Load the .isolate file, process its conditions, retrieve the command and
1083 # dependencies.
1084 configs = load_isolate_as_config(eval_content(content), None, DEFAULT_OSES)
1085 config = configs.per_os.get(flavor) or configs.per_os.get(None)
1086 if not config:
1087 raise ExecutionError('Failed to load configuration for \'%s\'' % flavor)
1088 # Merge tracked and untracked dependencies, isolate.py doesn't care about the
1089 # trackability of the dependencies, only the build tool does.
1090 dependencies = [
1091 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1092 ]
1093 touched = [f.replace('/', os.path.sep) for f in config.touched]
1094 return config.command, dependencies, touched, config.read_only
1095
1096
1097class Flattenable(object):
1098 """Represents data that can be represented as a json file."""
1099 MEMBERS = ()
1100
1101 def flatten(self):
1102 """Returns a json-serializable version of itself.
1103
1104 Skips None entries.
1105 """
1106 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1107 return dict((member, value) for member, value in items if value is not None)
1108
1109 @classmethod
1110 def load(cls, data):
1111 """Loads a flattened version."""
1112 data = data.copy()
1113 out = cls()
1114 for member in out.MEMBERS:
1115 if member in data:
1116 # Access to a protected member XXX of a client class
1117 # pylint: disable=W0212
1118 out._load_member(member, data.pop(member))
1119 if data:
1120 raise ValueError(
1121 'Found unexpected entry %s while constructing an object %s' %
1122 (data, cls.__name__), data, cls.__name__)
1123 return out
1124
1125 def _load_member(self, member, value):
1126 """Loads a member into self."""
1127 setattr(self, member, value)
1128
1129 @classmethod
1130 def load_file(cls, filename):
1131 """Loads the data from a file or return an empty instance."""
1132 out = cls()
1133 try:
1134 out = cls.load(trace_inputs.read_json(filename))
1135 logging.debug('Loaded %s(%s)' % (cls.__name__, filename))
1136 except (IOError, ValueError):
1137 logging.warn('Failed to load %s' % filename)
1138 return out
1139
1140
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001141class IsolatedFile(Flattenable):
1142 """Describes the content of a .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001143
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001144 This file is used by run_isolated.py so its content is strictly only
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001145 what is necessary to run the test outside of a checkout.
1146
1147 It is important to note that the 'files' dict keys are using native OS path
1148 separator instead of '/' used in .isolate file.
1149 """
1150 MEMBERS = (
1151 'command',
1152 'files',
1153 'os',
1154 'read_only',
1155 'relative_cwd',
1156 )
1157
1158 os = get_flavor()
1159
1160 def __init__(self):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001161 super(IsolatedFile, self).__init__()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001162 self.command = []
1163 self.files = {}
1164 self.read_only = None
1165 self.relative_cwd = None
1166
1167 def update(self, command, infiles, touched, read_only, relative_cwd):
1168 """Updates the result state with new information."""
1169 self.command = command
1170 # Add new files.
1171 for f in infiles:
1172 self.files.setdefault(f, {})
1173 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001174 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001175 # Prune extraneous files that are not a dependency anymore.
1176 for f in set(self.files).difference(set(infiles).union(touched)):
1177 del self.files[f]
1178 if read_only is not None:
1179 self.read_only = read_only
1180 self.relative_cwd = relative_cwd
1181
1182 def _load_member(self, member, value):
1183 if member == 'os':
1184 if value != self.os:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001185 raise run_isolated.ConfigError(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001186 'The .isolated file was created on another platform')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001187 else:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001188 super(IsolatedFile, self)._load_member(member, value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001189
1190 def __str__(self):
1191 out = '%s(\n' % self.__class__.__name__
1192 out += ' command: %s\n' % self.command
1193 out += ' files: %d\n' % len(self.files)
1194 out += ' read_only: %s\n' % self.read_only
1195 out += ' relative_cwd: %s)' % self.relative_cwd
1196 return out
1197
1198
1199class SavedState(Flattenable):
1200 """Describes the content of a .state file.
1201
1202 The items in this file are simply to improve the developer's life and aren't
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001203 used by run_isolated.py. This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001204
1205 isolate_file permits to find back root_dir, variables are used for stateful
1206 rerun.
1207 """
1208 MEMBERS = (
1209 'isolate_file',
1210 'variables',
1211 )
1212
1213 def __init__(self):
1214 super(SavedState, self).__init__()
1215 self.isolate_file = None
1216 self.variables = {}
1217
1218 def update(self, isolate_file, variables):
1219 """Updates the saved state with new information."""
1220 self.isolate_file = isolate_file
1221 self.variables.update(variables)
1222
1223 @classmethod
1224 def load(cls, data):
1225 out = super(SavedState, cls).load(data)
1226 if out.isolate_file:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001227 out.isolate_file = trace_inputs.get_native_path_case(
1228 unicode(out.isolate_file))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001229 return out
1230
1231 def __str__(self):
1232 out = '%s(\n' % self.__class__.__name__
1233 out += ' isolate_file: %s\n' % self.isolate_file
1234 out += ' variables: %s' % ''.join(
1235 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1236 out += ')'
1237 return out
1238
1239
1240class CompleteState(object):
1241 """Contains all the state to run the task at hand."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001242 def __init__(self, isolated_filepath, isolated, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001243 super(CompleteState, self).__init__()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001244 self.isolated_filepath = isolated_filepath
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001245 # Contains the data that will be used by run_isolated.py
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001246 self.isolated = isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001247 # Contains the data to ease developer's use-case but that is not strictly
1248 # necessary.
1249 self.saved_state = saved_state
1250
1251 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001252 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001253 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001254 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001255 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001256 isolated_filepath,
1257 IsolatedFile.load_file(isolated_filepath),
1258 SavedState.load_file(isolatedfile_to_state(isolated_filepath)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001259
csharp@chromium.org01856802012-11-12 17:48:13 +00001260 def load_isolate(self, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001261 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001262 .isolate file.
1263
1264 Processes the loaded data, deduce root_dir, relative_cwd.
1265 """
1266 # Make sure to not depend on os.getcwd().
1267 assert os.path.isabs(isolate_file), isolate_file
1268 logging.info(
1269 'CompleteState.load_isolate(%s, %s)' % (isolate_file, variables))
1270 relative_base_dir = os.path.dirname(isolate_file)
1271
1272 # Processes the variables and update the saved state.
1273 variables = process_variables(variables, relative_base_dir)
1274 self.saved_state.update(isolate_file, variables)
1275
1276 with open(isolate_file, 'r') as f:
1277 # At that point, variables are not replaced yet in command and infiles.
1278 # infiles may contain directory entries and is in posix style.
1279 command, infiles, touched, read_only = load_isolate_for_flavor(
1280 f.read(), get_flavor())
1281 command = [eval_variables(i, self.saved_state.variables) for i in command]
1282 infiles = [eval_variables(f, self.saved_state.variables) for f in infiles]
1283 touched = [eval_variables(f, self.saved_state.variables) for f in touched]
1284 # root_dir is automatically determined by the deepest root accessed with the
1285 # form '../../foo/bar'.
1286 root_dir = determine_root_dir(relative_base_dir, infiles + touched)
1287 # The relative directory is automatically determined by the relative path
1288 # between root_dir and the directory containing the .isolate file,
1289 # isolate_base_dir.
1290 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
1291 # Normalize the files based to root_dir. It is important to keep the
1292 # trailing os.path.sep at that step.
1293 infiles = [
1294 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1295 for f in infiles
1296 ]
1297 touched = [
1298 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1299 for f in touched
1300 ]
1301 # Expand the directories by listing each file inside. Up to now, trailing
1302 # os.path.sep must be kept. Do not expand 'touched'.
1303 infiles = expand_directories_and_symlinks(
1304 root_dir,
1305 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001306 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
1307 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001308
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001309 # Finally, update the new stuff in the foo.isolated file, the file that is
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001310 # used by run_isolated.py.
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001311 self.isolated.update(command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001312 logging.debug(self)
1313
maruel@chromium.org9268f042012-10-17 17:36:41 +00001314 def process_inputs(self, subdir):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001315 """Updates self.isolated.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001316
maruel@chromium.org9268f042012-10-17 17:36:41 +00001317 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1318 file is tainted.
1319
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001320 See process_input() for more information.
1321 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001322 for infile in sorted(self.isolated.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001323 if subdir and not infile.startswith(subdir):
1324 self.isolated.files.pop(infile)
1325 else:
1326 filepath = os.path.join(self.root_dir, infile)
1327 self.isolated.files[infile] = process_input(
1328 filepath, self.isolated.files[infile], self.isolated.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001329
1330 def save_files(self):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001331 """Saves both self.isolated and self.saved_state."""
1332 logging.debug('Dumping to %s' % self.isolated_filepath)
1333 trace_inputs.write_json(
1334 self.isolated_filepath, self.isolated.flatten(), True)
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001335 total_bytes = sum(i.get('s', 0) for i in self.isolated.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001336 if total_bytes:
1337 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001338 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001339 logging.debug('Dumping to %s' % saved_state_file)
1340 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1341
1342 @property
1343 def root_dir(self):
1344 """isolate_file is always inside relative_cwd relative to root_dir."""
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001345 if not self.saved_state.isolate_file:
1346 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001347 isolate_dir = os.path.dirname(self.saved_state.isolate_file)
1348 # Special case '.'.
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001349 if self.isolated.relative_cwd == '.':
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001350 return isolate_dir
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001351 assert isolate_dir.endswith(self.isolated.relative_cwd), (
1352 isolate_dir, self.isolated.relative_cwd)
1353 return isolate_dir[:-(len(self.isolated.relative_cwd) + 1)]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001354
1355 @property
1356 def resultdir(self):
1357 """Directory containing the results, usually equivalent to the variable
1358 PRODUCT_DIR.
1359 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001360 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001361
1362 def __str__(self):
1363 def indent(data, indent_length):
1364 """Indents text."""
1365 spacing = ' ' * indent_length
1366 return ''.join(spacing + l for l in str(data).splitlines(True))
1367
1368 out = '%s(\n' % self.__class__.__name__
1369 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001370 out += ' result: %s\n' % indent(self.isolated, 2)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001371 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1372 return out
1373
1374
maruel@chromium.org9268f042012-10-17 17:36:41 +00001375def load_complete_state(options, subdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001376 """Loads a CompleteState.
1377
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001378 This includes data from .isolate, .isolated and .state files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001379
1380 Arguments:
1381 options: Options instance generated with OptionParserIsolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001382 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001383 if options.isolated:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001384 # Load the previous state if it was present. Namely, "foo.isolated" and
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001385 # "foo.state".
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001386 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001387 else:
1388 # Constructs a dummy object that cannot be saved. Useful for temporary
1389 # commands like 'run'.
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001390 complete_state = CompleteState(None, IsolatedFile(), SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001391 options.isolate = options.isolate or complete_state.saved_state.isolate_file
1392 if not options.isolate:
1393 raise ExecutionError('A .isolate file is required.')
1394 if (complete_state.saved_state.isolate_file and
1395 options.isolate != complete_state.saved_state.isolate_file):
1396 raise ExecutionError(
1397 '%s and %s do not match.' % (
1398 options.isolate, complete_state.saved_state.isolate_file))
1399
1400 # Then load the .isolate and expands directories.
csharp@chromium.org01856802012-11-12 17:48:13 +00001401 complete_state.load_isolate(options.isolate, options.variables,
1402 options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001403
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001404 # Regenerate complete_state.isolated.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001405 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001406 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001407 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1408 subdir = subdir.replace('/', os.path.sep)
1409 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001410 return complete_state
1411
1412
1413def read_trace_as_isolate_dict(complete_state):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001414 """Reads a trace and returns the .isolate dictionary.
1415
1416 Returns exceptions during the log parsing so it can be re-raised.
1417 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001418 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001419 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001420 if not os.path.isfile(logfile):
1421 raise ExecutionError(
1422 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1423 try:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00001424 data = api.parse_log(logfile, default_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001425 exceptions = [i['exception'] for i in data if 'exception' in i]
1426 results = (i['results'] for i in data if 'results' in i)
1427 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1428 files = set(sum((result.existent for result in results_stripped), []))
1429 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001430 value = generate_isolate(
1431 tracked,
1432 [],
1433 touched,
1434 complete_state.root_dir,
1435 complete_state.saved_state.variables,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001436 complete_state.isolated.relative_cwd)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001437 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001438 except trace_inputs.TracingFailure, e:
1439 raise ExecutionError(
1440 'Reading traces failed for: %s\n%s' %
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001441 (' '.join(complete_state.isolated.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001442
1443
1444def print_all(comment, data, stream):
1445 """Prints a complete .isolate file and its top-level file comment into a
1446 stream.
1447 """
1448 if comment:
1449 stream.write(comment)
1450 pretty_print(data, stream)
1451
1452
1453def merge(complete_state):
1454 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001455 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001456
1457 # Now take that data and union it into the original .isolate file.
1458 with open(complete_state.saved_state.isolate_file, 'r') as f:
1459 prev_content = f.read()
1460 prev_config = load_isolate_as_config(
1461 eval_content(prev_content),
1462 extract_comment(prev_content),
1463 DEFAULT_OSES)
1464 new_config = load_isolate_as_config(value, '', DEFAULT_OSES)
1465 config = union(prev_config, new_config)
1466 # pylint: disable=E1103
1467 data = convert_map_to_isolate_dict(
1468 *reduce_inputs(*invert_map(config.flatten())))
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001469 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001470 with open(complete_state.saved_state.isolate_file, 'wb') as f:
1471 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001472 if exceptions:
1473 # It got an exception, raise the first one.
1474 raise \
1475 exceptions[0][0], \
1476 exceptions[0][1], \
1477 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001478
1479
1480def CMDcheck(args):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001481 """Checks that all the inputs are present and update .isolated."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001482 parser = OptionParserIsolate(command='check')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001483 parser.add_option('--subdir', help='Filters to a subdirectory')
1484 options, args = parser.parse_args(args)
1485 if args:
1486 parser.error('Unsupported argument: %s' % args)
1487 complete_state = load_complete_state(options, options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001488
1489 # Nothing is done specifically. Just store the result and state.
1490 complete_state.save_files()
1491 return 0
1492
1493
1494def CMDhashtable(args):
1495 """Creates a hash table content addressed object store.
1496
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001497 All the files listed in the .isolated file are put in the output directory
1498 with the file name being the sha-1 of the file's content.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001499 """
1500 parser = OptionParserIsolate(command='hashtable')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001501 parser.add_option('--subdir', help='Filters to a subdirectory')
1502 options, args = parser.parse_args(args)
1503 if args:
1504 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001505
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001506 with run_isolated.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001507 success = False
1508 try:
maruel@chromium.org9268f042012-10-17 17:36:41 +00001509 complete_state = load_complete_state(options, options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 options.outdir = (
1511 options.outdir or os.path.join(complete_state.resultdir, 'hashtable'))
1512 # Make sure that complete_state isn't modified until save_files() is
1513 # called, because any changes made to it here will propagate to the files
1514 # created (which is probably not intended).
1515 complete_state.save_files()
1516
1517 logging.info('Creating content addressed object store with %d item',
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001518 len(complete_state.isolated.files))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001519
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001520 with open(complete_state.isolated_filepath, 'rb') as f:
maruel@chromium.org861a5e72012-10-09 14:49:42 +00001521 content = f.read()
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001522 isolated_metadata = {
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001523 'h': hashlib.sha1(content).hexdigest(),
1524 's': len(content),
csharp@chromium.orgd62bcb92012-10-16 17:45:33 +00001525 'priority': '0'
1526 }
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001527
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001528 infiles = complete_state.isolated.files
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001529 infiles[complete_state.isolated_filepath] = isolated_metadata
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001530
1531 if re.match(r'^https?://.+$', options.outdir):
maruel@chromium.orgc6f90062012-11-07 18:32:22 +00001532 isolateserver_archive.upload_sha1_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001533 base_url=options.outdir,
1534 indir=complete_state.root_dir,
1535 infiles=infiles)
1536 else:
1537 recreate_tree(
1538 outdir=options.outdir,
1539 indir=complete_state.root_dir,
1540 infiles=infiles,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001541 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001542 as_sha1=True)
1543 success = True
1544 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001545 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001546 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001547 if not success and os.path.isfile(options.isolated):
1548 os.remove(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001549
1550
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001551def CMDmerge(args):
1552 """Reads and merges the data from the trace back into the original .isolate.
1553
1554 Ignores --outdir.
1555 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001556 parser = OptionParserIsolate(command='merge', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001557 options, args = parser.parse_args(args)
1558 if args:
1559 parser.error('Unsupported argument: %s' % args)
1560 complete_state = load_complete_state(options, None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001561 merge(complete_state)
1562 return 0
1563
1564
1565def CMDread(args):
1566 """Reads the trace file generated with command 'trace'.
1567
1568 Ignores --outdir.
1569 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001570 parser = OptionParserIsolate(command='read', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001571 options, args = parser.parse_args(args)
1572 if args:
1573 parser.error('Unsupported argument: %s' % args)
1574 complete_state = load_complete_state(options, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001575 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001576 pretty_print(value, sys.stdout)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001577 if exceptions:
1578 # It got an exception, raise the first one.
1579 raise \
1580 exceptions[0][0], \
1581 exceptions[0][1], \
1582 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001583 return 0
1584
1585
1586def CMDremap(args):
1587 """Creates a directory with all the dependencies mapped into it.
1588
1589 Useful to test manually why a test is failing. The target executable is not
1590 run.
1591 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001592 parser = OptionParserIsolate(command='remap', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001593 options, args = parser.parse_args(args)
1594 if args:
1595 parser.error('Unsupported argument: %s' % args)
1596 complete_state = load_complete_state(options, None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001597
1598 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001599 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001600 'isolate', complete_state.root_dir)
1601 else:
1602 if not os.path.isdir(options.outdir):
1603 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001604 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001605 if len(os.listdir(options.outdir)):
1606 raise ExecutionError('Can\'t remap in a non-empty directory')
1607 recreate_tree(
1608 outdir=options.outdir,
1609 indir=complete_state.root_dir,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001610 infiles=complete_state.isolated.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001611 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001612 as_sha1=False)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001613 if complete_state.isolated.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001614 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001615
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001616 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001617 complete_state.save_files()
1618 return 0
1619
1620
1621def CMDrun(args):
1622 """Runs the test executable in an isolated (temporary) directory.
1623
1624 All the dependencies are mapped into the temporary directory and the
1625 directory is cleaned up after the target exits. Warning: if -outdir is
1626 specified, it is deleted upon exit.
1627
1628 Argument processing stops at the first non-recognized argument and these
1629 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001630 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001631 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001632 parser = OptionParserIsolate(command='run', require_isolated=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001633 parser.enable_interspersed_args()
1634 options, args = parser.parse_args(args)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001635 complete_state = load_complete_state(options, None)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001636 cmd = complete_state.isolated.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001637 if not cmd:
1638 raise ExecutionError('No command to run')
1639 cmd = trace_inputs.fix_python_path(cmd)
1640 try:
1641 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001642 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001643 'isolate', complete_state.root_dir)
1644 else:
1645 if not os.path.isdir(options.outdir):
1646 os.makedirs(options.outdir)
1647 recreate_tree(
1648 outdir=options.outdir,
1649 indir=complete_state.root_dir,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001650 infiles=complete_state.isolated.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001651 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001652 as_sha1=False)
1653 cwd = os.path.normpath(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001654 os.path.join(options.outdir, complete_state.isolated.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001655 if not os.path.isdir(cwd):
1656 # It can happen when no files are mapped from the directory containing the
1657 # .isolate file. But the directory must exist to be the current working
1658 # directory.
1659 os.makedirs(cwd)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001660 if complete_state.isolated.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001661 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001662 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1663 result = subprocess.call(cmd, cwd=cwd)
1664 finally:
1665 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001666 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001667
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001668 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001669 complete_state.save_files()
1670 return result
1671
1672
1673def CMDtrace(args):
1674 """Traces the target using trace_inputs.py.
1675
1676 It runs the executable without remapping it, and traces all the files it and
1677 its child processes access. Then the 'read' command can be used to generate an
1678 updated .isolate file out of it.
1679
1680 Argument processing stops at the first non-recognized argument and these
1681 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001682 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001683 """
1684 parser = OptionParserIsolate(command='trace')
1685 parser.enable_interspersed_args()
1686 parser.add_option(
1687 '-m', '--merge', action='store_true',
1688 help='After tracing, merge the results back in the .isolate file')
1689 options, args = parser.parse_args(args)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001690 complete_state = load_complete_state(options, None)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001691 cmd = complete_state.isolated.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001692 if not cmd:
1693 raise ExecutionError('No command to run')
1694 cmd = trace_inputs.fix_python_path(cmd)
1695 cwd = os.path.normpath(os.path.join(
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001696 unicode(complete_state.root_dir), complete_state.isolated.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001697 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1698 if not os.path.isfile(cmd[0]):
1699 raise ExecutionError(
1700 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001701 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1702 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001703 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001704 api.clean_trace(logfile)
1705 try:
1706 with api.get_tracer(logfile) as tracer:
1707 result, _ = tracer.trace(
1708 cmd,
1709 cwd,
1710 'default',
1711 True)
1712 except trace_inputs.TracingFailure, e:
1713 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1714
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001715 if result:
1716 logging.error('Tracer exited with %d, which means the tests probably '
1717 'failed so the trace is probably incomplete.', result)
1718
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001719 complete_state.save_files()
1720
1721 if options.merge:
1722 merge(complete_state)
1723
1724 return result
1725
1726
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001727def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001728 """Adds --isolated and --variable to an OptionParser."""
1729 parser.add_option(
1730 '-s', '--isolated',
1731 metavar='FILE',
1732 help='.isolated file to generate or read')
1733 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001734 parser.add_option(
1735 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001736 dest='isolated',
1737 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001738 default_variables = [('OS', get_flavor())]
1739 if sys.platform in ('win32', 'cygwin'):
1740 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
1741 else:
1742 default_variables.append(('EXECUTABLE_SUFFIX', ''))
1743 parser.add_option(
1744 '-V', '--variable',
1745 nargs=2,
1746 action='append',
1747 default=default_variables,
1748 dest='variables',
1749 metavar='FOO BAR',
1750 help='Variables to process in the .isolate file, default: %default. '
1751 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001752 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001753
1754
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001755def parse_variable_option(parser, options, require_isolated):
1756 """Processes --isolated and --variable."""
1757 if options.isolated:
1758 options.isolated = os.path.abspath(
1759 options.isolated.replace('/', os.path.sep))
1760 if require_isolated and not options.isolated:
1761 parser.error('--isolated is required.')
1762 if options.isolated and not options.isolated.endswith('.isolated'):
1763 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001764 options.variables = dict(options.variables)
1765
1766
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001767class OptionParserIsolate(trace_inputs.OptionParserWithNiceDescription):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001768 """Adds automatic --isolate, --isolated, --out and --variable handling."""
1769 def __init__(self, require_isolated=True, **kwargs):
maruel@chromium.org55276902012-10-05 20:56:19 +00001770 trace_inputs.OptionParserWithNiceDescription.__init__(
1771 self,
1772 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1773 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001774 group = optparse.OptionGroup(self, "Common options")
1775 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001776 '-i', '--isolate',
1777 metavar='FILE',
1778 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001779 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001780 group.add_option(
1781 '-o', '--outdir', metavar='DIR',
1782 help='Directory used to recreate the tree or store the hash table. '
1783 'If the environment variable ISOLATE_HASH_TABLE_DIR exists, it '
1784 'will be used. Otherwise, for run and remap, uses a /tmp '
1785 'subdirectory. For the other modes, defaults to the directory '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001786 'containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00001787 group.add_option(
1788 '--ignore_broken_items', action='store_true',
1789 help='Indicates that invalid entries in the isolated file won\'t '
1790 'cause exceptions, but instead will just be logged.')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001791 self.add_option_group(group)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001792 self.require_isolated = require_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001793
1794 def parse_args(self, *args, **kwargs):
1795 """Makes sure the paths make sense.
1796
1797 On Windows, / and \ are often mixed together in a path.
1798 """
1799 options, args = trace_inputs.OptionParserWithNiceDescription.parse_args(
1800 self, *args, **kwargs)
1801 if not self.allow_interspersed_args and args:
1802 self.error('Unsupported argument: %s' % args)
1803
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001804 parse_variable_option(self, options, self.require_isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001805
1806 if options.isolate:
1807 options.isolate = trace_inputs.get_native_path_case(
1808 os.path.abspath(
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001809 unicode(options.isolate.replace('/', os.path.sep))))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001810
1811 if options.outdir and not re.match(r'^https?://.+$', options.outdir):
1812 options.outdir = os.path.abspath(
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001813 unicode(options.outdir.replace('/', os.path.sep)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001814
1815 return options, args
1816
1817
1818### Glue code to make all the commands works magically.
1819
1820
1821CMDhelp = trace_inputs.CMDhelp
1822
1823
1824def main(argv):
1825 try:
1826 return trace_inputs.main_impl(argv)
1827 except (
1828 ExecutionError,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001829 run_isolated.MappingError,
1830 run_isolated.ConfigError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001831 sys.stderr.write('\nError: ')
1832 sys.stderr.write(str(e))
1833 sys.stderr.write('\n')
1834 return 1
1835
1836
1837if __name__ == '__main__':
1838 sys.exit(main(sys.argv[1:]))