blob: 2f586b883c4cd5606d128ce401777963b2a40438 [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.
223 if 'link' in metadata:
224 # Skip links when storing a hashtable.
225 continue
226 outfile = os.path.join(outdir, metadata['sha-1'])
227 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.org861a5e72012-10-09 14:49:42 +0000230 if not 'size' in metadata:
231 raise run_isolated.MappingError(
232 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000233 if metadata['size'] == os.stat(outfile).st_size:
234 continue
235 else:
236 logging.warn('Overwritting %s' % metadata['sha-1'])
237 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.
245 # if metadata.get('touched_only') == True:
246 # open(outfile, 'ab').close()
247 if 'link' in metadata:
248 pointed = metadata['link']
249 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.
275 # if prevdict.get('touched_only') == True:
276 # # The file's content is ignored. Skip the time and hard code mode.
277 # if get_flavor() != 'win':
278 # out['mode'] = stat.S_IRUSR | stat.S_IRGRP
279 # out['size'] = 0
280 # out['sha-1'] = SHA_1_NULL
281 # out['touched_only'] = True
282 # 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
307 out['mode'] = filemode
308
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.
313 out['timestamp'] = int(round(filestats.st_mtime))
314
315 if not is_link:
316 out['size'] = filestats.st_size
317 # If the timestamp wasn't updated and the file size is still the same, carry
318 # on the sha-1.
319 if (prevdict.get('timestamp') == out['timestamp'] and
320 prevdict.get('size') == out['size']):
321 # Reuse the previous hash if available.
322 out['sha-1'] = prevdict.get('sha-1')
323 if not out.get('sha-1'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000324 with open(filepath, 'rb') as f:
325 out['sha-1'] = 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.
328 if prevdict.get('timestamp') == out['timestamp']:
329 # Reuse the previous link destination if available.
330 out['link'] = prevdict.get('link')
331 if out.get('link') is None:
332 out['link'] = 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
457def generate_simplified(
458 tracked, untracked, touched, root_dir, variables, relative_cwd):
459 """Generates a clean and complete .isolate 'variables' dictionary.
460
461 Cleans up and extracts only files from within root_dir then processes
462 variables and relative_cwd.
463 """
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000464 root_dir = os.path.realpath(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000465 logging.info(
466 'generate_simplified(%d files, %s, %s, %s)' %
467 (len(tracked) + len(untracked) + len(touched),
468 root_dir, variables, relative_cwd))
469 # Constants.
470 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
471 # separator.
472 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
473 EXECUTABLE = re.compile(
474 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
475 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
476 r'$')
477
478 # Preparation work.
479 relative_cwd = cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000480 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000481 # Creates the right set of variables here. We only care about PATH_VARIABLES.
482 variables = dict(
483 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
484 for k in PATH_VARIABLES if k in variables)
485
486 # Actual work: Process the files.
487 # TODO(maruel): if all the files in a directory are in part tracked and in
488 # part untracked, the directory will not be extracted. Tracked files should be
489 # 'promoted' to be untracked as needed.
490 tracked = trace_inputs.extract_directories(
491 root_dir, tracked, default_blacklist)
492 untracked = trace_inputs.extract_directories(
493 root_dir, untracked, default_blacklist)
494 # touched is not compressed, otherwise it would result in files to be archived
495 # that we don't need.
496
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000497 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000498 def fix(f):
499 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000500 # Important, GYP stores the files with / and not \.
501 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000502 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000503 # If it's not already a variable.
504 if not f.startswith('<'):
505 # relative_cwd is usually the directory containing the gyp file. It may be
506 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000507 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000508 # Convert the whole thing to / since it's isolate's speak.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000509 f = posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000510 posixpath.join(root_dir_posix, f),
511 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000512
513 for variable, root_path in variables.iteritems():
514 if f.startswith(root_path):
515 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000516 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000517 break
518
519 # Now strips off known files we want to ignore and to any specific mangling
520 # as necessary. It's easier to do it here than generate a blacklist.
521 match = EXECUTABLE.match(f)
522 if match:
523 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
524
525 # Blacklist logs and 'First Run' in the PRODUCT_DIR. First Run is not
526 # created by the compile, but by the test itself.
527 if LOG_FILE.match(f) or f == '<(PRODUCT_DIR)/First Run':
528 return None
529
530 if sys.platform == 'darwin':
531 # On OSX, the name of the output is dependent on gyp define, it can be
532 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
533 # Framework.framework'. Furthermore, they are versioned with a gyp
534 # variable. To lower the complexity of the .isolate file, remove all the
535 # individual entries that show up under any of the 4 entries and replace
536 # them with the directory itself. Overall, this results in a bit more
537 # files than strictly necessary.
538 OSX_BUNDLES = (
539 '<(PRODUCT_DIR)/Chromium Framework.framework/',
540 '<(PRODUCT_DIR)/Chromium.app/',
541 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
542 '<(PRODUCT_DIR)/Google Chrome.app/',
543 )
544 for prefix in OSX_BUNDLES:
545 if f.startswith(prefix):
546 # Note this result in duplicate values, so the a set() must be used to
547 # remove duplicates.
548 return prefix
549
550 return f
551
552 tracked = set(filter(None, (fix(f.path) for f in tracked)))
553 untracked = set(filter(None, (fix(f.path) for f in untracked)))
554 touched = set(filter(None, (fix(f.path) for f in touched)))
555 out = classify_files(root_dir, tracked, untracked)
556 if touched:
557 out[KEY_TOUCHED] = sorted(touched)
558 return out
559
560
561def generate_isolate(
562 tracked, untracked, touched, root_dir, variables, relative_cwd):
563 """Generates a clean and complete .isolate file."""
564 result = generate_simplified(
565 tracked, untracked, touched, root_dir, variables, relative_cwd)
566 return {
567 'conditions': [
568 ['OS=="%s"' % get_flavor(), {
569 'variables': result,
570 }],
571 ],
572 }
573
574
575def split_touched(files):
576 """Splits files that are touched vs files that are read."""
577 tracked = []
578 touched = []
579 for f in files:
580 if f.size:
581 tracked.append(f)
582 else:
583 touched.append(f)
584 return tracked, touched
585
586
587def pretty_print(variables, stdout):
588 """Outputs a gyp compatible list from the decoded variables.
589
590 Similar to pprint.print() but with NIH syndrome.
591 """
592 # Order the dictionary keys by these keys in priority.
593 ORDER = (
594 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
595 KEY_TRACKED, KEY_UNTRACKED)
596
597 def sorting_key(x):
598 """Gives priority to 'most important' keys before the others."""
599 if x in ORDER:
600 return str(ORDER.index(x))
601 return x
602
603 def loop_list(indent, items):
604 for item in items:
605 if isinstance(item, basestring):
606 stdout.write('%s\'%s\',\n' % (indent, item))
607 elif isinstance(item, dict):
608 stdout.write('%s{\n' % indent)
609 loop_dict(indent + ' ', item)
610 stdout.write('%s},\n' % indent)
611 elif isinstance(item, list):
612 # A list inside a list will write the first item embedded.
613 stdout.write('%s[' % indent)
614 for index, i in enumerate(item):
615 if isinstance(i, basestring):
616 stdout.write(
617 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
618 elif isinstance(i, dict):
619 stdout.write('{\n')
620 loop_dict(indent + ' ', i)
621 if index != len(item) - 1:
622 x = ', '
623 else:
624 x = ''
625 stdout.write('%s}%s' % (indent, x))
626 else:
627 assert False
628 stdout.write('],\n')
629 else:
630 assert False
631
632 def loop_dict(indent, items):
633 for key in sorted(items, key=sorting_key):
634 item = items[key]
635 stdout.write("%s'%s': " % (indent, key))
636 if isinstance(item, dict):
637 stdout.write('{\n')
638 loop_dict(indent + ' ', item)
639 stdout.write(indent + '},\n')
640 elif isinstance(item, list):
641 stdout.write('[\n')
642 loop_list(indent + ' ', item)
643 stdout.write(indent + '],\n')
644 elif isinstance(item, basestring):
645 stdout.write(
646 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
647 elif item in (True, False, None):
648 stdout.write('%s\n' % item)
649 else:
650 assert False, item
651
652 stdout.write('{\n')
653 loop_dict(' ', variables)
654 stdout.write('}\n')
655
656
657def union(lhs, rhs):
658 """Merges two compatible datastructures composed of dict/list/set."""
659 assert lhs is not None or rhs is not None
660 if lhs is None:
661 return copy.deepcopy(rhs)
662 if rhs is None:
663 return copy.deepcopy(lhs)
664 assert type(lhs) == type(rhs), (lhs, rhs)
665 if hasattr(lhs, 'union'):
666 # Includes set, OSSettings and Configs.
667 return lhs.union(rhs)
668 if isinstance(lhs, dict):
669 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
670 elif isinstance(lhs, list):
671 # Do not go inside the list.
672 return lhs + rhs
673 assert False, type(lhs)
674
675
676def extract_comment(content):
677 """Extracts file level comment."""
678 out = []
679 for line in content.splitlines(True):
680 if line.startswith('#'):
681 out.append(line)
682 else:
683 break
684 return ''.join(out)
685
686
687def eval_content(content):
688 """Evaluates a python file and return the value defined in it.
689
690 Used in practice for .isolate files.
691 """
692 globs = {'__builtins__': None}
693 locs = {}
694 value = eval(content, globs, locs)
695 assert locs == {}, locs
696 assert globs == {'__builtins__': None}, globs
697 return value
698
699
700def verify_variables(variables):
701 """Verifies the |variables| dictionary is in the expected format."""
702 VALID_VARIABLES = [
703 KEY_TOUCHED,
704 KEY_TRACKED,
705 KEY_UNTRACKED,
706 'command',
707 'read_only',
708 ]
709 assert isinstance(variables, dict), variables
710 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
711 for name, value in variables.iteritems():
712 if name == 'read_only':
713 assert value in (True, False, None), value
714 else:
715 assert isinstance(value, list), value
716 assert all(isinstance(i, basestring) for i in value), value
717
718
719def verify_condition(condition):
720 """Verifies the |condition| dictionary is in the expected format."""
721 VALID_INSIDE_CONDITION = ['variables']
722 assert isinstance(condition, list), condition
723 assert 2 <= len(condition) <= 3, condition
724 assert re.match(r'OS==\"([a-z]+)\"', condition[0]), condition[0]
725 for c in condition[1:]:
726 assert isinstance(c, dict), c
727 assert set(VALID_INSIDE_CONDITION).issuperset(set(c)), c.keys()
728 verify_variables(c.get('variables', {}))
729
730
731def verify_root(value):
732 VALID_ROOTS = ['variables', 'conditions']
733 assert isinstance(value, dict), value
734 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
735 verify_variables(value.get('variables', {}))
736
737 conditions = value.get('conditions', [])
738 assert isinstance(conditions, list), conditions
739 for condition in conditions:
740 verify_condition(condition)
741
742
743def remove_weak_dependencies(values, key, item, item_oses):
744 """Remove any oses from this key if the item is already under a strong key."""
745 if key == KEY_TOUCHED:
746 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
747 oses = values.get(stronger_key, {}).get(item, None)
748 if oses:
749 item_oses -= oses
750
751 return item_oses
752
753
csharp@chromium.org31176252012-11-02 13:04:40 +0000754def remove_repeated_dependencies(folders, key, item, item_oses):
755 """Remove any OSes from this key if the item is in a folder that is already
756 included."""
757
758 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
759 for (folder, oses) in folders.iteritems():
760 if folder != item and item.startswith(folder):
761 item_oses -= oses
762
763 return item_oses
764
765
766def get_folders(values_dict):
767 """Return a dict of all the folders in the given value_dict."""
768 return dict((item, oses) for (item, oses) in values_dict.iteritems()
769 if item.endswith('/'))
770
771
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000772def invert_map(variables):
773 """Converts a dict(OS, dict(deptype, list(dependencies)) to a flattened view.
774
775 Returns a tuple of:
776 1. dict(deptype, dict(dependency, set(OSes)) for easier processing.
777 2. All the OSes found as a set.
778 """
779 KEYS = (
780 KEY_TOUCHED,
781 KEY_TRACKED,
782 KEY_UNTRACKED,
783 'command',
784 'read_only',
785 )
786 out = dict((key, {}) for key in KEYS)
787 for os_name, values in variables.iteritems():
788 for key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED):
789 for item in values.get(key, []):
790 out[key].setdefault(item, set()).add(os_name)
791
792 # command needs special handling.
793 command = tuple(values.get('command', []))
794 out['command'].setdefault(command, set()).add(os_name)
795
796 # read_only needs special handling.
797 out['read_only'].setdefault(values.get('read_only'), set()).add(os_name)
798 return out, set(variables)
799
800
801def reduce_inputs(values, oses):
802 """Reduces the invert_map() output to the strictest minimum list.
803
804 1. Construct the inverse map first.
805 2. Look at each individual file and directory, map where they are used and
806 reconstruct the inverse dictionary.
807 3. Do not convert back to negative if only 2 OSes were merged.
808
809 Returns a tuple of:
810 1. the minimized dictionary
811 2. oses passed through as-is.
812 """
813 KEYS = (
814 KEY_TOUCHED,
815 KEY_TRACKED,
816 KEY_UNTRACKED,
817 'command',
818 'read_only',
819 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000820
821 # Folders can only live in KEY_UNTRACKED.
822 folders = get_folders(values.get(KEY_UNTRACKED, {}))
823
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000824 out = dict((key, {}) for key in KEYS)
825 assert all(oses), oses
826 if len(oses) > 2:
827 for key in KEYS:
828 for item, item_oses in values.get(key, {}).iteritems():
829 item_oses = remove_weak_dependencies(values, key, item, item_oses)
csharp@chromium.org31176252012-11-02 13:04:40 +0000830 item_oses = remove_repeated_dependencies(folders, key, item, item_oses)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000831 if not item_oses:
832 continue
833
834 # Converts all oses.difference('foo') to '!foo'.
835 assert all(item_oses), item_oses
836 missing = oses.difference(item_oses)
837 if len(missing) == 1:
838 # Replace it with a negative.
839 out[key][item] = set(['!' + tuple(missing)[0]])
840 elif not missing:
841 out[key][item] = set([None])
842 else:
843 out[key][item] = set(item_oses)
844 else:
845 for key in KEYS:
846 for item, item_oses in values.get(key, {}).iteritems():
847 item_oses = remove_weak_dependencies(values, key, item, item_oses)
848 if not item_oses:
849 continue
850
851 # Converts all oses.difference('foo') to '!foo'.
852 assert None not in item_oses, item_oses
853 out[key][item] = set(item_oses)
854 return out, oses
855
856
857def convert_map_to_isolate_dict(values, oses):
858 """Regenerates back a .isolate configuration dict from files and dirs
859 mappings generated from reduce_inputs().
860 """
861 # First, inverse the mapping to make it dict first.
862 config = {}
863 for key in values:
864 for item, oses in values[key].iteritems():
865 if item is None:
866 # For read_only default.
867 continue
868 for cond_os in oses:
869 cond_key = None if cond_os is None else cond_os.lstrip('!')
870 # Insert the if/else dicts.
871 condition_values = config.setdefault(cond_key, [{}, {}])
872 # If condition is negative, use index 1, else use index 0.
873 cond_value = condition_values[int((cond_os or '').startswith('!'))]
874 variables = cond_value.setdefault('variables', {})
875
876 if item in (True, False):
877 # One-off for read_only.
878 variables[key] = item
879 else:
880 if isinstance(item, tuple) and item:
881 # One-off for command.
882 # Do not merge lists and do not sort!
883 # Note that item is a tuple.
884 assert key not in variables
885 variables[key] = list(item)
886 elif item:
887 # The list of items (files or dirs). Append the new item and keep
888 # the list sorted.
889 l = variables.setdefault(key, [])
890 l.append(item)
891 l.sort()
892
893 out = {}
894 for o in sorted(config):
895 d = config[o]
896 if o is None:
897 assert not d[1]
898 out = union(out, d[0])
899 else:
900 c = out.setdefault('conditions', [])
901 if d[1]:
902 c.append(['OS=="%s"' % o] + d)
903 else:
904 c.append(['OS=="%s"' % o] + d[0:1])
905 return out
906
907
908### Internal state files.
909
910
911class OSSettings(object):
912 """Represents the dependencies for an OS. The structure is immutable.
913
914 It's the .isolate settings for a specific file.
915 """
916 def __init__(self, name, values):
917 self.name = name
918 verify_variables(values)
919 self.touched = sorted(values.get(KEY_TOUCHED, []))
920 self.tracked = sorted(values.get(KEY_TRACKED, []))
921 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
922 self.command = values.get('command', [])[:]
923 self.read_only = values.get('read_only')
924
925 def union(self, rhs):
926 assert self.name == rhs.name
maruel@chromium.org669edcb2012-11-02 19:16:14 +0000927 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000928 var = {
929 KEY_TOUCHED: sorted(self.touched + rhs.touched),
930 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
931 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
932 'command': self.command or rhs.command,
933 'read_only': rhs.read_only if self.read_only is None else self.read_only,
934 }
935 return OSSettings(self.name, var)
936
937 def flatten(self):
938 out = {}
939 if self.command:
940 out['command'] = self.command
941 if self.touched:
942 out[KEY_TOUCHED] = self.touched
943 if self.tracked:
944 out[KEY_TRACKED] = self.tracked
945 if self.untracked:
946 out[KEY_UNTRACKED] = self.untracked
947 if self.read_only is not None:
948 out['read_only'] = self.read_only
949 return out
950
951
952class Configs(object):
953 """Represents a processed .isolate file.
954
955 Stores the file in a processed way, split by each the OS-specific
956 configurations.
957
958 The self.per_os[None] member contains all the 'else' clauses plus the default
959 values. It is not included in the flatten() result.
960 """
961 def __init__(self, oses, file_comment):
962 self.file_comment = file_comment
963 self.per_os = {
964 None: OSSettings(None, {}),
965 }
966 self.per_os.update(dict((name, OSSettings(name, {})) for name in oses))
967
968 def union(self, rhs):
969 items = list(set(self.per_os.keys() + rhs.per_os.keys()))
970 # Takes the first file comment, prefering lhs.
971 out = Configs(items, self.file_comment or rhs.file_comment)
972 for key in items:
973 out.per_os[key] = union(self.per_os.get(key), rhs.per_os.get(key))
974 return out
975
976 def add_globals(self, values):
977 for key in self.per_os:
978 self.per_os[key] = self.per_os[key].union(OSSettings(key, values))
979
980 def add_values(self, for_os, values):
981 self.per_os[for_os] = self.per_os[for_os].union(OSSettings(for_os, values))
982
983 def add_negative_values(self, for_os, values):
984 """Includes the variables to all OSes except |for_os|.
985
986 This includes 'None' so unknown OSes gets it too.
987 """
988 for key in self.per_os:
989 if key != for_os:
990 self.per_os[key] = self.per_os[key].union(OSSettings(key, values))
991
992 def flatten(self):
993 """Returns a flat dictionary representation of the configuration.
994
995 Skips None pseudo-OS.
996 """
997 return dict(
998 (k, v.flatten()) for k, v in self.per_os.iteritems() if k is not None)
999
1000
1001def load_isolate_as_config(value, file_comment, default_oses):
1002 """Parses one .isolate file and returns a Configs() instance.
1003
1004 |value| is the loaded dictionary that was defined in the gyp file.
1005
1006 The expected format is strict, anything diverting from the format below will
1007 throw an assert:
1008 {
1009 'variables': {
1010 'command': [
1011 ...
1012 ],
1013 'isolate_dependency_tracked': [
1014 ...
1015 ],
1016 'isolate_dependency_untracked': [
1017 ...
1018 ],
1019 'read_only': False,
1020 },
1021 'conditions': [
1022 ['OS=="<os>"', {
1023 'variables': {
1024 ...
1025 },
1026 }, { # else
1027 'variables': {
1028 ...
1029 },
1030 }],
1031 ...
1032 ],
1033 }
1034 """
1035 verify_root(value)
1036
1037 # Scan to get the list of OSes.
1038 conditions = value.get('conditions', [])
1039 oses = set(re.match(r'OS==\"([a-z]+)\"', c[0]).group(1) for c in conditions)
1040 oses = oses.union(default_oses)
1041 configs = Configs(oses, file_comment)
1042
1043 # Global level variables.
1044 configs.add_globals(value.get('variables', {}))
1045
1046 # OS specific variables.
1047 for condition in conditions:
1048 condition_os = re.match(r'OS==\"([a-z]+)\"', condition[0]).group(1)
1049 configs.add_values(condition_os, condition[1].get('variables', {}))
1050 if len(condition) > 2:
1051 configs.add_negative_values(
1052 condition_os, condition[2].get('variables', {}))
1053 return configs
1054
1055
1056def load_isolate_for_flavor(content, flavor):
1057 """Loads the .isolate file and returns the information unprocessed.
1058
1059 Returns the command, dependencies and read_only flag. The dependencies are
1060 fixed to use os.path.sep.
1061 """
1062 # Load the .isolate file, process its conditions, retrieve the command and
1063 # dependencies.
1064 configs = load_isolate_as_config(eval_content(content), None, DEFAULT_OSES)
1065 config = configs.per_os.get(flavor) or configs.per_os.get(None)
1066 if not config:
1067 raise ExecutionError('Failed to load configuration for \'%s\'' % flavor)
1068 # Merge tracked and untracked dependencies, isolate.py doesn't care about the
1069 # trackability of the dependencies, only the build tool does.
1070 dependencies = [
1071 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1072 ]
1073 touched = [f.replace('/', os.path.sep) for f in config.touched]
1074 return config.command, dependencies, touched, config.read_only
1075
1076
1077class Flattenable(object):
1078 """Represents data that can be represented as a json file."""
1079 MEMBERS = ()
1080
1081 def flatten(self):
1082 """Returns a json-serializable version of itself.
1083
1084 Skips None entries.
1085 """
1086 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1087 return dict((member, value) for member, value in items if value is not None)
1088
1089 @classmethod
1090 def load(cls, data):
1091 """Loads a flattened version."""
1092 data = data.copy()
1093 out = cls()
1094 for member in out.MEMBERS:
1095 if member in data:
1096 # Access to a protected member XXX of a client class
1097 # pylint: disable=W0212
1098 out._load_member(member, data.pop(member))
1099 if data:
1100 raise ValueError(
1101 'Found unexpected entry %s while constructing an object %s' %
1102 (data, cls.__name__), data, cls.__name__)
1103 return out
1104
1105 def _load_member(self, member, value):
1106 """Loads a member into self."""
1107 setattr(self, member, value)
1108
1109 @classmethod
1110 def load_file(cls, filename):
1111 """Loads the data from a file or return an empty instance."""
1112 out = cls()
1113 try:
1114 out = cls.load(trace_inputs.read_json(filename))
1115 logging.debug('Loaded %s(%s)' % (cls.__name__, filename))
1116 except (IOError, ValueError):
1117 logging.warn('Failed to load %s' % filename)
1118 return out
1119
1120
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001121class IsolatedFile(Flattenable):
1122 """Describes the content of a .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001123
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001124 This file is used by run_isolated.py so its content is strictly only
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001125 what is necessary to run the test outside of a checkout.
1126
1127 It is important to note that the 'files' dict keys are using native OS path
1128 separator instead of '/' used in .isolate file.
1129 """
1130 MEMBERS = (
1131 'command',
1132 'files',
1133 'os',
1134 'read_only',
1135 'relative_cwd',
1136 )
1137
1138 os = get_flavor()
1139
1140 def __init__(self):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001141 super(IsolatedFile, self).__init__()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001142 self.command = []
1143 self.files = {}
1144 self.read_only = None
1145 self.relative_cwd = None
1146
1147 def update(self, command, infiles, touched, read_only, relative_cwd):
1148 """Updates the result state with new information."""
1149 self.command = command
1150 # Add new files.
1151 for f in infiles:
1152 self.files.setdefault(f, {})
1153 for f in touched:
1154 self.files.setdefault(f, {})['touched_only'] = True
1155 # Prune extraneous files that are not a dependency anymore.
1156 for f in set(self.files).difference(set(infiles).union(touched)):
1157 del self.files[f]
1158 if read_only is not None:
1159 self.read_only = read_only
1160 self.relative_cwd = relative_cwd
1161
1162 def _load_member(self, member, value):
1163 if member == 'os':
1164 if value != self.os:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001165 raise run_isolated.ConfigError(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001166 'The .isolated file was created on another platform')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001167 else:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001168 super(IsolatedFile, self)._load_member(member, value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001169
1170 def __str__(self):
1171 out = '%s(\n' % self.__class__.__name__
1172 out += ' command: %s\n' % self.command
1173 out += ' files: %d\n' % len(self.files)
1174 out += ' read_only: %s\n' % self.read_only
1175 out += ' relative_cwd: %s)' % self.relative_cwd
1176 return out
1177
1178
1179class SavedState(Flattenable):
1180 """Describes the content of a .state file.
1181
1182 The items in this file are simply to improve the developer's life and aren't
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001183 used by run_isolated.py. This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001184
1185 isolate_file permits to find back root_dir, variables are used for stateful
1186 rerun.
1187 """
1188 MEMBERS = (
1189 'isolate_file',
1190 'variables',
1191 )
1192
1193 def __init__(self):
1194 super(SavedState, self).__init__()
1195 self.isolate_file = None
1196 self.variables = {}
1197
1198 def update(self, isolate_file, variables):
1199 """Updates the saved state with new information."""
1200 self.isolate_file = isolate_file
1201 self.variables.update(variables)
1202
1203 @classmethod
1204 def load(cls, data):
1205 out = super(SavedState, cls).load(data)
1206 if out.isolate_file:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001207 out.isolate_file = trace_inputs.get_native_path_case(
1208 unicode(out.isolate_file))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001209 return out
1210
1211 def __str__(self):
1212 out = '%s(\n' % self.__class__.__name__
1213 out += ' isolate_file: %s\n' % self.isolate_file
1214 out += ' variables: %s' % ''.join(
1215 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1216 out += ')'
1217 return out
1218
1219
1220class CompleteState(object):
1221 """Contains all the state to run the task at hand."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001222 def __init__(self, isolated_filepath, isolated, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001223 super(CompleteState, self).__init__()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001224 self.isolated_filepath = isolated_filepath
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001225 # Contains the data that will be used by run_isolated.py
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001226 self.isolated = isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001227 # Contains the data to ease developer's use-case but that is not strictly
1228 # necessary.
1229 self.saved_state = saved_state
1230
1231 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001232 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001233 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001234 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001235 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001236 isolated_filepath,
1237 IsolatedFile.load_file(isolated_filepath),
1238 SavedState.load_file(isolatedfile_to_state(isolated_filepath)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001239
csharp@chromium.org01856802012-11-12 17:48:13 +00001240 def load_isolate(self, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001241 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001242 .isolate file.
1243
1244 Processes the loaded data, deduce root_dir, relative_cwd.
1245 """
1246 # Make sure to not depend on os.getcwd().
1247 assert os.path.isabs(isolate_file), isolate_file
1248 logging.info(
1249 'CompleteState.load_isolate(%s, %s)' % (isolate_file, variables))
1250 relative_base_dir = os.path.dirname(isolate_file)
1251
1252 # Processes the variables and update the saved state.
1253 variables = process_variables(variables, relative_base_dir)
1254 self.saved_state.update(isolate_file, variables)
1255
1256 with open(isolate_file, 'r') as f:
1257 # At that point, variables are not replaced yet in command and infiles.
1258 # infiles may contain directory entries and is in posix style.
1259 command, infiles, touched, read_only = load_isolate_for_flavor(
1260 f.read(), get_flavor())
1261 command = [eval_variables(i, self.saved_state.variables) for i in command]
1262 infiles = [eval_variables(f, self.saved_state.variables) for f in infiles]
1263 touched = [eval_variables(f, self.saved_state.variables) for f in touched]
1264 # root_dir is automatically determined by the deepest root accessed with the
1265 # form '../../foo/bar'.
1266 root_dir = determine_root_dir(relative_base_dir, infiles + touched)
1267 # The relative directory is automatically determined by the relative path
1268 # between root_dir and the directory containing the .isolate file,
1269 # isolate_base_dir.
1270 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
1271 # Normalize the files based to root_dir. It is important to keep the
1272 # trailing os.path.sep at that step.
1273 infiles = [
1274 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1275 for f in infiles
1276 ]
1277 touched = [
1278 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1279 for f in touched
1280 ]
1281 # Expand the directories by listing each file inside. Up to now, trailing
1282 # os.path.sep must be kept. Do not expand 'touched'.
1283 infiles = expand_directories_and_symlinks(
1284 root_dir,
1285 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001286 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
1287 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001288
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001289 # Finally, update the new stuff in the foo.isolated file, the file that is
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001290 # used by run_isolated.py.
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001291 self.isolated.update(command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001292 logging.debug(self)
1293
maruel@chromium.org9268f042012-10-17 17:36:41 +00001294 def process_inputs(self, subdir):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001295 """Updates self.isolated.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001296
maruel@chromium.org9268f042012-10-17 17:36:41 +00001297 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1298 file is tainted.
1299
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001300 See process_input() for more information.
1301 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001302 for infile in sorted(self.isolated.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001303 if subdir and not infile.startswith(subdir):
1304 self.isolated.files.pop(infile)
1305 else:
1306 filepath = os.path.join(self.root_dir, infile)
1307 self.isolated.files[infile] = process_input(
1308 filepath, self.isolated.files[infile], self.isolated.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001309
1310 def save_files(self):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001311 """Saves both self.isolated and self.saved_state."""
1312 logging.debug('Dumping to %s' % self.isolated_filepath)
1313 trace_inputs.write_json(
1314 self.isolated_filepath, self.isolated.flatten(), True)
1315 total_bytes = sum(i
1316 .get('size', 0) for i in self.isolated.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001317 if total_bytes:
1318 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001319 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001320 logging.debug('Dumping to %s' % saved_state_file)
1321 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1322
1323 @property
1324 def root_dir(self):
1325 """isolate_file is always inside relative_cwd relative to root_dir."""
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001326 if not self.saved_state.isolate_file:
1327 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001328 isolate_dir = os.path.dirname(self.saved_state.isolate_file)
1329 # Special case '.'.
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001330 if self.isolated.relative_cwd == '.':
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001331 return isolate_dir
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001332 assert isolate_dir.endswith(self.isolated.relative_cwd), (
1333 isolate_dir, self.isolated.relative_cwd)
1334 return isolate_dir[:-(len(self.isolated.relative_cwd) + 1)]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001335
1336 @property
1337 def resultdir(self):
1338 """Directory containing the results, usually equivalent to the variable
1339 PRODUCT_DIR.
1340 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001341 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001342
1343 def __str__(self):
1344 def indent(data, indent_length):
1345 """Indents text."""
1346 spacing = ' ' * indent_length
1347 return ''.join(spacing + l for l in str(data).splitlines(True))
1348
1349 out = '%s(\n' % self.__class__.__name__
1350 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001351 out += ' result: %s\n' % indent(self.isolated, 2)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001352 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1353 return out
1354
1355
maruel@chromium.org9268f042012-10-17 17:36:41 +00001356def load_complete_state(options, subdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001357 """Loads a CompleteState.
1358
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001359 This includes data from .isolate, .isolated and .state files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001360
1361 Arguments:
1362 options: Options instance generated with OptionParserIsolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001363 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001364 if options.isolated:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001365 # Load the previous state if it was present. Namely, "foo.isolated" and
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001366 # "foo.state".
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001367 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001368 else:
1369 # Constructs a dummy object that cannot be saved. Useful for temporary
1370 # commands like 'run'.
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001371 complete_state = CompleteState(None, IsolatedFile(), SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001372 options.isolate = options.isolate or complete_state.saved_state.isolate_file
1373 if not options.isolate:
1374 raise ExecutionError('A .isolate file is required.')
1375 if (complete_state.saved_state.isolate_file and
1376 options.isolate != complete_state.saved_state.isolate_file):
1377 raise ExecutionError(
1378 '%s and %s do not match.' % (
1379 options.isolate, complete_state.saved_state.isolate_file))
1380
1381 # Then load the .isolate and expands directories.
csharp@chromium.org01856802012-11-12 17:48:13 +00001382 complete_state.load_isolate(options.isolate, options.variables,
1383 options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001384
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001385 # Regenerate complete_state.isolated.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001386 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001387 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001388 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1389 subdir = subdir.replace('/', os.path.sep)
1390 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001391 return complete_state
1392
1393
1394def read_trace_as_isolate_dict(complete_state):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001395 """Reads a trace and returns the .isolate dictionary.
1396
1397 Returns exceptions during the log parsing so it can be re-raised.
1398 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001399 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001400 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001401 if not os.path.isfile(logfile):
1402 raise ExecutionError(
1403 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1404 try:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00001405 data = api.parse_log(logfile, default_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001406 exceptions = [i['exception'] for i in data if 'exception' in i]
1407 results = (i['results'] for i in data if 'results' in i)
1408 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1409 files = set(sum((result.existent for result in results_stripped), []))
1410 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001411 value = generate_isolate(
1412 tracked,
1413 [],
1414 touched,
1415 complete_state.root_dir,
1416 complete_state.saved_state.variables,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001417 complete_state.isolated.relative_cwd)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001418 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001419 except trace_inputs.TracingFailure, e:
1420 raise ExecutionError(
1421 'Reading traces failed for: %s\n%s' %
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001422 (' '.join(complete_state.isolated.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001423
1424
1425def print_all(comment, data, stream):
1426 """Prints a complete .isolate file and its top-level file comment into a
1427 stream.
1428 """
1429 if comment:
1430 stream.write(comment)
1431 pretty_print(data, stream)
1432
1433
1434def merge(complete_state):
1435 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001436 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001437
1438 # Now take that data and union it into the original .isolate file.
1439 with open(complete_state.saved_state.isolate_file, 'r') as f:
1440 prev_content = f.read()
1441 prev_config = load_isolate_as_config(
1442 eval_content(prev_content),
1443 extract_comment(prev_content),
1444 DEFAULT_OSES)
1445 new_config = load_isolate_as_config(value, '', DEFAULT_OSES)
1446 config = union(prev_config, new_config)
1447 # pylint: disable=E1103
1448 data = convert_map_to_isolate_dict(
1449 *reduce_inputs(*invert_map(config.flatten())))
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001450 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001451 with open(complete_state.saved_state.isolate_file, 'wb') as f:
1452 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001453 if exceptions:
1454 # It got an exception, raise the first one.
1455 raise \
1456 exceptions[0][0], \
1457 exceptions[0][1], \
1458 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001459
1460
1461def CMDcheck(args):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001462 """Checks that all the inputs are present and update .isolated."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001463 parser = OptionParserIsolate(command='check')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001464 parser.add_option('--subdir', help='Filters to a subdirectory')
1465 options, args = parser.parse_args(args)
1466 if args:
1467 parser.error('Unsupported argument: %s' % args)
1468 complete_state = load_complete_state(options, options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001469
1470 # Nothing is done specifically. Just store the result and state.
1471 complete_state.save_files()
1472 return 0
1473
1474
1475def CMDhashtable(args):
1476 """Creates a hash table content addressed object store.
1477
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001478 All the files listed in the .isolated file are put in the output directory
1479 with the file name being the sha-1 of the file's content.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001480 """
1481 parser = OptionParserIsolate(command='hashtable')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001482 parser.add_option('--subdir', help='Filters to a subdirectory')
1483 options, args = parser.parse_args(args)
1484 if args:
1485 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001486
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001487 with run_isolated.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001488 success = False
1489 try:
maruel@chromium.org9268f042012-10-17 17:36:41 +00001490 complete_state = load_complete_state(options, options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001491 options.outdir = (
1492 options.outdir or os.path.join(complete_state.resultdir, 'hashtable'))
1493 # Make sure that complete_state isn't modified until save_files() is
1494 # called, because any changes made to it here will propagate to the files
1495 # created (which is probably not intended).
1496 complete_state.save_files()
1497
1498 logging.info('Creating content addressed object store with %d item',
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001499 len(complete_state.isolated.files))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001500
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001501 with open(complete_state.isolated_filepath, 'rb') as f:
maruel@chromium.org861a5e72012-10-09 14:49:42 +00001502 content = f.read()
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001503 isolated_hash = hashlib.sha1(content).hexdigest()
1504 isolated_metadata = {
1505 'sha-1': isolated_hash,
csharp@chromium.orgd62bcb92012-10-16 17:45:33 +00001506 'size': len(content),
1507 'priority': '0'
1508 }
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001509
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001510 infiles = complete_state.isolated.files
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001511 infiles[complete_state.isolated_filepath] = isolated_metadata
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001512
1513 if re.match(r'^https?://.+$', options.outdir):
maruel@chromium.orgc6f90062012-11-07 18:32:22 +00001514 isolateserver_archive.upload_sha1_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001515 base_url=options.outdir,
1516 indir=complete_state.root_dir,
1517 infiles=infiles)
1518 else:
1519 recreate_tree(
1520 outdir=options.outdir,
1521 indir=complete_state.root_dir,
1522 infiles=infiles,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001523 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001524 as_sha1=True)
1525 success = True
1526 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001527 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001528 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001529 if not success and os.path.isfile(options.isolated):
1530 os.remove(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001531
1532
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001533def CMDmerge(args):
1534 """Reads and merges the data from the trace back into the original .isolate.
1535
1536 Ignores --outdir.
1537 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001538 parser = OptionParserIsolate(command='merge', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001539 options, args = parser.parse_args(args)
1540 if args:
1541 parser.error('Unsupported argument: %s' % args)
1542 complete_state = load_complete_state(options, None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001543 merge(complete_state)
1544 return 0
1545
1546
1547def CMDread(args):
1548 """Reads the trace file generated with command 'trace'.
1549
1550 Ignores --outdir.
1551 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001552 parser = OptionParserIsolate(command='read', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001553 options, args = parser.parse_args(args)
1554 if args:
1555 parser.error('Unsupported argument: %s' % args)
1556 complete_state = load_complete_state(options, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001557 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001558 pretty_print(value, sys.stdout)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001559 if exceptions:
1560 # It got an exception, raise the first one.
1561 raise \
1562 exceptions[0][0], \
1563 exceptions[0][1], \
1564 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001565 return 0
1566
1567
1568def CMDremap(args):
1569 """Creates a directory with all the dependencies mapped into it.
1570
1571 Useful to test manually why a test is failing. The target executable is not
1572 run.
1573 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001574 parser = OptionParserIsolate(command='remap', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001575 options, args = parser.parse_args(args)
1576 if args:
1577 parser.error('Unsupported argument: %s' % args)
1578 complete_state = load_complete_state(options, None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001579
1580 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001581 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001582 'isolate', complete_state.root_dir)
1583 else:
1584 if not os.path.isdir(options.outdir):
1585 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001586 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001587 if len(os.listdir(options.outdir)):
1588 raise ExecutionError('Can\'t remap in a non-empty directory')
1589 recreate_tree(
1590 outdir=options.outdir,
1591 indir=complete_state.root_dir,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001592 infiles=complete_state.isolated.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001593 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001594 as_sha1=False)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001595 if complete_state.isolated.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001596 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001597
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001598 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001599 complete_state.save_files()
1600 return 0
1601
1602
1603def CMDrun(args):
1604 """Runs the test executable in an isolated (temporary) directory.
1605
1606 All the dependencies are mapped into the temporary directory and the
1607 directory is cleaned up after the target exits. Warning: if -outdir is
1608 specified, it is deleted upon exit.
1609
1610 Argument processing stops at the first non-recognized argument and these
1611 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001612 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001613 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001614 parser = OptionParserIsolate(command='run', require_isolated=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001615 parser.enable_interspersed_args()
1616 options, args = parser.parse_args(args)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001617 complete_state = load_complete_state(options, None)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001618 cmd = complete_state.isolated.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001619 if not cmd:
1620 raise ExecutionError('No command to run')
1621 cmd = trace_inputs.fix_python_path(cmd)
1622 try:
1623 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001624 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001625 'isolate', complete_state.root_dir)
1626 else:
1627 if not os.path.isdir(options.outdir):
1628 os.makedirs(options.outdir)
1629 recreate_tree(
1630 outdir=options.outdir,
1631 indir=complete_state.root_dir,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001632 infiles=complete_state.isolated.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001633 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001634 as_sha1=False)
1635 cwd = os.path.normpath(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001636 os.path.join(options.outdir, complete_state.isolated.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001637 if not os.path.isdir(cwd):
1638 # It can happen when no files are mapped from the directory containing the
1639 # .isolate file. But the directory must exist to be the current working
1640 # directory.
1641 os.makedirs(cwd)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001642 if complete_state.isolated.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001643 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001644 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1645 result = subprocess.call(cmd, cwd=cwd)
1646 finally:
1647 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001648 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001649
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001650 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001651 complete_state.save_files()
1652 return result
1653
1654
1655def CMDtrace(args):
1656 """Traces the target using trace_inputs.py.
1657
1658 It runs the executable without remapping it, and traces all the files it and
1659 its child processes access. Then the 'read' command can be used to generate an
1660 updated .isolate file out of it.
1661
1662 Argument processing stops at the first non-recognized argument and these
1663 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001664 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001665 """
1666 parser = OptionParserIsolate(command='trace')
1667 parser.enable_interspersed_args()
1668 parser.add_option(
1669 '-m', '--merge', action='store_true',
1670 help='After tracing, merge the results back in the .isolate file')
1671 options, args = parser.parse_args(args)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001672 complete_state = load_complete_state(options, None)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001673 cmd = complete_state.isolated.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001674 if not cmd:
1675 raise ExecutionError('No command to run')
1676 cmd = trace_inputs.fix_python_path(cmd)
1677 cwd = os.path.normpath(os.path.join(
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001678 unicode(complete_state.root_dir), complete_state.isolated.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001679 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1680 if not os.path.isfile(cmd[0]):
1681 raise ExecutionError(
1682 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001683 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1684 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001685 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001686 api.clean_trace(logfile)
1687 try:
1688 with api.get_tracer(logfile) as tracer:
1689 result, _ = tracer.trace(
1690 cmd,
1691 cwd,
1692 'default',
1693 True)
1694 except trace_inputs.TracingFailure, e:
1695 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1696
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001697 if result:
1698 logging.error('Tracer exited with %d, which means the tests probably '
1699 'failed so the trace is probably incomplete.', result)
1700
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001701 complete_state.save_files()
1702
1703 if options.merge:
1704 merge(complete_state)
1705
1706 return result
1707
1708
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001709def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001710 """Adds --isolated and --variable to an OptionParser."""
1711 parser.add_option(
1712 '-s', '--isolated',
1713 metavar='FILE',
1714 help='.isolated file to generate or read')
1715 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001716 parser.add_option(
1717 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001718 dest='isolated',
1719 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001720 default_variables = [('OS', get_flavor())]
1721 if sys.platform in ('win32', 'cygwin'):
1722 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
1723 else:
1724 default_variables.append(('EXECUTABLE_SUFFIX', ''))
1725 parser.add_option(
1726 '-V', '--variable',
1727 nargs=2,
1728 action='append',
1729 default=default_variables,
1730 dest='variables',
1731 metavar='FOO BAR',
1732 help='Variables to process in the .isolate file, default: %default. '
1733 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001734 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001735
1736
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001737def parse_variable_option(parser, options, require_isolated):
1738 """Processes --isolated and --variable."""
1739 if options.isolated:
1740 options.isolated = os.path.abspath(
1741 options.isolated.replace('/', os.path.sep))
1742 if require_isolated and not options.isolated:
1743 parser.error('--isolated is required.')
1744 if options.isolated and not options.isolated.endswith('.isolated'):
1745 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001746 options.variables = dict(options.variables)
1747
1748
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001749class OptionParserIsolate(trace_inputs.OptionParserWithNiceDescription):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001750 """Adds automatic --isolate, --isolated, --out and --variable handling."""
1751 def __init__(self, require_isolated=True, **kwargs):
maruel@chromium.org55276902012-10-05 20:56:19 +00001752 trace_inputs.OptionParserWithNiceDescription.__init__(
1753 self,
1754 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1755 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001756 group = optparse.OptionGroup(self, "Common options")
1757 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001758 '-i', '--isolate',
1759 metavar='FILE',
1760 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001761 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001762 group.add_option(
1763 '-o', '--outdir', metavar='DIR',
1764 help='Directory used to recreate the tree or store the hash table. '
1765 'If the environment variable ISOLATE_HASH_TABLE_DIR exists, it '
1766 'will be used. Otherwise, for run and remap, uses a /tmp '
1767 'subdirectory. For the other modes, defaults to the directory '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001768 'containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00001769 group.add_option(
1770 '--ignore_broken_items', action='store_true',
1771 help='Indicates that invalid entries in the isolated file won\'t '
1772 'cause exceptions, but instead will just be logged.')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001773 self.add_option_group(group)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001774 self.require_isolated = require_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001775
1776 def parse_args(self, *args, **kwargs):
1777 """Makes sure the paths make sense.
1778
1779 On Windows, / and \ are often mixed together in a path.
1780 """
1781 options, args = trace_inputs.OptionParserWithNiceDescription.parse_args(
1782 self, *args, **kwargs)
1783 if not self.allow_interspersed_args and args:
1784 self.error('Unsupported argument: %s' % args)
1785
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001786 parse_variable_option(self, options, self.require_isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001787
1788 if options.isolate:
1789 options.isolate = trace_inputs.get_native_path_case(
1790 os.path.abspath(
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001791 unicode(options.isolate.replace('/', os.path.sep))))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001792
1793 if options.outdir and not re.match(r'^https?://.+$', options.outdir):
1794 options.outdir = os.path.abspath(
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001795 unicode(options.outdir.replace('/', os.path.sep)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001796
1797 return options, args
1798
1799
1800### Glue code to make all the commands works magically.
1801
1802
1803CMDhelp = trace_inputs.CMDhelp
1804
1805
1806def main(argv):
1807 try:
1808 return trace_inputs.main_impl(argv)
1809 except (
1810 ExecutionError,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001811 run_isolated.MappingError,
1812 run_isolated.ConfigError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001813 sys.stderr.write('\nError: ')
1814 sys.stderr.write(str(e))
1815 sys.stderr.write('\n')
1816 return 1
1817
1818
1819if __name__ == '__main__':
1820 sys.exit(main(sys.argv[1:]))