blob: 67cf15e8b5bbf82bb83dcf1ac173b249d435ba66 [file] [log] [blame]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Front end tool to manage .isolate files and corresponding tests.
7
8Run ./isolate.py --help for more detailed information.
9
10See more information at
11http://dev.chromium.org/developers/testing/isolated-testing
12"""
13
benrg@chromium.org609b7982013-02-07 16:44:46 +000014import ast
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000015import copy
16import hashlib
benrg@chromium.org609b7982013-02-07 16:44:46 +000017import itertools
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000018import logging
19import optparse
20import os
21import posixpath
22import re
23import stat
24import subprocess
25import sys
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000026
maruel@chromium.orgc6f90062012-11-07 18:32:22 +000027import isolateserver_archive
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000028import run_isolated
benrg@chromium.org609b7982013-02-07 16:44:46 +000029import short_expression_finder
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000030import trace_inputs
31
32# Import here directly so isolate is easier to use as a library.
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000033from run_isolated import get_flavor
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000034
35
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000036PATH_VARIABLES = ('DEPTH', 'PRODUCT_DIR')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000037
38# Files that should be 0-length when mapped.
39KEY_TOUCHED = 'isolate_dependency_touched'
40# Files that should be tracked by the build tool.
41KEY_TRACKED = 'isolate_dependency_tracked'
42# Files that should not be tracked by the build tool.
43KEY_UNTRACKED = 'isolate_dependency_untracked'
44
45_GIT_PATH = os.path.sep + '.git'
46_SVN_PATH = os.path.sep + '.svn'
47
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000048
49class ExecutionError(Exception):
50 """A generic error occurred."""
51 def __str__(self):
52 return self.args[0]
53
54
55### Path handling code.
56
57
58def relpath(path, root):
59 """os.path.relpath() that keeps trailing os.path.sep."""
60 out = os.path.relpath(path, root)
61 if path.endswith(os.path.sep):
62 out += os.path.sep
63 return out
64
65
maruel@chromium.org8abec8b2013-04-16 19:34:20 +000066def safe_relpath(filepath, basepath):
67 """Do not throw on Windows when filepath and basepath are on different drives.
68
69 Different than relpath() above since this one doesn't keep the trailing
70 os.path.sep and it swallows exceptions on Windows and return the original
71 absolute path in the case of different drives.
72 """
73 try:
74 return os.path.relpath(filepath, basepath)
75 except ValueError:
76 assert sys.platform == 'win32'
77 return filepath
78
79
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000080def normpath(path):
81 """os.path.normpath() that keeps trailing os.path.sep."""
82 out = os.path.normpath(path)
83 if path.endswith(os.path.sep):
84 out += os.path.sep
85 return out
86
87
88def posix_relpath(path, root):
89 """posix.relpath() that keeps trailing slash."""
90 out = posixpath.relpath(path, root)
91 if path.endswith('/'):
92 out += '/'
93 return out
94
95
96def cleanup_path(x):
97 """Cleans up a relative path. Converts any os.path.sep to '/' on Windows."""
98 if x:
99 x = x.rstrip(os.path.sep).replace(os.path.sep, '/')
100 if x == '.':
101 x = ''
102 if x:
103 x += '/'
104 return x
105
106
maruel@chromium.orgb9520b02013-03-13 18:00:03 +0000107def is_url(path):
108 return bool(re.match(r'^https?://.+$', path))
109
110
maruel@chromium.orgd2627672013-04-03 15:30:24 +0000111def chromium_default_blacklist(f):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000112 """Filters unimportant files normally ignored."""
113 return (
maruel@chromium.orgd2627672013-04-03 15:30:24 +0000114 f.endswith(('.pyc', '.swp', '.run_test_cases', 'testserver.log')) or
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000115 _GIT_PATH in f or
116 _SVN_PATH in f or
117 f in ('.git', '.svn'))
118
119
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000120def path_starts_with(prefix, path):
121 """Returns true if the components of the path |prefix| are the same as the
122 initial components of |path| (or all of the components of |path|). The paths
123 must be absolute.
124 """
125 assert os.path.isabs(prefix) and os.path.isabs(path)
126 prefix = os.path.normpath(prefix)
127 path = os.path.normpath(path)
128 assert prefix == trace_inputs.get_native_path_case(prefix), prefix
129 assert path == trace_inputs.get_native_path_case(path), path
130 prefix = prefix.rstrip(os.path.sep) + os.path.sep
131 path = path.rstrip(os.path.sep) + os.path.sep
132 return path.startswith(prefix)
133
134
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000135def fix_native_path_case(root, path):
136 """Ensures that each component of |path| has the proper native case by
137 iterating slowly over the directory elements of |path|."""
138 native_case_path = root
139 for raw_part in path.split(os.sep):
140 if not raw_part or raw_part == '.':
141 break
142
143 part = trace_inputs.find_item_native_case(native_case_path, raw_part)
144 if not part:
145 raise run_isolated.MappingError(
146 'Input file %s doesn\'t exist' %
147 os.path.join(native_case_path, raw_part))
148 native_case_path = os.path.join(native_case_path, part)
149
150 return os.path.normpath(native_case_path)
151
152
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000153def expand_symlinks(indir, relfile):
154 """Follows symlinks in |relfile|, but treating symlinks that point outside the
155 build tree as if they were ordinary directories/files. Returns the final
156 symlink-free target and a list of paths to symlinks encountered in the
157 process.
158
159 The rule about symlinks outside the build tree is for the benefit of the
160 Chromium OS ebuild, which symlinks the output directory to an unrelated path
161 in the chroot.
162
163 Fails when a directory loop is detected, although in theory we could support
164 that case.
165 """
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000166 is_directory = relfile.endswith(os.path.sep)
167 done = indir
168 todo = relfile.strip(os.path.sep)
169 symlinks = []
170
171 while todo:
172 pre_symlink, symlink, post_symlink = trace_inputs.split_at_symlink(
173 done, todo)
174 if not symlink:
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000175 todo = fix_native_path_case(done, todo)
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000176 done = os.path.join(done, todo)
177 break
178 symlink_path = os.path.join(done, pre_symlink, symlink)
179 post_symlink = post_symlink.lstrip(os.path.sep)
180 # readlink doesn't exist on Windows.
181 # pylint: disable=E1101
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000182 target = os.path.normpath(os.path.join(done, pre_symlink))
183 symlink_target = os.readlink(symlink_path)
maruel@chromium.org28c19672013-04-29 18:51:09 +0000184 if os.path.isabs(symlink_target):
185 # Absolute path are considered a normal directories. The use case is
186 # generally someone who puts the output directory on a separate drive.
187 target = symlink_target
188 else:
189 # The symlink itself could be using the wrong path case.
190 target = fix_native_path_case(target, symlink_target)
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000191
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000192 if not os.path.exists(target):
193 raise run_isolated.MappingError(
194 'Symlink target doesn\'t exist: %s -> %s' % (symlink_path, target))
195 target = trace_inputs.get_native_path_case(target)
196 if not path_starts_with(indir, target):
197 done = symlink_path
198 todo = post_symlink
199 continue
200 if path_starts_with(target, symlink_path):
201 raise run_isolated.MappingError(
202 'Can\'t map recursive symlink reference %s -> %s' %
203 (symlink_path, target))
204 logging.info('Found symlink: %s -> %s', symlink_path, target)
205 symlinks.append(os.path.relpath(symlink_path, indir))
206 # Treat the common prefix of the old and new paths as done, and start
207 # scanning again.
208 target = target.split(os.path.sep)
209 symlink_path = symlink_path.split(os.path.sep)
210 prefix_length = 0
211 for target_piece, symlink_path_piece in zip(target, symlink_path):
212 if target_piece == symlink_path_piece:
213 prefix_length += 1
214 else:
215 break
216 done = os.path.sep.join(target[:prefix_length])
217 todo = os.path.join(
218 os.path.sep.join(target[prefix_length:]), post_symlink)
219
220 relfile = os.path.relpath(done, indir)
221 relfile = relfile.rstrip(os.path.sep) + is_directory * os.path.sep
222 return relfile, symlinks
223
224
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000225def expand_directory_and_symlink(indir, relfile, blacklist, follow_symlinks):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000226 """Expands a single input. It can result in multiple outputs.
227
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000228 This function is recursive when relfile is a directory.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000229
230 Note: this code doesn't properly handle recursive symlink like one created
231 with:
232 ln -s .. foo
233 """
234 if os.path.isabs(relfile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000235 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000236 'Can\'t map absolute path %s' % relfile)
237
238 infile = normpath(os.path.join(indir, relfile))
239 if not infile.startswith(indir):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000240 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000241 'Can\'t map file %s outside %s' % (infile, indir))
242
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000243 filepath = os.path.join(indir, relfile)
244 native_filepath = trace_inputs.get_native_path_case(filepath)
245 if filepath != native_filepath:
maruel@chromium.org59bb2a32013-03-21 17:08:39 +0000246 # Special case './'.
247 if filepath != native_filepath + '.' + os.path.sep:
maruel@chromium.org7f66a982013-06-06 15:58:59 +0000248 # Give up enforcing strict path case on OSX. Really, it's that sad. The
249 # case where it happens is very specific and hard to reproduce:
250 # get_native_path_case(
251 # u'Foo.framework/Versions/A/Resources/Something.nib') will return
252 # u'Foo.framework/Versions/A/resources/Something.nib', e.g. lowercase 'r'.
253 #
254 # Note that this is really something deep in OSX because running
255 # ls Foo.framework/Versions/A
256 # will print out 'Resources', while trace_inputs.get_native_path_case()
257 # returns a lower case 'r'.
258 #
259 # So *something* is happening under the hood resulting in the command 'ls'
260 # and Carbon.File.FSPathMakeRef('path').FSRefMakePath() to disagree. We
261 # have no idea why.
262 if sys.platform != 'darwin':
263 raise run_isolated.MappingError(
264 'File path doesn\'t equal native file path\n%s != %s' %
265 (filepath, native_filepath))
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000266
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000267 symlinks = []
268 if follow_symlinks:
269 relfile, symlinks = expand_symlinks(indir, relfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000270
271 if relfile.endswith(os.path.sep):
272 if not os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000273 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000274 '%s is not a directory but ends with "%s"' % (infile, os.path.sep))
275
maruel@chromium.org59bb2a32013-03-21 17:08:39 +0000276 # Special case './'.
277 if relfile.startswith('.' + os.path.sep):
278 relfile = relfile[2:]
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000279 outfiles = symlinks
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000280 try:
281 for filename in os.listdir(infile):
282 inner_relfile = os.path.join(relfile, filename)
283 if blacklist(inner_relfile):
284 continue
285 if os.path.isdir(os.path.join(indir, inner_relfile)):
286 inner_relfile += os.path.sep
287 outfiles.extend(
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000288 expand_directory_and_symlink(indir, inner_relfile, blacklist,
289 follow_symlinks))
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000290 return outfiles
291 except OSError as e:
maruel@chromium.org1cd786e2013-04-26 18:48:40 +0000292 raise run_isolated.MappingError(
293 'Unable to iterate over directory %s.\n%s' % (infile, e))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000294 else:
295 # Always add individual files even if they were blacklisted.
296 if os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000297 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000298 'Input directory %s must have a trailing slash' % infile)
299
300 if not os.path.isfile(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000301 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000302 'Input file %s doesn\'t exist' % infile)
303
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000304 return symlinks + [relfile]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000305
306
csharp@chromium.org01856802012-11-12 17:48:13 +0000307def expand_directories_and_symlinks(indir, infiles, blacklist,
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000308 follow_symlinks, ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000309 """Expands the directories and the symlinks, applies the blacklist and
310 verifies files exist.
311
312 Files are specified in os native path separator.
313 """
314 outfiles = []
315 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +0000316 try:
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000317 outfiles.extend(expand_directory_and_symlink(indir, relfile, blacklist,
318 follow_symlinks))
csharp@chromium.org01856802012-11-12 17:48:13 +0000319 except run_isolated.MappingError as e:
320 if ignore_broken_items:
321 logging.info('warning: %s', e)
322 else:
323 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000324 return outfiles
325
326
327def recreate_tree(outdir, indir, infiles, action, as_sha1):
328 """Creates a new tree with only the input files in it.
329
330 Arguments:
331 outdir: Output directory to create the files in.
332 indir: Root directory the infiles are based in.
333 infiles: dict of files to map from |indir| to |outdir|.
334 action: See assert below.
335 as_sha1: Output filename is the sha1 instead of relfile.
336 """
337 logging.info(
338 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_sha1=%s)' %
339 (outdir, indir, len(infiles), action, as_sha1))
340
341 assert action in (
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000342 run_isolated.HARDLINK,
343 run_isolated.SYMLINK,
344 run_isolated.COPY)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000345 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000346 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000347 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000348 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000349
350 for relfile, metadata in infiles.iteritems():
351 infile = os.path.join(indir, relfile)
352 if as_sha1:
353 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000354 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000355 # Skip links when storing a hashtable.
356 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000357 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000358 if os.path.isfile(outfile):
359 # Just do a quick check that the file size matches. No need to stat()
360 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000361 if not 's' in metadata:
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000362 raise run_isolated.MappingError(
363 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000364 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000365 continue
366 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000367 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000368 os.remove(outfile)
369 else:
370 outfile = os.path.join(outdir, relfile)
371 outsubdir = os.path.dirname(outfile)
372 if not os.path.isdir(outsubdir):
373 os.makedirs(outsubdir)
374
375 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000376 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000377 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000378 if 'l' in metadata:
379 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000380 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000381 # symlink doesn't exist on Windows.
382 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000383 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000384 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000385
386
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000387def process_input(filepath, prevdict, read_only, flavor):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000388 """Processes an input file, a dependency, and return meta data about it.
389
390 Arguments:
391 - filepath: File to act on.
392 - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
393 to skip recalculating the hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000394 - read_only: If True, the file mode is manipulated. In practice, only save
395 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
396 windows, mode is not set since all files are 'executable' by
397 default.
398
399 Behaviors:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000400 - Retrieves the file mode, file size, file timestamp, file link
401 destination if it is a file link and calcultate the SHA-1 of the file's
402 content if the path points to a file and not a symlink.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000403 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000404 out = {}
405 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000406 # if prevdict.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000407 # # The file's content is ignored. Skip the time and hard code mode.
408 # if get_flavor() != 'win':
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000409 # out['m'] = stat.S_IRUSR | stat.S_IRGRP
410 # out['s'] = 0
411 # out['h'] = SHA_1_NULL
412 # out['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000413 # return out
414
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000415 # Always check the file stat and check if it is a link. The timestamp is used
416 # to know if the file's content/symlink destination should be looked into.
417 # E.g. only reuse from prevdict if the timestamp hasn't changed.
418 # There is the risk of the file's timestamp being reset to its last value
419 # manually while its content changed. We don't protect against that use case.
420 try:
421 filestats = os.lstat(filepath)
422 except OSError:
423 # The file is not present.
424 raise run_isolated.MappingError('%s is missing' % filepath)
425 is_link = stat.S_ISLNK(filestats.st_mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000426
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000427 if flavor != 'win':
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000428 # Ignore file mode on Windows since it's not really useful there.
429 filemode = stat.S_IMODE(filestats.st_mode)
430 # Remove write access for group and all access to 'others'.
431 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
432 if read_only:
433 filemode &= ~stat.S_IWUSR
434 if filemode & stat.S_IXUSR:
435 filemode |= stat.S_IXGRP
436 else:
437 filemode &= ~stat.S_IXGRP
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000438 out['m'] = filemode
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000439
440 # Used to skip recalculating the hash or link destination. Use the most recent
441 # update time.
442 # TODO(maruel): Save it in the .state file instead of .isolated so the
443 # .isolated file is deterministic.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000444 out['t'] = int(round(filestats.st_mtime))
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000445
446 if not is_link:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000447 out['s'] = filestats.st_size
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000448 # If the timestamp wasn't updated and the file size is still the same, carry
449 # on the sha-1.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000450 if (prevdict.get('t') == out['t'] and
451 prevdict.get('s') == out['s']):
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000452 # Reuse the previous hash if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000453 out['h'] = prevdict.get('h')
454 if not out.get('h'):
maruel@chromium.org6da38772012-12-11 21:36:37 +0000455 out['h'] = isolateserver_archive.sha1_file(filepath)
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000456 else:
457 # If the timestamp wasn't updated, carry on the link destination.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000458 if prevdict.get('t') == out['t']:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000459 # Reuse the previous link destination if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000460 out['l'] = prevdict.get('l')
461 if out.get('l') is None:
maruel@chromium.org8d159e32013-04-18 15:29:50 +0000462 # The link could be in an incorrect path case. In practice, this only
463 # happen on OSX on case insensitive HFS.
464 # TODO(maruel): It'd be better if it was only done once, in
465 # expand_directory_and_symlink(), so it would not be necessary to do again
466 # here.
467 symlink_value = os.readlink(filepath) # pylint: disable=E1101
468 filedir = trace_inputs.get_native_path_case(os.path.dirname(filepath))
469 native_dest = fix_native_path_case(filedir, symlink_value)
470 out['l'] = os.path.relpath(native_dest, filedir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000471 return out
472
473
474### Variable stuff.
475
476
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000477def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000478 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000479 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000480
481
482def determine_root_dir(relative_root, infiles):
483 """For a list of infiles, determines the deepest root directory that is
484 referenced indirectly.
485
486 All arguments must be using os.path.sep.
487 """
488 # The trick used to determine the root directory is to look at "how far" back
489 # up it is looking up.
490 deepest_root = relative_root
491 for i in infiles:
492 x = relative_root
493 while i.startswith('..' + os.path.sep):
494 i = i[3:]
495 assert not i.startswith(os.path.sep)
496 x = os.path.dirname(x)
497 if deepest_root.startswith(x):
498 deepest_root = x
499 logging.debug(
500 'determine_root_dir(%s, %d files) -> %s' % (
501 relative_root, len(infiles), deepest_root))
502 return deepest_root
503
504
505def replace_variable(part, variables):
506 m = re.match(r'<\(([A-Z_]+)\)', part)
507 if m:
508 if m.group(1) not in variables:
509 raise ExecutionError(
510 'Variable "%s" was not found in %s.\nDid you forget to specify '
511 '--variable?' % (m.group(1), variables))
512 return variables[m.group(1)]
513 return part
514
515
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000516def process_variables(cwd, variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000517 """Processes path variables as a special case and returns a copy of the dict.
518
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000519 For each 'path' variable: first normalizes it based on |cwd|, verifies it
520 exists then sets it as relative to relative_base_dir.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000521 """
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000522 relative_base_dir = trace_inputs.get_native_path_case(relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000523 variables = variables.copy()
524 for i in PATH_VARIABLES:
525 if i not in variables:
526 continue
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000527 variable = variables[i].strip()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000528 # Variables could contain / or \ on windows. Always normalize to
529 # os.path.sep.
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000530 variable = variable.replace('/', os.path.sep)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000531 variable = os.path.join(cwd, variable)
532 variable = os.path.normpath(variable)
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000533 variable = trace_inputs.get_native_path_case(variable)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000534 if not os.path.isdir(variable):
535 raise ExecutionError('%s=%s is not a directory' % (i, variable))
536
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000537 # All variables are relative to the .isolate file.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000538 variable = os.path.relpath(variable, relative_base_dir)
539 logging.debug(
540 'Translated variable %s from %s to %s', i, variables[i], variable)
541 variables[i] = variable
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000542 return variables
543
544
545def eval_variables(item, variables):
546 """Replaces the .isolate variables in a string item.
547
548 Note that the .isolate format is a subset of the .gyp dialect.
549 """
550 return ''.join(
551 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
552
553
554def classify_files(root_dir, tracked, untracked):
555 """Converts the list of files into a .isolate 'variables' dictionary.
556
557 Arguments:
558 - tracked: list of files names to generate a dictionary out of that should
559 probably be tracked.
560 - untracked: list of files names that must not be tracked.
561 """
562 # These directories are not guaranteed to be always present on every builder.
563 OPTIONAL_DIRECTORIES = (
564 'test/data/plugin',
565 'third_party/WebKit/LayoutTests',
566 )
567
568 new_tracked = []
569 new_untracked = list(untracked)
570
571 def should_be_tracked(filepath):
572 """Returns True if it is a file without whitespace in a non-optional
573 directory that has no symlink in its path.
574 """
575 if filepath.endswith('/'):
576 return False
577 if ' ' in filepath:
578 return False
579 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
580 return False
581 # Look if any element in the path is a symlink.
582 split = filepath.split('/')
583 for i in range(len(split)):
584 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
585 return False
586 return True
587
588 for filepath in sorted(tracked):
589 if should_be_tracked(filepath):
590 new_tracked.append(filepath)
591 else:
592 # Anything else.
593 new_untracked.append(filepath)
594
595 variables = {}
596 if new_tracked:
597 variables[KEY_TRACKED] = sorted(new_tracked)
598 if new_untracked:
599 variables[KEY_UNTRACKED] = sorted(new_untracked)
600 return variables
601
602
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000603def chromium_fix(f, variables):
604 """Fixes an isolate dependnecy with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000605 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
606 # separator.
607 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000608 # Ignored items.
609 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000610 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000611 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000612 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000613 # 'First Run' is not created by the compile, but by the test itself.
614 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000615
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000616 # Blacklist logs and other unimportant files.
617 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
618 logging.debug('Ignoring %s', f)
619 return None
620
maruel@chromium.org7650e422012-11-16 21:56:42 +0000621 EXECUTABLE = re.compile(
622 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
623 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
624 r'$')
625 match = EXECUTABLE.match(f)
626 if match:
627 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
628
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000629 if sys.platform == 'darwin':
630 # On OSX, the name of the output is dependent on gyp define, it can be
631 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
632 # Framework.framework'. Furthermore, they are versioned with a gyp
633 # variable. To lower the complexity of the .isolate file, remove all the
634 # individual entries that show up under any of the 4 entries and replace
635 # them with the directory itself. Overall, this results in a bit more
636 # files than strictly necessary.
637 OSX_BUNDLES = (
638 '<(PRODUCT_DIR)/Chromium Framework.framework/',
639 '<(PRODUCT_DIR)/Chromium.app/',
640 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
641 '<(PRODUCT_DIR)/Google Chrome.app/',
642 )
643 for prefix in OSX_BUNDLES:
644 if f.startswith(prefix):
645 # Note this result in duplicate values, so the a set() must be used to
646 # remove duplicates.
647 return prefix
648 return f
649
650
651def generate_simplified(
652 tracked, untracked, touched, root_dir, variables, relative_cwd):
653 """Generates a clean and complete .isolate 'variables' dictionary.
654
655 Cleans up and extracts only files from within root_dir then processes
656 variables and relative_cwd.
657 """
658 root_dir = os.path.realpath(root_dir)
659 logging.info(
660 'generate_simplified(%d files, %s, %s, %s)' %
661 (len(tracked) + len(untracked) + len(touched),
662 root_dir, variables, relative_cwd))
663
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000664 # Preparation work.
665 relative_cwd = cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000666 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000667 # Creates the right set of variables here. We only care about PATH_VARIABLES.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000668 path_variables = dict(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000669 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
670 for k in PATH_VARIABLES if k in variables)
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000671 variables = variables.copy()
672 variables.update(path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000673
674 # Actual work: Process the files.
675 # TODO(maruel): if all the files in a directory are in part tracked and in
676 # part untracked, the directory will not be extracted. Tracked files should be
677 # 'promoted' to be untracked as needed.
678 tracked = trace_inputs.extract_directories(
maruel@chromium.orgd2627672013-04-03 15:30:24 +0000679 root_dir, tracked, chromium_default_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000680 untracked = trace_inputs.extract_directories(
maruel@chromium.orgd2627672013-04-03 15:30:24 +0000681 root_dir, untracked, chromium_default_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000682 # touched is not compressed, otherwise it would result in files to be archived
683 # that we don't need.
684
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000685 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000686 def fix(f):
687 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000688 # Important, GYP stores the files with / and not \.
689 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000690 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000691 # If it's not already a variable.
692 if not f.startswith('<'):
693 # relative_cwd is usually the directory containing the gyp file. It may be
694 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000695 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000696 # Convert the whole thing to / since it's isolate's speak.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000697 f = posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000698 posixpath.join(root_dir_posix, f),
699 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000700
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000701 for variable, root_path in path_variables.iteritems():
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000702 if f.startswith(root_path):
703 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000704 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000705 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000706 return f
707
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000708 def fix_all(items):
709 """Reduces the items to convert variables, removes unneeded items, apply
710 chromium-specific fixes and only return unique items.
711 """
712 variables_converted = (fix(f.path) for f in items)
713 chromium_fixed = (chromium_fix(f, variables) for f in variables_converted)
714 return set(f for f in chromium_fixed if f)
715
716 tracked = fix_all(tracked)
717 untracked = fix_all(untracked)
718 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000719 out = classify_files(root_dir, tracked, untracked)
720 if touched:
721 out[KEY_TOUCHED] = sorted(touched)
722 return out
723
724
benrg@chromium.org609b7982013-02-07 16:44:46 +0000725def chromium_filter_flags(variables):
726 """Filters out build flags used in Chromium that we don't want to treat as
727 configuration variables.
728 """
729 # TODO(benrg): Need a better way to determine this.
730 blacklist = set(PATH_VARIABLES + ('EXECUTABLE_SUFFIX', 'FLAG'))
731 return dict((k, v) for k, v in variables.iteritems() if k not in blacklist)
732
733
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000734def generate_isolate(
735 tracked, untracked, touched, root_dir, variables, relative_cwd):
736 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000737 dependencies = generate_simplified(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000738 tracked, untracked, touched, root_dir, variables, relative_cwd)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000739 config_variables = chromium_filter_flags(variables)
740 config_variable_names, config_values = zip(
741 *sorted(config_variables.iteritems()))
742 out = Configs(None)
743 # The new dependencies apply to just one configuration, namely config_values.
744 out.merge_dependencies(dependencies, config_variable_names, [config_values])
745 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000746
747
748def split_touched(files):
749 """Splits files that are touched vs files that are read."""
750 tracked = []
751 touched = []
752 for f in files:
753 if f.size:
754 tracked.append(f)
755 else:
756 touched.append(f)
757 return tracked, touched
758
759
760def pretty_print(variables, stdout):
761 """Outputs a gyp compatible list from the decoded variables.
762
763 Similar to pprint.print() but with NIH syndrome.
764 """
765 # Order the dictionary keys by these keys in priority.
766 ORDER = (
767 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
768 KEY_TRACKED, KEY_UNTRACKED)
769
770 def sorting_key(x):
771 """Gives priority to 'most important' keys before the others."""
772 if x in ORDER:
773 return str(ORDER.index(x))
774 return x
775
776 def loop_list(indent, items):
777 for item in items:
778 if isinstance(item, basestring):
779 stdout.write('%s\'%s\',\n' % (indent, item))
780 elif isinstance(item, dict):
781 stdout.write('%s{\n' % indent)
782 loop_dict(indent + ' ', item)
783 stdout.write('%s},\n' % indent)
784 elif isinstance(item, list):
785 # A list inside a list will write the first item embedded.
786 stdout.write('%s[' % indent)
787 for index, i in enumerate(item):
788 if isinstance(i, basestring):
789 stdout.write(
790 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
791 elif isinstance(i, dict):
792 stdout.write('{\n')
793 loop_dict(indent + ' ', i)
794 if index != len(item) - 1:
795 x = ', '
796 else:
797 x = ''
798 stdout.write('%s}%s' % (indent, x))
799 else:
800 assert False
801 stdout.write('],\n')
802 else:
803 assert False
804
805 def loop_dict(indent, items):
806 for key in sorted(items, key=sorting_key):
807 item = items[key]
808 stdout.write("%s'%s': " % (indent, key))
809 if isinstance(item, dict):
810 stdout.write('{\n')
811 loop_dict(indent + ' ', item)
812 stdout.write(indent + '},\n')
813 elif isinstance(item, list):
814 stdout.write('[\n')
815 loop_list(indent + ' ', item)
816 stdout.write(indent + '],\n')
817 elif isinstance(item, basestring):
818 stdout.write(
819 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
820 elif item in (True, False, None):
821 stdout.write('%s\n' % item)
822 else:
823 assert False, item
824
825 stdout.write('{\n')
826 loop_dict(' ', variables)
827 stdout.write('}\n')
828
829
830def union(lhs, rhs):
831 """Merges two compatible datastructures composed of dict/list/set."""
832 assert lhs is not None or rhs is not None
833 if lhs is None:
834 return copy.deepcopy(rhs)
835 if rhs is None:
836 return copy.deepcopy(lhs)
837 assert type(lhs) == type(rhs), (lhs, rhs)
838 if hasattr(lhs, 'union'):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000839 # Includes set, ConfigSettings and Configs.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000840 return lhs.union(rhs)
841 if isinstance(lhs, dict):
842 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
843 elif isinstance(lhs, list):
844 # Do not go inside the list.
845 return lhs + rhs
846 assert False, type(lhs)
847
848
849def extract_comment(content):
850 """Extracts file level comment."""
851 out = []
852 for line in content.splitlines(True):
853 if line.startswith('#'):
854 out.append(line)
855 else:
856 break
857 return ''.join(out)
858
859
860def eval_content(content):
861 """Evaluates a python file and return the value defined in it.
862
863 Used in practice for .isolate files.
864 """
865 globs = {'__builtins__': None}
866 locs = {}
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000867 try:
868 value = eval(content, globs, locs)
869 except TypeError as e:
870 e.args = list(e.args) + [content]
871 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000872 assert locs == {}, locs
873 assert globs == {'__builtins__': None}, globs
874 return value
875
876
benrg@chromium.org609b7982013-02-07 16:44:46 +0000877def match_configs(expr, config_variables, all_configs):
878 """Returns the configs from |all_configs| that match the |expr|, where
879 the elements of |all_configs| are tuples of values for the |config_variables|.
880 Example:
881 >>> match_configs(expr = "(foo==1 or foo==2) and bar=='b'",
882 config_variables = ["foo", "bar"],
883 all_configs = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
884 [(1, 'b'), (2, 'b')]
885 """
886 return [
887 config for config in all_configs
888 if eval(expr, dict(zip(config_variables, config)))
889 ]
890
891
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000892def verify_variables(variables):
893 """Verifies the |variables| dictionary is in the expected format."""
894 VALID_VARIABLES = [
895 KEY_TOUCHED,
896 KEY_TRACKED,
897 KEY_UNTRACKED,
898 'command',
899 'read_only',
900 ]
901 assert isinstance(variables, dict), variables
902 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
903 for name, value in variables.iteritems():
904 if name == 'read_only':
905 assert value in (True, False, None), value
906 else:
907 assert isinstance(value, list), value
908 assert all(isinstance(i, basestring) for i in value), value
909
910
benrg@chromium.org609b7982013-02-07 16:44:46 +0000911def verify_ast(expr, variables_and_values):
912 """Verifies that |expr| is of the form
913 expr ::= expr ( "or" | "and" ) expr
914 | identifier "==" ( string | int )
915 Also collects the variable identifiers and string/int values in the dict
916 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
917 """
918 assert isinstance(expr, (ast.BoolOp, ast.Compare))
919 if isinstance(expr, ast.BoolOp):
920 assert isinstance(expr.op, (ast.And, ast.Or))
921 for subexpr in expr.values:
922 verify_ast(subexpr, variables_and_values)
923 else:
924 assert isinstance(expr.left.ctx, ast.Load)
925 assert len(expr.ops) == 1
926 assert isinstance(expr.ops[0], ast.Eq)
927 var_values = variables_and_values.setdefault(expr.left.id, set())
928 rhs = expr.comparators[0]
929 assert isinstance(rhs, (ast.Str, ast.Num))
930 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
931
932
933def verify_condition(condition, variables_and_values):
934 """Verifies the |condition| dictionary is in the expected format.
935 See verify_ast() for the meaning of |variables_and_values|.
936 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000937 VALID_INSIDE_CONDITION = ['variables']
938 assert isinstance(condition, list), condition
benrg@chromium.org609b7982013-02-07 16:44:46 +0000939 assert len(condition) == 2, condition
940 expr, then = condition
941
942 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
943 verify_ast(test_ast.body, variables_and_values)
944
945 assert isinstance(then, dict), then
946 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
947 verify_variables(then['variables'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000948
949
benrg@chromium.org609b7982013-02-07 16:44:46 +0000950def verify_root(value, variables_and_values):
951 """Verifies that |value| is the parsed form of a valid .isolate file.
952 See verify_ast() for the meaning of |variables_and_values|.
953 """
954 VALID_ROOTS = ['includes', 'conditions']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000955 assert isinstance(value, dict), value
956 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000957
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000958 includes = value.get('includes', [])
959 assert isinstance(includes, list), includes
960 for include in includes:
961 assert isinstance(include, basestring), include
962
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000963 conditions = value.get('conditions', [])
964 assert isinstance(conditions, list), conditions
965 for condition in conditions:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000966 verify_condition(condition, variables_and_values)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000967
968
benrg@chromium.org609b7982013-02-07 16:44:46 +0000969def remove_weak_dependencies(values, key, item, item_configs):
970 """Removes any configs from this key if the item is already under a
971 strong key.
972 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000973 if key == KEY_TOUCHED:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000974 item_configs = set(item_configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000975 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000976 try:
977 item_configs -= values[stronger_key][item]
978 except KeyError:
979 pass
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000980
benrg@chromium.org609b7982013-02-07 16:44:46 +0000981 return item_configs
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000982
983
benrg@chromium.org609b7982013-02-07 16:44:46 +0000984def remove_repeated_dependencies(folders, key, item, item_configs):
985 """Removes any configs from this key if the item is in a folder that is
986 already included."""
csharp@chromium.org31176252012-11-02 13:04:40 +0000987
988 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000989 item_configs = set(item_configs)
990 for (folder, configs) in folders.iteritems():
csharp@chromium.org31176252012-11-02 13:04:40 +0000991 if folder != item and item.startswith(folder):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000992 item_configs -= configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000993
benrg@chromium.org609b7982013-02-07 16:44:46 +0000994 return item_configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000995
996
997def get_folders(values_dict):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000998 """Returns a dict of all the folders in the given value_dict."""
999 return dict(
1000 (item, configs) for (item, configs) in values_dict.iteritems()
1001 if item.endswith('/')
1002 )
csharp@chromium.org31176252012-11-02 13:04:40 +00001003
1004
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001005def invert_map(variables):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001006 """Converts {config: {deptype: list(depvals)}} to
1007 {deptype: {depval: set(configs)}}.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001008 """
1009 KEYS = (
1010 KEY_TOUCHED,
1011 KEY_TRACKED,
1012 KEY_UNTRACKED,
1013 'command',
1014 'read_only',
1015 )
1016 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001017 for config, values in variables.iteritems():
1018 for key in KEYS:
1019 if key == 'command':
1020 items = [tuple(values[key])] if key in values else []
1021 elif key == 'read_only':
1022 items = [values[key]] if key in values else []
1023 else:
1024 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
1025 items = values.get(key, [])
1026 for item in items:
1027 out[key].setdefault(item, set()).add(config)
1028 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001029
1030
benrg@chromium.org609b7982013-02-07 16:44:46 +00001031def reduce_inputs(values):
1032 """Reduces the output of invert_map() to the strictest minimum list.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001033
benrg@chromium.org609b7982013-02-07 16:44:46 +00001034 Looks at each individual file and directory, maps where they are used and
1035 reconstructs the inverse dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001036
benrg@chromium.org609b7982013-02-07 16:44:46 +00001037 Returns the minimized dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001038 """
1039 KEYS = (
1040 KEY_TOUCHED,
1041 KEY_TRACKED,
1042 KEY_UNTRACKED,
1043 'command',
1044 'read_only',
1045 )
csharp@chromium.org31176252012-11-02 13:04:40 +00001046
1047 # Folders can only live in KEY_UNTRACKED.
1048 folders = get_folders(values.get(KEY_UNTRACKED, {}))
1049
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001050 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001051 for key in KEYS:
1052 for item, item_configs in values.get(key, {}).iteritems():
1053 item_configs = remove_weak_dependencies(values, key, item, item_configs)
1054 item_configs = remove_repeated_dependencies(
1055 folders, key, item, item_configs)
1056 if item_configs:
1057 out[key][item] = item_configs
1058 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001059
1060
benrg@chromium.org609b7982013-02-07 16:44:46 +00001061def convert_map_to_isolate_dict(values, config_variables):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001062 """Regenerates back a .isolate configuration dict from files and dirs
1063 mappings generated from reduce_inputs().
1064 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001065 # Gather a list of configurations for set inversion later.
1066 all_mentioned_configs = set()
1067 for configs_by_item in values.itervalues():
1068 for configs in configs_by_item.itervalues():
1069 all_mentioned_configs.update(configs)
1070
1071 # Invert the mapping to make it dict first.
1072 conditions = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001073 for key in values:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001074 for item, configs in values[key].iteritems():
1075 then = conditions.setdefault(frozenset(configs), {})
1076 variables = then.setdefault('variables', {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001077
benrg@chromium.org609b7982013-02-07 16:44:46 +00001078 if item in (True, False):
1079 # One-off for read_only.
1080 variables[key] = item
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001081 else:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001082 assert item
1083 if isinstance(item, tuple):
1084 # One-off for command.
1085 # Do not merge lists and do not sort!
1086 # Note that item is a tuple.
1087 assert key not in variables
1088 variables[key] = list(item)
1089 else:
1090 # The list of items (files or dirs). Append the new item and keep
1091 # the list sorted.
1092 l = variables.setdefault(key, [])
1093 l.append(item)
1094 l.sort()
1095
1096 if all_mentioned_configs:
1097 config_values = map(set, zip(*all_mentioned_configs))
1098 sef = short_expression_finder.ShortExpressionFinder(
1099 zip(config_variables, config_values))
1100
1101 conditions = sorted(
1102 [sef.get_expr(configs), then] for configs, then in conditions.iteritems())
1103 return {'conditions': conditions}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001104
1105
1106### Internal state files.
1107
1108
benrg@chromium.org609b7982013-02-07 16:44:46 +00001109class ConfigSettings(object):
1110 """Represents the dependency variables for a single build configuration.
1111 The structure is immutable.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001112 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001113 def __init__(self, config, values):
1114 self.config = config
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001115 verify_variables(values)
1116 self.touched = sorted(values.get(KEY_TOUCHED, []))
1117 self.tracked = sorted(values.get(KEY_TRACKED, []))
1118 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
1119 self.command = values.get('command', [])[:]
1120 self.read_only = values.get('read_only')
1121
1122 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001123 assert not (self.config and rhs.config) or (self.config == rhs.config)
maruel@chromium.org669edcb2012-11-02 19:16:14 +00001124 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001125 var = {
1126 KEY_TOUCHED: sorted(self.touched + rhs.touched),
1127 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
1128 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
1129 'command': self.command or rhs.command,
1130 'read_only': rhs.read_only if self.read_only is None else self.read_only,
1131 }
benrg@chromium.org609b7982013-02-07 16:44:46 +00001132 return ConfigSettings(self.config or rhs.config, var)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001133
1134 def flatten(self):
1135 out = {}
1136 if self.command:
1137 out['command'] = self.command
1138 if self.touched:
1139 out[KEY_TOUCHED] = self.touched
1140 if self.tracked:
1141 out[KEY_TRACKED] = self.tracked
1142 if self.untracked:
1143 out[KEY_UNTRACKED] = self.untracked
1144 if self.read_only is not None:
1145 out['read_only'] = self.read_only
1146 return out
1147
1148
1149class Configs(object):
1150 """Represents a processed .isolate file.
1151
benrg@chromium.org609b7982013-02-07 16:44:46 +00001152 Stores the file in a processed way, split by configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001153 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001154 def __init__(self, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001155 self.file_comment = file_comment
benrg@chromium.org609b7982013-02-07 16:44:46 +00001156 # The keys of by_config are tuples of values for the configuration
1157 # variables. The names of the variables (which must be the same for
1158 # every by_config key) are kept in config_variables. Initially by_config
1159 # is empty and we don't know what configuration variables will be used,
1160 # so config_variables also starts out empty. It will be set by the first
1161 # call to union() or merge_dependencies().
1162 self.by_config = {}
1163 self.config_variables = ()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001164
1165 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001166 """Adds variables from rhs (a Configs) to the existing variables.
1167 """
1168 config_variables = self.config_variables
1169 if not config_variables:
1170 config_variables = rhs.config_variables
1171 else:
1172 # We can't proceed if this isn't true since we don't know the correct
1173 # default values for extra variables. The variables are sorted so we
1174 # don't need to worry about permutations.
1175 if rhs.config_variables and rhs.config_variables != config_variables:
1176 raise ExecutionError(
1177 'Variables in merged .isolate files do not match: %r and %r' % (
1178 config_variables, rhs.config_variables))
1179
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001180 # Takes the first file comment, prefering lhs.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001181 out = Configs(self.file_comment or rhs.file_comment)
1182 out.config_variables = config_variables
1183 for config in set(self.by_config) | set(rhs.by_config):
1184 out.by_config[config] = union(
1185 self.by_config.get(config), rhs.by_config.get(config))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001186 return out
1187
benrg@chromium.org609b7982013-02-07 16:44:46 +00001188 def merge_dependencies(self, values, config_variables, configs):
1189 """Adds new dependencies to this object for the given configurations.
1190 Arguments:
1191 values: A variables dict as found in a .isolate file, e.g.,
1192 {KEY_TOUCHED: [...], 'command': ...}.
1193 config_variables: An ordered list of configuration variables, e.g.,
1194 ["OS", "chromeos"]. If this object already contains any dependencies,
1195 the configuration variables must match.
1196 configs: a list of tuples of values of the configuration variables,
1197 e.g., [("mac", 0), ("linux", 1)]. The dependencies in |values|
1198 are added to all of these configurations, and other configurations
1199 are unchanged.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001200 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001201 if not values:
1202 return
1203
1204 if not self.config_variables:
1205 self.config_variables = config_variables
1206 else:
1207 # See comment in Configs.union().
1208 assert self.config_variables == config_variables
1209
1210 for config in configs:
1211 self.by_config[config] = union(
1212 self.by_config.get(config), ConfigSettings(config, values))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001213
1214 def flatten(self):
1215 """Returns a flat dictionary representation of the configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001216 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001217 return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
1218
1219 def make_isolate_file(self):
1220 """Returns a dictionary suitable for writing to a .isolate file.
1221 """
1222 dependencies_by_config = self.flatten()
1223 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
1224 return convert_map_to_isolate_dict(configs_by_dependency,
1225 self.config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001226
1227
benrg@chromium.org609b7982013-02-07 16:44:46 +00001228# TODO(benrg): Remove this function when no old-format files are left.
1229def convert_old_to_new_format(value):
1230 """Converts from the old .isolate format, which only has one variable (OS),
1231 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
1232 and allows conditions that depend on the set of all OSes, to the new format,
1233 which allows any set of variables, has no hardcoded values, and only allows
1234 explicit positive tests of variable values.
1235 """
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001236 conditions = value.get('conditions', [])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001237 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
1238 return value # Nothing to change
1239
1240 def parse_condition(cond):
1241 return re.match(r'OS=="(\w+)"\Z', cond[0]).group(1)
1242
1243 oses = set(map(parse_condition, conditions))
1244 default_oses = set(['linux', 'mac', 'win'])
1245 oses = sorted(oses | default_oses)
1246
1247 def if_not_os(not_os, then):
1248 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
1249 return [expr, then]
1250
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001251 conditions = [
1252 cond[:2] for cond in conditions if cond[1]
1253 ] + [
1254 if_not_os(parse_condition(cond), cond[2])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001255 for cond in conditions if len(cond) == 3
1256 ]
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001257
benrg@chromium.org609b7982013-02-07 16:44:46 +00001258 if 'variables' in value:
1259 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
1260 conditions.sort()
1261
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001262 value = value.copy()
1263 value['conditions'] = conditions
benrg@chromium.org609b7982013-02-07 16:44:46 +00001264 return value
1265
1266
1267def load_isolate_as_config(isolate_dir, value, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001268 """Parses one .isolate file and returns a Configs() instance.
1269
1270 |value| is the loaded dictionary that was defined in the gyp file.
1271
1272 The expected format is strict, anything diverting from the format below will
1273 throw an assert:
1274 {
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001275 'includes': [
1276 'foo.isolate',
1277 ],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001278 'conditions': [
benrg@chromium.org609b7982013-02-07 16:44:46 +00001279 ['OS=="vms" and foo=42', {
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001280 'variables': {
benrg@chromium.org609b7982013-02-07 16:44:46 +00001281 'command': [
1282 ...
1283 ],
1284 'isolate_dependency_tracked': [
1285 ...
1286 ],
1287 'isolate_dependency_untracked': [
1288 ...
1289 ],
1290 'read_only': False,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001291 },
1292 }],
1293 ...
1294 ],
1295 }
1296 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001297 value = convert_old_to_new_format(value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001298
benrg@chromium.org609b7982013-02-07 16:44:46 +00001299 variables_and_values = {}
1300 verify_root(value, variables_and_values)
1301 if variables_and_values:
1302 config_variables, config_values = zip(
1303 *sorted(variables_and_values.iteritems()))
1304 all_configs = list(itertools.product(*config_values))
1305 else:
1306 config_variables = None
1307 all_configs = []
1308
1309 isolate = Configs(file_comment)
1310
1311 # Add configuration-specific variables.
1312 for expr, then in value.get('conditions', []):
1313 configs = match_configs(expr, config_variables, all_configs)
1314 isolate.merge_dependencies(then['variables'], config_variables, configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001315
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001316 # Load the includes.
1317 for include in value.get('includes', []):
1318 if os.path.isabs(include):
1319 raise ExecutionError(
1320 'Failed to load configuration; absolute include path \'%s\'' %
1321 include)
1322 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
1323 with open(included_isolate, 'r') as f:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001324 included_isolate = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001325 os.path.dirname(included_isolate),
1326 eval_content(f.read()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001327 None)
1328 isolate = union(isolate, included_isolate)
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001329
benrg@chromium.org609b7982013-02-07 16:44:46 +00001330 return isolate
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001331
1332
benrg@chromium.org609b7982013-02-07 16:44:46 +00001333def load_isolate_for_config(isolate_dir, content, variables):
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001334 """Loads the .isolate file and returns the information unprocessed but
1335 filtered for the specific OS.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001336
1337 Returns the command, dependencies and read_only flag. The dependencies are
1338 fixed to use os.path.sep.
1339 """
1340 # Load the .isolate file, process its conditions, retrieve the command and
1341 # dependencies.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001342 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
1343 try:
1344 config = tuple(variables[var] for var in isolate.config_variables)
1345 except KeyError:
1346 raise ExecutionError(
1347 'These configuration variables were missing from the command line: %s' %
1348 ', '.join(sorted(set(isolate.config_variables) - set(variables))))
1349 config = isolate.by_config.get(config)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001350 if not config:
csharp@chromium.org2a3d7d52013-03-23 12:54:37 +00001351 raise ExecutionError('Failed to load configuration for (%s)' %
1352 ', '.join(isolate.config_variables))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001353 # Merge tracked and untracked variables, isolate.py doesn't care about the
1354 # trackability of the variables, only the build tool does.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001355 dependencies = [
1356 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1357 ]
1358 touched = [f.replace('/', os.path.sep) for f in config.touched]
1359 return config.command, dependencies, touched, config.read_only
1360
1361
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001362def chromium_save_isolated(isolated, data, variables):
1363 """Writes one or many .isolated files.
1364
1365 This slightly increases the cold cache cost but greatly reduce the warm cache
1366 cost by splitting low-churn files off the master .isolated file. It also
1367 reduces overall isolateserver memcache consumption.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001368
1369 Returns the list of child isolated files that are included by |isolated|.
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001370 """
1371 slaves = []
1372
1373 def extract_into_included_isolated(prefix):
1374 new_slave = {'files': {}, 'os': data['os']}
1375 for f in data['files'].keys():
1376 if f.startswith(prefix):
1377 new_slave['files'][f] = data['files'].pop(f)
1378 if new_slave['files']:
1379 slaves.append(new_slave)
1380
1381 # Split test/data/ in its own .isolated file.
1382 extract_into_included_isolated(os.path.join('test', 'data', ''))
1383
1384 # Split everything out of PRODUCT_DIR in its own .isolated file.
1385 if variables.get('PRODUCT_DIR'):
1386 extract_into_included_isolated(variables['PRODUCT_DIR'])
1387
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001388 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001389 for index, f in enumerate(slaves):
1390 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
1391 trace_inputs.write_json(slavepath, f, True)
1392 data.setdefault('includes', []).append(
1393 isolateserver_archive.sha1_file(slavepath))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001394 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001395
1396 trace_inputs.write_json(isolated, data, True)
1397 return files
1398
1399
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001400class Flattenable(object):
1401 """Represents data that can be represented as a json file."""
1402 MEMBERS = ()
1403
1404 def flatten(self):
1405 """Returns a json-serializable version of itself.
1406
1407 Skips None entries.
1408 """
1409 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1410 return dict((member, value) for member, value in items if value is not None)
1411
1412 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001413 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001414 """Loads a flattened version."""
1415 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001416 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001417 for member in out.MEMBERS:
1418 if member in data:
1419 # Access to a protected member XXX of a client class
1420 # pylint: disable=W0212
1421 out._load_member(member, data.pop(member))
1422 if data:
1423 raise ValueError(
1424 'Found unexpected entry %s while constructing an object %s' %
1425 (data, cls.__name__), data, cls.__name__)
1426 return out
1427
1428 def _load_member(self, member, value):
1429 """Loads a member into self."""
1430 setattr(self, member, value)
1431
1432 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001433 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001434 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001435 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001436 out = cls.load(trace_inputs.read_json(filename), *args, **kwargs)
1437 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001438 except (IOError, ValueError):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001439 # On failure, loads the default instance.
1440 out = cls(*args, **kwargs)
1441 logging.warn('Failed to load %s', filename)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001442 return out
1443
1444
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001445class SavedState(Flattenable):
1446 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001447
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001448 This file caches the items calculated by this script and is used to increase
1449 the performance of the script. This file is not loaded by run_isolated.py.
1450 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001451
1452 It is important to note that the 'files' dict keys are using native OS path
1453 separator instead of '/' used in .isolate file.
1454 """
1455 MEMBERS = (
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001456 # Cache of the processed command. This value is saved because .isolated
1457 # files are never loaded by isolate.py so it's the only way to load the
1458 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001459 'command',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001460 # Cache of the files found so the next run can skip sha1 calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001461 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001462 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001463 'isolate_file',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001464 # List of included .isolated files. Used to support/remember 'slave'
1465 # .isolated files. Relative path to isolated_basedir.
1466 'child_isolated_files',
1467 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001468 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001469 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001470 'relative_cwd',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001471 # GYP variables used to generate the .isolated file. Variables are saved so
1472 # a user can use isolate.py after building and the GYP variables are still
1473 # defined.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001474 'variables',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001475 )
1476
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001477 def __init__(self, isolated_basedir):
1478 """Creates an empty SavedState.
1479
1480 |isolated_basedir| is the directory where the .isolated and .isolated.state
1481 files are saved.
1482 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001483 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001484 assert os.path.isabs(isolated_basedir), isolated_basedir
1485 assert os.path.isdir(isolated_basedir), isolated_basedir
1486 self.isolated_basedir = isolated_basedir
1487
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001488 self.command = []
1489 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001490 self.isolate_file = None
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001491 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001492 self.read_only = None
1493 self.relative_cwd = None
benrg@chromium.org609b7982013-02-07 16:44:46 +00001494 self.variables = {'OS': get_flavor()}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001495
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001496 def update(self, isolate_file, variables):
1497 """Updates the saved state with new data to keep GYP variables and internal
1498 reference to the original .isolate file.
1499 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +00001500 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001501 # Convert back to a relative path. On Windows, if the isolate and
1502 # isolated files are on different drives, isolate_file will stay an absolute
1503 # path.
1504 isolate_file = safe_relpath(isolate_file, self.isolated_basedir)
1505
1506 # The same .isolate file should always be used to generate the .isolated and
1507 # .isolated.state.
1508 assert isolate_file == self.isolate_file or not self.isolate_file, (
1509 isolate_file, self.isolate_file)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001510 self.isolate_file = isolate_file
1511 self.variables.update(variables)
1512
1513 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
1514 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001515
1516 The new files in |infiles| are added to self.files dict but their sha1 is
1517 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001518 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001519 self.command = command
1520 # Add new files.
1521 for f in infiles:
1522 self.files.setdefault(f, {})
1523 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001524 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001525 # Prune extraneous files that are not a dependency anymore.
1526 for f in set(self.files).difference(set(infiles).union(touched)):
1527 del self.files[f]
1528 if read_only is not None:
1529 self.read_only = read_only
1530 self.relative_cwd = relative_cwd
1531
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001532 def to_isolated(self):
1533 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001534
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001535 http://chromium.org/developers/testing/isolated-testing/design
1536 """
1537 def strip(data):
1538 """Returns a 'files' entry with only the whitelisted keys."""
1539 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
1540
1541 out = {
1542 'files': dict(
1543 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001544 'os': self.variables['OS'],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001545 }
1546 if self.command:
1547 out['command'] = self.command
1548 if self.read_only is not None:
1549 out['read_only'] = self.read_only
1550 if self.relative_cwd:
1551 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001552 return out
1553
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001554 @property
1555 def isolate_filepath(self):
1556 """Returns the absolute path of self.isolate_file."""
1557 return os.path.normpath(
1558 os.path.join(self.isolated_basedir, self.isolate_file))
1559
1560 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001561 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001562 def load(cls, data, isolated_basedir): # pylint: disable=W0221
1563 """Special case loading to disallow different OS.
1564
1565 It is not possible to load a .isolated.state files from a different OS, this
1566 file is saved in OS-specific format.
1567 """
1568 out = super(SavedState, cls).load(data, isolated_basedir)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001569 if 'os' in data:
1570 out.variables['OS'] = data['os']
1571 if out.variables['OS'] != get_flavor():
1572 raise run_isolated.ConfigError(
1573 'The .isolated.state file was created on another platform')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001574 # The .isolate file must be valid. It could be absolute on Windows if the
1575 # drive containing the .isolate and the drive containing the .isolated files
1576 # differ.
1577 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
1578 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001579 return out
1580
1581 def __str__(self):
1582 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001583 out += ' command: %s\n' % self.command
1584 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001585 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001586 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +00001587 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001588 out += ' child_isolated_files: %s\n' % self.child_isolated_files
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001589 out += ' variables: %s' % ''.join(
1590 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1591 out += ')'
1592 return out
1593
1594
1595class CompleteState(object):
1596 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001597 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001598 super(CompleteState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001599 assert os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001600 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001601 # Contains the data to ease developer's use-case but that is not strictly
1602 # necessary.
1603 self.saved_state = saved_state
1604
1605 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001606 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001607 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001608 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001609 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001610 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001611 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001612 SavedState.load_file(
1613 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001614
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001615 def load_isolate(self, cwd, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001616 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001617 .isolate file.
1618
1619 Processes the loaded data, deduce root_dir, relative_cwd.
1620 """
1621 # Make sure to not depend on os.getcwd().
1622 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.orga3da9122013-03-28 13:27:09 +00001623 isolate_file = trace_inputs.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001624 logging.info(
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001625 'CompleteState.load_isolate(%s, %s, %s, %s)',
1626 cwd, isolate_file, variables, ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001627 relative_base_dir = os.path.dirname(isolate_file)
1628
1629 # Processes the variables and update the saved state.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001630 variables = process_variables(cwd, variables, relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001631 self.saved_state.update(isolate_file, variables)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001632 variables = self.saved_state.variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001633
1634 with open(isolate_file, 'r') as f:
1635 # At that point, variables are not replaced yet in command and infiles.
1636 # infiles may contain directory entries and is in posix style.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001637 command, infiles, touched, read_only = load_isolate_for_config(
1638 os.path.dirname(isolate_file), f.read(), variables)
1639 command = [eval_variables(i, variables) for i in command]
1640 infiles = [eval_variables(f, variables) for f in infiles]
1641 touched = [eval_variables(f, variables) for f in touched]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001642 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +00001643 # form '../../foo/bar'. Note that path variables must be taken in account
1644 # too, add them as if they were input files.
1645 path_variables = [variables[v] for v in PATH_VARIABLES if v in variables]
1646 root_dir = determine_root_dir(
1647 relative_base_dir, infiles + touched + path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001648 # The relative directory is automatically determined by the relative path
1649 # between root_dir and the directory containing the .isolate file,
1650 # isolate_base_dir.
1651 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
benrg@chromium.org9ae72862013-02-11 05:05:51 +00001652 # Now that we know where the root is, check that the PATH_VARIABLES point
1653 # inside it.
1654 for i in PATH_VARIABLES:
1655 if i in variables:
1656 if not path_starts_with(
1657 root_dir, os.path.join(relative_base_dir, variables[i])):
1658 raise run_isolated.MappingError(
maruel@chromium.org75584e22013-06-20 01:40:24 +00001659 'Path variable %s=%r points outside the inferred root directory'
1660 ' %s' % (i, variables[i], root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001661 # Normalize the files based to root_dir. It is important to keep the
1662 # trailing os.path.sep at that step.
1663 infiles = [
1664 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1665 for f in infiles
1666 ]
1667 touched = [
1668 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1669 for f in touched
1670 ]
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001671 follow_symlinks = variables['OS'] != 'win'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001672 # Expand the directories by listing each file inside. Up to now, trailing
1673 # os.path.sep must be kept. Do not expand 'touched'.
1674 infiles = expand_directories_and_symlinks(
1675 root_dir,
1676 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001677 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001678 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +00001679 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001680
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001681 # If we ignore broken items then remove any missing touched items.
1682 if ignore_broken_items:
1683 original_touched_count = len(touched)
1684 touched = [touch for touch in touched if os.path.exists(touch)]
1685
1686 if len(touched) != original_touched_count:
1687 logging.info('warning: removed %d invalid touched entries',
1688 len(touched) - original_touched_count)
1689
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001690 # Finally, update the new data to be able to generate the foo.isolated file,
1691 # the file that is used by run_isolated.py.
1692 self.saved_state.update_isolated(
1693 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001694 logging.debug(self)
1695
maruel@chromium.org9268f042012-10-17 17:36:41 +00001696 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001697 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001698
maruel@chromium.org9268f042012-10-17 17:36:41 +00001699 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1700 file is tainted.
1701
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001702 See process_input() for more information.
1703 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001704 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001705 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001706 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001707 else:
1708 filepath = os.path.join(self.root_dir, infile)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001709 self.saved_state.files[infile] = process_input(
1710 filepath,
1711 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +00001712 self.saved_state.read_only,
1713 self.saved_state.variables['OS'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001714
1715 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001716 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001717 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001718 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001719 self.isolated_filepath,
1720 self.saved_state.to_isolated(),
1721 self.saved_state.variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001722 total_bytes = sum(
1723 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001724 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001725 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001726 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001727 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001728 logging.debug('Dumping to %s' % saved_state_file)
1729 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1730
1731 @property
1732 def root_dir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001733 """Returns the absolute path of the root_dir to reference the .isolate file
1734 via relative_cwd.
1735
1736 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
1737 to isolate_filepath.
1738 """
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001739 if not self.saved_state.isolate_file:
1740 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001741 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001742 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001743 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001744 root_dir = isolate_dir
1745 else:
1746 assert isolate_dir.endswith(self.saved_state.relative_cwd), (
1747 isolate_dir, self.saved_state.relative_cwd)
1748 # Walk back back to the root directory.
1749 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
1750 return trace_inputs.get_native_path_case(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001751
1752 @property
1753 def resultdir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001754 """Returns the absolute path containing the .isolated file.
1755
1756 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
1757 path as the value.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001758 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001759 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001760
1761 def __str__(self):
1762 def indent(data, indent_length):
1763 """Indents text."""
1764 spacing = ' ' * indent_length
1765 return ''.join(spacing + l for l in str(data).splitlines(True))
1766
1767 out = '%s(\n' % self.__class__.__name__
1768 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001769 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1770 return out
1771
1772
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001773def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001774 """Loads a CompleteState.
1775
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001776 This includes data from .isolate and .isolated.state files. Never reads the
1777 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001778
1779 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001780 options: Options instance generated with OptionParserIsolate. For either
1781 options.isolate and options.isolated, if the value is set, it is an
1782 absolute path.
1783 cwd: base directory to be used when loading the .isolate file.
1784 subdir: optional argument to only process file in the subdirectory, relative
1785 to CompleteState.root_dir.
1786 skip_update: Skip trying to load the .isolate file and processing the
1787 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001788 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001789 assert not options.isolate or os.path.isabs(options.isolate)
1790 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.orga3da9122013-03-28 13:27:09 +00001791 cwd = trace_inputs.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001792 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001793 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001794 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001795 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001796 else:
1797 # Constructs a dummy object that cannot be saved. Useful for temporary
1798 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001799 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001800
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001801 if not options.isolate:
1802 if not complete_state.saved_state.isolate_file:
1803 if not skip_update:
1804 raise ExecutionError('A .isolate file is required.')
1805 isolate = None
1806 else:
1807 isolate = complete_state.saved_state.isolate_filepath
1808 else:
1809 isolate = options.isolate
1810 if complete_state.saved_state.isolate_file:
1811 rel_isolate = safe_relpath(
1812 options.isolate, complete_state.saved_state.isolated_basedir)
1813 if rel_isolate != complete_state.saved_state.isolate_file:
1814 raise ExecutionError(
1815 '%s and %s do not match.' % (
1816 options.isolate, complete_state.saved_state.isolate_file))
1817
1818 if not skip_update:
1819 # Then load the .isolate and expands directories.
1820 complete_state.load_isolate(
1821 cwd, isolate, options.variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001822
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001823 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001824 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001825 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001826 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1827 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001828
1829 if not skip_update:
1830 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001831 return complete_state
1832
1833
1834def read_trace_as_isolate_dict(complete_state):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001835 """Reads a trace and returns the .isolate dictionary.
1836
1837 Returns exceptions during the log parsing so it can be re-raised.
1838 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001839 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001840 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001841 if not os.path.isfile(logfile):
1842 raise ExecutionError(
1843 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1844 try:
maruel@chromium.orgd2627672013-04-03 15:30:24 +00001845 data = api.parse_log(logfile, chromium_default_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001846 exceptions = [i['exception'] for i in data if 'exception' in i]
1847 results = (i['results'] for i in data if 'results' in i)
1848 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1849 files = set(sum((result.existent for result in results_stripped), []))
1850 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001851 value = generate_isolate(
1852 tracked,
1853 [],
1854 touched,
1855 complete_state.root_dir,
1856 complete_state.saved_state.variables,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001857 complete_state.saved_state.relative_cwd)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001858 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001859 except trace_inputs.TracingFailure, e:
1860 raise ExecutionError(
1861 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001862 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001863
1864
1865def print_all(comment, data, stream):
1866 """Prints a complete .isolate file and its top-level file comment into a
1867 stream.
1868 """
1869 if comment:
1870 stream.write(comment)
1871 pretty_print(data, stream)
1872
1873
1874def merge(complete_state):
1875 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001876 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001877
1878 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001879 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001880 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001881 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001882 prev_config = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001883 isolate_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001884 eval_content(prev_content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001885 extract_comment(prev_content))
1886 new_config = load_isolate_as_config(isolate_dir, value, '')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001887 config = union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001888 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001889 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001890 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001891 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001892 if exceptions:
1893 # It got an exception, raise the first one.
1894 raise \
1895 exceptions[0][0], \
1896 exceptions[0][1], \
1897 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001898
1899
1900def CMDcheck(args):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001901 """Checks that all the inputs are present and generates .isolated."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001902 parser = OptionParserIsolate(command='check')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001903 parser.add_option('--subdir', help='Filters to a subdirectory')
1904 options, args = parser.parse_args(args)
1905 if args:
1906 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001907 complete_state = load_complete_state(
1908 options, os.getcwd(), options.subdir, False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001909
1910 # Nothing is done specifically. Just store the result and state.
1911 complete_state.save_files()
1912 return 0
1913
1914
1915def CMDhashtable(args):
1916 """Creates a hash table content addressed object store.
1917
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001918 All the files listed in the .isolated file are put in the output directory
1919 with the file name being the sha-1 of the file's content.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001920 """
1921 parser = OptionParserIsolate(command='hashtable')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001922 parser.add_option('--subdir', help='Filters to a subdirectory')
1923 options, args = parser.parse_args(args)
1924 if args:
1925 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001926
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001927 with run_isolated.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001928 success = False
1929 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001930 complete_state = load_complete_state(
1931 options, os.getcwd(), options.subdir, False)
1932 if not options.outdir:
1933 options.outdir = os.path.join(
1934 os.path.dirname(complete_state.isolated_filepath), 'hashtable')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001935 # Make sure that complete_state isn't modified until save_files() is
1936 # called, because any changes made to it here will propagate to the files
1937 # created (which is probably not intended).
1938 complete_state.save_files()
1939
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001940 infiles = complete_state.saved_state.files
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001941 # Add all the .isolated files.
maruel@chromium.org87f11962013-04-10 21:27:28 +00001942 isolated_hash = []
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001943 isolated_files = [
1944 options.isolated,
1945 ] + complete_state.saved_state.child_isolated_files
1946 for item in isolated_files:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001947 item_path = os.path.join(
1948 os.path.dirname(complete_state.isolated_filepath), item)
maruel@chromium.org87f11962013-04-10 21:27:28 +00001949 # Do not use isolateserver_archive.sha1_file() here because the file is
1950 # likely smallish (under 500kb) and its file size is needed.
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001951 with open(item_path, 'rb') as f:
1952 content = f.read()
maruel@chromium.org87f11962013-04-10 21:27:28 +00001953 isolated_hash.append(hashlib.sha1(content).hexdigest())
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001954 isolated_metadata = {
maruel@chromium.org87f11962013-04-10 21:27:28 +00001955 'h': isolated_hash[-1],
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001956 's': len(content),
1957 'priority': '0'
1958 }
1959 infiles[item_path] = isolated_metadata
1960
1961 logging.info('Creating content addressed object store with %d item',
1962 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001963
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001964 if is_url(options.outdir):
maruel@chromium.orgc6f90062012-11-07 18:32:22 +00001965 isolateserver_archive.upload_sha1_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001966 base_url=options.outdir,
1967 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00001968 infiles=infiles,
1969 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001970 else:
1971 recreate_tree(
1972 outdir=options.outdir,
1973 indir=complete_state.root_dir,
1974 infiles=infiles,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001975 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001976 as_sha1=True)
1977 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001978 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001979 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001980 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001981 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001982 if not success and os.path.isfile(options.isolated):
1983 os.remove(options.isolated)
maruel@chromium.org87f11962013-04-10 21:27:28 +00001984 return not success
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001985
1986
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001987def CMDmerge(args):
1988 """Reads and merges the data from the trace back into the original .isolate.
1989
1990 Ignores --outdir.
1991 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001992 parser = OptionParserIsolate(command='merge', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001993 options, args = parser.parse_args(args)
1994 if args:
1995 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001996 complete_state = load_complete_state(options, os.getcwd(), None, False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001997 merge(complete_state)
1998 return 0
1999
2000
2001def CMDread(args):
2002 """Reads the trace file generated with command 'trace'.
2003
2004 Ignores --outdir.
2005 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002006 parser = OptionParserIsolate(command='read', require_isolated=False)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002007 parser.add_option(
2008 '--skip-refresh', action='store_true',
2009 help='Skip reading .isolate file and do not refresh the sha1 of '
2010 'dependencies')
maruel@chromium.org9268f042012-10-17 17:36:41 +00002011 options, args = parser.parse_args(args)
2012 if args:
2013 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002014 complete_state = load_complete_state(
2015 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00002016 value, exceptions = read_trace_as_isolate_dict(complete_state)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002017 pretty_print(value, sys.stdout)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00002018 if exceptions:
2019 # It got an exception, raise the first one.
2020 raise \
2021 exceptions[0][0], \
2022 exceptions[0][1], \
2023 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002024 return 0
2025
2026
2027def CMDremap(args):
2028 """Creates a directory with all the dependencies mapped into it.
2029
2030 Useful to test manually why a test is failing. The target executable is not
2031 run.
2032 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002033 parser = OptionParserIsolate(command='remap', require_isolated=False)
maruel@chromium.org9268f042012-10-17 17:36:41 +00002034 options, args = parser.parse_args(args)
2035 if args:
2036 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002037 complete_state = load_complete_state(options, os.getcwd(), None, False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002038
2039 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002040 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002041 'isolate', complete_state.root_dir)
2042 else:
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002043 if is_url(options.outdir):
2044 raise ExecutionError('Can\'t use url for --outdir with mode remap')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002045 if not os.path.isdir(options.outdir):
2046 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00002047 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002048 if len(os.listdir(options.outdir)):
2049 raise ExecutionError('Can\'t remap in a non-empty directory')
2050 recreate_tree(
2051 outdir=options.outdir,
2052 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002053 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002054 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002055 as_sha1=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002056 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002057 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002058
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002059 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002060 complete_state.save_files()
2061 return 0
2062
2063
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002064def CMDrewrite(args):
2065 """Rewrites a .isolate file into the canonical format."""
2066 parser = OptionParserIsolate(command='rewrite', require_isolated=False)
2067 options, args = parser.parse_args(args)
2068 if args:
2069 parser.error('Unsupported argument: %s' % args)
2070
2071 if options.isolated:
2072 # Load the previous state if it was present. Namely, "foo.isolated.state".
2073 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002074 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002075 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002076 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002077 if not isolate:
2078 raise ExecutionError('A .isolate file is required.')
2079 with open(isolate, 'r') as f:
2080 content = f.read()
2081 config = load_isolate_as_config(
2082 os.path.dirname(os.path.abspath(isolate)),
2083 eval_content(content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00002084 extract_comment(content))
2085 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002086 print('Updating %s' % isolate)
2087 with open(isolate, 'wb') as f:
2088 print_all(config.file_comment, data, f)
2089 return 0
2090
2091
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002092def CMDrun(args):
2093 """Runs the test executable in an isolated (temporary) directory.
2094
2095 All the dependencies are mapped into the temporary directory and the
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002096 directory is cleaned up after the target exits. Warning: if --outdir is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002097 specified, it is deleted upon exit.
2098
2099 Argument processing stops at the first non-recognized argument and these
2100 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002101 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002102 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002103 parser = OptionParserIsolate(command='run', require_isolated=False)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002104 parser.add_option(
2105 '--skip-refresh', action='store_true',
2106 help='Skip reading .isolate file and do not refresh the sha1 of '
2107 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002108 parser.enable_interspersed_args()
2109 options, args = parser.parse_args(args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002110 complete_state = load_complete_state(
2111 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002112 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002113 if not cmd:
2114 raise ExecutionError('No command to run')
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002115 if options.outdir and is_url(options.outdir):
2116 raise ExecutionError('Can\'t use url for --outdir with mode run')
2117
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002118 cmd = trace_inputs.fix_python_path(cmd)
2119 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002120 root_dir = complete_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002121 if not options.outdir:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002122 if not os.path.isabs(root_dir):
2123 root_dir = os.path.join(os.path.dirname(options.isolated), root_dir)
2124 options.outdir = run_isolated.make_temp_dir('isolate', root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002125 else:
2126 if not os.path.isdir(options.outdir):
2127 os.makedirs(options.outdir)
2128 recreate_tree(
2129 outdir=options.outdir,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002130 indir=root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002131 infiles=complete_state.saved_state.files,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002132 action=run_isolated.HARDLINK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002133 as_sha1=False)
2134 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002135 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002136 if not os.path.isdir(cwd):
2137 # It can happen when no files are mapped from the directory containing the
2138 # .isolate file. But the directory must exist to be the current working
2139 # directory.
2140 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002141 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002142 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002143 logging.info('Running %s, cwd=%s' % (cmd, cwd))
2144 result = subprocess.call(cmd, cwd=cwd)
2145 finally:
2146 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002147 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002148
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002149 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002150 complete_state.save_files()
2151 return result
2152
2153
2154def CMDtrace(args):
2155 """Traces the target using trace_inputs.py.
2156
2157 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002158 its child processes access. Then the 'merge' command can be used to generate
2159 an updated .isolate file out of it or the 'read' command to print it out to
2160 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002161
2162 Argument processing stops at the first non-recognized argument and these
2163 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002164 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002165 """
2166 parser = OptionParserIsolate(command='trace')
2167 parser.enable_interspersed_args()
2168 parser.add_option(
2169 '-m', '--merge', action='store_true',
2170 help='After tracing, merge the results back in the .isolate file')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002171 parser.add_option(
2172 '--skip-refresh', action='store_true',
2173 help='Skip reading .isolate file and do not refresh the sha1 of '
2174 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002175 options, args = parser.parse_args(args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002176 complete_state = load_complete_state(
2177 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002178 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002179 if not cmd:
2180 raise ExecutionError('No command to run')
2181 cmd = trace_inputs.fix_python_path(cmd)
2182 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002183 unicode(complete_state.root_dir),
2184 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00002185 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
2186 if not os.path.isfile(cmd[0]):
2187 raise ExecutionError(
2188 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002189 logging.info('Running %s, cwd=%s' % (cmd, cwd))
2190 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002191 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002192 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002193 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002194 try:
2195 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002196 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002197 cmd,
2198 cwd,
2199 'default',
2200 True)
2201 except trace_inputs.TracingFailure, e:
2202 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
2203
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002204 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002205 logging.error(
2206 'Tracer exited with %d, which means the tests probably failed so the '
2207 'trace is probably incomplete.', result)
2208 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002209
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002210 complete_state.save_files()
2211
2212 if options.merge:
2213 merge(complete_state)
2214
2215 return result
2216
2217
maruel@chromium.org712454d2013-04-04 17:52:34 +00002218def _process_variable_arg(_option, _opt, _value, parser):
2219 if not parser.rargs:
2220 raise optparse.OptionValueError(
2221 'Please use --variable FOO=BAR or --variable FOO BAR')
2222 k = parser.rargs.pop(0)
2223 if '=' in k:
2224 parser.values.variables.append(tuple(k.split('=', 1)))
2225 else:
2226 if not parser.rargs:
2227 raise optparse.OptionValueError(
2228 'Please use --variable FOO=BAR or --variable FOO BAR')
2229 v = parser.rargs.pop(0)
2230 parser.values.variables.append((k, v))
2231
2232
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002233def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002234 """Adds --isolated and --variable to an OptionParser."""
2235 parser.add_option(
2236 '-s', '--isolated',
2237 metavar='FILE',
2238 help='.isolated file to generate or read')
2239 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002240 parser.add_option(
2241 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002242 dest='isolated',
2243 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002244 default_variables = [('OS', get_flavor())]
2245 if sys.platform in ('win32', 'cygwin'):
2246 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
2247 else:
2248 default_variables.append(('EXECUTABLE_SUFFIX', ''))
2249 parser.add_option(
2250 '-V', '--variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00002251 action='callback',
2252 callback=_process_variable_arg,
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002253 default=default_variables,
2254 dest='variables',
2255 metavar='FOO BAR',
2256 help='Variables to process in the .isolate file, default: %default. '
2257 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002258 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002259
2260
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002261def parse_isolated_option(parser, options, cwd, require_isolated):
2262 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002263 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002264 options.isolated = os.path.normpath(
2265 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002266 if require_isolated and not options.isolated:
csharp@chromium.org707f0452012-11-26 21:50:40 +00002267 parser.error('--isolated is required. Visit http://chromium.org/developers/'
2268 'testing/isolated-testing#TOC-Where-can-I-find-the-.isolated-'
2269 'file- to see how to create the .isolated file.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002270 if options.isolated and not options.isolated.endswith('.isolated'):
2271 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002272
2273
2274def parse_variable_option(options):
2275 """Processes --variable."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002276 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
2277 # but it wouldn't be backward compatible.
2278 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002279 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002280 try:
2281 return int(s)
2282 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002283 return s.decode('utf-8')
benrg@chromium.org609b7982013-02-07 16:44:46 +00002284 options.variables = dict((k, try_make_int(v)) for k, v in options.variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002285
2286
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002287class OptionParserIsolate(trace_inputs.OptionParserWithNiceDescription):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002288 """Adds automatic --isolate, --isolated, --out and --variable handling."""
2289 def __init__(self, require_isolated=True, **kwargs):
maruel@chromium.org55276902012-10-05 20:56:19 +00002290 trace_inputs.OptionParserWithNiceDescription.__init__(
2291 self,
2292 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
2293 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002294 group = optparse.OptionGroup(self, "Common options")
2295 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002296 '-i', '--isolate',
2297 metavar='FILE',
2298 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002299 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002300 group.add_option(
2301 '-o', '--outdir', metavar='DIR',
2302 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002303 'Defaults: run|remap: a /tmp subdirectory, others: '
2304 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00002305 group.add_option(
2306 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002307 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
2308 help='Indicates that invalid entries in the isolated file to be '
2309 'only be logged and not stop processing. Defaults to True if '
2310 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002311 self.add_option_group(group)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002312 self.require_isolated = require_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002313
2314 def parse_args(self, *args, **kwargs):
2315 """Makes sure the paths make sense.
2316
2317 On Windows, / and \ are often mixed together in a path.
2318 """
2319 options, args = trace_inputs.OptionParserWithNiceDescription.parse_args(
2320 self, *args, **kwargs)
2321 if not self.allow_interspersed_args and args:
2322 self.error('Unsupported argument: %s' % args)
2323
maruel@chromium.orga3da9122013-03-28 13:27:09 +00002324 cwd = trace_inputs.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002325 parse_isolated_option(self, options, cwd, self.require_isolated)
2326 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002327
2328 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002329 # TODO(maruel): Work with non-ASCII.
2330 # The path must be in native path case for tracing purposes.
2331 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
2332 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
2333 options.isolate = trace_inputs.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002334
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002335 if options.outdir and not is_url(options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002336 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
2337 # outdir doesn't need native path case since tracing is never done from
2338 # there.
2339 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002340
2341 return options, args
2342
2343
2344### Glue code to make all the commands works magically.
2345
2346
2347CMDhelp = trace_inputs.CMDhelp
2348
2349
2350def main(argv):
2351 try:
2352 return trace_inputs.main_impl(argv)
2353 except (
2354 ExecutionError,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002355 run_isolated.MappingError,
2356 run_isolated.ConfigError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002357 sys.stderr.write('\nError: ')
2358 sys.stderr.write(str(e))
2359 sys.stderr.write('\n')
2360 return 1
2361
2362
2363if __name__ == '__main__':
2364 sys.exit(main(sys.argv[1:]))