blob: 5846a0a74619a9b5baba57d9caea88e9fdf70e5e [file] [log] [blame]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
maruel@chromium.orge5322512013-08-19 20:17:57 +00006"""Front end tool to operate on .isolate files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00007
maruel@chromium.orge5322512013-08-19 20:17:57 +00008This includes creating, merging or compiling them to generate a .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00009
10See more information at
maruel@chromium.orge5322512013-08-19 20:17:57 +000011 https://code.google.com/p/swarming/wiki/IsolateDesign
12 https://code.google.com/p/swarming/wiki/IsolateUserGuide
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000013"""
maruel@chromium.orge5322512013-08-19 20:17:57 +000014# Run ./isolate.py --help for more detailed information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000015
benrg@chromium.org609b7982013-02-07 16:44:46 +000016import ast
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000017import copy
18import hashlib
benrg@chromium.org609b7982013-02-07 16:44:46 +000019import itertools
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000020import logging
21import optparse
22import os
23import posixpath
24import re
25import stat
26import subprocess
27import sys
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000028
maruel@chromium.orgc6f90062012-11-07 18:32:22 +000029import isolateserver_archive
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000030import run_isolated
benrg@chromium.org609b7982013-02-07 16:44:46 +000031import short_expression_finder
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000032import trace_inputs
33
34# Import here directly so isolate is easier to use as a library.
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000035from run_isolated import get_flavor
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000036
maruel@chromium.orge5322512013-08-19 20:17:57 +000037from third_party import colorama
38from third_party.depot_tools import fix_encoding
39from third_party.depot_tools import subcommand
40
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000041
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000042PATH_VARIABLES = ('DEPTH', 'PRODUCT_DIR')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000043
44# Files that should be 0-length when mapped.
45KEY_TOUCHED = 'isolate_dependency_touched'
46# Files that should be tracked by the build tool.
47KEY_TRACKED = 'isolate_dependency_tracked'
48# Files that should not be tracked by the build tool.
49KEY_UNTRACKED = 'isolate_dependency_untracked'
50
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000051
52class ExecutionError(Exception):
53 """A generic error occurred."""
54 def __str__(self):
55 return self.args[0]
56
57
58### Path handling code.
59
60
maruel@chromium.org3683afe2013-07-27 00:09:27 +000061DEFAULT_BLACKLIST = (
62 # Temporary vim or python files.
63 r'^.+\.(?:pyc|swp)$',
64 # .git or .svn directory.
65 r'^(?:.+' + re.escape(os.path.sep) + r'|)\.(?:git|svn)$',
66)
67
68
69# Chromium-specific.
70DEFAULT_BLACKLIST += (
71 r'^.+\.(?:run_test_cases)$',
72 r'^(?:.+' + re.escape(os.path.sep) + r'|)testserver\.log$',
73)
74
75
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000076def relpath(path, root):
77 """os.path.relpath() that keeps trailing os.path.sep."""
78 out = os.path.relpath(path, root)
79 if path.endswith(os.path.sep):
80 out += os.path.sep
81 return out
82
83
maruel@chromium.org8abec8b2013-04-16 19:34:20 +000084def safe_relpath(filepath, basepath):
85 """Do not throw on Windows when filepath and basepath are on different drives.
86
87 Different than relpath() above since this one doesn't keep the trailing
88 os.path.sep and it swallows exceptions on Windows and return the original
89 absolute path in the case of different drives.
90 """
91 try:
92 return os.path.relpath(filepath, basepath)
93 except ValueError:
94 assert sys.platform == 'win32'
95 return filepath
96
97
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000098def normpath(path):
99 """os.path.normpath() that keeps trailing os.path.sep."""
100 out = os.path.normpath(path)
101 if path.endswith(os.path.sep):
102 out += os.path.sep
103 return out
104
105
106def posix_relpath(path, root):
107 """posix.relpath() that keeps trailing slash."""
108 out = posixpath.relpath(path, root)
109 if path.endswith('/'):
110 out += '/'
111 return out
112
113
114def cleanup_path(x):
115 """Cleans up a relative path. Converts any os.path.sep to '/' on Windows."""
116 if x:
117 x = x.rstrip(os.path.sep).replace(os.path.sep, '/')
118 if x == '.':
119 x = ''
120 if x:
121 x += '/'
122 return x
123
124
maruel@chromium.orgb9520b02013-03-13 18:00:03 +0000125def is_url(path):
126 return bool(re.match(r'^https?://.+$', path))
127
128
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000129def path_starts_with(prefix, path):
130 """Returns true if the components of the path |prefix| are the same as the
131 initial components of |path| (or all of the components of |path|). The paths
132 must be absolute.
133 """
134 assert os.path.isabs(prefix) and os.path.isabs(path)
135 prefix = os.path.normpath(prefix)
136 path = os.path.normpath(path)
137 assert prefix == trace_inputs.get_native_path_case(prefix), prefix
138 assert path == trace_inputs.get_native_path_case(path), path
139 prefix = prefix.rstrip(os.path.sep) + os.path.sep
140 path = path.rstrip(os.path.sep) + os.path.sep
141 return path.startswith(prefix)
142
143
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000144def fix_native_path_case(root, path):
145 """Ensures that each component of |path| has the proper native case by
146 iterating slowly over the directory elements of |path|."""
147 native_case_path = root
148 for raw_part in path.split(os.sep):
149 if not raw_part or raw_part == '.':
150 break
151
152 part = trace_inputs.find_item_native_case(native_case_path, raw_part)
153 if not part:
154 raise run_isolated.MappingError(
155 'Input file %s doesn\'t exist' %
156 os.path.join(native_case_path, raw_part))
157 native_case_path = os.path.join(native_case_path, part)
158
159 return os.path.normpath(native_case_path)
160
161
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000162def expand_symlinks(indir, relfile):
163 """Follows symlinks in |relfile|, but treating symlinks that point outside the
164 build tree as if they were ordinary directories/files. Returns the final
165 symlink-free target and a list of paths to symlinks encountered in the
166 process.
167
168 The rule about symlinks outside the build tree is for the benefit of the
169 Chromium OS ebuild, which symlinks the output directory to an unrelated path
170 in the chroot.
171
172 Fails when a directory loop is detected, although in theory we could support
173 that case.
174 """
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000175 is_directory = relfile.endswith(os.path.sep)
176 done = indir
177 todo = relfile.strip(os.path.sep)
178 symlinks = []
179
180 while todo:
181 pre_symlink, symlink, post_symlink = trace_inputs.split_at_symlink(
182 done, todo)
183 if not symlink:
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000184 todo = fix_native_path_case(done, todo)
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000185 done = os.path.join(done, todo)
186 break
187 symlink_path = os.path.join(done, pre_symlink, symlink)
188 post_symlink = post_symlink.lstrip(os.path.sep)
189 # readlink doesn't exist on Windows.
190 # pylint: disable=E1101
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000191 target = os.path.normpath(os.path.join(done, pre_symlink))
192 symlink_target = os.readlink(symlink_path)
maruel@chromium.org28c19672013-04-29 18:51:09 +0000193 if os.path.isabs(symlink_target):
194 # Absolute path are considered a normal directories. The use case is
195 # generally someone who puts the output directory on a separate drive.
196 target = symlink_target
197 else:
198 # The symlink itself could be using the wrong path case.
199 target = fix_native_path_case(target, symlink_target)
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000200
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000201 if not os.path.exists(target):
202 raise run_isolated.MappingError(
203 'Symlink target doesn\'t exist: %s -> %s' % (symlink_path, target))
204 target = trace_inputs.get_native_path_case(target)
205 if not path_starts_with(indir, target):
206 done = symlink_path
207 todo = post_symlink
208 continue
209 if path_starts_with(target, symlink_path):
210 raise run_isolated.MappingError(
211 'Can\'t map recursive symlink reference %s -> %s' %
212 (symlink_path, target))
213 logging.info('Found symlink: %s -> %s', symlink_path, target)
214 symlinks.append(os.path.relpath(symlink_path, indir))
215 # Treat the common prefix of the old and new paths as done, and start
216 # scanning again.
217 target = target.split(os.path.sep)
218 symlink_path = symlink_path.split(os.path.sep)
219 prefix_length = 0
220 for target_piece, symlink_path_piece in zip(target, symlink_path):
221 if target_piece == symlink_path_piece:
222 prefix_length += 1
223 else:
224 break
225 done = os.path.sep.join(target[:prefix_length])
226 todo = os.path.join(
227 os.path.sep.join(target[prefix_length:]), post_symlink)
228
229 relfile = os.path.relpath(done, indir)
230 relfile = relfile.rstrip(os.path.sep) + is_directory * os.path.sep
231 return relfile, symlinks
232
233
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000234def expand_directory_and_symlink(indir, relfile, blacklist, follow_symlinks):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000235 """Expands a single input. It can result in multiple outputs.
236
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000237 This function is recursive when relfile is a directory.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000238
239 Note: this code doesn't properly handle recursive symlink like one created
240 with:
241 ln -s .. foo
242 """
243 if os.path.isabs(relfile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000244 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000245 'Can\'t map absolute path %s' % relfile)
246
247 infile = normpath(os.path.join(indir, relfile))
248 if not infile.startswith(indir):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000249 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000250 'Can\'t map file %s outside %s' % (infile, indir))
251
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000252 filepath = os.path.join(indir, relfile)
253 native_filepath = trace_inputs.get_native_path_case(filepath)
254 if filepath != native_filepath:
maruel@chromium.org59bb2a32013-03-21 17:08:39 +0000255 # Special case './'.
256 if filepath != native_filepath + '.' + os.path.sep:
maruel@chromium.org7f66a982013-06-06 15:58:59 +0000257 # Give up enforcing strict path case on OSX. Really, it's that sad. The
258 # case where it happens is very specific and hard to reproduce:
259 # get_native_path_case(
260 # u'Foo.framework/Versions/A/Resources/Something.nib') will return
261 # u'Foo.framework/Versions/A/resources/Something.nib', e.g. lowercase 'r'.
262 #
263 # Note that this is really something deep in OSX because running
264 # ls Foo.framework/Versions/A
265 # will print out 'Resources', while trace_inputs.get_native_path_case()
266 # returns a lower case 'r'.
267 #
268 # So *something* is happening under the hood resulting in the command 'ls'
269 # and Carbon.File.FSPathMakeRef('path').FSRefMakePath() to disagree. We
270 # have no idea why.
271 if sys.platform != 'darwin':
272 raise run_isolated.MappingError(
273 'File path doesn\'t equal native file path\n%s != %s' %
274 (filepath, native_filepath))
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000275
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000276 symlinks = []
277 if follow_symlinks:
278 relfile, symlinks = expand_symlinks(indir, relfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000279
280 if relfile.endswith(os.path.sep):
281 if not os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000282 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000283 '%s is not a directory but ends with "%s"' % (infile, os.path.sep))
284
maruel@chromium.org59bb2a32013-03-21 17:08:39 +0000285 # Special case './'.
286 if relfile.startswith('.' + os.path.sep):
287 relfile = relfile[2:]
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000288 outfiles = symlinks
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000289 try:
290 for filename in os.listdir(infile):
291 inner_relfile = os.path.join(relfile, filename)
292 if blacklist(inner_relfile):
293 continue
294 if os.path.isdir(os.path.join(indir, inner_relfile)):
295 inner_relfile += os.path.sep
296 outfiles.extend(
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000297 expand_directory_and_symlink(indir, inner_relfile, blacklist,
298 follow_symlinks))
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000299 return outfiles
300 except OSError as e:
maruel@chromium.org1cd786e2013-04-26 18:48:40 +0000301 raise run_isolated.MappingError(
302 'Unable to iterate over directory %s.\n%s' % (infile, e))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000303 else:
304 # Always add individual files even if they were blacklisted.
305 if os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000306 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000307 'Input directory %s must have a trailing slash' % infile)
308
309 if not os.path.isfile(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000310 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000311 'Input file %s doesn\'t exist' % infile)
312
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000313 return symlinks + [relfile]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000314
315
csharp@chromium.org01856802012-11-12 17:48:13 +0000316def expand_directories_and_symlinks(indir, infiles, blacklist,
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000317 follow_symlinks, ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000318 """Expands the directories and the symlinks, applies the blacklist and
319 verifies files exist.
320
321 Files are specified in os native path separator.
322 """
323 outfiles = []
324 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +0000325 try:
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000326 outfiles.extend(expand_directory_and_symlink(indir, relfile, blacklist,
327 follow_symlinks))
csharp@chromium.org01856802012-11-12 17:48:13 +0000328 except run_isolated.MappingError as e:
329 if ignore_broken_items:
330 logging.info('warning: %s', e)
331 else:
332 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000333 return outfiles
334
335
336def recreate_tree(outdir, indir, infiles, action, as_sha1):
337 """Creates a new tree with only the input files in it.
338
339 Arguments:
340 outdir: Output directory to create the files in.
341 indir: Root directory the infiles are based in.
342 infiles: dict of files to map from |indir| to |outdir|.
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000343 action: One of accepted action of run_isolated.link_file().
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000344 as_sha1: Output filename is the sha1 instead of relfile.
345 """
346 logging.info(
347 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_sha1=%s)' %
348 (outdir, indir, len(infiles), action, as_sha1))
349
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000350 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000351 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000352 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000353 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000354
355 for relfile, metadata in infiles.iteritems():
356 infile = os.path.join(indir, relfile)
357 if as_sha1:
358 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000359 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000360 # Skip links when storing a hashtable.
361 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000362 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000363 if os.path.isfile(outfile):
364 # Just do a quick check that the file size matches. No need to stat()
365 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000366 if not 's' in metadata:
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000367 raise run_isolated.MappingError(
368 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000369 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000370 continue
371 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000372 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000373 os.remove(outfile)
374 else:
375 outfile = os.path.join(outdir, relfile)
376 outsubdir = os.path.dirname(outfile)
377 if not os.path.isdir(outsubdir):
378 os.makedirs(outsubdir)
379
380 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000381 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000382 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000383 if 'l' in metadata:
384 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000385 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000386 # symlink doesn't exist on Windows.
387 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000388 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000389 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000390
391
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000392def process_input(filepath, prevdict, read_only, flavor):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000393 """Processes an input file, a dependency, and return meta data about it.
394
395 Arguments:
396 - filepath: File to act on.
397 - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
398 to skip recalculating the hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000399 - read_only: If True, the file mode is manipulated. In practice, only save
400 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
401 windows, mode is not set since all files are 'executable' by
402 default.
403
404 Behaviors:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000405 - Retrieves the file mode, file size, file timestamp, file link
406 destination if it is a file link and calcultate the SHA-1 of the file's
407 content if the path points to a file and not a symlink.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000408 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000409 out = {}
410 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000411 # if prevdict.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000412 # # The file's content is ignored. Skip the time and hard code mode.
413 # if get_flavor() != 'win':
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000414 # out['m'] = stat.S_IRUSR | stat.S_IRGRP
415 # out['s'] = 0
416 # out['h'] = SHA_1_NULL
417 # out['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000418 # return out
419
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000420 # Always check the file stat and check if it is a link. The timestamp is used
421 # to know if the file's content/symlink destination should be looked into.
422 # E.g. only reuse from prevdict if the timestamp hasn't changed.
423 # There is the risk of the file's timestamp being reset to its last value
424 # manually while its content changed. We don't protect against that use case.
425 try:
426 filestats = os.lstat(filepath)
427 except OSError:
428 # The file is not present.
429 raise run_isolated.MappingError('%s is missing' % filepath)
430 is_link = stat.S_ISLNK(filestats.st_mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000431
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000432 if flavor != 'win':
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000433 # Ignore file mode on Windows since it's not really useful there.
434 filemode = stat.S_IMODE(filestats.st_mode)
435 # Remove write access for group and all access to 'others'.
436 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
437 if read_only:
438 filemode &= ~stat.S_IWUSR
439 if filemode & stat.S_IXUSR:
440 filemode |= stat.S_IXGRP
441 else:
442 filemode &= ~stat.S_IXGRP
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000443 out['m'] = filemode
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000444
445 # Used to skip recalculating the hash or link destination. Use the most recent
446 # update time.
447 # TODO(maruel): Save it in the .state file instead of .isolated so the
448 # .isolated file is deterministic.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000449 out['t'] = int(round(filestats.st_mtime))
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000450
451 if not is_link:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000452 out['s'] = filestats.st_size
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000453 # If the timestamp wasn't updated and the file size is still the same, carry
454 # on the sha-1.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000455 if (prevdict.get('t') == out['t'] and
456 prevdict.get('s') == out['s']):
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000457 # Reuse the previous hash if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000458 out['h'] = prevdict.get('h')
459 if not out.get('h'):
maruel@chromium.org6da38772012-12-11 21:36:37 +0000460 out['h'] = isolateserver_archive.sha1_file(filepath)
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000461 else:
462 # If the timestamp wasn't updated, carry on the link destination.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000463 if prevdict.get('t') == out['t']:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000464 # Reuse the previous link destination if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000465 out['l'] = prevdict.get('l')
466 if out.get('l') is None:
maruel@chromium.org8d159e32013-04-18 15:29:50 +0000467 # The link could be in an incorrect path case. In practice, this only
468 # happen on OSX on case insensitive HFS.
469 # TODO(maruel): It'd be better if it was only done once, in
470 # expand_directory_and_symlink(), so it would not be necessary to do again
471 # here.
472 symlink_value = os.readlink(filepath) # pylint: disable=E1101
473 filedir = trace_inputs.get_native_path_case(os.path.dirname(filepath))
474 native_dest = fix_native_path_case(filedir, symlink_value)
475 out['l'] = os.path.relpath(native_dest, filedir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000476 return out
477
478
479### Variable stuff.
480
481
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000482def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000483 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000484 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000485
486
487def determine_root_dir(relative_root, infiles):
488 """For a list of infiles, determines the deepest root directory that is
489 referenced indirectly.
490
491 All arguments must be using os.path.sep.
492 """
493 # The trick used to determine the root directory is to look at "how far" back
494 # up it is looking up.
495 deepest_root = relative_root
496 for i in infiles:
497 x = relative_root
498 while i.startswith('..' + os.path.sep):
499 i = i[3:]
500 assert not i.startswith(os.path.sep)
501 x = os.path.dirname(x)
502 if deepest_root.startswith(x):
503 deepest_root = x
504 logging.debug(
505 'determine_root_dir(%s, %d files) -> %s' % (
506 relative_root, len(infiles), deepest_root))
507 return deepest_root
508
509
510def replace_variable(part, variables):
511 m = re.match(r'<\(([A-Z_]+)\)', part)
512 if m:
513 if m.group(1) not in variables:
514 raise ExecutionError(
515 'Variable "%s" was not found in %s.\nDid you forget to specify '
516 '--variable?' % (m.group(1), variables))
517 return variables[m.group(1)]
518 return part
519
520
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000521def process_variables(cwd, variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000522 """Processes path variables as a special case and returns a copy of the dict.
523
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000524 For each 'path' variable: first normalizes it based on |cwd|, verifies it
525 exists then sets it as relative to relative_base_dir.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000526 """
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000527 relative_base_dir = trace_inputs.get_native_path_case(relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000528 variables = variables.copy()
529 for i in PATH_VARIABLES:
530 if i not in variables:
531 continue
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000532 variable = variables[i].strip()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000533 # Variables could contain / or \ on windows. Always normalize to
534 # os.path.sep.
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000535 variable = variable.replace('/', os.path.sep)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000536 variable = os.path.join(cwd, variable)
537 variable = os.path.normpath(variable)
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000538 variable = trace_inputs.get_native_path_case(variable)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000539 if not os.path.isdir(variable):
540 raise ExecutionError('%s=%s is not a directory' % (i, variable))
541
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000542 # All variables are relative to the .isolate file.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000543 variable = os.path.relpath(variable, relative_base_dir)
544 logging.debug(
545 'Translated variable %s from %s to %s', i, variables[i], variable)
546 variables[i] = variable
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000547 return variables
548
549
550def eval_variables(item, variables):
551 """Replaces the .isolate variables in a string item.
552
553 Note that the .isolate format is a subset of the .gyp dialect.
554 """
555 return ''.join(
556 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
557
558
559def classify_files(root_dir, tracked, untracked):
560 """Converts the list of files into a .isolate 'variables' dictionary.
561
562 Arguments:
563 - tracked: list of files names to generate a dictionary out of that should
564 probably be tracked.
565 - untracked: list of files names that must not be tracked.
566 """
567 # These directories are not guaranteed to be always present on every builder.
568 OPTIONAL_DIRECTORIES = (
569 'test/data/plugin',
570 'third_party/WebKit/LayoutTests',
571 )
572
573 new_tracked = []
574 new_untracked = list(untracked)
575
576 def should_be_tracked(filepath):
577 """Returns True if it is a file without whitespace in a non-optional
578 directory that has no symlink in its path.
579 """
580 if filepath.endswith('/'):
581 return False
582 if ' ' in filepath:
583 return False
584 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
585 return False
586 # Look if any element in the path is a symlink.
587 split = filepath.split('/')
588 for i in range(len(split)):
589 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
590 return False
591 return True
592
593 for filepath in sorted(tracked):
594 if should_be_tracked(filepath):
595 new_tracked.append(filepath)
596 else:
597 # Anything else.
598 new_untracked.append(filepath)
599
600 variables = {}
601 if new_tracked:
602 variables[KEY_TRACKED] = sorted(new_tracked)
603 if new_untracked:
604 variables[KEY_UNTRACKED] = sorted(new_untracked)
605 return variables
606
607
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000608def chromium_fix(f, variables):
609 """Fixes an isolate dependnecy with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000610 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
611 # separator.
612 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000613 # Ignored items.
614 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000615 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000616 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000617 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000618 # 'First Run' is not created by the compile, but by the test itself.
619 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000620
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000621 # Blacklist logs and other unimportant files.
622 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
623 logging.debug('Ignoring %s', f)
624 return None
625
maruel@chromium.org7650e422012-11-16 21:56:42 +0000626 EXECUTABLE = re.compile(
627 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
628 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
629 r'$')
630 match = EXECUTABLE.match(f)
631 if match:
632 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
633
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000634 if sys.platform == 'darwin':
635 # On OSX, the name of the output is dependent on gyp define, it can be
636 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
637 # Framework.framework'. Furthermore, they are versioned with a gyp
638 # variable. To lower the complexity of the .isolate file, remove all the
639 # individual entries that show up under any of the 4 entries and replace
640 # them with the directory itself. Overall, this results in a bit more
641 # files than strictly necessary.
642 OSX_BUNDLES = (
643 '<(PRODUCT_DIR)/Chromium Framework.framework/',
644 '<(PRODUCT_DIR)/Chromium.app/',
645 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
646 '<(PRODUCT_DIR)/Google Chrome.app/',
647 )
648 for prefix in OSX_BUNDLES:
649 if f.startswith(prefix):
650 # Note this result in duplicate values, so the a set() must be used to
651 # remove duplicates.
652 return prefix
653 return f
654
655
656def generate_simplified(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000657 tracked, untracked, touched, root_dir, variables, relative_cwd,
658 trace_blacklist):
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000659 """Generates a clean and complete .isolate 'variables' dictionary.
660
661 Cleans up and extracts only files from within root_dir then processes
662 variables and relative_cwd.
663 """
664 root_dir = os.path.realpath(root_dir)
665 logging.info(
666 'generate_simplified(%d files, %s, %s, %s)' %
667 (len(tracked) + len(untracked) + len(touched),
668 root_dir, variables, relative_cwd))
669
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000670 # Preparation work.
671 relative_cwd = cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000672 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000673 # Creates the right set of variables here. We only care about PATH_VARIABLES.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000674 path_variables = dict(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000675 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
676 for k in PATH_VARIABLES if k in variables)
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000677 variables = variables.copy()
678 variables.update(path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000679
680 # Actual work: Process the files.
681 # TODO(maruel): if all the files in a directory are in part tracked and in
682 # part untracked, the directory will not be extracted. Tracked files should be
683 # 'promoted' to be untracked as needed.
684 tracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000685 root_dir, tracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000686 untracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000687 root_dir, untracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000688 # touched is not compressed, otherwise it would result in files to be archived
689 # that we don't need.
690
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000691 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000692 def fix(f):
693 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000694 # Important, GYP stores the files with / and not \.
695 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000696 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000697 # If it's not already a variable.
698 if not f.startswith('<'):
699 # relative_cwd is usually the directory containing the gyp file. It may be
700 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000701 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000702 # Convert the whole thing to / since it's isolate's speak.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000703 f = posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000704 posixpath.join(root_dir_posix, f),
705 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000706
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000707 for variable, root_path in path_variables.iteritems():
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000708 if f.startswith(root_path):
709 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000710 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000711 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000712 return f
713
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000714 def fix_all(items):
715 """Reduces the items to convert variables, removes unneeded items, apply
716 chromium-specific fixes and only return unique items.
717 """
718 variables_converted = (fix(f.path) for f in items)
719 chromium_fixed = (chromium_fix(f, variables) for f in variables_converted)
720 return set(f for f in chromium_fixed if f)
721
722 tracked = fix_all(tracked)
723 untracked = fix_all(untracked)
724 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000725 out = classify_files(root_dir, tracked, untracked)
726 if touched:
727 out[KEY_TOUCHED] = sorted(touched)
728 return out
729
730
benrg@chromium.org609b7982013-02-07 16:44:46 +0000731def chromium_filter_flags(variables):
732 """Filters out build flags used in Chromium that we don't want to treat as
733 configuration variables.
734 """
735 # TODO(benrg): Need a better way to determine this.
736 blacklist = set(PATH_VARIABLES + ('EXECUTABLE_SUFFIX', 'FLAG'))
737 return dict((k, v) for k, v in variables.iteritems() if k not in blacklist)
738
739
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000740def generate_isolate(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000741 tracked, untracked, touched, root_dir, variables, relative_cwd,
742 trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000743 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000744 dependencies = generate_simplified(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000745 tracked, untracked, touched, root_dir, variables, relative_cwd,
746 trace_blacklist)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000747 config_variables = chromium_filter_flags(variables)
748 config_variable_names, config_values = zip(
749 *sorted(config_variables.iteritems()))
750 out = Configs(None)
751 # The new dependencies apply to just one configuration, namely config_values.
752 out.merge_dependencies(dependencies, config_variable_names, [config_values])
753 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000754
755
756def split_touched(files):
757 """Splits files that are touched vs files that are read."""
758 tracked = []
759 touched = []
760 for f in files:
761 if f.size:
762 tracked.append(f)
763 else:
764 touched.append(f)
765 return tracked, touched
766
767
768def pretty_print(variables, stdout):
769 """Outputs a gyp compatible list from the decoded variables.
770
771 Similar to pprint.print() but with NIH syndrome.
772 """
773 # Order the dictionary keys by these keys in priority.
774 ORDER = (
775 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
776 KEY_TRACKED, KEY_UNTRACKED)
777
778 def sorting_key(x):
779 """Gives priority to 'most important' keys before the others."""
780 if x in ORDER:
781 return str(ORDER.index(x))
782 return x
783
784 def loop_list(indent, items):
785 for item in items:
786 if isinstance(item, basestring):
787 stdout.write('%s\'%s\',\n' % (indent, item))
788 elif isinstance(item, dict):
789 stdout.write('%s{\n' % indent)
790 loop_dict(indent + ' ', item)
791 stdout.write('%s},\n' % indent)
792 elif isinstance(item, list):
793 # A list inside a list will write the first item embedded.
794 stdout.write('%s[' % indent)
795 for index, i in enumerate(item):
796 if isinstance(i, basestring):
797 stdout.write(
798 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
799 elif isinstance(i, dict):
800 stdout.write('{\n')
801 loop_dict(indent + ' ', i)
802 if index != len(item) - 1:
803 x = ', '
804 else:
805 x = ''
806 stdout.write('%s}%s' % (indent, x))
807 else:
808 assert False
809 stdout.write('],\n')
810 else:
811 assert False
812
813 def loop_dict(indent, items):
814 for key in sorted(items, key=sorting_key):
815 item = items[key]
816 stdout.write("%s'%s': " % (indent, key))
817 if isinstance(item, dict):
818 stdout.write('{\n')
819 loop_dict(indent + ' ', item)
820 stdout.write(indent + '},\n')
821 elif isinstance(item, list):
822 stdout.write('[\n')
823 loop_list(indent + ' ', item)
824 stdout.write(indent + '],\n')
825 elif isinstance(item, basestring):
826 stdout.write(
827 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
828 elif item in (True, False, None):
829 stdout.write('%s\n' % item)
830 else:
831 assert False, item
832
833 stdout.write('{\n')
834 loop_dict(' ', variables)
835 stdout.write('}\n')
836
837
838def union(lhs, rhs):
839 """Merges two compatible datastructures composed of dict/list/set."""
840 assert lhs is not None or rhs is not None
841 if lhs is None:
842 return copy.deepcopy(rhs)
843 if rhs is None:
844 return copy.deepcopy(lhs)
845 assert type(lhs) == type(rhs), (lhs, rhs)
846 if hasattr(lhs, 'union'):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000847 # Includes set, ConfigSettings and Configs.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000848 return lhs.union(rhs)
849 if isinstance(lhs, dict):
850 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
851 elif isinstance(lhs, list):
852 # Do not go inside the list.
853 return lhs + rhs
854 assert False, type(lhs)
855
856
857def extract_comment(content):
858 """Extracts file level comment."""
859 out = []
860 for line in content.splitlines(True):
861 if line.startswith('#'):
862 out.append(line)
863 else:
864 break
865 return ''.join(out)
866
867
868def eval_content(content):
869 """Evaluates a python file and return the value defined in it.
870
871 Used in practice for .isolate files.
872 """
873 globs = {'__builtins__': None}
874 locs = {}
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000875 try:
876 value = eval(content, globs, locs)
877 except TypeError as e:
878 e.args = list(e.args) + [content]
879 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000880 assert locs == {}, locs
881 assert globs == {'__builtins__': None}, globs
882 return value
883
884
benrg@chromium.org609b7982013-02-07 16:44:46 +0000885def match_configs(expr, config_variables, all_configs):
886 """Returns the configs from |all_configs| that match the |expr|, where
887 the elements of |all_configs| are tuples of values for the |config_variables|.
888 Example:
889 >>> match_configs(expr = "(foo==1 or foo==2) and bar=='b'",
890 config_variables = ["foo", "bar"],
891 all_configs = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
892 [(1, 'b'), (2, 'b')]
893 """
894 return [
895 config for config in all_configs
896 if eval(expr, dict(zip(config_variables, config)))
897 ]
898
899
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000900def verify_variables(variables):
901 """Verifies the |variables| dictionary is in the expected format."""
902 VALID_VARIABLES = [
903 KEY_TOUCHED,
904 KEY_TRACKED,
905 KEY_UNTRACKED,
906 'command',
907 'read_only',
908 ]
909 assert isinstance(variables, dict), variables
910 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
911 for name, value in variables.iteritems():
912 if name == 'read_only':
913 assert value in (True, False, None), value
914 else:
915 assert isinstance(value, list), value
916 assert all(isinstance(i, basestring) for i in value), value
917
918
benrg@chromium.org609b7982013-02-07 16:44:46 +0000919def verify_ast(expr, variables_and_values):
920 """Verifies that |expr| is of the form
921 expr ::= expr ( "or" | "and" ) expr
922 | identifier "==" ( string | int )
923 Also collects the variable identifiers and string/int values in the dict
924 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
925 """
926 assert isinstance(expr, (ast.BoolOp, ast.Compare))
927 if isinstance(expr, ast.BoolOp):
928 assert isinstance(expr.op, (ast.And, ast.Or))
929 for subexpr in expr.values:
930 verify_ast(subexpr, variables_and_values)
931 else:
932 assert isinstance(expr.left.ctx, ast.Load)
933 assert len(expr.ops) == 1
934 assert isinstance(expr.ops[0], ast.Eq)
935 var_values = variables_and_values.setdefault(expr.left.id, set())
936 rhs = expr.comparators[0]
937 assert isinstance(rhs, (ast.Str, ast.Num))
938 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
939
940
941def verify_condition(condition, variables_and_values):
942 """Verifies the |condition| dictionary is in the expected format.
943 See verify_ast() for the meaning of |variables_and_values|.
944 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000945 VALID_INSIDE_CONDITION = ['variables']
946 assert isinstance(condition, list), condition
benrg@chromium.org609b7982013-02-07 16:44:46 +0000947 assert len(condition) == 2, condition
948 expr, then = condition
949
950 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
951 verify_ast(test_ast.body, variables_and_values)
952
953 assert isinstance(then, dict), then
954 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
955 verify_variables(then['variables'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000956
957
benrg@chromium.org609b7982013-02-07 16:44:46 +0000958def verify_root(value, variables_and_values):
959 """Verifies that |value| is the parsed form of a valid .isolate file.
960 See verify_ast() for the meaning of |variables_and_values|.
961 """
962 VALID_ROOTS = ['includes', 'conditions']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000963 assert isinstance(value, dict), value
964 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000965
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000966 includes = value.get('includes', [])
967 assert isinstance(includes, list), includes
968 for include in includes:
969 assert isinstance(include, basestring), include
970
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000971 conditions = value.get('conditions', [])
972 assert isinstance(conditions, list), conditions
973 for condition in conditions:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000974 verify_condition(condition, variables_and_values)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000975
976
benrg@chromium.org609b7982013-02-07 16:44:46 +0000977def remove_weak_dependencies(values, key, item, item_configs):
978 """Removes any configs from this key if the item is already under a
979 strong key.
980 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000981 if key == KEY_TOUCHED:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000982 item_configs = set(item_configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000983 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000984 try:
985 item_configs -= values[stronger_key][item]
986 except KeyError:
987 pass
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000988
benrg@chromium.org609b7982013-02-07 16:44:46 +0000989 return item_configs
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000990
991
benrg@chromium.org609b7982013-02-07 16:44:46 +0000992def remove_repeated_dependencies(folders, key, item, item_configs):
993 """Removes any configs from this key if the item is in a folder that is
994 already included."""
csharp@chromium.org31176252012-11-02 13:04:40 +0000995
996 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000997 item_configs = set(item_configs)
998 for (folder, configs) in folders.iteritems():
csharp@chromium.org31176252012-11-02 13:04:40 +0000999 if folder != item and item.startswith(folder):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001000 item_configs -= configs
csharp@chromium.org31176252012-11-02 13:04:40 +00001001
benrg@chromium.org609b7982013-02-07 16:44:46 +00001002 return item_configs
csharp@chromium.org31176252012-11-02 13:04:40 +00001003
1004
1005def get_folders(values_dict):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001006 """Returns a dict of all the folders in the given value_dict."""
1007 return dict(
1008 (item, configs) for (item, configs) in values_dict.iteritems()
1009 if item.endswith('/')
1010 )
csharp@chromium.org31176252012-11-02 13:04:40 +00001011
1012
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001013def invert_map(variables):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001014 """Converts {config: {deptype: list(depvals)}} to
1015 {deptype: {depval: set(configs)}}.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001016 """
1017 KEYS = (
1018 KEY_TOUCHED,
1019 KEY_TRACKED,
1020 KEY_UNTRACKED,
1021 'command',
1022 'read_only',
1023 )
1024 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001025 for config, values in variables.iteritems():
1026 for key in KEYS:
1027 if key == 'command':
1028 items = [tuple(values[key])] if key in values else []
1029 elif key == 'read_only':
1030 items = [values[key]] if key in values else []
1031 else:
1032 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
1033 items = values.get(key, [])
1034 for item in items:
1035 out[key].setdefault(item, set()).add(config)
1036 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001037
1038
benrg@chromium.org609b7982013-02-07 16:44:46 +00001039def reduce_inputs(values):
1040 """Reduces the output of invert_map() to the strictest minimum list.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001041
benrg@chromium.org609b7982013-02-07 16:44:46 +00001042 Looks at each individual file and directory, maps where they are used and
1043 reconstructs the inverse dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001044
benrg@chromium.org609b7982013-02-07 16:44:46 +00001045 Returns the minimized dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001046 """
1047 KEYS = (
1048 KEY_TOUCHED,
1049 KEY_TRACKED,
1050 KEY_UNTRACKED,
1051 'command',
1052 'read_only',
1053 )
csharp@chromium.org31176252012-11-02 13:04:40 +00001054
1055 # Folders can only live in KEY_UNTRACKED.
1056 folders = get_folders(values.get(KEY_UNTRACKED, {}))
1057
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001058 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001059 for key in KEYS:
1060 for item, item_configs in values.get(key, {}).iteritems():
1061 item_configs = remove_weak_dependencies(values, key, item, item_configs)
1062 item_configs = remove_repeated_dependencies(
1063 folders, key, item, item_configs)
1064 if item_configs:
1065 out[key][item] = item_configs
1066 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001067
1068
benrg@chromium.org609b7982013-02-07 16:44:46 +00001069def convert_map_to_isolate_dict(values, config_variables):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001070 """Regenerates back a .isolate configuration dict from files and dirs
1071 mappings generated from reduce_inputs().
1072 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001073 # Gather a list of configurations for set inversion later.
1074 all_mentioned_configs = set()
1075 for configs_by_item in values.itervalues():
1076 for configs in configs_by_item.itervalues():
1077 all_mentioned_configs.update(configs)
1078
1079 # Invert the mapping to make it dict first.
1080 conditions = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001081 for key in values:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001082 for item, configs in values[key].iteritems():
1083 then = conditions.setdefault(frozenset(configs), {})
1084 variables = then.setdefault('variables', {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001085
benrg@chromium.org609b7982013-02-07 16:44:46 +00001086 if item in (True, False):
1087 # One-off for read_only.
1088 variables[key] = item
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001089 else:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001090 assert item
1091 if isinstance(item, tuple):
1092 # One-off for command.
1093 # Do not merge lists and do not sort!
1094 # Note that item is a tuple.
1095 assert key not in variables
1096 variables[key] = list(item)
1097 else:
1098 # The list of items (files or dirs). Append the new item and keep
1099 # the list sorted.
1100 l = variables.setdefault(key, [])
1101 l.append(item)
1102 l.sort()
1103
1104 if all_mentioned_configs:
1105 config_values = map(set, zip(*all_mentioned_configs))
1106 sef = short_expression_finder.ShortExpressionFinder(
1107 zip(config_variables, config_values))
1108
1109 conditions = sorted(
1110 [sef.get_expr(configs), then] for configs, then in conditions.iteritems())
1111 return {'conditions': conditions}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001112
1113
1114### Internal state files.
1115
1116
benrg@chromium.org609b7982013-02-07 16:44:46 +00001117class ConfigSettings(object):
1118 """Represents the dependency variables for a single build configuration.
1119 The structure is immutable.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001120 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001121 def __init__(self, config, values):
1122 self.config = config
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001123 verify_variables(values)
1124 self.touched = sorted(values.get(KEY_TOUCHED, []))
1125 self.tracked = sorted(values.get(KEY_TRACKED, []))
1126 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
1127 self.command = values.get('command', [])[:]
1128 self.read_only = values.get('read_only')
1129
1130 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001131 assert not (self.config and rhs.config) or (self.config == rhs.config)
maruel@chromium.org669edcb2012-11-02 19:16:14 +00001132 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001133 var = {
1134 KEY_TOUCHED: sorted(self.touched + rhs.touched),
1135 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
1136 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
1137 'command': self.command or rhs.command,
1138 'read_only': rhs.read_only if self.read_only is None else self.read_only,
1139 }
benrg@chromium.org609b7982013-02-07 16:44:46 +00001140 return ConfigSettings(self.config or rhs.config, var)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001141
1142 def flatten(self):
1143 out = {}
1144 if self.command:
1145 out['command'] = self.command
1146 if self.touched:
1147 out[KEY_TOUCHED] = self.touched
1148 if self.tracked:
1149 out[KEY_TRACKED] = self.tracked
1150 if self.untracked:
1151 out[KEY_UNTRACKED] = self.untracked
1152 if self.read_only is not None:
1153 out['read_only'] = self.read_only
1154 return out
1155
1156
1157class Configs(object):
1158 """Represents a processed .isolate file.
1159
benrg@chromium.org609b7982013-02-07 16:44:46 +00001160 Stores the file in a processed way, split by configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001161 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001162 def __init__(self, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001163 self.file_comment = file_comment
benrg@chromium.org609b7982013-02-07 16:44:46 +00001164 # The keys of by_config are tuples of values for the configuration
1165 # variables. The names of the variables (which must be the same for
1166 # every by_config key) are kept in config_variables. Initially by_config
1167 # is empty and we don't know what configuration variables will be used,
1168 # so config_variables also starts out empty. It will be set by the first
1169 # call to union() or merge_dependencies().
1170 self.by_config = {}
1171 self.config_variables = ()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001172
1173 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001174 """Adds variables from rhs (a Configs) to the existing variables.
1175 """
1176 config_variables = self.config_variables
1177 if not config_variables:
1178 config_variables = rhs.config_variables
1179 else:
1180 # We can't proceed if this isn't true since we don't know the correct
1181 # default values for extra variables. The variables are sorted so we
1182 # don't need to worry about permutations.
1183 if rhs.config_variables and rhs.config_variables != config_variables:
1184 raise ExecutionError(
1185 'Variables in merged .isolate files do not match: %r and %r' % (
1186 config_variables, rhs.config_variables))
1187
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001188 # Takes the first file comment, prefering lhs.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001189 out = Configs(self.file_comment or rhs.file_comment)
1190 out.config_variables = config_variables
1191 for config in set(self.by_config) | set(rhs.by_config):
1192 out.by_config[config] = union(
1193 self.by_config.get(config), rhs.by_config.get(config))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001194 return out
1195
benrg@chromium.org609b7982013-02-07 16:44:46 +00001196 def merge_dependencies(self, values, config_variables, configs):
1197 """Adds new dependencies to this object for the given configurations.
1198 Arguments:
1199 values: A variables dict as found in a .isolate file, e.g.,
1200 {KEY_TOUCHED: [...], 'command': ...}.
1201 config_variables: An ordered list of configuration variables, e.g.,
1202 ["OS", "chromeos"]. If this object already contains any dependencies,
1203 the configuration variables must match.
1204 configs: a list of tuples of values of the configuration variables,
1205 e.g., [("mac", 0), ("linux", 1)]. The dependencies in |values|
1206 are added to all of these configurations, and other configurations
1207 are unchanged.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001208 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001209 if not values:
1210 return
1211
1212 if not self.config_variables:
1213 self.config_variables = config_variables
1214 else:
1215 # See comment in Configs.union().
1216 assert self.config_variables == config_variables
1217
1218 for config in configs:
1219 self.by_config[config] = union(
1220 self.by_config.get(config), ConfigSettings(config, values))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001221
1222 def flatten(self):
1223 """Returns a flat dictionary representation of the configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001224 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001225 return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
1226
1227 def make_isolate_file(self):
1228 """Returns a dictionary suitable for writing to a .isolate file.
1229 """
1230 dependencies_by_config = self.flatten()
1231 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
1232 return convert_map_to_isolate_dict(configs_by_dependency,
1233 self.config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001234
1235
benrg@chromium.org609b7982013-02-07 16:44:46 +00001236# TODO(benrg): Remove this function when no old-format files are left.
1237def convert_old_to_new_format(value):
1238 """Converts from the old .isolate format, which only has one variable (OS),
1239 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
1240 and allows conditions that depend on the set of all OSes, to the new format,
1241 which allows any set of variables, has no hardcoded values, and only allows
1242 explicit positive tests of variable values.
1243 """
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001244 conditions = value.get('conditions', [])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001245 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
1246 return value # Nothing to change
1247
1248 def parse_condition(cond):
1249 return re.match(r'OS=="(\w+)"\Z', cond[0]).group(1)
1250
1251 oses = set(map(parse_condition, conditions))
1252 default_oses = set(['linux', 'mac', 'win'])
1253 oses = sorted(oses | default_oses)
1254
1255 def if_not_os(not_os, then):
1256 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
1257 return [expr, then]
1258
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001259 conditions = [
1260 cond[:2] for cond in conditions if cond[1]
1261 ] + [
1262 if_not_os(parse_condition(cond), cond[2])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001263 for cond in conditions if len(cond) == 3
1264 ]
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001265
benrg@chromium.org609b7982013-02-07 16:44:46 +00001266 if 'variables' in value:
1267 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
1268 conditions.sort()
1269
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001270 value = value.copy()
1271 value['conditions'] = conditions
benrg@chromium.org609b7982013-02-07 16:44:46 +00001272 return value
1273
1274
1275def load_isolate_as_config(isolate_dir, value, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001276 """Parses one .isolate file and returns a Configs() instance.
1277
1278 |value| is the loaded dictionary that was defined in the gyp file.
1279
1280 The expected format is strict, anything diverting from the format below will
1281 throw an assert:
1282 {
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001283 'includes': [
1284 'foo.isolate',
1285 ],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001286 'conditions': [
benrg@chromium.org609b7982013-02-07 16:44:46 +00001287 ['OS=="vms" and foo=42', {
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001288 'variables': {
benrg@chromium.org609b7982013-02-07 16:44:46 +00001289 'command': [
1290 ...
1291 ],
1292 'isolate_dependency_tracked': [
1293 ...
1294 ],
1295 'isolate_dependency_untracked': [
1296 ...
1297 ],
1298 'read_only': False,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001299 },
1300 }],
1301 ...
1302 ],
1303 }
1304 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001305 value = convert_old_to_new_format(value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001306
benrg@chromium.org609b7982013-02-07 16:44:46 +00001307 variables_and_values = {}
1308 verify_root(value, variables_and_values)
1309 if variables_and_values:
1310 config_variables, config_values = zip(
1311 *sorted(variables_and_values.iteritems()))
1312 all_configs = list(itertools.product(*config_values))
1313 else:
1314 config_variables = None
1315 all_configs = []
1316
1317 isolate = Configs(file_comment)
1318
1319 # Add configuration-specific variables.
1320 for expr, then in value.get('conditions', []):
1321 configs = match_configs(expr, config_variables, all_configs)
1322 isolate.merge_dependencies(then['variables'], config_variables, configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001323
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001324 # Load the includes.
1325 for include in value.get('includes', []):
1326 if os.path.isabs(include):
1327 raise ExecutionError(
1328 'Failed to load configuration; absolute include path \'%s\'' %
1329 include)
1330 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
1331 with open(included_isolate, 'r') as f:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001332 included_isolate = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001333 os.path.dirname(included_isolate),
1334 eval_content(f.read()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001335 None)
1336 isolate = union(isolate, included_isolate)
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001337
benrg@chromium.org609b7982013-02-07 16:44:46 +00001338 return isolate
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001339
1340
benrg@chromium.org609b7982013-02-07 16:44:46 +00001341def load_isolate_for_config(isolate_dir, content, variables):
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001342 """Loads the .isolate file and returns the information unprocessed but
1343 filtered for the specific OS.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001344
1345 Returns the command, dependencies and read_only flag. The dependencies are
1346 fixed to use os.path.sep.
1347 """
1348 # Load the .isolate file, process its conditions, retrieve the command and
1349 # dependencies.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001350 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
1351 try:
1352 config = tuple(variables[var] for var in isolate.config_variables)
1353 except KeyError:
1354 raise ExecutionError(
1355 'These configuration variables were missing from the command line: %s' %
1356 ', '.join(sorted(set(isolate.config_variables) - set(variables))))
1357 config = isolate.by_config.get(config)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001358 if not config:
csharp@chromium.org2a3d7d52013-03-23 12:54:37 +00001359 raise ExecutionError('Failed to load configuration for (%s)' %
1360 ', '.join(isolate.config_variables))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001361 # Merge tracked and untracked variables, isolate.py doesn't care about the
1362 # trackability of the variables, only the build tool does.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001363 dependencies = [
1364 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1365 ]
1366 touched = [f.replace('/', os.path.sep) for f in config.touched]
1367 return config.command, dependencies, touched, config.read_only
1368
1369
maruel@chromium.orgdcdbfc82013-07-25 18:54:57 +00001370def save_isolated(isolated, data):
1371 """Writes one or multiple .isolated files.
1372
1373 Note: this reference implementation does not create child .isolated file so it
1374 always returns an empty list.
1375
1376 Returns the list of child isolated files that are included by |isolated|.
1377 """
1378 trace_inputs.write_json(isolated, data, True)
1379 return []
1380
1381
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001382def chromium_save_isolated(isolated, data, variables):
1383 """Writes one or many .isolated files.
1384
1385 This slightly increases the cold cache cost but greatly reduce the warm cache
1386 cost by splitting low-churn files off the master .isolated file. It also
1387 reduces overall isolateserver memcache consumption.
1388 """
1389 slaves = []
1390
1391 def extract_into_included_isolated(prefix):
1392 new_slave = {'files': {}, 'os': data['os']}
1393 for f in data['files'].keys():
1394 if f.startswith(prefix):
1395 new_slave['files'][f] = data['files'].pop(f)
1396 if new_slave['files']:
1397 slaves.append(new_slave)
1398
1399 # Split test/data/ in its own .isolated file.
1400 extract_into_included_isolated(os.path.join('test', 'data', ''))
1401
1402 # Split everything out of PRODUCT_DIR in its own .isolated file.
1403 if variables.get('PRODUCT_DIR'):
1404 extract_into_included_isolated(variables['PRODUCT_DIR'])
1405
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001406 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001407 for index, f in enumerate(slaves):
1408 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
1409 trace_inputs.write_json(slavepath, f, True)
1410 data.setdefault('includes', []).append(
1411 isolateserver_archive.sha1_file(slavepath))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001412 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001413
maruel@chromium.orgdcdbfc82013-07-25 18:54:57 +00001414 files.extend(save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001415 return files
1416
1417
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001418class Flattenable(object):
1419 """Represents data that can be represented as a json file."""
1420 MEMBERS = ()
1421
1422 def flatten(self):
1423 """Returns a json-serializable version of itself.
1424
1425 Skips None entries.
1426 """
1427 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1428 return dict((member, value) for member, value in items if value is not None)
1429
1430 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001431 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001432 """Loads a flattened version."""
1433 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001434 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001435 for member in out.MEMBERS:
1436 if member in data:
1437 # Access to a protected member XXX of a client class
1438 # pylint: disable=W0212
1439 out._load_member(member, data.pop(member))
1440 if data:
1441 raise ValueError(
1442 'Found unexpected entry %s while constructing an object %s' %
1443 (data, cls.__name__), data, cls.__name__)
1444 return out
1445
1446 def _load_member(self, member, value):
1447 """Loads a member into self."""
1448 setattr(self, member, value)
1449
1450 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001451 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001452 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001453 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001454 out = cls.load(trace_inputs.read_json(filename), *args, **kwargs)
1455 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001456 except (IOError, ValueError):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001457 # On failure, loads the default instance.
1458 out = cls(*args, **kwargs)
1459 logging.warn('Failed to load %s', filename)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001460 return out
1461
1462
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001463class SavedState(Flattenable):
1464 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001465
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001466 This file caches the items calculated by this script and is used to increase
1467 the performance of the script. This file is not loaded by run_isolated.py.
1468 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001469
1470 It is important to note that the 'files' dict keys are using native OS path
1471 separator instead of '/' used in .isolate file.
1472 """
1473 MEMBERS = (
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001474 # Cache of the processed command. This value is saved because .isolated
1475 # files are never loaded by isolate.py so it's the only way to load the
1476 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001477 'command',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001478 # Cache of the files found so the next run can skip sha1 calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001479 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001480 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001481 'isolate_file',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001482 # List of included .isolated files. Used to support/remember 'slave'
1483 # .isolated files. Relative path to isolated_basedir.
1484 'child_isolated_files',
1485 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001486 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001487 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001488 'relative_cwd',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001489 # GYP variables used to generate the .isolated file. Variables are saved so
1490 # a user can use isolate.py after building and the GYP variables are still
1491 # defined.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001492 'variables',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001493 )
1494
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001495 def __init__(self, isolated_basedir):
1496 """Creates an empty SavedState.
1497
1498 |isolated_basedir| is the directory where the .isolated and .isolated.state
1499 files are saved.
1500 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001501 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001502 assert os.path.isabs(isolated_basedir), isolated_basedir
1503 assert os.path.isdir(isolated_basedir), isolated_basedir
1504 self.isolated_basedir = isolated_basedir
1505
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001506 self.command = []
1507 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001508 self.isolate_file = None
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001509 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 self.read_only = None
1511 self.relative_cwd = None
benrg@chromium.org609b7982013-02-07 16:44:46 +00001512 self.variables = {'OS': get_flavor()}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001513
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001514 def update(self, isolate_file, variables):
1515 """Updates the saved state with new data to keep GYP variables and internal
1516 reference to the original .isolate file.
1517 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +00001518 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001519 # Convert back to a relative path. On Windows, if the isolate and
1520 # isolated files are on different drives, isolate_file will stay an absolute
1521 # path.
1522 isolate_file = safe_relpath(isolate_file, self.isolated_basedir)
1523
1524 # The same .isolate file should always be used to generate the .isolated and
1525 # .isolated.state.
1526 assert isolate_file == self.isolate_file or not self.isolate_file, (
1527 isolate_file, self.isolate_file)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001528 self.isolate_file = isolate_file
1529 self.variables.update(variables)
1530
1531 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
1532 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001533
1534 The new files in |infiles| are added to self.files dict but their sha1 is
1535 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001536 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001537 self.command = command
1538 # Add new files.
1539 for f in infiles:
1540 self.files.setdefault(f, {})
1541 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001542 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001543 # Prune extraneous files that are not a dependency anymore.
1544 for f in set(self.files).difference(set(infiles).union(touched)):
1545 del self.files[f]
1546 if read_only is not None:
1547 self.read_only = read_only
1548 self.relative_cwd = relative_cwd
1549
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001550 def to_isolated(self):
1551 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001552
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001553 https://code.google.com/p/swarming/wiki/IsolateDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001554 """
1555 def strip(data):
1556 """Returns a 'files' entry with only the whitelisted keys."""
1557 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
1558
1559 out = {
1560 'files': dict(
1561 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001562 'os': self.variables['OS'],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001563 }
1564 if self.command:
1565 out['command'] = self.command
1566 if self.read_only is not None:
1567 out['read_only'] = self.read_only
1568 if self.relative_cwd:
1569 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001570 return out
1571
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001572 @property
1573 def isolate_filepath(self):
1574 """Returns the absolute path of self.isolate_file."""
1575 return os.path.normpath(
1576 os.path.join(self.isolated_basedir, self.isolate_file))
1577
1578 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001579 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001580 def load(cls, data, isolated_basedir): # pylint: disable=W0221
1581 """Special case loading to disallow different OS.
1582
1583 It is not possible to load a .isolated.state files from a different OS, this
1584 file is saved in OS-specific format.
1585 """
1586 out = super(SavedState, cls).load(data, isolated_basedir)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001587 if 'os' in data:
1588 out.variables['OS'] = data['os']
1589 if out.variables['OS'] != get_flavor():
1590 raise run_isolated.ConfigError(
1591 'The .isolated.state file was created on another platform')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001592 # The .isolate file must be valid. It could be absolute on Windows if the
1593 # drive containing the .isolate and the drive containing the .isolated files
1594 # differ.
1595 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
1596 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001597 return out
1598
1599 def __str__(self):
1600 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001601 out += ' command: %s\n' % self.command
1602 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001603 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001604 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +00001605 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001606 out += ' child_isolated_files: %s\n' % self.child_isolated_files
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001607 out += ' variables: %s' % ''.join(
1608 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1609 out += ')'
1610 return out
1611
1612
1613class CompleteState(object):
1614 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001615 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001616 super(CompleteState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001617 assert os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001618 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001619 # Contains the data to ease developer's use-case but that is not strictly
1620 # necessary.
1621 self.saved_state = saved_state
1622
1623 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001624 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001625 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001626 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001627 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001628 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001629 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001630 SavedState.load_file(
1631 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001632
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001633 def load_isolate(self, cwd, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001634 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001635 .isolate file.
1636
1637 Processes the loaded data, deduce root_dir, relative_cwd.
1638 """
1639 # Make sure to not depend on os.getcwd().
1640 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.orga3da9122013-03-28 13:27:09 +00001641 isolate_file = trace_inputs.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001642 logging.info(
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001643 'CompleteState.load_isolate(%s, %s, %s, %s)',
1644 cwd, isolate_file, variables, ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001645 relative_base_dir = os.path.dirname(isolate_file)
1646
1647 # Processes the variables and update the saved state.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001648 variables = process_variables(cwd, variables, relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001649 self.saved_state.update(isolate_file, variables)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001650 variables = self.saved_state.variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001651
1652 with open(isolate_file, 'r') as f:
1653 # At that point, variables are not replaced yet in command and infiles.
1654 # infiles may contain directory entries and is in posix style.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001655 command, infiles, touched, read_only = load_isolate_for_config(
1656 os.path.dirname(isolate_file), f.read(), variables)
1657 command = [eval_variables(i, variables) for i in command]
1658 infiles = [eval_variables(f, variables) for f in infiles]
1659 touched = [eval_variables(f, variables) for f in touched]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001660 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +00001661 # form '../../foo/bar'. Note that path variables must be taken in account
1662 # too, add them as if they were input files.
1663 path_variables = [variables[v] for v in PATH_VARIABLES if v in variables]
1664 root_dir = determine_root_dir(
1665 relative_base_dir, infiles + touched + path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001666 # The relative directory is automatically determined by the relative path
1667 # between root_dir and the directory containing the .isolate file,
1668 # isolate_base_dir.
1669 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
benrg@chromium.org9ae72862013-02-11 05:05:51 +00001670 # Now that we know where the root is, check that the PATH_VARIABLES point
1671 # inside it.
1672 for i in PATH_VARIABLES:
1673 if i in variables:
1674 if not path_starts_with(
1675 root_dir, os.path.join(relative_base_dir, variables[i])):
1676 raise run_isolated.MappingError(
maruel@chromium.org75584e22013-06-20 01:40:24 +00001677 'Path variable %s=%r points outside the inferred root directory'
1678 ' %s' % (i, variables[i], root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001679 # Normalize the files based to root_dir. It is important to keep the
1680 # trailing os.path.sep at that step.
1681 infiles = [
1682 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1683 for f in infiles
1684 ]
1685 touched = [
1686 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1687 for f in touched
1688 ]
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001689 follow_symlinks = variables['OS'] != 'win'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001690 # Expand the directories by listing each file inside. Up to now, trailing
1691 # os.path.sep must be kept. Do not expand 'touched'.
1692 infiles = expand_directories_and_symlinks(
1693 root_dir,
1694 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001695 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001696 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +00001697 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001698
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001699 # If we ignore broken items then remove any missing touched items.
1700 if ignore_broken_items:
1701 original_touched_count = len(touched)
1702 touched = [touch for touch in touched if os.path.exists(touch)]
1703
1704 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +00001705 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001706 len(touched) - original_touched_count)
1707
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001708 # Finally, update the new data to be able to generate the foo.isolated file,
1709 # the file that is used by run_isolated.py.
1710 self.saved_state.update_isolated(
1711 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001712 logging.debug(self)
1713
maruel@chromium.org9268f042012-10-17 17:36:41 +00001714 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001715 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001716
maruel@chromium.org9268f042012-10-17 17:36:41 +00001717 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1718 file is tainted.
1719
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001720 See process_input() for more information.
1721 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001722 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001723 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001724 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001725 else:
1726 filepath = os.path.join(self.root_dir, infile)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001727 self.saved_state.files[infile] = process_input(
1728 filepath,
1729 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +00001730 self.saved_state.read_only,
1731 self.saved_state.variables['OS'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001732
1733 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001734 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001735 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001736 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001737 self.isolated_filepath,
1738 self.saved_state.to_isolated(),
1739 self.saved_state.variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001740 total_bytes = sum(
1741 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001742 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001743 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001744 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001745 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001746 logging.debug('Dumping to %s' % saved_state_file)
1747 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1748
1749 @property
1750 def root_dir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001751 """Returns the absolute path of the root_dir to reference the .isolate file
1752 via relative_cwd.
1753
1754 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
1755 to isolate_filepath.
1756 """
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001757 if not self.saved_state.isolate_file:
1758 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001759 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001760 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001761 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001762 root_dir = isolate_dir
1763 else:
1764 assert isolate_dir.endswith(self.saved_state.relative_cwd), (
1765 isolate_dir, self.saved_state.relative_cwd)
1766 # Walk back back to the root directory.
1767 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
1768 return trace_inputs.get_native_path_case(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001769
1770 @property
1771 def resultdir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001772 """Returns the absolute path containing the .isolated file.
1773
1774 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
1775 path as the value.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001776 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001777 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001778
1779 def __str__(self):
1780 def indent(data, indent_length):
1781 """Indents text."""
1782 spacing = ' ' * indent_length
1783 return ''.join(spacing + l for l in str(data).splitlines(True))
1784
1785 out = '%s(\n' % self.__class__.__name__
1786 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001787 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1788 return out
1789
1790
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001791def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001792 """Loads a CompleteState.
1793
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001794 This includes data from .isolate and .isolated.state files. Never reads the
1795 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001796
1797 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001798 options: Options instance generated with OptionParserIsolate. For either
1799 options.isolate and options.isolated, if the value is set, it is an
1800 absolute path.
1801 cwd: base directory to be used when loading the .isolate file.
1802 subdir: optional argument to only process file in the subdirectory, relative
1803 to CompleteState.root_dir.
1804 skip_update: Skip trying to load the .isolate file and processing the
1805 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001806 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001807 assert not options.isolate or os.path.isabs(options.isolate)
1808 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.orga3da9122013-03-28 13:27:09 +00001809 cwd = trace_inputs.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001810 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001811 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001812 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001813 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001814 else:
1815 # Constructs a dummy object that cannot be saved. Useful for temporary
1816 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001817 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001818
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001819 if not options.isolate:
1820 if not complete_state.saved_state.isolate_file:
1821 if not skip_update:
1822 raise ExecutionError('A .isolate file is required.')
1823 isolate = None
1824 else:
1825 isolate = complete_state.saved_state.isolate_filepath
1826 else:
1827 isolate = options.isolate
1828 if complete_state.saved_state.isolate_file:
1829 rel_isolate = safe_relpath(
1830 options.isolate, complete_state.saved_state.isolated_basedir)
1831 if rel_isolate != complete_state.saved_state.isolate_file:
1832 raise ExecutionError(
1833 '%s and %s do not match.' % (
1834 options.isolate, complete_state.saved_state.isolate_file))
1835
1836 if not skip_update:
1837 # Then load the .isolate and expands directories.
1838 complete_state.load_isolate(
1839 cwd, isolate, options.variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001840
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001841 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001842 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001843 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001844 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1845 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001846
1847 if not skip_update:
1848 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001849 return complete_state
1850
1851
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001852def read_trace_as_isolate_dict(complete_state, trace_blacklist):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001853 """Reads a trace and returns the .isolate dictionary.
1854
1855 Returns exceptions during the log parsing so it can be re-raised.
1856 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001857 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001858 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001859 if not os.path.isfile(logfile):
1860 raise ExecutionError(
1861 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1862 try:
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001863 data = api.parse_log(logfile, trace_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001864 exceptions = [i['exception'] for i in data if 'exception' in i]
1865 results = (i['results'] for i in data if 'results' in i)
1866 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1867 files = set(sum((result.existent for result in results_stripped), []))
1868 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001869 value = generate_isolate(
1870 tracked,
1871 [],
1872 touched,
1873 complete_state.root_dir,
1874 complete_state.saved_state.variables,
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001875 complete_state.saved_state.relative_cwd,
1876 trace_blacklist)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001877 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001878 except trace_inputs.TracingFailure, e:
1879 raise ExecutionError(
1880 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001881 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001882
1883
1884def print_all(comment, data, stream):
1885 """Prints a complete .isolate file and its top-level file comment into a
1886 stream.
1887 """
1888 if comment:
1889 stream.write(comment)
1890 pretty_print(data, stream)
1891
1892
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001893def merge(complete_state, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001894 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001895 value, exceptions = read_trace_as_isolate_dict(
1896 complete_state, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001897
1898 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001899 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001900 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001901 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001902 prev_config = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001903 isolate_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001904 eval_content(prev_content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001905 extract_comment(prev_content))
1906 new_config = load_isolate_as_config(isolate_dir, value, '')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001907 config = union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001908 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001909 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001910 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001911 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001912 if exceptions:
1913 # It got an exception, raise the first one.
1914 raise \
1915 exceptions[0][0], \
1916 exceptions[0][1], \
1917 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001918
1919
maruel@chromium.orge5322512013-08-19 20:17:57 +00001920def CMDcheck(parser, args):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001921 """Checks that all the inputs are present and generates .isolated."""
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.org8abec8b2013-04-16 19:34:20 +00001926 complete_state = load_complete_state(
1927 options, os.getcwd(), options.subdir, False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001928
1929 # Nothing is done specifically. Just store the result and state.
1930 complete_state.save_files()
1931 return 0
1932
1933
maruel@chromium.orge5322512013-08-19 20:17:57 +00001934def CMDhashtable(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001935 """Creates a hash table content addressed object store.
1936
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001937 All the files listed in the .isolated file are put in the output directory
1938 with the file name being the sha-1 of the file's content.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001939 """
maruel@chromium.org9268f042012-10-17 17:36:41 +00001940 parser.add_option('--subdir', help='Filters to a subdirectory')
1941 options, args = parser.parse_args(args)
1942 if args:
1943 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001944
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001945 with run_isolated.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001946 success = False
1947 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001948 complete_state = load_complete_state(
1949 options, os.getcwd(), options.subdir, False)
1950 if not options.outdir:
1951 options.outdir = os.path.join(
1952 os.path.dirname(complete_state.isolated_filepath), 'hashtable')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001953 # Make sure that complete_state isn't modified until save_files() is
1954 # called, because any changes made to it here will propagate to the files
1955 # created (which is probably not intended).
1956 complete_state.save_files()
1957
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001958 infiles = complete_state.saved_state.files
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001959 # Add all the .isolated files.
maruel@chromium.org87f11962013-04-10 21:27:28 +00001960 isolated_hash = []
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001961 isolated_files = [
1962 options.isolated,
1963 ] + complete_state.saved_state.child_isolated_files
1964 for item in isolated_files:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001965 item_path = os.path.join(
1966 os.path.dirname(complete_state.isolated_filepath), item)
maruel@chromium.org87f11962013-04-10 21:27:28 +00001967 # Do not use isolateserver_archive.sha1_file() here because the file is
1968 # likely smallish (under 500kb) and its file size is needed.
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001969 with open(item_path, 'rb') as f:
1970 content = f.read()
maruel@chromium.org87f11962013-04-10 21:27:28 +00001971 isolated_hash.append(hashlib.sha1(content).hexdigest())
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001972 isolated_metadata = {
maruel@chromium.org87f11962013-04-10 21:27:28 +00001973 'h': isolated_hash[-1],
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001974 's': len(content),
1975 'priority': '0'
1976 }
1977 infiles[item_path] = isolated_metadata
1978
1979 logging.info('Creating content addressed object store with %d item',
1980 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001981
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001982 if is_url(options.outdir):
maruel@chromium.orgc6f90062012-11-07 18:32:22 +00001983 isolateserver_archive.upload_sha1_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001984 base_url=options.outdir,
1985 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00001986 infiles=infiles,
1987 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001988 else:
1989 recreate_tree(
1990 outdir=options.outdir,
1991 indir=complete_state.root_dir,
1992 infiles=infiles,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00001993 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001994 as_sha1=True)
1995 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001996 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001997 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001998 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001999 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002000 if not success and os.path.isfile(options.isolated):
2001 os.remove(options.isolated)
maruel@chromium.org87f11962013-04-10 21:27:28 +00002002 return not success
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002003
2004
maruel@chromium.orge5322512013-08-19 20:17:57 +00002005def CMDmerge(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002006 """Reads and merges the data from the trace back into the original .isolate.
2007
2008 Ignores --outdir.
2009 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002010 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002011 add_trace_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00002012 options, args = parser.parse_args(args)
2013 if args:
2014 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002015 complete_state = load_complete_state(options, os.getcwd(), None, False)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002016 blacklist = trace_inputs.gen_blacklist(options.trace_blacklist)
2017 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002018 return 0
2019
2020
maruel@chromium.orge5322512013-08-19 20:17:57 +00002021def CMDread(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002022 """Reads the trace file generated with command 'trace'.
2023
2024 Ignores --outdir.
2025 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002026 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002027 add_trace_option(parser)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002028 parser.add_option(
2029 '--skip-refresh', action='store_true',
2030 help='Skip reading .isolate file and do not refresh the sha1 of '
2031 'dependencies')
maruel@chromium.org9268f042012-10-17 17:36:41 +00002032 options, args = parser.parse_args(args)
2033 if args:
2034 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002035 complete_state = load_complete_state(
2036 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002037 blacklist = trace_inputs.gen_blacklist(options.trace_blacklist)
2038 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002039 pretty_print(value, sys.stdout)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00002040 if exceptions:
2041 # It got an exception, raise the first one.
2042 raise \
2043 exceptions[0][0], \
2044 exceptions[0][1], \
2045 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002046 return 0
2047
2048
maruel@chromium.orge5322512013-08-19 20:17:57 +00002049def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002050 """Creates a directory with all the dependencies mapped into it.
2051
2052 Useful to test manually why a test is failing. The target executable is not
2053 run.
2054 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002055 parser.require_isolated = False
maruel@chromium.org9268f042012-10-17 17:36:41 +00002056 options, args = parser.parse_args(args)
2057 if args:
2058 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002059 complete_state = load_complete_state(options, os.getcwd(), None, False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002060
2061 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002062 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002063 'isolate', complete_state.root_dir)
2064 else:
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002065 if is_url(options.outdir):
2066 raise ExecutionError('Can\'t use url for --outdir with mode remap')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002067 if not os.path.isdir(options.outdir):
2068 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00002069 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002070 if len(os.listdir(options.outdir)):
2071 raise ExecutionError('Can\'t remap in a non-empty directory')
2072 recreate_tree(
2073 outdir=options.outdir,
2074 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002075 infiles=complete_state.saved_state.files,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00002076 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002077 as_sha1=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002078 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002079 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002080
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002081 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002082 complete_state.save_files()
2083 return 0
2084
2085
maruel@chromium.orge5322512013-08-19 20:17:57 +00002086def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002087 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00002088 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002089 options, args = parser.parse_args(args)
2090 if args:
2091 parser.error('Unsupported argument: %s' % args)
2092
2093 if options.isolated:
2094 # Load the previous state if it was present. Namely, "foo.isolated.state".
2095 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002096 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002097 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002098 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002099 if not isolate:
2100 raise ExecutionError('A .isolate file is required.')
2101 with open(isolate, 'r') as f:
2102 content = f.read()
2103 config = load_isolate_as_config(
2104 os.path.dirname(os.path.abspath(isolate)),
2105 eval_content(content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00002106 extract_comment(content))
2107 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002108 print('Updating %s' % isolate)
2109 with open(isolate, 'wb') as f:
2110 print_all(config.file_comment, data, f)
2111 return 0
2112
2113
maruel@chromium.orge5322512013-08-19 20:17:57 +00002114def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002115 """Runs the test executable in an isolated (temporary) directory.
2116
2117 All the dependencies are mapped into the temporary directory and the
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002118 directory is cleaned up after the target exits. Warning: if --outdir is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002119 specified, it is deleted upon exit.
2120
2121 Argument processing stops at the first non-recognized argument and these
2122 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002123 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002124 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002125 parser.require_isolated = False
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002126 parser.add_option(
2127 '--skip-refresh', action='store_true',
2128 help='Skip reading .isolate file and do not refresh the sha1 of '
2129 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002130 parser.enable_interspersed_args()
2131 options, args = parser.parse_args(args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002132 complete_state = load_complete_state(
2133 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002134 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002135 if not cmd:
2136 raise ExecutionError('No command to run')
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002137 if options.outdir and is_url(options.outdir):
2138 raise ExecutionError('Can\'t use url for --outdir with mode run')
2139
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002140 cmd = trace_inputs.fix_python_path(cmd)
2141 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002142 root_dir = complete_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002143 if not options.outdir:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002144 if not os.path.isabs(root_dir):
2145 root_dir = os.path.join(os.path.dirname(options.isolated), root_dir)
2146 options.outdir = run_isolated.make_temp_dir('isolate', root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002147 else:
2148 if not os.path.isdir(options.outdir):
2149 os.makedirs(options.outdir)
2150 recreate_tree(
2151 outdir=options.outdir,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002152 indir=root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002153 infiles=complete_state.saved_state.files,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00002154 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002155 as_sha1=False)
2156 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002157 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002158 if not os.path.isdir(cwd):
2159 # It can happen when no files are mapped from the directory containing the
2160 # .isolate file. But the directory must exist to be the current working
2161 # directory.
2162 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002163 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002164 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002165 logging.info('Running %s, cwd=%s' % (cmd, cwd))
2166 result = subprocess.call(cmd, cwd=cwd)
2167 finally:
2168 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002169 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002170
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002171 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002172 complete_state.save_files()
2173 return result
2174
2175
maruel@chromium.orge5322512013-08-19 20:17:57 +00002176def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002177 """Traces the target using trace_inputs.py.
2178
2179 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002180 its child processes access. Then the 'merge' command can be used to generate
2181 an updated .isolate file out of it or the 'read' command to print it out to
2182 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002183
2184 Argument processing stops at the first non-recognized argument and these
2185 arguments are appended to the command line of the target to run. For example,
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002186 use: isolate.py --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002187 """
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002188 add_trace_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002189 parser.enable_interspersed_args()
2190 parser.add_option(
2191 '-m', '--merge', action='store_true',
2192 help='After tracing, merge the results back in the .isolate file')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002193 parser.add_option(
2194 '--skip-refresh', action='store_true',
2195 help='Skip reading .isolate file and do not refresh the sha1 of '
2196 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002197 options, args = parser.parse_args(args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002198 complete_state = load_complete_state(
2199 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002200 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002201 if not cmd:
2202 raise ExecutionError('No command to run')
2203 cmd = trace_inputs.fix_python_path(cmd)
2204 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002205 unicode(complete_state.root_dir),
2206 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00002207 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
2208 if not os.path.isfile(cmd[0]):
2209 raise ExecutionError(
2210 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002211 logging.info('Running %s, cwd=%s' % (cmd, cwd))
2212 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002213 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002214 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002215 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002216 try:
2217 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002218 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002219 cmd,
2220 cwd,
2221 'default',
2222 True)
2223 except trace_inputs.TracingFailure, e:
2224 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
2225
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002226 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002227 logging.error(
2228 'Tracer exited with %d, which means the tests probably failed so the '
2229 'trace is probably incomplete.', result)
2230 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002231
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002232 complete_state.save_files()
2233
2234 if options.merge:
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002235 blacklist = trace_inputs.gen_blacklist(options.trace_blacklist)
2236 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002237
2238 return result
2239
2240
maruel@chromium.org712454d2013-04-04 17:52:34 +00002241def _process_variable_arg(_option, _opt, _value, parser):
2242 if not parser.rargs:
2243 raise optparse.OptionValueError(
2244 'Please use --variable FOO=BAR or --variable FOO BAR')
2245 k = parser.rargs.pop(0)
2246 if '=' in k:
2247 parser.values.variables.append(tuple(k.split('=', 1)))
2248 else:
2249 if not parser.rargs:
2250 raise optparse.OptionValueError(
2251 'Please use --variable FOO=BAR or --variable FOO BAR')
2252 v = parser.rargs.pop(0)
2253 parser.values.variables.append((k, v))
2254
2255
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002256def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002257 """Adds --isolated and --variable to an OptionParser."""
2258 parser.add_option(
2259 '-s', '--isolated',
2260 metavar='FILE',
2261 help='.isolated file to generate or read')
2262 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002263 parser.add_option(
2264 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002265 dest='isolated',
2266 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002267 default_variables = [('OS', get_flavor())]
2268 if sys.platform in ('win32', 'cygwin'):
2269 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
2270 else:
2271 default_variables.append(('EXECUTABLE_SUFFIX', ''))
2272 parser.add_option(
2273 '-V', '--variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00002274 action='callback',
2275 callback=_process_variable_arg,
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002276 default=default_variables,
2277 dest='variables',
2278 metavar='FOO BAR',
2279 help='Variables to process in the .isolate file, default: %default. '
2280 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002281 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002282
2283
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002284def add_trace_option(parser):
2285 """Adds --trace-blacklist to the parser."""
2286 parser.add_option(
2287 '--trace-blacklist',
2288 action='append', default=list(DEFAULT_BLACKLIST),
2289 help='List of regexp to use as blacklist filter for files to consider '
2290 'important, not to be confused with --blacklist which blacklists '
2291 'test case.')
2292
2293
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002294def parse_isolated_option(parser, options, cwd, require_isolated):
2295 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002296 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002297 options.isolated = os.path.normpath(
2298 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002299 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00002300 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002301 if options.isolated and not options.isolated.endswith('.isolated'):
2302 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002303
2304
2305def parse_variable_option(options):
2306 """Processes --variable."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002307 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
2308 # but it wouldn't be backward compatible.
2309 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002310 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002311 try:
2312 return int(s)
2313 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002314 return s.decode('utf-8')
benrg@chromium.org609b7982013-02-07 16:44:46 +00002315 options.variables = dict((k, try_make_int(v)) for k, v in options.variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002316
2317
maruel@chromium.orge5322512013-08-19 20:17:57 +00002318class OptionParserIsolate(trace_inputs.OptionParserWithLogging):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002319 """Adds automatic --isolate, --isolated, --out and --variable handling."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00002320 # Set it to False if it is not required, e.g. it can be passed on but do not
2321 # fail if not given.
2322 require_isolated = True
2323
2324 def __init__(self, **kwargs):
2325 trace_inputs.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00002326 self,
2327 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
2328 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002329 group = optparse.OptionGroup(self, "Common options")
2330 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002331 '-i', '--isolate',
2332 metavar='FILE',
2333 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002334 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002335 group.add_option(
2336 '-o', '--outdir', metavar='DIR',
2337 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002338 'Defaults: run|remap: a /tmp subdirectory, others: '
2339 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00002340 group.add_option(
2341 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002342 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
2343 help='Indicates that invalid entries in the isolated file to be '
2344 'only be logged and not stop processing. Defaults to True if '
2345 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002346 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002347
2348 def parse_args(self, *args, **kwargs):
2349 """Makes sure the paths make sense.
2350
2351 On Windows, / and \ are often mixed together in a path.
2352 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002353 options, args = trace_inputs.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002354 self, *args, **kwargs)
2355 if not self.allow_interspersed_args and args:
2356 self.error('Unsupported argument: %s' % args)
2357
maruel@chromium.orga3da9122013-03-28 13:27:09 +00002358 cwd = trace_inputs.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002359 parse_isolated_option(self, options, cwd, self.require_isolated)
2360 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002361
2362 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002363 # TODO(maruel): Work with non-ASCII.
2364 # The path must be in native path case for tracing purposes.
2365 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
2366 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
2367 options.isolate = trace_inputs.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002368
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002369 if options.outdir and not is_url(options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002370 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
2371 # outdir doesn't need native path case since tracing is never done from
2372 # there.
2373 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002374
2375 return options, args
2376
2377
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002378def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00002379 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002380 try:
maruel@chromium.orge5322512013-08-19 20:17:57 +00002381 return dispatcher.execute(OptionParserIsolate(), argv)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002382 except (
2383 ExecutionError,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002384 run_isolated.MappingError,
2385 run_isolated.ConfigError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002386 sys.stderr.write('\nError: ')
2387 sys.stderr.write(str(e))
2388 sys.stderr.write('\n')
2389 return 1
2390
2391
2392if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00002393 fix_encoding.fix_encoding()
2394 trace_inputs.disable_buffering()
2395 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002396 sys.exit(main(sys.argv[1:]))