blob: 080c001bc09a962a3e29a57c15b98cbc1a074bc5 [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.orgd2627672013-04-03 15:30:24 +000097def chromium_default_blacklist(f):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000098 """Filters unimportant files normally ignored."""
99 return (
maruel@chromium.orgd2627672013-04-03 15:30:24 +0000100 f.endswith(('.pyc', '.swp', '.run_test_cases', 'testserver.log')) or
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000101 _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
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000121def fix_native_path_case(root, path):
122 """Ensures that each component of |path| has the proper native case by
123 iterating slowly over the directory elements of |path|."""
124 native_case_path = root
125 for raw_part in path.split(os.sep):
126 if not raw_part or raw_part == '.':
127 break
128
129 part = trace_inputs.find_item_native_case(native_case_path, raw_part)
130 if not part:
131 raise run_isolated.MappingError(
132 'Input file %s doesn\'t exist' %
133 os.path.join(native_case_path, raw_part))
134 native_case_path = os.path.join(native_case_path, part)
135
136 return os.path.normpath(native_case_path)
137
138
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000139def expand_symlinks(indir, relfile):
140 """Follows symlinks in |relfile|, but treating symlinks that point outside the
141 build tree as if they were ordinary directories/files. Returns the final
142 symlink-free target and a list of paths to symlinks encountered in the
143 process.
144
145 The rule about symlinks outside the build tree is for the benefit of the
146 Chromium OS ebuild, which symlinks the output directory to an unrelated path
147 in the chroot.
148
149 Fails when a directory loop is detected, although in theory we could support
150 that case.
151 """
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000152 is_directory = relfile.endswith(os.path.sep)
153 done = indir
154 todo = relfile.strip(os.path.sep)
155 symlinks = []
156
157 while todo:
158 pre_symlink, symlink, post_symlink = trace_inputs.split_at_symlink(
159 done, todo)
160 if not symlink:
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000161 todo = fix_native_path_case(done, todo)
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000162 done = os.path.join(done, todo)
163 break
164 symlink_path = os.path.join(done, pre_symlink, symlink)
165 post_symlink = post_symlink.lstrip(os.path.sep)
166 # readlink doesn't exist on Windows.
167 # pylint: disable=E1101
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000168 target = os.path.normpath(os.path.join(done, pre_symlink))
169 symlink_target = os.readlink(symlink_path)
170
171 # The symlink itself could be using the wrong path case.
172 target = fix_native_path_case(target, symlink_target)
173
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000174 if not os.path.exists(target):
175 raise run_isolated.MappingError(
176 'Symlink target doesn\'t exist: %s -> %s' % (symlink_path, target))
177 target = trace_inputs.get_native_path_case(target)
178 if not path_starts_with(indir, target):
179 done = symlink_path
180 todo = post_symlink
181 continue
182 if path_starts_with(target, symlink_path):
183 raise run_isolated.MappingError(
184 'Can\'t map recursive symlink reference %s -> %s' %
185 (symlink_path, target))
186 logging.info('Found symlink: %s -> %s', symlink_path, target)
187 symlinks.append(os.path.relpath(symlink_path, indir))
188 # Treat the common prefix of the old and new paths as done, and start
189 # scanning again.
190 target = target.split(os.path.sep)
191 symlink_path = symlink_path.split(os.path.sep)
192 prefix_length = 0
193 for target_piece, symlink_path_piece in zip(target, symlink_path):
194 if target_piece == symlink_path_piece:
195 prefix_length += 1
196 else:
197 break
198 done = os.path.sep.join(target[:prefix_length])
199 todo = os.path.join(
200 os.path.sep.join(target[prefix_length:]), post_symlink)
201
202 relfile = os.path.relpath(done, indir)
203 relfile = relfile.rstrip(os.path.sep) + is_directory * os.path.sep
204 return relfile, symlinks
205
206
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000207def expand_directory_and_symlink(indir, relfile, blacklist, follow_symlinks):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000208 """Expands a single input. It can result in multiple outputs.
209
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000210 This function is recursive when relfile is a directory.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000211
212 Note: this code doesn't properly handle recursive symlink like one created
213 with:
214 ln -s .. foo
215 """
216 if os.path.isabs(relfile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000217 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000218 'Can\'t map absolute path %s' % relfile)
219
220 infile = normpath(os.path.join(indir, relfile))
221 if not infile.startswith(indir):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000222 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000223 'Can\'t map file %s outside %s' % (infile, indir))
224
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000225 filepath = os.path.join(indir, relfile)
226 native_filepath = trace_inputs.get_native_path_case(filepath)
227 if filepath != native_filepath:
maruel@chromium.org59bb2a32013-03-21 17:08:39 +0000228 # Special case './'.
229 if filepath != native_filepath + '.' + os.path.sep:
230 raise run_isolated.MappingError(
231 'File path doesn\'t equal native file path\n%s != %s' %
232 (filepath, native_filepath))
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000233
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000234 symlinks = []
235 if follow_symlinks:
236 relfile, symlinks = expand_symlinks(indir, relfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000237
238 if relfile.endswith(os.path.sep):
239 if not os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000240 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000241 '%s is not a directory but ends with "%s"' % (infile, os.path.sep))
242
maruel@chromium.org59bb2a32013-03-21 17:08:39 +0000243 # Special case './'.
244 if relfile.startswith('.' + os.path.sep):
245 relfile = relfile[2:]
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000246 outfiles = symlinks
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000247 try:
248 for filename in os.listdir(infile):
249 inner_relfile = os.path.join(relfile, filename)
250 if blacklist(inner_relfile):
251 continue
252 if os.path.isdir(os.path.join(indir, inner_relfile)):
253 inner_relfile += os.path.sep
254 outfiles.extend(
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000255 expand_directory_and_symlink(indir, inner_relfile, blacklist,
256 follow_symlinks))
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000257 return outfiles
258 except OSError as e:
259 raise run_isolated.MappingError('Unable to iterate over directories.\n'
260 '%s' % e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000261 else:
262 # Always add individual files even if they were blacklisted.
263 if os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000264 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000265 'Input directory %s must have a trailing slash' % infile)
266
267 if not os.path.isfile(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000268 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000269 'Input file %s doesn\'t exist' % infile)
270
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000271 return symlinks + [relfile]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000272
273
csharp@chromium.org01856802012-11-12 17:48:13 +0000274def expand_directories_and_symlinks(indir, infiles, blacklist,
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000275 follow_symlinks, ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000276 """Expands the directories and the symlinks, applies the blacklist and
277 verifies files exist.
278
279 Files are specified in os native path separator.
280 """
281 outfiles = []
282 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +0000283 try:
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000284 outfiles.extend(expand_directory_and_symlink(indir, relfile, blacklist,
285 follow_symlinks))
csharp@chromium.org01856802012-11-12 17:48:13 +0000286 except run_isolated.MappingError as e:
287 if ignore_broken_items:
288 logging.info('warning: %s', e)
289 else:
290 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000291 return outfiles
292
293
294def recreate_tree(outdir, indir, infiles, action, as_sha1):
295 """Creates a new tree with only the input files in it.
296
297 Arguments:
298 outdir: Output directory to create the files in.
299 indir: Root directory the infiles are based in.
300 infiles: dict of files to map from |indir| to |outdir|.
301 action: See assert below.
302 as_sha1: Output filename is the sha1 instead of relfile.
303 """
304 logging.info(
305 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_sha1=%s)' %
306 (outdir, indir, len(infiles), action, as_sha1))
307
308 assert action in (
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000309 run_isolated.HARDLINK,
310 run_isolated.SYMLINK,
311 run_isolated.COPY)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000312 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000313 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000314 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000315 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000316
317 for relfile, metadata in infiles.iteritems():
318 infile = os.path.join(indir, relfile)
319 if as_sha1:
320 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000321 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000322 # Skip links when storing a hashtable.
323 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000324 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000325 if os.path.isfile(outfile):
326 # Just do a quick check that the file size matches. No need to stat()
327 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000328 if not 's' in metadata:
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000329 raise run_isolated.MappingError(
330 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000331 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000332 continue
333 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000334 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000335 os.remove(outfile)
336 else:
337 outfile = os.path.join(outdir, relfile)
338 outsubdir = os.path.dirname(outfile)
339 if not os.path.isdir(outsubdir):
340 os.makedirs(outsubdir)
341
342 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000343 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000344 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000345 if 'l' in metadata:
346 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000347 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000348 # symlink doesn't exist on Windows.
349 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000350 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000351 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000352
353
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000354def process_input(filepath, prevdict, read_only, flavor):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000355 """Processes an input file, a dependency, and return meta data about it.
356
357 Arguments:
358 - filepath: File to act on.
359 - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
360 to skip recalculating the hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000361 - read_only: If True, the file mode is manipulated. In practice, only save
362 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
363 windows, mode is not set since all files are 'executable' by
364 default.
365
366 Behaviors:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000367 - Retrieves the file mode, file size, file timestamp, file link
368 destination if it is a file link and calcultate the SHA-1 of the file's
369 content if the path points to a file and not a symlink.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000370 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000371 out = {}
372 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000373 # if prevdict.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000374 # # The file's content is ignored. Skip the time and hard code mode.
375 # if get_flavor() != 'win':
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000376 # out['m'] = stat.S_IRUSR | stat.S_IRGRP
377 # out['s'] = 0
378 # out['h'] = SHA_1_NULL
379 # out['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000380 # return out
381
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000382 # Always check the file stat and check if it is a link. The timestamp is used
383 # to know if the file's content/symlink destination should be looked into.
384 # E.g. only reuse from prevdict if the timestamp hasn't changed.
385 # There is the risk of the file's timestamp being reset to its last value
386 # manually while its content changed. We don't protect against that use case.
387 try:
388 filestats = os.lstat(filepath)
389 except OSError:
390 # The file is not present.
391 raise run_isolated.MappingError('%s is missing' % filepath)
392 is_link = stat.S_ISLNK(filestats.st_mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000393
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000394 if flavor != 'win':
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000395 # Ignore file mode on Windows since it's not really useful there.
396 filemode = stat.S_IMODE(filestats.st_mode)
397 # Remove write access for group and all access to 'others'.
398 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
399 if read_only:
400 filemode &= ~stat.S_IWUSR
401 if filemode & stat.S_IXUSR:
402 filemode |= stat.S_IXGRP
403 else:
404 filemode &= ~stat.S_IXGRP
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000405 out['m'] = filemode
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000406
407 # Used to skip recalculating the hash or link destination. Use the most recent
408 # update time.
409 # TODO(maruel): Save it in the .state file instead of .isolated so the
410 # .isolated file is deterministic.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000411 out['t'] = int(round(filestats.st_mtime))
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000412
413 if not is_link:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000414 out['s'] = filestats.st_size
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000415 # If the timestamp wasn't updated and the file size is still the same, carry
416 # on the sha-1.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000417 if (prevdict.get('t') == out['t'] and
418 prevdict.get('s') == out['s']):
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000419 # Reuse the previous hash if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000420 out['h'] = prevdict.get('h')
421 if not out.get('h'):
maruel@chromium.org6da38772012-12-11 21:36:37 +0000422 out['h'] = isolateserver_archive.sha1_file(filepath)
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000423 else:
424 # If the timestamp wasn't updated, carry on the link destination.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000425 if prevdict.get('t') == out['t']:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000426 # Reuse the previous link destination if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000427 out['l'] = prevdict.get('l')
428 if out.get('l') is None:
429 out['l'] = os.readlink(filepath) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000430 return out
431
432
433### Variable stuff.
434
435
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000436def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000437 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000438 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000439
440
441def determine_root_dir(relative_root, infiles):
442 """For a list of infiles, determines the deepest root directory that is
443 referenced indirectly.
444
445 All arguments must be using os.path.sep.
446 """
447 # The trick used to determine the root directory is to look at "how far" back
448 # up it is looking up.
449 deepest_root = relative_root
450 for i in infiles:
451 x = relative_root
452 while i.startswith('..' + os.path.sep):
453 i = i[3:]
454 assert not i.startswith(os.path.sep)
455 x = os.path.dirname(x)
456 if deepest_root.startswith(x):
457 deepest_root = x
458 logging.debug(
459 'determine_root_dir(%s, %d files) -> %s' % (
460 relative_root, len(infiles), deepest_root))
461 return deepest_root
462
463
464def replace_variable(part, variables):
465 m = re.match(r'<\(([A-Z_]+)\)', part)
466 if m:
467 if m.group(1) not in variables:
468 raise ExecutionError(
469 'Variable "%s" was not found in %s.\nDid you forget to specify '
470 '--variable?' % (m.group(1), variables))
471 return variables[m.group(1)]
472 return part
473
474
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000475def process_variables(cwd, variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000476 """Processes path variables as a special case and returns a copy of the dict.
477
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000478 For each 'path' variable: first normalizes it based on |cwd|, verifies it
479 exists then sets it as relative to relative_base_dir.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000480 """
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000481 relative_base_dir = trace_inputs.get_native_path_case(relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000482 variables = variables.copy()
483 for i in PATH_VARIABLES:
484 if i not in variables:
485 continue
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000486 variable = variables[i].strip()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000487 # Variables could contain / or \ on windows. Always normalize to
488 # os.path.sep.
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000489 variable = variable.replace('/', os.path.sep)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000490 variable = os.path.join(cwd, variable)
491 variable = os.path.normpath(variable)
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000492 variable = trace_inputs.get_native_path_case(variable)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000493 if not os.path.isdir(variable):
494 raise ExecutionError('%s=%s is not a directory' % (i, variable))
495
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000496 # All variables are relative to the .isolate file.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000497 variable = os.path.relpath(variable, relative_base_dir)
498 logging.debug(
499 'Translated variable %s from %s to %s', i, variables[i], variable)
500 variables[i] = variable
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000501 return variables
502
503
504def eval_variables(item, variables):
505 """Replaces the .isolate variables in a string item.
506
507 Note that the .isolate format is a subset of the .gyp dialect.
508 """
509 return ''.join(
510 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
511
512
513def classify_files(root_dir, tracked, untracked):
514 """Converts the list of files into a .isolate 'variables' dictionary.
515
516 Arguments:
517 - tracked: list of files names to generate a dictionary out of that should
518 probably be tracked.
519 - untracked: list of files names that must not be tracked.
520 """
521 # These directories are not guaranteed to be always present on every builder.
522 OPTIONAL_DIRECTORIES = (
523 'test/data/plugin',
524 'third_party/WebKit/LayoutTests',
525 )
526
527 new_tracked = []
528 new_untracked = list(untracked)
529
530 def should_be_tracked(filepath):
531 """Returns True if it is a file without whitespace in a non-optional
532 directory that has no symlink in its path.
533 """
534 if filepath.endswith('/'):
535 return False
536 if ' ' in filepath:
537 return False
538 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
539 return False
540 # Look if any element in the path is a symlink.
541 split = filepath.split('/')
542 for i in range(len(split)):
543 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
544 return False
545 return True
546
547 for filepath in sorted(tracked):
548 if should_be_tracked(filepath):
549 new_tracked.append(filepath)
550 else:
551 # Anything else.
552 new_untracked.append(filepath)
553
554 variables = {}
555 if new_tracked:
556 variables[KEY_TRACKED] = sorted(new_tracked)
557 if new_untracked:
558 variables[KEY_UNTRACKED] = sorted(new_untracked)
559 return variables
560
561
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000562def chromium_fix(f, variables):
563 """Fixes an isolate dependnecy with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000564 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
565 # separator.
566 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000567 # Ignored items.
568 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000569 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000570 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000571 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000572 # 'First Run' is not created by the compile, but by the test itself.
573 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000574
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000575 # Blacklist logs and other unimportant files.
576 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
577 logging.debug('Ignoring %s', f)
578 return None
579
maruel@chromium.org7650e422012-11-16 21:56:42 +0000580 EXECUTABLE = re.compile(
581 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
582 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
583 r'$')
584 match = EXECUTABLE.match(f)
585 if match:
586 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
587
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000588 if sys.platform == 'darwin':
589 # On OSX, the name of the output is dependent on gyp define, it can be
590 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
591 # Framework.framework'. Furthermore, they are versioned with a gyp
592 # variable. To lower the complexity of the .isolate file, remove all the
593 # individual entries that show up under any of the 4 entries and replace
594 # them with the directory itself. Overall, this results in a bit more
595 # files than strictly necessary.
596 OSX_BUNDLES = (
597 '<(PRODUCT_DIR)/Chromium Framework.framework/',
598 '<(PRODUCT_DIR)/Chromium.app/',
599 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
600 '<(PRODUCT_DIR)/Google Chrome.app/',
601 )
602 for prefix in OSX_BUNDLES:
603 if f.startswith(prefix):
604 # Note this result in duplicate values, so the a set() must be used to
605 # remove duplicates.
606 return prefix
607 return f
608
609
610def generate_simplified(
611 tracked, untracked, touched, root_dir, variables, relative_cwd):
612 """Generates a clean and complete .isolate 'variables' dictionary.
613
614 Cleans up and extracts only files from within root_dir then processes
615 variables and relative_cwd.
616 """
617 root_dir = os.path.realpath(root_dir)
618 logging.info(
619 'generate_simplified(%d files, %s, %s, %s)' %
620 (len(tracked) + len(untracked) + len(touched),
621 root_dir, variables, relative_cwd))
622
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000623 # Preparation work.
624 relative_cwd = cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000625 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000626 # Creates the right set of variables here. We only care about PATH_VARIABLES.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000627 path_variables = dict(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000628 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
629 for k in PATH_VARIABLES if k in variables)
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000630 variables = variables.copy()
631 variables.update(path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000632
633 # Actual work: Process the files.
634 # TODO(maruel): if all the files in a directory are in part tracked and in
635 # part untracked, the directory will not be extracted. Tracked files should be
636 # 'promoted' to be untracked as needed.
637 tracked = trace_inputs.extract_directories(
maruel@chromium.orgd2627672013-04-03 15:30:24 +0000638 root_dir, tracked, chromium_default_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000639 untracked = trace_inputs.extract_directories(
maruel@chromium.orgd2627672013-04-03 15:30:24 +0000640 root_dir, untracked, chromium_default_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000641 # touched is not compressed, otherwise it would result in files to be archived
642 # that we don't need.
643
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000644 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000645 def fix(f):
646 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000647 # Important, GYP stores the files with / and not \.
648 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000649 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000650 # If it's not already a variable.
651 if not f.startswith('<'):
652 # relative_cwd is usually the directory containing the gyp file. It may be
653 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000654 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000655 # Convert the whole thing to / since it's isolate's speak.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000656 f = posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000657 posixpath.join(root_dir_posix, f),
658 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000659
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000660 for variable, root_path in path_variables.iteritems():
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000661 if f.startswith(root_path):
662 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000663 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000664 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000665 return f
666
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000667 def fix_all(items):
668 """Reduces the items to convert variables, removes unneeded items, apply
669 chromium-specific fixes and only return unique items.
670 """
671 variables_converted = (fix(f.path) for f in items)
672 chromium_fixed = (chromium_fix(f, variables) for f in variables_converted)
673 return set(f for f in chromium_fixed if f)
674
675 tracked = fix_all(tracked)
676 untracked = fix_all(untracked)
677 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000678 out = classify_files(root_dir, tracked, untracked)
679 if touched:
680 out[KEY_TOUCHED] = sorted(touched)
681 return out
682
683
benrg@chromium.org609b7982013-02-07 16:44:46 +0000684def chromium_filter_flags(variables):
685 """Filters out build flags used in Chromium that we don't want to treat as
686 configuration variables.
687 """
688 # TODO(benrg): Need a better way to determine this.
689 blacklist = set(PATH_VARIABLES + ('EXECUTABLE_SUFFIX', 'FLAG'))
690 return dict((k, v) for k, v in variables.iteritems() if k not in blacklist)
691
692
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000693def generate_isolate(
694 tracked, untracked, touched, root_dir, variables, relative_cwd):
695 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000696 dependencies = generate_simplified(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000697 tracked, untracked, touched, root_dir, variables, relative_cwd)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000698 config_variables = chromium_filter_flags(variables)
699 config_variable_names, config_values = zip(
700 *sorted(config_variables.iteritems()))
701 out = Configs(None)
702 # The new dependencies apply to just one configuration, namely config_values.
703 out.merge_dependencies(dependencies, config_variable_names, [config_values])
704 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000705
706
707def split_touched(files):
708 """Splits files that are touched vs files that are read."""
709 tracked = []
710 touched = []
711 for f in files:
712 if f.size:
713 tracked.append(f)
714 else:
715 touched.append(f)
716 return tracked, touched
717
718
719def pretty_print(variables, stdout):
720 """Outputs a gyp compatible list from the decoded variables.
721
722 Similar to pprint.print() but with NIH syndrome.
723 """
724 # Order the dictionary keys by these keys in priority.
725 ORDER = (
726 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
727 KEY_TRACKED, KEY_UNTRACKED)
728
729 def sorting_key(x):
730 """Gives priority to 'most important' keys before the others."""
731 if x in ORDER:
732 return str(ORDER.index(x))
733 return x
734
735 def loop_list(indent, items):
736 for item in items:
737 if isinstance(item, basestring):
738 stdout.write('%s\'%s\',\n' % (indent, item))
739 elif isinstance(item, dict):
740 stdout.write('%s{\n' % indent)
741 loop_dict(indent + ' ', item)
742 stdout.write('%s},\n' % indent)
743 elif isinstance(item, list):
744 # A list inside a list will write the first item embedded.
745 stdout.write('%s[' % indent)
746 for index, i in enumerate(item):
747 if isinstance(i, basestring):
748 stdout.write(
749 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
750 elif isinstance(i, dict):
751 stdout.write('{\n')
752 loop_dict(indent + ' ', i)
753 if index != len(item) - 1:
754 x = ', '
755 else:
756 x = ''
757 stdout.write('%s}%s' % (indent, x))
758 else:
759 assert False
760 stdout.write('],\n')
761 else:
762 assert False
763
764 def loop_dict(indent, items):
765 for key in sorted(items, key=sorting_key):
766 item = items[key]
767 stdout.write("%s'%s': " % (indent, key))
768 if isinstance(item, dict):
769 stdout.write('{\n')
770 loop_dict(indent + ' ', item)
771 stdout.write(indent + '},\n')
772 elif isinstance(item, list):
773 stdout.write('[\n')
774 loop_list(indent + ' ', item)
775 stdout.write(indent + '],\n')
776 elif isinstance(item, basestring):
777 stdout.write(
778 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
779 elif item in (True, False, None):
780 stdout.write('%s\n' % item)
781 else:
782 assert False, item
783
784 stdout.write('{\n')
785 loop_dict(' ', variables)
786 stdout.write('}\n')
787
788
789def union(lhs, rhs):
790 """Merges two compatible datastructures composed of dict/list/set."""
791 assert lhs is not None or rhs is not None
792 if lhs is None:
793 return copy.deepcopy(rhs)
794 if rhs is None:
795 return copy.deepcopy(lhs)
796 assert type(lhs) == type(rhs), (lhs, rhs)
797 if hasattr(lhs, 'union'):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000798 # Includes set, ConfigSettings and Configs.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000799 return lhs.union(rhs)
800 if isinstance(lhs, dict):
801 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
802 elif isinstance(lhs, list):
803 # Do not go inside the list.
804 return lhs + rhs
805 assert False, type(lhs)
806
807
808def extract_comment(content):
809 """Extracts file level comment."""
810 out = []
811 for line in content.splitlines(True):
812 if line.startswith('#'):
813 out.append(line)
814 else:
815 break
816 return ''.join(out)
817
818
819def eval_content(content):
820 """Evaluates a python file and return the value defined in it.
821
822 Used in practice for .isolate files.
823 """
824 globs = {'__builtins__': None}
825 locs = {}
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000826 try:
827 value = eval(content, globs, locs)
828 except TypeError as e:
829 e.args = list(e.args) + [content]
830 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000831 assert locs == {}, locs
832 assert globs == {'__builtins__': None}, globs
833 return value
834
835
benrg@chromium.org609b7982013-02-07 16:44:46 +0000836def match_configs(expr, config_variables, all_configs):
837 """Returns the configs from |all_configs| that match the |expr|, where
838 the elements of |all_configs| are tuples of values for the |config_variables|.
839 Example:
840 >>> match_configs(expr = "(foo==1 or foo==2) and bar=='b'",
841 config_variables = ["foo", "bar"],
842 all_configs = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
843 [(1, 'b'), (2, 'b')]
844 """
845 return [
846 config for config in all_configs
847 if eval(expr, dict(zip(config_variables, config)))
848 ]
849
850
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000851def verify_variables(variables):
852 """Verifies the |variables| dictionary is in the expected format."""
853 VALID_VARIABLES = [
854 KEY_TOUCHED,
855 KEY_TRACKED,
856 KEY_UNTRACKED,
857 'command',
858 'read_only',
859 ]
860 assert isinstance(variables, dict), variables
861 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
862 for name, value in variables.iteritems():
863 if name == 'read_only':
864 assert value in (True, False, None), value
865 else:
866 assert isinstance(value, list), value
867 assert all(isinstance(i, basestring) for i in value), value
868
869
benrg@chromium.org609b7982013-02-07 16:44:46 +0000870def verify_ast(expr, variables_and_values):
871 """Verifies that |expr| is of the form
872 expr ::= expr ( "or" | "and" ) expr
873 | identifier "==" ( string | int )
874 Also collects the variable identifiers and string/int values in the dict
875 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
876 """
877 assert isinstance(expr, (ast.BoolOp, ast.Compare))
878 if isinstance(expr, ast.BoolOp):
879 assert isinstance(expr.op, (ast.And, ast.Or))
880 for subexpr in expr.values:
881 verify_ast(subexpr, variables_and_values)
882 else:
883 assert isinstance(expr.left.ctx, ast.Load)
884 assert len(expr.ops) == 1
885 assert isinstance(expr.ops[0], ast.Eq)
886 var_values = variables_and_values.setdefault(expr.left.id, set())
887 rhs = expr.comparators[0]
888 assert isinstance(rhs, (ast.Str, ast.Num))
889 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
890
891
892def verify_condition(condition, variables_and_values):
893 """Verifies the |condition| dictionary is in the expected format.
894 See verify_ast() for the meaning of |variables_and_values|.
895 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000896 VALID_INSIDE_CONDITION = ['variables']
897 assert isinstance(condition, list), condition
benrg@chromium.org609b7982013-02-07 16:44:46 +0000898 assert len(condition) == 2, condition
899 expr, then = condition
900
901 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
902 verify_ast(test_ast.body, variables_and_values)
903
904 assert isinstance(then, dict), then
905 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
906 verify_variables(then['variables'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000907
908
benrg@chromium.org609b7982013-02-07 16:44:46 +0000909def verify_root(value, variables_and_values):
910 """Verifies that |value| is the parsed form of a valid .isolate file.
911 See verify_ast() for the meaning of |variables_and_values|.
912 """
913 VALID_ROOTS = ['includes', 'conditions']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000914 assert isinstance(value, dict), value
915 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000916
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000917 includes = value.get('includes', [])
918 assert isinstance(includes, list), includes
919 for include in includes:
920 assert isinstance(include, basestring), include
921
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000922 conditions = value.get('conditions', [])
923 assert isinstance(conditions, list), conditions
924 for condition in conditions:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000925 verify_condition(condition, variables_and_values)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000926
927
benrg@chromium.org609b7982013-02-07 16:44:46 +0000928def remove_weak_dependencies(values, key, item, item_configs):
929 """Removes any configs from this key if the item is already under a
930 strong key.
931 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000932 if key == KEY_TOUCHED:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000933 item_configs = set(item_configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000934 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000935 try:
936 item_configs -= values[stronger_key][item]
937 except KeyError:
938 pass
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000939
benrg@chromium.org609b7982013-02-07 16:44:46 +0000940 return item_configs
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000941
942
benrg@chromium.org609b7982013-02-07 16:44:46 +0000943def remove_repeated_dependencies(folders, key, item, item_configs):
944 """Removes any configs from this key if the item is in a folder that is
945 already included."""
csharp@chromium.org31176252012-11-02 13:04:40 +0000946
947 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000948 item_configs = set(item_configs)
949 for (folder, configs) in folders.iteritems():
csharp@chromium.org31176252012-11-02 13:04:40 +0000950 if folder != item and item.startswith(folder):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000951 item_configs -= configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000952
benrg@chromium.org609b7982013-02-07 16:44:46 +0000953 return item_configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000954
955
956def get_folders(values_dict):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000957 """Returns a dict of all the folders in the given value_dict."""
958 return dict(
959 (item, configs) for (item, configs) in values_dict.iteritems()
960 if item.endswith('/')
961 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000962
963
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000964def invert_map(variables):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000965 """Converts {config: {deptype: list(depvals)}} to
966 {deptype: {depval: set(configs)}}.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000967 """
968 KEYS = (
969 KEY_TOUCHED,
970 KEY_TRACKED,
971 KEY_UNTRACKED,
972 'command',
973 'read_only',
974 )
975 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000976 for config, values in variables.iteritems():
977 for key in KEYS:
978 if key == 'command':
979 items = [tuple(values[key])] if key in values else []
980 elif key == 'read_only':
981 items = [values[key]] if key in values else []
982 else:
983 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
984 items = values.get(key, [])
985 for item in items:
986 out[key].setdefault(item, set()).add(config)
987 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000988
989
benrg@chromium.org609b7982013-02-07 16:44:46 +0000990def reduce_inputs(values):
991 """Reduces the output of invert_map() to the strictest minimum list.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000992
benrg@chromium.org609b7982013-02-07 16:44:46 +0000993 Looks at each individual file and directory, maps where they are used and
994 reconstructs the inverse dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000995
benrg@chromium.org609b7982013-02-07 16:44:46 +0000996 Returns the minimized dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000997 """
998 KEYS = (
999 KEY_TOUCHED,
1000 KEY_TRACKED,
1001 KEY_UNTRACKED,
1002 'command',
1003 'read_only',
1004 )
csharp@chromium.org31176252012-11-02 13:04:40 +00001005
1006 # Folders can only live in KEY_UNTRACKED.
1007 folders = get_folders(values.get(KEY_UNTRACKED, {}))
1008
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001009 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001010 for key in KEYS:
1011 for item, item_configs in values.get(key, {}).iteritems():
1012 item_configs = remove_weak_dependencies(values, key, item, item_configs)
1013 item_configs = remove_repeated_dependencies(
1014 folders, key, item, item_configs)
1015 if item_configs:
1016 out[key][item] = item_configs
1017 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001018
1019
benrg@chromium.org609b7982013-02-07 16:44:46 +00001020def convert_map_to_isolate_dict(values, config_variables):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001021 """Regenerates back a .isolate configuration dict from files and dirs
1022 mappings generated from reduce_inputs().
1023 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001024 # Gather a list of configurations for set inversion later.
1025 all_mentioned_configs = set()
1026 for configs_by_item in values.itervalues():
1027 for configs in configs_by_item.itervalues():
1028 all_mentioned_configs.update(configs)
1029
1030 # Invert the mapping to make it dict first.
1031 conditions = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001032 for key in values:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001033 for item, configs in values[key].iteritems():
1034 then = conditions.setdefault(frozenset(configs), {})
1035 variables = then.setdefault('variables', {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001036
benrg@chromium.org609b7982013-02-07 16:44:46 +00001037 if item in (True, False):
1038 # One-off for read_only.
1039 variables[key] = item
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001040 else:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001041 assert item
1042 if isinstance(item, tuple):
1043 # One-off for command.
1044 # Do not merge lists and do not sort!
1045 # Note that item is a tuple.
1046 assert key not in variables
1047 variables[key] = list(item)
1048 else:
1049 # The list of items (files or dirs). Append the new item and keep
1050 # the list sorted.
1051 l = variables.setdefault(key, [])
1052 l.append(item)
1053 l.sort()
1054
1055 if all_mentioned_configs:
1056 config_values = map(set, zip(*all_mentioned_configs))
1057 sef = short_expression_finder.ShortExpressionFinder(
1058 zip(config_variables, config_values))
1059
1060 conditions = sorted(
1061 [sef.get_expr(configs), then] for configs, then in conditions.iteritems())
1062 return {'conditions': conditions}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001063
1064
1065### Internal state files.
1066
1067
benrg@chromium.org609b7982013-02-07 16:44:46 +00001068class ConfigSettings(object):
1069 """Represents the dependency variables for a single build configuration.
1070 The structure is immutable.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001071 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001072 def __init__(self, config, values):
1073 self.config = config
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001074 verify_variables(values)
1075 self.touched = sorted(values.get(KEY_TOUCHED, []))
1076 self.tracked = sorted(values.get(KEY_TRACKED, []))
1077 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
1078 self.command = values.get('command', [])[:]
1079 self.read_only = values.get('read_only')
1080
1081 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001082 assert not (self.config and rhs.config) or (self.config == rhs.config)
maruel@chromium.org669edcb2012-11-02 19:16:14 +00001083 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001084 var = {
1085 KEY_TOUCHED: sorted(self.touched + rhs.touched),
1086 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
1087 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
1088 'command': self.command or rhs.command,
1089 'read_only': rhs.read_only if self.read_only is None else self.read_only,
1090 }
benrg@chromium.org609b7982013-02-07 16:44:46 +00001091 return ConfigSettings(self.config or rhs.config, var)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001092
1093 def flatten(self):
1094 out = {}
1095 if self.command:
1096 out['command'] = self.command
1097 if self.touched:
1098 out[KEY_TOUCHED] = self.touched
1099 if self.tracked:
1100 out[KEY_TRACKED] = self.tracked
1101 if self.untracked:
1102 out[KEY_UNTRACKED] = self.untracked
1103 if self.read_only is not None:
1104 out['read_only'] = self.read_only
1105 return out
1106
1107
1108class Configs(object):
1109 """Represents a processed .isolate file.
1110
benrg@chromium.org609b7982013-02-07 16:44:46 +00001111 Stores the file in a processed way, split by configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001112 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001113 def __init__(self, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001114 self.file_comment = file_comment
benrg@chromium.org609b7982013-02-07 16:44:46 +00001115 # The keys of by_config are tuples of values for the configuration
1116 # variables. The names of the variables (which must be the same for
1117 # every by_config key) are kept in config_variables. Initially by_config
1118 # is empty and we don't know what configuration variables will be used,
1119 # so config_variables also starts out empty. It will be set by the first
1120 # call to union() or merge_dependencies().
1121 self.by_config = {}
1122 self.config_variables = ()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001123
1124 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001125 """Adds variables from rhs (a Configs) to the existing variables.
1126 """
1127 config_variables = self.config_variables
1128 if not config_variables:
1129 config_variables = rhs.config_variables
1130 else:
1131 # We can't proceed if this isn't true since we don't know the correct
1132 # default values for extra variables. The variables are sorted so we
1133 # don't need to worry about permutations.
1134 if rhs.config_variables and rhs.config_variables != config_variables:
1135 raise ExecutionError(
1136 'Variables in merged .isolate files do not match: %r and %r' % (
1137 config_variables, rhs.config_variables))
1138
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001139 # Takes the first file comment, prefering lhs.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001140 out = Configs(self.file_comment or rhs.file_comment)
1141 out.config_variables = config_variables
1142 for config in set(self.by_config) | set(rhs.by_config):
1143 out.by_config[config] = union(
1144 self.by_config.get(config), rhs.by_config.get(config))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001145 return out
1146
benrg@chromium.org609b7982013-02-07 16:44:46 +00001147 def merge_dependencies(self, values, config_variables, configs):
1148 """Adds new dependencies to this object for the given configurations.
1149 Arguments:
1150 values: A variables dict as found in a .isolate file, e.g.,
1151 {KEY_TOUCHED: [...], 'command': ...}.
1152 config_variables: An ordered list of configuration variables, e.g.,
1153 ["OS", "chromeos"]. If this object already contains any dependencies,
1154 the configuration variables must match.
1155 configs: a list of tuples of values of the configuration variables,
1156 e.g., [("mac", 0), ("linux", 1)]. The dependencies in |values|
1157 are added to all of these configurations, and other configurations
1158 are unchanged.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001159 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001160 if not values:
1161 return
1162
1163 if not self.config_variables:
1164 self.config_variables = config_variables
1165 else:
1166 # See comment in Configs.union().
1167 assert self.config_variables == config_variables
1168
1169 for config in configs:
1170 self.by_config[config] = union(
1171 self.by_config.get(config), ConfigSettings(config, values))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001172
1173 def flatten(self):
1174 """Returns a flat dictionary representation of the configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001175 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001176 return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
1177
1178 def make_isolate_file(self):
1179 """Returns a dictionary suitable for writing to a .isolate file.
1180 """
1181 dependencies_by_config = self.flatten()
1182 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
1183 return convert_map_to_isolate_dict(configs_by_dependency,
1184 self.config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001185
1186
benrg@chromium.org609b7982013-02-07 16:44:46 +00001187# TODO(benrg): Remove this function when no old-format files are left.
1188def convert_old_to_new_format(value):
1189 """Converts from the old .isolate format, which only has one variable (OS),
1190 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
1191 and allows conditions that depend on the set of all OSes, to the new format,
1192 which allows any set of variables, has no hardcoded values, and only allows
1193 explicit positive tests of variable values.
1194 """
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001195 conditions = value.get('conditions', [])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001196 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
1197 return value # Nothing to change
1198
1199 def parse_condition(cond):
1200 return re.match(r'OS=="(\w+)"\Z', cond[0]).group(1)
1201
1202 oses = set(map(parse_condition, conditions))
1203 default_oses = set(['linux', 'mac', 'win'])
1204 oses = sorted(oses | default_oses)
1205
1206 def if_not_os(not_os, then):
1207 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
1208 return [expr, then]
1209
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001210 conditions = [
1211 cond[:2] for cond in conditions if cond[1]
1212 ] + [
1213 if_not_os(parse_condition(cond), cond[2])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001214 for cond in conditions if len(cond) == 3
1215 ]
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001216
benrg@chromium.org609b7982013-02-07 16:44:46 +00001217 if 'variables' in value:
1218 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
1219 conditions.sort()
1220
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001221 value = value.copy()
1222 value['conditions'] = conditions
benrg@chromium.org609b7982013-02-07 16:44:46 +00001223 return value
1224
1225
1226def load_isolate_as_config(isolate_dir, value, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001227 """Parses one .isolate file and returns a Configs() instance.
1228
1229 |value| is the loaded dictionary that was defined in the gyp file.
1230
1231 The expected format is strict, anything diverting from the format below will
1232 throw an assert:
1233 {
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001234 'includes': [
1235 'foo.isolate',
1236 ],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001237 'conditions': [
benrg@chromium.org609b7982013-02-07 16:44:46 +00001238 ['OS=="vms" and foo=42', {
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001239 'variables': {
benrg@chromium.org609b7982013-02-07 16:44:46 +00001240 'command': [
1241 ...
1242 ],
1243 'isolate_dependency_tracked': [
1244 ...
1245 ],
1246 'isolate_dependency_untracked': [
1247 ...
1248 ],
1249 'read_only': False,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001250 },
1251 }],
1252 ...
1253 ],
1254 }
1255 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001256 value = convert_old_to_new_format(value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001257
benrg@chromium.org609b7982013-02-07 16:44:46 +00001258 variables_and_values = {}
1259 verify_root(value, variables_and_values)
1260 if variables_and_values:
1261 config_variables, config_values = zip(
1262 *sorted(variables_and_values.iteritems()))
1263 all_configs = list(itertools.product(*config_values))
1264 else:
1265 config_variables = None
1266 all_configs = []
1267
1268 isolate = Configs(file_comment)
1269
1270 # Add configuration-specific variables.
1271 for expr, then in value.get('conditions', []):
1272 configs = match_configs(expr, config_variables, all_configs)
1273 isolate.merge_dependencies(then['variables'], config_variables, configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001274
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001275 # Load the includes.
1276 for include in value.get('includes', []):
1277 if os.path.isabs(include):
1278 raise ExecutionError(
1279 'Failed to load configuration; absolute include path \'%s\'' %
1280 include)
1281 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
1282 with open(included_isolate, 'r') as f:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001283 included_isolate = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001284 os.path.dirname(included_isolate),
1285 eval_content(f.read()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001286 None)
1287 isolate = union(isolate, included_isolate)
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001288
benrg@chromium.org609b7982013-02-07 16:44:46 +00001289 return isolate
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001290
1291
benrg@chromium.org609b7982013-02-07 16:44:46 +00001292def load_isolate_for_config(isolate_dir, content, variables):
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001293 """Loads the .isolate file and returns the information unprocessed but
1294 filtered for the specific OS.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001295
1296 Returns the command, dependencies and read_only flag. The dependencies are
1297 fixed to use os.path.sep.
1298 """
1299 # Load the .isolate file, process its conditions, retrieve the command and
1300 # dependencies.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001301 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
1302 try:
1303 config = tuple(variables[var] for var in isolate.config_variables)
1304 except KeyError:
1305 raise ExecutionError(
1306 'These configuration variables were missing from the command line: %s' %
1307 ', '.join(sorted(set(isolate.config_variables) - set(variables))))
1308 config = isolate.by_config.get(config)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001309 if not config:
csharp@chromium.org2a3d7d52013-03-23 12:54:37 +00001310 raise ExecutionError('Failed to load configuration for (%s)' %
1311 ', '.join(isolate.config_variables))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001312 # Merge tracked and untracked variables, isolate.py doesn't care about the
1313 # trackability of the variables, only the build tool does.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001314 dependencies = [
1315 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1316 ]
1317 touched = [f.replace('/', os.path.sep) for f in config.touched]
1318 return config.command, dependencies, touched, config.read_only
1319
1320
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001321def chromium_save_isolated(isolated, data, variables):
1322 """Writes one or many .isolated files.
1323
1324 This slightly increases the cold cache cost but greatly reduce the warm cache
1325 cost by splitting low-churn files off the master .isolated file. It also
1326 reduces overall isolateserver memcache consumption.
1327 """
1328 slaves = []
1329
1330 def extract_into_included_isolated(prefix):
1331 new_slave = {'files': {}, 'os': data['os']}
1332 for f in data['files'].keys():
1333 if f.startswith(prefix):
1334 new_slave['files'][f] = data['files'].pop(f)
1335 if new_slave['files']:
1336 slaves.append(new_slave)
1337
1338 # Split test/data/ in its own .isolated file.
1339 extract_into_included_isolated(os.path.join('test', 'data', ''))
1340
1341 # Split everything out of PRODUCT_DIR in its own .isolated file.
1342 if variables.get('PRODUCT_DIR'):
1343 extract_into_included_isolated(variables['PRODUCT_DIR'])
1344
1345 files = [isolated]
1346 for index, f in enumerate(slaves):
1347 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
1348 trace_inputs.write_json(slavepath, f, True)
1349 data.setdefault('includes', []).append(
1350 isolateserver_archive.sha1_file(slavepath))
1351 files.append(slavepath)
1352
1353 trace_inputs.write_json(isolated, data, True)
1354 return files
1355
1356
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001357class Flattenable(object):
1358 """Represents data that can be represented as a json file."""
1359 MEMBERS = ()
1360
1361 def flatten(self):
1362 """Returns a json-serializable version of itself.
1363
1364 Skips None entries.
1365 """
1366 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1367 return dict((member, value) for member, value in items if value is not None)
1368
1369 @classmethod
1370 def load(cls, data):
1371 """Loads a flattened version."""
1372 data = data.copy()
1373 out = cls()
1374 for member in out.MEMBERS:
1375 if member in data:
1376 # Access to a protected member XXX of a client class
1377 # pylint: disable=W0212
1378 out._load_member(member, data.pop(member))
1379 if data:
1380 raise ValueError(
1381 'Found unexpected entry %s while constructing an object %s' %
1382 (data, cls.__name__), data, cls.__name__)
1383 return out
1384
1385 def _load_member(self, member, value):
1386 """Loads a member into self."""
1387 setattr(self, member, value)
1388
1389 @classmethod
1390 def load_file(cls, filename):
1391 """Loads the data from a file or return an empty instance."""
1392 out = cls()
1393 try:
1394 out = cls.load(trace_inputs.read_json(filename))
1395 logging.debug('Loaded %s(%s)' % (cls.__name__, filename))
1396 except (IOError, ValueError):
1397 logging.warn('Failed to load %s' % filename)
1398 return out
1399
1400
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001401class SavedState(Flattenable):
1402 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001403
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001404 This file caches the items calculated by this script and is used to increase
1405 the performance of the script. This file is not loaded by run_isolated.py.
1406 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001407
1408 It is important to note that the 'files' dict keys are using native OS path
1409 separator instead of '/' used in .isolate file.
1410 """
1411 MEMBERS = (
1412 'command',
1413 'files',
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001414 'isolate_file',
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001415 'isolated_files',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001416 'read_only',
1417 'relative_cwd',
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001418 'variables',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001419 )
1420
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001421 def __init__(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001422 super(SavedState, self).__init__()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001423 self.command = []
1424 self.files = {}
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001425 # Link back to the .isolate file.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001426 self.isolate_file = None
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001427 # Used to support/remember 'slave' .isolated files.
1428 self.isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001429 self.read_only = None
1430 self.relative_cwd = None
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001431 # Variables are saved so a user can use isolate.py after building and the
1432 # GYP variables are still defined.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001433 self.variables = {'OS': get_flavor()}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001434
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001435 def update(self, isolate_file, variables):
1436 """Updates the saved state with new data to keep GYP variables and internal
1437 reference to the original .isolate file.
1438 """
1439 self.isolate_file = isolate_file
1440 self.variables.update(variables)
1441
1442 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
1443 """Updates the saved state with data necessary to generate a .isolated file.
1444 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001445 self.command = command
1446 # Add new files.
1447 for f in infiles:
1448 self.files.setdefault(f, {})
1449 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001450 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001451 # Prune extraneous files that are not a dependency anymore.
1452 for f in set(self.files).difference(set(infiles).union(touched)):
1453 del self.files[f]
1454 if read_only is not None:
1455 self.read_only = read_only
1456 self.relative_cwd = relative_cwd
1457
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001458 def to_isolated(self):
1459 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001460
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001461 http://chromium.org/developers/testing/isolated-testing/design
1462 """
1463 def strip(data):
1464 """Returns a 'files' entry with only the whitelisted keys."""
1465 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
1466
1467 out = {
1468 'files': dict(
1469 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001470 'os': self.variables['OS'],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001471 }
1472 if self.command:
1473 out['command'] = self.command
1474 if self.read_only is not None:
1475 out['read_only'] = self.read_only
1476 if self.relative_cwd:
1477 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001478 return out
1479
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001480 @classmethod
1481 def load(cls, data):
1482 out = super(SavedState, cls).load(data)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001483 if 'os' in data:
1484 out.variables['OS'] = data['os']
1485 if out.variables['OS'] != get_flavor():
1486 raise run_isolated.ConfigError(
1487 'The .isolated.state file was created on another platform')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001488 if out.isolate_file:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001489 out.isolate_file = trace_inputs.get_native_path_case(
1490 unicode(out.isolate_file))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001491 return out
1492
1493 def __str__(self):
1494 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001495 out += ' command: %s\n' % self.command
1496 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001497 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001498 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +00001499 out += ' relative_cwd: %s\n' % self.relative_cwd
1500 out += ' isolated_files: %s\n' % self.isolated_files
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001501 out += ' variables: %s' % ''.join(
1502 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1503 out += ')'
1504 return out
1505
1506
1507class CompleteState(object):
1508 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001509 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 super(CompleteState, self).__init__()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001511 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001512 # Contains the data to ease developer's use-case but that is not strictly
1513 # necessary.
1514 self.saved_state = saved_state
1515
1516 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001517 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001518 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001519 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001520 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001521 isolated_filepath,
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001522 SavedState.load_file(isolatedfile_to_state(isolated_filepath)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001523
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001524 def load_isolate(self, cwd, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001525 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001526 .isolate file.
1527
1528 Processes the loaded data, deduce root_dir, relative_cwd.
1529 """
1530 # Make sure to not depend on os.getcwd().
1531 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.orga3da9122013-03-28 13:27:09 +00001532 isolate_file = trace_inputs.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001533 logging.info(
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001534 'CompleteState.load_isolate(%s, %s, %s, %s)',
1535 cwd, isolate_file, variables, ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001536 relative_base_dir = os.path.dirname(isolate_file)
1537
1538 # Processes the variables and update the saved state.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001539 variables = process_variables(cwd, variables, relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001540 self.saved_state.update(isolate_file, variables)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001541 variables = self.saved_state.variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001542
1543 with open(isolate_file, 'r') as f:
1544 # At that point, variables are not replaced yet in command and infiles.
1545 # infiles may contain directory entries and is in posix style.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001546 command, infiles, touched, read_only = load_isolate_for_config(
1547 os.path.dirname(isolate_file), f.read(), variables)
1548 command = [eval_variables(i, variables) for i in command]
1549 infiles = [eval_variables(f, variables) for f in infiles]
1550 touched = [eval_variables(f, variables) for f in touched]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001551 # root_dir is automatically determined by the deepest root accessed with the
1552 # form '../../foo/bar'.
1553 root_dir = determine_root_dir(relative_base_dir, infiles + touched)
1554 # The relative directory is automatically determined by the relative path
1555 # between root_dir and the directory containing the .isolate file,
1556 # isolate_base_dir.
1557 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
benrg@chromium.org9ae72862013-02-11 05:05:51 +00001558 # Now that we know where the root is, check that the PATH_VARIABLES point
1559 # inside it.
1560 for i in PATH_VARIABLES:
1561 if i in variables:
1562 if not path_starts_with(
1563 root_dir, os.path.join(relative_base_dir, variables[i])):
1564 raise run_isolated.MappingError(
1565 'Path variable %s=%r points outside the inferred root directory' %
1566 (i, variables[i]))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001567 # Normalize the files based to root_dir. It is important to keep the
1568 # trailing os.path.sep at that step.
1569 infiles = [
1570 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1571 for f in infiles
1572 ]
1573 touched = [
1574 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1575 for f in touched
1576 ]
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001577 follow_symlinks = variables['OS'] != 'win'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001578 # Expand the directories by listing each file inside. Up to now, trailing
1579 # os.path.sep must be kept. Do not expand 'touched'.
1580 infiles = expand_directories_and_symlinks(
1581 root_dir,
1582 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001583 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001584 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +00001585 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001586
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001587 # If we ignore broken items then remove any missing touched items.
1588 if ignore_broken_items:
1589 original_touched_count = len(touched)
1590 touched = [touch for touch in touched if os.path.exists(touch)]
1591
1592 if len(touched) != original_touched_count:
1593 logging.info('warning: removed %d invalid touched entries',
1594 len(touched) - original_touched_count)
1595
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001596 # Finally, update the new data to be able to generate the foo.isolated file,
1597 # the file that is used by run_isolated.py.
1598 self.saved_state.update_isolated(
1599 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001600 logging.debug(self)
1601
maruel@chromium.org9268f042012-10-17 17:36:41 +00001602 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001603 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001604
maruel@chromium.org9268f042012-10-17 17:36:41 +00001605 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1606 file is tainted.
1607
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001608 See process_input() for more information.
1609 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001610 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001611 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001612 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001613 else:
1614 filepath = os.path.join(self.root_dir, infile)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001615 self.saved_state.files[infile] = process_input(
1616 filepath,
1617 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +00001618 self.saved_state.read_only,
1619 self.saved_state.variables['OS'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001620
1621 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001622 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001623 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001624 self.saved_state.isolated_files = chromium_save_isolated(
1625 self.isolated_filepath,
1626 self.saved_state.to_isolated(),
1627 self.saved_state.variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001628 total_bytes = sum(
1629 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001630 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001631 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001632 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001633 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001634 logging.debug('Dumping to %s' % saved_state_file)
1635 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1636
1637 @property
1638 def root_dir(self):
1639 """isolate_file is always inside relative_cwd relative to root_dir."""
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001640 if not self.saved_state.isolate_file:
1641 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001642 isolate_dir = os.path.dirname(self.saved_state.isolate_file)
1643 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001644 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001645 return isolate_dir
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001646 assert isolate_dir.endswith(self.saved_state.relative_cwd), (
1647 isolate_dir, self.saved_state.relative_cwd)
1648 return isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001649
1650 @property
1651 def resultdir(self):
1652 """Directory containing the results, usually equivalent to the variable
1653 PRODUCT_DIR.
1654 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001655 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001656
1657 def __str__(self):
1658 def indent(data, indent_length):
1659 """Indents text."""
1660 spacing = ' ' * indent_length
1661 return ''.join(spacing + l for l in str(data).splitlines(True))
1662
1663 out = '%s(\n' % self.__class__.__name__
1664 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001665 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1666 return out
1667
1668
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001669def load_complete_state(options, cwd, subdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001670 """Loads a CompleteState.
1671
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001672 This includes data from .isolate and .isolated.state files. Never reads the
1673 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001674
1675 Arguments:
1676 options: Options instance generated with OptionParserIsolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001677 """
maruel@chromium.orga3da9122013-03-28 13:27:09 +00001678 cwd = trace_inputs.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001679 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001680 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001681 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001682 else:
1683 # Constructs a dummy object that cannot be saved. Useful for temporary
1684 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001685 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001686 options.isolate = options.isolate or complete_state.saved_state.isolate_file
1687 if not options.isolate:
1688 raise ExecutionError('A .isolate file is required.')
1689 if (complete_state.saved_state.isolate_file and
1690 options.isolate != complete_state.saved_state.isolate_file):
1691 raise ExecutionError(
1692 '%s and %s do not match.' % (
1693 options.isolate, complete_state.saved_state.isolate_file))
1694
1695 # Then load the .isolate and expands directories.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001696 complete_state.load_isolate(
1697 cwd, options.isolate, options.variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001698
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001699 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001700 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001701 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001702 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1703 subdir = subdir.replace('/', os.path.sep)
1704 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001705 return complete_state
1706
1707
1708def read_trace_as_isolate_dict(complete_state):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001709 """Reads a trace and returns the .isolate dictionary.
1710
1711 Returns exceptions during the log parsing so it can be re-raised.
1712 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001713 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001714 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001715 if not os.path.isfile(logfile):
1716 raise ExecutionError(
1717 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1718 try:
maruel@chromium.orgd2627672013-04-03 15:30:24 +00001719 data = api.parse_log(logfile, chromium_default_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001720 exceptions = [i['exception'] for i in data if 'exception' in i]
1721 results = (i['results'] for i in data if 'results' in i)
1722 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1723 files = set(sum((result.existent for result in results_stripped), []))
1724 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001725 value = generate_isolate(
1726 tracked,
1727 [],
1728 touched,
1729 complete_state.root_dir,
1730 complete_state.saved_state.variables,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001731 complete_state.saved_state.relative_cwd)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001732 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001733 except trace_inputs.TracingFailure, e:
1734 raise ExecutionError(
1735 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001736 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001737
1738
1739def print_all(comment, data, stream):
1740 """Prints a complete .isolate file and its top-level file comment into a
1741 stream.
1742 """
1743 if comment:
1744 stream.write(comment)
1745 pretty_print(data, stream)
1746
1747
1748def merge(complete_state):
1749 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001750 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001751
1752 # Now take that data and union it into the original .isolate file.
1753 with open(complete_state.saved_state.isolate_file, 'r') as f:
1754 prev_content = f.read()
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001755 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001756 prev_config = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001757 isolate_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001758 eval_content(prev_content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001759 extract_comment(prev_content))
1760 new_config = load_isolate_as_config(isolate_dir, value, '')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001761 config = union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001762 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001763 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001764 with open(complete_state.saved_state.isolate_file, 'wb') as f:
1765 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001766 if exceptions:
1767 # It got an exception, raise the first one.
1768 raise \
1769 exceptions[0][0], \
1770 exceptions[0][1], \
1771 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001772
1773
1774def CMDcheck(args):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001775 """Checks that all the inputs are present and generates .isolated."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001776 parser = OptionParserIsolate(command='check')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001777 parser.add_option('--subdir', help='Filters to a subdirectory')
1778 options, args = parser.parse_args(args)
1779 if args:
1780 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001781 complete_state = load_complete_state(options, os.getcwd(), options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001782
1783 # Nothing is done specifically. Just store the result and state.
1784 complete_state.save_files()
1785 return 0
1786
1787
1788def CMDhashtable(args):
1789 """Creates a hash table content addressed object store.
1790
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001791 All the files listed in the .isolated file are put in the output directory
1792 with the file name being the sha-1 of the file's content.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001793 """
1794 parser = OptionParserIsolate(command='hashtable')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001795 parser.add_option('--subdir', help='Filters to a subdirectory')
1796 options, args = parser.parse_args(args)
1797 if args:
1798 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001799
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001800 with run_isolated.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001801 success = False
1802 try:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001803 complete_state = load_complete_state(options, os.getcwd(), options.subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001804 options.outdir = (
1805 options.outdir or os.path.join(complete_state.resultdir, 'hashtable'))
1806 # Make sure that complete_state isn't modified until save_files() is
1807 # called, because any changes made to it here will propagate to the files
1808 # created (which is probably not intended).
1809 complete_state.save_files()
1810
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001811 infiles = complete_state.saved_state.files
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001812 # Add all the .isolated files.
1813 for item in complete_state.saved_state.isolated_files:
1814 item_path = os.path.join(
1815 os.path.dirname(complete_state.isolated_filepath), item)
1816 with open(item_path, 'rb') as f:
1817 content = f.read()
1818 isolated_metadata = {
1819 'h': hashlib.sha1(content).hexdigest(),
1820 's': len(content),
1821 'priority': '0'
1822 }
1823 infiles[item_path] = isolated_metadata
1824
1825 logging.info('Creating content addressed object store with %d item',
1826 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001827
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001828 if is_url(options.outdir):
maruel@chromium.orgc6f90062012-11-07 18:32:22 +00001829 isolateserver_archive.upload_sha1_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001830 base_url=options.outdir,
1831 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00001832 infiles=infiles,
1833 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001834 else:
1835 recreate_tree(
1836 outdir=options.outdir,
1837 indir=complete_state.root_dir,
1838 infiles=infiles,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001839 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001840 as_sha1=True)
1841 success = True
1842 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001843 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001844 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001845 if not success and os.path.isfile(options.isolated):
1846 os.remove(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001847
1848
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001849def CMDmerge(args):
1850 """Reads and merges the data from the trace back into the original .isolate.
1851
1852 Ignores --outdir.
1853 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001854 parser = OptionParserIsolate(command='merge', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001855 options, args = parser.parse_args(args)
1856 if args:
1857 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001858 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001859 merge(complete_state)
1860 return 0
1861
1862
1863def CMDread(args):
1864 """Reads the trace file generated with command 'trace'.
1865
1866 Ignores --outdir.
1867 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001868 parser = OptionParserIsolate(command='read', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001869 options, args = parser.parse_args(args)
1870 if args:
1871 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001872 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001873 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001874 pretty_print(value, sys.stdout)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001875 if exceptions:
1876 # It got an exception, raise the first one.
1877 raise \
1878 exceptions[0][0], \
1879 exceptions[0][1], \
1880 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001881 return 0
1882
1883
1884def CMDremap(args):
1885 """Creates a directory with all the dependencies mapped into it.
1886
1887 Useful to test manually why a test is failing. The target executable is not
1888 run.
1889 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001890 parser = OptionParserIsolate(command='remap', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001891 options, args = parser.parse_args(args)
1892 if args:
1893 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001894 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001895
1896 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001897 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001898 'isolate', complete_state.root_dir)
1899 else:
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001900 if is_url(options.outdir):
1901 raise ExecutionError('Can\'t use url for --outdir with mode remap')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001902 if not os.path.isdir(options.outdir):
1903 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001904 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001905 if len(os.listdir(options.outdir)):
1906 raise ExecutionError('Can\'t remap in a non-empty directory')
1907 recreate_tree(
1908 outdir=options.outdir,
1909 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001910 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001911 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001912 as_sha1=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001913 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001914 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001915
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001916 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001917 complete_state.save_files()
1918 return 0
1919
1920
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001921def CMDrewrite(args):
1922 """Rewrites a .isolate file into the canonical format."""
1923 parser = OptionParserIsolate(command='rewrite', require_isolated=False)
1924 options, args = parser.parse_args(args)
1925 if args:
1926 parser.error('Unsupported argument: %s' % args)
1927
1928 if options.isolated:
1929 # Load the previous state if it was present. Namely, "foo.isolated.state".
1930 complete_state = CompleteState.load_files(options.isolated)
1931 else:
1932 # Constructs a dummy object that cannot be saved. Useful for temporary
1933 # commands like 'run'.
1934 complete_state = CompleteState(None, SavedState())
1935 isolate = options.isolate or complete_state.saved_state.isolate_file
1936 if not isolate:
1937 raise ExecutionError('A .isolate file is required.')
1938 with open(isolate, 'r') as f:
1939 content = f.read()
1940 config = load_isolate_as_config(
1941 os.path.dirname(os.path.abspath(isolate)),
1942 eval_content(content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001943 extract_comment(content))
1944 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001945 print('Updating %s' % isolate)
1946 with open(isolate, 'wb') as f:
1947 print_all(config.file_comment, data, f)
1948 return 0
1949
1950
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001951def CMDrun(args):
1952 """Runs the test executable in an isolated (temporary) directory.
1953
1954 All the dependencies are mapped into the temporary directory and the
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001955 directory is cleaned up after the target exits. Warning: if --outdir is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001956 specified, it is deleted upon exit.
1957
1958 Argument processing stops at the first non-recognized argument and these
1959 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001960 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001961 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001962 parser = OptionParserIsolate(command='run', require_isolated=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001963 parser.enable_interspersed_args()
1964 options, args = parser.parse_args(args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001965 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001966 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001967 if not cmd:
1968 raise ExecutionError('No command to run')
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001969 if options.outdir and is_url(options.outdir):
1970 raise ExecutionError('Can\'t use url for --outdir with mode run')
1971
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001972 cmd = trace_inputs.fix_python_path(cmd)
1973 try:
1974 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001975 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001976 'isolate', complete_state.root_dir)
1977 else:
1978 if not os.path.isdir(options.outdir):
1979 os.makedirs(options.outdir)
1980 recreate_tree(
1981 outdir=options.outdir,
1982 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001983 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001984 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001985 as_sha1=False)
1986 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001987 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001988 if not os.path.isdir(cwd):
1989 # It can happen when no files are mapped from the directory containing the
1990 # .isolate file. But the directory must exist to be the current working
1991 # directory.
1992 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001993 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001994 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001995 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1996 result = subprocess.call(cmd, cwd=cwd)
1997 finally:
1998 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001999 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002000
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002001 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002002 complete_state.save_files()
2003 return result
2004
2005
2006def CMDtrace(args):
2007 """Traces the target using trace_inputs.py.
2008
2009 It runs the executable without remapping it, and traces all the files it and
2010 its child processes access. Then the 'read' command can be used to generate an
2011 updated .isolate file out of it.
2012
2013 Argument processing stops at the first non-recognized argument and these
2014 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002015 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002016 """
2017 parser = OptionParserIsolate(command='trace')
2018 parser.enable_interspersed_args()
2019 parser.add_option(
2020 '-m', '--merge', action='store_true',
2021 help='After tracing, merge the results back in the .isolate file')
2022 options, args = parser.parse_args(args)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002023 complete_state = load_complete_state(options, os.getcwd(), None)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002024 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002025 if not cmd:
2026 raise ExecutionError('No command to run')
2027 cmd = trace_inputs.fix_python_path(cmd)
2028 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002029 unicode(complete_state.root_dir),
2030 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00002031 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
2032 if not os.path.isfile(cmd[0]):
2033 raise ExecutionError(
2034 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002035 logging.info('Running %s, cwd=%s' % (cmd, cwd))
2036 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002037 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002038 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002039 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002040 try:
2041 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002042 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002043 cmd,
2044 cwd,
2045 'default',
2046 True)
2047 except trace_inputs.TracingFailure, e:
2048 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
2049
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002050 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002051 logging.error(
2052 'Tracer exited with %d, which means the tests probably failed so the '
2053 'trace is probably incomplete.', result)
2054 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002055
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002056 complete_state.save_files()
2057
2058 if options.merge:
2059 merge(complete_state)
2060
2061 return result
2062
2063
maruel@chromium.org712454d2013-04-04 17:52:34 +00002064def _process_variable_arg(_option, _opt, _value, parser):
2065 if not parser.rargs:
2066 raise optparse.OptionValueError(
2067 'Please use --variable FOO=BAR or --variable FOO BAR')
2068 k = parser.rargs.pop(0)
2069 if '=' in k:
2070 parser.values.variables.append(tuple(k.split('=', 1)))
2071 else:
2072 if not parser.rargs:
2073 raise optparse.OptionValueError(
2074 'Please use --variable FOO=BAR or --variable FOO BAR')
2075 v = parser.rargs.pop(0)
2076 parser.values.variables.append((k, v))
2077
2078
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002079def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002080 """Adds --isolated and --variable to an OptionParser."""
2081 parser.add_option(
2082 '-s', '--isolated',
2083 metavar='FILE',
2084 help='.isolated file to generate or read')
2085 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002086 parser.add_option(
2087 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002088 dest='isolated',
2089 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002090 default_variables = [('OS', get_flavor())]
2091 if sys.platform in ('win32', 'cygwin'):
2092 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
2093 else:
2094 default_variables.append(('EXECUTABLE_SUFFIX', ''))
2095 parser.add_option(
2096 '-V', '--variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00002097 action='callback',
2098 callback=_process_variable_arg,
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002099 default=default_variables,
2100 dest='variables',
2101 metavar='FOO BAR',
2102 help='Variables to process in the .isolate file, default: %default. '
2103 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002104 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002105
2106
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002107def parse_isolated_option(parser, options, cwd, require_isolated):
2108 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002109 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002110 options.isolated = os.path.normpath(
2111 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002112 if require_isolated and not options.isolated:
csharp@chromium.org707f0452012-11-26 21:50:40 +00002113 parser.error('--isolated is required. Visit http://chromium.org/developers/'
2114 'testing/isolated-testing#TOC-Where-can-I-find-the-.isolated-'
2115 'file- to see how to create the .isolated file.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002116 if options.isolated and not options.isolated.endswith('.isolated'):
2117 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002118
2119
2120def parse_variable_option(options):
2121 """Processes --variable."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002122 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
2123 # but it wouldn't be backward compatible.
2124 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002125 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002126 try:
2127 return int(s)
2128 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002129 return s.decode('utf-8')
benrg@chromium.org609b7982013-02-07 16:44:46 +00002130 options.variables = dict((k, try_make_int(v)) for k, v in options.variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002131
2132
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002133class OptionParserIsolate(trace_inputs.OptionParserWithNiceDescription):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002134 """Adds automatic --isolate, --isolated, --out and --variable handling."""
2135 def __init__(self, require_isolated=True, **kwargs):
maruel@chromium.org55276902012-10-05 20:56:19 +00002136 trace_inputs.OptionParserWithNiceDescription.__init__(
2137 self,
2138 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
2139 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002140 group = optparse.OptionGroup(self, "Common options")
2141 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002142 '-i', '--isolate',
2143 metavar='FILE',
2144 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002145 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002146 group.add_option(
2147 '-o', '--outdir', metavar='DIR',
2148 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002149 'Defaults: run|remap: a /tmp subdirectory, others: '
2150 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00002151 group.add_option(
2152 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002153 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
2154 help='Indicates that invalid entries in the isolated file to be '
2155 'only be logged and not stop processing. Defaults to True if '
2156 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002157 self.add_option_group(group)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002158 self.require_isolated = require_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002159
2160 def parse_args(self, *args, **kwargs):
2161 """Makes sure the paths make sense.
2162
2163 On Windows, / and \ are often mixed together in a path.
2164 """
2165 options, args = trace_inputs.OptionParserWithNiceDescription.parse_args(
2166 self, *args, **kwargs)
2167 if not self.allow_interspersed_args and args:
2168 self.error('Unsupported argument: %s' % args)
2169
maruel@chromium.orga3da9122013-03-28 13:27:09 +00002170 cwd = trace_inputs.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002171 parse_isolated_option(self, options, cwd, self.require_isolated)
2172 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002173
2174 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002175 # TODO(maruel): Work with non-ASCII.
2176 # The path must be in native path case for tracing purposes.
2177 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
2178 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
2179 options.isolate = trace_inputs.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002180
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002181 if options.outdir and not is_url(options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002182 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
2183 # outdir doesn't need native path case since tracing is never done from
2184 # there.
2185 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002186
2187 return options, args
2188
2189
2190### Glue code to make all the commands works magically.
2191
2192
2193CMDhelp = trace_inputs.CMDhelp
2194
2195
2196def main(argv):
2197 try:
2198 return trace_inputs.main_impl(argv)
2199 except (
2200 ExecutionError,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002201 run_isolated.MappingError,
2202 run_isolated.ConfigError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002203 sys.stderr.write('\nError: ')
2204 sys.stderr.write(str(e))
2205 sys.stderr.write('\n')
2206 return 1
2207
2208
2209if __name__ == '__main__':
2210 sys.exit(main(sys.argv[1:]))