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