blob: 2d924126b21352c46ce2e194d66bc132f52fa383 [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
175def expand_directories_and_symlinks(indir, infiles, blacklist):
176 """Expands the directories and the symlinks, applies the blacklist and
177 verifies files exist.
178
179 Files are specified in os native path separator.
180 """
181 outfiles = []
182 for relfile in infiles:
183 outfiles.extend(expand_directory_and_symlink(indir, relfile, blacklist))
184 return outfiles
185
186
187def recreate_tree(outdir, indir, infiles, action, as_sha1):
188 """Creates a new tree with only the input files in it.
189
190 Arguments:
191 outdir: Output directory to create the files in.
192 indir: Root directory the infiles are based in.
193 infiles: dict of files to map from |indir| to |outdir|.
194 action: See assert below.
195 as_sha1: Output filename is the sha1 instead of relfile.
196 """
197 logging.info(
198 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_sha1=%s)' %
199 (outdir, indir, len(infiles), action, as_sha1))
200
201 assert action in (
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000202 run_isolated.HARDLINK,
203 run_isolated.SYMLINK,
204 run_isolated.COPY)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000205 outdir = os.path.normpath(outdir)
206 if not os.path.isdir(outdir):
207 logging.info ('Creating %s' % outdir)
208 os.makedirs(outdir)
209 # Do not call abspath until the directory exists.
210 outdir = os.path.abspath(outdir)
211
212 for relfile, metadata in infiles.iteritems():
213 infile = os.path.join(indir, relfile)
214 if as_sha1:
215 # Do the hashtable specific checks.
216 if 'link' in metadata:
217 # Skip links when storing a hashtable.
218 continue
219 outfile = os.path.join(outdir, metadata['sha-1'])
220 if os.path.isfile(outfile):
221 # Just do a quick check that the file size matches. No need to stat()
222 # again the input file, grab the value from the dict.
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000223 if not 'size' in metadata:
224 raise run_isolated.MappingError(
225 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000226 if metadata['size'] == os.stat(outfile).st_size:
227 continue
228 else:
229 logging.warn('Overwritting %s' % metadata['sha-1'])
230 os.remove(outfile)
231 else:
232 outfile = os.path.join(outdir, relfile)
233 outsubdir = os.path.dirname(outfile)
234 if not os.path.isdir(outsubdir):
235 os.makedirs(outsubdir)
236
237 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
238 # if metadata.get('touched_only') == True:
239 # open(outfile, 'ab').close()
240 if 'link' in metadata:
241 pointed = metadata['link']
242 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000243 # symlink doesn't exist on Windows.
244 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000245 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000246 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000247
248
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000249def process_input(filepath, prevdict, read_only):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000250 """Processes an input file, a dependency, and return meta data about it.
251
252 Arguments:
253 - filepath: File to act on.
254 - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
255 to skip recalculating the hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000256 - read_only: If True, the file mode is manipulated. In practice, only save
257 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
258 windows, mode is not set since all files are 'executable' by
259 default.
260
261 Behaviors:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000262 - Retrieves the file mode, file size, file timestamp, file link
263 destination if it is a file link and calcultate the SHA-1 of the file's
264 content if the path points to a file and not a symlink.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000265 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000266 out = {}
267 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
268 # if prevdict.get('touched_only') == True:
269 # # The file's content is ignored. Skip the time and hard code mode.
270 # if get_flavor() != 'win':
271 # out['mode'] = stat.S_IRUSR | stat.S_IRGRP
272 # out['size'] = 0
273 # out['sha-1'] = SHA_1_NULL
274 # out['touched_only'] = True
275 # return out
276
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000277 # Always check the file stat and check if it is a link. The timestamp is used
278 # to know if the file's content/symlink destination should be looked into.
279 # E.g. only reuse from prevdict if the timestamp hasn't changed.
280 # There is the risk of the file's timestamp being reset to its last value
281 # manually while its content changed. We don't protect against that use case.
282 try:
283 filestats = os.lstat(filepath)
284 except OSError:
285 # The file is not present.
286 raise run_isolated.MappingError('%s is missing' % filepath)
287 is_link = stat.S_ISLNK(filestats.st_mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000288
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000289 if get_flavor() != 'win':
290 # Ignore file mode on Windows since it's not really useful there.
291 filemode = stat.S_IMODE(filestats.st_mode)
292 # Remove write access for group and all access to 'others'.
293 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
294 if read_only:
295 filemode &= ~stat.S_IWUSR
296 if filemode & stat.S_IXUSR:
297 filemode |= stat.S_IXGRP
298 else:
299 filemode &= ~stat.S_IXGRP
300 out['mode'] = filemode
301
302 # Used to skip recalculating the hash or link destination. Use the most recent
303 # update time.
304 # TODO(maruel): Save it in the .state file instead of .isolated so the
305 # .isolated file is deterministic.
306 out['timestamp'] = int(round(filestats.st_mtime))
307
308 if not is_link:
309 out['size'] = filestats.st_size
310 # If the timestamp wasn't updated and the file size is still the same, carry
311 # on the sha-1.
312 if (prevdict.get('timestamp') == out['timestamp'] and
313 prevdict.get('size') == out['size']):
314 # Reuse the previous hash if available.
315 out['sha-1'] = prevdict.get('sha-1')
316 if not out.get('sha-1'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000317 with open(filepath, 'rb') as f:
318 out['sha-1'] = hashlib.sha1(f.read()).hexdigest()
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000319 else:
320 # If the timestamp wasn't updated, carry on the link destination.
321 if prevdict.get('timestamp') == out['timestamp']:
322 # Reuse the previous link destination if available.
323 out['link'] = prevdict.get('link')
324 if out.get('link') is None:
325 out['link'] = os.readlink(filepath) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000326 return out
327
328
329### Variable stuff.
330
331
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000332def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000333 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000334 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000335
336
337def determine_root_dir(relative_root, infiles):
338 """For a list of infiles, determines the deepest root directory that is
339 referenced indirectly.
340
341 All arguments must be using os.path.sep.
342 """
343 # The trick used to determine the root directory is to look at "how far" back
344 # up it is looking up.
345 deepest_root = relative_root
346 for i in infiles:
347 x = relative_root
348 while i.startswith('..' + os.path.sep):
349 i = i[3:]
350 assert not i.startswith(os.path.sep)
351 x = os.path.dirname(x)
352 if deepest_root.startswith(x):
353 deepest_root = x
354 logging.debug(
355 'determine_root_dir(%s, %d files) -> %s' % (
356 relative_root, len(infiles), deepest_root))
357 return deepest_root
358
359
360def replace_variable(part, variables):
361 m = re.match(r'<\(([A-Z_]+)\)', part)
362 if m:
363 if m.group(1) not in variables:
364 raise ExecutionError(
365 'Variable "%s" was not found in %s.\nDid you forget to specify '
366 '--variable?' % (m.group(1), variables))
367 return variables[m.group(1)]
368 return part
369
370
371def process_variables(variables, relative_base_dir):
372 """Processes path variables as a special case and returns a copy of the dict.
373
374 For each 'path' variable: first normalizes it, verifies it exists, converts it
375 to an absolute path, then sets it as relative to relative_base_dir.
376 """
377 variables = variables.copy()
378 for i in PATH_VARIABLES:
379 if i not in variables:
380 continue
381 variable = os.path.normpath(variables[i])
382 if not os.path.isdir(variable):
383 raise ExecutionError('%s=%s is not a directory' % (i, variable))
384 # Variables could contain / or \ on windows. Always normalize to
385 # os.path.sep.
386 variable = os.path.abspath(variable.replace('/', os.path.sep))
387 # All variables are relative to the .isolate file.
388 variables[i] = os.path.relpath(variable, relative_base_dir)
389 return variables
390
391
392def eval_variables(item, variables):
393 """Replaces the .isolate variables in a string item.
394
395 Note that the .isolate format is a subset of the .gyp dialect.
396 """
397 return ''.join(
398 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
399
400
401def classify_files(root_dir, tracked, untracked):
402 """Converts the list of files into a .isolate 'variables' dictionary.
403
404 Arguments:
405 - tracked: list of files names to generate a dictionary out of that should
406 probably be tracked.
407 - untracked: list of files names that must not be tracked.
408 """
409 # These directories are not guaranteed to be always present on every builder.
410 OPTIONAL_DIRECTORIES = (
411 'test/data/plugin',
412 'third_party/WebKit/LayoutTests',
413 )
414
415 new_tracked = []
416 new_untracked = list(untracked)
417
418 def should_be_tracked(filepath):
419 """Returns True if it is a file without whitespace in a non-optional
420 directory that has no symlink in its path.
421 """
422 if filepath.endswith('/'):
423 return False
424 if ' ' in filepath:
425 return False
426 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
427 return False
428 # Look if any element in the path is a symlink.
429 split = filepath.split('/')
430 for i in range(len(split)):
431 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
432 return False
433 return True
434
435 for filepath in sorted(tracked):
436 if should_be_tracked(filepath):
437 new_tracked.append(filepath)
438 else:
439 # Anything else.
440 new_untracked.append(filepath)
441
442 variables = {}
443 if new_tracked:
444 variables[KEY_TRACKED] = sorted(new_tracked)
445 if new_untracked:
446 variables[KEY_UNTRACKED] = sorted(new_untracked)
447 return variables
448
449
450def generate_simplified(
451 tracked, untracked, touched, root_dir, variables, relative_cwd):
452 """Generates a clean and complete .isolate 'variables' dictionary.
453
454 Cleans up and extracts only files from within root_dir then processes
455 variables and relative_cwd.
456 """
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000457 root_dir = os.path.realpath(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000458 logging.info(
459 'generate_simplified(%d files, %s, %s, %s)' %
460 (len(tracked) + len(untracked) + len(touched),
461 root_dir, variables, relative_cwd))
462 # Constants.
463 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
464 # separator.
465 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
466 EXECUTABLE = re.compile(
467 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
468 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
469 r'$')
470
471 # Preparation work.
472 relative_cwd = cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000473 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000474 # Creates the right set of variables here. We only care about PATH_VARIABLES.
475 variables = dict(
476 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
477 for k in PATH_VARIABLES if k in variables)
478
479 # Actual work: Process the files.
480 # TODO(maruel): if all the files in a directory are in part tracked and in
481 # part untracked, the directory will not be extracted. Tracked files should be
482 # 'promoted' to be untracked as needed.
483 tracked = trace_inputs.extract_directories(
484 root_dir, tracked, default_blacklist)
485 untracked = trace_inputs.extract_directories(
486 root_dir, untracked, default_blacklist)
487 # touched is not compressed, otherwise it would result in files to be archived
488 # that we don't need.
489
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000490 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000491 def fix(f):
492 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000493 # Important, GYP stores the files with / and not \.
494 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000495 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000496 # If it's not already a variable.
497 if not f.startswith('<'):
498 # relative_cwd is usually the directory containing the gyp file. It may be
499 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000500 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000501 # Convert the whole thing to / since it's isolate's speak.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000502 f = posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000503 posixpath.join(root_dir_posix, f),
504 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000505
506 for variable, root_path in variables.iteritems():
507 if f.startswith(root_path):
508 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000509 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000510 break
511
512 # Now strips off known files we want to ignore and to any specific mangling
513 # as necessary. It's easier to do it here than generate a blacklist.
514 match = EXECUTABLE.match(f)
515 if match:
516 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
517
518 # Blacklist logs and 'First Run' in the PRODUCT_DIR. First Run is not
519 # created by the compile, but by the test itself.
520 if LOG_FILE.match(f) or f == '<(PRODUCT_DIR)/First Run':
521 return None
522
523 if sys.platform == 'darwin':
524 # On OSX, the name of the output is dependent on gyp define, it can be
525 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
526 # Framework.framework'. Furthermore, they are versioned with a gyp
527 # variable. To lower the complexity of the .isolate file, remove all the
528 # individual entries that show up under any of the 4 entries and replace
529 # them with the directory itself. Overall, this results in a bit more
530 # files than strictly necessary.
531 OSX_BUNDLES = (
532 '<(PRODUCT_DIR)/Chromium Framework.framework/',
533 '<(PRODUCT_DIR)/Chromium.app/',
534 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
535 '<(PRODUCT_DIR)/Google Chrome.app/',
536 )
537 for prefix in OSX_BUNDLES:
538 if f.startswith(prefix):
539 # Note this result in duplicate values, so the a set() must be used to
540 # remove duplicates.
541 return prefix
542
543 return f
544
545 tracked = set(filter(None, (fix(f.path) for f in tracked)))
546 untracked = set(filter(None, (fix(f.path) for f in untracked)))
547 touched = set(filter(None, (fix(f.path) for f in touched)))
548 out = classify_files(root_dir, tracked, untracked)
549 if touched:
550 out[KEY_TOUCHED] = sorted(touched)
551 return out
552
553
554def generate_isolate(
555 tracked, untracked, touched, root_dir, variables, relative_cwd):
556 """Generates a clean and complete .isolate file."""
557 result = generate_simplified(
558 tracked, untracked, touched, root_dir, variables, relative_cwd)
559 return {
560 'conditions': [
561 ['OS=="%s"' % get_flavor(), {
562 'variables': result,
563 }],
564 ],
565 }
566
567
568def split_touched(files):
569 """Splits files that are touched vs files that are read."""
570 tracked = []
571 touched = []
572 for f in files:
573 if f.size:
574 tracked.append(f)
575 else:
576 touched.append(f)
577 return tracked, touched
578
579
580def pretty_print(variables, stdout):
581 """Outputs a gyp compatible list from the decoded variables.
582
583 Similar to pprint.print() but with NIH syndrome.
584 """
585 # Order the dictionary keys by these keys in priority.
586 ORDER = (
587 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
588 KEY_TRACKED, KEY_UNTRACKED)
589
590 def sorting_key(x):
591 """Gives priority to 'most important' keys before the others."""
592 if x in ORDER:
593 return str(ORDER.index(x))
594 return x
595
596 def loop_list(indent, items):
597 for item in items:
598 if isinstance(item, basestring):
599 stdout.write('%s\'%s\',\n' % (indent, item))
600 elif isinstance(item, dict):
601 stdout.write('%s{\n' % indent)
602 loop_dict(indent + ' ', item)
603 stdout.write('%s},\n' % indent)
604 elif isinstance(item, list):
605 # A list inside a list will write the first item embedded.
606 stdout.write('%s[' % indent)
607 for index, i in enumerate(item):
608 if isinstance(i, basestring):
609 stdout.write(
610 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
611 elif isinstance(i, dict):
612 stdout.write('{\n')
613 loop_dict(indent + ' ', i)
614 if index != len(item) - 1:
615 x = ', '
616 else:
617 x = ''
618 stdout.write('%s}%s' % (indent, x))
619 else:
620 assert False
621 stdout.write('],\n')
622 else:
623 assert False
624
625 def loop_dict(indent, items):
626 for key in sorted(items, key=sorting_key):
627 item = items[key]
628 stdout.write("%s'%s': " % (indent, key))
629 if isinstance(item, dict):
630 stdout.write('{\n')
631 loop_dict(indent + ' ', item)
632 stdout.write(indent + '},\n')
633 elif isinstance(item, list):
634 stdout.write('[\n')
635 loop_list(indent + ' ', item)
636 stdout.write(indent + '],\n')
637 elif isinstance(item, basestring):
638 stdout.write(
639 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
640 elif item in (True, False, None):
641 stdout.write('%s\n' % item)
642 else:
643 assert False, item
644
645 stdout.write('{\n')
646 loop_dict(' ', variables)
647 stdout.write('}\n')
648
649
650def union(lhs, rhs):
651 """Merges two compatible datastructures composed of dict/list/set."""
652 assert lhs is not None or rhs is not None
653 if lhs is None:
654 return copy.deepcopy(rhs)
655 if rhs is None:
656 return copy.deepcopy(lhs)
657 assert type(lhs) == type(rhs), (lhs, rhs)
658 if hasattr(lhs, 'union'):
659 # Includes set, OSSettings and Configs.
660 return lhs.union(rhs)
661 if isinstance(lhs, dict):
662 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
663 elif isinstance(lhs, list):
664 # Do not go inside the list.
665 return lhs + rhs
666 assert False, type(lhs)
667
668
669def extract_comment(content):
670 """Extracts file level comment."""
671 out = []
672 for line in content.splitlines(True):
673 if line.startswith('#'):
674 out.append(line)
675 else:
676 break
677 return ''.join(out)
678
679
680def eval_content(content):
681 """Evaluates a python file and return the value defined in it.
682
683 Used in practice for .isolate files.
684 """
685 globs = {'__builtins__': None}
686 locs = {}
687 value = eval(content, globs, locs)
688 assert locs == {}, locs
689 assert globs == {'__builtins__': None}, globs
690 return value
691
692
693def verify_variables(variables):
694 """Verifies the |variables| dictionary is in the expected format."""
695 VALID_VARIABLES = [
696 KEY_TOUCHED,
697 KEY_TRACKED,
698 KEY_UNTRACKED,
699 'command',
700 'read_only',
701 ]
702 assert isinstance(variables, dict), variables
703 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
704 for name, value in variables.iteritems():
705 if name == 'read_only':
706 assert value in (True, False, None), value
707 else:
708 assert isinstance(value, list), value
709 assert all(isinstance(i, basestring) for i in value), value
710
711
712def verify_condition(condition):
713 """Verifies the |condition| dictionary is in the expected format."""
714 VALID_INSIDE_CONDITION = ['variables']
715 assert isinstance(condition, list), condition
716 assert 2 <= len(condition) <= 3, condition
717 assert re.match(r'OS==\"([a-z]+)\"', condition[0]), condition[0]
718 for c in condition[1:]:
719 assert isinstance(c, dict), c
720 assert set(VALID_INSIDE_CONDITION).issuperset(set(c)), c.keys()
721 verify_variables(c.get('variables', {}))
722
723
724def verify_root(value):
725 VALID_ROOTS = ['variables', 'conditions']
726 assert isinstance(value, dict), value
727 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
728 verify_variables(value.get('variables', {}))
729
730 conditions = value.get('conditions', [])
731 assert isinstance(conditions, list), conditions
732 for condition in conditions:
733 verify_condition(condition)
734
735
736def remove_weak_dependencies(values, key, item, item_oses):
737 """Remove any oses from this key if the item is already under a strong key."""
738 if key == KEY_TOUCHED:
739 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
740 oses = values.get(stronger_key, {}).get(item, None)
741 if oses:
742 item_oses -= oses
743
744 return item_oses
745
746
csharp@chromium.org31176252012-11-02 13:04:40 +0000747def remove_repeated_dependencies(folders, key, item, item_oses):
748 """Remove any OSes from this key if the item is in a folder that is already
749 included."""
750
751 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
752 for (folder, oses) in folders.iteritems():
753 if folder != item and item.startswith(folder):
754 item_oses -= oses
755
756 return item_oses
757
758
759def get_folders(values_dict):
760 """Return a dict of all the folders in the given value_dict."""
761 return dict((item, oses) for (item, oses) in values_dict.iteritems()
762 if item.endswith('/'))
763
764
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000765def invert_map(variables):
766 """Converts a dict(OS, dict(deptype, list(dependencies)) to a flattened view.
767
768 Returns a tuple of:
769 1. dict(deptype, dict(dependency, set(OSes)) for easier processing.
770 2. All the OSes found as a set.
771 """
772 KEYS = (
773 KEY_TOUCHED,
774 KEY_TRACKED,
775 KEY_UNTRACKED,
776 'command',
777 'read_only',
778 )
779 out = dict((key, {}) for key in KEYS)
780 for os_name, values in variables.iteritems():
781 for key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED):
782 for item in values.get(key, []):
783 out[key].setdefault(item, set()).add(os_name)
784
785 # command needs special handling.
786 command = tuple(values.get('command', []))
787 out['command'].setdefault(command, set()).add(os_name)
788
789 # read_only needs special handling.
790 out['read_only'].setdefault(values.get('read_only'), set()).add(os_name)
791 return out, set(variables)
792
793
794def reduce_inputs(values, oses):
795 """Reduces the invert_map() output to the strictest minimum list.
796
797 1. Construct the inverse map first.
798 2. Look at each individual file and directory, map where they are used and
799 reconstruct the inverse dictionary.
800 3. Do not convert back to negative if only 2 OSes were merged.
801
802 Returns a tuple of:
803 1. the minimized dictionary
804 2. oses passed through as-is.
805 """
806 KEYS = (
807 KEY_TOUCHED,
808 KEY_TRACKED,
809 KEY_UNTRACKED,
810 'command',
811 'read_only',
812 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000813
814 # Folders can only live in KEY_UNTRACKED.
815 folders = get_folders(values.get(KEY_UNTRACKED, {}))
816
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000817 out = dict((key, {}) for key in KEYS)
818 assert all(oses), oses
819 if len(oses) > 2:
820 for key in KEYS:
821 for item, item_oses in values.get(key, {}).iteritems():
822 item_oses = remove_weak_dependencies(values, key, item, item_oses)
csharp@chromium.org31176252012-11-02 13:04:40 +0000823 item_oses = remove_repeated_dependencies(folders, key, item, item_oses)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000824 if not item_oses:
825 continue
826
827 # Converts all oses.difference('foo') to '!foo'.
828 assert all(item_oses), item_oses
829 missing = oses.difference(item_oses)
830 if len(missing) == 1:
831 # Replace it with a negative.
832 out[key][item] = set(['!' + tuple(missing)[0]])
833 elif not missing:
834 out[key][item] = set([None])
835 else:
836 out[key][item] = set(item_oses)
837 else:
838 for key in KEYS:
839 for item, item_oses in values.get(key, {}).iteritems():
840 item_oses = remove_weak_dependencies(values, key, item, item_oses)
841 if not item_oses:
842 continue
843
844 # Converts all oses.difference('foo') to '!foo'.
845 assert None not in item_oses, item_oses
846 out[key][item] = set(item_oses)
847 return out, oses
848
849
850def convert_map_to_isolate_dict(values, oses):
851 """Regenerates back a .isolate configuration dict from files and dirs
852 mappings generated from reduce_inputs().
853 """
854 # First, inverse the mapping to make it dict first.
855 config = {}
856 for key in values:
857 for item, oses in values[key].iteritems():
858 if item is None:
859 # For read_only default.
860 continue
861 for cond_os in oses:
862 cond_key = None if cond_os is None else cond_os.lstrip('!')
863 # Insert the if/else dicts.
864 condition_values = config.setdefault(cond_key, [{}, {}])
865 # If condition is negative, use index 1, else use index 0.
866 cond_value = condition_values[int((cond_os or '').startswith('!'))]
867 variables = cond_value.setdefault('variables', {})
868
869 if item in (True, False):
870 # One-off for read_only.
871 variables[key] = item
872 else:
873 if isinstance(item, tuple) and item:
874 # One-off for command.
875 # Do not merge lists and do not sort!
876 # Note that item is a tuple.
877 assert key not in variables
878 variables[key] = list(item)
879 elif item:
880 # The list of items (files or dirs). Append the new item and keep
881 # the list sorted.
882 l = variables.setdefault(key, [])
883 l.append(item)
884 l.sort()
885
886 out = {}
887 for o in sorted(config):
888 d = config[o]
889 if o is None:
890 assert not d[1]
891 out = union(out, d[0])
892 else:
893 c = out.setdefault('conditions', [])
894 if d[1]:
895 c.append(['OS=="%s"' % o] + d)
896 else:
897 c.append(['OS=="%s"' % o] + d[0:1])
898 return out
899
900
901### Internal state files.
902
903
904class OSSettings(object):
905 """Represents the dependencies for an OS. The structure is immutable.
906
907 It's the .isolate settings for a specific file.
908 """
909 def __init__(self, name, values):
910 self.name = name
911 verify_variables(values)
912 self.touched = sorted(values.get(KEY_TOUCHED, []))
913 self.tracked = sorted(values.get(KEY_TRACKED, []))
914 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
915 self.command = values.get('command', [])[:]
916 self.read_only = values.get('read_only')
917
918 def union(self, rhs):
919 assert self.name == rhs.name
maruel@chromium.org669edcb2012-11-02 19:16:14 +0000920 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000921 var = {
922 KEY_TOUCHED: sorted(self.touched + rhs.touched),
923 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
924 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
925 'command': self.command or rhs.command,
926 'read_only': rhs.read_only if self.read_only is None else self.read_only,
927 }
928 return OSSettings(self.name, var)
929
930 def flatten(self):
931 out = {}
932 if self.command:
933 out['command'] = self.command
934 if self.touched:
935 out[KEY_TOUCHED] = self.touched
936 if self.tracked:
937 out[KEY_TRACKED] = self.tracked
938 if self.untracked:
939 out[KEY_UNTRACKED] = self.untracked
940 if self.read_only is not None:
941 out['read_only'] = self.read_only
942 return out
943
944
945class Configs(object):
946 """Represents a processed .isolate file.
947
948 Stores the file in a processed way, split by each the OS-specific
949 configurations.
950
951 The self.per_os[None] member contains all the 'else' clauses plus the default
952 values. It is not included in the flatten() result.
953 """
954 def __init__(self, oses, file_comment):
955 self.file_comment = file_comment
956 self.per_os = {
957 None: OSSettings(None, {}),
958 }
959 self.per_os.update(dict((name, OSSettings(name, {})) for name in oses))
960
961 def union(self, rhs):
962 items = list(set(self.per_os.keys() + rhs.per_os.keys()))
963 # Takes the first file comment, prefering lhs.
964 out = Configs(items, self.file_comment or rhs.file_comment)
965 for key in items:
966 out.per_os[key] = union(self.per_os.get(key), rhs.per_os.get(key))
967 return out
968
969 def add_globals(self, values):
970 for key in self.per_os:
971 self.per_os[key] = self.per_os[key].union(OSSettings(key, values))
972
973 def add_values(self, for_os, values):
974 self.per_os[for_os] = self.per_os[for_os].union(OSSettings(for_os, values))
975
976 def add_negative_values(self, for_os, values):
977 """Includes the variables to all OSes except |for_os|.
978
979 This includes 'None' so unknown OSes gets it too.
980 """
981 for key in self.per_os:
982 if key != for_os:
983 self.per_os[key] = self.per_os[key].union(OSSettings(key, values))
984
985 def flatten(self):
986 """Returns a flat dictionary representation of the configuration.
987
988 Skips None pseudo-OS.
989 """
990 return dict(
991 (k, v.flatten()) for k, v in self.per_os.iteritems() if k is not None)
992
993
994def load_isolate_as_config(value, file_comment, default_oses):
995 """Parses one .isolate file and returns a Configs() instance.
996
997 |value| is the loaded dictionary that was defined in the gyp file.
998
999 The expected format is strict, anything diverting from the format below will
1000 throw an assert:
1001 {
1002 'variables': {
1003 'command': [
1004 ...
1005 ],
1006 'isolate_dependency_tracked': [
1007 ...
1008 ],
1009 'isolate_dependency_untracked': [
1010 ...
1011 ],
1012 'read_only': False,
1013 },
1014 'conditions': [
1015 ['OS=="<os>"', {
1016 'variables': {
1017 ...
1018 },
1019 }, { # else
1020 'variables': {
1021 ...
1022 },
1023 }],
1024 ...
1025 ],
1026 }
1027 """
1028 verify_root(value)
1029
1030 # Scan to get the list of OSes.
1031 conditions = value.get('conditions', [])
1032 oses = set(re.match(r'OS==\"([a-z]+)\"', c[0]).group(1) for c in conditions)
1033 oses = oses.union(default_oses)
1034 configs = Configs(oses, file_comment)
1035
1036 # Global level variables.
1037 configs.add_globals(value.get('variables', {}))
1038
1039 # OS specific variables.
1040 for condition in conditions:
1041 condition_os = re.match(r'OS==\"([a-z]+)\"', condition[0]).group(1)
1042 configs.add_values(condition_os, condition[1].get('variables', {}))
1043 if len(condition) > 2:
1044 configs.add_negative_values(
1045 condition_os, condition[2].get('variables', {}))
1046 return configs
1047
1048
1049def load_isolate_for_flavor(content, flavor):
1050 """Loads the .isolate file and returns the information unprocessed.
1051
1052 Returns the command, dependencies and read_only flag. The dependencies are
1053 fixed to use os.path.sep.
1054 """
1055 # Load the .isolate file, process its conditions, retrieve the command and
1056 # dependencies.
1057 configs = load_isolate_as_config(eval_content(content), None, DEFAULT_OSES)
1058 config = configs.per_os.get(flavor) or configs.per_os.get(None)
1059 if not config:
1060 raise ExecutionError('Failed to load configuration for \'%s\'' % flavor)
1061 # Merge tracked and untracked dependencies, isolate.py doesn't care about the
1062 # trackability of the dependencies, only the build tool does.
1063 dependencies = [
1064 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1065 ]
1066 touched = [f.replace('/', os.path.sep) for f in config.touched]
1067 return config.command, dependencies, touched, config.read_only
1068
1069
1070class Flattenable(object):
1071 """Represents data that can be represented as a json file."""
1072 MEMBERS = ()
1073
1074 def flatten(self):
1075 """Returns a json-serializable version of itself.
1076
1077 Skips None entries.
1078 """
1079 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1080 return dict((member, value) for member, value in items if value is not None)
1081
1082 @classmethod
1083 def load(cls, data):
1084 """Loads a flattened version."""
1085 data = data.copy()
1086 out = cls()
1087 for member in out.MEMBERS:
1088 if member in data:
1089 # Access to a protected member XXX of a client class
1090 # pylint: disable=W0212
1091 out._load_member(member, data.pop(member))
1092 if data:
1093 raise ValueError(
1094 'Found unexpected entry %s while constructing an object %s' %
1095 (data, cls.__name__), data, cls.__name__)
1096 return out
1097
1098 def _load_member(self, member, value):
1099 """Loads a member into self."""
1100 setattr(self, member, value)
1101
1102 @classmethod
1103 def load_file(cls, filename):
1104 """Loads the data from a file or return an empty instance."""
1105 out = cls()
1106 try:
1107 out = cls.load(trace_inputs.read_json(filename))
1108 logging.debug('Loaded %s(%s)' % (cls.__name__, filename))
1109 except (IOError, ValueError):
1110 logging.warn('Failed to load %s' % filename)
1111 return out
1112
1113
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001114class IsolatedFile(Flattenable):
1115 """Describes the content of a .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001116
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001117 This file is used by run_isolated.py so its content is strictly only
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001118 what is necessary to run the test outside of a checkout.
1119
1120 It is important to note that the 'files' dict keys are using native OS path
1121 separator instead of '/' used in .isolate file.
1122 """
1123 MEMBERS = (
1124 'command',
1125 'files',
1126 'os',
1127 'read_only',
1128 'relative_cwd',
1129 )
1130
1131 os = get_flavor()
1132
1133 def __init__(self):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001134 super(IsolatedFile, self).__init__()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001135 self.command = []
1136 self.files = {}
1137 self.read_only = None
1138 self.relative_cwd = None
1139
1140 def update(self, command, infiles, touched, read_only, relative_cwd):
1141 """Updates the result state with new information."""
1142 self.command = command
1143 # Add new files.
1144 for f in infiles:
1145 self.files.setdefault(f, {})
1146 for f in touched:
1147 self.files.setdefault(f, {})['touched_only'] = True
1148 # Prune extraneous files that are not a dependency anymore.
1149 for f in set(self.files).difference(set(infiles).union(touched)):
1150 del self.files[f]
1151 if read_only is not None:
1152 self.read_only = read_only
1153 self.relative_cwd = relative_cwd
1154
1155 def _load_member(self, member, value):
1156 if member == 'os':
1157 if value != self.os:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001158 raise run_isolated.ConfigError(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001159 'The .isolated file was created on another platform')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001160 else:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001161 super(IsolatedFile, self)._load_member(member, value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001162
1163 def __str__(self):
1164 out = '%s(\n' % self.__class__.__name__
1165 out += ' command: %s\n' % self.command
1166 out += ' files: %d\n' % len(self.files)
1167 out += ' read_only: %s\n' % self.read_only
1168 out += ' relative_cwd: %s)' % self.relative_cwd
1169 return out
1170
1171
1172class SavedState(Flattenable):
1173 """Describes the content of a .state file.
1174
1175 The items in this file are simply to improve the developer's life and aren't
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001176 used by run_isolated.py. This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001177
1178 isolate_file permits to find back root_dir, variables are used for stateful
1179 rerun.
1180 """
1181 MEMBERS = (
1182 'isolate_file',
1183 'variables',
1184 )
1185
1186 def __init__(self):
1187 super(SavedState, self).__init__()
1188 self.isolate_file = None
1189 self.variables = {}
1190
1191 def update(self, isolate_file, variables):
1192 """Updates the saved state with new information."""
1193 self.isolate_file = isolate_file
1194 self.variables.update(variables)
1195
1196 @classmethod
1197 def load(cls, data):
1198 out = super(SavedState, cls).load(data)
1199 if out.isolate_file:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001200 out.isolate_file = trace_inputs.get_native_path_case(
1201 unicode(out.isolate_file))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001202 return out
1203
1204 def __str__(self):
1205 out = '%s(\n' % self.__class__.__name__
1206 out += ' isolate_file: %s\n' % self.isolate_file
1207 out += ' variables: %s' % ''.join(
1208 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1209 out += ')'
1210 return out
1211
1212
1213class CompleteState(object):
1214 """Contains all the state to run the task at hand."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001215 def __init__(self, isolated_filepath, isolated, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001216 super(CompleteState, self).__init__()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001217 self.isolated_filepath = isolated_filepath
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001218 # Contains the data that will be used by run_isolated.py
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001219 self.isolated = isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001220 # Contains the data to ease developer's use-case but that is not strictly
1221 # necessary.
1222 self.saved_state = saved_state
1223
1224 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001225 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001226 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001227 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001228 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001229 isolated_filepath,
1230 IsolatedFile.load_file(isolated_filepath),
1231 SavedState.load_file(isolatedfile_to_state(isolated_filepath)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001232
1233 def load_isolate(self, isolate_file, variables):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001234 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001235 .isolate file.
1236
1237 Processes the loaded data, deduce root_dir, relative_cwd.
1238 """
1239 # Make sure to not depend on os.getcwd().
1240 assert os.path.isabs(isolate_file), isolate_file
1241 logging.info(
1242 'CompleteState.load_isolate(%s, %s)' % (isolate_file, variables))
1243 relative_base_dir = os.path.dirname(isolate_file)
1244
1245 # Processes the variables and update the saved state.
1246 variables = process_variables(variables, relative_base_dir)
1247 self.saved_state.update(isolate_file, variables)
1248
1249 with open(isolate_file, 'r') as f:
1250 # At that point, variables are not replaced yet in command and infiles.
1251 # infiles may contain directory entries and is in posix style.
1252 command, infiles, touched, read_only = load_isolate_for_flavor(
1253 f.read(), get_flavor())
1254 command = [eval_variables(i, self.saved_state.variables) for i in command]
1255 infiles = [eval_variables(f, self.saved_state.variables) for f in infiles]
1256 touched = [eval_variables(f, self.saved_state.variables) for f in touched]
1257 # root_dir is automatically determined by the deepest root accessed with the
1258 # form '../../foo/bar'.
1259 root_dir = determine_root_dir(relative_base_dir, infiles + touched)
1260 # The relative directory is automatically determined by the relative path
1261 # between root_dir and the directory containing the .isolate file,
1262 # isolate_base_dir.
1263 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
1264 # Normalize the files based to root_dir. It is important to keep the
1265 # trailing os.path.sep at that step.
1266 infiles = [
1267 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1268 for f in infiles
1269 ]
1270 touched = [
1271 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1272 for f in touched
1273 ]
1274 # Expand the directories by listing each file inside. Up to now, trailing
1275 # os.path.sep must be kept. Do not expand 'touched'.
1276 infiles = expand_directories_and_symlinks(
1277 root_dir,
1278 infiles,
1279 lambda x: re.match(r'.*\.(git|svn|pyc)$', x))
1280
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001281 # Finally, update the new stuff in the foo.isolated file, the file that is
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001282 # used by run_isolated.py.
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001283 self.isolated.update(command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001284 logging.debug(self)
1285
maruel@chromium.org9268f042012-10-17 17:36:41 +00001286 def process_inputs(self, subdir):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001287 """Updates self.isolated.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001288
maruel@chromium.org9268f042012-10-17 17:36:41 +00001289 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1290 file is tainted.
1291
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001292 See process_input() for more information.
1293 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001294 for infile in sorted(self.isolated.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001295 if subdir and not infile.startswith(subdir):
1296 self.isolated.files.pop(infile)
1297 else:
1298 filepath = os.path.join(self.root_dir, infile)
1299 self.isolated.files[infile] = process_input(
1300 filepath, self.isolated.files[infile], self.isolated.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001301
1302 def save_files(self):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001303 """Saves both self.isolated and self.saved_state."""
1304 logging.debug('Dumping to %s' % self.isolated_filepath)
1305 trace_inputs.write_json(
1306 self.isolated_filepath, self.isolated.flatten(), True)
1307 total_bytes = sum(i
1308 .get('size', 0) for i in self.isolated.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001309 if total_bytes:
1310 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001311 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001312 logging.debug('Dumping to %s' % saved_state_file)
1313 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1314
1315 @property
1316 def root_dir(self):
1317 """isolate_file is always inside relative_cwd relative to root_dir."""
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001318 if not self.saved_state.isolate_file:
1319 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001320 isolate_dir = os.path.dirname(self.saved_state.isolate_file)
1321 # Special case '.'.
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001322 if self.isolated.relative_cwd == '.':
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001323 return isolate_dir
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001324 assert isolate_dir.endswith(self.isolated.relative_cwd), (
1325 isolate_dir, self.isolated.relative_cwd)
1326 return isolate_dir[:-(len(self.isolated.relative_cwd) + 1)]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001327
1328 @property
1329 def resultdir(self):
1330 """Directory containing the results, usually equivalent to the variable
1331 PRODUCT_DIR.
1332 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001333 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001334
1335 def __str__(self):
1336 def indent(data, indent_length):
1337 """Indents text."""
1338 spacing = ' ' * indent_length
1339 return ''.join(spacing + l for l in str(data).splitlines(True))
1340
1341 out = '%s(\n' % self.__class__.__name__
1342 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001343 out += ' result: %s\n' % indent(self.isolated, 2)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001344 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1345 return out
1346
1347
maruel@chromium.org9268f042012-10-17 17:36:41 +00001348def load_complete_state(options, subdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001349 """Loads a CompleteState.
1350
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001351 This includes data from .isolate, .isolated and .state files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001352
1353 Arguments:
1354 options: Options instance generated with OptionParserIsolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001355 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001356 if options.isolated:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001357 # Load the previous state if it was present. Namely, "foo.isolated" and
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001358 # "foo.state".
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001359 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001360 else:
1361 # Constructs a dummy object that cannot be saved. Useful for temporary
1362 # commands like 'run'.
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001363 complete_state = CompleteState(None, IsolatedFile(), SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001364 options.isolate = options.isolate or complete_state.saved_state.isolate_file
1365 if not options.isolate:
1366 raise ExecutionError('A .isolate file is required.')
1367 if (complete_state.saved_state.isolate_file and
1368 options.isolate != complete_state.saved_state.isolate_file):
1369 raise ExecutionError(
1370 '%s and %s do not match.' % (
1371 options.isolate, complete_state.saved_state.isolate_file))
1372
1373 # Then load the .isolate and expands directories.
1374 complete_state.load_isolate(options.isolate, options.variables)
1375
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001376 # Regenerate complete_state.isolated.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001377 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001378 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001379 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1380 subdir = subdir.replace('/', os.path.sep)
1381 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001382 return complete_state
1383
1384
1385def read_trace_as_isolate_dict(complete_state):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001386 """Reads a trace and returns the .isolate dictionary.
1387
1388 Returns exceptions during the log parsing so it can be re-raised.
1389 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001390 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001391 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001392 if not os.path.isfile(logfile):
1393 raise ExecutionError(
1394 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1395 try:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00001396 data = api.parse_log(logfile, default_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001397 exceptions = [i['exception'] for i in data if 'exception' in i]
1398 results = (i['results'] for i in data if 'results' in i)
1399 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1400 files = set(sum((result.existent for result in results_stripped), []))
1401 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001402 value = generate_isolate(
1403 tracked,
1404 [],
1405 touched,
1406 complete_state.root_dir,
1407 complete_state.saved_state.variables,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001408 complete_state.isolated.relative_cwd)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001409 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001410 except trace_inputs.TracingFailure, e:
1411 raise ExecutionError(
1412 'Reading traces failed for: %s\n%s' %
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001413 (' '.join(complete_state.isolated.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001414
1415
1416def print_all(comment, data, stream):
1417 """Prints a complete .isolate file and its top-level file comment into a
1418 stream.
1419 """
1420 if comment:
1421 stream.write(comment)
1422 pretty_print(data, stream)
1423
1424
1425def merge(complete_state):
1426 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001427 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001428
1429 # Now take that data and union it into the original .isolate file.
1430 with open(complete_state.saved_state.isolate_file, 'r') as f:
1431 prev_content = f.read()
1432 prev_config = load_isolate_as_config(
1433 eval_content(prev_content),
1434 extract_comment(prev_content),
1435 DEFAULT_OSES)
1436 new_config = load_isolate_as_config(value, '', DEFAULT_OSES)
1437 config = union(prev_config, new_config)
1438 # pylint: disable=E1103
1439 data = convert_map_to_isolate_dict(
1440 *reduce_inputs(*invert_map(config.flatten())))
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001441 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001442 with open(complete_state.saved_state.isolate_file, 'wb') as f:
1443 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001444 if exceptions:
1445 # It got an exception, raise the first one.
1446 raise \
1447 exceptions[0][0], \
1448 exceptions[0][1], \
1449 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001450
1451
1452def CMDcheck(args):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001453 """Checks that all the inputs are present and update .isolated."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001454 parser = OptionParserIsolate(command='check')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001455 parser.add_option('--subdir', help='Filters to a subdirectory')
1456 options, args = parser.parse_args(args)
1457 if args:
1458 parser.error('Unsupported argument: %s' % args)
1459 complete_state = load_complete_state(options, options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001460
1461 # Nothing is done specifically. Just store the result and state.
1462 complete_state.save_files()
1463 return 0
1464
1465
1466def CMDhashtable(args):
1467 """Creates a hash table content addressed object store.
1468
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001469 All the files listed in the .isolated file are put in the output directory
1470 with the file name being the sha-1 of the file's content.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001471 """
1472 parser = OptionParserIsolate(command='hashtable')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001473 parser.add_option('--subdir', help='Filters to a subdirectory')
1474 options, args = parser.parse_args(args)
1475 if args:
1476 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001477
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001478 with run_isolated.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001479 success = False
1480 try:
maruel@chromium.org9268f042012-10-17 17:36:41 +00001481 complete_state = load_complete_state(options, options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001482 options.outdir = (
1483 options.outdir or os.path.join(complete_state.resultdir, 'hashtable'))
1484 # Make sure that complete_state isn't modified until save_files() is
1485 # called, because any changes made to it here will propagate to the files
1486 # created (which is probably not intended).
1487 complete_state.save_files()
1488
1489 logging.info('Creating content addressed object store with %d item',
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001490 len(complete_state.isolated.files))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001491
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001492 with open(complete_state.isolated_filepath, 'rb') as f:
maruel@chromium.org861a5e72012-10-09 14:49:42 +00001493 content = f.read()
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001494 isolated_hash = hashlib.sha1(content).hexdigest()
1495 isolated_metadata = {
1496 'sha-1': isolated_hash,
csharp@chromium.orgd62bcb92012-10-16 17:45:33 +00001497 'size': len(content),
1498 'priority': '0'
1499 }
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001500
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001501 infiles = complete_state.isolated.files
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001502 infiles[complete_state.isolated_filepath] = isolated_metadata
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001503
1504 if re.match(r'^https?://.+$', options.outdir):
maruel@chromium.orgc6f90062012-11-07 18:32:22 +00001505 isolateserver_archive.upload_sha1_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001506 base_url=options.outdir,
1507 indir=complete_state.root_dir,
1508 infiles=infiles)
1509 else:
1510 recreate_tree(
1511 outdir=options.outdir,
1512 indir=complete_state.root_dir,
1513 infiles=infiles,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001514 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001515 as_sha1=True)
1516 success = True
1517 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001518 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001519 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001520 if not success and os.path.isfile(options.isolated):
1521 os.remove(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001522
1523
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001524def CMDmerge(args):
1525 """Reads and merges the data from the trace back into the original .isolate.
1526
1527 Ignores --outdir.
1528 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001529 parser = OptionParserIsolate(command='merge', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001530 options, args = parser.parse_args(args)
1531 if args:
1532 parser.error('Unsupported argument: %s' % args)
1533 complete_state = load_complete_state(options, None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001534 merge(complete_state)
1535 return 0
1536
1537
1538def CMDread(args):
1539 """Reads the trace file generated with command 'trace'.
1540
1541 Ignores --outdir.
1542 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001543 parser = OptionParserIsolate(command='read', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001544 options, args = parser.parse_args(args)
1545 if args:
1546 parser.error('Unsupported argument: %s' % args)
1547 complete_state = load_complete_state(options, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001548 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001549 pretty_print(value, sys.stdout)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001550 if exceptions:
1551 # It got an exception, raise the first one.
1552 raise \
1553 exceptions[0][0], \
1554 exceptions[0][1], \
1555 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001556 return 0
1557
1558
1559def CMDremap(args):
1560 """Creates a directory with all the dependencies mapped into it.
1561
1562 Useful to test manually why a test is failing. The target executable is not
1563 run.
1564 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001565 parser = OptionParserIsolate(command='remap', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001566 options, args = parser.parse_args(args)
1567 if args:
1568 parser.error('Unsupported argument: %s' % args)
1569 complete_state = load_complete_state(options, None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001570
1571 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001572 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001573 'isolate', complete_state.root_dir)
1574 else:
1575 if not os.path.isdir(options.outdir):
1576 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001577 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001578 if len(os.listdir(options.outdir)):
1579 raise ExecutionError('Can\'t remap in a non-empty directory')
1580 recreate_tree(
1581 outdir=options.outdir,
1582 indir=complete_state.root_dir,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001583 infiles=complete_state.isolated.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001584 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001585 as_sha1=False)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001586 if complete_state.isolated.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001587 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001588
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001589 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001590 complete_state.save_files()
1591 return 0
1592
1593
1594def CMDrun(args):
1595 """Runs the test executable in an isolated (temporary) directory.
1596
1597 All the dependencies are mapped into the temporary directory and the
1598 directory is cleaned up after the target exits. Warning: if -outdir is
1599 specified, it is deleted upon exit.
1600
1601 Argument processing stops at the first non-recognized argument and these
1602 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001603 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001604 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001605 parser = OptionParserIsolate(command='run', require_isolated=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001606 parser.enable_interspersed_args()
1607 options, args = parser.parse_args(args)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001608 complete_state = load_complete_state(options, None)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001609 cmd = complete_state.isolated.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001610 if not cmd:
1611 raise ExecutionError('No command to run')
1612 cmd = trace_inputs.fix_python_path(cmd)
1613 try:
1614 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001615 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001616 'isolate', complete_state.root_dir)
1617 else:
1618 if not os.path.isdir(options.outdir):
1619 os.makedirs(options.outdir)
1620 recreate_tree(
1621 outdir=options.outdir,
1622 indir=complete_state.root_dir,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001623 infiles=complete_state.isolated.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001624 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001625 as_sha1=False)
1626 cwd = os.path.normpath(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001627 os.path.join(options.outdir, complete_state.isolated.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001628 if not os.path.isdir(cwd):
1629 # It can happen when no files are mapped from the directory containing the
1630 # .isolate file. But the directory must exist to be the current working
1631 # directory.
1632 os.makedirs(cwd)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001633 if complete_state.isolated.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001634 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001635 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1636 result = subprocess.call(cmd, cwd=cwd)
1637 finally:
1638 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001639 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001640
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001641 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001642 complete_state.save_files()
1643 return result
1644
1645
1646def CMDtrace(args):
1647 """Traces the target using trace_inputs.py.
1648
1649 It runs the executable without remapping it, and traces all the files it and
1650 its child processes access. Then the 'read' command can be used to generate an
1651 updated .isolate file out of it.
1652
1653 Argument processing stops at the first non-recognized argument and these
1654 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001655 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001656 """
1657 parser = OptionParserIsolate(command='trace')
1658 parser.enable_interspersed_args()
1659 parser.add_option(
1660 '-m', '--merge', action='store_true',
1661 help='After tracing, merge the results back in the .isolate file')
1662 options, args = parser.parse_args(args)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001663 complete_state = load_complete_state(options, None)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001664 cmd = complete_state.isolated.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001665 if not cmd:
1666 raise ExecutionError('No command to run')
1667 cmd = trace_inputs.fix_python_path(cmd)
1668 cwd = os.path.normpath(os.path.join(
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001669 unicode(complete_state.root_dir), complete_state.isolated.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001670 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1671 if not os.path.isfile(cmd[0]):
1672 raise ExecutionError(
1673 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001674 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1675 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001676 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001677 api.clean_trace(logfile)
1678 try:
1679 with api.get_tracer(logfile) as tracer:
1680 result, _ = tracer.trace(
1681 cmd,
1682 cwd,
1683 'default',
1684 True)
1685 except trace_inputs.TracingFailure, e:
1686 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1687
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001688 if result:
1689 logging.error('Tracer exited with %d, which means the tests probably '
1690 'failed so the trace is probably incomplete.', result)
1691
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001692 complete_state.save_files()
1693
1694 if options.merge:
1695 merge(complete_state)
1696
1697 return result
1698
1699
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001700def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001701 """Adds --isolated and --variable to an OptionParser."""
1702 parser.add_option(
1703 '-s', '--isolated',
1704 metavar='FILE',
1705 help='.isolated file to generate or read')
1706 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001707 parser.add_option(
1708 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001709 dest='isolated',
1710 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001711 default_variables = [('OS', get_flavor())]
1712 if sys.platform in ('win32', 'cygwin'):
1713 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
1714 else:
1715 default_variables.append(('EXECUTABLE_SUFFIX', ''))
1716 parser.add_option(
1717 '-V', '--variable',
1718 nargs=2,
1719 action='append',
1720 default=default_variables,
1721 dest='variables',
1722 metavar='FOO BAR',
1723 help='Variables to process in the .isolate file, default: %default. '
1724 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001725 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001726
1727
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001728def parse_variable_option(parser, options, require_isolated):
1729 """Processes --isolated and --variable."""
1730 if options.isolated:
1731 options.isolated = os.path.abspath(
1732 options.isolated.replace('/', os.path.sep))
1733 if require_isolated and not options.isolated:
1734 parser.error('--isolated is required.')
1735 if options.isolated and not options.isolated.endswith('.isolated'):
1736 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001737 options.variables = dict(options.variables)
1738
1739
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001740class OptionParserIsolate(trace_inputs.OptionParserWithNiceDescription):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001741 """Adds automatic --isolate, --isolated, --out and --variable handling."""
1742 def __init__(self, require_isolated=True, **kwargs):
maruel@chromium.org55276902012-10-05 20:56:19 +00001743 trace_inputs.OptionParserWithNiceDescription.__init__(
1744 self,
1745 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1746 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001747 group = optparse.OptionGroup(self, "Common options")
1748 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001749 '-i', '--isolate',
1750 metavar='FILE',
1751 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001752 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001753 group.add_option(
1754 '-o', '--outdir', metavar='DIR',
1755 help='Directory used to recreate the tree or store the hash table. '
1756 'If the environment variable ISOLATE_HASH_TABLE_DIR exists, it '
1757 'will be used. Otherwise, for run and remap, uses a /tmp '
1758 'subdirectory. For the other modes, defaults to the directory '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001759 'containing --isolated')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001760 self.add_option_group(group)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001761 self.require_isolated = require_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001762
1763 def parse_args(self, *args, **kwargs):
1764 """Makes sure the paths make sense.
1765
1766 On Windows, / and \ are often mixed together in a path.
1767 """
1768 options, args = trace_inputs.OptionParserWithNiceDescription.parse_args(
1769 self, *args, **kwargs)
1770 if not self.allow_interspersed_args and args:
1771 self.error('Unsupported argument: %s' % args)
1772
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001773 parse_variable_option(self, options, self.require_isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001774
1775 if options.isolate:
1776 options.isolate = trace_inputs.get_native_path_case(
1777 os.path.abspath(
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001778 unicode(options.isolate.replace('/', os.path.sep))))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001779
1780 if options.outdir and not re.match(r'^https?://.+$', options.outdir):
1781 options.outdir = os.path.abspath(
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001782 unicode(options.outdir.replace('/', os.path.sep)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001783
1784 return options, args
1785
1786
1787### Glue code to make all the commands works magically.
1788
1789
1790CMDhelp = trace_inputs.CMDhelp
1791
1792
1793def main(argv):
1794 try:
1795 return trace_inputs.main_impl(argv)
1796 except (
1797 ExecutionError,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001798 run_isolated.MappingError,
1799 run_isolated.ConfigError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001800 sys.stderr.write('\nError: ')
1801 sys.stderr.write(str(e))
1802 sys.stderr.write('\n')
1803 return 1
1804
1805
1806if __name__ == '__main__':
1807 sys.exit(main(sys.argv[1:]))