blob: 1b75ac0387ca837c1f394b68073044f21656b7c5 [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
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001555 # If we ignore broken items then remove any missing touched items.
1556 if ignore_broken_items:
1557 original_touched_count = len(touched)
1558 touched = [touch for touch in touched if os.path.exists(touch)]
1559
1560 if len(touched) != original_touched_count:
1561 logging.info('warning: removed %d invalid touched entries',
1562 len(touched) - original_touched_count)
1563
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001564 # Finally, update the new data to be able to generate the foo.isolated file,
1565 # the file that is used by run_isolated.py.
1566 self.saved_state.update_isolated(
1567 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001568 logging.debug(self)
1569
maruel@chromium.org9268f042012-10-17 17:36:41 +00001570 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001571 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001572
maruel@chromium.org9268f042012-10-17 17:36:41 +00001573 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1574 file is tainted.
1575
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001576 See process_input() for more information.
1577 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001578 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001579 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001580 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001581 else:
1582 filepath = os.path.join(self.root_dir, infile)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001583 self.saved_state.files[infile] = process_input(
1584 filepath,
1585 self.saved_state.files[infile],
1586 self.saved_state.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001587
1588 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001589 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001590 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001591 self.saved_state.isolated_files = chromium_save_isolated(
1592 self.isolated_filepath,
1593 self.saved_state.to_isolated(),
1594 self.saved_state.variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001595 total_bytes = sum(
1596 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001597 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001598 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001599 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001600 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001601 logging.debug('Dumping to %s' % saved_state_file)
1602 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1603
1604 @property
1605 def root_dir(self):
1606 """isolate_file is always inside relative_cwd relative to root_dir."""
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001607 if not self.saved_state.isolate_file:
1608 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001609 isolate_dir = os.path.dirname(self.saved_state.isolate_file)
1610 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001611 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001612 return isolate_dir
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001613 assert isolate_dir.endswith(self.saved_state.relative_cwd), (
1614 isolate_dir, self.saved_state.relative_cwd)
1615 return isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001616
1617 @property
1618 def resultdir(self):
1619 """Directory containing the results, usually equivalent to the variable
1620 PRODUCT_DIR.
1621 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001622 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001623
1624 def __str__(self):
1625 def indent(data, indent_length):
1626 """Indents text."""
1627 spacing = ' ' * indent_length
1628 return ''.join(spacing + l for l in str(data).splitlines(True))
1629
1630 out = '%s(\n' % self.__class__.__name__
1631 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001632 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1633 return out
1634
1635
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001636def load_complete_state(options, cwd, subdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001637 """Loads a CompleteState.
1638
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001639 This includes data from .isolate and .isolated.state files. Never reads the
1640 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001641
1642 Arguments:
1643 options: Options instance generated with OptionParserIsolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001644 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001645 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001646 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001647 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001648 else:
1649 # Constructs a dummy object that cannot be saved. Useful for temporary
1650 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001651 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001652 options.isolate = options.isolate or complete_state.saved_state.isolate_file
1653 if not options.isolate:
1654 raise ExecutionError('A .isolate file is required.')
1655 if (complete_state.saved_state.isolate_file and
1656 options.isolate != complete_state.saved_state.isolate_file):
1657 raise ExecutionError(
1658 '%s and %s do not match.' % (
1659 options.isolate, complete_state.saved_state.isolate_file))
1660
1661 # Then load the .isolate and expands directories.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001662 complete_state.load_isolate(
1663 cwd, options.isolate, options.variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001664
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001665 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001666 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001667 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001668 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1669 subdir = subdir.replace('/', os.path.sep)
1670 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001671 return complete_state
1672
1673
1674def read_trace_as_isolate_dict(complete_state):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001675 """Reads a trace and returns the .isolate dictionary.
1676
1677 Returns exceptions during the log parsing so it can be re-raised.
1678 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001679 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001680 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001681 if not os.path.isfile(logfile):
1682 raise ExecutionError(
1683 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1684 try:
maruel@chromium.orgec74ff82012-10-29 18:14:47 +00001685 data = api.parse_log(logfile, default_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001686 exceptions = [i['exception'] for i in data if 'exception' in i]
1687 results = (i['results'] for i in data if 'results' in i)
1688 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1689 files = set(sum((result.existent for result in results_stripped), []))
1690 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001691 value = generate_isolate(
1692 tracked,
1693 [],
1694 touched,
1695 complete_state.root_dir,
1696 complete_state.saved_state.variables,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001697 complete_state.saved_state.relative_cwd)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001698 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001699 except trace_inputs.TracingFailure, e:
1700 raise ExecutionError(
1701 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001702 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001703
1704
1705def print_all(comment, data, stream):
1706 """Prints a complete .isolate file and its top-level file comment into a
1707 stream.
1708 """
1709 if comment:
1710 stream.write(comment)
1711 pretty_print(data, stream)
1712
1713
1714def merge(complete_state):
1715 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001716 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001717
1718 # Now take that data and union it into the original .isolate file.
1719 with open(complete_state.saved_state.isolate_file, 'r') as f:
1720 prev_content = f.read()
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001721 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001722 prev_config = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001723 isolate_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001724 eval_content(prev_content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001725 extract_comment(prev_content))
1726 new_config = load_isolate_as_config(isolate_dir, value, '')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001727 config = union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001728 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001729 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001730 with open(complete_state.saved_state.isolate_file, 'wb') as f:
1731 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001732 if exceptions:
1733 # It got an exception, raise the first one.
1734 raise \
1735 exceptions[0][0], \
1736 exceptions[0][1], \
1737 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001738
1739
1740def CMDcheck(args):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001741 """Checks that all the inputs are present and generates .isolated."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001742 parser = OptionParserIsolate(command='check')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001743 parser.add_option('--subdir', help='Filters to a subdirectory')
1744 options, args = parser.parse_args(args)
1745 if args:
1746 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001747 complete_state = load_complete_state(options, os.getcwd(), options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001748
1749 # Nothing is done specifically. Just store the result and state.
1750 complete_state.save_files()
1751 return 0
1752
1753
1754def CMDhashtable(args):
1755 """Creates a hash table content addressed object store.
1756
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001757 All the files listed in the .isolated file are put in the output directory
1758 with the file name being the sha-1 of the file's content.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001759 """
1760 parser = OptionParserIsolate(command='hashtable')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001761 parser.add_option('--subdir', help='Filters to a subdirectory')
1762 options, args = parser.parse_args(args)
1763 if args:
1764 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001765
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001766 with run_isolated.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001767 success = False
1768 try:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001769 complete_state = load_complete_state(options, os.getcwd(), options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001770 options.outdir = (
1771 options.outdir or os.path.join(complete_state.resultdir, 'hashtable'))
1772 # Make sure that complete_state isn't modified until save_files() is
1773 # called, because any changes made to it here will propagate to the files
1774 # created (which is probably not intended).
1775 complete_state.save_files()
1776
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001777 infiles = complete_state.saved_state.files
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001778 # Add all the .isolated files.
1779 for item in complete_state.saved_state.isolated_files:
1780 item_path = os.path.join(
1781 os.path.dirname(complete_state.isolated_filepath), item)
1782 with open(item_path, 'rb') as f:
1783 content = f.read()
1784 isolated_metadata = {
1785 'h': hashlib.sha1(content).hexdigest(),
1786 's': len(content),
1787 'priority': '0'
1788 }
1789 infiles[item_path] = isolated_metadata
1790
1791 logging.info('Creating content addressed object store with %d item',
1792 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001793
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001794 if is_url(options.outdir):
maruel@chromium.orgc6f90062012-11-07 18:32:22 +00001795 isolateserver_archive.upload_sha1_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001796 base_url=options.outdir,
1797 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00001798 infiles=infiles,
1799 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001800 else:
1801 recreate_tree(
1802 outdir=options.outdir,
1803 indir=complete_state.root_dir,
1804 infiles=infiles,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001805 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001806 as_sha1=True)
1807 success = True
1808 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001809 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001810 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001811 if not success and os.path.isfile(options.isolated):
1812 os.remove(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001813
1814
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001815def CMDmerge(args):
1816 """Reads and merges the data from the trace back into the original .isolate.
1817
1818 Ignores --outdir.
1819 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001820 parser = OptionParserIsolate(command='merge', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001821 options, args = parser.parse_args(args)
1822 if args:
1823 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001824 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001825 merge(complete_state)
1826 return 0
1827
1828
1829def CMDread(args):
1830 """Reads the trace file generated with command 'trace'.
1831
1832 Ignores --outdir.
1833 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001834 parser = OptionParserIsolate(command='read', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001835 options, args = parser.parse_args(args)
1836 if args:
1837 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001838 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001839 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001840 pretty_print(value, sys.stdout)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001841 if exceptions:
1842 # It got an exception, raise the first one.
1843 raise \
1844 exceptions[0][0], \
1845 exceptions[0][1], \
1846 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001847 return 0
1848
1849
1850def CMDremap(args):
1851 """Creates a directory with all the dependencies mapped into it.
1852
1853 Useful to test manually why a test is failing. The target executable is not
1854 run.
1855 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001856 parser = OptionParserIsolate(command='remap', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001857 options, args = parser.parse_args(args)
1858 if args:
1859 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001860 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001861
1862 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001863 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001864 'isolate', complete_state.root_dir)
1865 else:
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001866 if is_url(options.outdir):
1867 raise ExecutionError('Can\'t use url for --outdir with mode remap')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001868 if not os.path.isdir(options.outdir):
1869 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001870 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001871 if len(os.listdir(options.outdir)):
1872 raise ExecutionError('Can\'t remap in a non-empty directory')
1873 recreate_tree(
1874 outdir=options.outdir,
1875 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001876 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001877 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001878 as_sha1=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001879 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001880 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001881
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001882 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001883 complete_state.save_files()
1884 return 0
1885
1886
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001887def CMDrewrite(args):
1888 """Rewrites a .isolate file into the canonical format."""
1889 parser = OptionParserIsolate(command='rewrite', require_isolated=False)
1890 options, args = parser.parse_args(args)
1891 if args:
1892 parser.error('Unsupported argument: %s' % args)
1893
1894 if options.isolated:
1895 # Load the previous state if it was present. Namely, "foo.isolated.state".
1896 complete_state = CompleteState.load_files(options.isolated)
1897 else:
1898 # Constructs a dummy object that cannot be saved. Useful for temporary
1899 # commands like 'run'.
1900 complete_state = CompleteState(None, SavedState())
1901 isolate = options.isolate or complete_state.saved_state.isolate_file
1902 if not isolate:
1903 raise ExecutionError('A .isolate file is required.')
1904 with open(isolate, 'r') as f:
1905 content = f.read()
1906 config = load_isolate_as_config(
1907 os.path.dirname(os.path.abspath(isolate)),
1908 eval_content(content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001909 extract_comment(content))
1910 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001911 print('Updating %s' % isolate)
1912 with open(isolate, 'wb') as f:
1913 print_all(config.file_comment, data, f)
1914 return 0
1915
1916
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001917def CMDrun(args):
1918 """Runs the test executable in an isolated (temporary) directory.
1919
1920 All the dependencies are mapped into the temporary directory and the
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001921 directory is cleaned up after the target exits. Warning: if --outdir is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001922 specified, it is deleted upon exit.
1923
1924 Argument processing stops at the first non-recognized argument and these
1925 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001926 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001927 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001928 parser = OptionParserIsolate(command='run', require_isolated=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001929 parser.enable_interspersed_args()
1930 options, args = parser.parse_args(args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001931 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001932 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001933 if not cmd:
1934 raise ExecutionError('No command to run')
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001935 if options.outdir and is_url(options.outdir):
1936 raise ExecutionError('Can\'t use url for --outdir with mode run')
1937
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001938 cmd = trace_inputs.fix_python_path(cmd)
1939 try:
1940 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001941 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001942 'isolate', complete_state.root_dir)
1943 else:
1944 if not os.path.isdir(options.outdir):
1945 os.makedirs(options.outdir)
1946 recreate_tree(
1947 outdir=options.outdir,
1948 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001949 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001950 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001951 as_sha1=False)
1952 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001953 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001954 if not os.path.isdir(cwd):
1955 # It can happen when no files are mapped from the directory containing the
1956 # .isolate file. But the directory must exist to be the current working
1957 # directory.
1958 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001959 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001960 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001961 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1962 result = subprocess.call(cmd, cwd=cwd)
1963 finally:
1964 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001965 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001966
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001967 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001968 complete_state.save_files()
1969 return result
1970
1971
1972def CMDtrace(args):
1973 """Traces the target using trace_inputs.py.
1974
1975 It runs the executable without remapping it, and traces all the files it and
1976 its child processes access. Then the 'read' command can be used to generate an
1977 updated .isolate file out of it.
1978
1979 Argument processing stops at the first non-recognized argument and these
1980 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001981 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001982 """
1983 parser = OptionParserIsolate(command='trace')
1984 parser.enable_interspersed_args()
1985 parser.add_option(
1986 '-m', '--merge', action='store_true',
1987 help='After tracing, merge the results back in the .isolate file')
1988 options, args = parser.parse_args(args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001989 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001990 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001991 if not cmd:
1992 raise ExecutionError('No command to run')
1993 cmd = trace_inputs.fix_python_path(cmd)
1994 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001995 unicode(complete_state.root_dir),
1996 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001997 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1998 if not os.path.isfile(cmd[0]):
1999 raise ExecutionError(
2000 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002001 logging.info('Running %s, cwd=%s' % (cmd, cwd))
2002 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002003 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002004 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002005 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002006 try:
2007 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002008 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002009 cmd,
2010 cwd,
2011 'default',
2012 True)
2013 except trace_inputs.TracingFailure, e:
2014 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
2015
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002016 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002017 logging.error(
2018 'Tracer exited with %d, which means the tests probably failed so the '
2019 'trace is probably incomplete.', result)
2020 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002021
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002022 complete_state.save_files()
2023
2024 if options.merge:
2025 merge(complete_state)
2026
2027 return result
2028
2029
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002030def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002031 """Adds --isolated and --variable to an OptionParser."""
2032 parser.add_option(
2033 '-s', '--isolated',
2034 metavar='FILE',
2035 help='.isolated file to generate or read')
2036 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002037 parser.add_option(
2038 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002039 dest='isolated',
2040 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002041 default_variables = [('OS', get_flavor())]
2042 if sys.platform in ('win32', 'cygwin'):
2043 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
2044 else:
2045 default_variables.append(('EXECUTABLE_SUFFIX', ''))
2046 parser.add_option(
2047 '-V', '--variable',
2048 nargs=2,
2049 action='append',
2050 default=default_variables,
2051 dest='variables',
2052 metavar='FOO BAR',
2053 help='Variables to process in the .isolate file, default: %default. '
2054 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002055 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002056
2057
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002058def parse_isolated_option(parser, options, cwd, require_isolated):
2059 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002060 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002061 options.isolated = os.path.normpath(
2062 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002063 if require_isolated and not options.isolated:
csharp@chromium.org707f0452012-11-26 21:50:40 +00002064 parser.error('--isolated is required. Visit http://chromium.org/developers/'
2065 'testing/isolated-testing#TOC-Where-can-I-find-the-.isolated-'
2066 'file- to see how to create the .isolated file.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002067 if options.isolated and not options.isolated.endswith('.isolated'):
2068 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002069
2070
2071def parse_variable_option(options):
2072 """Processes --variable."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002073 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
2074 # but it wouldn't be backward compatible.
2075 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002076 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002077 try:
2078 return int(s)
2079 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002080 return s.decode('utf-8')
benrg@chromium.org609b7982013-02-07 16:44:46 +00002081 options.variables = dict((k, try_make_int(v)) for k, v in options.variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002082
2083
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002084class OptionParserIsolate(trace_inputs.OptionParserWithNiceDescription):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002085 """Adds automatic --isolate, --isolated, --out and --variable handling."""
2086 def __init__(self, require_isolated=True, **kwargs):
maruel@chromium.org55276902012-10-05 20:56:19 +00002087 trace_inputs.OptionParserWithNiceDescription.__init__(
2088 self,
2089 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
2090 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002091 group = optparse.OptionGroup(self, "Common options")
2092 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002093 '-i', '--isolate',
2094 metavar='FILE',
2095 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002096 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002097 group.add_option(
2098 '-o', '--outdir', metavar='DIR',
2099 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002100 'Defaults: run|remap: a /tmp subdirectory, others: '
2101 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00002102 group.add_option(
2103 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002104 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
2105 help='Indicates that invalid entries in the isolated file to be '
2106 'only be logged and not stop processing. Defaults to True if '
2107 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002108 self.add_option_group(group)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002109 self.require_isolated = require_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002110
2111 def parse_args(self, *args, **kwargs):
2112 """Makes sure the paths make sense.
2113
2114 On Windows, / and \ are often mixed together in a path.
2115 """
2116 options, args = trace_inputs.OptionParserWithNiceDescription.parse_args(
2117 self, *args, **kwargs)
2118 if not self.allow_interspersed_args and args:
2119 self.error('Unsupported argument: %s' % args)
2120
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002121 cwd = os.getcwd()
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002122 parse_isolated_option(self, options, cwd, self.require_isolated)
2123 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002124
2125 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002126 # TODO(maruel): Work with non-ASCII.
2127 # The path must be in native path case for tracing purposes.
2128 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
2129 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
2130 options.isolate = trace_inputs.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002131
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002132 if options.outdir and not is_url(options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002133 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
2134 # outdir doesn't need native path case since tracing is never done from
2135 # there.
2136 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002137
2138 return options, args
2139
2140
2141### Glue code to make all the commands works magically.
2142
2143
2144CMDhelp = trace_inputs.CMDhelp
2145
2146
2147def main(argv):
2148 try:
2149 return trace_inputs.main_impl(argv)
2150 except (
2151 ExecutionError,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002152 run_isolated.MappingError,
2153 run_isolated.ConfigError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002154 sys.stderr.write('\nError: ')
2155 sys.stderr.write(str(e))
2156 sys.stderr.write('\n')
2157 return 1
2158
2159
2160if __name__ == '__main__':
2161 sys.exit(main(sys.argv[1:]))