blob: f4e79dcc64a5096b23706146eca67d0a569ec1fd [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 = []
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000153 try:
154 for filename in os.listdir(infile):
155 inner_relfile = os.path.join(relfile, filename)
156 if blacklist(inner_relfile):
157 continue
158 if os.path.isdir(os.path.join(indir, inner_relfile)):
159 inner_relfile += os.path.sep
160 outfiles.extend(
161 expand_directory_and_symlink(indir, inner_relfile, blacklist))
162 return outfiles
163 except OSError as e:
164 raise run_isolated.MappingError('Unable to iterate over directories.\n'
165 '%s' % e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000166 else:
167 # Always add individual files even if they were blacklisted.
168 if os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000169 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000170 'Input directory %s must have a trailing slash' % infile)
171
172 if not os.path.isfile(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000173 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000174 'Input file %s doesn\'t exist' % infile)
175
176 return [relfile]
177
178
csharp@chromium.org01856802012-11-12 17:48:13 +0000179def expand_directories_and_symlinks(indir, infiles, blacklist,
180 ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000181 """Expands the directories and the symlinks, applies the blacklist and
182 verifies files exist.
183
184 Files are specified in os native path separator.
185 """
186 outfiles = []
187 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +0000188 try:
189 outfiles.extend(expand_directory_and_symlink(indir, relfile, blacklist))
190 except run_isolated.MappingError as e:
191 if ignore_broken_items:
192 logging.info('warning: %s', e)
193 else:
194 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000195 return outfiles
196
197
198def recreate_tree(outdir, indir, infiles, action, as_sha1):
199 """Creates a new tree with only the input files in it.
200
201 Arguments:
202 outdir: Output directory to create the files in.
203 indir: Root directory the infiles are based in.
204 infiles: dict of files to map from |indir| to |outdir|.
205 action: See assert below.
206 as_sha1: Output filename is the sha1 instead of relfile.
207 """
208 logging.info(
209 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_sha1=%s)' %
210 (outdir, indir, len(infiles), action, as_sha1))
211
212 assert action in (
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000213 run_isolated.HARDLINK,
214 run_isolated.SYMLINK,
215 run_isolated.COPY)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000216 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000217 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000218 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000219 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000220
221 for relfile, metadata in infiles.iteritems():
222 infile = os.path.join(indir, relfile)
223 if as_sha1:
224 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000225 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000226 # Skip links when storing a hashtable.
227 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000228 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000229 if os.path.isfile(outfile):
230 # Just do a quick check that the file size matches. No need to stat()
231 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000232 if not 's' in metadata:
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000233 raise run_isolated.MappingError(
234 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000235 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000236 continue
237 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000238 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000239 os.remove(outfile)
240 else:
241 outfile = os.path.join(outdir, relfile)
242 outsubdir = os.path.dirname(outfile)
243 if not os.path.isdir(outsubdir):
244 os.makedirs(outsubdir)
245
246 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000247 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000248 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000249 if 'l' in metadata:
250 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000251 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000252 # symlink doesn't exist on Windows.
253 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000254 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000255 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000256
257
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000258def process_input(filepath, prevdict, read_only):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000259 """Processes an input file, a dependency, and return meta data about it.
260
261 Arguments:
262 - filepath: File to act on.
263 - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
264 to skip recalculating the hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000265 - read_only: If True, the file mode is manipulated. In practice, only save
266 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
267 windows, mode is not set since all files are 'executable' by
268 default.
269
270 Behaviors:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000271 - Retrieves the file mode, file size, file timestamp, file link
272 destination if it is a file link and calcultate the SHA-1 of the file's
273 content if the path points to a file and not a symlink.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000274 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000275 out = {}
276 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000277 # if prevdict.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000278 # # The file's content is ignored. Skip the time and hard code mode.
279 # if get_flavor() != 'win':
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000280 # out['m'] = stat.S_IRUSR | stat.S_IRGRP
281 # out['s'] = 0
282 # out['h'] = SHA_1_NULL
283 # out['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000284 # return out
285
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000286 # Always check the file stat and check if it is a link. The timestamp is used
287 # to know if the file's content/symlink destination should be looked into.
288 # E.g. only reuse from prevdict if the timestamp hasn't changed.
289 # There is the risk of the file's timestamp being reset to its last value
290 # manually while its content changed. We don't protect against that use case.
291 try:
292 filestats = os.lstat(filepath)
293 except OSError:
294 # The file is not present.
295 raise run_isolated.MappingError('%s is missing' % filepath)
296 is_link = stat.S_ISLNK(filestats.st_mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000297
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000298 if get_flavor() != 'win':
299 # Ignore file mode on Windows since it's not really useful there.
300 filemode = stat.S_IMODE(filestats.st_mode)
301 # Remove write access for group and all access to 'others'.
302 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
303 if read_only:
304 filemode &= ~stat.S_IWUSR
305 if filemode & stat.S_IXUSR:
306 filemode |= stat.S_IXGRP
307 else:
308 filemode &= ~stat.S_IXGRP
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000309 out['m'] = filemode
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000310
311 # Used to skip recalculating the hash or link destination. Use the most recent
312 # update time.
313 # TODO(maruel): Save it in the .state file instead of .isolated so the
314 # .isolated file is deterministic.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000315 out['t'] = int(round(filestats.st_mtime))
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000316
317 if not is_link:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000318 out['s'] = filestats.st_size
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000319 # If the timestamp wasn't updated and the file size is still the same, carry
320 # on the sha-1.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000321 if (prevdict.get('t') == out['t'] and
322 prevdict.get('s') == out['s']):
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000323 # Reuse the previous hash if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000324 out['h'] = prevdict.get('h')
325 if not out.get('h'):
maruel@chromium.org6da38772012-12-11 21:36:37 +0000326 out['h'] = isolateserver_archive.sha1_file(filepath)
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000327 else:
328 # If the timestamp wasn't updated, carry on the link destination.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000329 if prevdict.get('t') == out['t']:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000330 # Reuse the previous link destination if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000331 out['l'] = prevdict.get('l')
332 if out.get('l') is None:
333 out['l'] = os.readlink(filepath) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000334 return out
335
336
337### Variable stuff.
338
339
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000340def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000341 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000342 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000343
344
345def determine_root_dir(relative_root, infiles):
346 """For a list of infiles, determines the deepest root directory that is
347 referenced indirectly.
348
349 All arguments must be using os.path.sep.
350 """
351 # The trick used to determine the root directory is to look at "how far" back
352 # up it is looking up.
353 deepest_root = relative_root
354 for i in infiles:
355 x = relative_root
356 while i.startswith('..' + os.path.sep):
357 i = i[3:]
358 assert not i.startswith(os.path.sep)
359 x = os.path.dirname(x)
360 if deepest_root.startswith(x):
361 deepest_root = x
362 logging.debug(
363 'determine_root_dir(%s, %d files) -> %s' % (
364 relative_root, len(infiles), deepest_root))
365 return deepest_root
366
367
368def replace_variable(part, variables):
369 m = re.match(r'<\(([A-Z_]+)\)', part)
370 if m:
371 if m.group(1) not in variables:
372 raise ExecutionError(
373 'Variable "%s" was not found in %s.\nDid you forget to specify '
374 '--variable?' % (m.group(1), variables))
375 return variables[m.group(1)]
376 return part
377
378
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000379def process_variables(cwd, variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000380 """Processes path variables as a special case and returns a copy of the dict.
381
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000382 For each 'path' variable: first normalizes it based on |cwd|, verifies it
383 exists then sets it as relative to relative_base_dir.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000384 """
385 variables = variables.copy()
386 for i in PATH_VARIABLES:
387 if i not in variables:
388 continue
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000389 # Variables could contain / or \ on windows. Always normalize to
390 # os.path.sep.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000391 variable = variables[i].replace('/', os.path.sep)
392 if os.path.isabs(variable):
393 raise ExecutionError(
csharp@chromium.org837352f2013-01-17 21:17:03 +0000394 'Variable can\'t be absolute: %s: %s' % (i, variables[i]))
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000395
396 variable = os.path.join(cwd, variable)
397 variable = os.path.normpath(variable)
398 if not os.path.isdir(variable):
399 raise ExecutionError('%s=%s is not a directory' % (i, variable))
400
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000401 # All variables are relative to the .isolate file.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000402 variable = os.path.relpath(variable, relative_base_dir)
403 logging.debug(
404 'Translated variable %s from %s to %s', i, variables[i], variable)
405 variables[i] = variable
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000406 return variables
407
408
409def eval_variables(item, variables):
410 """Replaces the .isolate variables in a string item.
411
412 Note that the .isolate format is a subset of the .gyp dialect.
413 """
414 return ''.join(
415 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
416
417
418def classify_files(root_dir, tracked, untracked):
419 """Converts the list of files into a .isolate 'variables' dictionary.
420
421 Arguments:
422 - tracked: list of files names to generate a dictionary out of that should
423 probably be tracked.
424 - untracked: list of files names that must not be tracked.
425 """
426 # These directories are not guaranteed to be always present on every builder.
427 OPTIONAL_DIRECTORIES = (
428 'test/data/plugin',
429 'third_party/WebKit/LayoutTests',
430 )
431
432 new_tracked = []
433 new_untracked = list(untracked)
434
435 def should_be_tracked(filepath):
436 """Returns True if it is a file without whitespace in a non-optional
437 directory that has no symlink in its path.
438 """
439 if filepath.endswith('/'):
440 return False
441 if ' ' in filepath:
442 return False
443 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
444 return False
445 # Look if any element in the path is a symlink.
446 split = filepath.split('/')
447 for i in range(len(split)):
448 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
449 return False
450 return True
451
452 for filepath in sorted(tracked):
453 if should_be_tracked(filepath):
454 new_tracked.append(filepath)
455 else:
456 # Anything else.
457 new_untracked.append(filepath)
458
459 variables = {}
460 if new_tracked:
461 variables[KEY_TRACKED] = sorted(new_tracked)
462 if new_untracked:
463 variables[KEY_UNTRACKED] = sorted(new_untracked)
464 return variables
465
466
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000467def chromium_fix(f, variables):
468 """Fixes an isolate dependnecy with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000469 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
470 # separator.
471 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000472 # Ignored items.
473 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000474 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000475 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000476 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000477 # 'First Run' is not created by the compile, but by the test itself.
478 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000479
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000480 # Blacklist logs and other unimportant files.
481 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
482 logging.debug('Ignoring %s', f)
483 return None
484
maruel@chromium.org7650e422012-11-16 21:56:42 +0000485 EXECUTABLE = re.compile(
486 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
487 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
488 r'$')
489 match = EXECUTABLE.match(f)
490 if match:
491 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
492
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000493 if sys.platform == 'darwin':
494 # On OSX, the name of the output is dependent on gyp define, it can be
495 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
496 # Framework.framework'. Furthermore, they are versioned with a gyp
497 # variable. To lower the complexity of the .isolate file, remove all the
498 # individual entries that show up under any of the 4 entries and replace
499 # them with the directory itself. Overall, this results in a bit more
500 # files than strictly necessary.
501 OSX_BUNDLES = (
502 '<(PRODUCT_DIR)/Chromium Framework.framework/',
503 '<(PRODUCT_DIR)/Chromium.app/',
504 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
505 '<(PRODUCT_DIR)/Google Chrome.app/',
506 )
507 for prefix in OSX_BUNDLES:
508 if f.startswith(prefix):
509 # Note this result in duplicate values, so the a set() must be used to
510 # remove duplicates.
511 return prefix
512 return f
513
514
515def generate_simplified(
516 tracked, untracked, touched, root_dir, variables, relative_cwd):
517 """Generates a clean and complete .isolate 'variables' dictionary.
518
519 Cleans up and extracts only files from within root_dir then processes
520 variables and relative_cwd.
521 """
522 root_dir = os.path.realpath(root_dir)
523 logging.info(
524 'generate_simplified(%d files, %s, %s, %s)' %
525 (len(tracked) + len(untracked) + len(touched),
526 root_dir, variables, relative_cwd))
527
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000528 # Preparation work.
529 relative_cwd = cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000530 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000531 # Creates the right set of variables here. We only care about PATH_VARIABLES.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000532 path_variables = dict(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000533 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
534 for k in PATH_VARIABLES if k in variables)
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000535 variables = variables.copy()
536 variables.update(path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000537
538 # Actual work: Process the files.
539 # TODO(maruel): if all the files in a directory are in part tracked and in
540 # part untracked, the directory will not be extracted. Tracked files should be
541 # 'promoted' to be untracked as needed.
542 tracked = trace_inputs.extract_directories(
543 root_dir, tracked, default_blacklist)
544 untracked = trace_inputs.extract_directories(
545 root_dir, untracked, default_blacklist)
546 # touched is not compressed, otherwise it would result in files to be archived
547 # that we don't need.
548
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000549 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000550 def fix(f):
551 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000552 # Important, GYP stores the files with / and not \.
553 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000554 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000555 # If it's not already a variable.
556 if not f.startswith('<'):
557 # relative_cwd is usually the directory containing the gyp file. It may be
558 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000559 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000560 # Convert the whole thing to / since it's isolate's speak.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000561 f = posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000562 posixpath.join(root_dir_posix, f),
563 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000564
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000565 for variable, root_path in path_variables.iteritems():
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000566 if f.startswith(root_path):
567 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000568 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000569 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000570 return f
571
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000572 def fix_all(items):
573 """Reduces the items to convert variables, removes unneeded items, apply
574 chromium-specific fixes and only return unique items.
575 """
576 variables_converted = (fix(f.path) for f in items)
577 chromium_fixed = (chromium_fix(f, variables) for f in variables_converted)
578 return set(f for f in chromium_fixed if f)
579
580 tracked = fix_all(tracked)
581 untracked = fix_all(untracked)
582 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000583 out = classify_files(root_dir, tracked, untracked)
584 if touched:
585 out[KEY_TOUCHED] = sorted(touched)
586 return out
587
588
589def generate_isolate(
590 tracked, untracked, touched, root_dir, variables, relative_cwd):
591 """Generates a clean and complete .isolate file."""
592 result = generate_simplified(
593 tracked, untracked, touched, root_dir, variables, relative_cwd)
594 return {
595 'conditions': [
596 ['OS=="%s"' % get_flavor(), {
597 'variables': result,
598 }],
599 ],
600 }
601
602
603def split_touched(files):
604 """Splits files that are touched vs files that are read."""
605 tracked = []
606 touched = []
607 for f in files:
608 if f.size:
609 tracked.append(f)
610 else:
611 touched.append(f)
612 return tracked, touched
613
614
615def pretty_print(variables, stdout):
616 """Outputs a gyp compatible list from the decoded variables.
617
618 Similar to pprint.print() but with NIH syndrome.
619 """
620 # Order the dictionary keys by these keys in priority.
621 ORDER = (
622 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
623 KEY_TRACKED, KEY_UNTRACKED)
624
625 def sorting_key(x):
626 """Gives priority to 'most important' keys before the others."""
627 if x in ORDER:
628 return str(ORDER.index(x))
629 return x
630
631 def loop_list(indent, items):
632 for item in items:
633 if isinstance(item, basestring):
634 stdout.write('%s\'%s\',\n' % (indent, item))
635 elif isinstance(item, dict):
636 stdout.write('%s{\n' % indent)
637 loop_dict(indent + ' ', item)
638 stdout.write('%s},\n' % indent)
639 elif isinstance(item, list):
640 # A list inside a list will write the first item embedded.
641 stdout.write('%s[' % indent)
642 for index, i in enumerate(item):
643 if isinstance(i, basestring):
644 stdout.write(
645 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
646 elif isinstance(i, dict):
647 stdout.write('{\n')
648 loop_dict(indent + ' ', i)
649 if index != len(item) - 1:
650 x = ', '
651 else:
652 x = ''
653 stdout.write('%s}%s' % (indent, x))
654 else:
655 assert False
656 stdout.write('],\n')
657 else:
658 assert False
659
660 def loop_dict(indent, items):
661 for key in sorted(items, key=sorting_key):
662 item = items[key]
663 stdout.write("%s'%s': " % (indent, key))
664 if isinstance(item, dict):
665 stdout.write('{\n')
666 loop_dict(indent + ' ', item)
667 stdout.write(indent + '},\n')
668 elif isinstance(item, list):
669 stdout.write('[\n')
670 loop_list(indent + ' ', item)
671 stdout.write(indent + '],\n')
672 elif isinstance(item, basestring):
673 stdout.write(
674 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
675 elif item in (True, False, None):
676 stdout.write('%s\n' % item)
677 else:
678 assert False, item
679
680 stdout.write('{\n')
681 loop_dict(' ', variables)
682 stdout.write('}\n')
683
684
685def union(lhs, rhs):
686 """Merges two compatible datastructures composed of dict/list/set."""
687 assert lhs is not None or rhs is not None
688 if lhs is None:
689 return copy.deepcopy(rhs)
690 if rhs is None:
691 return copy.deepcopy(lhs)
692 assert type(lhs) == type(rhs), (lhs, rhs)
693 if hasattr(lhs, 'union'):
694 # Includes set, OSSettings and Configs.
695 return lhs.union(rhs)
696 if isinstance(lhs, dict):
697 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
698 elif isinstance(lhs, list):
699 # Do not go inside the list.
700 return lhs + rhs
701 assert False, type(lhs)
702
703
704def extract_comment(content):
705 """Extracts file level comment."""
706 out = []
707 for line in content.splitlines(True):
708 if line.startswith('#'):
709 out.append(line)
710 else:
711 break
712 return ''.join(out)
713
714
715def eval_content(content):
716 """Evaluates a python file and return the value defined in it.
717
718 Used in practice for .isolate files.
719 """
720 globs = {'__builtins__': None}
721 locs = {}
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000722 try:
723 value = eval(content, globs, locs)
724 except TypeError as e:
725 e.args = list(e.args) + [content]
726 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000727 assert locs == {}, locs
728 assert globs == {'__builtins__': None}, globs
729 return value
730
731
732def verify_variables(variables):
733 """Verifies the |variables| dictionary is in the expected format."""
734 VALID_VARIABLES = [
735 KEY_TOUCHED,
736 KEY_TRACKED,
737 KEY_UNTRACKED,
738 'command',
739 'read_only',
740 ]
741 assert isinstance(variables, dict), variables
742 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
743 for name, value in variables.iteritems():
744 if name == 'read_only':
745 assert value in (True, False, None), value
746 else:
747 assert isinstance(value, list), value
748 assert all(isinstance(i, basestring) for i in value), value
749
750
751def verify_condition(condition):
752 """Verifies the |condition| dictionary is in the expected format."""
753 VALID_INSIDE_CONDITION = ['variables']
754 assert isinstance(condition, list), condition
755 assert 2 <= len(condition) <= 3, condition
756 assert re.match(r'OS==\"([a-z]+)\"', condition[0]), condition[0]
757 for c in condition[1:]:
758 assert isinstance(c, dict), c
759 assert set(VALID_INSIDE_CONDITION).issuperset(set(c)), c.keys()
760 verify_variables(c.get('variables', {}))
761
762
763def verify_root(value):
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000764 VALID_ROOTS = ['includes', 'variables', 'conditions']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000765 assert isinstance(value, dict), value
766 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
767 verify_variables(value.get('variables', {}))
768
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000769 includes = value.get('includes', [])
770 assert isinstance(includes, list), includes
771 for include in includes:
772 assert isinstance(include, basestring), include
773
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000774 conditions = value.get('conditions', [])
775 assert isinstance(conditions, list), conditions
776 for condition in conditions:
777 verify_condition(condition)
778
779
780def remove_weak_dependencies(values, key, item, item_oses):
781 """Remove any oses from this key if the item is already under a strong key."""
782 if key == KEY_TOUCHED:
783 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
784 oses = values.get(stronger_key, {}).get(item, None)
785 if oses:
786 item_oses -= oses
787
788 return item_oses
789
790
csharp@chromium.org31176252012-11-02 13:04:40 +0000791def remove_repeated_dependencies(folders, key, item, item_oses):
792 """Remove any OSes from this key if the item is in a folder that is already
793 included."""
794
795 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
796 for (folder, oses) in folders.iteritems():
797 if folder != item and item.startswith(folder):
798 item_oses -= oses
799
800 return item_oses
801
802
803def get_folders(values_dict):
804 """Return a dict of all the folders in the given value_dict."""
805 return dict((item, oses) for (item, oses) in values_dict.iteritems()
806 if item.endswith('/'))
807
808
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000809def invert_map(variables):
810 """Converts a dict(OS, dict(deptype, list(dependencies)) to a flattened view.
811
812 Returns a tuple of:
813 1. dict(deptype, dict(dependency, set(OSes)) for easier processing.
814 2. All the OSes found as a set.
815 """
816 KEYS = (
817 KEY_TOUCHED,
818 KEY_TRACKED,
819 KEY_UNTRACKED,
820 'command',
821 'read_only',
822 )
823 out = dict((key, {}) for key in KEYS)
824 for os_name, values in variables.iteritems():
825 for key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED):
826 for item in values.get(key, []):
827 out[key].setdefault(item, set()).add(os_name)
828
829 # command needs special handling.
830 command = tuple(values.get('command', []))
831 out['command'].setdefault(command, set()).add(os_name)
832
833 # read_only needs special handling.
834 out['read_only'].setdefault(values.get('read_only'), set()).add(os_name)
835 return out, set(variables)
836
837
838def reduce_inputs(values, oses):
839 """Reduces the invert_map() output to the strictest minimum list.
840
841 1. Construct the inverse map first.
842 2. Look at each individual file and directory, map where they are used and
843 reconstruct the inverse dictionary.
844 3. Do not convert back to negative if only 2 OSes were merged.
845
846 Returns a tuple of:
847 1. the minimized dictionary
848 2. oses passed through as-is.
849 """
850 KEYS = (
851 KEY_TOUCHED,
852 KEY_TRACKED,
853 KEY_UNTRACKED,
854 'command',
855 'read_only',
856 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000857
858 # Folders can only live in KEY_UNTRACKED.
859 folders = get_folders(values.get(KEY_UNTRACKED, {}))
860
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000861 out = dict((key, {}) for key in KEYS)
862 assert all(oses), oses
863 if len(oses) > 2:
864 for key in KEYS:
865 for item, item_oses in values.get(key, {}).iteritems():
866 item_oses = remove_weak_dependencies(values, key, item, item_oses)
csharp@chromium.org31176252012-11-02 13:04:40 +0000867 item_oses = remove_repeated_dependencies(folders, key, item, item_oses)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000868 if not item_oses:
869 continue
870
871 # Converts all oses.difference('foo') to '!foo'.
872 assert all(item_oses), item_oses
873 missing = oses.difference(item_oses)
874 if len(missing) == 1:
875 # Replace it with a negative.
876 out[key][item] = set(['!' + tuple(missing)[0]])
877 elif not missing:
878 out[key][item] = set([None])
879 else:
880 out[key][item] = set(item_oses)
881 else:
882 for key in KEYS:
883 for item, item_oses in values.get(key, {}).iteritems():
884 item_oses = remove_weak_dependencies(values, key, item, item_oses)
885 if not item_oses:
886 continue
887
888 # Converts all oses.difference('foo') to '!foo'.
889 assert None not in item_oses, item_oses
890 out[key][item] = set(item_oses)
891 return out, oses
892
893
894def convert_map_to_isolate_dict(values, oses):
895 """Regenerates back a .isolate configuration dict from files and dirs
896 mappings generated from reduce_inputs().
897 """
898 # First, inverse the mapping to make it dict first.
899 config = {}
900 for key in values:
901 for item, oses in values[key].iteritems():
902 if item is None:
903 # For read_only default.
904 continue
905 for cond_os in oses:
906 cond_key = None if cond_os is None else cond_os.lstrip('!')
907 # Insert the if/else dicts.
908 condition_values = config.setdefault(cond_key, [{}, {}])
909 # If condition is negative, use index 1, else use index 0.
910 cond_value = condition_values[int((cond_os or '').startswith('!'))]
911 variables = cond_value.setdefault('variables', {})
912
913 if item in (True, False):
914 # One-off for read_only.
915 variables[key] = item
916 else:
917 if isinstance(item, tuple) and item:
918 # One-off for command.
919 # Do not merge lists and do not sort!
920 # Note that item is a tuple.
921 assert key not in variables
922 variables[key] = list(item)
923 elif item:
924 # The list of items (files or dirs). Append the new item and keep
925 # the list sorted.
926 l = variables.setdefault(key, [])
927 l.append(item)
928 l.sort()
929
930 out = {}
931 for o in sorted(config):
932 d = config[o]
933 if o is None:
934 assert not d[1]
935 out = union(out, d[0])
936 else:
937 c = out.setdefault('conditions', [])
938 if d[1]:
939 c.append(['OS=="%s"' % o] + d)
940 else:
941 c.append(['OS=="%s"' % o] + d[0:1])
942 return out
943
944
945### Internal state files.
946
947
948class OSSettings(object):
949 """Represents the dependencies for an OS. The structure is immutable.
950
951 It's the .isolate settings for a specific file.
952 """
953 def __init__(self, name, values):
954 self.name = name
955 verify_variables(values)
956 self.touched = sorted(values.get(KEY_TOUCHED, []))
957 self.tracked = sorted(values.get(KEY_TRACKED, []))
958 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
959 self.command = values.get('command', [])[:]
960 self.read_only = values.get('read_only')
961
962 def union(self, rhs):
963 assert self.name == rhs.name
maruel@chromium.org669edcb2012-11-02 19:16:14 +0000964 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000965 var = {
966 KEY_TOUCHED: sorted(self.touched + rhs.touched),
967 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
968 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
969 'command': self.command or rhs.command,
970 'read_only': rhs.read_only if self.read_only is None else self.read_only,
971 }
972 return OSSettings(self.name, var)
973
974 def flatten(self):
975 out = {}
976 if self.command:
977 out['command'] = self.command
978 if self.touched:
979 out[KEY_TOUCHED] = self.touched
980 if self.tracked:
981 out[KEY_TRACKED] = self.tracked
982 if self.untracked:
983 out[KEY_UNTRACKED] = self.untracked
984 if self.read_only is not None:
985 out['read_only'] = self.read_only
986 return out
987
988
989class Configs(object):
990 """Represents a processed .isolate file.
991
992 Stores the file in a processed way, split by each the OS-specific
993 configurations.
994
995 The self.per_os[None] member contains all the 'else' clauses plus the default
996 values. It is not included in the flatten() result.
997 """
998 def __init__(self, oses, file_comment):
999 self.file_comment = file_comment
1000 self.per_os = {
1001 None: OSSettings(None, {}),
1002 }
1003 self.per_os.update(dict((name, OSSettings(name, {})) for name in oses))
1004
1005 def union(self, rhs):
1006 items = list(set(self.per_os.keys() + rhs.per_os.keys()))
1007 # Takes the first file comment, prefering lhs.
1008 out = Configs(items, self.file_comment or rhs.file_comment)
1009 for key in items:
1010 out.per_os[key] = union(self.per_os.get(key), rhs.per_os.get(key))
1011 return out
1012
1013 def add_globals(self, values):
1014 for key in self.per_os:
1015 self.per_os[key] = self.per_os[key].union(OSSettings(key, values))
1016
1017 def add_values(self, for_os, values):
1018 self.per_os[for_os] = self.per_os[for_os].union(OSSettings(for_os, values))
1019
1020 def add_negative_values(self, for_os, values):
1021 """Includes the variables to all OSes except |for_os|.
1022
1023 This includes 'None' so unknown OSes gets it too.
1024 """
1025 for key in self.per_os:
1026 if key != for_os:
1027 self.per_os[key] = self.per_os[key].union(OSSettings(key, values))
1028
1029 def flatten(self):
1030 """Returns a flat dictionary representation of the configuration.
1031
1032 Skips None pseudo-OS.
1033 """
1034 return dict(
1035 (k, v.flatten()) for k, v in self.per_os.iteritems() if k is not None)
1036
1037
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001038def load_isolate_as_config(isolate_dir, value, file_comment, default_oses):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001039 """Parses one .isolate file and returns a Configs() instance.
1040
1041 |value| is the loaded dictionary that was defined in the gyp file.
1042
1043 The expected format is strict, anything diverting from the format below will
1044 throw an assert:
1045 {
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001046 'includes': [
1047 'foo.isolate',
1048 ],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001049 'variables': {
1050 'command': [
1051 ...
1052 ],
1053 'isolate_dependency_tracked': [
1054 ...
1055 ],
1056 'isolate_dependency_untracked': [
1057 ...
1058 ],
1059 'read_only': False,
1060 },
1061 'conditions': [
1062 ['OS=="<os>"', {
1063 'variables': {
1064 ...
1065 },
1066 }, { # else
1067 'variables': {
1068 ...
1069 },
1070 }],
1071 ...
1072 ],
1073 }
1074 """
1075 verify_root(value)
1076
1077 # Scan to get the list of OSes.
1078 conditions = value.get('conditions', [])
1079 oses = set(re.match(r'OS==\"([a-z]+)\"', c[0]).group(1) for c in conditions)
1080 oses = oses.union(default_oses)
1081 configs = Configs(oses, file_comment)
1082
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001083 # Load the includes.
1084 for include in value.get('includes', []):
1085 if os.path.isabs(include):
1086 raise ExecutionError(
1087 'Failed to load configuration; absolute include path \'%s\'' %
1088 include)
1089 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
1090 with open(included_isolate, 'r') as f:
1091 included_config = load_isolate_as_config(
1092 os.path.dirname(included_isolate),
1093 eval_content(f.read()),
1094 None,
1095 default_oses)
1096 configs = union(configs, included_config)
1097
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001098 # Global level variables.
1099 configs.add_globals(value.get('variables', {}))
1100
1101 # OS specific variables.
1102 for condition in conditions:
1103 condition_os = re.match(r'OS==\"([a-z]+)\"', condition[0]).group(1)
1104 configs.add_values(condition_os, condition[1].get('variables', {}))
1105 if len(condition) > 2:
1106 configs.add_negative_values(
1107 condition_os, condition[2].get('variables', {}))
1108 return configs
1109
1110
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001111def load_isolate_for_flavor(isolate_dir, content, flavor):
1112 """Loads the .isolate file and returns the information unprocessed but
1113 filtered for the specific OS.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001114
1115 Returns the command, dependencies and read_only flag. The dependencies are
1116 fixed to use os.path.sep.
1117 """
1118 # Load the .isolate file, process its conditions, retrieve the command and
1119 # dependencies.
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001120 configs = load_isolate_as_config(
1121 isolate_dir, eval_content(content), None, DEFAULT_OSES)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001122 config = configs.per_os.get(flavor) or configs.per_os.get(None)
1123 if not config:
1124 raise ExecutionError('Failed to load configuration for \'%s\'' % flavor)
1125 # Merge tracked and untracked dependencies, isolate.py doesn't care about the
1126 # trackability of the dependencies, only the build tool does.
1127 dependencies = [
1128 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1129 ]
1130 touched = [f.replace('/', os.path.sep) for f in config.touched]
1131 return config.command, dependencies, touched, config.read_only
1132
1133
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001134def chromium_save_isolated(isolated, data, variables):
1135 """Writes one or many .isolated files.
1136
1137 This slightly increases the cold cache cost but greatly reduce the warm cache
1138 cost by splitting low-churn files off the master .isolated file. It also
1139 reduces overall isolateserver memcache consumption.
1140 """
1141 slaves = []
1142
1143 def extract_into_included_isolated(prefix):
1144 new_slave = {'files': {}, 'os': data['os']}
1145 for f in data['files'].keys():
1146 if f.startswith(prefix):
1147 new_slave['files'][f] = data['files'].pop(f)
1148 if new_slave['files']:
1149 slaves.append(new_slave)
1150
1151 # Split test/data/ in its own .isolated file.
1152 extract_into_included_isolated(os.path.join('test', 'data', ''))
1153
1154 # Split everything out of PRODUCT_DIR in its own .isolated file.
1155 if variables.get('PRODUCT_DIR'):
1156 extract_into_included_isolated(variables['PRODUCT_DIR'])
1157
1158 files = [isolated]
1159 for index, f in enumerate(slaves):
1160 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
1161 trace_inputs.write_json(slavepath, f, True)
1162 data.setdefault('includes', []).append(
1163 isolateserver_archive.sha1_file(slavepath))
1164 files.append(slavepath)
1165
1166 trace_inputs.write_json(isolated, data, True)
1167 return files
1168
1169
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001170class Flattenable(object):
1171 """Represents data that can be represented as a json file."""
1172 MEMBERS = ()
1173
1174 def flatten(self):
1175 """Returns a json-serializable version of itself.
1176
1177 Skips None entries.
1178 """
1179 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1180 return dict((member, value) for member, value in items if value is not None)
1181
1182 @classmethod
1183 def load(cls, data):
1184 """Loads a flattened version."""
1185 data = data.copy()
1186 out = cls()
1187 for member in out.MEMBERS:
1188 if member in data:
1189 # Access to a protected member XXX of a client class
1190 # pylint: disable=W0212
1191 out._load_member(member, data.pop(member))
1192 if data:
1193 raise ValueError(
1194 'Found unexpected entry %s while constructing an object %s' %
1195 (data, cls.__name__), data, cls.__name__)
1196 return out
1197
1198 def _load_member(self, member, value):
1199 """Loads a member into self."""
1200 setattr(self, member, value)
1201
1202 @classmethod
1203 def load_file(cls, filename):
1204 """Loads the data from a file or return an empty instance."""
1205 out = cls()
1206 try:
1207 out = cls.load(trace_inputs.read_json(filename))
1208 logging.debug('Loaded %s(%s)' % (cls.__name__, filename))
1209 except (IOError, ValueError):
1210 logging.warn('Failed to load %s' % filename)
1211 return out
1212
1213
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001214class SavedState(Flattenable):
1215 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001216
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001217 This file caches the items calculated by this script and is used to increase
1218 the performance of the script. This file is not loaded by run_isolated.py.
1219 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001220
1221 It is important to note that the 'files' dict keys are using native OS path
1222 separator instead of '/' used in .isolate file.
1223 """
1224 MEMBERS = (
1225 'command',
1226 'files',
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001227 'isolate_file',
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001228 'isolated_files',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001229 'os',
1230 'read_only',
1231 'relative_cwd',
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001232 'variables',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001233 )
1234
1235 os = get_flavor()
1236
1237 def __init__(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001238 super(SavedState, self).__init__()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001239 self.command = []
1240 self.files = {}
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001241 # Link back to the .isolate file.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001242 self.isolate_file = None
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001243 # Used to support/remember 'slave' .isolated files.
1244 self.isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001245 self.read_only = None
1246 self.relative_cwd = None
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001247 # Variables are saved so a user can use isolate.py after building and the
1248 # GYP variables are still defined.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001249 self.variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001250
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001251 def update(self, isolate_file, variables):
1252 """Updates the saved state with new data to keep GYP variables and internal
1253 reference to the original .isolate file.
1254 """
1255 self.isolate_file = isolate_file
1256 self.variables.update(variables)
1257
1258 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
1259 """Updates the saved state with data necessary to generate a .isolated file.
1260 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001261 self.command = command
1262 # Add new files.
1263 for f in infiles:
1264 self.files.setdefault(f, {})
1265 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001266 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001267 # Prune extraneous files that are not a dependency anymore.
1268 for f in set(self.files).difference(set(infiles).union(touched)):
1269 del self.files[f]
1270 if read_only is not None:
1271 self.read_only = read_only
1272 self.relative_cwd = relative_cwd
1273
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001274 def to_isolated(self):
1275 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001276
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001277 http://chromium.org/developers/testing/isolated-testing/design
1278 """
1279 def strip(data):
1280 """Returns a 'files' entry with only the whitelisted keys."""
1281 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
1282
1283 out = {
1284 'files': dict(
1285 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
1286 'os': self.os,
1287 }
1288 if self.command:
1289 out['command'] = self.command
1290 if self.read_only is not None:
1291 out['read_only'] = self.read_only
1292 if self.relative_cwd:
1293 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001294 return out
1295
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001296 @classmethod
1297 def load(cls, data):
1298 out = super(SavedState, cls).load(data)
1299 if out.isolate_file:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001300 out.isolate_file = trace_inputs.get_native_path_case(
1301 unicode(out.isolate_file))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001302 return out
1303
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001304 def _load_member(self, member, value):
1305 if member == 'os':
1306 if value != self.os:
1307 raise run_isolated.ConfigError(
1308 'The .isolated file was created on another platform')
1309 else:
1310 super(SavedState, self)._load_member(member, value)
1311
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001312 def __str__(self):
1313 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001314 out += ' command: %s\n' % self.command
1315 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001316 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001317 out += ' read_only: %s\n' % self.read_only
1318 out += ' relative_cwd: %s' % self.relative_cwd
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001319 out += ' isolated_files: %s' % self.isolated_files
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001320 out += ' variables: %s' % ''.join(
1321 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1322 out += ')'
1323 return out
1324
1325
1326class CompleteState(object):
1327 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001328 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001329 super(CompleteState, self).__init__()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001330 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001331 # Contains the data to ease developer's use-case but that is not strictly
1332 # necessary.
1333 self.saved_state = saved_state
1334
1335 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001336 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001337 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001338 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001339 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001340 isolated_filepath,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001341 SavedState.load_file(isolatedfile_to_state(isolated_filepath)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001342
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001343 def load_isolate(self, cwd, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001344 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001345 .isolate file.
1346
1347 Processes the loaded data, deduce root_dir, relative_cwd.
1348 """
1349 # Make sure to not depend on os.getcwd().
1350 assert os.path.isabs(isolate_file), isolate_file
1351 logging.info(
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001352 'CompleteState.load_isolate(%s, %s, %s, %s)',
1353 cwd, isolate_file, variables, ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001354 relative_base_dir = os.path.dirname(isolate_file)
1355
1356 # Processes the variables and update the saved state.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001357 variables = process_variables(cwd, variables, relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001358 self.saved_state.update(isolate_file, variables)
1359
1360 with open(isolate_file, 'r') as f:
1361 # At that point, variables are not replaced yet in command and infiles.
1362 # infiles may contain directory entries and is in posix style.
1363 command, infiles, touched, read_only = load_isolate_for_flavor(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001364 os.path.dirname(isolate_file), f.read(), get_flavor())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001365 command = [eval_variables(i, self.saved_state.variables) for i in command]
1366 infiles = [eval_variables(f, self.saved_state.variables) for f in infiles]
1367 touched = [eval_variables(f, self.saved_state.variables) for f in touched]
1368 # root_dir is automatically determined by the deepest root accessed with the
1369 # form '../../foo/bar'.
1370 root_dir = determine_root_dir(relative_base_dir, infiles + touched)
1371 # The relative directory is automatically determined by the relative path
1372 # between root_dir and the directory containing the .isolate file,
1373 # isolate_base_dir.
1374 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
1375 # Normalize the files based to root_dir. It is important to keep the
1376 # trailing os.path.sep at that step.
1377 infiles = [
1378 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1379 for f in infiles
1380 ]
1381 touched = [
1382 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1383 for f in touched
1384 ]
1385 # Expand the directories by listing each file inside. Up to now, trailing
1386 # os.path.sep must be kept. Do not expand 'touched'.
1387 infiles = expand_directories_and_symlinks(
1388 root_dir,
1389 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001390 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
1391 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001392
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001393 # Finally, update the new data to be able to generate the foo.isolated file,
1394 # the file that is used by run_isolated.py.
1395 self.saved_state.update_isolated(
1396 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001397 logging.debug(self)
1398
maruel@chromium.org9268f042012-10-17 17:36:41 +00001399 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001400 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001401
maruel@chromium.org9268f042012-10-17 17:36:41 +00001402 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1403 file is tainted.
1404
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001405 See process_input() for more information.
1406 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001407 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001408 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001409 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001410 else:
1411 filepath = os.path.join(self.root_dir, infile)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001412 self.saved_state.files[infile] = process_input(
1413 filepath,
1414 self.saved_state.files[infile],
1415 self.saved_state.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001416
1417 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001418 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001419 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001420 self.saved_state.isolated_files = chromium_save_isolated(
1421 self.isolated_filepath,
1422 self.saved_state.to_isolated(),
1423 self.saved_state.variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001424 total_bytes = sum(
1425 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001426 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001427 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001428 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001429 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001430 logging.debug('Dumping to %s' % saved_state_file)
1431 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1432
1433 @property
1434 def root_dir(self):
1435 """isolate_file is always inside relative_cwd relative to root_dir."""
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001436 if not self.saved_state.isolate_file:
1437 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001438 isolate_dir = os.path.dirname(self.saved_state.isolate_file)
1439 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001440 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001441 return isolate_dir
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001442 assert isolate_dir.endswith(self.saved_state.relative_cwd), (
1443 isolate_dir, self.saved_state.relative_cwd)
1444 return isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001445
1446 @property
1447 def resultdir(self):
1448 """Directory containing the results, usually equivalent to the variable
1449 PRODUCT_DIR.
1450 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001451 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001452
1453 def __str__(self):
1454 def indent(data, indent_length):
1455 """Indents text."""
1456 spacing = ' ' * indent_length
1457 return ''.join(spacing + l for l in str(data).splitlines(True))
1458
1459 out = '%s(\n' % self.__class__.__name__
1460 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001461 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1462 return out
1463
1464
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001465def load_complete_state(options, cwd, subdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001466 """Loads a CompleteState.
1467
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001468 This includes data from .isolate and .isolated.state files. Never reads the
1469 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001470
1471 Arguments:
1472 options: Options instance generated with OptionParserIsolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001473 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001474 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001475 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001476 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001477 else:
1478 # Constructs a dummy object that cannot be saved. Useful for temporary
1479 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001480 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001481 options.isolate = options.isolate or complete_state.saved_state.isolate_file
1482 if not options.isolate:
1483 raise ExecutionError('A .isolate file is required.')
1484 if (complete_state.saved_state.isolate_file and
1485 options.isolate != complete_state.saved_state.isolate_file):
1486 raise ExecutionError(
1487 '%s and %s do not match.' % (
1488 options.isolate, complete_state.saved_state.isolate_file))
1489
1490 # Then load the .isolate and expands directories.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001491 complete_state.load_isolate(
1492 cwd, options.isolate, options.variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001493
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001494 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001495 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001496 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001497 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1498 subdir = subdir.replace('/', os.path.sep)
1499 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001500 return complete_state
1501
1502
1503def read_trace_as_isolate_dict(complete_state):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001504 """Reads a trace and returns the .isolate dictionary.
1505
1506 Returns exceptions during the log parsing so it can be re-raised.
1507 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001508 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001509 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 if not os.path.isfile(logfile):
1511 raise ExecutionError(
1512 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1513 try:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00001514 data = api.parse_log(logfile, default_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001515 exceptions = [i['exception'] for i in data if 'exception' in i]
1516 results = (i['results'] for i in data if 'results' in i)
1517 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1518 files = set(sum((result.existent for result in results_stripped), []))
1519 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001520 value = generate_isolate(
1521 tracked,
1522 [],
1523 touched,
1524 complete_state.root_dir,
1525 complete_state.saved_state.variables,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001526 complete_state.saved_state.relative_cwd)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001527 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001528 except trace_inputs.TracingFailure, e:
1529 raise ExecutionError(
1530 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001531 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001532
1533
1534def print_all(comment, data, stream):
1535 """Prints a complete .isolate file and its top-level file comment into a
1536 stream.
1537 """
1538 if comment:
1539 stream.write(comment)
1540 pretty_print(data, stream)
1541
1542
1543def merge(complete_state):
1544 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001545 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001546
1547 # Now take that data and union it into the original .isolate file.
1548 with open(complete_state.saved_state.isolate_file, 'r') as f:
1549 prev_content = f.read()
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001550 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001551 prev_config = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001552 isolate_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001553 eval_content(prev_content),
1554 extract_comment(prev_content),
1555 DEFAULT_OSES)
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001556 new_config = load_isolate_as_config(isolate_dir, value, '', DEFAULT_OSES)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001557 config = union(prev_config, new_config)
1558 # pylint: disable=E1103
1559 data = convert_map_to_isolate_dict(
1560 *reduce_inputs(*invert_map(config.flatten())))
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001561 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001562 with open(complete_state.saved_state.isolate_file, 'wb') as f:
1563 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001564 if exceptions:
1565 # It got an exception, raise the first one.
1566 raise \
1567 exceptions[0][0], \
1568 exceptions[0][1], \
1569 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001570
1571
1572def CMDcheck(args):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001573 """Checks that all the inputs are present and generates .isolated."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001574 parser = OptionParserIsolate(command='check')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001575 parser.add_option('--subdir', help='Filters to a subdirectory')
1576 options, args = parser.parse_args(args)
1577 if args:
1578 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001579 complete_state = load_complete_state(options, os.getcwd(), options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001580
1581 # Nothing is done specifically. Just store the result and state.
1582 complete_state.save_files()
1583 return 0
1584
1585
1586def CMDhashtable(args):
1587 """Creates a hash table content addressed object store.
1588
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001589 All the files listed in the .isolated file are put in the output directory
1590 with the file name being the sha-1 of the file's content.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001591 """
1592 parser = OptionParserIsolate(command='hashtable')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001593 parser.add_option('--subdir', help='Filters to a subdirectory')
1594 options, args = parser.parse_args(args)
1595 if args:
1596 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001597
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001598 with run_isolated.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001599 success = False
1600 try:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001601 complete_state = load_complete_state(options, os.getcwd(), options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001602 options.outdir = (
1603 options.outdir or os.path.join(complete_state.resultdir, 'hashtable'))
1604 # Make sure that complete_state isn't modified until save_files() is
1605 # called, because any changes made to it here will propagate to the files
1606 # created (which is probably not intended).
1607 complete_state.save_files()
1608
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001609 infiles = complete_state.saved_state.files
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001610 # Add all the .isolated files.
1611 for item in complete_state.saved_state.isolated_files:
1612 item_path = os.path.join(
1613 os.path.dirname(complete_state.isolated_filepath), item)
1614 with open(item_path, 'rb') as f:
1615 content = f.read()
1616 isolated_metadata = {
1617 'h': hashlib.sha1(content).hexdigest(),
1618 's': len(content),
1619 'priority': '0'
1620 }
1621 infiles[item_path] = isolated_metadata
1622
1623 logging.info('Creating content addressed object store with %d item',
1624 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001625
1626 if re.match(r'^https?://.+$', options.outdir):
maruel@chromium.orgc6f90062012-11-07 18:32:22 +00001627 isolateserver_archive.upload_sha1_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001628 base_url=options.outdir,
1629 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00001630 infiles=infiles,
1631 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001632 else:
1633 recreate_tree(
1634 outdir=options.outdir,
1635 indir=complete_state.root_dir,
1636 infiles=infiles,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001637 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001638 as_sha1=True)
1639 success = True
1640 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001641 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001642 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001643 if not success and os.path.isfile(options.isolated):
1644 os.remove(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001645
1646
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001647def CMDmerge(args):
1648 """Reads and merges the data from the trace back into the original .isolate.
1649
1650 Ignores --outdir.
1651 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001652 parser = OptionParserIsolate(command='merge', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001653 options, args = parser.parse_args(args)
1654 if args:
1655 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001656 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001657 merge(complete_state)
1658 return 0
1659
1660
1661def CMDread(args):
1662 """Reads the trace file generated with command 'trace'.
1663
1664 Ignores --outdir.
1665 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001666 parser = OptionParserIsolate(command='read', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001667 options, args = parser.parse_args(args)
1668 if args:
1669 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001670 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001671 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001672 pretty_print(value, sys.stdout)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001673 if exceptions:
1674 # It got an exception, raise the first one.
1675 raise \
1676 exceptions[0][0], \
1677 exceptions[0][1], \
1678 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001679 return 0
1680
1681
1682def CMDremap(args):
1683 """Creates a directory with all the dependencies mapped into it.
1684
1685 Useful to test manually why a test is failing. The target executable is not
1686 run.
1687 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001688 parser = OptionParserIsolate(command='remap', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001689 options, args = parser.parse_args(args)
1690 if args:
1691 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001692 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001693
1694 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001695 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001696 'isolate', complete_state.root_dir)
1697 else:
1698 if not os.path.isdir(options.outdir):
1699 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001700 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001701 if len(os.listdir(options.outdir)):
1702 raise ExecutionError('Can\'t remap in a non-empty directory')
1703 recreate_tree(
1704 outdir=options.outdir,
1705 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001706 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001707 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001708 as_sha1=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001709 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001710 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001711
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001712 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001713 complete_state.save_files()
1714 return 0
1715
1716
1717def CMDrun(args):
1718 """Runs the test executable in an isolated (temporary) directory.
1719
1720 All the dependencies are mapped into the temporary directory and the
1721 directory is cleaned up after the target exits. Warning: if -outdir is
1722 specified, it is deleted upon exit.
1723
1724 Argument processing stops at the first non-recognized argument and these
1725 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001726 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001727 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001728 parser = OptionParserIsolate(command='run', require_isolated=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001729 parser.enable_interspersed_args()
1730 options, args = parser.parse_args(args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001731 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001732 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001733 if not cmd:
1734 raise ExecutionError('No command to run')
1735 cmd = trace_inputs.fix_python_path(cmd)
1736 try:
1737 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001738 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001739 'isolate', complete_state.root_dir)
1740 else:
1741 if not os.path.isdir(options.outdir):
1742 os.makedirs(options.outdir)
1743 recreate_tree(
1744 outdir=options.outdir,
1745 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001746 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001747 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001748 as_sha1=False)
1749 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001750 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001751 if not os.path.isdir(cwd):
1752 # It can happen when no files are mapped from the directory containing the
1753 # .isolate file. But the directory must exist to be the current working
1754 # directory.
1755 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001756 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001757 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001758 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1759 result = subprocess.call(cmd, cwd=cwd)
1760 finally:
1761 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001762 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001763
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001764 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001765 complete_state.save_files()
1766 return result
1767
1768
1769def CMDtrace(args):
1770 """Traces the target using trace_inputs.py.
1771
1772 It runs the executable without remapping it, and traces all the files it and
1773 its child processes access. Then the 'read' command can be used to generate an
1774 updated .isolate file out of it.
1775
1776 Argument processing stops at the first non-recognized argument and these
1777 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001778 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001779 """
1780 parser = OptionParserIsolate(command='trace')
1781 parser.enable_interspersed_args()
1782 parser.add_option(
1783 '-m', '--merge', action='store_true',
1784 help='After tracing, merge the results back in the .isolate file')
1785 options, args = parser.parse_args(args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001786 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001787 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001788 if not cmd:
1789 raise ExecutionError('No command to run')
1790 cmd = trace_inputs.fix_python_path(cmd)
1791 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001792 unicode(complete_state.root_dir),
1793 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001794 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1795 if not os.path.isfile(cmd[0]):
1796 raise ExecutionError(
1797 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001798 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1799 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001800 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001801 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001802 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001803 try:
1804 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001805 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001806 cmd,
1807 cwd,
1808 'default',
1809 True)
1810 except trace_inputs.TracingFailure, e:
1811 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1812
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001813 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001814 logging.error(
1815 'Tracer exited with %d, which means the tests probably failed so the '
1816 'trace is probably incomplete.', result)
1817 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001818
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001819 complete_state.save_files()
1820
1821 if options.merge:
1822 merge(complete_state)
1823
1824 return result
1825
1826
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001827def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001828 """Adds --isolated and --variable to an OptionParser."""
1829 parser.add_option(
1830 '-s', '--isolated',
1831 metavar='FILE',
1832 help='.isolated file to generate or read')
1833 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001834 parser.add_option(
1835 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001836 dest='isolated',
1837 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001838 default_variables = [('OS', get_flavor())]
1839 if sys.platform in ('win32', 'cygwin'):
1840 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
1841 else:
1842 default_variables.append(('EXECUTABLE_SUFFIX', ''))
1843 parser.add_option(
1844 '-V', '--variable',
1845 nargs=2,
1846 action='append',
1847 default=default_variables,
1848 dest='variables',
1849 metavar='FOO BAR',
1850 help='Variables to process in the .isolate file, default: %default. '
1851 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001852 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001853
1854
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001855def parse_variable_option(parser, options, cwd, require_isolated):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001856 """Processes --isolated and --variable."""
1857 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001858 options.isolated = os.path.normpath(
1859 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001860 if require_isolated and not options.isolated:
csharp@chromium.org707f0452012-11-26 21:50:40 +00001861 parser.error('--isolated is required. Visit http://chromium.org/developers/'
1862 'testing/isolated-testing#TOC-Where-can-I-find-the-.isolated-'
1863 'file- to see how to create the .isolated file.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001864 if options.isolated and not options.isolated.endswith('.isolated'):
1865 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001866 options.variables = dict(options.variables)
1867
1868
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001869class OptionParserIsolate(trace_inputs.OptionParserWithNiceDescription):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001870 """Adds automatic --isolate, --isolated, --out and --variable handling."""
1871 def __init__(self, require_isolated=True, **kwargs):
maruel@chromium.org55276902012-10-05 20:56:19 +00001872 trace_inputs.OptionParserWithNiceDescription.__init__(
1873 self,
1874 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1875 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001876 group = optparse.OptionGroup(self, "Common options")
1877 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001878 '-i', '--isolate',
1879 metavar='FILE',
1880 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001881 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001882 group.add_option(
1883 '-o', '--outdir', metavar='DIR',
1884 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00001885 'Defaults: run|remap: a /tmp subdirectory, others: '
1886 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00001887 group.add_option(
1888 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00001889 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1890 help='Indicates that invalid entries in the isolated file to be '
1891 'only be logged and not stop processing. Defaults to True if '
1892 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001893 self.add_option_group(group)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001894 self.require_isolated = require_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001895
1896 def parse_args(self, *args, **kwargs):
1897 """Makes sure the paths make sense.
1898
1899 On Windows, / and \ are often mixed together in a path.
1900 """
1901 options, args = trace_inputs.OptionParserWithNiceDescription.parse_args(
1902 self, *args, **kwargs)
1903 if not self.allow_interspersed_args and args:
1904 self.error('Unsupported argument: %s' % args)
1905
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001906 cwd = os.getcwd()
1907 parse_variable_option(self, options, cwd, self.require_isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001908
1909 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001910 # TODO(maruel): Work with non-ASCII.
1911 # The path must be in native path case for tracing purposes.
1912 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1913 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
1914 options.isolate = trace_inputs.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001915
1916 if options.outdir and not re.match(r'^https?://.+$', options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001917 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1918 # outdir doesn't need native path case since tracing is never done from
1919 # there.
1920 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001921
1922 return options, args
1923
1924
1925### Glue code to make all the commands works magically.
1926
1927
1928CMDhelp = trace_inputs.CMDhelp
1929
1930
1931def main(argv):
1932 try:
1933 return trace_inputs.main_impl(argv)
1934 except (
1935 ExecutionError,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001936 run_isolated.MappingError,
1937 run_isolated.ConfigError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001938 sys.stderr.write('\nError: ')
1939 sys.stderr.write(str(e))
1940 sys.stderr.write('\n')
1941 return 1
1942
1943
1944if __name__ == '__main__':
1945 sys.exit(main(sys.argv[1:]))