blob: cc0e150877dc16cf4de2a120b71ddc67035a8a35 [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
maruel@chromium.orge5322512013-08-19 20:17:57 +00006"""Front end tool to operate on .isolate files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00007
maruel@chromium.orge5322512013-08-19 20:17:57 +00008This includes creating, merging or compiling them to generate a .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00009
10See more information at
maruel@chromium.orge5322512013-08-19 20:17:57 +000011 https://code.google.com/p/swarming/wiki/IsolateDesign
12 https://code.google.com/p/swarming/wiki/IsolateUserGuide
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000013"""
maruel@chromium.orge5322512013-08-19 20:17:57 +000014# Run ./isolate.py --help for more detailed information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000015
benrg@chromium.org609b7982013-02-07 16:44:46 +000016import ast
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000017import copy
benrg@chromium.org609b7982013-02-07 16:44:46 +000018import itertools
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000019import logging
20import optparse
21import os
22import posixpath
23import re
24import stat
25import subprocess
26import sys
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000027
maruel@chromium.orgfb78d432013-08-28 21:22:40 +000028import isolateserver
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000029import run_isolated
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
maruel@chromium.orge5322512013-08-19 20:17:57 +000035from third_party import colorama
36from third_party.depot_tools import fix_encoding
37from third_party.depot_tools import subcommand
38
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000039from utils import tools
maruel@chromium.orgb61979a2013-08-29 15:18:51 +000040from utils import short_expression_finder
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000041
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000042
maruel@chromium.org29029882013-08-30 12:15:40 +000043__version__ = '0.1.1'
maruel@chromium.org3d671992013-08-20 00:38:27 +000044
45
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000046PATH_VARIABLES = ('DEPTH', 'PRODUCT_DIR')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000047
48# Files that should be 0-length when mapped.
49KEY_TOUCHED = 'isolate_dependency_touched'
50# Files that should be tracked by the build tool.
51KEY_TRACKED = 'isolate_dependency_tracked'
52# Files that should not be tracked by the build tool.
53KEY_UNTRACKED = 'isolate_dependency_untracked'
54
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000055
56class ExecutionError(Exception):
57 """A generic error occurred."""
58 def __str__(self):
59 return self.args[0]
60
61
62### Path handling code.
63
64
maruel@chromium.org3683afe2013-07-27 00:09:27 +000065DEFAULT_BLACKLIST = (
66 # Temporary vim or python files.
67 r'^.+\.(?:pyc|swp)$',
68 # .git or .svn directory.
69 r'^(?:.+' + re.escape(os.path.sep) + r'|)\.(?:git|svn)$',
70)
71
72
73# Chromium-specific.
74DEFAULT_BLACKLIST += (
75 r'^.+\.(?:run_test_cases)$',
76 r'^(?:.+' + re.escape(os.path.sep) + r'|)testserver\.log$',
77)
78
79
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000080def relpath(path, root):
81 """os.path.relpath() that keeps trailing os.path.sep."""
82 out = os.path.relpath(path, root)
83 if path.endswith(os.path.sep):
84 out += os.path.sep
85 return out
86
87
maruel@chromium.org8abec8b2013-04-16 19:34:20 +000088def safe_relpath(filepath, basepath):
89 """Do not throw on Windows when filepath and basepath are on different drives.
90
91 Different than relpath() above since this one doesn't keep the trailing
92 os.path.sep and it swallows exceptions on Windows and return the original
93 absolute path in the case of different drives.
94 """
95 try:
96 return os.path.relpath(filepath, basepath)
97 except ValueError:
98 assert sys.platform == 'win32'
99 return filepath
100
101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000102def normpath(path):
103 """os.path.normpath() that keeps trailing os.path.sep."""
104 out = os.path.normpath(path)
105 if path.endswith(os.path.sep):
106 out += os.path.sep
107 return out
108
109
110def posix_relpath(path, root):
111 """posix.relpath() that keeps trailing slash."""
112 out = posixpath.relpath(path, root)
113 if path.endswith('/'):
114 out += '/'
115 return out
116
117
118def cleanup_path(x):
119 """Cleans up a relative path. Converts any os.path.sep to '/' on Windows."""
120 if x:
121 x = x.rstrip(os.path.sep).replace(os.path.sep, '/')
122 if x == '.':
123 x = ''
124 if x:
125 x += '/'
126 return x
127
128
maruel@chromium.orgb9520b02013-03-13 18:00:03 +0000129def is_url(path):
130 return bool(re.match(r'^https?://.+$', path))
131
132
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000133def path_starts_with(prefix, path):
134 """Returns true if the components of the path |prefix| are the same as the
135 initial components of |path| (or all of the components of |path|). The paths
136 must be absolute.
137 """
138 assert os.path.isabs(prefix) and os.path.isabs(path)
139 prefix = os.path.normpath(prefix)
140 path = os.path.normpath(path)
141 assert prefix == trace_inputs.get_native_path_case(prefix), prefix
142 assert path == trace_inputs.get_native_path_case(path), path
143 prefix = prefix.rstrip(os.path.sep) + os.path.sep
144 path = path.rstrip(os.path.sep) + os.path.sep
145 return path.startswith(prefix)
146
147
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000148def fix_native_path_case(root, path):
149 """Ensures that each component of |path| has the proper native case by
150 iterating slowly over the directory elements of |path|."""
151 native_case_path = root
152 for raw_part in path.split(os.sep):
153 if not raw_part or raw_part == '.':
154 break
155
156 part = trace_inputs.find_item_native_case(native_case_path, raw_part)
157 if not part:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000158 raise isolateserver.MappingError(
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000159 'Input file %s doesn\'t exist' %
160 os.path.join(native_case_path, raw_part))
161 native_case_path = os.path.join(native_case_path, part)
162
163 return os.path.normpath(native_case_path)
164
165
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000166def expand_symlinks(indir, relfile):
167 """Follows symlinks in |relfile|, but treating symlinks that point outside the
168 build tree as if they were ordinary directories/files. Returns the final
169 symlink-free target and a list of paths to symlinks encountered in the
170 process.
171
172 The rule about symlinks outside the build tree is for the benefit of the
173 Chromium OS ebuild, which symlinks the output directory to an unrelated path
174 in the chroot.
175
176 Fails when a directory loop is detected, although in theory we could support
177 that case.
178 """
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000179 is_directory = relfile.endswith(os.path.sep)
180 done = indir
181 todo = relfile.strip(os.path.sep)
182 symlinks = []
183
184 while todo:
185 pre_symlink, symlink, post_symlink = trace_inputs.split_at_symlink(
186 done, todo)
187 if not symlink:
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000188 todo = fix_native_path_case(done, todo)
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000189 done = os.path.join(done, todo)
190 break
191 symlink_path = os.path.join(done, pre_symlink, symlink)
192 post_symlink = post_symlink.lstrip(os.path.sep)
193 # readlink doesn't exist on Windows.
194 # pylint: disable=E1101
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000195 target = os.path.normpath(os.path.join(done, pre_symlink))
196 symlink_target = os.readlink(symlink_path)
maruel@chromium.org28c19672013-04-29 18:51:09 +0000197 if os.path.isabs(symlink_target):
198 # Absolute path are considered a normal directories. The use case is
199 # generally someone who puts the output directory on a separate drive.
200 target = symlink_target
201 else:
202 # The symlink itself could be using the wrong path case.
203 target = fix_native_path_case(target, symlink_target)
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000204
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000205 if not os.path.exists(target):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000206 raise isolateserver.MappingError(
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000207 'Symlink target doesn\'t exist: %s -> %s' % (symlink_path, target))
208 target = trace_inputs.get_native_path_case(target)
209 if not path_starts_with(indir, target):
210 done = symlink_path
211 todo = post_symlink
212 continue
213 if path_starts_with(target, symlink_path):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000214 raise isolateserver.MappingError(
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000215 'Can\'t map recursive symlink reference %s -> %s' %
216 (symlink_path, target))
217 logging.info('Found symlink: %s -> %s', symlink_path, target)
218 symlinks.append(os.path.relpath(symlink_path, indir))
219 # Treat the common prefix of the old and new paths as done, and start
220 # scanning again.
221 target = target.split(os.path.sep)
222 symlink_path = symlink_path.split(os.path.sep)
223 prefix_length = 0
224 for target_piece, symlink_path_piece in zip(target, symlink_path):
225 if target_piece == symlink_path_piece:
226 prefix_length += 1
227 else:
228 break
229 done = os.path.sep.join(target[:prefix_length])
230 todo = os.path.join(
231 os.path.sep.join(target[prefix_length:]), post_symlink)
232
233 relfile = os.path.relpath(done, indir)
234 relfile = relfile.rstrip(os.path.sep) + is_directory * os.path.sep
235 return relfile, symlinks
236
237
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000238def expand_directory_and_symlink(indir, relfile, blacklist, follow_symlinks):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000239 """Expands a single input. It can result in multiple outputs.
240
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000241 This function is recursive when relfile is a directory.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000242
243 Note: this code doesn't properly handle recursive symlink like one created
244 with:
245 ln -s .. foo
246 """
247 if os.path.isabs(relfile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000248 raise isolateserver.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000249 'Can\'t map absolute path %s' % relfile)
250
251 infile = normpath(os.path.join(indir, relfile))
252 if not infile.startswith(indir):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000253 raise isolateserver.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000254 'Can\'t map file %s outside %s' % (infile, indir))
255
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000256 filepath = os.path.join(indir, relfile)
257 native_filepath = trace_inputs.get_native_path_case(filepath)
258 if filepath != native_filepath:
maruel@chromium.org59bb2a32013-03-21 17:08:39 +0000259 # Special case './'.
260 if filepath != native_filepath + '.' + os.path.sep:
maruel@chromium.org7f66a982013-06-06 15:58:59 +0000261 # Give up enforcing strict path case on OSX. Really, it's that sad. The
262 # case where it happens is very specific and hard to reproduce:
263 # get_native_path_case(
264 # u'Foo.framework/Versions/A/Resources/Something.nib') will return
265 # u'Foo.framework/Versions/A/resources/Something.nib', e.g. lowercase 'r'.
266 #
267 # Note that this is really something deep in OSX because running
268 # ls Foo.framework/Versions/A
269 # will print out 'Resources', while trace_inputs.get_native_path_case()
270 # returns a lower case 'r'.
271 #
272 # So *something* is happening under the hood resulting in the command 'ls'
273 # and Carbon.File.FSPathMakeRef('path').FSRefMakePath() to disagree. We
274 # have no idea why.
275 if sys.platform != 'darwin':
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000276 raise isolateserver.MappingError(
maruel@chromium.org7f66a982013-06-06 15:58:59 +0000277 'File path doesn\'t equal native file path\n%s != %s' %
278 (filepath, native_filepath))
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000279
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000280 symlinks = []
281 if follow_symlinks:
282 relfile, symlinks = expand_symlinks(indir, relfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000283
284 if relfile.endswith(os.path.sep):
285 if not os.path.isdir(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000286 raise isolateserver.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000287 '%s is not a directory but ends with "%s"' % (infile, os.path.sep))
288
maruel@chromium.org59bb2a32013-03-21 17:08:39 +0000289 # Special case './'.
290 if relfile.startswith('.' + os.path.sep):
291 relfile = relfile[2:]
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000292 outfiles = symlinks
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000293 try:
294 for filename in os.listdir(infile):
295 inner_relfile = os.path.join(relfile, filename)
296 if blacklist(inner_relfile):
297 continue
298 if os.path.isdir(os.path.join(indir, inner_relfile)):
299 inner_relfile += os.path.sep
300 outfiles.extend(
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000301 expand_directory_and_symlink(indir, inner_relfile, blacklist,
302 follow_symlinks))
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000303 return outfiles
304 except OSError as e:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000305 raise isolateserver.MappingError(
maruel@chromium.org1cd786e2013-04-26 18:48:40 +0000306 'Unable to iterate over directory %s.\n%s' % (infile, e))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000307 else:
308 # Always add individual files even if they were blacklisted.
309 if os.path.isdir(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000310 raise isolateserver.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000311 'Input directory %s must have a trailing slash' % infile)
312
313 if not os.path.isfile(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000314 raise isolateserver.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000315 'Input file %s doesn\'t exist' % infile)
316
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000317 return symlinks + [relfile]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000318
319
csharp@chromium.org01856802012-11-12 17:48:13 +0000320def expand_directories_and_symlinks(indir, infiles, blacklist,
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000321 follow_symlinks, ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000322 """Expands the directories and the symlinks, applies the blacklist and
323 verifies files exist.
324
325 Files are specified in os native path separator.
326 """
327 outfiles = []
328 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +0000329 try:
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000330 outfiles.extend(expand_directory_and_symlink(indir, relfile, blacklist,
331 follow_symlinks))
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000332 except isolateserver.MappingError as e:
csharp@chromium.org01856802012-11-12 17:48:13 +0000333 if ignore_broken_items:
334 logging.info('warning: %s', e)
335 else:
336 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000337 return outfiles
338
339
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000340def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000341 """Creates a new tree with only the input files in it.
342
343 Arguments:
344 outdir: Output directory to create the files in.
345 indir: Root directory the infiles are based in.
346 infiles: dict of files to map from |indir| to |outdir|.
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000347 action: One of accepted action of run_isolated.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000348 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000349 """
350 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000351 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
352 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000353
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000354 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000355 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000356 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000357 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000358
359 for relfile, metadata in infiles.iteritems():
360 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000361 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000362 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000363 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000364 # Skip links when storing a hashtable.
365 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000366 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000367 if os.path.isfile(outfile):
368 # Just do a quick check that the file size matches. No need to stat()
369 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000370 if not 's' in metadata:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000371 raise isolateserver.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000372 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000373 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000374 continue
375 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000376 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000377 os.remove(outfile)
378 else:
379 outfile = os.path.join(outdir, relfile)
380 outsubdir = os.path.dirname(outfile)
381 if not os.path.isdir(outsubdir):
382 os.makedirs(outsubdir)
383
384 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000385 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000386 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000387 if 'l' in metadata:
388 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000389 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000390 # symlink doesn't exist on Windows.
391 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000392 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000393 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000394
395
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000396def process_input(filepath, prevdict, read_only, flavor, algo):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000397 """Processes an input file, a dependency, and return meta data about it.
398
399 Arguments:
400 - filepath: File to act on.
401 - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
402 to skip recalculating the hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000403 - read_only: If True, the file mode is manipulated. In practice, only save
404 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
405 windows, mode is not set since all files are 'executable' by
406 default.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000407 - algo: Hashing algorithm used.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000408
409 Behaviors:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000410 - Retrieves the file mode, file size, file timestamp, file link
411 destination if it is a file link and calcultate the SHA-1 of the file's
412 content if the path points to a file and not a symlink.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000413 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000414 out = {}
415 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000416 # if prevdict.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000417 # # The file's content is ignored. Skip the time and hard code mode.
418 # if get_flavor() != 'win':
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000419 # out['m'] = stat.S_IRUSR | stat.S_IRGRP
420 # out['s'] = 0
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000421 # out['h'] = algo().hexdigest()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000422 # out['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000423 # return out
424
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000425 # Always check the file stat and check if it is a link. The timestamp is used
426 # to know if the file's content/symlink destination should be looked into.
427 # E.g. only reuse from prevdict if the timestamp hasn't changed.
428 # There is the risk of the file's timestamp being reset to its last value
429 # manually while its content changed. We don't protect against that use case.
430 try:
431 filestats = os.lstat(filepath)
432 except OSError:
433 # The file is not present.
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000434 raise isolateserver.MappingError('%s is missing' % filepath)
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000435 is_link = stat.S_ISLNK(filestats.st_mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000436
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000437 if flavor != 'win':
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000438 # Ignore file mode on Windows since it's not really useful there.
439 filemode = stat.S_IMODE(filestats.st_mode)
440 # Remove write access for group and all access to 'others'.
441 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
442 if read_only:
443 filemode &= ~stat.S_IWUSR
444 if filemode & stat.S_IXUSR:
445 filemode |= stat.S_IXGRP
446 else:
447 filemode &= ~stat.S_IXGRP
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000448 if not is_link:
449 out['m'] = filemode
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000450
451 # Used to skip recalculating the hash or link destination. Use the most recent
452 # update time.
453 # TODO(maruel): Save it in the .state file instead of .isolated so the
454 # .isolated file is deterministic.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000455 out['t'] = int(round(filestats.st_mtime))
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000456
457 if not is_link:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000458 out['s'] = filestats.st_size
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000459 # If the timestamp wasn't updated and the file size is still the same, carry
460 # on the sha-1.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000461 if (prevdict.get('t') == out['t'] and
462 prevdict.get('s') == out['s']):
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000463 # Reuse the previous hash if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000464 out['h'] = prevdict.get('h')
465 if not out.get('h'):
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000466 out['h'] = isolateserver.hash_file(filepath, algo)
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000467 else:
468 # If the timestamp wasn't updated, carry on the link destination.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000469 if prevdict.get('t') == out['t']:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000470 # Reuse the previous link destination if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000471 out['l'] = prevdict.get('l')
472 if out.get('l') is None:
maruel@chromium.org8d159e32013-04-18 15:29:50 +0000473 # The link could be in an incorrect path case. In practice, this only
474 # happen on OSX on case insensitive HFS.
475 # TODO(maruel): It'd be better if it was only done once, in
476 # expand_directory_and_symlink(), so it would not be necessary to do again
477 # here.
478 symlink_value = os.readlink(filepath) # pylint: disable=E1101
479 filedir = trace_inputs.get_native_path_case(os.path.dirname(filepath))
480 native_dest = fix_native_path_case(filedir, symlink_value)
481 out['l'] = os.path.relpath(native_dest, filedir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000482 return out
483
484
485### Variable stuff.
486
487
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000488def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000489 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000490 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000491
492
493def determine_root_dir(relative_root, infiles):
494 """For a list of infiles, determines the deepest root directory that is
495 referenced indirectly.
496
497 All arguments must be using os.path.sep.
498 """
499 # The trick used to determine the root directory is to look at "how far" back
500 # up it is looking up.
501 deepest_root = relative_root
502 for i in infiles:
503 x = relative_root
504 while i.startswith('..' + os.path.sep):
505 i = i[3:]
506 assert not i.startswith(os.path.sep)
507 x = os.path.dirname(x)
508 if deepest_root.startswith(x):
509 deepest_root = x
510 logging.debug(
511 'determine_root_dir(%s, %d files) -> %s' % (
512 relative_root, len(infiles), deepest_root))
513 return deepest_root
514
515
516def replace_variable(part, variables):
517 m = re.match(r'<\(([A-Z_]+)\)', part)
518 if m:
519 if m.group(1) not in variables:
520 raise ExecutionError(
521 'Variable "%s" was not found in %s.\nDid you forget to specify '
522 '--variable?' % (m.group(1), variables))
523 return variables[m.group(1)]
524 return part
525
526
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000527def process_variables(cwd, variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000528 """Processes path variables as a special case and returns a copy of the dict.
529
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000530 For each 'path' variable: first normalizes it based on |cwd|, verifies it
531 exists then sets it as relative to relative_base_dir.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000532 """
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000533 relative_base_dir = trace_inputs.get_native_path_case(relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000534 variables = variables.copy()
535 for i in PATH_VARIABLES:
536 if i not in variables:
537 continue
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000538 variable = variables[i].strip()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000539 # Variables could contain / or \ on windows. Always normalize to
540 # os.path.sep.
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000541 variable = variable.replace('/', os.path.sep)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000542 variable = os.path.join(cwd, variable)
543 variable = os.path.normpath(variable)
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000544 variable = trace_inputs.get_native_path_case(variable)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000545 if not os.path.isdir(variable):
546 raise ExecutionError('%s=%s is not a directory' % (i, variable))
547
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000548 # All variables are relative to the .isolate file.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000549 variable = os.path.relpath(variable, relative_base_dir)
550 logging.debug(
551 'Translated variable %s from %s to %s', i, variables[i], variable)
552 variables[i] = variable
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000553 return variables
554
555
556def eval_variables(item, variables):
557 """Replaces the .isolate variables in a string item.
558
559 Note that the .isolate format is a subset of the .gyp dialect.
560 """
561 return ''.join(
562 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
563
564
565def classify_files(root_dir, tracked, untracked):
566 """Converts the list of files into a .isolate 'variables' dictionary.
567
568 Arguments:
569 - tracked: list of files names to generate a dictionary out of that should
570 probably be tracked.
571 - untracked: list of files names that must not be tracked.
572 """
573 # These directories are not guaranteed to be always present on every builder.
574 OPTIONAL_DIRECTORIES = (
575 'test/data/plugin',
576 'third_party/WebKit/LayoutTests',
577 )
578
579 new_tracked = []
580 new_untracked = list(untracked)
581
582 def should_be_tracked(filepath):
583 """Returns True if it is a file without whitespace in a non-optional
584 directory that has no symlink in its path.
585 """
586 if filepath.endswith('/'):
587 return False
588 if ' ' in filepath:
589 return False
590 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
591 return False
592 # Look if any element in the path is a symlink.
593 split = filepath.split('/')
594 for i in range(len(split)):
595 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
596 return False
597 return True
598
599 for filepath in sorted(tracked):
600 if should_be_tracked(filepath):
601 new_tracked.append(filepath)
602 else:
603 # Anything else.
604 new_untracked.append(filepath)
605
606 variables = {}
607 if new_tracked:
608 variables[KEY_TRACKED] = sorted(new_tracked)
609 if new_untracked:
610 variables[KEY_UNTRACKED] = sorted(new_untracked)
611 return variables
612
613
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000614def chromium_fix(f, variables):
615 """Fixes an isolate dependnecy with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000616 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
617 # separator.
618 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000619 # Ignored items.
620 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000621 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000622 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000623 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000624 # 'First Run' is not created by the compile, but by the test itself.
625 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000626
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000627 # Blacklist logs and other unimportant files.
628 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
629 logging.debug('Ignoring %s', f)
630 return None
631
maruel@chromium.org7650e422012-11-16 21:56:42 +0000632 EXECUTABLE = re.compile(
633 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
634 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
635 r'$')
636 match = EXECUTABLE.match(f)
637 if match:
638 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
639
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000640 if sys.platform == 'darwin':
641 # On OSX, the name of the output is dependent on gyp define, it can be
642 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
643 # Framework.framework'. Furthermore, they are versioned with a gyp
644 # variable. To lower the complexity of the .isolate file, remove all the
645 # individual entries that show up under any of the 4 entries and replace
646 # them with the directory itself. Overall, this results in a bit more
647 # files than strictly necessary.
648 OSX_BUNDLES = (
649 '<(PRODUCT_DIR)/Chromium Framework.framework/',
650 '<(PRODUCT_DIR)/Chromium.app/',
651 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
652 '<(PRODUCT_DIR)/Google Chrome.app/',
653 )
654 for prefix in OSX_BUNDLES:
655 if f.startswith(prefix):
656 # Note this result in duplicate values, so the a set() must be used to
657 # remove duplicates.
658 return prefix
659 return f
660
661
662def generate_simplified(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000663 tracked, untracked, touched, root_dir, variables, relative_cwd,
664 trace_blacklist):
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000665 """Generates a clean and complete .isolate 'variables' dictionary.
666
667 Cleans up and extracts only files from within root_dir then processes
668 variables and relative_cwd.
669 """
670 root_dir = os.path.realpath(root_dir)
671 logging.info(
672 'generate_simplified(%d files, %s, %s, %s)' %
673 (len(tracked) + len(untracked) + len(touched),
674 root_dir, variables, relative_cwd))
675
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000676 # Preparation work.
677 relative_cwd = cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000678 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000679 # Creates the right set of variables here. We only care about PATH_VARIABLES.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000680 path_variables = dict(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000681 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
682 for k in PATH_VARIABLES if k in variables)
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000683 variables = variables.copy()
684 variables.update(path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000685
686 # Actual work: Process the files.
687 # TODO(maruel): if all the files in a directory are in part tracked and in
688 # part untracked, the directory will not be extracted. Tracked files should be
689 # 'promoted' to be untracked as needed.
690 tracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000691 root_dir, tracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000692 untracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000693 root_dir, untracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000694 # touched is not compressed, otherwise it would result in files to be archived
695 # that we don't need.
696
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000697 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000698 def fix(f):
699 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000700 # Important, GYP stores the files with / and not \.
701 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000702 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000703 # If it's not already a variable.
704 if not f.startswith('<'):
705 # relative_cwd is usually the directory containing the gyp file. It may be
706 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000707 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000708 # Convert the whole thing to / since it's isolate's speak.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000709 f = posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000710 posixpath.join(root_dir_posix, f),
711 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000712
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000713 for variable, root_path in path_variables.iteritems():
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000714 if f.startswith(root_path):
715 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000716 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000717 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000718 return f
719
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000720 def fix_all(items):
721 """Reduces the items to convert variables, removes unneeded items, apply
722 chromium-specific fixes and only return unique items.
723 """
724 variables_converted = (fix(f.path) for f in items)
725 chromium_fixed = (chromium_fix(f, variables) for f in variables_converted)
726 return set(f for f in chromium_fixed if f)
727
728 tracked = fix_all(tracked)
729 untracked = fix_all(untracked)
730 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000731 out = classify_files(root_dir, tracked, untracked)
732 if touched:
733 out[KEY_TOUCHED] = sorted(touched)
734 return out
735
736
benrg@chromium.org609b7982013-02-07 16:44:46 +0000737def chromium_filter_flags(variables):
738 """Filters out build flags used in Chromium that we don't want to treat as
739 configuration variables.
740 """
741 # TODO(benrg): Need a better way to determine this.
742 blacklist = set(PATH_VARIABLES + ('EXECUTABLE_SUFFIX', 'FLAG'))
743 return dict((k, v) for k, v in variables.iteritems() if k not in blacklist)
744
745
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000746def generate_isolate(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000747 tracked, untracked, touched, root_dir, variables, relative_cwd,
748 trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000749 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000750 dependencies = generate_simplified(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000751 tracked, untracked, touched, root_dir, variables, relative_cwd,
752 trace_blacklist)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000753 config_variables = chromium_filter_flags(variables)
754 config_variable_names, config_values = zip(
755 *sorted(config_variables.iteritems()))
756 out = Configs(None)
757 # The new dependencies apply to just one configuration, namely config_values.
758 out.merge_dependencies(dependencies, config_variable_names, [config_values])
759 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000760
761
762def split_touched(files):
763 """Splits files that are touched vs files that are read."""
764 tracked = []
765 touched = []
766 for f in files:
767 if f.size:
768 tracked.append(f)
769 else:
770 touched.append(f)
771 return tracked, touched
772
773
774def pretty_print(variables, stdout):
775 """Outputs a gyp compatible list from the decoded variables.
776
777 Similar to pprint.print() but with NIH syndrome.
778 """
779 # Order the dictionary keys by these keys in priority.
780 ORDER = (
781 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
782 KEY_TRACKED, KEY_UNTRACKED)
783
784 def sorting_key(x):
785 """Gives priority to 'most important' keys before the others."""
786 if x in ORDER:
787 return str(ORDER.index(x))
788 return x
789
790 def loop_list(indent, items):
791 for item in items:
792 if isinstance(item, basestring):
793 stdout.write('%s\'%s\',\n' % (indent, item))
794 elif isinstance(item, dict):
795 stdout.write('%s{\n' % indent)
796 loop_dict(indent + ' ', item)
797 stdout.write('%s},\n' % indent)
798 elif isinstance(item, list):
799 # A list inside a list will write the first item embedded.
800 stdout.write('%s[' % indent)
801 for index, i in enumerate(item):
802 if isinstance(i, basestring):
803 stdout.write(
804 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
805 elif isinstance(i, dict):
806 stdout.write('{\n')
807 loop_dict(indent + ' ', i)
808 if index != len(item) - 1:
809 x = ', '
810 else:
811 x = ''
812 stdout.write('%s}%s' % (indent, x))
813 else:
814 assert False
815 stdout.write('],\n')
816 else:
817 assert False
818
819 def loop_dict(indent, items):
820 for key in sorted(items, key=sorting_key):
821 item = items[key]
822 stdout.write("%s'%s': " % (indent, key))
823 if isinstance(item, dict):
824 stdout.write('{\n')
825 loop_dict(indent + ' ', item)
826 stdout.write(indent + '},\n')
827 elif isinstance(item, list):
828 stdout.write('[\n')
829 loop_list(indent + ' ', item)
830 stdout.write(indent + '],\n')
831 elif isinstance(item, basestring):
832 stdout.write(
833 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
834 elif item in (True, False, None):
835 stdout.write('%s\n' % item)
836 else:
837 assert False, item
838
839 stdout.write('{\n')
840 loop_dict(' ', variables)
841 stdout.write('}\n')
842
843
844def union(lhs, rhs):
845 """Merges two compatible datastructures composed of dict/list/set."""
846 assert lhs is not None or rhs is not None
847 if lhs is None:
848 return copy.deepcopy(rhs)
849 if rhs is None:
850 return copy.deepcopy(lhs)
851 assert type(lhs) == type(rhs), (lhs, rhs)
852 if hasattr(lhs, 'union'):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000853 # Includes set, ConfigSettings and Configs.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000854 return lhs.union(rhs)
855 if isinstance(lhs, dict):
856 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
857 elif isinstance(lhs, list):
858 # Do not go inside the list.
859 return lhs + rhs
860 assert False, type(lhs)
861
862
863def extract_comment(content):
864 """Extracts file level comment."""
865 out = []
866 for line in content.splitlines(True):
867 if line.startswith('#'):
868 out.append(line)
869 else:
870 break
871 return ''.join(out)
872
873
874def eval_content(content):
875 """Evaluates a python file and return the value defined in it.
876
877 Used in practice for .isolate files.
878 """
879 globs = {'__builtins__': None}
880 locs = {}
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000881 try:
882 value = eval(content, globs, locs)
883 except TypeError as e:
884 e.args = list(e.args) + [content]
885 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000886 assert locs == {}, locs
887 assert globs == {'__builtins__': None}, globs
888 return value
889
890
benrg@chromium.org609b7982013-02-07 16:44:46 +0000891def match_configs(expr, config_variables, all_configs):
892 """Returns the configs from |all_configs| that match the |expr|, where
893 the elements of |all_configs| are tuples of values for the |config_variables|.
894 Example:
895 >>> match_configs(expr = "(foo==1 or foo==2) and bar=='b'",
896 config_variables = ["foo", "bar"],
897 all_configs = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
898 [(1, 'b'), (2, 'b')]
899 """
900 return [
901 config for config in all_configs
902 if eval(expr, dict(zip(config_variables, config)))
903 ]
904
905
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000906def verify_variables(variables):
907 """Verifies the |variables| dictionary is in the expected format."""
908 VALID_VARIABLES = [
909 KEY_TOUCHED,
910 KEY_TRACKED,
911 KEY_UNTRACKED,
912 'command',
913 'read_only',
914 ]
915 assert isinstance(variables, dict), variables
916 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
917 for name, value in variables.iteritems():
918 if name == 'read_only':
919 assert value in (True, False, None), value
920 else:
921 assert isinstance(value, list), value
922 assert all(isinstance(i, basestring) for i in value), value
923
924
benrg@chromium.org609b7982013-02-07 16:44:46 +0000925def verify_ast(expr, variables_and_values):
926 """Verifies that |expr| is of the form
927 expr ::= expr ( "or" | "and" ) expr
928 | identifier "==" ( string | int )
929 Also collects the variable identifiers and string/int values in the dict
930 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
931 """
932 assert isinstance(expr, (ast.BoolOp, ast.Compare))
933 if isinstance(expr, ast.BoolOp):
934 assert isinstance(expr.op, (ast.And, ast.Or))
935 for subexpr in expr.values:
936 verify_ast(subexpr, variables_and_values)
937 else:
938 assert isinstance(expr.left.ctx, ast.Load)
939 assert len(expr.ops) == 1
940 assert isinstance(expr.ops[0], ast.Eq)
941 var_values = variables_and_values.setdefault(expr.left.id, set())
942 rhs = expr.comparators[0]
943 assert isinstance(rhs, (ast.Str, ast.Num))
944 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
945
946
947def verify_condition(condition, variables_and_values):
948 """Verifies the |condition| dictionary is in the expected format.
949 See verify_ast() for the meaning of |variables_and_values|.
950 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000951 VALID_INSIDE_CONDITION = ['variables']
952 assert isinstance(condition, list), condition
benrg@chromium.org609b7982013-02-07 16:44:46 +0000953 assert len(condition) == 2, condition
954 expr, then = condition
955
956 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
957 verify_ast(test_ast.body, variables_and_values)
958
959 assert isinstance(then, dict), then
960 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
961 verify_variables(then['variables'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000962
963
benrg@chromium.org609b7982013-02-07 16:44:46 +0000964def verify_root(value, variables_and_values):
965 """Verifies that |value| is the parsed form of a valid .isolate file.
966 See verify_ast() for the meaning of |variables_and_values|.
967 """
968 VALID_ROOTS = ['includes', 'conditions']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000969 assert isinstance(value, dict), value
970 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000971
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000972 includes = value.get('includes', [])
973 assert isinstance(includes, list), includes
974 for include in includes:
975 assert isinstance(include, basestring), include
976
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000977 conditions = value.get('conditions', [])
978 assert isinstance(conditions, list), conditions
979 for condition in conditions:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000980 verify_condition(condition, variables_and_values)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000981
982
benrg@chromium.org609b7982013-02-07 16:44:46 +0000983def remove_weak_dependencies(values, key, item, item_configs):
984 """Removes any configs from this key if the item is already under a
985 strong key.
986 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000987 if key == KEY_TOUCHED:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000988 item_configs = set(item_configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000989 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000990 try:
991 item_configs -= values[stronger_key][item]
992 except KeyError:
993 pass
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000994
benrg@chromium.org609b7982013-02-07 16:44:46 +0000995 return item_configs
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000996
997
benrg@chromium.org609b7982013-02-07 16:44:46 +0000998def remove_repeated_dependencies(folders, key, item, item_configs):
999 """Removes any configs from this key if the item is in a folder that is
1000 already included."""
csharp@chromium.org31176252012-11-02 13:04:40 +00001001
1002 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001003 item_configs = set(item_configs)
1004 for (folder, configs) in folders.iteritems():
csharp@chromium.org31176252012-11-02 13:04:40 +00001005 if folder != item and item.startswith(folder):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001006 item_configs -= configs
csharp@chromium.org31176252012-11-02 13:04:40 +00001007
benrg@chromium.org609b7982013-02-07 16:44:46 +00001008 return item_configs
csharp@chromium.org31176252012-11-02 13:04:40 +00001009
1010
1011def get_folders(values_dict):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001012 """Returns a dict of all the folders in the given value_dict."""
1013 return dict(
1014 (item, configs) for (item, configs) in values_dict.iteritems()
1015 if item.endswith('/')
1016 )
csharp@chromium.org31176252012-11-02 13:04:40 +00001017
1018
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001019def invert_map(variables):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001020 """Converts {config: {deptype: list(depvals)}} to
1021 {deptype: {depval: set(configs)}}.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001022 """
1023 KEYS = (
1024 KEY_TOUCHED,
1025 KEY_TRACKED,
1026 KEY_UNTRACKED,
1027 'command',
1028 'read_only',
1029 )
1030 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001031 for config, values in variables.iteritems():
1032 for key in KEYS:
1033 if key == 'command':
1034 items = [tuple(values[key])] if key in values else []
1035 elif key == 'read_only':
1036 items = [values[key]] if key in values else []
1037 else:
1038 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
1039 items = values.get(key, [])
1040 for item in items:
1041 out[key].setdefault(item, set()).add(config)
1042 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001043
1044
benrg@chromium.org609b7982013-02-07 16:44:46 +00001045def reduce_inputs(values):
1046 """Reduces the output of invert_map() to the strictest minimum list.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001047
benrg@chromium.org609b7982013-02-07 16:44:46 +00001048 Looks at each individual file and directory, maps where they are used and
1049 reconstructs the inverse dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001050
benrg@chromium.org609b7982013-02-07 16:44:46 +00001051 Returns the minimized dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001052 """
1053 KEYS = (
1054 KEY_TOUCHED,
1055 KEY_TRACKED,
1056 KEY_UNTRACKED,
1057 'command',
1058 'read_only',
1059 )
csharp@chromium.org31176252012-11-02 13:04:40 +00001060
1061 # Folders can only live in KEY_UNTRACKED.
1062 folders = get_folders(values.get(KEY_UNTRACKED, {}))
1063
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001064 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001065 for key in KEYS:
1066 for item, item_configs in values.get(key, {}).iteritems():
1067 item_configs = remove_weak_dependencies(values, key, item, item_configs)
1068 item_configs = remove_repeated_dependencies(
1069 folders, key, item, item_configs)
1070 if item_configs:
1071 out[key][item] = item_configs
1072 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001073
1074
benrg@chromium.org609b7982013-02-07 16:44:46 +00001075def convert_map_to_isolate_dict(values, config_variables):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001076 """Regenerates back a .isolate configuration dict from files and dirs
1077 mappings generated from reduce_inputs().
1078 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001079 # Gather a list of configurations for set inversion later.
1080 all_mentioned_configs = set()
1081 for configs_by_item in values.itervalues():
1082 for configs in configs_by_item.itervalues():
1083 all_mentioned_configs.update(configs)
1084
1085 # Invert the mapping to make it dict first.
1086 conditions = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001087 for key in values:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001088 for item, configs in values[key].iteritems():
1089 then = conditions.setdefault(frozenset(configs), {})
1090 variables = then.setdefault('variables', {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001091
benrg@chromium.org609b7982013-02-07 16:44:46 +00001092 if item in (True, False):
1093 # One-off for read_only.
1094 variables[key] = item
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001095 else:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001096 assert item
1097 if isinstance(item, tuple):
1098 # One-off for command.
1099 # Do not merge lists and do not sort!
1100 # Note that item is a tuple.
1101 assert key not in variables
1102 variables[key] = list(item)
1103 else:
1104 # The list of items (files or dirs). Append the new item and keep
1105 # the list sorted.
1106 l = variables.setdefault(key, [])
1107 l.append(item)
1108 l.sort()
1109
1110 if all_mentioned_configs:
1111 config_values = map(set, zip(*all_mentioned_configs))
1112 sef = short_expression_finder.ShortExpressionFinder(
1113 zip(config_variables, config_values))
1114
1115 conditions = sorted(
1116 [sef.get_expr(configs), then] for configs, then in conditions.iteritems())
1117 return {'conditions': conditions}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001118
1119
1120### Internal state files.
1121
1122
benrg@chromium.org609b7982013-02-07 16:44:46 +00001123class ConfigSettings(object):
1124 """Represents the dependency variables for a single build configuration.
1125 The structure is immutable.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001126 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001127 def __init__(self, config, values):
1128 self.config = config
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001129 verify_variables(values)
1130 self.touched = sorted(values.get(KEY_TOUCHED, []))
1131 self.tracked = sorted(values.get(KEY_TRACKED, []))
1132 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
1133 self.command = values.get('command', [])[:]
1134 self.read_only = values.get('read_only')
1135
1136 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001137 assert not (self.config and rhs.config) or (self.config == rhs.config)
maruel@chromium.org669edcb2012-11-02 19:16:14 +00001138 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001139 var = {
1140 KEY_TOUCHED: sorted(self.touched + rhs.touched),
1141 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
1142 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
1143 'command': self.command or rhs.command,
1144 'read_only': rhs.read_only if self.read_only is None else self.read_only,
1145 }
benrg@chromium.org609b7982013-02-07 16:44:46 +00001146 return ConfigSettings(self.config or rhs.config, var)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001147
1148 def flatten(self):
1149 out = {}
1150 if self.command:
1151 out['command'] = self.command
1152 if self.touched:
1153 out[KEY_TOUCHED] = self.touched
1154 if self.tracked:
1155 out[KEY_TRACKED] = self.tracked
1156 if self.untracked:
1157 out[KEY_UNTRACKED] = self.untracked
1158 if self.read_only is not None:
1159 out['read_only'] = self.read_only
1160 return out
1161
1162
1163class Configs(object):
1164 """Represents a processed .isolate file.
1165
benrg@chromium.org609b7982013-02-07 16:44:46 +00001166 Stores the file in a processed way, split by configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001167 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001168 def __init__(self, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001169 self.file_comment = file_comment
benrg@chromium.org609b7982013-02-07 16:44:46 +00001170 # The keys of by_config are tuples of values for the configuration
1171 # variables. The names of the variables (which must be the same for
1172 # every by_config key) are kept in config_variables. Initially by_config
1173 # is empty and we don't know what configuration variables will be used,
1174 # so config_variables also starts out empty. It will be set by the first
1175 # call to union() or merge_dependencies().
1176 self.by_config = {}
1177 self.config_variables = ()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001178
1179 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001180 """Adds variables from rhs (a Configs) to the existing variables.
1181 """
1182 config_variables = self.config_variables
1183 if not config_variables:
1184 config_variables = rhs.config_variables
1185 else:
1186 # We can't proceed if this isn't true since we don't know the correct
1187 # default values for extra variables. The variables are sorted so we
1188 # don't need to worry about permutations.
1189 if rhs.config_variables and rhs.config_variables != config_variables:
1190 raise ExecutionError(
1191 'Variables in merged .isolate files do not match: %r and %r' % (
1192 config_variables, rhs.config_variables))
1193
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001194 # Takes the first file comment, prefering lhs.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001195 out = Configs(self.file_comment or rhs.file_comment)
1196 out.config_variables = config_variables
1197 for config in set(self.by_config) | set(rhs.by_config):
1198 out.by_config[config] = union(
1199 self.by_config.get(config), rhs.by_config.get(config))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001200 return out
1201
benrg@chromium.org609b7982013-02-07 16:44:46 +00001202 def merge_dependencies(self, values, config_variables, configs):
1203 """Adds new dependencies to this object for the given configurations.
1204 Arguments:
1205 values: A variables dict as found in a .isolate file, e.g.,
1206 {KEY_TOUCHED: [...], 'command': ...}.
1207 config_variables: An ordered list of configuration variables, e.g.,
1208 ["OS", "chromeos"]. If this object already contains any dependencies,
1209 the configuration variables must match.
1210 configs: a list of tuples of values of the configuration variables,
1211 e.g., [("mac", 0), ("linux", 1)]. The dependencies in |values|
1212 are added to all of these configurations, and other configurations
1213 are unchanged.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001214 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001215 if not values:
1216 return
1217
1218 if not self.config_variables:
1219 self.config_variables = config_variables
1220 else:
1221 # See comment in Configs.union().
1222 assert self.config_variables == config_variables
1223
1224 for config in configs:
1225 self.by_config[config] = union(
1226 self.by_config.get(config), ConfigSettings(config, values))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001227
1228 def flatten(self):
1229 """Returns a flat dictionary representation of the configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001230 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001231 return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
1232
1233 def make_isolate_file(self):
1234 """Returns a dictionary suitable for writing to a .isolate file.
1235 """
1236 dependencies_by_config = self.flatten()
1237 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
1238 return convert_map_to_isolate_dict(configs_by_dependency,
1239 self.config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001240
1241
benrg@chromium.org609b7982013-02-07 16:44:46 +00001242# TODO(benrg): Remove this function when no old-format files are left.
1243def convert_old_to_new_format(value):
1244 """Converts from the old .isolate format, which only has one variable (OS),
1245 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
1246 and allows conditions that depend on the set of all OSes, to the new format,
1247 which allows any set of variables, has no hardcoded values, and only allows
1248 explicit positive tests of variable values.
1249 """
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001250 conditions = value.get('conditions', [])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001251 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
1252 return value # Nothing to change
1253
1254 def parse_condition(cond):
1255 return re.match(r'OS=="(\w+)"\Z', cond[0]).group(1)
1256
1257 oses = set(map(parse_condition, conditions))
1258 default_oses = set(['linux', 'mac', 'win'])
1259 oses = sorted(oses | default_oses)
1260
1261 def if_not_os(not_os, then):
1262 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
1263 return [expr, then]
1264
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001265 conditions = [
1266 cond[:2] for cond in conditions if cond[1]
1267 ] + [
1268 if_not_os(parse_condition(cond), cond[2])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001269 for cond in conditions if len(cond) == 3
1270 ]
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001271
benrg@chromium.org609b7982013-02-07 16:44:46 +00001272 if 'variables' in value:
1273 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
1274 conditions.sort()
1275
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001276 value = value.copy()
1277 value['conditions'] = conditions
benrg@chromium.org609b7982013-02-07 16:44:46 +00001278 return value
1279
1280
1281def load_isolate_as_config(isolate_dir, value, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001282 """Parses one .isolate file and returns a Configs() instance.
1283
1284 |value| is the loaded dictionary that was defined in the gyp file.
1285
1286 The expected format is strict, anything diverting from the format below will
1287 throw an assert:
1288 {
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001289 'includes': [
1290 'foo.isolate',
1291 ],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001292 'conditions': [
benrg@chromium.org609b7982013-02-07 16:44:46 +00001293 ['OS=="vms" and foo=42', {
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001294 'variables': {
benrg@chromium.org609b7982013-02-07 16:44:46 +00001295 'command': [
1296 ...
1297 ],
1298 'isolate_dependency_tracked': [
1299 ...
1300 ],
1301 'isolate_dependency_untracked': [
1302 ...
1303 ],
1304 'read_only': False,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001305 },
1306 }],
1307 ...
1308 ],
1309 }
1310 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001311 value = convert_old_to_new_format(value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001312
benrg@chromium.org609b7982013-02-07 16:44:46 +00001313 variables_and_values = {}
1314 verify_root(value, variables_and_values)
1315 if variables_and_values:
1316 config_variables, config_values = zip(
1317 *sorted(variables_and_values.iteritems()))
1318 all_configs = list(itertools.product(*config_values))
1319 else:
1320 config_variables = None
1321 all_configs = []
1322
1323 isolate = Configs(file_comment)
1324
1325 # Add configuration-specific variables.
1326 for expr, then in value.get('conditions', []):
1327 configs = match_configs(expr, config_variables, all_configs)
1328 isolate.merge_dependencies(then['variables'], config_variables, configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001329
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001330 # Load the includes.
1331 for include in value.get('includes', []):
1332 if os.path.isabs(include):
1333 raise ExecutionError(
1334 'Failed to load configuration; absolute include path \'%s\'' %
1335 include)
1336 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
1337 with open(included_isolate, 'r') as f:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001338 included_isolate = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001339 os.path.dirname(included_isolate),
1340 eval_content(f.read()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001341 None)
1342 isolate = union(isolate, included_isolate)
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001343
benrg@chromium.org609b7982013-02-07 16:44:46 +00001344 return isolate
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001345
1346
benrg@chromium.org609b7982013-02-07 16:44:46 +00001347def load_isolate_for_config(isolate_dir, content, variables):
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001348 """Loads the .isolate file and returns the information unprocessed but
1349 filtered for the specific OS.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001350
1351 Returns the command, dependencies and read_only flag. The dependencies are
1352 fixed to use os.path.sep.
1353 """
1354 # Load the .isolate file, process its conditions, retrieve the command and
1355 # dependencies.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001356 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
1357 try:
1358 config = tuple(variables[var] for var in isolate.config_variables)
1359 except KeyError:
1360 raise ExecutionError(
1361 'These configuration variables were missing from the command line: %s' %
1362 ', '.join(sorted(set(isolate.config_variables) - set(variables))))
1363 config = isolate.by_config.get(config)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001364 if not config:
csharp@chromium.org2a3d7d52013-03-23 12:54:37 +00001365 raise ExecutionError('Failed to load configuration for (%s)' %
1366 ', '.join(isolate.config_variables))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001367 # Merge tracked and untracked variables, isolate.py doesn't care about the
1368 # trackability of the variables, only the build tool does.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001369 dependencies = [
1370 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1371 ]
1372 touched = [f.replace('/', os.path.sep) for f in config.touched]
1373 return config.command, dependencies, touched, config.read_only
1374
1375
maruel@chromium.orgdcdbfc82013-07-25 18:54:57 +00001376def save_isolated(isolated, data):
1377 """Writes one or multiple .isolated files.
1378
1379 Note: this reference implementation does not create child .isolated file so it
1380 always returns an empty list.
1381
1382 Returns the list of child isolated files that are included by |isolated|.
1383 """
1384 trace_inputs.write_json(isolated, data, True)
1385 return []
1386
1387
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001388def chromium_save_isolated(isolated, data, variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001389 """Writes one or many .isolated files.
1390
1391 This slightly increases the cold cache cost but greatly reduce the warm cache
1392 cost by splitting low-churn files off the master .isolated file. It also
1393 reduces overall isolateserver memcache consumption.
1394 """
1395 slaves = []
1396
1397 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001398 new_slave = {
1399 'algo': data['algo'],
1400 'files': {},
1401 'os': data['os'],
1402 'version': data['version'],
1403 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001404 for f in data['files'].keys():
1405 if f.startswith(prefix):
1406 new_slave['files'][f] = data['files'].pop(f)
1407 if new_slave['files']:
1408 slaves.append(new_slave)
1409
1410 # Split test/data/ in its own .isolated file.
1411 extract_into_included_isolated(os.path.join('test', 'data', ''))
1412
1413 # Split everything out of PRODUCT_DIR in its own .isolated file.
1414 if variables.get('PRODUCT_DIR'):
1415 extract_into_included_isolated(variables['PRODUCT_DIR'])
1416
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001417 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001418 for index, f in enumerate(slaves):
1419 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
1420 trace_inputs.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001421 data.setdefault('includes', []).append(
1422 isolateserver.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001423 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001424
maruel@chromium.orgdcdbfc82013-07-25 18:54:57 +00001425 files.extend(save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001426 return files
1427
1428
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001429class Flattenable(object):
1430 """Represents data that can be represented as a json file."""
1431 MEMBERS = ()
1432
1433 def flatten(self):
1434 """Returns a json-serializable version of itself.
1435
1436 Skips None entries.
1437 """
1438 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1439 return dict((member, value) for member, value in items if value is not None)
1440
1441 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001442 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001443 """Loads a flattened version."""
1444 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001445 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001446 for member in out.MEMBERS:
1447 if member in data:
1448 # Access to a protected member XXX of a client class
1449 # pylint: disable=W0212
1450 out._load_member(member, data.pop(member))
1451 if data:
1452 raise ValueError(
1453 'Found unexpected entry %s while constructing an object %s' %
1454 (data, cls.__name__), data, cls.__name__)
1455 return out
1456
1457 def _load_member(self, member, value):
1458 """Loads a member into self."""
1459 setattr(self, member, value)
1460
1461 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001462 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001463 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001464 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001465 out = cls.load(trace_inputs.read_json(filename), *args, **kwargs)
1466 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001467 except (IOError, ValueError):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001468 # On failure, loads the default instance.
1469 out = cls(*args, **kwargs)
1470 logging.warn('Failed to load %s', filename)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001471 return out
1472
1473
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001474class SavedState(Flattenable):
1475 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001476
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001477 This file caches the items calculated by this script and is used to increase
1478 the performance of the script. This file is not loaded by run_isolated.py.
1479 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001480
1481 It is important to note that the 'files' dict keys are using native OS path
1482 separator instead of '/' used in .isolate file.
1483 """
1484 MEMBERS = (
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001485 # Algorithm used to generate the hash. The only supported value is at the
1486 # time of writting 'sha-1'.
1487 'algo',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001488 # Cache of the processed command. This value is saved because .isolated
1489 # files are never loaded by isolate.py so it's the only way to load the
1490 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001491 'command',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001492 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001493 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001494 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001495 'isolate_file',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001496 # List of included .isolated files. Used to support/remember 'slave'
1497 # .isolated files. Relative path to isolated_basedir.
1498 'child_isolated_files',
1499 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001500 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001501 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001502 'relative_cwd',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001503 # GYP variables used to generate the .isolated file. Variables are saved so
1504 # a user can use isolate.py after building and the GYP variables are still
1505 # defined.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001506 'variables',
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001507 # Version of the file format in format 'major.minor'. Any non-breaking
1508 # change must update minor. Any breaking change must update major.
1509 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 )
1511
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001512 def __init__(self, isolated_basedir):
1513 """Creates an empty SavedState.
1514
1515 |isolated_basedir| is the directory where the .isolated and .isolated.state
1516 files are saved.
1517 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001518 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001519 assert os.path.isabs(isolated_basedir), isolated_basedir
1520 assert os.path.isdir(isolated_basedir), isolated_basedir
1521 self.isolated_basedir = isolated_basedir
1522
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001523 # The default algorithm used.
1524 self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001525 self.command = []
1526 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001527 self.isolate_file = None
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001528 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001529 self.read_only = None
1530 self.relative_cwd = None
benrg@chromium.org609b7982013-02-07 16:44:46 +00001531 self.variables = {'OS': get_flavor()}
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001532 # The current version.
1533 self.version = '1.0'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001534
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001535 def update(self, isolate_file, variables):
1536 """Updates the saved state with new data to keep GYP variables and internal
1537 reference to the original .isolate file.
1538 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +00001539 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001540 # Convert back to a relative path. On Windows, if the isolate and
1541 # isolated files are on different drives, isolate_file will stay an absolute
1542 # path.
1543 isolate_file = safe_relpath(isolate_file, self.isolated_basedir)
1544
1545 # The same .isolate file should always be used to generate the .isolated and
1546 # .isolated.state.
1547 assert isolate_file == self.isolate_file or not self.isolate_file, (
1548 isolate_file, self.isolate_file)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001549 self.isolate_file = isolate_file
1550 self.variables.update(variables)
1551
1552 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
1553 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001554
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001555 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001556 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001557 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001558 self.command = command
1559 # Add new files.
1560 for f in infiles:
1561 self.files.setdefault(f, {})
1562 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001563 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001564 # Prune extraneous files that are not a dependency anymore.
1565 for f in set(self.files).difference(set(infiles).union(touched)):
1566 del self.files[f]
1567 if read_only is not None:
1568 self.read_only = read_only
1569 self.relative_cwd = relative_cwd
1570
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001571 def to_isolated(self):
1572 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001573
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001574 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001575 """
1576 def strip(data):
1577 """Returns a 'files' entry with only the whitelisted keys."""
1578 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
1579
1580 out = {
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001581 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001582 'files': dict(
1583 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001584 'os': self.variables['OS'],
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001585 'version': self.version,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001586 }
1587 if self.command:
1588 out['command'] = self.command
1589 if self.read_only is not None:
1590 out['read_only'] = self.read_only
1591 if self.relative_cwd:
1592 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001593 return out
1594
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001595 @property
1596 def isolate_filepath(self):
1597 """Returns the absolute path of self.isolate_file."""
1598 return os.path.normpath(
1599 os.path.join(self.isolated_basedir, self.isolate_file))
1600
1601 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001602 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001603 def load(cls, data, isolated_basedir): # pylint: disable=W0221
1604 """Special case loading to disallow different OS.
1605
1606 It is not possible to load a .isolated.state files from a different OS, this
1607 file is saved in OS-specific format.
1608 """
1609 out = super(SavedState, cls).load(data, isolated_basedir)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001610 if 'os' in data:
1611 out.variables['OS'] = data['os']
1612 if out.variables['OS'] != get_flavor():
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001613 raise isolateserver.ConfigError(
benrg@chromium.org609b7982013-02-07 16:44:46 +00001614 'The .isolated.state file was created on another platform')
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001615
1616 # Converts human readable form back into the proper class type.
1617 algo = data.get('algo', 'sha-1')
1618 if not algo in isolateserver.SUPPORTED_ALGOS:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001619 raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001620 out.algo = isolateserver.SUPPORTED_ALGOS[algo]
1621
1622 # For example, 1.1 is guaranteed to be backward compatible with 1.0 code.
1623 if not re.match(r'^(\d+)\.(\d+)$', out.version):
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001624 raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001625 if out.version.split('.', 1)[0] != '1':
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001626 raise isolateserver.ConfigError(
1627 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001628
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001629 # The .isolate file must be valid. It could be absolute on Windows if the
1630 # drive containing the .isolate and the drive containing the .isolated files
1631 # differ.
1632 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
1633 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001634 return out
1635
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001636 def flatten(self):
1637 """Makes sure 'algo' is in human readable form."""
1638 out = super(SavedState, self).flatten()
1639 out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
1640 return out
1641
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001642 def __str__(self):
1643 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001644 out += ' command: %s\n' % self.command
1645 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001646 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001647 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +00001648 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001649 out += ' child_isolated_files: %s\n' % self.child_isolated_files
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001650 out += ' variables: %s' % ''.join(
1651 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1652 out += ')'
1653 return out
1654
1655
1656class CompleteState(object):
1657 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001658 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001659 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +00001660 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001661 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001662 # Contains the data to ease developer's use-case but that is not strictly
1663 # necessary.
1664 self.saved_state = saved_state
1665
1666 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001667 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001668 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001669 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001670 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001671 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001672 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001673 SavedState.load_file(
1674 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001675
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001676 def load_isolate(self, cwd, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001677 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001678 .isolate file.
1679
1680 Processes the loaded data, deduce root_dir, relative_cwd.
1681 """
1682 # Make sure to not depend on os.getcwd().
1683 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.orga3da9122013-03-28 13:27:09 +00001684 isolate_file = trace_inputs.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001685 logging.info(
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001686 'CompleteState.load_isolate(%s, %s, %s, %s)',
1687 cwd, isolate_file, variables, ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001688 relative_base_dir = os.path.dirname(isolate_file)
1689
1690 # Processes the variables and update the saved state.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001691 variables = process_variables(cwd, variables, relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001692 self.saved_state.update(isolate_file, variables)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001693 variables = self.saved_state.variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001694
1695 with open(isolate_file, 'r') as f:
1696 # At that point, variables are not replaced yet in command and infiles.
1697 # infiles may contain directory entries and is in posix style.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001698 command, infiles, touched, read_only = load_isolate_for_config(
1699 os.path.dirname(isolate_file), f.read(), variables)
1700 command = [eval_variables(i, variables) for i in command]
1701 infiles = [eval_variables(f, variables) for f in infiles]
1702 touched = [eval_variables(f, variables) for f in touched]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001703 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +00001704 # form '../../foo/bar'. Note that path variables must be taken in account
1705 # too, add them as if they were input files.
1706 path_variables = [variables[v] for v in PATH_VARIABLES if v in variables]
1707 root_dir = determine_root_dir(
1708 relative_base_dir, infiles + touched + path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001709 # The relative directory is automatically determined by the relative path
1710 # between root_dir and the directory containing the .isolate file,
1711 # isolate_base_dir.
1712 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
benrg@chromium.org9ae72862013-02-11 05:05:51 +00001713 # Now that we know where the root is, check that the PATH_VARIABLES point
1714 # inside it.
1715 for i in PATH_VARIABLES:
1716 if i in variables:
1717 if not path_starts_with(
1718 root_dir, os.path.join(relative_base_dir, variables[i])):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +00001719 raise isolateserver.MappingError(
maruel@chromium.org75584e22013-06-20 01:40:24 +00001720 'Path variable %s=%r points outside the inferred root directory'
1721 ' %s' % (i, variables[i], root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001722 # Normalize the files based to root_dir. It is important to keep the
1723 # trailing os.path.sep at that step.
1724 infiles = [
1725 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1726 for f in infiles
1727 ]
1728 touched = [
1729 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1730 for f in touched
1731 ]
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001732 follow_symlinks = variables['OS'] != 'win'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001733 # Expand the directories by listing each file inside. Up to now, trailing
1734 # os.path.sep must be kept. Do not expand 'touched'.
1735 infiles = expand_directories_and_symlinks(
1736 root_dir,
1737 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001738 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001739 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +00001740 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001741
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001742 # If we ignore broken items then remove any missing touched items.
1743 if ignore_broken_items:
1744 original_touched_count = len(touched)
1745 touched = [touch for touch in touched if os.path.exists(touch)]
1746
1747 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +00001748 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001749 len(touched) - original_touched_count)
1750
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001751 # Finally, update the new data to be able to generate the foo.isolated file,
1752 # the file that is used by run_isolated.py.
1753 self.saved_state.update_isolated(
1754 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001755 logging.debug(self)
1756
maruel@chromium.org9268f042012-10-17 17:36:41 +00001757 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001758 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001759
maruel@chromium.org9268f042012-10-17 17:36:41 +00001760 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1761 file is tainted.
1762
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001763 See process_input() for more information.
1764 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001765 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001766 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001767 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001768 else:
1769 filepath = os.path.join(self.root_dir, infile)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001770 self.saved_state.files[infile] = process_input(
1771 filepath,
1772 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +00001773 self.saved_state.read_only,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001774 self.saved_state.variables['OS'],
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001775 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001776
1777 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001778 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001779 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001780 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001781 self.isolated_filepath,
1782 self.saved_state.to_isolated(),
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001783 self.saved_state.variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001784 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001785 total_bytes = sum(
1786 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001787 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001788 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001789 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001790 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001791 logging.debug('Dumping to %s' % saved_state_file)
1792 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1793
1794 @property
1795 def root_dir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001796 """Returns the absolute path of the root_dir to reference the .isolate file
1797 via relative_cwd.
1798
1799 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
1800 to isolate_filepath.
1801 """
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001802 if not self.saved_state.isolate_file:
1803 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001804 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001805 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001806 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001807 root_dir = isolate_dir
1808 else:
1809 assert isolate_dir.endswith(self.saved_state.relative_cwd), (
1810 isolate_dir, self.saved_state.relative_cwd)
1811 # Walk back back to the root directory.
1812 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
1813 return trace_inputs.get_native_path_case(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001814
1815 @property
1816 def resultdir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001817 """Returns the absolute path containing the .isolated file.
1818
1819 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
1820 path as the value.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001821 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001822 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001823
1824 def __str__(self):
1825 def indent(data, indent_length):
1826 """Indents text."""
1827 spacing = ' ' * indent_length
1828 return ''.join(spacing + l for l in str(data).splitlines(True))
1829
1830 out = '%s(\n' % self.__class__.__name__
1831 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001832 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1833 return out
1834
1835
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001836def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001837 """Loads a CompleteState.
1838
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001839 This includes data from .isolate and .isolated.state files. Never reads the
1840 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001841
1842 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001843 options: Options instance generated with OptionParserIsolate. For either
1844 options.isolate and options.isolated, if the value is set, it is an
1845 absolute path.
1846 cwd: base directory to be used when loading the .isolate file.
1847 subdir: optional argument to only process file in the subdirectory, relative
1848 to CompleteState.root_dir.
1849 skip_update: Skip trying to load the .isolate file and processing the
1850 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001851 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001852 assert not options.isolate or os.path.isabs(options.isolate)
1853 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.orga3da9122013-03-28 13:27:09 +00001854 cwd = trace_inputs.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001855 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001856 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001857 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001858 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001859 else:
1860 # Constructs a dummy object that cannot be saved. Useful for temporary
1861 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001862 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001863
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001864 if not options.isolate:
1865 if not complete_state.saved_state.isolate_file:
1866 if not skip_update:
1867 raise ExecutionError('A .isolate file is required.')
1868 isolate = None
1869 else:
1870 isolate = complete_state.saved_state.isolate_filepath
1871 else:
1872 isolate = options.isolate
1873 if complete_state.saved_state.isolate_file:
1874 rel_isolate = safe_relpath(
1875 options.isolate, complete_state.saved_state.isolated_basedir)
1876 if rel_isolate != complete_state.saved_state.isolate_file:
1877 raise ExecutionError(
1878 '%s and %s do not match.' % (
1879 options.isolate, complete_state.saved_state.isolate_file))
1880
1881 if not skip_update:
1882 # Then load the .isolate and expands directories.
1883 complete_state.load_isolate(
1884 cwd, isolate, options.variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001885
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001886 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001887 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001888 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001889 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1890 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001891
1892 if not skip_update:
1893 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001894 return complete_state
1895
1896
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001897def read_trace_as_isolate_dict(complete_state, trace_blacklist):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001898 """Reads a trace and returns the .isolate dictionary.
1899
1900 Returns exceptions during the log parsing so it can be re-raised.
1901 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001902 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001903 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001904 if not os.path.isfile(logfile):
1905 raise ExecutionError(
1906 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1907 try:
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001908 data = api.parse_log(logfile, trace_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001909 exceptions = [i['exception'] for i in data if 'exception' in i]
1910 results = (i['results'] for i in data if 'results' in i)
1911 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1912 files = set(sum((result.existent for result in results_stripped), []))
1913 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001914 value = generate_isolate(
1915 tracked,
1916 [],
1917 touched,
1918 complete_state.root_dir,
1919 complete_state.saved_state.variables,
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001920 complete_state.saved_state.relative_cwd,
1921 trace_blacklist)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001922 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001923 except trace_inputs.TracingFailure, e:
1924 raise ExecutionError(
1925 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001926 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001927
1928
1929def print_all(comment, data, stream):
1930 """Prints a complete .isolate file and its top-level file comment into a
1931 stream.
1932 """
1933 if comment:
1934 stream.write(comment)
1935 pretty_print(data, stream)
1936
1937
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001938def merge(complete_state, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001939 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001940 value, exceptions = read_trace_as_isolate_dict(
1941 complete_state, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001942
1943 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001944 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001945 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001946 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001947 prev_config = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001948 isolate_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001949 eval_content(prev_content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001950 extract_comment(prev_content))
1951 new_config = load_isolate_as_config(isolate_dir, value, '')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001952 config = union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001953 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001954 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001955 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001956 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001957 if exceptions:
1958 # It got an exception, raise the first one.
1959 raise \
1960 exceptions[0][0], \
1961 exceptions[0][1], \
1962 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001963
1964
maruel@chromium.org29029882013-08-30 12:15:40 +00001965### Commands.
1966
1967
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001968def CMDarchive(parser, args):
1969 """Creates a .isolated file and uploads the tree to an isolate server.
maruel@chromium.org29029882013-08-30 12:15:40 +00001970
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001971 All the files listed in the .isolated file are put in the isolate server
1972 cache via isolateserver.py.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001973 """
maruel@chromium.org9268f042012-10-17 17:36:41 +00001974 parser.add_option('--subdir', help='Filters to a subdirectory')
1975 options, args = parser.parse_args(args)
1976 if args:
1977 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001978
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001979 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001980 success = False
1981 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001982 complete_state = load_complete_state(
1983 options, os.getcwd(), options.subdir, False)
1984 if not options.outdir:
1985 options.outdir = os.path.join(
1986 os.path.dirname(complete_state.isolated_filepath), 'hashtable')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001987 # Make sure that complete_state isn't modified until save_files() is
1988 # called, because any changes made to it here will propagate to the files
1989 # created (which is probably not intended).
1990 complete_state.save_files()
1991
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001992 infiles = complete_state.saved_state.files
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001993 # Add all the .isolated files.
maruel@chromium.org87f11962013-04-10 21:27:28 +00001994 isolated_hash = []
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001995 isolated_files = [
1996 options.isolated,
1997 ] + complete_state.saved_state.child_isolated_files
1998 for item in isolated_files:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001999 item_path = os.path.join(
2000 os.path.dirname(complete_state.isolated_filepath), item)
maruel@chromium.org7b844a62013-09-17 13:04:59 +00002001 # Do not use isolateserver.hash_file() here because the file is
maruel@chromium.org87f11962013-04-10 21:27:28 +00002002 # likely smallish (under 500kb) and its file size is needed.
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00002003 with open(item_path, 'rb') as f:
2004 content = f.read()
maruel@chromium.org385d73d2013-09-19 18:33:21 +00002005 isolated_hash.append(
2006 complete_state.saved_state.algo(content).hexdigest())
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00002007 isolated_metadata = {
maruel@chromium.org87f11962013-04-10 21:27:28 +00002008 'h': isolated_hash[-1],
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00002009 's': len(content),
2010 'priority': '0'
2011 }
2012 infiles[item_path] = isolated_metadata
2013
2014 logging.info('Creating content addressed object store with %d item',
2015 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002016
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002017 if is_url(options.outdir):
maruel@chromium.org7b844a62013-09-17 13:04:59 +00002018 isolateserver.upload_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002019 base_url=options.outdir,
2020 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00002021 infiles=infiles,
2022 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002023 else:
2024 recreate_tree(
2025 outdir=options.outdir,
2026 indir=complete_state.root_dir,
2027 infiles=infiles,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00002028 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00002029 as_hash=True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002030 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002031 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002032 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002033 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002034 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002035 if not success and os.path.isfile(options.isolated):
2036 os.remove(options.isolated)
maruel@chromium.org87f11962013-04-10 21:27:28 +00002037 return not success
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002038
2039
maruel@chromium.org2f952d82013-09-13 01:53:17 +00002040def CMDcheck(parser, args):
2041 """Checks that all the inputs are present and generates .isolated."""
2042 parser.add_option('--subdir', help='Filters to a subdirectory')
2043 options, args = parser.parse_args(args)
2044 if args:
2045 parser.error('Unsupported argument: %s' % args)
2046
2047 complete_state = load_complete_state(
2048 options, os.getcwd(), options.subdir, False)
2049
2050 # Nothing is done specifically. Just store the result and state.
2051 complete_state.save_files()
2052 return 0
2053
2054
2055CMDhashtable = CMDarchive
2056
2057
maruel@chromium.orge5322512013-08-19 20:17:57 +00002058def CMDmerge(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002059 """Reads and merges the data from the trace back into the original .isolate.
2060
2061 Ignores --outdir.
2062 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002063 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002064 add_trace_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00002065 options, args = parser.parse_args(args)
2066 if args:
2067 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00002068
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002069 complete_state = load_complete_state(options, os.getcwd(), None, False)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002070 blacklist = trace_inputs.gen_blacklist(options.trace_blacklist)
2071 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002072 return 0
2073
2074
maruel@chromium.orge5322512013-08-19 20:17:57 +00002075def CMDread(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002076 """Reads the trace file generated with command 'trace'.
2077
2078 Ignores --outdir.
2079 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002080 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002081 add_trace_option(parser)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002082 parser.add_option(
2083 '--skip-refresh', action='store_true',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00002084 help='Skip reading .isolate file and do not refresh the hash of '
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002085 'dependencies')
maruel@chromium.org29029882013-08-30 12:15:40 +00002086 parser.add_option(
2087 '-m', '--merge', action='store_true',
2088 help='merge the results back in the .isolate file instead of printing')
maruel@chromium.org9268f042012-10-17 17:36:41 +00002089 options, args = parser.parse_args(args)
2090 if args:
2091 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00002092
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002093 complete_state = load_complete_state(
2094 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002095 blacklist = trace_inputs.gen_blacklist(options.trace_blacklist)
2096 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
maruel@chromium.org29029882013-08-30 12:15:40 +00002097 if options.merge:
2098 merge(complete_state, blacklist)
2099 else:
2100 pretty_print(value, sys.stdout)
2101
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00002102 if exceptions:
2103 # It got an exception, raise the first one.
2104 raise \
2105 exceptions[0][0], \
2106 exceptions[0][1], \
2107 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002108 return 0
2109
2110
maruel@chromium.orge5322512013-08-19 20:17:57 +00002111def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002112 """Creates a directory with all the dependencies mapped into it.
2113
2114 Useful to test manually why a test is failing. The target executable is not
2115 run.
2116 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002117 parser.require_isolated = False
maruel@chromium.org9268f042012-10-17 17:36:41 +00002118 options, args = parser.parse_args(args)
2119 if args:
2120 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002121 complete_state = load_complete_state(options, os.getcwd(), None, False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002122
2123 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002124 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002125 'isolate', complete_state.root_dir)
2126 else:
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002127 if is_url(options.outdir):
maruel@chromium.org29029882013-08-30 12:15:40 +00002128 parser.error('Can\'t use url for --outdir with mode remap.')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002129 if not os.path.isdir(options.outdir):
2130 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00002131 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002132 if len(os.listdir(options.outdir)):
2133 raise ExecutionError('Can\'t remap in a non-empty directory')
2134 recreate_tree(
2135 outdir=options.outdir,
2136 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002137 infiles=complete_state.saved_state.files,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00002138 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00002139 as_hash=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002140 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002141 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002142
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002143 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002144 complete_state.save_files()
2145 return 0
2146
2147
maruel@chromium.orge5322512013-08-19 20:17:57 +00002148def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002149 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00002150 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002151 options, args = parser.parse_args(args)
2152 if args:
2153 parser.error('Unsupported argument: %s' % args)
2154
2155 if options.isolated:
2156 # Load the previous state if it was present. Namely, "foo.isolated.state".
2157 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002158 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002159 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002160 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002161 if not isolate:
maruel@chromium.org29029882013-08-30 12:15:40 +00002162 parser.error('--isolate is required.')
2163
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002164 with open(isolate, 'r') as f:
2165 content = f.read()
2166 config = load_isolate_as_config(
2167 os.path.dirname(os.path.abspath(isolate)),
2168 eval_content(content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00002169 extract_comment(content))
2170 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002171 print('Updating %s' % isolate)
2172 with open(isolate, 'wb') as f:
2173 print_all(config.file_comment, data, f)
2174 return 0
2175
2176
maruel@chromium.org29029882013-08-30 12:15:40 +00002177@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00002178def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002179 """Runs the test executable in an isolated (temporary) directory.
2180
2181 All the dependencies are mapped into the temporary directory and the
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002182 directory is cleaned up after the target exits. Warning: if --outdir is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002183 specified, it is deleted upon exit.
2184
maruel@chromium.org29029882013-08-30 12:15:40 +00002185 Argument processing stops at -- and these arguments are appended to the
2186 command line of the target to run. For example, use:
2187 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002188 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002189 parser.require_isolated = False
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002190 parser.add_option(
2191 '--skip-refresh', action='store_true',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00002192 help='Skip reading .isolate file and do not refresh the hash of '
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002193 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002194 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00002195 if options.outdir and is_url(options.outdir):
2196 parser.error('Can\'t use url for --outdir with mode run.')
2197
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002198 complete_state = load_complete_state(
2199 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002200 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002201 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00002202 raise ExecutionError('No command to run.')
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002203
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002204 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002205 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002206 root_dir = complete_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002207 if not options.outdir:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002208 if not os.path.isabs(root_dir):
2209 root_dir = os.path.join(os.path.dirname(options.isolated), root_dir)
2210 options.outdir = run_isolated.make_temp_dir('isolate', root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002211 else:
2212 if not os.path.isdir(options.outdir):
2213 os.makedirs(options.outdir)
2214 recreate_tree(
2215 outdir=options.outdir,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002216 indir=root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002217 infiles=complete_state.saved_state.files,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00002218 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00002219 as_hash=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002220 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002221 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002222 if not os.path.isdir(cwd):
2223 # It can happen when no files are mapped from the directory containing the
2224 # .isolate file. But the directory must exist to be the current working
2225 # directory.
2226 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002227 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002228 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002229 logging.info('Running %s, cwd=%s' % (cmd, cwd))
2230 result = subprocess.call(cmd, cwd=cwd)
2231 finally:
2232 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002233 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002234
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002235 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002236 complete_state.save_files()
2237 return result
2238
2239
maruel@chromium.org29029882013-08-30 12:15:40 +00002240@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00002241def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002242 """Traces the target using trace_inputs.py.
2243
2244 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002245 its child processes access. Then the 'merge' command can be used to generate
2246 an updated .isolate file out of it or the 'read' command to print it out to
2247 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002248
maruel@chromium.org29029882013-08-30 12:15:40 +00002249 Argument processing stops at -- and these arguments are appended to the
2250 command line of the target to run. For example, use:
2251 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002252 """
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002253 add_trace_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002254 parser.add_option(
2255 '-m', '--merge', action='store_true',
2256 help='After tracing, merge the results back in the .isolate file')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002257 parser.add_option(
2258 '--skip-refresh', action='store_true',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00002259 help='Skip reading .isolate file and do not refresh the hash of '
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002260 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002261 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00002262
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002263 complete_state = load_complete_state(
2264 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002265 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002266 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00002267 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002268 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002269 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002270 unicode(complete_state.root_dir),
2271 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00002272 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
2273 if not os.path.isfile(cmd[0]):
2274 raise ExecutionError(
2275 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002276 logging.info('Running %s, cwd=%s' % (cmd, cwd))
2277 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002278 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002279 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002280 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002281 try:
2282 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002283 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002284 cmd,
2285 cwd,
2286 'default',
2287 True)
2288 except trace_inputs.TracingFailure, e:
2289 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
2290
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002291 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002292 logging.error(
2293 'Tracer exited with %d, which means the tests probably failed so the '
2294 'trace is probably incomplete.', result)
2295 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002296
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002297 complete_state.save_files()
2298
2299 if options.merge:
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002300 blacklist = trace_inputs.gen_blacklist(options.trace_blacklist)
2301 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002302
2303 return result
2304
2305
maruel@chromium.org712454d2013-04-04 17:52:34 +00002306def _process_variable_arg(_option, _opt, _value, parser):
2307 if not parser.rargs:
2308 raise optparse.OptionValueError(
2309 'Please use --variable FOO=BAR or --variable FOO BAR')
2310 k = parser.rargs.pop(0)
2311 if '=' in k:
2312 parser.values.variables.append(tuple(k.split('=', 1)))
2313 else:
2314 if not parser.rargs:
2315 raise optparse.OptionValueError(
2316 'Please use --variable FOO=BAR or --variable FOO BAR')
2317 v = parser.rargs.pop(0)
2318 parser.values.variables.append((k, v))
2319
2320
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002321def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002322 """Adds --isolated and --variable to an OptionParser."""
2323 parser.add_option(
2324 '-s', '--isolated',
2325 metavar='FILE',
2326 help='.isolated file to generate or read')
2327 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002328 parser.add_option(
2329 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002330 dest='isolated',
2331 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002332 default_variables = [('OS', get_flavor())]
2333 if sys.platform in ('win32', 'cygwin'):
2334 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
2335 else:
2336 default_variables.append(('EXECUTABLE_SUFFIX', ''))
2337 parser.add_option(
2338 '-V', '--variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00002339 action='callback',
2340 callback=_process_variable_arg,
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002341 default=default_variables,
2342 dest='variables',
2343 metavar='FOO BAR',
2344 help='Variables to process in the .isolate file, default: %default. '
2345 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002346 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002347
2348
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002349def add_trace_option(parser):
2350 """Adds --trace-blacklist to the parser."""
2351 parser.add_option(
2352 '--trace-blacklist',
2353 action='append', default=list(DEFAULT_BLACKLIST),
2354 help='List of regexp to use as blacklist filter for files to consider '
2355 'important, not to be confused with --blacklist which blacklists '
2356 'test case.')
2357
2358
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002359def parse_isolated_option(parser, options, cwd, require_isolated):
2360 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002361 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002362 options.isolated = os.path.normpath(
2363 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002364 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00002365 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002366 if options.isolated and not options.isolated.endswith('.isolated'):
2367 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002368
2369
2370def parse_variable_option(options):
2371 """Processes --variable."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002372 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
2373 # but it wouldn't be backward compatible.
2374 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002375 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002376 try:
2377 return int(s)
2378 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002379 return s.decode('utf-8')
benrg@chromium.org609b7982013-02-07 16:44:46 +00002380 options.variables = dict((k, try_make_int(v)) for k, v in options.variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002381
2382
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002383class OptionParserIsolate(tools.OptionParserWithLogging):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002384 """Adds automatic --isolate, --isolated, --out and --variable handling."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00002385 # Set it to False if it is not required, e.g. it can be passed on but do not
2386 # fail if not given.
2387 require_isolated = True
2388
2389 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002390 tools.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00002391 self,
2392 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
2393 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002394 group = optparse.OptionGroup(self, "Common options")
2395 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002396 '-i', '--isolate',
2397 metavar='FILE',
2398 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002399 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002400 group.add_option(
2401 '-o', '--outdir', metavar='DIR',
2402 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002403 'Defaults: run|remap: a /tmp subdirectory, others: '
2404 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00002405 group.add_option(
2406 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002407 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
2408 help='Indicates that invalid entries in the isolated file to be '
2409 'only be logged and not stop processing. Defaults to True if '
2410 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002411 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002412
2413 def parse_args(self, *args, **kwargs):
2414 """Makes sure the paths make sense.
2415
2416 On Windows, / and \ are often mixed together in a path.
2417 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002418 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002419 self, *args, **kwargs)
2420 if not self.allow_interspersed_args and args:
2421 self.error('Unsupported argument: %s' % args)
2422
maruel@chromium.orga3da9122013-03-28 13:27:09 +00002423 cwd = trace_inputs.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002424 parse_isolated_option(self, options, cwd, self.require_isolated)
2425 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002426
2427 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002428 # TODO(maruel): Work with non-ASCII.
2429 # The path must be in native path case for tracing purposes.
2430 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
2431 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
2432 options.isolate = trace_inputs.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002433
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002434 if options.outdir and not is_url(options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002435 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
2436 # outdir doesn't need native path case since tracing is never done from
2437 # there.
2438 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002439
2440 return options, args
2441
2442
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002443def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00002444 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002445 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00002446 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002447 except (
2448 ExecutionError,
maruel@chromium.org9958e4a2013-09-17 00:01:48 +00002449 isolateserver.ConfigError,
2450 isolateserver.MappingError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002451 sys.stderr.write('\nError: ')
2452 sys.stderr.write(str(e))
2453 sys.stderr.write('\n')
2454 return 1
2455
2456
2457if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00002458 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002459 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00002460 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002461 sys.exit(main(sys.argv[1:]))