blob: 66d0b6181318ef35d6a7d212b118a02f3853340d [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
benrg@chromium.org609b7982013-02-07 16:44:46 +000014import ast
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000015import copy
16import hashlib
benrg@chromium.org609b7982013-02-07 16:44:46 +000017import itertools
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000018import logging
19import optparse
20import os
21import posixpath
22import re
23import stat
24import subprocess
25import sys
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000026
maruel@chromium.orgc6f90062012-11-07 18:32:22 +000027import isolateserver_archive
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000028import run_isolated
benrg@chromium.org609b7982013-02-07 16:44:46 +000029import short_expression_finder
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000030import trace_inputs
31
32# Import here directly so isolate is easier to use as a library.
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000033from run_isolated import get_flavor
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000034
35
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000036PATH_VARIABLES = ('DEPTH', 'PRODUCT_DIR')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000037
38# Files that should be 0-length when mapped.
39KEY_TOUCHED = 'isolate_dependency_touched'
40# Files that should be tracked by the build tool.
41KEY_TRACKED = 'isolate_dependency_tracked'
42# Files that should not be tracked by the build tool.
43KEY_UNTRACKED = 'isolate_dependency_untracked'
44
45_GIT_PATH = os.path.sep + '.git'
46_SVN_PATH = os.path.sep + '.svn'
47
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000048
49class ExecutionError(Exception):
50 """A generic error occurred."""
51 def __str__(self):
52 return self.args[0]
53
54
55### Path handling code.
56
57
58def relpath(path, root):
59 """os.path.relpath() that keeps trailing os.path.sep."""
60 out = os.path.relpath(path, root)
61 if path.endswith(os.path.sep):
62 out += os.path.sep
63 return out
64
65
66def normpath(path):
67 """os.path.normpath() that keeps trailing os.path.sep."""
68 out = os.path.normpath(path)
69 if path.endswith(os.path.sep):
70 out += os.path.sep
71 return out
72
73
74def posix_relpath(path, root):
75 """posix.relpath() that keeps trailing slash."""
76 out = posixpath.relpath(path, root)
77 if path.endswith('/'):
78 out += '/'
79 return out
80
81
82def cleanup_path(x):
83 """Cleans up a relative path. Converts any os.path.sep to '/' on Windows."""
84 if x:
85 x = x.rstrip(os.path.sep).replace(os.path.sep, '/')
86 if x == '.':
87 x = ''
88 if x:
89 x += '/'
90 return x
91
92
maruel@chromium.orgb9520b02013-03-13 18:00:03 +000093def is_url(path):
94 return bool(re.match(r'^https?://.+$', path))
95
96
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000097def default_blacklist(f):
98 """Filters unimportant files normally ignored."""
99 return (
100 f.endswith(('.pyc', '.run_test_cases', 'testserver.log')) or
101 _GIT_PATH in f or
102 _SVN_PATH in f or
103 f in ('.git', '.svn'))
104
105
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000106def path_starts_with(prefix, path):
107 """Returns true if the components of the path |prefix| are the same as the
108 initial components of |path| (or all of the components of |path|). The paths
109 must be absolute.
110 """
111 assert os.path.isabs(prefix) and os.path.isabs(path)
112 prefix = os.path.normpath(prefix)
113 path = os.path.normpath(path)
114 assert prefix == trace_inputs.get_native_path_case(prefix), prefix
115 assert path == trace_inputs.get_native_path_case(path), path
116 prefix = prefix.rstrip(os.path.sep) + os.path.sep
117 path = path.rstrip(os.path.sep) + os.path.sep
118 return path.startswith(prefix)
119
120
121def expand_symlinks(indir, relfile):
122 """Follows symlinks in |relfile|, but treating symlinks that point outside the
123 build tree as if they were ordinary directories/files. Returns the final
124 symlink-free target and a list of paths to symlinks encountered in the
125 process.
126
127 The rule about symlinks outside the build tree is for the benefit of the
128 Chromium OS ebuild, which symlinks the output directory to an unrelated path
129 in the chroot.
130
131 Fails when a directory loop is detected, although in theory we could support
132 that case.
133 """
134 if sys.platform == 'win32':
135 return relfile, []
136
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000137 is_directory = relfile.endswith(os.path.sep)
138 done = indir
139 todo = relfile.strip(os.path.sep)
140 symlinks = []
141
142 while todo:
143 pre_symlink, symlink, post_symlink = trace_inputs.split_at_symlink(
144 done, todo)
145 if not symlink:
146 done = os.path.join(done, todo)
147 break
148 symlink_path = os.path.join(done, pre_symlink, symlink)
149 post_symlink = post_symlink.lstrip(os.path.sep)
150 # readlink doesn't exist on Windows.
151 # pylint: disable=E1101
152 target = os.readlink(symlink_path)
153 target = os.path.normpath(os.path.join(done, pre_symlink, target))
154 if not os.path.exists(target):
155 raise run_isolated.MappingError(
156 'Symlink target doesn\'t exist: %s -> %s' % (symlink_path, target))
157 target = trace_inputs.get_native_path_case(target)
158 if not path_starts_with(indir, target):
159 done = symlink_path
160 todo = post_symlink
161 continue
162 if path_starts_with(target, symlink_path):
163 raise run_isolated.MappingError(
164 'Can\'t map recursive symlink reference %s -> %s' %
165 (symlink_path, target))
166 logging.info('Found symlink: %s -> %s', symlink_path, target)
167 symlinks.append(os.path.relpath(symlink_path, indir))
168 # Treat the common prefix of the old and new paths as done, and start
169 # scanning again.
170 target = target.split(os.path.sep)
171 symlink_path = symlink_path.split(os.path.sep)
172 prefix_length = 0
173 for target_piece, symlink_path_piece in zip(target, symlink_path):
174 if target_piece == symlink_path_piece:
175 prefix_length += 1
176 else:
177 break
178 done = os.path.sep.join(target[:prefix_length])
179 todo = os.path.join(
180 os.path.sep.join(target[prefix_length:]), post_symlink)
181
182 relfile = os.path.relpath(done, indir)
183 relfile = relfile.rstrip(os.path.sep) + is_directory * os.path.sep
184 return relfile, symlinks
185
186
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000187def expand_directory_and_symlink(indir, relfile, blacklist):
188 """Expands a single input. It can result in multiple outputs.
189
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000190 This function is recursive when relfile is a directory.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000191
192 Note: this code doesn't properly handle recursive symlink like one created
193 with:
194 ln -s .. foo
195 """
196 if os.path.isabs(relfile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000197 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000198 'Can\'t map absolute path %s' % relfile)
199
200 infile = normpath(os.path.join(indir, relfile))
201 if not infile.startswith(indir):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000202 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000203 'Can\'t map file %s outside %s' % (infile, indir))
204
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000205 filepath = os.path.join(indir, relfile)
206 native_filepath = trace_inputs.get_native_path_case(filepath)
207 if filepath != native_filepath:
208 raise run_isolated.MappingError('File path doesn\'t equal native file '
209 'path\n%s!=%s' % (filepath,
210 native_filepath))
211
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000212 relfile, symlinks = expand_symlinks(indir, relfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000213
214 if relfile.endswith(os.path.sep):
215 if not os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000216 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000217 '%s is not a directory but ends with "%s"' % (infile, os.path.sep))
218
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000219 outfiles = symlinks
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000220 try:
221 for filename in os.listdir(infile):
222 inner_relfile = os.path.join(relfile, filename)
223 if blacklist(inner_relfile):
224 continue
225 if os.path.isdir(os.path.join(indir, inner_relfile)):
226 inner_relfile += os.path.sep
227 outfiles.extend(
228 expand_directory_and_symlink(indir, inner_relfile, blacklist))
229 return outfiles
230 except OSError as e:
231 raise run_isolated.MappingError('Unable to iterate over directories.\n'
232 '%s' % e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000233 else:
234 # Always add individual files even if they were blacklisted.
235 if os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000236 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000237 'Input directory %s must have a trailing slash' % infile)
238
239 if not os.path.isfile(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000240 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000241 'Input file %s doesn\'t exist' % infile)
242
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000243 return symlinks + [relfile]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000244
245
csharp@chromium.org01856802012-11-12 17:48:13 +0000246def expand_directories_and_symlinks(indir, infiles, blacklist,
247 ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000248 """Expands the directories and the symlinks, applies the blacklist and
249 verifies files exist.
250
251 Files are specified in os native path separator.
252 """
253 outfiles = []
254 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +0000255 try:
256 outfiles.extend(expand_directory_and_symlink(indir, relfile, blacklist))
257 except run_isolated.MappingError as e:
258 if ignore_broken_items:
259 logging.info('warning: %s', e)
260 else:
261 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000262 return outfiles
263
264
265def recreate_tree(outdir, indir, infiles, action, as_sha1):
266 """Creates a new tree with only the input files in it.
267
268 Arguments:
269 outdir: Output directory to create the files in.
270 indir: Root directory the infiles are based in.
271 infiles: dict of files to map from |indir| to |outdir|.
272 action: See assert below.
273 as_sha1: Output filename is the sha1 instead of relfile.
274 """
275 logging.info(
276 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_sha1=%s)' %
277 (outdir, indir, len(infiles), action, as_sha1))
278
279 assert action in (
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000280 run_isolated.HARDLINK,
281 run_isolated.SYMLINK,
282 run_isolated.COPY)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000283 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000284 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000285 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000286 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000287
288 for relfile, metadata in infiles.iteritems():
289 infile = os.path.join(indir, relfile)
290 if as_sha1:
291 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000292 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000293 # Skip links when storing a hashtable.
294 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000295 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000296 if os.path.isfile(outfile):
297 # Just do a quick check that the file size matches. No need to stat()
298 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000299 if not 's' in metadata:
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000300 raise run_isolated.MappingError(
301 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000302 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000303 continue
304 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000305 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000306 os.remove(outfile)
307 else:
308 outfile = os.path.join(outdir, relfile)
309 outsubdir = os.path.dirname(outfile)
310 if not os.path.isdir(outsubdir):
311 os.makedirs(outsubdir)
312
313 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000314 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000315 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000316 if 'l' in metadata:
317 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000318 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000319 # symlink doesn't exist on Windows.
320 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000321 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000322 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000323
324
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000325def process_input(filepath, prevdict, read_only):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000326 """Processes an input file, a dependency, and return meta data about it.
327
328 Arguments:
329 - filepath: File to act on.
330 - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
331 to skip recalculating the hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000332 - read_only: If True, the file mode is manipulated. In practice, only save
333 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
334 windows, mode is not set since all files are 'executable' by
335 default.
336
337 Behaviors:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000338 - Retrieves the file mode, file size, file timestamp, file link
339 destination if it is a file link and calcultate the SHA-1 of the file's
340 content if the path points to a file and not a symlink.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000341 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000342 out = {}
343 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000344 # if prevdict.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000345 # # The file's content is ignored. Skip the time and hard code mode.
346 # if get_flavor() != 'win':
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000347 # out['m'] = stat.S_IRUSR | stat.S_IRGRP
348 # out['s'] = 0
349 # out['h'] = SHA_1_NULL
350 # out['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000351 # return out
352
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000353 # Always check the file stat and check if it is a link. The timestamp is used
354 # to know if the file's content/symlink destination should be looked into.
355 # E.g. only reuse from prevdict if the timestamp hasn't changed.
356 # There is the risk of the file's timestamp being reset to its last value
357 # manually while its content changed. We don't protect against that use case.
358 try:
359 filestats = os.lstat(filepath)
360 except OSError:
361 # The file is not present.
362 raise run_isolated.MappingError('%s is missing' % filepath)
363 is_link = stat.S_ISLNK(filestats.st_mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000364
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000365 if get_flavor() != 'win':
366 # Ignore file mode on Windows since it's not really useful there.
367 filemode = stat.S_IMODE(filestats.st_mode)
368 # Remove write access for group and all access to 'others'.
369 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
370 if read_only:
371 filemode &= ~stat.S_IWUSR
372 if filemode & stat.S_IXUSR:
373 filemode |= stat.S_IXGRP
374 else:
375 filemode &= ~stat.S_IXGRP
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000376 out['m'] = filemode
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000377
378 # Used to skip recalculating the hash or link destination. Use the most recent
379 # update time.
380 # TODO(maruel): Save it in the .state file instead of .isolated so the
381 # .isolated file is deterministic.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000382 out['t'] = int(round(filestats.st_mtime))
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000383
384 if not is_link:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000385 out['s'] = filestats.st_size
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000386 # If the timestamp wasn't updated and the file size is still the same, carry
387 # on the sha-1.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000388 if (prevdict.get('t') == out['t'] and
389 prevdict.get('s') == out['s']):
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000390 # Reuse the previous hash if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000391 out['h'] = prevdict.get('h')
392 if not out.get('h'):
maruel@chromium.org6da38772012-12-11 21:36:37 +0000393 out['h'] = isolateserver_archive.sha1_file(filepath)
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000394 else:
395 # If the timestamp wasn't updated, carry on the link destination.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000396 if prevdict.get('t') == out['t']:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000397 # Reuse the previous link destination if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000398 out['l'] = prevdict.get('l')
399 if out.get('l') is None:
400 out['l'] = os.readlink(filepath) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000401 return out
402
403
404### Variable stuff.
405
406
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000407def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000408 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000409 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000410
411
412def determine_root_dir(relative_root, infiles):
413 """For a list of infiles, determines the deepest root directory that is
414 referenced indirectly.
415
416 All arguments must be using os.path.sep.
417 """
418 # The trick used to determine the root directory is to look at "how far" back
419 # up it is looking up.
420 deepest_root = relative_root
421 for i in infiles:
422 x = relative_root
423 while i.startswith('..' + os.path.sep):
424 i = i[3:]
425 assert not i.startswith(os.path.sep)
426 x = os.path.dirname(x)
427 if deepest_root.startswith(x):
428 deepest_root = x
429 logging.debug(
430 'determine_root_dir(%s, %d files) -> %s' % (
431 relative_root, len(infiles), deepest_root))
432 return deepest_root
433
434
435def replace_variable(part, variables):
436 m = re.match(r'<\(([A-Z_]+)\)', part)
437 if m:
438 if m.group(1) not in variables:
439 raise ExecutionError(
440 'Variable "%s" was not found in %s.\nDid you forget to specify '
441 '--variable?' % (m.group(1), variables))
442 return variables[m.group(1)]
443 return part
444
445
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000446def process_variables(cwd, variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000447 """Processes path variables as a special case and returns a copy of the dict.
448
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000449 For each 'path' variable: first normalizes it based on |cwd|, verifies it
450 exists then sets it as relative to relative_base_dir.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000451 """
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000452 relative_base_dir = trace_inputs.get_native_path_case(relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000453 variables = variables.copy()
454 for i in PATH_VARIABLES:
455 if i not in variables:
456 continue
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000457 variable = variables[i].strip()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000458 # Variables could contain / or \ on windows. Always normalize to
459 # os.path.sep.
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000460 variable = variable.replace('/', os.path.sep)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000461 variable = os.path.join(cwd, variable)
462 variable = os.path.normpath(variable)
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000463 variable = trace_inputs.get_native_path_case(variable)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000464 if not os.path.isdir(variable):
465 raise ExecutionError('%s=%s is not a directory' % (i, variable))
466
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000467 # All variables are relative to the .isolate file.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000468 variable = os.path.relpath(variable, relative_base_dir)
469 logging.debug(
470 'Translated variable %s from %s to %s', i, variables[i], variable)
471 variables[i] = variable
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000472 return variables
473
474
475def eval_variables(item, variables):
476 """Replaces the .isolate variables in a string item.
477
478 Note that the .isolate format is a subset of the .gyp dialect.
479 """
480 return ''.join(
481 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
482
483
484def classify_files(root_dir, tracked, untracked):
485 """Converts the list of files into a .isolate 'variables' dictionary.
486
487 Arguments:
488 - tracked: list of files names to generate a dictionary out of that should
489 probably be tracked.
490 - untracked: list of files names that must not be tracked.
491 """
492 # These directories are not guaranteed to be always present on every builder.
493 OPTIONAL_DIRECTORIES = (
494 'test/data/plugin',
495 'third_party/WebKit/LayoutTests',
496 )
497
498 new_tracked = []
499 new_untracked = list(untracked)
500
501 def should_be_tracked(filepath):
502 """Returns True if it is a file without whitespace in a non-optional
503 directory that has no symlink in its path.
504 """
505 if filepath.endswith('/'):
506 return False
507 if ' ' in filepath:
508 return False
509 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
510 return False
511 # Look if any element in the path is a symlink.
512 split = filepath.split('/')
513 for i in range(len(split)):
514 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
515 return False
516 return True
517
518 for filepath in sorted(tracked):
519 if should_be_tracked(filepath):
520 new_tracked.append(filepath)
521 else:
522 # Anything else.
523 new_untracked.append(filepath)
524
525 variables = {}
526 if new_tracked:
527 variables[KEY_TRACKED] = sorted(new_tracked)
528 if new_untracked:
529 variables[KEY_UNTRACKED] = sorted(new_untracked)
530 return variables
531
532
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000533def chromium_fix(f, variables):
534 """Fixes an isolate dependnecy with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000535 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
536 # separator.
537 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000538 # Ignored items.
539 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000540 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000541 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000542 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000543 # 'First Run' is not created by the compile, but by the test itself.
544 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000545
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000546 # Blacklist logs and other unimportant files.
547 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
548 logging.debug('Ignoring %s', f)
549 return None
550
maruel@chromium.org7650e422012-11-16 21:56:42 +0000551 EXECUTABLE = re.compile(
552 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
553 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
554 r'$')
555 match = EXECUTABLE.match(f)
556 if match:
557 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
558
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000559 if sys.platform == 'darwin':
560 # On OSX, the name of the output is dependent on gyp define, it can be
561 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
562 # Framework.framework'. Furthermore, they are versioned with a gyp
563 # variable. To lower the complexity of the .isolate file, remove all the
564 # individual entries that show up under any of the 4 entries and replace
565 # them with the directory itself. Overall, this results in a bit more
566 # files than strictly necessary.
567 OSX_BUNDLES = (
568 '<(PRODUCT_DIR)/Chromium Framework.framework/',
569 '<(PRODUCT_DIR)/Chromium.app/',
570 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
571 '<(PRODUCT_DIR)/Google Chrome.app/',
572 )
573 for prefix in OSX_BUNDLES:
574 if f.startswith(prefix):
575 # Note this result in duplicate values, so the a set() must be used to
576 # remove duplicates.
577 return prefix
578 return f
579
580
581def generate_simplified(
582 tracked, untracked, touched, root_dir, variables, relative_cwd):
583 """Generates a clean and complete .isolate 'variables' dictionary.
584
585 Cleans up and extracts only files from within root_dir then processes
586 variables and relative_cwd.
587 """
588 root_dir = os.path.realpath(root_dir)
589 logging.info(
590 'generate_simplified(%d files, %s, %s, %s)' %
591 (len(tracked) + len(untracked) + len(touched),
592 root_dir, variables, relative_cwd))
593
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000594 # Preparation work.
595 relative_cwd = cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000596 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000597 # Creates the right set of variables here. We only care about PATH_VARIABLES.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000598 path_variables = dict(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000599 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
600 for k in PATH_VARIABLES if k in variables)
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000601 variables = variables.copy()
602 variables.update(path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000603
604 # Actual work: Process the files.
605 # TODO(maruel): if all the files in a directory are in part tracked and in
606 # part untracked, the directory will not be extracted. Tracked files should be
607 # 'promoted' to be untracked as needed.
608 tracked = trace_inputs.extract_directories(
609 root_dir, tracked, default_blacklist)
610 untracked = trace_inputs.extract_directories(
611 root_dir, untracked, default_blacklist)
612 # touched is not compressed, otherwise it would result in files to be archived
613 # that we don't need.
614
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000615 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000616 def fix(f):
617 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000618 # Important, GYP stores the files with / and not \.
619 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000620 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000621 # If it's not already a variable.
622 if not f.startswith('<'):
623 # relative_cwd is usually the directory containing the gyp file. It may be
624 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000625 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000626 # Convert the whole thing to / since it's isolate's speak.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000627 f = posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000628 posixpath.join(root_dir_posix, f),
629 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000630
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000631 for variable, root_path in path_variables.iteritems():
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000632 if f.startswith(root_path):
633 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000634 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000635 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000636 return f
637
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000638 def fix_all(items):
639 """Reduces the items to convert variables, removes unneeded items, apply
640 chromium-specific fixes and only return unique items.
641 """
642 variables_converted = (fix(f.path) for f in items)
643 chromium_fixed = (chromium_fix(f, variables) for f in variables_converted)
644 return set(f for f in chromium_fixed if f)
645
646 tracked = fix_all(tracked)
647 untracked = fix_all(untracked)
648 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000649 out = classify_files(root_dir, tracked, untracked)
650 if touched:
651 out[KEY_TOUCHED] = sorted(touched)
652 return out
653
654
benrg@chromium.org609b7982013-02-07 16:44:46 +0000655def chromium_filter_flags(variables):
656 """Filters out build flags used in Chromium that we don't want to treat as
657 configuration variables.
658 """
659 # TODO(benrg): Need a better way to determine this.
660 blacklist = set(PATH_VARIABLES + ('EXECUTABLE_SUFFIX', 'FLAG'))
661 return dict((k, v) for k, v in variables.iteritems() if k not in blacklist)
662
663
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000664def generate_isolate(
665 tracked, untracked, touched, root_dir, variables, relative_cwd):
666 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000667 dependencies = generate_simplified(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000668 tracked, untracked, touched, root_dir, variables, relative_cwd)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000669 config_variables = chromium_filter_flags(variables)
670 config_variable_names, config_values = zip(
671 *sorted(config_variables.iteritems()))
672 out = Configs(None)
673 # The new dependencies apply to just one configuration, namely config_values.
674 out.merge_dependencies(dependencies, config_variable_names, [config_values])
675 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000676
677
678def split_touched(files):
679 """Splits files that are touched vs files that are read."""
680 tracked = []
681 touched = []
682 for f in files:
683 if f.size:
684 tracked.append(f)
685 else:
686 touched.append(f)
687 return tracked, touched
688
689
690def pretty_print(variables, stdout):
691 """Outputs a gyp compatible list from the decoded variables.
692
693 Similar to pprint.print() but with NIH syndrome.
694 """
695 # Order the dictionary keys by these keys in priority.
696 ORDER = (
697 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
698 KEY_TRACKED, KEY_UNTRACKED)
699
700 def sorting_key(x):
701 """Gives priority to 'most important' keys before the others."""
702 if x in ORDER:
703 return str(ORDER.index(x))
704 return x
705
706 def loop_list(indent, items):
707 for item in items:
708 if isinstance(item, basestring):
709 stdout.write('%s\'%s\',\n' % (indent, item))
710 elif isinstance(item, dict):
711 stdout.write('%s{\n' % indent)
712 loop_dict(indent + ' ', item)
713 stdout.write('%s},\n' % indent)
714 elif isinstance(item, list):
715 # A list inside a list will write the first item embedded.
716 stdout.write('%s[' % indent)
717 for index, i in enumerate(item):
718 if isinstance(i, basestring):
719 stdout.write(
720 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
721 elif isinstance(i, dict):
722 stdout.write('{\n')
723 loop_dict(indent + ' ', i)
724 if index != len(item) - 1:
725 x = ', '
726 else:
727 x = ''
728 stdout.write('%s}%s' % (indent, x))
729 else:
730 assert False
731 stdout.write('],\n')
732 else:
733 assert False
734
735 def loop_dict(indent, items):
736 for key in sorted(items, key=sorting_key):
737 item = items[key]
738 stdout.write("%s'%s': " % (indent, key))
739 if isinstance(item, dict):
740 stdout.write('{\n')
741 loop_dict(indent + ' ', item)
742 stdout.write(indent + '},\n')
743 elif isinstance(item, list):
744 stdout.write('[\n')
745 loop_list(indent + ' ', item)
746 stdout.write(indent + '],\n')
747 elif isinstance(item, basestring):
748 stdout.write(
749 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
750 elif item in (True, False, None):
751 stdout.write('%s\n' % item)
752 else:
753 assert False, item
754
755 stdout.write('{\n')
756 loop_dict(' ', variables)
757 stdout.write('}\n')
758
759
760def union(lhs, rhs):
761 """Merges two compatible datastructures composed of dict/list/set."""
762 assert lhs is not None or rhs is not None
763 if lhs is None:
764 return copy.deepcopy(rhs)
765 if rhs is None:
766 return copy.deepcopy(lhs)
767 assert type(lhs) == type(rhs), (lhs, rhs)
768 if hasattr(lhs, 'union'):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000769 # Includes set, ConfigSettings and Configs.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000770 return lhs.union(rhs)
771 if isinstance(lhs, dict):
772 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
773 elif isinstance(lhs, list):
774 # Do not go inside the list.
775 return lhs + rhs
776 assert False, type(lhs)
777
778
779def extract_comment(content):
780 """Extracts file level comment."""
781 out = []
782 for line in content.splitlines(True):
783 if line.startswith('#'):
784 out.append(line)
785 else:
786 break
787 return ''.join(out)
788
789
790def eval_content(content):
791 """Evaluates a python file and return the value defined in it.
792
793 Used in practice for .isolate files.
794 """
795 globs = {'__builtins__': None}
796 locs = {}
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000797 try:
798 value = eval(content, globs, locs)
799 except TypeError as e:
800 e.args = list(e.args) + [content]
801 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000802 assert locs == {}, locs
803 assert globs == {'__builtins__': None}, globs
804 return value
805
806
benrg@chromium.org609b7982013-02-07 16:44:46 +0000807def match_configs(expr, config_variables, all_configs):
808 """Returns the configs from |all_configs| that match the |expr|, where
809 the elements of |all_configs| are tuples of values for the |config_variables|.
810 Example:
811 >>> match_configs(expr = "(foo==1 or foo==2) and bar=='b'",
812 config_variables = ["foo", "bar"],
813 all_configs = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
814 [(1, 'b'), (2, 'b')]
815 """
816 return [
817 config for config in all_configs
818 if eval(expr, dict(zip(config_variables, config)))
819 ]
820
821
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000822def verify_variables(variables):
823 """Verifies the |variables| dictionary is in the expected format."""
824 VALID_VARIABLES = [
825 KEY_TOUCHED,
826 KEY_TRACKED,
827 KEY_UNTRACKED,
828 'command',
829 'read_only',
830 ]
831 assert isinstance(variables, dict), variables
832 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
833 for name, value in variables.iteritems():
834 if name == 'read_only':
835 assert value in (True, False, None), value
836 else:
837 assert isinstance(value, list), value
838 assert all(isinstance(i, basestring) for i in value), value
839
840
benrg@chromium.org609b7982013-02-07 16:44:46 +0000841def verify_ast(expr, variables_and_values):
842 """Verifies that |expr| is of the form
843 expr ::= expr ( "or" | "and" ) expr
844 | identifier "==" ( string | int )
845 Also collects the variable identifiers and string/int values in the dict
846 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
847 """
848 assert isinstance(expr, (ast.BoolOp, ast.Compare))
849 if isinstance(expr, ast.BoolOp):
850 assert isinstance(expr.op, (ast.And, ast.Or))
851 for subexpr in expr.values:
852 verify_ast(subexpr, variables_and_values)
853 else:
854 assert isinstance(expr.left.ctx, ast.Load)
855 assert len(expr.ops) == 1
856 assert isinstance(expr.ops[0], ast.Eq)
857 var_values = variables_and_values.setdefault(expr.left.id, set())
858 rhs = expr.comparators[0]
859 assert isinstance(rhs, (ast.Str, ast.Num))
860 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
861
862
863def verify_condition(condition, variables_and_values):
864 """Verifies the |condition| dictionary is in the expected format.
865 See verify_ast() for the meaning of |variables_and_values|.
866 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000867 VALID_INSIDE_CONDITION = ['variables']
868 assert isinstance(condition, list), condition
benrg@chromium.org609b7982013-02-07 16:44:46 +0000869 assert len(condition) == 2, condition
870 expr, then = condition
871
872 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
873 verify_ast(test_ast.body, variables_and_values)
874
875 assert isinstance(then, dict), then
876 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
877 verify_variables(then['variables'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000878
879
benrg@chromium.org609b7982013-02-07 16:44:46 +0000880def verify_root(value, variables_and_values):
881 """Verifies that |value| is the parsed form of a valid .isolate file.
882 See verify_ast() for the meaning of |variables_and_values|.
883 """
884 VALID_ROOTS = ['includes', 'conditions']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000885 assert isinstance(value, dict), value
886 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000887
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000888 includes = value.get('includes', [])
889 assert isinstance(includes, list), includes
890 for include in includes:
891 assert isinstance(include, basestring), include
892
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000893 conditions = value.get('conditions', [])
894 assert isinstance(conditions, list), conditions
895 for condition in conditions:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000896 verify_condition(condition, variables_and_values)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000897
898
benrg@chromium.org609b7982013-02-07 16:44:46 +0000899def remove_weak_dependencies(values, key, item, item_configs):
900 """Removes any configs from this key if the item is already under a
901 strong key.
902 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000903 if key == KEY_TOUCHED:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000904 item_configs = set(item_configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000905 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000906 try:
907 item_configs -= values[stronger_key][item]
908 except KeyError:
909 pass
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000910
benrg@chromium.org609b7982013-02-07 16:44:46 +0000911 return item_configs
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000912
913
benrg@chromium.org609b7982013-02-07 16:44:46 +0000914def remove_repeated_dependencies(folders, key, item, item_configs):
915 """Removes any configs from this key if the item is in a folder that is
916 already included."""
csharp@chromium.org31176252012-11-02 13:04:40 +0000917
918 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000919 item_configs = set(item_configs)
920 for (folder, configs) in folders.iteritems():
csharp@chromium.org31176252012-11-02 13:04:40 +0000921 if folder != item and item.startswith(folder):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000922 item_configs -= configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000923
benrg@chromium.org609b7982013-02-07 16:44:46 +0000924 return item_configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000925
926
927def get_folders(values_dict):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000928 """Returns a dict of all the folders in the given value_dict."""
929 return dict(
930 (item, configs) for (item, configs) in values_dict.iteritems()
931 if item.endswith('/')
932 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000933
934
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000935def invert_map(variables):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000936 """Converts {config: {deptype: list(depvals)}} to
937 {deptype: {depval: set(configs)}}.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000938 """
939 KEYS = (
940 KEY_TOUCHED,
941 KEY_TRACKED,
942 KEY_UNTRACKED,
943 'command',
944 'read_only',
945 )
946 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000947 for config, values in variables.iteritems():
948 for key in KEYS:
949 if key == 'command':
950 items = [tuple(values[key])] if key in values else []
951 elif key == 'read_only':
952 items = [values[key]] if key in values else []
953 else:
954 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
955 items = values.get(key, [])
956 for item in items:
957 out[key].setdefault(item, set()).add(config)
958 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000959
960
benrg@chromium.org609b7982013-02-07 16:44:46 +0000961def reduce_inputs(values):
962 """Reduces the output of invert_map() to the strictest minimum list.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000963
benrg@chromium.org609b7982013-02-07 16:44:46 +0000964 Looks at each individual file and directory, maps where they are used and
965 reconstructs the inverse dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000966
benrg@chromium.org609b7982013-02-07 16:44:46 +0000967 Returns the minimized dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000968 """
969 KEYS = (
970 KEY_TOUCHED,
971 KEY_TRACKED,
972 KEY_UNTRACKED,
973 'command',
974 'read_only',
975 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000976
977 # Folders can only live in KEY_UNTRACKED.
978 folders = get_folders(values.get(KEY_UNTRACKED, {}))
979
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000980 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000981 for key in KEYS:
982 for item, item_configs in values.get(key, {}).iteritems():
983 item_configs = remove_weak_dependencies(values, key, item, item_configs)
984 item_configs = remove_repeated_dependencies(
985 folders, key, item, item_configs)
986 if item_configs:
987 out[key][item] = item_configs
988 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000989
990
benrg@chromium.org609b7982013-02-07 16:44:46 +0000991def convert_map_to_isolate_dict(values, config_variables):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000992 """Regenerates back a .isolate configuration dict from files and dirs
993 mappings generated from reduce_inputs().
994 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000995 # Gather a list of configurations for set inversion later.
996 all_mentioned_configs = set()
997 for configs_by_item in values.itervalues():
998 for configs in configs_by_item.itervalues():
999 all_mentioned_configs.update(configs)
1000
1001 # Invert the mapping to make it dict first.
1002 conditions = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001003 for key in values:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001004 for item, configs in values[key].iteritems():
1005 then = conditions.setdefault(frozenset(configs), {})
1006 variables = then.setdefault('variables', {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001007
benrg@chromium.org609b7982013-02-07 16:44:46 +00001008 if item in (True, False):
1009 # One-off for read_only.
1010 variables[key] = item
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001011 else:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001012 assert item
1013 if isinstance(item, tuple):
1014 # One-off for command.
1015 # Do not merge lists and do not sort!
1016 # Note that item is a tuple.
1017 assert key not in variables
1018 variables[key] = list(item)
1019 else:
1020 # The list of items (files or dirs). Append the new item and keep
1021 # the list sorted.
1022 l = variables.setdefault(key, [])
1023 l.append(item)
1024 l.sort()
1025
1026 if all_mentioned_configs:
1027 config_values = map(set, zip(*all_mentioned_configs))
1028 sef = short_expression_finder.ShortExpressionFinder(
1029 zip(config_variables, config_values))
1030
1031 conditions = sorted(
1032 [sef.get_expr(configs), then] for configs, then in conditions.iteritems())
1033 return {'conditions': conditions}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001034
1035
1036### Internal state files.
1037
1038
benrg@chromium.org609b7982013-02-07 16:44:46 +00001039class ConfigSettings(object):
1040 """Represents the dependency variables for a single build configuration.
1041 The structure is immutable.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001042 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001043 def __init__(self, config, values):
1044 self.config = config
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001045 verify_variables(values)
1046 self.touched = sorted(values.get(KEY_TOUCHED, []))
1047 self.tracked = sorted(values.get(KEY_TRACKED, []))
1048 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
1049 self.command = values.get('command', [])[:]
1050 self.read_only = values.get('read_only')
1051
1052 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001053 assert not (self.config and rhs.config) or (self.config == rhs.config)
maruel@chromium.org669edcb2012-11-02 19:16:14 +00001054 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001055 var = {
1056 KEY_TOUCHED: sorted(self.touched + rhs.touched),
1057 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
1058 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
1059 'command': self.command or rhs.command,
1060 'read_only': rhs.read_only if self.read_only is None else self.read_only,
1061 }
benrg@chromium.org609b7982013-02-07 16:44:46 +00001062 return ConfigSettings(self.config or rhs.config, var)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001063
1064 def flatten(self):
1065 out = {}
1066 if self.command:
1067 out['command'] = self.command
1068 if self.touched:
1069 out[KEY_TOUCHED] = self.touched
1070 if self.tracked:
1071 out[KEY_TRACKED] = self.tracked
1072 if self.untracked:
1073 out[KEY_UNTRACKED] = self.untracked
1074 if self.read_only is not None:
1075 out['read_only'] = self.read_only
1076 return out
1077
1078
1079class Configs(object):
1080 """Represents a processed .isolate file.
1081
benrg@chromium.org609b7982013-02-07 16:44:46 +00001082 Stores the file in a processed way, split by configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001083 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001084 def __init__(self, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001085 self.file_comment = file_comment
benrg@chromium.org609b7982013-02-07 16:44:46 +00001086 # The keys of by_config are tuples of values for the configuration
1087 # variables. The names of the variables (which must be the same for
1088 # every by_config key) are kept in config_variables. Initially by_config
1089 # is empty and we don't know what configuration variables will be used,
1090 # so config_variables also starts out empty. It will be set by the first
1091 # call to union() or merge_dependencies().
1092 self.by_config = {}
1093 self.config_variables = ()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001094
1095 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001096 """Adds variables from rhs (a Configs) to the existing variables.
1097 """
1098 config_variables = self.config_variables
1099 if not config_variables:
1100 config_variables = rhs.config_variables
1101 else:
1102 # We can't proceed if this isn't true since we don't know the correct
1103 # default values for extra variables. The variables are sorted so we
1104 # don't need to worry about permutations.
1105 if rhs.config_variables and rhs.config_variables != config_variables:
1106 raise ExecutionError(
1107 'Variables in merged .isolate files do not match: %r and %r' % (
1108 config_variables, rhs.config_variables))
1109
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001110 # Takes the first file comment, prefering lhs.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001111 out = Configs(self.file_comment or rhs.file_comment)
1112 out.config_variables = config_variables
1113 for config in set(self.by_config) | set(rhs.by_config):
1114 out.by_config[config] = union(
1115 self.by_config.get(config), rhs.by_config.get(config))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001116 return out
1117
benrg@chromium.org609b7982013-02-07 16:44:46 +00001118 def merge_dependencies(self, values, config_variables, configs):
1119 """Adds new dependencies to this object for the given configurations.
1120 Arguments:
1121 values: A variables dict as found in a .isolate file, e.g.,
1122 {KEY_TOUCHED: [...], 'command': ...}.
1123 config_variables: An ordered list of configuration variables, e.g.,
1124 ["OS", "chromeos"]. If this object already contains any dependencies,
1125 the configuration variables must match.
1126 configs: a list of tuples of values of the configuration variables,
1127 e.g., [("mac", 0), ("linux", 1)]. The dependencies in |values|
1128 are added to all of these configurations, and other configurations
1129 are unchanged.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001130 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001131 if not values:
1132 return
1133
1134 if not self.config_variables:
1135 self.config_variables = config_variables
1136 else:
1137 # See comment in Configs.union().
1138 assert self.config_variables == config_variables
1139
1140 for config in configs:
1141 self.by_config[config] = union(
1142 self.by_config.get(config), ConfigSettings(config, values))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001143
1144 def flatten(self):
1145 """Returns a flat dictionary representation of the configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001146 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001147 return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
1148
1149 def make_isolate_file(self):
1150 """Returns a dictionary suitable for writing to a .isolate file.
1151 """
1152 dependencies_by_config = self.flatten()
1153 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
1154 return convert_map_to_isolate_dict(configs_by_dependency,
1155 self.config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001156
1157
benrg@chromium.org609b7982013-02-07 16:44:46 +00001158# TODO(benrg): Remove this function when no old-format files are left.
1159def convert_old_to_new_format(value):
1160 """Converts from the old .isolate format, which only has one variable (OS),
1161 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
1162 and allows conditions that depend on the set of all OSes, to the new format,
1163 which allows any set of variables, has no hardcoded values, and only allows
1164 explicit positive tests of variable values.
1165 """
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001166 conditions = value.get('conditions', [])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001167 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
1168 return value # Nothing to change
1169
1170 def parse_condition(cond):
1171 return re.match(r'OS=="(\w+)"\Z', cond[0]).group(1)
1172
1173 oses = set(map(parse_condition, conditions))
1174 default_oses = set(['linux', 'mac', 'win'])
1175 oses = sorted(oses | default_oses)
1176
1177 def if_not_os(not_os, then):
1178 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
1179 return [expr, then]
1180
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001181 conditions = [
1182 cond[:2] for cond in conditions if cond[1]
1183 ] + [
1184 if_not_os(parse_condition(cond), cond[2])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001185 for cond in conditions if len(cond) == 3
1186 ]
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001187
benrg@chromium.org609b7982013-02-07 16:44:46 +00001188 if 'variables' in value:
1189 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
1190 conditions.sort()
1191
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001192 value = value.copy()
1193 value['conditions'] = conditions
benrg@chromium.org609b7982013-02-07 16:44:46 +00001194 return value
1195
1196
1197def load_isolate_as_config(isolate_dir, value, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001198 """Parses one .isolate file and returns a Configs() instance.
1199
1200 |value| is the loaded dictionary that was defined in the gyp file.
1201
1202 The expected format is strict, anything diverting from the format below will
1203 throw an assert:
1204 {
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001205 'includes': [
1206 'foo.isolate',
1207 ],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001208 'conditions': [
benrg@chromium.org609b7982013-02-07 16:44:46 +00001209 ['OS=="vms" and foo=42', {
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001210 'variables': {
benrg@chromium.org609b7982013-02-07 16:44:46 +00001211 'command': [
1212 ...
1213 ],
1214 'isolate_dependency_tracked': [
1215 ...
1216 ],
1217 'isolate_dependency_untracked': [
1218 ...
1219 ],
1220 'read_only': False,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001221 },
1222 }],
1223 ...
1224 ],
1225 }
1226 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001227 value = convert_old_to_new_format(value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001228
benrg@chromium.org609b7982013-02-07 16:44:46 +00001229 variables_and_values = {}
1230 verify_root(value, variables_and_values)
1231 if variables_and_values:
1232 config_variables, config_values = zip(
1233 *sorted(variables_and_values.iteritems()))
1234 all_configs = list(itertools.product(*config_values))
1235 else:
1236 config_variables = None
1237 all_configs = []
1238
1239 isolate = Configs(file_comment)
1240
1241 # Add configuration-specific variables.
1242 for expr, then in value.get('conditions', []):
1243 configs = match_configs(expr, config_variables, all_configs)
1244 isolate.merge_dependencies(then['variables'], config_variables, configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001245
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001246 # Load the includes.
1247 for include in value.get('includes', []):
1248 if os.path.isabs(include):
1249 raise ExecutionError(
1250 'Failed to load configuration; absolute include path \'%s\'' %
1251 include)
1252 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
1253 with open(included_isolate, 'r') as f:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001254 included_isolate = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001255 os.path.dirname(included_isolate),
1256 eval_content(f.read()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001257 None)
1258 isolate = union(isolate, included_isolate)
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001259
benrg@chromium.org609b7982013-02-07 16:44:46 +00001260 return isolate
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001261
1262
benrg@chromium.org609b7982013-02-07 16:44:46 +00001263def load_isolate_for_config(isolate_dir, content, variables):
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001264 """Loads the .isolate file and returns the information unprocessed but
1265 filtered for the specific OS.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001266
1267 Returns the command, dependencies and read_only flag. The dependencies are
1268 fixed to use os.path.sep.
1269 """
1270 # Load the .isolate file, process its conditions, retrieve the command and
1271 # dependencies.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001272 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
1273 try:
1274 config = tuple(variables[var] for var in isolate.config_variables)
1275 except KeyError:
1276 raise ExecutionError(
1277 'These configuration variables were missing from the command line: %s' %
1278 ', '.join(sorted(set(isolate.config_variables) - set(variables))))
1279 config = isolate.by_config.get(config)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001280 if not config:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001281 raise ExecutionError('Failed to load configuration for (%s) = (%s)' % (
1282 ', '.join(isolate.config_variables), ', '.join(map(str, config))))
1283 # Merge tracked and untracked variables, isolate.py doesn't care about the
1284 # trackability of the variables, only the build tool does.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001285 dependencies = [
1286 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1287 ]
1288 touched = [f.replace('/', os.path.sep) for f in config.touched]
1289 return config.command, dependencies, touched, config.read_only
1290
1291
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001292def chromium_save_isolated(isolated, data, variables):
1293 """Writes one or many .isolated files.
1294
1295 This slightly increases the cold cache cost but greatly reduce the warm cache
1296 cost by splitting low-churn files off the master .isolated file. It also
1297 reduces overall isolateserver memcache consumption.
1298 """
1299 slaves = []
1300
1301 def extract_into_included_isolated(prefix):
1302 new_slave = {'files': {}, 'os': data['os']}
1303 for f in data['files'].keys():
1304 if f.startswith(prefix):
1305 new_slave['files'][f] = data['files'].pop(f)
1306 if new_slave['files']:
1307 slaves.append(new_slave)
1308
1309 # Split test/data/ in its own .isolated file.
1310 extract_into_included_isolated(os.path.join('test', 'data', ''))
1311
1312 # Split everything out of PRODUCT_DIR in its own .isolated file.
1313 if variables.get('PRODUCT_DIR'):
1314 extract_into_included_isolated(variables['PRODUCT_DIR'])
1315
1316 files = [isolated]
1317 for index, f in enumerate(slaves):
1318 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
1319 trace_inputs.write_json(slavepath, f, True)
1320 data.setdefault('includes', []).append(
1321 isolateserver_archive.sha1_file(slavepath))
1322 files.append(slavepath)
1323
1324 trace_inputs.write_json(isolated, data, True)
1325 return files
1326
1327
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001328class Flattenable(object):
1329 """Represents data that can be represented as a json file."""
1330 MEMBERS = ()
1331
1332 def flatten(self):
1333 """Returns a json-serializable version of itself.
1334
1335 Skips None entries.
1336 """
1337 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1338 return dict((member, value) for member, value in items if value is not None)
1339
1340 @classmethod
1341 def load(cls, data):
1342 """Loads a flattened version."""
1343 data = data.copy()
1344 out = cls()
1345 for member in out.MEMBERS:
1346 if member in data:
1347 # Access to a protected member XXX of a client class
1348 # pylint: disable=W0212
1349 out._load_member(member, data.pop(member))
1350 if data:
1351 raise ValueError(
1352 'Found unexpected entry %s while constructing an object %s' %
1353 (data, cls.__name__), data, cls.__name__)
1354 return out
1355
1356 def _load_member(self, member, value):
1357 """Loads a member into self."""
1358 setattr(self, member, value)
1359
1360 @classmethod
1361 def load_file(cls, filename):
1362 """Loads the data from a file or return an empty instance."""
1363 out = cls()
1364 try:
1365 out = cls.load(trace_inputs.read_json(filename))
1366 logging.debug('Loaded %s(%s)' % (cls.__name__, filename))
1367 except (IOError, ValueError):
1368 logging.warn('Failed to load %s' % filename)
1369 return out
1370
1371
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001372class SavedState(Flattenable):
1373 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001374
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001375 This file caches the items calculated by this script and is used to increase
1376 the performance of the script. This file is not loaded by run_isolated.py.
1377 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001378
1379 It is important to note that the 'files' dict keys are using native OS path
1380 separator instead of '/' used in .isolate file.
1381 """
1382 MEMBERS = (
1383 'command',
1384 'files',
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001385 'isolate_file',
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001386 'isolated_files',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001387 'read_only',
1388 'relative_cwd',
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001389 'variables',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001390 )
1391
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001392 def __init__(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001393 super(SavedState, self).__init__()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001394 self.command = []
1395 self.files = {}
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001396 # Link back to the .isolate file.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001397 self.isolate_file = None
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001398 # Used to support/remember 'slave' .isolated files.
1399 self.isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001400 self.read_only = None
1401 self.relative_cwd = None
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001402 # Variables are saved so a user can use isolate.py after building and the
1403 # GYP variables are still defined.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001404 self.variables = {'OS': get_flavor()}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001405
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001406 def update(self, isolate_file, variables):
1407 """Updates the saved state with new data to keep GYP variables and internal
1408 reference to the original .isolate file.
1409 """
1410 self.isolate_file = isolate_file
1411 self.variables.update(variables)
1412
1413 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
1414 """Updates the saved state with data necessary to generate a .isolated file.
1415 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001416 self.command = command
1417 # Add new files.
1418 for f in infiles:
1419 self.files.setdefault(f, {})
1420 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001421 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001422 # Prune extraneous files that are not a dependency anymore.
1423 for f in set(self.files).difference(set(infiles).union(touched)):
1424 del self.files[f]
1425 if read_only is not None:
1426 self.read_only = read_only
1427 self.relative_cwd = relative_cwd
1428
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001429 def to_isolated(self):
1430 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001431
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001432 http://chromium.org/developers/testing/isolated-testing/design
1433 """
1434 def strip(data):
1435 """Returns a 'files' entry with only the whitelisted keys."""
1436 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
1437
1438 out = {
1439 'files': dict(
1440 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001441 'os': self.variables['OS'],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001442 }
1443 if self.command:
1444 out['command'] = self.command
1445 if self.read_only is not None:
1446 out['read_only'] = self.read_only
1447 if self.relative_cwd:
1448 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001449 return out
1450
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001451 @classmethod
1452 def load(cls, data):
1453 out = super(SavedState, cls).load(data)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001454 if 'os' in data:
1455 out.variables['OS'] = data['os']
1456 if out.variables['OS'] != get_flavor():
1457 raise run_isolated.ConfigError(
1458 'The .isolated.state file was created on another platform')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001459 if out.isolate_file:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001460 out.isolate_file = trace_inputs.get_native_path_case(
1461 unicode(out.isolate_file))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001462 return out
1463
1464 def __str__(self):
1465 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001466 out += ' command: %s\n' % self.command
1467 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001468 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001469 out += ' read_only: %s\n' % self.read_only
1470 out += ' relative_cwd: %s' % self.relative_cwd
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001471 out += ' isolated_files: %s' % self.isolated_files
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001472 out += ' variables: %s' % ''.join(
1473 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1474 out += ')'
1475 return out
1476
1477
1478class CompleteState(object):
1479 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001480 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001481 super(CompleteState, self).__init__()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001482 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001483 # Contains the data to ease developer's use-case but that is not strictly
1484 # necessary.
1485 self.saved_state = saved_state
1486
1487 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001488 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001489 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001490 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001491 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001492 isolated_filepath,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001493 SavedState.load_file(isolatedfile_to_state(isolated_filepath)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001494
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001495 def load_isolate(self, cwd, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001496 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001497 .isolate file.
1498
1499 Processes the loaded data, deduce root_dir, relative_cwd.
1500 """
1501 # Make sure to not depend on os.getcwd().
1502 assert os.path.isabs(isolate_file), isolate_file
1503 logging.info(
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001504 'CompleteState.load_isolate(%s, %s, %s, %s)',
1505 cwd, isolate_file, variables, ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001506 relative_base_dir = os.path.dirname(isolate_file)
1507
1508 # Processes the variables and update the saved state.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001509 variables = process_variables(cwd, variables, relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 self.saved_state.update(isolate_file, variables)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001511 variables = self.saved_state.variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001512
1513 with open(isolate_file, 'r') as f:
1514 # At that point, variables are not replaced yet in command and infiles.
1515 # infiles may contain directory entries and is in posix style.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001516 command, infiles, touched, read_only = load_isolate_for_config(
1517 os.path.dirname(isolate_file), f.read(), variables)
1518 command = [eval_variables(i, variables) for i in command]
1519 infiles = [eval_variables(f, variables) for f in infiles]
1520 touched = [eval_variables(f, variables) for f in touched]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001521 # root_dir is automatically determined by the deepest root accessed with the
1522 # form '../../foo/bar'.
1523 root_dir = determine_root_dir(relative_base_dir, infiles + touched)
1524 # The relative directory is automatically determined by the relative path
1525 # between root_dir and the directory containing the .isolate file,
1526 # isolate_base_dir.
1527 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
benrg@chromium.org9ae72862013-02-11 05:05:51 +00001528 # Now that we know where the root is, check that the PATH_VARIABLES point
1529 # inside it.
1530 for i in PATH_VARIABLES:
1531 if i in variables:
1532 if not path_starts_with(
1533 root_dir, os.path.join(relative_base_dir, variables[i])):
1534 raise run_isolated.MappingError(
1535 'Path variable %s=%r points outside the inferred root directory' %
1536 (i, variables[i]))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001537 # Normalize the files based to root_dir. It is important to keep the
1538 # trailing os.path.sep at that step.
1539 infiles = [
1540 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1541 for f in infiles
1542 ]
1543 touched = [
1544 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1545 for f in touched
1546 ]
1547 # Expand the directories by listing each file inside. Up to now, trailing
1548 # os.path.sep must be kept. Do not expand 'touched'.
1549 infiles = expand_directories_and_symlinks(
1550 root_dir,
1551 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001552 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
1553 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001554
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001555 # Finally, update the new data to be able to generate the foo.isolated file,
1556 # the file that is used by run_isolated.py.
1557 self.saved_state.update_isolated(
1558 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001559 logging.debug(self)
1560
maruel@chromium.org9268f042012-10-17 17:36:41 +00001561 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001562 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001563
maruel@chromium.org9268f042012-10-17 17:36:41 +00001564 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1565 file is tainted.
1566
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001567 See process_input() for more information.
1568 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001569 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001570 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001571 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001572 else:
1573 filepath = os.path.join(self.root_dir, infile)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001574 self.saved_state.files[infile] = process_input(
1575 filepath,
1576 self.saved_state.files[infile],
1577 self.saved_state.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001578
1579 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001580 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001581 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001582 self.saved_state.isolated_files = chromium_save_isolated(
1583 self.isolated_filepath,
1584 self.saved_state.to_isolated(),
1585 self.saved_state.variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001586 total_bytes = sum(
1587 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001588 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001589 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001590 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001591 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001592 logging.debug('Dumping to %s' % saved_state_file)
1593 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1594
1595 @property
1596 def root_dir(self):
1597 """isolate_file is always inside relative_cwd relative to root_dir."""
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001598 if not self.saved_state.isolate_file:
1599 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001600 isolate_dir = os.path.dirname(self.saved_state.isolate_file)
1601 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001602 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001603 return isolate_dir
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001604 assert isolate_dir.endswith(self.saved_state.relative_cwd), (
1605 isolate_dir, self.saved_state.relative_cwd)
1606 return isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001607
1608 @property
1609 def resultdir(self):
1610 """Directory containing the results, usually equivalent to the variable
1611 PRODUCT_DIR.
1612 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001613 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001614
1615 def __str__(self):
1616 def indent(data, indent_length):
1617 """Indents text."""
1618 spacing = ' ' * indent_length
1619 return ''.join(spacing + l for l in str(data).splitlines(True))
1620
1621 out = '%s(\n' % self.__class__.__name__
1622 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001623 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1624 return out
1625
1626
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001627def load_complete_state(options, cwd, subdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001628 """Loads a CompleteState.
1629
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001630 This includes data from .isolate and .isolated.state files. Never reads the
1631 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001632
1633 Arguments:
1634 options: Options instance generated with OptionParserIsolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001635 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001636 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001637 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001638 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001639 else:
1640 # Constructs a dummy object that cannot be saved. Useful for temporary
1641 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001642 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001643 options.isolate = options.isolate or complete_state.saved_state.isolate_file
1644 if not options.isolate:
1645 raise ExecutionError('A .isolate file is required.')
1646 if (complete_state.saved_state.isolate_file and
1647 options.isolate != complete_state.saved_state.isolate_file):
1648 raise ExecutionError(
1649 '%s and %s do not match.' % (
1650 options.isolate, complete_state.saved_state.isolate_file))
1651
1652 # Then load the .isolate and expands directories.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001653 complete_state.load_isolate(
1654 cwd, options.isolate, options.variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001655
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001656 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001657 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001658 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001659 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1660 subdir = subdir.replace('/', os.path.sep)
1661 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001662 return complete_state
1663
1664
1665def read_trace_as_isolate_dict(complete_state):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001666 """Reads a trace and returns the .isolate dictionary.
1667
1668 Returns exceptions during the log parsing so it can be re-raised.
1669 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001670 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001671 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001672 if not os.path.isfile(logfile):
1673 raise ExecutionError(
1674 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1675 try:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00001676 data = api.parse_log(logfile, default_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001677 exceptions = [i['exception'] for i in data if 'exception' in i]
1678 results = (i['results'] for i in data if 'results' in i)
1679 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1680 files = set(sum((result.existent for result in results_stripped), []))
1681 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001682 value = generate_isolate(
1683 tracked,
1684 [],
1685 touched,
1686 complete_state.root_dir,
1687 complete_state.saved_state.variables,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001688 complete_state.saved_state.relative_cwd)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001689 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001690 except trace_inputs.TracingFailure, e:
1691 raise ExecutionError(
1692 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001693 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001694
1695
1696def print_all(comment, data, stream):
1697 """Prints a complete .isolate file and its top-level file comment into a
1698 stream.
1699 """
1700 if comment:
1701 stream.write(comment)
1702 pretty_print(data, stream)
1703
1704
1705def merge(complete_state):
1706 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001707 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001708
1709 # Now take that data and union it into the original .isolate file.
1710 with open(complete_state.saved_state.isolate_file, 'r') as f:
1711 prev_content = f.read()
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001712 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001713 prev_config = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001714 isolate_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001715 eval_content(prev_content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001716 extract_comment(prev_content))
1717 new_config = load_isolate_as_config(isolate_dir, value, '')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001718 config = union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001719 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001720 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001721 with open(complete_state.saved_state.isolate_file, 'wb') as f:
1722 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001723 if exceptions:
1724 # It got an exception, raise the first one.
1725 raise \
1726 exceptions[0][0], \
1727 exceptions[0][1], \
1728 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001729
1730
1731def CMDcheck(args):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001732 """Checks that all the inputs are present and generates .isolated."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001733 parser = OptionParserIsolate(command='check')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001734 parser.add_option('--subdir', help='Filters to a subdirectory')
1735 options, args = parser.parse_args(args)
1736 if args:
1737 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001738 complete_state = load_complete_state(options, os.getcwd(), options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001739
1740 # Nothing is done specifically. Just store the result and state.
1741 complete_state.save_files()
1742 return 0
1743
1744
1745def CMDhashtable(args):
1746 """Creates a hash table content addressed object store.
1747
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001748 All the files listed in the .isolated file are put in the output directory
1749 with the file name being the sha-1 of the file's content.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001750 """
1751 parser = OptionParserIsolate(command='hashtable')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001752 parser.add_option('--subdir', help='Filters to a subdirectory')
1753 options, args = parser.parse_args(args)
1754 if args:
1755 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001756
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001757 with run_isolated.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001758 success = False
1759 try:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001760 complete_state = load_complete_state(options, os.getcwd(), options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001761 options.outdir = (
1762 options.outdir or os.path.join(complete_state.resultdir, 'hashtable'))
1763 # Make sure that complete_state isn't modified until save_files() is
1764 # called, because any changes made to it here will propagate to the files
1765 # created (which is probably not intended).
1766 complete_state.save_files()
1767
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001768 infiles = complete_state.saved_state.files
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001769 # Add all the .isolated files.
1770 for item in complete_state.saved_state.isolated_files:
1771 item_path = os.path.join(
1772 os.path.dirname(complete_state.isolated_filepath), item)
1773 with open(item_path, 'rb') as f:
1774 content = f.read()
1775 isolated_metadata = {
1776 'h': hashlib.sha1(content).hexdigest(),
1777 's': len(content),
1778 'priority': '0'
1779 }
1780 infiles[item_path] = isolated_metadata
1781
1782 logging.info('Creating content addressed object store with %d item',
1783 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001784
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001785 if is_url(options.outdir):
maruel@chromium.orgc6f90062012-11-07 18:32:22 +00001786 isolateserver_archive.upload_sha1_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001787 base_url=options.outdir,
1788 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00001789 infiles=infiles,
1790 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001791 else:
1792 recreate_tree(
1793 outdir=options.outdir,
1794 indir=complete_state.root_dir,
1795 infiles=infiles,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001796 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001797 as_sha1=True)
1798 success = True
1799 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001800 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001801 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001802 if not success and os.path.isfile(options.isolated):
1803 os.remove(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001804
1805
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001806def CMDmerge(args):
1807 """Reads and merges the data from the trace back into the original .isolate.
1808
1809 Ignores --outdir.
1810 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001811 parser = OptionParserIsolate(command='merge', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001812 options, args = parser.parse_args(args)
1813 if args:
1814 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001815 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001816 merge(complete_state)
1817 return 0
1818
1819
1820def CMDread(args):
1821 """Reads the trace file generated with command 'trace'.
1822
1823 Ignores --outdir.
1824 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001825 parser = OptionParserIsolate(command='read', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001826 options, args = parser.parse_args(args)
1827 if args:
1828 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001829 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001830 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001831 pretty_print(value, sys.stdout)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001832 if exceptions:
1833 # It got an exception, raise the first one.
1834 raise \
1835 exceptions[0][0], \
1836 exceptions[0][1], \
1837 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001838 return 0
1839
1840
1841def CMDremap(args):
1842 """Creates a directory with all the dependencies mapped into it.
1843
1844 Useful to test manually why a test is failing. The target executable is not
1845 run.
1846 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001847 parser = OptionParserIsolate(command='remap', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001848 options, args = parser.parse_args(args)
1849 if args:
1850 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001851 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001852
1853 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001854 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001855 'isolate', complete_state.root_dir)
1856 else:
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001857 if is_url(options.outdir):
1858 raise ExecutionError('Can\'t use url for --outdir with mode remap')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001859 if not os.path.isdir(options.outdir):
1860 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001861 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001862 if len(os.listdir(options.outdir)):
1863 raise ExecutionError('Can\'t remap in a non-empty directory')
1864 recreate_tree(
1865 outdir=options.outdir,
1866 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001867 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001868 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001869 as_sha1=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001870 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001871 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001872
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001873 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001874 complete_state.save_files()
1875 return 0
1876
1877
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001878def CMDrewrite(args):
1879 """Rewrites a .isolate file into the canonical format."""
1880 parser = OptionParserIsolate(command='rewrite', require_isolated=False)
1881 options, args = parser.parse_args(args)
1882 if args:
1883 parser.error('Unsupported argument: %s' % args)
1884
1885 if options.isolated:
1886 # Load the previous state if it was present. Namely, "foo.isolated.state".
1887 complete_state = CompleteState.load_files(options.isolated)
1888 else:
1889 # Constructs a dummy object that cannot be saved. Useful for temporary
1890 # commands like 'run'.
1891 complete_state = CompleteState(None, SavedState())
1892 isolate = options.isolate or complete_state.saved_state.isolate_file
1893 if not isolate:
1894 raise ExecutionError('A .isolate file is required.')
1895 with open(isolate, 'r') as f:
1896 content = f.read()
1897 config = load_isolate_as_config(
1898 os.path.dirname(os.path.abspath(isolate)),
1899 eval_content(content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001900 extract_comment(content))
1901 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001902 print('Updating %s' % isolate)
1903 with open(isolate, 'wb') as f:
1904 print_all(config.file_comment, data, f)
1905 return 0
1906
1907
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001908def CMDrun(args):
1909 """Runs the test executable in an isolated (temporary) directory.
1910
1911 All the dependencies are mapped into the temporary directory and the
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001912 directory is cleaned up after the target exits. Warning: if --outdir is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001913 specified, it is deleted upon exit.
1914
1915 Argument processing stops at the first non-recognized argument and these
1916 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001917 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001918 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001919 parser = OptionParserIsolate(command='run', require_isolated=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001920 parser.enable_interspersed_args()
1921 options, args = parser.parse_args(args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001922 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001923 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001924 if not cmd:
1925 raise ExecutionError('No command to run')
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001926 if options.outdir and is_url(options.outdir):
1927 raise ExecutionError('Can\'t use url for --outdir with mode run')
1928
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001929 cmd = trace_inputs.fix_python_path(cmd)
1930 try:
1931 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001932 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001933 'isolate', complete_state.root_dir)
1934 else:
1935 if not os.path.isdir(options.outdir):
1936 os.makedirs(options.outdir)
1937 recreate_tree(
1938 outdir=options.outdir,
1939 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001940 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001941 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001942 as_sha1=False)
1943 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001944 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001945 if not os.path.isdir(cwd):
1946 # It can happen when no files are mapped from the directory containing the
1947 # .isolate file. But the directory must exist to be the current working
1948 # directory.
1949 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001950 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001951 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001952 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1953 result = subprocess.call(cmd, cwd=cwd)
1954 finally:
1955 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001956 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001957
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001958 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001959 complete_state.save_files()
1960 return result
1961
1962
1963def CMDtrace(args):
1964 """Traces the target using trace_inputs.py.
1965
1966 It runs the executable without remapping it, and traces all the files it and
1967 its child processes access. Then the 'read' command can be used to generate an
1968 updated .isolate file out of it.
1969
1970 Argument processing stops at the first non-recognized argument and these
1971 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001972 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001973 """
1974 parser = OptionParserIsolate(command='trace')
1975 parser.enable_interspersed_args()
1976 parser.add_option(
1977 '-m', '--merge', action='store_true',
1978 help='After tracing, merge the results back in the .isolate file')
1979 options, args = parser.parse_args(args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001980 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001981 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001982 if not cmd:
1983 raise ExecutionError('No command to run')
1984 cmd = trace_inputs.fix_python_path(cmd)
1985 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001986 unicode(complete_state.root_dir),
1987 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001988 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1989 if not os.path.isfile(cmd[0]):
1990 raise ExecutionError(
1991 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001992 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1993 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001994 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001995 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001996 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001997 try:
1998 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001999 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002000 cmd,
2001 cwd,
2002 'default',
2003 True)
2004 except trace_inputs.TracingFailure, e:
2005 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
2006
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002007 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002008 logging.error(
2009 'Tracer exited with %d, which means the tests probably failed so the '
2010 'trace is probably incomplete.', result)
2011 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002012
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002013 complete_state.save_files()
2014
2015 if options.merge:
2016 merge(complete_state)
2017
2018 return result
2019
2020
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002021def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002022 """Adds --isolated and --variable to an OptionParser."""
2023 parser.add_option(
2024 '-s', '--isolated',
2025 metavar='FILE',
2026 help='.isolated file to generate or read')
2027 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002028 parser.add_option(
2029 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002030 dest='isolated',
2031 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002032 default_variables = [('OS', get_flavor())]
2033 if sys.platform in ('win32', 'cygwin'):
2034 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
2035 else:
2036 default_variables.append(('EXECUTABLE_SUFFIX', ''))
2037 parser.add_option(
2038 '-V', '--variable',
2039 nargs=2,
2040 action='append',
2041 default=default_variables,
2042 dest='variables',
2043 metavar='FOO BAR',
2044 help='Variables to process in the .isolate file, default: %default. '
2045 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002046 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002047
2048
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002049def parse_isolated_option(parser, options, cwd, require_isolated):
2050 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002051 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002052 options.isolated = os.path.normpath(
2053 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002054 if require_isolated and not options.isolated:
csharp@chromium.org707f0452012-11-26 21:50:40 +00002055 parser.error('--isolated is required. Visit http://chromium.org/developers/'
2056 'testing/isolated-testing#TOC-Where-can-I-find-the-.isolated-'
2057 'file- to see how to create the .isolated file.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002058 if options.isolated and not options.isolated.endswith('.isolated'):
2059 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002060
2061
2062def parse_variable_option(options):
2063 """Processes --variable."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002064 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
2065 # but it wouldn't be backward compatible.
2066 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002067 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002068 try:
2069 return int(s)
2070 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002071 return s.decode('utf-8')
benrg@chromium.org609b7982013-02-07 16:44:46 +00002072 options.variables = dict((k, try_make_int(v)) for k, v in options.variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002073
2074
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002075class OptionParserIsolate(trace_inputs.OptionParserWithNiceDescription):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002076 """Adds automatic --isolate, --isolated, --out and --variable handling."""
2077 def __init__(self, require_isolated=True, **kwargs):
maruel@chromium.org55276902012-10-05 20:56:19 +00002078 trace_inputs.OptionParserWithNiceDescription.__init__(
2079 self,
2080 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
2081 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002082 group = optparse.OptionGroup(self, "Common options")
2083 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002084 '-i', '--isolate',
2085 metavar='FILE',
2086 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002087 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002088 group.add_option(
2089 '-o', '--outdir', metavar='DIR',
2090 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002091 'Defaults: run|remap: a /tmp subdirectory, others: '
2092 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00002093 group.add_option(
2094 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002095 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
2096 help='Indicates that invalid entries in the isolated file to be '
2097 'only be logged and not stop processing. Defaults to True if '
2098 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002099 self.add_option_group(group)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002100 self.require_isolated = require_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002101
2102 def parse_args(self, *args, **kwargs):
2103 """Makes sure the paths make sense.
2104
2105 On Windows, / and \ are often mixed together in a path.
2106 """
2107 options, args = trace_inputs.OptionParserWithNiceDescription.parse_args(
2108 self, *args, **kwargs)
2109 if not self.allow_interspersed_args and args:
2110 self.error('Unsupported argument: %s' % args)
2111
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002112 cwd = os.getcwd()
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002113 parse_isolated_option(self, options, cwd, self.require_isolated)
2114 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002115
2116 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002117 # TODO(maruel): Work with non-ASCII.
2118 # The path must be in native path case for tracing purposes.
2119 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
2120 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
2121 options.isolate = trace_inputs.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002122
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002123 if options.outdir and not is_url(options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002124 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
2125 # outdir doesn't need native path case since tracing is never done from
2126 # there.
2127 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002128
2129 return options, args
2130
2131
2132### Glue code to make all the commands works magically.
2133
2134
2135CMDhelp = trace_inputs.CMDhelp
2136
2137
2138def main(argv):
2139 try:
2140 return trace_inputs.main_impl(argv)
2141 except (
2142 ExecutionError,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002143 run_isolated.MappingError,
2144 run_isolated.ConfigError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002145 sys.stderr.write('\nError: ')
2146 sys.stderr.write(str(e))
2147 sys.stderr.write('\n')
2148 return 1
2149
2150
2151if __name__ == '__main__':
2152 sys.exit(main(sys.argv[1:]))