blob: 966d102a6b9fc746b68ee02b7b97ce9edeab0a79 [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.org61a9b3b2012-12-12 17:18:52 +0000212 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000213 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000214 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000215 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000216
217 for relfile, metadata in infiles.iteritems():
218 infile = os.path.join(indir, relfile)
219 if as_sha1:
220 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000221 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000222 # Skip links when storing a hashtable.
223 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000224 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000225 if os.path.isfile(outfile):
226 # Just do a quick check that the file size matches. No need to stat()
227 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000228 if not 's' in metadata:
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000229 raise run_isolated.MappingError(
230 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000231 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000232 continue
233 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000234 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000235 os.remove(outfile)
236 else:
237 outfile = os.path.join(outdir, relfile)
238 outsubdir = os.path.dirname(outfile)
239 if not os.path.isdir(outsubdir):
240 os.makedirs(outsubdir)
241
242 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000243 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000244 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000245 if 'l' in metadata:
246 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000247 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000248 # symlink doesn't exist on Windows.
249 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000250 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000251 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000252
253
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000254def process_input(filepath, prevdict, read_only):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000255 """Processes an input file, a dependency, and return meta data about it.
256
257 Arguments:
258 - filepath: File to act on.
259 - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
260 to skip recalculating the hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000261 - read_only: If True, the file mode is manipulated. In practice, only save
262 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
263 windows, mode is not set since all files are 'executable' by
264 default.
265
266 Behaviors:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000267 - Retrieves the file mode, file size, file timestamp, file link
268 destination if it is a file link and calcultate the SHA-1 of the file's
269 content if the path points to a file and not a symlink.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000270 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000271 out = {}
272 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000273 # if prevdict.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000274 # # The file's content is ignored. Skip the time and hard code mode.
275 # if get_flavor() != 'win':
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000276 # out['m'] = stat.S_IRUSR | stat.S_IRGRP
277 # out['s'] = 0
278 # out['h'] = SHA_1_NULL
279 # out['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000280 # return out
281
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000282 # Always check the file stat and check if it is a link. The timestamp is used
283 # to know if the file's content/symlink destination should be looked into.
284 # E.g. only reuse from prevdict if the timestamp hasn't changed.
285 # There is the risk of the file's timestamp being reset to its last value
286 # manually while its content changed. We don't protect against that use case.
287 try:
288 filestats = os.lstat(filepath)
289 except OSError:
290 # The file is not present.
291 raise run_isolated.MappingError('%s is missing' % filepath)
292 is_link = stat.S_ISLNK(filestats.st_mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000293
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000294 if get_flavor() != 'win':
295 # Ignore file mode on Windows since it's not really useful there.
296 filemode = stat.S_IMODE(filestats.st_mode)
297 # Remove write access for group and all access to 'others'.
298 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
299 if read_only:
300 filemode &= ~stat.S_IWUSR
301 if filemode & stat.S_IXUSR:
302 filemode |= stat.S_IXGRP
303 else:
304 filemode &= ~stat.S_IXGRP
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000305 out['m'] = filemode
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000306
307 # Used to skip recalculating the hash or link destination. Use the most recent
308 # update time.
309 # TODO(maruel): Save it in the .state file instead of .isolated so the
310 # .isolated file is deterministic.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000311 out['t'] = int(round(filestats.st_mtime))
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000312
313 if not is_link:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000314 out['s'] = filestats.st_size
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000315 # If the timestamp wasn't updated and the file size is still the same, carry
316 # on the sha-1.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000317 if (prevdict.get('t') == out['t'] and
318 prevdict.get('s') == out['s']):
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000319 # Reuse the previous hash if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000320 out['h'] = prevdict.get('h')
321 if not out.get('h'):
maruel@chromium.org6da38772012-12-11 21:36:37 +0000322 out['h'] = isolateserver_archive.sha1_file(filepath)
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000323 else:
324 # If the timestamp wasn't updated, carry on the link destination.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000325 if prevdict.get('t') == out['t']:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000326 # Reuse the previous link destination if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000327 out['l'] = prevdict.get('l')
328 if out.get('l') is None:
329 out['l'] = os.readlink(filepath) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000330 return out
331
332
333### Variable stuff.
334
335
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000336def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000337 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000338 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000339
340
341def determine_root_dir(relative_root, infiles):
342 """For a list of infiles, determines the deepest root directory that is
343 referenced indirectly.
344
345 All arguments must be using os.path.sep.
346 """
347 # The trick used to determine the root directory is to look at "how far" back
348 # up it is looking up.
349 deepest_root = relative_root
350 for i in infiles:
351 x = relative_root
352 while i.startswith('..' + os.path.sep):
353 i = i[3:]
354 assert not i.startswith(os.path.sep)
355 x = os.path.dirname(x)
356 if deepest_root.startswith(x):
357 deepest_root = x
358 logging.debug(
359 'determine_root_dir(%s, %d files) -> %s' % (
360 relative_root, len(infiles), deepest_root))
361 return deepest_root
362
363
364def replace_variable(part, variables):
365 m = re.match(r'<\(([A-Z_]+)\)', part)
366 if m:
367 if m.group(1) not in variables:
368 raise ExecutionError(
369 'Variable "%s" was not found in %s.\nDid you forget to specify '
370 '--variable?' % (m.group(1), variables))
371 return variables[m.group(1)]
372 return part
373
374
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000375def process_variables(cwd, variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000376 """Processes path variables as a special case and returns a copy of the dict.
377
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000378 For each 'path' variable: first normalizes it based on |cwd|, verifies it
379 exists then sets it as relative to relative_base_dir.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000380 """
381 variables = variables.copy()
382 for i in PATH_VARIABLES:
383 if i not in variables:
384 continue
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000385 # Variables could contain / or \ on windows. Always normalize to
386 # os.path.sep.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000387 variable = variables[i].replace('/', os.path.sep)
388 if os.path.isabs(variable):
389 raise ExecutionError(
390 'Variable can\'t absolute: %s: %s' % (i, variables[i]))
391
392 variable = os.path.join(cwd, variable)
393 variable = os.path.normpath(variable)
394 if not os.path.isdir(variable):
395 raise ExecutionError('%s=%s is not a directory' % (i, variable))
396
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000397 # All variables are relative to the .isolate file.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000398 variable = os.path.relpath(variable, relative_base_dir)
399 logging.debug(
400 'Translated variable %s from %s to %s', i, variables[i], variable)
401 variables[i] = variable
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000402 return variables
403
404
405def eval_variables(item, variables):
406 """Replaces the .isolate variables in a string item.
407
408 Note that the .isolate format is a subset of the .gyp dialect.
409 """
410 return ''.join(
411 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
412
413
414def classify_files(root_dir, tracked, untracked):
415 """Converts the list of files into a .isolate 'variables' dictionary.
416
417 Arguments:
418 - tracked: list of files names to generate a dictionary out of that should
419 probably be tracked.
420 - untracked: list of files names that must not be tracked.
421 """
422 # These directories are not guaranteed to be always present on every builder.
423 OPTIONAL_DIRECTORIES = (
424 'test/data/plugin',
425 'third_party/WebKit/LayoutTests',
426 )
427
428 new_tracked = []
429 new_untracked = list(untracked)
430
431 def should_be_tracked(filepath):
432 """Returns True if it is a file without whitespace in a non-optional
433 directory that has no symlink in its path.
434 """
435 if filepath.endswith('/'):
436 return False
437 if ' ' in filepath:
438 return False
439 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
440 return False
441 # Look if any element in the path is a symlink.
442 split = filepath.split('/')
443 for i in range(len(split)):
444 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
445 return False
446 return True
447
448 for filepath in sorted(tracked):
449 if should_be_tracked(filepath):
450 new_tracked.append(filepath)
451 else:
452 # Anything else.
453 new_untracked.append(filepath)
454
455 variables = {}
456 if new_tracked:
457 variables[KEY_TRACKED] = sorted(new_tracked)
458 if new_untracked:
459 variables[KEY_UNTRACKED] = sorted(new_untracked)
460 return variables
461
462
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000463def chromium_fix(f, variables):
464 """Fixes an isolate dependnecy with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000465 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
466 # separator.
467 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000468 # Ignored items.
469 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000470 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000471 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000472 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000473 # 'First Run' is not created by the compile, but by the test itself.
474 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000475
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000476 # Blacklist logs and other unimportant files.
477 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
478 logging.debug('Ignoring %s', f)
479 return None
480
maruel@chromium.org7650e422012-11-16 21:56:42 +0000481 EXECUTABLE = re.compile(
482 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
483 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
484 r'$')
485 match = EXECUTABLE.match(f)
486 if match:
487 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
488
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000489 if sys.platform == 'darwin':
490 # On OSX, the name of the output is dependent on gyp define, it can be
491 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
492 # Framework.framework'. Furthermore, they are versioned with a gyp
493 # variable. To lower the complexity of the .isolate file, remove all the
494 # individual entries that show up under any of the 4 entries and replace
495 # them with the directory itself. Overall, this results in a bit more
496 # files than strictly necessary.
497 OSX_BUNDLES = (
498 '<(PRODUCT_DIR)/Chromium Framework.framework/',
499 '<(PRODUCT_DIR)/Chromium.app/',
500 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
501 '<(PRODUCT_DIR)/Google Chrome.app/',
502 )
503 for prefix in OSX_BUNDLES:
504 if f.startswith(prefix):
505 # Note this result in duplicate values, so the a set() must be used to
506 # remove duplicates.
507 return prefix
508 return f
509
510
511def generate_simplified(
512 tracked, untracked, touched, root_dir, variables, relative_cwd):
513 """Generates a clean and complete .isolate 'variables' dictionary.
514
515 Cleans up and extracts only files from within root_dir then processes
516 variables and relative_cwd.
517 """
518 root_dir = os.path.realpath(root_dir)
519 logging.info(
520 'generate_simplified(%d files, %s, %s, %s)' %
521 (len(tracked) + len(untracked) + len(touched),
522 root_dir, variables, relative_cwd))
523
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000524 # Preparation work.
525 relative_cwd = cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000526 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000527 # Creates the right set of variables here. We only care about PATH_VARIABLES.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000528 path_variables = dict(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000529 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
530 for k in PATH_VARIABLES if k in variables)
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000531 variables = variables.copy()
532 variables.update(path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000533
534 # Actual work: Process the files.
535 # TODO(maruel): if all the files in a directory are in part tracked and in
536 # part untracked, the directory will not be extracted. Tracked files should be
537 # 'promoted' to be untracked as needed.
538 tracked = trace_inputs.extract_directories(
539 root_dir, tracked, default_blacklist)
540 untracked = trace_inputs.extract_directories(
541 root_dir, untracked, default_blacklist)
542 # touched is not compressed, otherwise it would result in files to be archived
543 # that we don't need.
544
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000545 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000546 def fix(f):
547 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000548 # Important, GYP stores the files with / and not \.
549 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000550 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000551 # If it's not already a variable.
552 if not f.startswith('<'):
553 # relative_cwd is usually the directory containing the gyp file. It may be
554 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000555 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000556 # Convert the whole thing to / since it's isolate's speak.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000557 f = posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000558 posixpath.join(root_dir_posix, f),
559 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000560
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000561 for variable, root_path in path_variables.iteritems():
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000562 if f.startswith(root_path):
563 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000564 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000565 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000566 return f
567
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000568 def fix_all(items):
569 """Reduces the items to convert variables, removes unneeded items, apply
570 chromium-specific fixes and only return unique items.
571 """
572 variables_converted = (fix(f.path) for f in items)
573 chromium_fixed = (chromium_fix(f, variables) for f in variables_converted)
574 return set(f for f in chromium_fixed if f)
575
576 tracked = fix_all(tracked)
577 untracked = fix_all(untracked)
578 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000579 out = classify_files(root_dir, tracked, untracked)
580 if touched:
581 out[KEY_TOUCHED] = sorted(touched)
582 return out
583
584
585def generate_isolate(
586 tracked, untracked, touched, root_dir, variables, relative_cwd):
587 """Generates a clean and complete .isolate file."""
588 result = generate_simplified(
589 tracked, untracked, touched, root_dir, variables, relative_cwd)
590 return {
591 'conditions': [
592 ['OS=="%s"' % get_flavor(), {
593 'variables': result,
594 }],
595 ],
596 }
597
598
599def split_touched(files):
600 """Splits files that are touched vs files that are read."""
601 tracked = []
602 touched = []
603 for f in files:
604 if f.size:
605 tracked.append(f)
606 else:
607 touched.append(f)
608 return tracked, touched
609
610
611def pretty_print(variables, stdout):
612 """Outputs a gyp compatible list from the decoded variables.
613
614 Similar to pprint.print() but with NIH syndrome.
615 """
616 # Order the dictionary keys by these keys in priority.
617 ORDER = (
618 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
619 KEY_TRACKED, KEY_UNTRACKED)
620
621 def sorting_key(x):
622 """Gives priority to 'most important' keys before the others."""
623 if x in ORDER:
624 return str(ORDER.index(x))
625 return x
626
627 def loop_list(indent, items):
628 for item in items:
629 if isinstance(item, basestring):
630 stdout.write('%s\'%s\',\n' % (indent, item))
631 elif isinstance(item, dict):
632 stdout.write('%s{\n' % indent)
633 loop_dict(indent + ' ', item)
634 stdout.write('%s},\n' % indent)
635 elif isinstance(item, list):
636 # A list inside a list will write the first item embedded.
637 stdout.write('%s[' % indent)
638 for index, i in enumerate(item):
639 if isinstance(i, basestring):
640 stdout.write(
641 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
642 elif isinstance(i, dict):
643 stdout.write('{\n')
644 loop_dict(indent + ' ', i)
645 if index != len(item) - 1:
646 x = ', '
647 else:
648 x = ''
649 stdout.write('%s}%s' % (indent, x))
650 else:
651 assert False
652 stdout.write('],\n')
653 else:
654 assert False
655
656 def loop_dict(indent, items):
657 for key in sorted(items, key=sorting_key):
658 item = items[key]
659 stdout.write("%s'%s': " % (indent, key))
660 if isinstance(item, dict):
661 stdout.write('{\n')
662 loop_dict(indent + ' ', item)
663 stdout.write(indent + '},\n')
664 elif isinstance(item, list):
665 stdout.write('[\n')
666 loop_list(indent + ' ', item)
667 stdout.write(indent + '],\n')
668 elif isinstance(item, basestring):
669 stdout.write(
670 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
671 elif item in (True, False, None):
672 stdout.write('%s\n' % item)
673 else:
674 assert False, item
675
676 stdout.write('{\n')
677 loop_dict(' ', variables)
678 stdout.write('}\n')
679
680
681def union(lhs, rhs):
682 """Merges two compatible datastructures composed of dict/list/set."""
683 assert lhs is not None or rhs is not None
684 if lhs is None:
685 return copy.deepcopy(rhs)
686 if rhs is None:
687 return copy.deepcopy(lhs)
688 assert type(lhs) == type(rhs), (lhs, rhs)
689 if hasattr(lhs, 'union'):
690 # Includes set, OSSettings and Configs.
691 return lhs.union(rhs)
692 if isinstance(lhs, dict):
693 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
694 elif isinstance(lhs, list):
695 # Do not go inside the list.
696 return lhs + rhs
697 assert False, type(lhs)
698
699
700def extract_comment(content):
701 """Extracts file level comment."""
702 out = []
703 for line in content.splitlines(True):
704 if line.startswith('#'):
705 out.append(line)
706 else:
707 break
708 return ''.join(out)
709
710
711def eval_content(content):
712 """Evaluates a python file and return the value defined in it.
713
714 Used in practice for .isolate files.
715 """
716 globs = {'__builtins__': None}
717 locs = {}
718 value = eval(content, globs, locs)
719 assert locs == {}, locs
720 assert globs == {'__builtins__': None}, globs
721 return value
722
723
724def verify_variables(variables):
725 """Verifies the |variables| dictionary is in the expected format."""
726 VALID_VARIABLES = [
727 KEY_TOUCHED,
728 KEY_TRACKED,
729 KEY_UNTRACKED,
730 'command',
731 'read_only',
732 ]
733 assert isinstance(variables, dict), variables
734 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
735 for name, value in variables.iteritems():
736 if name == 'read_only':
737 assert value in (True, False, None), value
738 else:
739 assert isinstance(value, list), value
740 assert all(isinstance(i, basestring) for i in value), value
741
742
743def verify_condition(condition):
744 """Verifies the |condition| dictionary is in the expected format."""
745 VALID_INSIDE_CONDITION = ['variables']
746 assert isinstance(condition, list), condition
747 assert 2 <= len(condition) <= 3, condition
748 assert re.match(r'OS==\"([a-z]+)\"', condition[0]), condition[0]
749 for c in condition[1:]:
750 assert isinstance(c, dict), c
751 assert set(VALID_INSIDE_CONDITION).issuperset(set(c)), c.keys()
752 verify_variables(c.get('variables', {}))
753
754
755def verify_root(value):
756 VALID_ROOTS = ['variables', 'conditions']
757 assert isinstance(value, dict), value
758 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
759 verify_variables(value.get('variables', {}))
760
761 conditions = value.get('conditions', [])
762 assert isinstance(conditions, list), conditions
763 for condition in conditions:
764 verify_condition(condition)
765
766
767def remove_weak_dependencies(values, key, item, item_oses):
768 """Remove any oses from this key if the item is already under a strong key."""
769 if key == KEY_TOUCHED:
770 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
771 oses = values.get(stronger_key, {}).get(item, None)
772 if oses:
773 item_oses -= oses
774
775 return item_oses
776
777
csharp@chromium.org31176252012-11-02 13:04:40 +0000778def remove_repeated_dependencies(folders, key, item, item_oses):
779 """Remove any OSes from this key if the item is in a folder that is already
780 included."""
781
782 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
783 for (folder, oses) in folders.iteritems():
784 if folder != item and item.startswith(folder):
785 item_oses -= oses
786
787 return item_oses
788
789
790def get_folders(values_dict):
791 """Return a dict of all the folders in the given value_dict."""
792 return dict((item, oses) for (item, oses) in values_dict.iteritems()
793 if item.endswith('/'))
794
795
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000796def invert_map(variables):
797 """Converts a dict(OS, dict(deptype, list(dependencies)) to a flattened view.
798
799 Returns a tuple of:
800 1. dict(deptype, dict(dependency, set(OSes)) for easier processing.
801 2. All the OSes found as a set.
802 """
803 KEYS = (
804 KEY_TOUCHED,
805 KEY_TRACKED,
806 KEY_UNTRACKED,
807 'command',
808 'read_only',
809 )
810 out = dict((key, {}) for key in KEYS)
811 for os_name, values in variables.iteritems():
812 for key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED):
813 for item in values.get(key, []):
814 out[key].setdefault(item, set()).add(os_name)
815
816 # command needs special handling.
817 command = tuple(values.get('command', []))
818 out['command'].setdefault(command, set()).add(os_name)
819
820 # read_only needs special handling.
821 out['read_only'].setdefault(values.get('read_only'), set()).add(os_name)
822 return out, set(variables)
823
824
825def reduce_inputs(values, oses):
826 """Reduces the invert_map() output to the strictest minimum list.
827
828 1. Construct the inverse map first.
829 2. Look at each individual file and directory, map where they are used and
830 reconstruct the inverse dictionary.
831 3. Do not convert back to negative if only 2 OSes were merged.
832
833 Returns a tuple of:
834 1. the minimized dictionary
835 2. oses passed through as-is.
836 """
837 KEYS = (
838 KEY_TOUCHED,
839 KEY_TRACKED,
840 KEY_UNTRACKED,
841 'command',
842 'read_only',
843 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000844
845 # Folders can only live in KEY_UNTRACKED.
846 folders = get_folders(values.get(KEY_UNTRACKED, {}))
847
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000848 out = dict((key, {}) for key in KEYS)
849 assert all(oses), oses
850 if len(oses) > 2:
851 for key in KEYS:
852 for item, item_oses in values.get(key, {}).iteritems():
853 item_oses = remove_weak_dependencies(values, key, item, item_oses)
csharp@chromium.org31176252012-11-02 13:04:40 +0000854 item_oses = remove_repeated_dependencies(folders, key, item, item_oses)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000855 if not item_oses:
856 continue
857
858 # Converts all oses.difference('foo') to '!foo'.
859 assert all(item_oses), item_oses
860 missing = oses.difference(item_oses)
861 if len(missing) == 1:
862 # Replace it with a negative.
863 out[key][item] = set(['!' + tuple(missing)[0]])
864 elif not missing:
865 out[key][item] = set([None])
866 else:
867 out[key][item] = set(item_oses)
868 else:
869 for key in KEYS:
870 for item, item_oses in values.get(key, {}).iteritems():
871 item_oses = remove_weak_dependencies(values, key, item, item_oses)
872 if not item_oses:
873 continue
874
875 # Converts all oses.difference('foo') to '!foo'.
876 assert None not in item_oses, item_oses
877 out[key][item] = set(item_oses)
878 return out, oses
879
880
881def convert_map_to_isolate_dict(values, oses):
882 """Regenerates back a .isolate configuration dict from files and dirs
883 mappings generated from reduce_inputs().
884 """
885 # First, inverse the mapping to make it dict first.
886 config = {}
887 for key in values:
888 for item, oses in values[key].iteritems():
889 if item is None:
890 # For read_only default.
891 continue
892 for cond_os in oses:
893 cond_key = None if cond_os is None else cond_os.lstrip('!')
894 # Insert the if/else dicts.
895 condition_values = config.setdefault(cond_key, [{}, {}])
896 # If condition is negative, use index 1, else use index 0.
897 cond_value = condition_values[int((cond_os or '').startswith('!'))]
898 variables = cond_value.setdefault('variables', {})
899
900 if item in (True, False):
901 # One-off for read_only.
902 variables[key] = item
903 else:
904 if isinstance(item, tuple) and item:
905 # One-off for command.
906 # Do not merge lists and do not sort!
907 # Note that item is a tuple.
908 assert key not in variables
909 variables[key] = list(item)
910 elif item:
911 # The list of items (files or dirs). Append the new item and keep
912 # the list sorted.
913 l = variables.setdefault(key, [])
914 l.append(item)
915 l.sort()
916
917 out = {}
918 for o in sorted(config):
919 d = config[o]
920 if o is None:
921 assert not d[1]
922 out = union(out, d[0])
923 else:
924 c = out.setdefault('conditions', [])
925 if d[1]:
926 c.append(['OS=="%s"' % o] + d)
927 else:
928 c.append(['OS=="%s"' % o] + d[0:1])
929 return out
930
931
932### Internal state files.
933
934
935class OSSettings(object):
936 """Represents the dependencies for an OS. The structure is immutable.
937
938 It's the .isolate settings for a specific file.
939 """
940 def __init__(self, name, values):
941 self.name = name
942 verify_variables(values)
943 self.touched = sorted(values.get(KEY_TOUCHED, []))
944 self.tracked = sorted(values.get(KEY_TRACKED, []))
945 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
946 self.command = values.get('command', [])[:]
947 self.read_only = values.get('read_only')
948
949 def union(self, rhs):
950 assert self.name == rhs.name
maruel@chromium.org669edcb2012-11-02 19:16:14 +0000951 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000952 var = {
953 KEY_TOUCHED: sorted(self.touched + rhs.touched),
954 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
955 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
956 'command': self.command or rhs.command,
957 'read_only': rhs.read_only if self.read_only is None else self.read_only,
958 }
959 return OSSettings(self.name, var)
960
961 def flatten(self):
962 out = {}
963 if self.command:
964 out['command'] = self.command
965 if self.touched:
966 out[KEY_TOUCHED] = self.touched
967 if self.tracked:
968 out[KEY_TRACKED] = self.tracked
969 if self.untracked:
970 out[KEY_UNTRACKED] = self.untracked
971 if self.read_only is not None:
972 out['read_only'] = self.read_only
973 return out
974
975
976class Configs(object):
977 """Represents a processed .isolate file.
978
979 Stores the file in a processed way, split by each the OS-specific
980 configurations.
981
982 The self.per_os[None] member contains all the 'else' clauses plus the default
983 values. It is not included in the flatten() result.
984 """
985 def __init__(self, oses, file_comment):
986 self.file_comment = file_comment
987 self.per_os = {
988 None: OSSettings(None, {}),
989 }
990 self.per_os.update(dict((name, OSSettings(name, {})) for name in oses))
991
992 def union(self, rhs):
993 items = list(set(self.per_os.keys() + rhs.per_os.keys()))
994 # Takes the first file comment, prefering lhs.
995 out = Configs(items, self.file_comment or rhs.file_comment)
996 for key in items:
997 out.per_os[key] = union(self.per_os.get(key), rhs.per_os.get(key))
998 return out
999
1000 def add_globals(self, values):
1001 for key in self.per_os:
1002 self.per_os[key] = self.per_os[key].union(OSSettings(key, values))
1003
1004 def add_values(self, for_os, values):
1005 self.per_os[for_os] = self.per_os[for_os].union(OSSettings(for_os, values))
1006
1007 def add_negative_values(self, for_os, values):
1008 """Includes the variables to all OSes except |for_os|.
1009
1010 This includes 'None' so unknown OSes gets it too.
1011 """
1012 for key in self.per_os:
1013 if key != for_os:
1014 self.per_os[key] = self.per_os[key].union(OSSettings(key, values))
1015
1016 def flatten(self):
1017 """Returns a flat dictionary representation of the configuration.
1018
1019 Skips None pseudo-OS.
1020 """
1021 return dict(
1022 (k, v.flatten()) for k, v in self.per_os.iteritems() if k is not None)
1023
1024
1025def load_isolate_as_config(value, file_comment, default_oses):
1026 """Parses one .isolate file and returns a Configs() instance.
1027
1028 |value| is the loaded dictionary that was defined in the gyp file.
1029
1030 The expected format is strict, anything diverting from the format below will
1031 throw an assert:
1032 {
1033 'variables': {
1034 'command': [
1035 ...
1036 ],
1037 'isolate_dependency_tracked': [
1038 ...
1039 ],
1040 'isolate_dependency_untracked': [
1041 ...
1042 ],
1043 'read_only': False,
1044 },
1045 'conditions': [
1046 ['OS=="<os>"', {
1047 'variables': {
1048 ...
1049 },
1050 }, { # else
1051 'variables': {
1052 ...
1053 },
1054 }],
1055 ...
1056 ],
1057 }
1058 """
1059 verify_root(value)
1060
1061 # Scan to get the list of OSes.
1062 conditions = value.get('conditions', [])
1063 oses = set(re.match(r'OS==\"([a-z]+)\"', c[0]).group(1) for c in conditions)
1064 oses = oses.union(default_oses)
1065 configs = Configs(oses, file_comment)
1066
1067 # Global level variables.
1068 configs.add_globals(value.get('variables', {}))
1069
1070 # OS specific variables.
1071 for condition in conditions:
1072 condition_os = re.match(r'OS==\"([a-z]+)\"', condition[0]).group(1)
1073 configs.add_values(condition_os, condition[1].get('variables', {}))
1074 if len(condition) > 2:
1075 configs.add_negative_values(
1076 condition_os, condition[2].get('variables', {}))
1077 return configs
1078
1079
1080def load_isolate_for_flavor(content, flavor):
1081 """Loads the .isolate file and returns the information unprocessed.
1082
1083 Returns the command, dependencies and read_only flag. The dependencies are
1084 fixed to use os.path.sep.
1085 """
1086 # Load the .isolate file, process its conditions, retrieve the command and
1087 # dependencies.
1088 configs = load_isolate_as_config(eval_content(content), None, DEFAULT_OSES)
1089 config = configs.per_os.get(flavor) or configs.per_os.get(None)
1090 if not config:
1091 raise ExecutionError('Failed to load configuration for \'%s\'' % flavor)
1092 # Merge tracked and untracked dependencies, isolate.py doesn't care about the
1093 # trackability of the dependencies, only the build tool does.
1094 dependencies = [
1095 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1096 ]
1097 touched = [f.replace('/', os.path.sep) for f in config.touched]
1098 return config.command, dependencies, touched, config.read_only
1099
1100
1101class Flattenable(object):
1102 """Represents data that can be represented as a json file."""
1103 MEMBERS = ()
1104
1105 def flatten(self):
1106 """Returns a json-serializable version of itself.
1107
1108 Skips None entries.
1109 """
1110 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1111 return dict((member, value) for member, value in items if value is not None)
1112
1113 @classmethod
1114 def load(cls, data):
1115 """Loads a flattened version."""
1116 data = data.copy()
1117 out = cls()
1118 for member in out.MEMBERS:
1119 if member in data:
1120 # Access to a protected member XXX of a client class
1121 # pylint: disable=W0212
1122 out._load_member(member, data.pop(member))
1123 if data:
1124 raise ValueError(
1125 'Found unexpected entry %s while constructing an object %s' %
1126 (data, cls.__name__), data, cls.__name__)
1127 return out
1128
1129 def _load_member(self, member, value):
1130 """Loads a member into self."""
1131 setattr(self, member, value)
1132
1133 @classmethod
1134 def load_file(cls, filename):
1135 """Loads the data from a file or return an empty instance."""
1136 out = cls()
1137 try:
1138 out = cls.load(trace_inputs.read_json(filename))
1139 logging.debug('Loaded %s(%s)' % (cls.__name__, filename))
1140 except (IOError, ValueError):
1141 logging.warn('Failed to load %s' % filename)
1142 return out
1143
1144
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001145class SavedState(Flattenable):
1146 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001147
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001148 This file caches the items calculated by this script and is used to increase
1149 the performance of the script. This file is not loaded by run_isolated.py.
1150 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001151
1152 It is important to note that the 'files' dict keys are using native OS path
1153 separator instead of '/' used in .isolate file.
1154 """
1155 MEMBERS = (
1156 'command',
1157 'files',
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001158 'isolate_file',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001159 'os',
1160 'read_only',
1161 'relative_cwd',
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001162 'variables',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001163 )
1164
1165 os = get_flavor()
1166
1167 def __init__(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001168 super(SavedState, self).__init__()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001169 self.command = []
1170 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001171 self.isolate_file = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001172 self.read_only = None
1173 self.relative_cwd = None
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001174 self.variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001175
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001176 def update(self, isolate_file, variables):
1177 """Updates the saved state with new data to keep GYP variables and internal
1178 reference to the original .isolate file.
1179 """
1180 self.isolate_file = isolate_file
1181 self.variables.update(variables)
1182
1183 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
1184 """Updates the saved state with data necessary to generate a .isolated file.
1185 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001186 self.command = command
1187 # Add new files.
1188 for f in infiles:
1189 self.files.setdefault(f, {})
1190 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001191 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001192 # Prune extraneous files that are not a dependency anymore.
1193 for f in set(self.files).difference(set(infiles).union(touched)):
1194 del self.files[f]
1195 if read_only is not None:
1196 self.read_only = read_only
1197 self.relative_cwd = relative_cwd
1198
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001199 def to_isolated(self):
1200 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001201
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001202 http://chromium.org/developers/testing/isolated-testing/design
1203 """
1204 def strip(data):
1205 """Returns a 'files' entry with only the whitelisted keys."""
1206 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
1207
1208 out = {
1209 'files': dict(
1210 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
1211 'os': self.os,
1212 }
1213 if self.command:
1214 out['command'] = self.command
1215 if self.read_only is not None:
1216 out['read_only'] = self.read_only
1217 if self.relative_cwd:
1218 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001219 return out
1220
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001221 @classmethod
1222 def load(cls, data):
1223 out = super(SavedState, cls).load(data)
1224 if out.isolate_file:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001225 out.isolate_file = trace_inputs.get_native_path_case(
1226 unicode(out.isolate_file))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001227 return out
1228
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001229 def _load_member(self, member, value):
1230 if member == 'os':
1231 if value != self.os:
1232 raise run_isolated.ConfigError(
1233 'The .isolated file was created on another platform')
1234 else:
1235 super(SavedState, self)._load_member(member, value)
1236
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001237 def __str__(self):
1238 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001239 out += ' command: %s\n' % self.command
1240 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001241 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001242 out += ' read_only: %s\n' % self.read_only
1243 out += ' relative_cwd: %s' % self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001244 out += ' variables: %s' % ''.join(
1245 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1246 out += ')'
1247 return out
1248
1249
1250class CompleteState(object):
1251 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001252 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001253 super(CompleteState, self).__init__()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001254 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001255 # Contains the data to ease developer's use-case but that is not strictly
1256 # necessary.
1257 self.saved_state = saved_state
1258
1259 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001260 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001261 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001262 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001263 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001264 isolated_filepath,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001265 SavedState.load_file(isolatedfile_to_state(isolated_filepath)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001266
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001267 def load_isolate(self, cwd, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001268 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001269 .isolate file.
1270
1271 Processes the loaded data, deduce root_dir, relative_cwd.
1272 """
1273 # Make sure to not depend on os.getcwd().
1274 assert os.path.isabs(isolate_file), isolate_file
1275 logging.info(
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001276 'CompleteState.load_isolate(%s, %s, %s, %s)',
1277 cwd, isolate_file, variables, ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001278 relative_base_dir = os.path.dirname(isolate_file)
1279
1280 # Processes the variables and update the saved state.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001281 variables = process_variables(cwd, variables, relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001282 self.saved_state.update(isolate_file, variables)
1283
1284 with open(isolate_file, 'r') as f:
1285 # At that point, variables are not replaced yet in command and infiles.
1286 # infiles may contain directory entries and is in posix style.
1287 command, infiles, touched, read_only = load_isolate_for_flavor(
1288 f.read(), get_flavor())
1289 command = [eval_variables(i, self.saved_state.variables) for i in command]
1290 infiles = [eval_variables(f, self.saved_state.variables) for f in infiles]
1291 touched = [eval_variables(f, self.saved_state.variables) for f in touched]
1292 # root_dir is automatically determined by the deepest root accessed with the
1293 # form '../../foo/bar'.
1294 root_dir = determine_root_dir(relative_base_dir, infiles + touched)
1295 # The relative directory is automatically determined by the relative path
1296 # between root_dir and the directory containing the .isolate file,
1297 # isolate_base_dir.
1298 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
1299 # Normalize the files based to root_dir. It is important to keep the
1300 # trailing os.path.sep at that step.
1301 infiles = [
1302 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1303 for f in infiles
1304 ]
1305 touched = [
1306 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1307 for f in touched
1308 ]
1309 # Expand the directories by listing each file inside. Up to now, trailing
1310 # os.path.sep must be kept. Do not expand 'touched'.
1311 infiles = expand_directories_and_symlinks(
1312 root_dir,
1313 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001314 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
1315 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001316
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001317 # Finally, update the new data to be able to generate the foo.isolated file,
1318 # the file that is used by run_isolated.py.
1319 self.saved_state.update_isolated(
1320 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001321 logging.debug(self)
1322
maruel@chromium.org9268f042012-10-17 17:36:41 +00001323 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001324 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001325
maruel@chromium.org9268f042012-10-17 17:36:41 +00001326 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1327 file is tainted.
1328
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001329 See process_input() for more information.
1330 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001331 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001332 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001333 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001334 else:
1335 filepath = os.path.join(self.root_dir, infile)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001336 self.saved_state.files[infile] = process_input(
1337 filepath,
1338 self.saved_state.files[infile],
1339 self.saved_state.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001340
1341 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001342 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001343 logging.debug('Dumping to %s' % self.isolated_filepath)
1344 trace_inputs.write_json(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001345 self.isolated_filepath, self.saved_state.to_isolated(), True)
1346 total_bytes = sum(
1347 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001348 if total_bytes:
1349 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001350 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001351 logging.debug('Dumping to %s' % saved_state_file)
1352 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1353
1354 @property
1355 def root_dir(self):
1356 """isolate_file is always inside relative_cwd relative to root_dir."""
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001357 if not self.saved_state.isolate_file:
1358 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001359 isolate_dir = os.path.dirname(self.saved_state.isolate_file)
1360 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001361 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001362 return isolate_dir
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001363 assert isolate_dir.endswith(self.saved_state.relative_cwd), (
1364 isolate_dir, self.saved_state.relative_cwd)
1365 return isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001366
1367 @property
1368 def resultdir(self):
1369 """Directory containing the results, usually equivalent to the variable
1370 PRODUCT_DIR.
1371 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001372 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001373
1374 def __str__(self):
1375 def indent(data, indent_length):
1376 """Indents text."""
1377 spacing = ' ' * indent_length
1378 return ''.join(spacing + l for l in str(data).splitlines(True))
1379
1380 out = '%s(\n' % self.__class__.__name__
1381 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001382 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1383 return out
1384
1385
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001386def load_complete_state(options, cwd, subdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001387 """Loads a CompleteState.
1388
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001389 This includes data from .isolate and .isolated.state files. Never reads the
1390 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001391
1392 Arguments:
1393 options: Options instance generated with OptionParserIsolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001394 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001395 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001396 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001397 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001398 else:
1399 # Constructs a dummy object that cannot be saved. Useful for temporary
1400 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001401 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001402 options.isolate = options.isolate or complete_state.saved_state.isolate_file
1403 if not options.isolate:
1404 raise ExecutionError('A .isolate file is required.')
1405 if (complete_state.saved_state.isolate_file and
1406 options.isolate != complete_state.saved_state.isolate_file):
1407 raise ExecutionError(
1408 '%s and %s do not match.' % (
1409 options.isolate, complete_state.saved_state.isolate_file))
1410
1411 # Then load the .isolate and expands directories.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001412 complete_state.load_isolate(
1413 cwd, options.isolate, options.variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001414
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001415 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001416 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001417 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001418 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1419 subdir = subdir.replace('/', os.path.sep)
1420 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001421 return complete_state
1422
1423
1424def read_trace_as_isolate_dict(complete_state):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001425 """Reads a trace and returns the .isolate dictionary.
1426
1427 Returns exceptions during the log parsing so it can be re-raised.
1428 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001429 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001430 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001431 if not os.path.isfile(logfile):
1432 raise ExecutionError(
1433 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1434 try:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00001435 data = api.parse_log(logfile, default_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001436 exceptions = [i['exception'] for i in data if 'exception' in i]
1437 results = (i['results'] for i in data if 'results' in i)
1438 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1439 files = set(sum((result.existent for result in results_stripped), []))
1440 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001441 value = generate_isolate(
1442 tracked,
1443 [],
1444 touched,
1445 complete_state.root_dir,
1446 complete_state.saved_state.variables,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001447 complete_state.saved_state.relative_cwd)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001448 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001449 except trace_inputs.TracingFailure, e:
1450 raise ExecutionError(
1451 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001452 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001453
1454
1455def print_all(comment, data, stream):
1456 """Prints a complete .isolate file and its top-level file comment into a
1457 stream.
1458 """
1459 if comment:
1460 stream.write(comment)
1461 pretty_print(data, stream)
1462
1463
1464def merge(complete_state):
1465 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001466 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001467
1468 # Now take that data and union it into the original .isolate file.
1469 with open(complete_state.saved_state.isolate_file, 'r') as f:
1470 prev_content = f.read()
1471 prev_config = load_isolate_as_config(
1472 eval_content(prev_content),
1473 extract_comment(prev_content),
1474 DEFAULT_OSES)
1475 new_config = load_isolate_as_config(value, '', DEFAULT_OSES)
1476 config = union(prev_config, new_config)
1477 # pylint: disable=E1103
1478 data = convert_map_to_isolate_dict(
1479 *reduce_inputs(*invert_map(config.flatten())))
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001480 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001481 with open(complete_state.saved_state.isolate_file, 'wb') as f:
1482 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001483 if exceptions:
1484 # It got an exception, raise the first one.
1485 raise \
1486 exceptions[0][0], \
1487 exceptions[0][1], \
1488 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001489
1490
1491def CMDcheck(args):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001492 """Checks that all the inputs are present and generates .isolated."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001493 parser = OptionParserIsolate(command='check')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001494 parser.add_option('--subdir', help='Filters to a subdirectory')
1495 options, args = parser.parse_args(args)
1496 if args:
1497 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001498 complete_state = load_complete_state(options, os.getcwd(), options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001499
1500 # Nothing is done specifically. Just store the result and state.
1501 complete_state.save_files()
1502 return 0
1503
1504
1505def CMDhashtable(args):
1506 """Creates a hash table content addressed object store.
1507
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001508 All the files listed in the .isolated file are put in the output directory
1509 with the file name being the sha-1 of the file's content.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 """
1511 parser = OptionParserIsolate(command='hashtable')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001512 parser.add_option('--subdir', help='Filters to a subdirectory')
1513 options, args = parser.parse_args(args)
1514 if args:
1515 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001516
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001517 with run_isolated.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001518 success = False
1519 try:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001520 complete_state = load_complete_state(options, os.getcwd(), options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001521 options.outdir = (
1522 options.outdir or os.path.join(complete_state.resultdir, 'hashtable'))
1523 # Make sure that complete_state isn't modified until save_files() is
1524 # called, because any changes made to it here will propagate to the files
1525 # created (which is probably not intended).
1526 complete_state.save_files()
1527
1528 logging.info('Creating content addressed object store with %d item',
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001529 len(complete_state.saved_state.files))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001530
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001531 with open(complete_state.isolated_filepath, 'rb') as f:
maruel@chromium.org861a5e72012-10-09 14:49:42 +00001532 content = f.read()
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001533 isolated_metadata = {
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001534 'h': hashlib.sha1(content).hexdigest(),
1535 's': len(content),
csharp@chromium.orgd62bcb92012-10-16 17:45:33 +00001536 'priority': '0'
1537 }
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001538
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001539 infiles = complete_state.saved_state.files
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001540 infiles[complete_state.isolated_filepath] = isolated_metadata
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001541
1542 if re.match(r'^https?://.+$', options.outdir):
maruel@chromium.orgc6f90062012-11-07 18:32:22 +00001543 isolateserver_archive.upload_sha1_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001544 base_url=options.outdir,
1545 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00001546 infiles=infiles,
1547 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001548 else:
1549 recreate_tree(
1550 outdir=options.outdir,
1551 indir=complete_state.root_dir,
1552 infiles=infiles,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001553 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001554 as_sha1=True)
1555 success = True
1556 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001557 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001558 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001559 if not success and os.path.isfile(options.isolated):
1560 os.remove(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001561
1562
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001563def CMDmerge(args):
1564 """Reads and merges the data from the trace back into the original .isolate.
1565
1566 Ignores --outdir.
1567 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001568 parser = OptionParserIsolate(command='merge', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001569 options, args = parser.parse_args(args)
1570 if args:
1571 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001572 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001573 merge(complete_state)
1574 return 0
1575
1576
1577def CMDread(args):
1578 """Reads the trace file generated with command 'trace'.
1579
1580 Ignores --outdir.
1581 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001582 parser = OptionParserIsolate(command='read', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001583 options, args = parser.parse_args(args)
1584 if args:
1585 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001586 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001587 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001588 pretty_print(value, sys.stdout)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001589 if exceptions:
1590 # It got an exception, raise the first one.
1591 raise \
1592 exceptions[0][0], \
1593 exceptions[0][1], \
1594 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001595 return 0
1596
1597
1598def CMDremap(args):
1599 """Creates a directory with all the dependencies mapped into it.
1600
1601 Useful to test manually why a test is failing. The target executable is not
1602 run.
1603 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001604 parser = OptionParserIsolate(command='remap', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001605 options, args = parser.parse_args(args)
1606 if args:
1607 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001608 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001609
1610 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001611 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001612 'isolate', complete_state.root_dir)
1613 else:
1614 if not os.path.isdir(options.outdir):
1615 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001616 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001617 if len(os.listdir(options.outdir)):
1618 raise ExecutionError('Can\'t remap in a non-empty directory')
1619 recreate_tree(
1620 outdir=options.outdir,
1621 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001622 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001623 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001624 as_sha1=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001625 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001626 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001627
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001628 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001629 complete_state.save_files()
1630 return 0
1631
1632
1633def CMDrun(args):
1634 """Runs the test executable in an isolated (temporary) directory.
1635
1636 All the dependencies are mapped into the temporary directory and the
1637 directory is cleaned up after the target exits. Warning: if -outdir is
1638 specified, it is deleted upon exit.
1639
1640 Argument processing stops at the first non-recognized argument and these
1641 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001642 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001643 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001644 parser = OptionParserIsolate(command='run', require_isolated=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001645 parser.enable_interspersed_args()
1646 options, args = parser.parse_args(args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001647 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001648 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001649 if not cmd:
1650 raise ExecutionError('No command to run')
1651 cmd = trace_inputs.fix_python_path(cmd)
1652 try:
1653 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001654 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001655 'isolate', complete_state.root_dir)
1656 else:
1657 if not os.path.isdir(options.outdir):
1658 os.makedirs(options.outdir)
1659 recreate_tree(
1660 outdir=options.outdir,
1661 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001662 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001663 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001664 as_sha1=False)
1665 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001666 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001667 if not os.path.isdir(cwd):
1668 # It can happen when no files are mapped from the directory containing the
1669 # .isolate file. But the directory must exist to be the current working
1670 # directory.
1671 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001672 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001673 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001674 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1675 result = subprocess.call(cmd, cwd=cwd)
1676 finally:
1677 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001678 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001679
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001680 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001681 complete_state.save_files()
1682 return result
1683
1684
1685def CMDtrace(args):
1686 """Traces the target using trace_inputs.py.
1687
1688 It runs the executable without remapping it, and traces all the files it and
1689 its child processes access. Then the 'read' command can be used to generate an
1690 updated .isolate file out of it.
1691
1692 Argument processing stops at the first non-recognized argument and these
1693 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001694 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001695 """
1696 parser = OptionParserIsolate(command='trace')
1697 parser.enable_interspersed_args()
1698 parser.add_option(
1699 '-m', '--merge', action='store_true',
1700 help='After tracing, merge the results back in the .isolate file')
1701 options, args = parser.parse_args(args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001702 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001703 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001704 if not cmd:
1705 raise ExecutionError('No command to run')
1706 cmd = trace_inputs.fix_python_path(cmd)
1707 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001708 unicode(complete_state.root_dir),
1709 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001710 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1711 if not os.path.isfile(cmd[0]):
1712 raise ExecutionError(
1713 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001714 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1715 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001716 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001717 api.clean_trace(logfile)
1718 try:
1719 with api.get_tracer(logfile) as tracer:
1720 result, _ = tracer.trace(
1721 cmd,
1722 cwd,
1723 'default',
1724 True)
1725 except trace_inputs.TracingFailure, e:
1726 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1727
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001728 if result:
1729 logging.error('Tracer exited with %d, which means the tests probably '
1730 'failed so the trace is probably incomplete.', result)
1731
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001732 complete_state.save_files()
1733
1734 if options.merge:
1735 merge(complete_state)
1736
1737 return result
1738
1739
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001740def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001741 """Adds --isolated and --variable to an OptionParser."""
1742 parser.add_option(
1743 '-s', '--isolated',
1744 metavar='FILE',
1745 help='.isolated file to generate or read')
1746 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001747 parser.add_option(
1748 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001749 dest='isolated',
1750 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001751 default_variables = [('OS', get_flavor())]
1752 if sys.platform in ('win32', 'cygwin'):
1753 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
1754 else:
1755 default_variables.append(('EXECUTABLE_SUFFIX', ''))
1756 parser.add_option(
1757 '-V', '--variable',
1758 nargs=2,
1759 action='append',
1760 default=default_variables,
1761 dest='variables',
1762 metavar='FOO BAR',
1763 help='Variables to process in the .isolate file, default: %default. '
1764 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001765 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001766
1767
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001768def parse_variable_option(parser, options, cwd, require_isolated):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001769 """Processes --isolated and --variable."""
1770 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001771 options.isolated = os.path.normpath(
1772 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001773 if require_isolated and not options.isolated:
csharp@chromium.org707f0452012-11-26 21:50:40 +00001774 parser.error('--isolated is required. Visit http://chromium.org/developers/'
1775 'testing/isolated-testing#TOC-Where-can-I-find-the-.isolated-'
1776 'file- to see how to create the .isolated file.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001777 if options.isolated and not options.isolated.endswith('.isolated'):
1778 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001779 options.variables = dict(options.variables)
1780
1781
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001782class OptionParserIsolate(trace_inputs.OptionParserWithNiceDescription):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001783 """Adds automatic --isolate, --isolated, --out and --variable handling."""
1784 def __init__(self, require_isolated=True, **kwargs):
maruel@chromium.org55276902012-10-05 20:56:19 +00001785 trace_inputs.OptionParserWithNiceDescription.__init__(
1786 self,
1787 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1788 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001789 group = optparse.OptionGroup(self, "Common options")
1790 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001791 '-i', '--isolate',
1792 metavar='FILE',
1793 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001794 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001795 group.add_option(
1796 '-o', '--outdir', metavar='DIR',
1797 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00001798 'Defaults: run|remap: a /tmp subdirectory, others: '
1799 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00001800 group.add_option(
1801 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00001802 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1803 help='Indicates that invalid entries in the isolated file to be '
1804 'only be logged and not stop processing. Defaults to True if '
1805 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001806 self.add_option_group(group)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001807 self.require_isolated = require_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001808
1809 def parse_args(self, *args, **kwargs):
1810 """Makes sure the paths make sense.
1811
1812 On Windows, / and \ are often mixed together in a path.
1813 """
1814 options, args = trace_inputs.OptionParserWithNiceDescription.parse_args(
1815 self, *args, **kwargs)
1816 if not self.allow_interspersed_args and args:
1817 self.error('Unsupported argument: %s' % args)
1818
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001819 cwd = os.getcwd()
1820 parse_variable_option(self, options, cwd, self.require_isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001821
1822 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001823 # TODO(maruel): Work with non-ASCII.
1824 # The path must be in native path case for tracing purposes.
1825 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1826 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
1827 options.isolate = trace_inputs.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001828
1829 if options.outdir and not re.match(r'^https?://.+$', options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001830 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1831 # outdir doesn't need native path case since tracing is never done from
1832 # there.
1833 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001834
1835 return options, args
1836
1837
1838### Glue code to make all the commands works magically.
1839
1840
1841CMDhelp = trace_inputs.CMDhelp
1842
1843
1844def main(argv):
1845 try:
1846 return trace_inputs.main_impl(argv)
1847 except (
1848 ExecutionError,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001849 run_isolated.MappingError,
1850 run_isolated.ConfigError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001851 sys.stderr.write('\nError: ')
1852 sys.stderr.write(str(e))
1853 sys.stderr.write('\n')
1854 return 1
1855
1856
1857if __name__ == '__main__':
1858 sys.exit(main(sys.argv[1:]))