blob: 3fb6768ac3b9fc7f761d14727b4ee2d7342832c5 [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.orgfb78d432013-08-28 21:22:40 +000029import isolateserver
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000030import run_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000031import trace_inputs
32
33# Import here directly so isolate is easier to use as a library.
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000034from run_isolated import get_flavor
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000035
maruel@chromium.orge5322512013-08-19 20:17:57 +000036from third_party import colorama
37from third_party.depot_tools import fix_encoding
38from third_party.depot_tools import subcommand
39
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000040from utils import tools
maruel@chromium.orgb61979a2013-08-29 15:18:51 +000041from utils import short_expression_finder
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000042
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000043
maruel@chromium.org29029882013-08-30 12:15:40 +000044__version__ = '0.1.1'
maruel@chromium.org3d671992013-08-20 00:38:27 +000045
46
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000047PATH_VARIABLES = ('DEPTH', 'PRODUCT_DIR')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000048
49# Files that should be 0-length when mapped.
50KEY_TOUCHED = 'isolate_dependency_touched'
51# Files that should be tracked by the build tool.
52KEY_TRACKED = 'isolate_dependency_tracked'
53# Files that should not be tracked by the build tool.
54KEY_UNTRACKED = 'isolate_dependency_untracked'
55
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000056
57class ExecutionError(Exception):
58 """A generic error occurred."""
59 def __str__(self):
60 return self.args[0]
61
62
63### Path handling code.
64
65
maruel@chromium.org3683afe2013-07-27 00:09:27 +000066DEFAULT_BLACKLIST = (
67 # Temporary vim or python files.
68 r'^.+\.(?:pyc|swp)$',
69 # .git or .svn directory.
70 r'^(?:.+' + re.escape(os.path.sep) + r'|)\.(?:git|svn)$',
71)
72
73
74# Chromium-specific.
75DEFAULT_BLACKLIST += (
76 r'^.+\.(?:run_test_cases)$',
77 r'^(?:.+' + re.escape(os.path.sep) + r'|)testserver\.log$',
78)
79
80
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000081def relpath(path, root):
82 """os.path.relpath() that keeps trailing os.path.sep."""
83 out = os.path.relpath(path, root)
84 if path.endswith(os.path.sep):
85 out += os.path.sep
86 return out
87
88
maruel@chromium.org8abec8b2013-04-16 19:34:20 +000089def safe_relpath(filepath, basepath):
90 """Do not throw on Windows when filepath and basepath are on different drives.
91
92 Different than relpath() above since this one doesn't keep the trailing
93 os.path.sep and it swallows exceptions on Windows and return the original
94 absolute path in the case of different drives.
95 """
96 try:
97 return os.path.relpath(filepath, basepath)
98 except ValueError:
99 assert sys.platform == 'win32'
100 return filepath
101
102
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000103def normpath(path):
104 """os.path.normpath() that keeps trailing os.path.sep."""
105 out = os.path.normpath(path)
106 if path.endswith(os.path.sep):
107 out += os.path.sep
108 return out
109
110
111def posix_relpath(path, root):
112 """posix.relpath() that keeps trailing slash."""
113 out = posixpath.relpath(path, root)
114 if path.endswith('/'):
115 out += '/'
116 return out
117
118
119def cleanup_path(x):
120 """Cleans up a relative path. Converts any os.path.sep to '/' on Windows."""
121 if x:
122 x = x.rstrip(os.path.sep).replace(os.path.sep, '/')
123 if x == '.':
124 x = ''
125 if x:
126 x += '/'
127 return x
128
129
maruel@chromium.orgb9520b02013-03-13 18:00:03 +0000130def is_url(path):
131 return bool(re.match(r'^https?://.+$', path))
132
133
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000134def path_starts_with(prefix, path):
135 """Returns true if the components of the path |prefix| are the same as the
136 initial components of |path| (or all of the components of |path|). The paths
137 must be absolute.
138 """
139 assert os.path.isabs(prefix) and os.path.isabs(path)
140 prefix = os.path.normpath(prefix)
141 path = os.path.normpath(path)
142 assert prefix == trace_inputs.get_native_path_case(prefix), prefix
143 assert path == trace_inputs.get_native_path_case(path), path
144 prefix = prefix.rstrip(os.path.sep) + os.path.sep
145 path = path.rstrip(os.path.sep) + os.path.sep
146 return path.startswith(prefix)
147
148
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000149def fix_native_path_case(root, path):
150 """Ensures that each component of |path| has the proper native case by
151 iterating slowly over the directory elements of |path|."""
152 native_case_path = root
153 for raw_part in path.split(os.sep):
154 if not raw_part or raw_part == '.':
155 break
156
157 part = trace_inputs.find_item_native_case(native_case_path, raw_part)
158 if not part:
159 raise run_isolated.MappingError(
160 'Input file %s doesn\'t exist' %
161 os.path.join(native_case_path, raw_part))
162 native_case_path = os.path.join(native_case_path, part)
163
164 return os.path.normpath(native_case_path)
165
166
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000167def expand_symlinks(indir, relfile):
168 """Follows symlinks in |relfile|, but treating symlinks that point outside the
169 build tree as if they were ordinary directories/files. Returns the final
170 symlink-free target and a list of paths to symlinks encountered in the
171 process.
172
173 The rule about symlinks outside the build tree is for the benefit of the
174 Chromium OS ebuild, which symlinks the output directory to an unrelated path
175 in the chroot.
176
177 Fails when a directory loop is detected, although in theory we could support
178 that case.
179 """
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000180 is_directory = relfile.endswith(os.path.sep)
181 done = indir
182 todo = relfile.strip(os.path.sep)
183 symlinks = []
184
185 while todo:
186 pre_symlink, symlink, post_symlink = trace_inputs.split_at_symlink(
187 done, todo)
188 if not symlink:
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000189 todo = fix_native_path_case(done, todo)
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000190 done = os.path.join(done, todo)
191 break
192 symlink_path = os.path.join(done, pre_symlink, symlink)
193 post_symlink = post_symlink.lstrip(os.path.sep)
194 # readlink doesn't exist on Windows.
195 # pylint: disable=E1101
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000196 target = os.path.normpath(os.path.join(done, pre_symlink))
197 symlink_target = os.readlink(symlink_path)
maruel@chromium.org28c19672013-04-29 18:51:09 +0000198 if os.path.isabs(symlink_target):
199 # Absolute path are considered a normal directories. The use case is
200 # generally someone who puts the output directory on a separate drive.
201 target = symlink_target
202 else:
203 # The symlink itself could be using the wrong path case.
204 target = fix_native_path_case(target, symlink_target)
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000205
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000206 if not os.path.exists(target):
207 raise run_isolated.MappingError(
208 'Symlink target doesn\'t exist: %s -> %s' % (symlink_path, target))
209 target = trace_inputs.get_native_path_case(target)
210 if not path_starts_with(indir, target):
211 done = symlink_path
212 todo = post_symlink
213 continue
214 if path_starts_with(target, symlink_path):
215 raise run_isolated.MappingError(
216 'Can\'t map recursive symlink reference %s -> %s' %
217 (symlink_path, target))
218 logging.info('Found symlink: %s -> %s', symlink_path, target)
219 symlinks.append(os.path.relpath(symlink_path, indir))
220 # Treat the common prefix of the old and new paths as done, and start
221 # scanning again.
222 target = target.split(os.path.sep)
223 symlink_path = symlink_path.split(os.path.sep)
224 prefix_length = 0
225 for target_piece, symlink_path_piece in zip(target, symlink_path):
226 if target_piece == symlink_path_piece:
227 prefix_length += 1
228 else:
229 break
230 done = os.path.sep.join(target[:prefix_length])
231 todo = os.path.join(
232 os.path.sep.join(target[prefix_length:]), post_symlink)
233
234 relfile = os.path.relpath(done, indir)
235 relfile = relfile.rstrip(os.path.sep) + is_directory * os.path.sep
236 return relfile, symlinks
237
238
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000239def expand_directory_and_symlink(indir, relfile, blacklist, follow_symlinks):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000240 """Expands a single input. It can result in multiple outputs.
241
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000242 This function is recursive when relfile is a directory.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000243
244 Note: this code doesn't properly handle recursive symlink like one created
245 with:
246 ln -s .. foo
247 """
248 if os.path.isabs(relfile):
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 absolute path %s' % relfile)
251
252 infile = normpath(os.path.join(indir, relfile))
253 if not infile.startswith(indir):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000254 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000255 'Can\'t map file %s outside %s' % (infile, indir))
256
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000257 filepath = os.path.join(indir, relfile)
258 native_filepath = trace_inputs.get_native_path_case(filepath)
259 if filepath != native_filepath:
maruel@chromium.org59bb2a32013-03-21 17:08:39 +0000260 # Special case './'.
261 if filepath != native_filepath + '.' + os.path.sep:
maruel@chromium.org7f66a982013-06-06 15:58:59 +0000262 # Give up enforcing strict path case on OSX. Really, it's that sad. The
263 # case where it happens is very specific and hard to reproduce:
264 # get_native_path_case(
265 # u'Foo.framework/Versions/A/Resources/Something.nib') will return
266 # u'Foo.framework/Versions/A/resources/Something.nib', e.g. lowercase 'r'.
267 #
268 # Note that this is really something deep in OSX because running
269 # ls Foo.framework/Versions/A
270 # will print out 'Resources', while trace_inputs.get_native_path_case()
271 # returns a lower case 'r'.
272 #
273 # So *something* is happening under the hood resulting in the command 'ls'
274 # and Carbon.File.FSPathMakeRef('path').FSRefMakePath() to disagree. We
275 # have no idea why.
276 if sys.platform != 'darwin':
277 raise run_isolated.MappingError(
278 'File path doesn\'t equal native file path\n%s != %s' %
279 (filepath, native_filepath))
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000280
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000281 symlinks = []
282 if follow_symlinks:
283 relfile, symlinks = expand_symlinks(indir, relfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000284
285 if relfile.endswith(os.path.sep):
286 if not os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000287 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000288 '%s is not a directory but ends with "%s"' % (infile, os.path.sep))
289
maruel@chromium.org59bb2a32013-03-21 17:08:39 +0000290 # Special case './'.
291 if relfile.startswith('.' + os.path.sep):
292 relfile = relfile[2:]
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000293 outfiles = symlinks
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000294 try:
295 for filename in os.listdir(infile):
296 inner_relfile = os.path.join(relfile, filename)
297 if blacklist(inner_relfile):
298 continue
299 if os.path.isdir(os.path.join(indir, inner_relfile)):
300 inner_relfile += os.path.sep
301 outfiles.extend(
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000302 expand_directory_and_symlink(indir, inner_relfile, blacklist,
303 follow_symlinks))
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000304 return outfiles
305 except OSError as e:
maruel@chromium.org1cd786e2013-04-26 18:48:40 +0000306 raise run_isolated.MappingError(
307 'Unable to iterate over directory %s.\n%s' % (infile, e))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000308 else:
309 # Always add individual files even if they were blacklisted.
310 if os.path.isdir(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000311 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000312 'Input directory %s must have a trailing slash' % infile)
313
314 if not os.path.isfile(infile):
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000315 raise run_isolated.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000316 'Input file %s doesn\'t exist' % infile)
317
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000318 return symlinks + [relfile]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000319
320
csharp@chromium.org01856802012-11-12 17:48:13 +0000321def expand_directories_and_symlinks(indir, infiles, blacklist,
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000322 follow_symlinks, ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000323 """Expands the directories and the symlinks, applies the blacklist and
324 verifies files exist.
325
326 Files are specified in os native path separator.
327 """
328 outfiles = []
329 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +0000330 try:
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000331 outfiles.extend(expand_directory_and_symlink(indir, relfile, blacklist,
332 follow_symlinks))
csharp@chromium.org01856802012-11-12 17:48:13 +0000333 except run_isolated.MappingError as e:
334 if ignore_broken_items:
335 logging.info('warning: %s', e)
336 else:
337 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000338 return outfiles
339
340
341def recreate_tree(outdir, indir, infiles, action, as_sha1):
342 """Creates a new tree with only the input files in it.
343
344 Arguments:
345 outdir: Output directory to create the files in.
346 indir: Root directory the infiles are based in.
347 infiles: dict of files to map from |indir| to |outdir|.
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000348 action: One of accepted action of run_isolated.link_file().
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000349 as_sha1: Output filename is the sha1 instead of relfile.
350 """
351 logging.info(
352 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_sha1=%s)' %
353 (outdir, indir, len(infiles), action, as_sha1))
354
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000355 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000356 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000357 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000358 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000359
360 for relfile, metadata in infiles.iteritems():
361 infile = os.path.join(indir, relfile)
362 if as_sha1:
363 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000364 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000365 # Skip links when storing a hashtable.
366 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000367 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000368 if os.path.isfile(outfile):
369 # Just do a quick check that the file size matches. No need to stat()
370 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000371 if not 's' in metadata:
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000372 raise run_isolated.MappingError(
373 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000374 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000375 continue
376 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000377 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000378 os.remove(outfile)
379 else:
380 outfile = os.path.join(outdir, relfile)
381 outsubdir = os.path.dirname(outfile)
382 if not os.path.isdir(outsubdir):
383 os.makedirs(outsubdir)
384
385 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000386 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000387 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000388 if 'l' in metadata:
389 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000390 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000391 # symlink doesn't exist on Windows.
392 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000393 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000394 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000395
396
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000397def process_input(filepath, prevdict, read_only, flavor):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000398 """Processes an input file, a dependency, and return meta data about it.
399
400 Arguments:
401 - filepath: File to act on.
402 - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
403 to skip recalculating the hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000404 - read_only: If True, the file mode is manipulated. In practice, only save
405 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
406 windows, mode is not set since all files are 'executable' by
407 default.
408
409 Behaviors:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000410 - Retrieves the file mode, file size, file timestamp, file link
411 destination if it is a file link and calcultate the SHA-1 of the file's
412 content if the path points to a file and not a symlink.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000413 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000414 out = {}
415 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000416 # if prevdict.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000417 # # The file's content is ignored. Skip the time and hard code mode.
418 # if get_flavor() != 'win':
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000419 # out['m'] = stat.S_IRUSR | stat.S_IRGRP
420 # out['s'] = 0
421 # out['h'] = SHA_1_NULL
422 # out['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000423 # return out
424
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000425 # Always check the file stat and check if it is a link. The timestamp is used
426 # to know if the file's content/symlink destination should be looked into.
427 # E.g. only reuse from prevdict if the timestamp hasn't changed.
428 # There is the risk of the file's timestamp being reset to its last value
429 # manually while its content changed. We don't protect against that use case.
430 try:
431 filestats = os.lstat(filepath)
432 except OSError:
433 # The file is not present.
434 raise run_isolated.MappingError('%s is missing' % filepath)
435 is_link = stat.S_ISLNK(filestats.st_mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000436
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000437 if flavor != 'win':
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000438 # Ignore file mode on Windows since it's not really useful there.
439 filemode = stat.S_IMODE(filestats.st_mode)
440 # Remove write access for group and all access to 'others'.
441 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
442 if read_only:
443 filemode &= ~stat.S_IWUSR
444 if filemode & stat.S_IXUSR:
445 filemode |= stat.S_IXGRP
446 else:
447 filemode &= ~stat.S_IXGRP
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000448 out['m'] = filemode
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000449
450 # Used to skip recalculating the hash or link destination. Use the most recent
451 # update time.
452 # TODO(maruel): Save it in the .state file instead of .isolated so the
453 # .isolated file is deterministic.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000454 out['t'] = int(round(filestats.st_mtime))
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000455
456 if not is_link:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000457 out['s'] = filestats.st_size
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000458 # If the timestamp wasn't updated and the file size is still the same, carry
459 # on the sha-1.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000460 if (prevdict.get('t') == out['t'] and
461 prevdict.get('s') == out['s']):
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000462 # Reuse the previous hash if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000463 out['h'] = prevdict.get('h')
464 if not out.get('h'):
maruel@chromium.orgfb78d432013-08-28 21:22:40 +0000465 out['h'] = isolateserver.sha1_file(filepath)
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000466 else:
467 # If the timestamp wasn't updated, carry on the link destination.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000468 if prevdict.get('t') == out['t']:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000469 # Reuse the previous link destination if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000470 out['l'] = prevdict.get('l')
471 if out.get('l') is None:
maruel@chromium.org8d159e32013-04-18 15:29:50 +0000472 # The link could be in an incorrect path case. In practice, this only
473 # happen on OSX on case insensitive HFS.
474 # TODO(maruel): It'd be better if it was only done once, in
475 # expand_directory_and_symlink(), so it would not be necessary to do again
476 # here.
477 symlink_value = os.readlink(filepath) # pylint: disable=E1101
478 filedir = trace_inputs.get_native_path_case(os.path.dirname(filepath))
479 native_dest = fix_native_path_case(filedir, symlink_value)
480 out['l'] = os.path.relpath(native_dest, filedir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000481 return out
482
483
484### Variable stuff.
485
486
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000487def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000488 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000489 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000490
491
492def determine_root_dir(relative_root, infiles):
493 """For a list of infiles, determines the deepest root directory that is
494 referenced indirectly.
495
496 All arguments must be using os.path.sep.
497 """
498 # The trick used to determine the root directory is to look at "how far" back
499 # up it is looking up.
500 deepest_root = relative_root
501 for i in infiles:
502 x = relative_root
503 while i.startswith('..' + os.path.sep):
504 i = i[3:]
505 assert not i.startswith(os.path.sep)
506 x = os.path.dirname(x)
507 if deepest_root.startswith(x):
508 deepest_root = x
509 logging.debug(
510 'determine_root_dir(%s, %d files) -> %s' % (
511 relative_root, len(infiles), deepest_root))
512 return deepest_root
513
514
515def replace_variable(part, variables):
516 m = re.match(r'<\(([A-Z_]+)\)', part)
517 if m:
518 if m.group(1) not in variables:
519 raise ExecutionError(
520 'Variable "%s" was not found in %s.\nDid you forget to specify '
521 '--variable?' % (m.group(1), variables))
522 return variables[m.group(1)]
523 return part
524
525
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000526def process_variables(cwd, variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000527 """Processes path variables as a special case and returns a copy of the dict.
528
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000529 For each 'path' variable: first normalizes it based on |cwd|, verifies it
530 exists then sets it as relative to relative_base_dir.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000531 """
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000532 relative_base_dir = trace_inputs.get_native_path_case(relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000533 variables = variables.copy()
534 for i in PATH_VARIABLES:
535 if i not in variables:
536 continue
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000537 variable = variables[i].strip()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000538 # Variables could contain / or \ on windows. Always normalize to
539 # os.path.sep.
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000540 variable = variable.replace('/', os.path.sep)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000541 variable = os.path.join(cwd, variable)
542 variable = os.path.normpath(variable)
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000543 variable = trace_inputs.get_native_path_case(variable)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000544 if not os.path.isdir(variable):
545 raise ExecutionError('%s=%s is not a directory' % (i, variable))
546
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000547 # All variables are relative to the .isolate file.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000548 variable = os.path.relpath(variable, relative_base_dir)
549 logging.debug(
550 'Translated variable %s from %s to %s', i, variables[i], variable)
551 variables[i] = variable
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000552 return variables
553
554
555def eval_variables(item, variables):
556 """Replaces the .isolate variables in a string item.
557
558 Note that the .isolate format is a subset of the .gyp dialect.
559 """
560 return ''.join(
561 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
562
563
564def classify_files(root_dir, tracked, untracked):
565 """Converts the list of files into a .isolate 'variables' dictionary.
566
567 Arguments:
568 - tracked: list of files names to generate a dictionary out of that should
569 probably be tracked.
570 - untracked: list of files names that must not be tracked.
571 """
572 # These directories are not guaranteed to be always present on every builder.
573 OPTIONAL_DIRECTORIES = (
574 'test/data/plugin',
575 'third_party/WebKit/LayoutTests',
576 )
577
578 new_tracked = []
579 new_untracked = list(untracked)
580
581 def should_be_tracked(filepath):
582 """Returns True if it is a file without whitespace in a non-optional
583 directory that has no symlink in its path.
584 """
585 if filepath.endswith('/'):
586 return False
587 if ' ' in filepath:
588 return False
589 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
590 return False
591 # Look if any element in the path is a symlink.
592 split = filepath.split('/')
593 for i in range(len(split)):
594 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
595 return False
596 return True
597
598 for filepath in sorted(tracked):
599 if should_be_tracked(filepath):
600 new_tracked.append(filepath)
601 else:
602 # Anything else.
603 new_untracked.append(filepath)
604
605 variables = {}
606 if new_tracked:
607 variables[KEY_TRACKED] = sorted(new_tracked)
608 if new_untracked:
609 variables[KEY_UNTRACKED] = sorted(new_untracked)
610 return variables
611
612
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000613def chromium_fix(f, variables):
614 """Fixes an isolate dependnecy with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000615 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
616 # separator.
617 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000618 # Ignored items.
619 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000620 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000621 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000622 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000623 # 'First Run' is not created by the compile, but by the test itself.
624 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000625
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000626 # Blacklist logs and other unimportant files.
627 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
628 logging.debug('Ignoring %s', f)
629 return None
630
maruel@chromium.org7650e422012-11-16 21:56:42 +0000631 EXECUTABLE = re.compile(
632 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
633 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
634 r'$')
635 match = EXECUTABLE.match(f)
636 if match:
637 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
638
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000639 if sys.platform == 'darwin':
640 # On OSX, the name of the output is dependent on gyp define, it can be
641 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
642 # Framework.framework'. Furthermore, they are versioned with a gyp
643 # variable. To lower the complexity of the .isolate file, remove all the
644 # individual entries that show up under any of the 4 entries and replace
645 # them with the directory itself. Overall, this results in a bit more
646 # files than strictly necessary.
647 OSX_BUNDLES = (
648 '<(PRODUCT_DIR)/Chromium Framework.framework/',
649 '<(PRODUCT_DIR)/Chromium.app/',
650 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
651 '<(PRODUCT_DIR)/Google Chrome.app/',
652 )
653 for prefix in OSX_BUNDLES:
654 if f.startswith(prefix):
655 # Note this result in duplicate values, so the a set() must be used to
656 # remove duplicates.
657 return prefix
658 return f
659
660
661def generate_simplified(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000662 tracked, untracked, touched, root_dir, variables, relative_cwd,
663 trace_blacklist):
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000664 """Generates a clean and complete .isolate 'variables' dictionary.
665
666 Cleans up and extracts only files from within root_dir then processes
667 variables and relative_cwd.
668 """
669 root_dir = os.path.realpath(root_dir)
670 logging.info(
671 'generate_simplified(%d files, %s, %s, %s)' %
672 (len(tracked) + len(untracked) + len(touched),
673 root_dir, variables, relative_cwd))
674
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000675 # Preparation work.
676 relative_cwd = cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000677 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000678 # Creates the right set of variables here. We only care about PATH_VARIABLES.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000679 path_variables = dict(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000680 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
681 for k in PATH_VARIABLES if k in variables)
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000682 variables = variables.copy()
683 variables.update(path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000684
685 # Actual work: Process the files.
686 # TODO(maruel): if all the files in a directory are in part tracked and in
687 # part untracked, the directory will not be extracted. Tracked files should be
688 # 'promoted' to be untracked as needed.
689 tracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000690 root_dir, tracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000691 untracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000692 root_dir, untracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000693 # touched is not compressed, otherwise it would result in files to be archived
694 # that we don't need.
695
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000696 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000697 def fix(f):
698 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000699 # Important, GYP stores the files with / and not \.
700 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000701 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000702 # If it's not already a variable.
703 if not f.startswith('<'):
704 # relative_cwd is usually the directory containing the gyp file. It may be
705 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000706 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000707 # Convert the whole thing to / since it's isolate's speak.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000708 f = posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000709 posixpath.join(root_dir_posix, f),
710 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000711
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000712 for variable, root_path in path_variables.iteritems():
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000713 if f.startswith(root_path):
714 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000715 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000716 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000717 return f
718
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000719 def fix_all(items):
720 """Reduces the items to convert variables, removes unneeded items, apply
721 chromium-specific fixes and only return unique items.
722 """
723 variables_converted = (fix(f.path) for f in items)
724 chromium_fixed = (chromium_fix(f, variables) for f in variables_converted)
725 return set(f for f in chromium_fixed if f)
726
727 tracked = fix_all(tracked)
728 untracked = fix_all(untracked)
729 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000730 out = classify_files(root_dir, tracked, untracked)
731 if touched:
732 out[KEY_TOUCHED] = sorted(touched)
733 return out
734
735
benrg@chromium.org609b7982013-02-07 16:44:46 +0000736def chromium_filter_flags(variables):
737 """Filters out build flags used in Chromium that we don't want to treat as
738 configuration variables.
739 """
740 # TODO(benrg): Need a better way to determine this.
741 blacklist = set(PATH_VARIABLES + ('EXECUTABLE_SUFFIX', 'FLAG'))
742 return dict((k, v) for k, v in variables.iteritems() if k not in blacklist)
743
744
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000745def generate_isolate(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000746 tracked, untracked, touched, root_dir, variables, relative_cwd,
747 trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000748 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000749 dependencies = generate_simplified(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000750 tracked, untracked, touched, root_dir, variables, relative_cwd,
751 trace_blacklist)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000752 config_variables = chromium_filter_flags(variables)
753 config_variable_names, config_values = zip(
754 *sorted(config_variables.iteritems()))
755 out = Configs(None)
756 # The new dependencies apply to just one configuration, namely config_values.
757 out.merge_dependencies(dependencies, config_variable_names, [config_values])
758 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000759
760
761def split_touched(files):
762 """Splits files that are touched vs files that are read."""
763 tracked = []
764 touched = []
765 for f in files:
766 if f.size:
767 tracked.append(f)
768 else:
769 touched.append(f)
770 return tracked, touched
771
772
773def pretty_print(variables, stdout):
774 """Outputs a gyp compatible list from the decoded variables.
775
776 Similar to pprint.print() but with NIH syndrome.
777 """
778 # Order the dictionary keys by these keys in priority.
779 ORDER = (
780 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
781 KEY_TRACKED, KEY_UNTRACKED)
782
783 def sorting_key(x):
784 """Gives priority to 'most important' keys before the others."""
785 if x in ORDER:
786 return str(ORDER.index(x))
787 return x
788
789 def loop_list(indent, items):
790 for item in items:
791 if isinstance(item, basestring):
792 stdout.write('%s\'%s\',\n' % (indent, item))
793 elif isinstance(item, dict):
794 stdout.write('%s{\n' % indent)
795 loop_dict(indent + ' ', item)
796 stdout.write('%s},\n' % indent)
797 elif isinstance(item, list):
798 # A list inside a list will write the first item embedded.
799 stdout.write('%s[' % indent)
800 for index, i in enumerate(item):
801 if isinstance(i, basestring):
802 stdout.write(
803 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
804 elif isinstance(i, dict):
805 stdout.write('{\n')
806 loop_dict(indent + ' ', i)
807 if index != len(item) - 1:
808 x = ', '
809 else:
810 x = ''
811 stdout.write('%s}%s' % (indent, x))
812 else:
813 assert False
814 stdout.write('],\n')
815 else:
816 assert False
817
818 def loop_dict(indent, items):
819 for key in sorted(items, key=sorting_key):
820 item = items[key]
821 stdout.write("%s'%s': " % (indent, key))
822 if isinstance(item, dict):
823 stdout.write('{\n')
824 loop_dict(indent + ' ', item)
825 stdout.write(indent + '},\n')
826 elif isinstance(item, list):
827 stdout.write('[\n')
828 loop_list(indent + ' ', item)
829 stdout.write(indent + '],\n')
830 elif isinstance(item, basestring):
831 stdout.write(
832 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
833 elif item in (True, False, None):
834 stdout.write('%s\n' % item)
835 else:
836 assert False, item
837
838 stdout.write('{\n')
839 loop_dict(' ', variables)
840 stdout.write('}\n')
841
842
843def union(lhs, rhs):
844 """Merges two compatible datastructures composed of dict/list/set."""
845 assert lhs is not None or rhs is not None
846 if lhs is None:
847 return copy.deepcopy(rhs)
848 if rhs is None:
849 return copy.deepcopy(lhs)
850 assert type(lhs) == type(rhs), (lhs, rhs)
851 if hasattr(lhs, 'union'):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000852 # Includes set, ConfigSettings and Configs.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000853 return lhs.union(rhs)
854 if isinstance(lhs, dict):
855 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
856 elif isinstance(lhs, list):
857 # Do not go inside the list.
858 return lhs + rhs
859 assert False, type(lhs)
860
861
862def extract_comment(content):
863 """Extracts file level comment."""
864 out = []
865 for line in content.splitlines(True):
866 if line.startswith('#'):
867 out.append(line)
868 else:
869 break
870 return ''.join(out)
871
872
873def eval_content(content):
874 """Evaluates a python file and return the value defined in it.
875
876 Used in practice for .isolate files.
877 """
878 globs = {'__builtins__': None}
879 locs = {}
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000880 try:
881 value = eval(content, globs, locs)
882 except TypeError as e:
883 e.args = list(e.args) + [content]
884 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000885 assert locs == {}, locs
886 assert globs == {'__builtins__': None}, globs
887 return value
888
889
benrg@chromium.org609b7982013-02-07 16:44:46 +0000890def match_configs(expr, config_variables, all_configs):
891 """Returns the configs from |all_configs| that match the |expr|, where
892 the elements of |all_configs| are tuples of values for the |config_variables|.
893 Example:
894 >>> match_configs(expr = "(foo==1 or foo==2) and bar=='b'",
895 config_variables = ["foo", "bar"],
896 all_configs = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
897 [(1, 'b'), (2, 'b')]
898 """
899 return [
900 config for config in all_configs
901 if eval(expr, dict(zip(config_variables, config)))
902 ]
903
904
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000905def verify_variables(variables):
906 """Verifies the |variables| dictionary is in the expected format."""
907 VALID_VARIABLES = [
908 KEY_TOUCHED,
909 KEY_TRACKED,
910 KEY_UNTRACKED,
911 'command',
912 'read_only',
913 ]
914 assert isinstance(variables, dict), variables
915 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
916 for name, value in variables.iteritems():
917 if name == 'read_only':
918 assert value in (True, False, None), value
919 else:
920 assert isinstance(value, list), value
921 assert all(isinstance(i, basestring) for i in value), value
922
923
benrg@chromium.org609b7982013-02-07 16:44:46 +0000924def verify_ast(expr, variables_and_values):
925 """Verifies that |expr| is of the form
926 expr ::= expr ( "or" | "and" ) expr
927 | identifier "==" ( string | int )
928 Also collects the variable identifiers and string/int values in the dict
929 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
930 """
931 assert isinstance(expr, (ast.BoolOp, ast.Compare))
932 if isinstance(expr, ast.BoolOp):
933 assert isinstance(expr.op, (ast.And, ast.Or))
934 for subexpr in expr.values:
935 verify_ast(subexpr, variables_and_values)
936 else:
937 assert isinstance(expr.left.ctx, ast.Load)
938 assert len(expr.ops) == 1
939 assert isinstance(expr.ops[0], ast.Eq)
940 var_values = variables_and_values.setdefault(expr.left.id, set())
941 rhs = expr.comparators[0]
942 assert isinstance(rhs, (ast.Str, ast.Num))
943 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
944
945
946def verify_condition(condition, variables_and_values):
947 """Verifies the |condition| dictionary is in the expected format.
948 See verify_ast() for the meaning of |variables_and_values|.
949 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000950 VALID_INSIDE_CONDITION = ['variables']
951 assert isinstance(condition, list), condition
benrg@chromium.org609b7982013-02-07 16:44:46 +0000952 assert len(condition) == 2, condition
953 expr, then = condition
954
955 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
956 verify_ast(test_ast.body, variables_and_values)
957
958 assert isinstance(then, dict), then
959 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
960 verify_variables(then['variables'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000961
962
benrg@chromium.org609b7982013-02-07 16:44:46 +0000963def verify_root(value, variables_and_values):
964 """Verifies that |value| is the parsed form of a valid .isolate file.
965 See verify_ast() for the meaning of |variables_and_values|.
966 """
967 VALID_ROOTS = ['includes', 'conditions']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000968 assert isinstance(value, dict), value
969 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000970
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000971 includes = value.get('includes', [])
972 assert isinstance(includes, list), includes
973 for include in includes:
974 assert isinstance(include, basestring), include
975
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000976 conditions = value.get('conditions', [])
977 assert isinstance(conditions, list), conditions
978 for condition in conditions:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000979 verify_condition(condition, variables_and_values)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000980
981
benrg@chromium.org609b7982013-02-07 16:44:46 +0000982def remove_weak_dependencies(values, key, item, item_configs):
983 """Removes any configs from this key if the item is already under a
984 strong key.
985 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000986 if key == KEY_TOUCHED:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000987 item_configs = set(item_configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000988 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000989 try:
990 item_configs -= values[stronger_key][item]
991 except KeyError:
992 pass
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000993
benrg@chromium.org609b7982013-02-07 16:44:46 +0000994 return item_configs
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000995
996
benrg@chromium.org609b7982013-02-07 16:44:46 +0000997def remove_repeated_dependencies(folders, key, item, item_configs):
998 """Removes any configs from this key if the item is in a folder that is
999 already included."""
csharp@chromium.org31176252012-11-02 13:04:40 +00001000
1001 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001002 item_configs = set(item_configs)
1003 for (folder, configs) in folders.iteritems():
csharp@chromium.org31176252012-11-02 13:04:40 +00001004 if folder != item and item.startswith(folder):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001005 item_configs -= configs
csharp@chromium.org31176252012-11-02 13:04:40 +00001006
benrg@chromium.org609b7982013-02-07 16:44:46 +00001007 return item_configs
csharp@chromium.org31176252012-11-02 13:04:40 +00001008
1009
1010def get_folders(values_dict):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001011 """Returns a dict of all the folders in the given value_dict."""
1012 return dict(
1013 (item, configs) for (item, configs) in values_dict.iteritems()
1014 if item.endswith('/')
1015 )
csharp@chromium.org31176252012-11-02 13:04:40 +00001016
1017
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001018def invert_map(variables):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001019 """Converts {config: {deptype: list(depvals)}} to
1020 {deptype: {depval: set(configs)}}.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001021 """
1022 KEYS = (
1023 KEY_TOUCHED,
1024 KEY_TRACKED,
1025 KEY_UNTRACKED,
1026 'command',
1027 'read_only',
1028 )
1029 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001030 for config, values in variables.iteritems():
1031 for key in KEYS:
1032 if key == 'command':
1033 items = [tuple(values[key])] if key in values else []
1034 elif key == 'read_only':
1035 items = [values[key]] if key in values else []
1036 else:
1037 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
1038 items = values.get(key, [])
1039 for item in items:
1040 out[key].setdefault(item, set()).add(config)
1041 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001042
1043
benrg@chromium.org609b7982013-02-07 16:44:46 +00001044def reduce_inputs(values):
1045 """Reduces the output of invert_map() to the strictest minimum list.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001046
benrg@chromium.org609b7982013-02-07 16:44:46 +00001047 Looks at each individual file and directory, maps where they are used and
1048 reconstructs the inverse dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001049
benrg@chromium.org609b7982013-02-07 16:44:46 +00001050 Returns the minimized dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001051 """
1052 KEYS = (
1053 KEY_TOUCHED,
1054 KEY_TRACKED,
1055 KEY_UNTRACKED,
1056 'command',
1057 'read_only',
1058 )
csharp@chromium.org31176252012-11-02 13:04:40 +00001059
1060 # Folders can only live in KEY_UNTRACKED.
1061 folders = get_folders(values.get(KEY_UNTRACKED, {}))
1062
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001063 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001064 for key in KEYS:
1065 for item, item_configs in values.get(key, {}).iteritems():
1066 item_configs = remove_weak_dependencies(values, key, item, item_configs)
1067 item_configs = remove_repeated_dependencies(
1068 folders, key, item, item_configs)
1069 if item_configs:
1070 out[key][item] = item_configs
1071 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001072
1073
benrg@chromium.org609b7982013-02-07 16:44:46 +00001074def convert_map_to_isolate_dict(values, config_variables):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001075 """Regenerates back a .isolate configuration dict from files and dirs
1076 mappings generated from reduce_inputs().
1077 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001078 # Gather a list of configurations for set inversion later.
1079 all_mentioned_configs = set()
1080 for configs_by_item in values.itervalues():
1081 for configs in configs_by_item.itervalues():
1082 all_mentioned_configs.update(configs)
1083
1084 # Invert the mapping to make it dict first.
1085 conditions = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001086 for key in values:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001087 for item, configs in values[key].iteritems():
1088 then = conditions.setdefault(frozenset(configs), {})
1089 variables = then.setdefault('variables', {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001090
benrg@chromium.org609b7982013-02-07 16:44:46 +00001091 if item in (True, False):
1092 # One-off for read_only.
1093 variables[key] = item
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001094 else:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001095 assert item
1096 if isinstance(item, tuple):
1097 # One-off for command.
1098 # Do not merge lists and do not sort!
1099 # Note that item is a tuple.
1100 assert key not in variables
1101 variables[key] = list(item)
1102 else:
1103 # The list of items (files or dirs). Append the new item and keep
1104 # the list sorted.
1105 l = variables.setdefault(key, [])
1106 l.append(item)
1107 l.sort()
1108
1109 if all_mentioned_configs:
1110 config_values = map(set, zip(*all_mentioned_configs))
1111 sef = short_expression_finder.ShortExpressionFinder(
1112 zip(config_variables, config_values))
1113
1114 conditions = sorted(
1115 [sef.get_expr(configs), then] for configs, then in conditions.iteritems())
1116 return {'conditions': conditions}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001117
1118
1119### Internal state files.
1120
1121
benrg@chromium.org609b7982013-02-07 16:44:46 +00001122class ConfigSettings(object):
1123 """Represents the dependency variables for a single build configuration.
1124 The structure is immutable.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001125 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001126 def __init__(self, config, values):
1127 self.config = config
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001128 verify_variables(values)
1129 self.touched = sorted(values.get(KEY_TOUCHED, []))
1130 self.tracked = sorted(values.get(KEY_TRACKED, []))
1131 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
1132 self.command = values.get('command', [])[:]
1133 self.read_only = values.get('read_only')
1134
1135 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001136 assert not (self.config and rhs.config) or (self.config == rhs.config)
maruel@chromium.org669edcb2012-11-02 19:16:14 +00001137 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001138 var = {
1139 KEY_TOUCHED: sorted(self.touched + rhs.touched),
1140 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
1141 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
1142 'command': self.command or rhs.command,
1143 'read_only': rhs.read_only if self.read_only is None else self.read_only,
1144 }
benrg@chromium.org609b7982013-02-07 16:44:46 +00001145 return ConfigSettings(self.config or rhs.config, var)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001146
1147 def flatten(self):
1148 out = {}
1149 if self.command:
1150 out['command'] = self.command
1151 if self.touched:
1152 out[KEY_TOUCHED] = self.touched
1153 if self.tracked:
1154 out[KEY_TRACKED] = self.tracked
1155 if self.untracked:
1156 out[KEY_UNTRACKED] = self.untracked
1157 if self.read_only is not None:
1158 out['read_only'] = self.read_only
1159 return out
1160
1161
1162class Configs(object):
1163 """Represents a processed .isolate file.
1164
benrg@chromium.org609b7982013-02-07 16:44:46 +00001165 Stores the file in a processed way, split by configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001166 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001167 def __init__(self, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001168 self.file_comment = file_comment
benrg@chromium.org609b7982013-02-07 16:44:46 +00001169 # The keys of by_config are tuples of values for the configuration
1170 # variables. The names of the variables (which must be the same for
1171 # every by_config key) are kept in config_variables. Initially by_config
1172 # is empty and we don't know what configuration variables will be used,
1173 # so config_variables also starts out empty. It will be set by the first
1174 # call to union() or merge_dependencies().
1175 self.by_config = {}
1176 self.config_variables = ()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001177
1178 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001179 """Adds variables from rhs (a Configs) to the existing variables.
1180 """
1181 config_variables = self.config_variables
1182 if not config_variables:
1183 config_variables = rhs.config_variables
1184 else:
1185 # We can't proceed if this isn't true since we don't know the correct
1186 # default values for extra variables. The variables are sorted so we
1187 # don't need to worry about permutations.
1188 if rhs.config_variables and rhs.config_variables != config_variables:
1189 raise ExecutionError(
1190 'Variables in merged .isolate files do not match: %r and %r' % (
1191 config_variables, rhs.config_variables))
1192
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001193 # Takes the first file comment, prefering lhs.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001194 out = Configs(self.file_comment or rhs.file_comment)
1195 out.config_variables = config_variables
1196 for config in set(self.by_config) | set(rhs.by_config):
1197 out.by_config[config] = union(
1198 self.by_config.get(config), rhs.by_config.get(config))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001199 return out
1200
benrg@chromium.org609b7982013-02-07 16:44:46 +00001201 def merge_dependencies(self, values, config_variables, configs):
1202 """Adds new dependencies to this object for the given configurations.
1203 Arguments:
1204 values: A variables dict as found in a .isolate file, e.g.,
1205 {KEY_TOUCHED: [...], 'command': ...}.
1206 config_variables: An ordered list of configuration variables, e.g.,
1207 ["OS", "chromeos"]. If this object already contains any dependencies,
1208 the configuration variables must match.
1209 configs: a list of tuples of values of the configuration variables,
1210 e.g., [("mac", 0), ("linux", 1)]. The dependencies in |values|
1211 are added to all of these configurations, and other configurations
1212 are unchanged.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001213 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001214 if not values:
1215 return
1216
1217 if not self.config_variables:
1218 self.config_variables = config_variables
1219 else:
1220 # See comment in Configs.union().
1221 assert self.config_variables == config_variables
1222
1223 for config in configs:
1224 self.by_config[config] = union(
1225 self.by_config.get(config), ConfigSettings(config, values))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001226
1227 def flatten(self):
1228 """Returns a flat dictionary representation of the configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001229 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001230 return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
1231
1232 def make_isolate_file(self):
1233 """Returns a dictionary suitable for writing to a .isolate file.
1234 """
1235 dependencies_by_config = self.flatten()
1236 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
1237 return convert_map_to_isolate_dict(configs_by_dependency,
1238 self.config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001239
1240
benrg@chromium.org609b7982013-02-07 16:44:46 +00001241# TODO(benrg): Remove this function when no old-format files are left.
1242def convert_old_to_new_format(value):
1243 """Converts from the old .isolate format, which only has one variable (OS),
1244 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
1245 and allows conditions that depend on the set of all OSes, to the new format,
1246 which allows any set of variables, has no hardcoded values, and only allows
1247 explicit positive tests of variable values.
1248 """
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001249 conditions = value.get('conditions', [])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001250 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
1251 return value # Nothing to change
1252
1253 def parse_condition(cond):
1254 return re.match(r'OS=="(\w+)"\Z', cond[0]).group(1)
1255
1256 oses = set(map(parse_condition, conditions))
1257 default_oses = set(['linux', 'mac', 'win'])
1258 oses = sorted(oses | default_oses)
1259
1260 def if_not_os(not_os, then):
1261 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
1262 return [expr, then]
1263
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001264 conditions = [
1265 cond[:2] for cond in conditions if cond[1]
1266 ] + [
1267 if_not_os(parse_condition(cond), cond[2])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001268 for cond in conditions if len(cond) == 3
1269 ]
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001270
benrg@chromium.org609b7982013-02-07 16:44:46 +00001271 if 'variables' in value:
1272 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
1273 conditions.sort()
1274
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001275 value = value.copy()
1276 value['conditions'] = conditions
benrg@chromium.org609b7982013-02-07 16:44:46 +00001277 return value
1278
1279
1280def load_isolate_as_config(isolate_dir, value, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001281 """Parses one .isolate file and returns a Configs() instance.
1282
1283 |value| is the loaded dictionary that was defined in the gyp file.
1284
1285 The expected format is strict, anything diverting from the format below will
1286 throw an assert:
1287 {
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001288 'includes': [
1289 'foo.isolate',
1290 ],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001291 'conditions': [
benrg@chromium.org609b7982013-02-07 16:44:46 +00001292 ['OS=="vms" and foo=42', {
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001293 'variables': {
benrg@chromium.org609b7982013-02-07 16:44:46 +00001294 'command': [
1295 ...
1296 ],
1297 'isolate_dependency_tracked': [
1298 ...
1299 ],
1300 'isolate_dependency_untracked': [
1301 ...
1302 ],
1303 'read_only': False,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001304 },
1305 }],
1306 ...
1307 ],
1308 }
1309 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001310 value = convert_old_to_new_format(value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001311
benrg@chromium.org609b7982013-02-07 16:44:46 +00001312 variables_and_values = {}
1313 verify_root(value, variables_and_values)
1314 if variables_and_values:
1315 config_variables, config_values = zip(
1316 *sorted(variables_and_values.iteritems()))
1317 all_configs = list(itertools.product(*config_values))
1318 else:
1319 config_variables = None
1320 all_configs = []
1321
1322 isolate = Configs(file_comment)
1323
1324 # Add configuration-specific variables.
1325 for expr, then in value.get('conditions', []):
1326 configs = match_configs(expr, config_variables, all_configs)
1327 isolate.merge_dependencies(then['variables'], config_variables, configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001328
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001329 # Load the includes.
1330 for include in value.get('includes', []):
1331 if os.path.isabs(include):
1332 raise ExecutionError(
1333 'Failed to load configuration; absolute include path \'%s\'' %
1334 include)
1335 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
1336 with open(included_isolate, 'r') as f:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001337 included_isolate = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001338 os.path.dirname(included_isolate),
1339 eval_content(f.read()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001340 None)
1341 isolate = union(isolate, included_isolate)
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001342
benrg@chromium.org609b7982013-02-07 16:44:46 +00001343 return isolate
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001344
1345
benrg@chromium.org609b7982013-02-07 16:44:46 +00001346def load_isolate_for_config(isolate_dir, content, variables):
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001347 """Loads the .isolate file and returns the information unprocessed but
1348 filtered for the specific OS.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001349
1350 Returns the command, dependencies and read_only flag. The dependencies are
1351 fixed to use os.path.sep.
1352 """
1353 # Load the .isolate file, process its conditions, retrieve the command and
1354 # dependencies.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001355 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
1356 try:
1357 config = tuple(variables[var] for var in isolate.config_variables)
1358 except KeyError:
1359 raise ExecutionError(
1360 'These configuration variables were missing from the command line: %s' %
1361 ', '.join(sorted(set(isolate.config_variables) - set(variables))))
1362 config = isolate.by_config.get(config)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001363 if not config:
csharp@chromium.org2a3d7d52013-03-23 12:54:37 +00001364 raise ExecutionError('Failed to load configuration for (%s)' %
1365 ', '.join(isolate.config_variables))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001366 # Merge tracked and untracked variables, isolate.py doesn't care about the
1367 # trackability of the variables, only the build tool does.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001368 dependencies = [
1369 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1370 ]
1371 touched = [f.replace('/', os.path.sep) for f in config.touched]
1372 return config.command, dependencies, touched, config.read_only
1373
1374
maruel@chromium.orgdcdbfc82013-07-25 18:54:57 +00001375def save_isolated(isolated, data):
1376 """Writes one or multiple .isolated files.
1377
1378 Note: this reference implementation does not create child .isolated file so it
1379 always returns an empty list.
1380
1381 Returns the list of child isolated files that are included by |isolated|.
1382 """
1383 trace_inputs.write_json(isolated, data, True)
1384 return []
1385
1386
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001387def chromium_save_isolated(isolated, data, variables):
1388 """Writes one or many .isolated files.
1389
1390 This slightly increases the cold cache cost but greatly reduce the warm cache
1391 cost by splitting low-churn files off the master .isolated file. It also
1392 reduces overall isolateserver memcache consumption.
1393 """
1394 slaves = []
1395
1396 def extract_into_included_isolated(prefix):
1397 new_slave = {'files': {}, 'os': data['os']}
1398 for f in data['files'].keys():
1399 if f.startswith(prefix):
1400 new_slave['files'][f] = data['files'].pop(f)
1401 if new_slave['files']:
1402 slaves.append(new_slave)
1403
1404 # Split test/data/ in its own .isolated file.
1405 extract_into_included_isolated(os.path.join('test', 'data', ''))
1406
1407 # Split everything out of PRODUCT_DIR in its own .isolated file.
1408 if variables.get('PRODUCT_DIR'):
1409 extract_into_included_isolated(variables['PRODUCT_DIR'])
1410
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001411 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001412 for index, f in enumerate(slaves):
1413 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
1414 trace_inputs.write_json(slavepath, f, True)
maruel@chromium.orgfb78d432013-08-28 21:22:40 +00001415 data.setdefault('includes', []).append(isolateserver.sha1_file(slavepath))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001416 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001417
maruel@chromium.orgdcdbfc82013-07-25 18:54:57 +00001418 files.extend(save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001419 return files
1420
1421
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001422class Flattenable(object):
1423 """Represents data that can be represented as a json file."""
1424 MEMBERS = ()
1425
1426 def flatten(self):
1427 """Returns a json-serializable version of itself.
1428
1429 Skips None entries.
1430 """
1431 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1432 return dict((member, value) for member, value in items if value is not None)
1433
1434 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001435 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001436 """Loads a flattened version."""
1437 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001438 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001439 for member in out.MEMBERS:
1440 if member in data:
1441 # Access to a protected member XXX of a client class
1442 # pylint: disable=W0212
1443 out._load_member(member, data.pop(member))
1444 if data:
1445 raise ValueError(
1446 'Found unexpected entry %s while constructing an object %s' %
1447 (data, cls.__name__), data, cls.__name__)
1448 return out
1449
1450 def _load_member(self, member, value):
1451 """Loads a member into self."""
1452 setattr(self, member, value)
1453
1454 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001455 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001456 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001457 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001458 out = cls.load(trace_inputs.read_json(filename), *args, **kwargs)
1459 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001460 except (IOError, ValueError):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001461 # On failure, loads the default instance.
1462 out = cls(*args, **kwargs)
1463 logging.warn('Failed to load %s', filename)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001464 return out
1465
1466
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001467class SavedState(Flattenable):
1468 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001469
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001470 This file caches the items calculated by this script and is used to increase
1471 the performance of the script. This file is not loaded by run_isolated.py.
1472 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001473
1474 It is important to note that the 'files' dict keys are using native OS path
1475 separator instead of '/' used in .isolate file.
1476 """
1477 MEMBERS = (
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001478 # Cache of the processed command. This value is saved because .isolated
1479 # files are never loaded by isolate.py so it's the only way to load the
1480 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001481 'command',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001482 # Cache of the files found so the next run can skip sha1 calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001483 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001484 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001485 'isolate_file',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001486 # List of included .isolated files. Used to support/remember 'slave'
1487 # .isolated files. Relative path to isolated_basedir.
1488 'child_isolated_files',
1489 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001490 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001491 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001492 'relative_cwd',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001493 # GYP variables used to generate the .isolated file. Variables are saved so
1494 # a user can use isolate.py after building and the GYP variables are still
1495 # defined.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001496 'variables',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001497 )
1498
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001499 def __init__(self, isolated_basedir):
1500 """Creates an empty SavedState.
1501
1502 |isolated_basedir| is the directory where the .isolated and .isolated.state
1503 files are saved.
1504 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001505 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001506 assert os.path.isabs(isolated_basedir), isolated_basedir
1507 assert os.path.isdir(isolated_basedir), isolated_basedir
1508 self.isolated_basedir = isolated_basedir
1509
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 self.command = []
1511 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001512 self.isolate_file = None
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001513 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001514 self.read_only = None
1515 self.relative_cwd = None
benrg@chromium.org609b7982013-02-07 16:44:46 +00001516 self.variables = {'OS': get_flavor()}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001517
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001518 def update(self, isolate_file, variables):
1519 """Updates the saved state with new data to keep GYP variables and internal
1520 reference to the original .isolate file.
1521 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +00001522 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001523 # Convert back to a relative path. On Windows, if the isolate and
1524 # isolated files are on different drives, isolate_file will stay an absolute
1525 # path.
1526 isolate_file = safe_relpath(isolate_file, self.isolated_basedir)
1527
1528 # The same .isolate file should always be used to generate the .isolated and
1529 # .isolated.state.
1530 assert isolate_file == self.isolate_file or not self.isolate_file, (
1531 isolate_file, self.isolate_file)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001532 self.isolate_file = isolate_file
1533 self.variables.update(variables)
1534
1535 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
1536 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001537
1538 The new files in |infiles| are added to self.files dict but their sha1 is
1539 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001540 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001541 self.command = command
1542 # Add new files.
1543 for f in infiles:
1544 self.files.setdefault(f, {})
1545 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001546 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001547 # Prune extraneous files that are not a dependency anymore.
1548 for f in set(self.files).difference(set(infiles).union(touched)):
1549 del self.files[f]
1550 if read_only is not None:
1551 self.read_only = read_only
1552 self.relative_cwd = relative_cwd
1553
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001554 def to_isolated(self):
1555 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001556
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001557 https://code.google.com/p/swarming/wiki/IsolateDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001558 """
1559 def strip(data):
1560 """Returns a 'files' entry with only the whitelisted keys."""
1561 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
1562
1563 out = {
1564 'files': dict(
1565 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001566 'os': self.variables['OS'],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001567 }
1568 if self.command:
1569 out['command'] = self.command
1570 if self.read_only is not None:
1571 out['read_only'] = self.read_only
1572 if self.relative_cwd:
1573 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001574 return out
1575
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001576 @property
1577 def isolate_filepath(self):
1578 """Returns the absolute path of self.isolate_file."""
1579 return os.path.normpath(
1580 os.path.join(self.isolated_basedir, self.isolate_file))
1581
1582 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001583 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001584 def load(cls, data, isolated_basedir): # pylint: disable=W0221
1585 """Special case loading to disallow different OS.
1586
1587 It is not possible to load a .isolated.state files from a different OS, this
1588 file is saved in OS-specific format.
1589 """
1590 out = super(SavedState, cls).load(data, isolated_basedir)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001591 if 'os' in data:
1592 out.variables['OS'] = data['os']
1593 if out.variables['OS'] != get_flavor():
1594 raise run_isolated.ConfigError(
1595 'The .isolated.state file was created on another platform')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001596 # The .isolate file must be valid. It could be absolute on Windows if the
1597 # drive containing the .isolate and the drive containing the .isolated files
1598 # differ.
1599 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
1600 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001601 return out
1602
1603 def __str__(self):
1604 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001605 out += ' command: %s\n' % self.command
1606 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001607 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001608 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +00001609 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001610 out += ' child_isolated_files: %s\n' % self.child_isolated_files
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001611 out += ' variables: %s' % ''.join(
1612 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1613 out += ')'
1614 return out
1615
1616
1617class CompleteState(object):
1618 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001619 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001620 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +00001621 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001622 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001623 # Contains the data to ease developer's use-case but that is not strictly
1624 # necessary.
1625 self.saved_state = saved_state
1626
1627 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001628 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001629 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001630 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001631 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001632 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001633 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001634 SavedState.load_file(
1635 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001636
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001637 def load_isolate(self, cwd, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001638 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001639 .isolate file.
1640
1641 Processes the loaded data, deduce root_dir, relative_cwd.
1642 """
1643 # Make sure to not depend on os.getcwd().
1644 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.orga3da9122013-03-28 13:27:09 +00001645 isolate_file = trace_inputs.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001646 logging.info(
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001647 'CompleteState.load_isolate(%s, %s, %s, %s)',
1648 cwd, isolate_file, variables, ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001649 relative_base_dir = os.path.dirname(isolate_file)
1650
1651 # Processes the variables and update the saved state.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001652 variables = process_variables(cwd, variables, relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001653 self.saved_state.update(isolate_file, variables)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001654 variables = self.saved_state.variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001655
1656 with open(isolate_file, 'r') as f:
1657 # At that point, variables are not replaced yet in command and infiles.
1658 # infiles may contain directory entries and is in posix style.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001659 command, infiles, touched, read_only = load_isolate_for_config(
1660 os.path.dirname(isolate_file), f.read(), variables)
1661 command = [eval_variables(i, variables) for i in command]
1662 infiles = [eval_variables(f, variables) for f in infiles]
1663 touched = [eval_variables(f, variables) for f in touched]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001664 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +00001665 # form '../../foo/bar'. Note that path variables must be taken in account
1666 # too, add them as if they were input files.
1667 path_variables = [variables[v] for v in PATH_VARIABLES if v in variables]
1668 root_dir = determine_root_dir(
1669 relative_base_dir, infiles + touched + path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001670 # The relative directory is automatically determined by the relative path
1671 # between root_dir and the directory containing the .isolate file,
1672 # isolate_base_dir.
1673 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
benrg@chromium.org9ae72862013-02-11 05:05:51 +00001674 # Now that we know where the root is, check that the PATH_VARIABLES point
1675 # inside it.
1676 for i in PATH_VARIABLES:
1677 if i in variables:
1678 if not path_starts_with(
1679 root_dir, os.path.join(relative_base_dir, variables[i])):
1680 raise run_isolated.MappingError(
maruel@chromium.org75584e22013-06-20 01:40:24 +00001681 'Path variable %s=%r points outside the inferred root directory'
1682 ' %s' % (i, variables[i], root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001683 # Normalize the files based to root_dir. It is important to keep the
1684 # trailing os.path.sep at that step.
1685 infiles = [
1686 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1687 for f in infiles
1688 ]
1689 touched = [
1690 relpath(normpath(os.path.join(relative_base_dir, f)), root_dir)
1691 for f in touched
1692 ]
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001693 follow_symlinks = variables['OS'] != 'win'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001694 # Expand the directories by listing each file inside. Up to now, trailing
1695 # os.path.sep must be kept. Do not expand 'touched'.
1696 infiles = expand_directories_and_symlinks(
1697 root_dir,
1698 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001699 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001700 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +00001701 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001702
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001703 # If we ignore broken items then remove any missing touched items.
1704 if ignore_broken_items:
1705 original_touched_count = len(touched)
1706 touched = [touch for touch in touched if os.path.exists(touch)]
1707
1708 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +00001709 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001710 len(touched) - original_touched_count)
1711
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001712 # Finally, update the new data to be able to generate the foo.isolated file,
1713 # the file that is used by run_isolated.py.
1714 self.saved_state.update_isolated(
1715 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001716 logging.debug(self)
1717
maruel@chromium.org9268f042012-10-17 17:36:41 +00001718 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001719 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001720
maruel@chromium.org9268f042012-10-17 17:36:41 +00001721 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1722 file is tainted.
1723
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001724 See process_input() for more information.
1725 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001726 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001727 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001728 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001729 else:
1730 filepath = os.path.join(self.root_dir, infile)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001731 self.saved_state.files[infile] = process_input(
1732 filepath,
1733 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +00001734 self.saved_state.read_only,
1735 self.saved_state.variables['OS'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001736
1737 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001738 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001739 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001740 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001741 self.isolated_filepath,
1742 self.saved_state.to_isolated(),
1743 self.saved_state.variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001744 total_bytes = sum(
1745 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001746 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001747 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001748 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001749 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001750 logging.debug('Dumping to %s' % saved_state_file)
1751 trace_inputs.write_json(saved_state_file, self.saved_state.flatten(), True)
1752
1753 @property
1754 def root_dir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001755 """Returns the absolute path of the root_dir to reference the .isolate file
1756 via relative_cwd.
1757
1758 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
1759 to isolate_filepath.
1760 """
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001761 if not self.saved_state.isolate_file:
1762 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001763 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001764 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001765 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001766 root_dir = isolate_dir
1767 else:
1768 assert isolate_dir.endswith(self.saved_state.relative_cwd), (
1769 isolate_dir, self.saved_state.relative_cwd)
1770 # Walk back back to the root directory.
1771 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
1772 return trace_inputs.get_native_path_case(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001773
1774 @property
1775 def resultdir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001776 """Returns the absolute path containing the .isolated file.
1777
1778 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
1779 path as the value.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001780 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001781 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001782
1783 def __str__(self):
1784 def indent(data, indent_length):
1785 """Indents text."""
1786 spacing = ' ' * indent_length
1787 return ''.join(spacing + l for l in str(data).splitlines(True))
1788
1789 out = '%s(\n' % self.__class__.__name__
1790 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001791 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1792 return out
1793
1794
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001795def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001796 """Loads a CompleteState.
1797
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001798 This includes data from .isolate and .isolated.state files. Never reads the
1799 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001800
1801 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001802 options: Options instance generated with OptionParserIsolate. For either
1803 options.isolate and options.isolated, if the value is set, it is an
1804 absolute path.
1805 cwd: base directory to be used when loading the .isolate file.
1806 subdir: optional argument to only process file in the subdirectory, relative
1807 to CompleteState.root_dir.
1808 skip_update: Skip trying to load the .isolate file and processing the
1809 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001810 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001811 assert not options.isolate or os.path.isabs(options.isolate)
1812 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.orga3da9122013-03-28 13:27:09 +00001813 cwd = trace_inputs.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001814 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001815 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001816 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001817 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001818 else:
1819 # Constructs a dummy object that cannot be saved. Useful for temporary
1820 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001821 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001822
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001823 if not options.isolate:
1824 if not complete_state.saved_state.isolate_file:
1825 if not skip_update:
1826 raise ExecutionError('A .isolate file is required.')
1827 isolate = None
1828 else:
1829 isolate = complete_state.saved_state.isolate_filepath
1830 else:
1831 isolate = options.isolate
1832 if complete_state.saved_state.isolate_file:
1833 rel_isolate = safe_relpath(
1834 options.isolate, complete_state.saved_state.isolated_basedir)
1835 if rel_isolate != complete_state.saved_state.isolate_file:
1836 raise ExecutionError(
1837 '%s and %s do not match.' % (
1838 options.isolate, complete_state.saved_state.isolate_file))
1839
1840 if not skip_update:
1841 # Then load the .isolate and expands directories.
1842 complete_state.load_isolate(
1843 cwd, isolate, options.variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001844
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001845 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001846 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001847 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001848 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1849 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001850
1851 if not skip_update:
1852 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001853 return complete_state
1854
1855
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001856def read_trace_as_isolate_dict(complete_state, trace_blacklist):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001857 """Reads a trace and returns the .isolate dictionary.
1858
1859 Returns exceptions during the log parsing so it can be re-raised.
1860 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001861 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001862 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001863 if not os.path.isfile(logfile):
1864 raise ExecutionError(
1865 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1866 try:
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001867 data = api.parse_log(logfile, trace_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001868 exceptions = [i['exception'] for i in data if 'exception' in i]
1869 results = (i['results'] for i in data if 'results' in i)
1870 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1871 files = set(sum((result.existent for result in results_stripped), []))
1872 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001873 value = generate_isolate(
1874 tracked,
1875 [],
1876 touched,
1877 complete_state.root_dir,
1878 complete_state.saved_state.variables,
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001879 complete_state.saved_state.relative_cwd,
1880 trace_blacklist)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001881 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001882 except trace_inputs.TracingFailure, e:
1883 raise ExecutionError(
1884 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001885 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001886
1887
1888def print_all(comment, data, stream):
1889 """Prints a complete .isolate file and its top-level file comment into a
1890 stream.
1891 """
1892 if comment:
1893 stream.write(comment)
1894 pretty_print(data, stream)
1895
1896
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001897def merge(complete_state, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001898 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001899 value, exceptions = read_trace_as_isolate_dict(
1900 complete_state, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001901
1902 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001903 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001904 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001905 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001906 prev_config = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001907 isolate_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001908 eval_content(prev_content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001909 extract_comment(prev_content))
1910 new_config = load_isolate_as_config(isolate_dir, value, '')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001911 config = union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001912 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001913 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001914 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001915 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001916 if exceptions:
1917 # It got an exception, raise the first one.
1918 raise \
1919 exceptions[0][0], \
1920 exceptions[0][1], \
1921 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001922
1923
maruel@chromium.org29029882013-08-30 12:15:40 +00001924### Commands.
1925
1926
maruel@chromium.orge5322512013-08-19 20:17:57 +00001927def CMDcheck(parser, args):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001928 """Checks that all the inputs are present and generates .isolated."""
maruel@chromium.org9268f042012-10-17 17:36:41 +00001929 parser.add_option('--subdir', help='Filters to a subdirectory')
1930 options, args = parser.parse_args(args)
1931 if args:
1932 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001933
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001934 complete_state = load_complete_state(
1935 options, os.getcwd(), options.subdir, False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001936
1937 # Nothing is done specifically. Just store the result and state.
1938 complete_state.save_files()
1939 return 0
1940
1941
maruel@chromium.orge5322512013-08-19 20:17:57 +00001942def CMDhashtable(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001943 """Creates a hash table content addressed object store.
1944
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001945 All the files listed in the .isolated file are put in the output directory
1946 with the file name being the sha-1 of the file's content.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001947 """
maruel@chromium.org9268f042012-10-17 17:36:41 +00001948 parser.add_option('--subdir', help='Filters to a subdirectory')
1949 options, args = parser.parse_args(args)
1950 if args:
1951 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001952
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001953 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001954 success = False
1955 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001956 complete_state = load_complete_state(
1957 options, os.getcwd(), options.subdir, False)
1958 if not options.outdir:
1959 options.outdir = os.path.join(
1960 os.path.dirname(complete_state.isolated_filepath), 'hashtable')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001961 # Make sure that complete_state isn't modified until save_files() is
1962 # called, because any changes made to it here will propagate to the files
1963 # created (which is probably not intended).
1964 complete_state.save_files()
1965
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001966 infiles = complete_state.saved_state.files
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001967 # Add all the .isolated files.
maruel@chromium.org87f11962013-04-10 21:27:28 +00001968 isolated_hash = []
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001969 isolated_files = [
1970 options.isolated,
1971 ] + complete_state.saved_state.child_isolated_files
1972 for item in isolated_files:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001973 item_path = os.path.join(
1974 os.path.dirname(complete_state.isolated_filepath), item)
maruel@chromium.orgfb78d432013-08-28 21:22:40 +00001975 # Do not use isolateserver.sha1_file() here because the file is
maruel@chromium.org87f11962013-04-10 21:27:28 +00001976 # likely smallish (under 500kb) and its file size is needed.
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001977 with open(item_path, 'rb') as f:
1978 content = f.read()
maruel@chromium.org87f11962013-04-10 21:27:28 +00001979 isolated_hash.append(hashlib.sha1(content).hexdigest())
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001980 isolated_metadata = {
maruel@chromium.org87f11962013-04-10 21:27:28 +00001981 'h': isolated_hash[-1],
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001982 's': len(content),
1983 'priority': '0'
1984 }
1985 infiles[item_path] = isolated_metadata
1986
1987 logging.info('Creating content addressed object store with %d item',
1988 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001989
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001990 if is_url(options.outdir):
maruel@chromium.orgfb78d432013-08-28 21:22:40 +00001991 isolateserver.upload_sha1_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001992 base_url=options.outdir,
1993 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00001994 infiles=infiles,
1995 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001996 else:
1997 recreate_tree(
1998 outdir=options.outdir,
1999 indir=complete_state.root_dir,
2000 infiles=infiles,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00002001 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002002 as_sha1=True)
2003 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002004 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002005 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002006 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002007 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002008 if not success and os.path.isfile(options.isolated):
2009 os.remove(options.isolated)
maruel@chromium.org87f11962013-04-10 21:27:28 +00002010 return not success
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002011
2012
maruel@chromium.orge5322512013-08-19 20:17:57 +00002013def CMDmerge(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002014 """Reads and merges the data from the trace back into the original .isolate.
2015
2016 Ignores --outdir.
2017 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002018 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002019 add_trace_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00002020 options, args = parser.parse_args(args)
2021 if args:
2022 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00002023
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002024 complete_state = load_complete_state(options, os.getcwd(), None, False)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002025 blacklist = trace_inputs.gen_blacklist(options.trace_blacklist)
2026 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002027 return 0
2028
2029
maruel@chromium.orge5322512013-08-19 20:17:57 +00002030def CMDread(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002031 """Reads the trace file generated with command 'trace'.
2032
2033 Ignores --outdir.
2034 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002035 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002036 add_trace_option(parser)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002037 parser.add_option(
2038 '--skip-refresh', action='store_true',
2039 help='Skip reading .isolate file and do not refresh the sha1 of '
2040 'dependencies')
maruel@chromium.org29029882013-08-30 12:15:40 +00002041 parser.add_option(
2042 '-m', '--merge', action='store_true',
2043 help='merge the results back in the .isolate file instead of printing')
maruel@chromium.org9268f042012-10-17 17:36:41 +00002044 options, args = parser.parse_args(args)
2045 if args:
2046 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00002047
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002048 complete_state = load_complete_state(
2049 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002050 blacklist = trace_inputs.gen_blacklist(options.trace_blacklist)
2051 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
maruel@chromium.org29029882013-08-30 12:15:40 +00002052 if options.merge:
2053 merge(complete_state, blacklist)
2054 else:
2055 pretty_print(value, sys.stdout)
2056
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00002057 if exceptions:
2058 # It got an exception, raise the first one.
2059 raise \
2060 exceptions[0][0], \
2061 exceptions[0][1], \
2062 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002063 return 0
2064
2065
maruel@chromium.orge5322512013-08-19 20:17:57 +00002066def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002067 """Creates a directory with all the dependencies mapped into it.
2068
2069 Useful to test manually why a test is failing. The target executable is not
2070 run.
2071 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002072 parser.require_isolated = False
maruel@chromium.org9268f042012-10-17 17:36:41 +00002073 options, args = parser.parse_args(args)
2074 if args:
2075 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002076 complete_state = load_complete_state(options, os.getcwd(), None, False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002077
2078 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002079 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002080 'isolate', complete_state.root_dir)
2081 else:
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002082 if is_url(options.outdir):
maruel@chromium.org29029882013-08-30 12:15:40 +00002083 parser.error('Can\'t use url for --outdir with mode remap.')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002084 if not os.path.isdir(options.outdir):
2085 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00002086 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002087 if len(os.listdir(options.outdir)):
2088 raise ExecutionError('Can\'t remap in a non-empty directory')
2089 recreate_tree(
2090 outdir=options.outdir,
2091 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002092 infiles=complete_state.saved_state.files,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00002093 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002094 as_sha1=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002095 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002096 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002097
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002098 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002099 complete_state.save_files()
2100 return 0
2101
2102
maruel@chromium.orge5322512013-08-19 20:17:57 +00002103def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002104 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00002105 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002106 options, args = parser.parse_args(args)
2107 if args:
2108 parser.error('Unsupported argument: %s' % args)
2109
2110 if options.isolated:
2111 # Load the previous state if it was present. Namely, "foo.isolated.state".
2112 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002113 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002114 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002115 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002116 if not isolate:
maruel@chromium.org29029882013-08-30 12:15:40 +00002117 parser.error('--isolate is required.')
2118
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002119 with open(isolate, 'r') as f:
2120 content = f.read()
2121 config = load_isolate_as_config(
2122 os.path.dirname(os.path.abspath(isolate)),
2123 eval_content(content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00002124 extract_comment(content))
2125 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002126 print('Updating %s' % isolate)
2127 with open(isolate, 'wb') as f:
2128 print_all(config.file_comment, data, f)
2129 return 0
2130
2131
maruel@chromium.org29029882013-08-30 12:15:40 +00002132@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00002133def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002134 """Runs the test executable in an isolated (temporary) directory.
2135
2136 All the dependencies are mapped into the temporary directory and the
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002137 directory is cleaned up after the target exits. Warning: if --outdir is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002138 specified, it is deleted upon exit.
2139
maruel@chromium.org29029882013-08-30 12:15:40 +00002140 Argument processing stops at -- and these arguments are appended to the
2141 command line of the target to run. For example, use:
2142 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002143 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002144 parser.require_isolated = False
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002145 parser.add_option(
2146 '--skip-refresh', action='store_true',
2147 help='Skip reading .isolate file and do not refresh the sha1 of '
2148 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002149 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00002150 if options.outdir and is_url(options.outdir):
2151 parser.error('Can\'t use url for --outdir with mode run.')
2152
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002153 complete_state = load_complete_state(
2154 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002155 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002156 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00002157 raise ExecutionError('No command to run.')
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002158
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002159 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002160 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002161 root_dir = complete_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002162 if not options.outdir:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002163 if not os.path.isabs(root_dir):
2164 root_dir = os.path.join(os.path.dirname(options.isolated), root_dir)
2165 options.outdir = run_isolated.make_temp_dir('isolate', root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002166 else:
2167 if not os.path.isdir(options.outdir):
2168 os.makedirs(options.outdir)
2169 recreate_tree(
2170 outdir=options.outdir,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002171 indir=root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002172 infiles=complete_state.saved_state.files,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00002173 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002174 as_sha1=False)
2175 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002176 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002177 if not os.path.isdir(cwd):
2178 # It can happen when no files are mapped from the directory containing the
2179 # .isolate file. But the directory must exist to be the current working
2180 # directory.
2181 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002182 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002183 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002184 logging.info('Running %s, cwd=%s' % (cmd, cwd))
2185 result = subprocess.call(cmd, cwd=cwd)
2186 finally:
2187 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002188 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002189
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002190 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002191 complete_state.save_files()
2192 return result
2193
2194
maruel@chromium.org29029882013-08-30 12:15:40 +00002195@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00002196def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002197 """Traces the target using trace_inputs.py.
2198
2199 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002200 its child processes access. Then the 'merge' command can be used to generate
2201 an updated .isolate file out of it or the 'read' command to print it out to
2202 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002203
maruel@chromium.org29029882013-08-30 12:15:40 +00002204 Argument processing stops at -- and these arguments are appended to the
2205 command line of the target to run. For example, use:
2206 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002207 """
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002208 add_trace_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002209 parser.add_option(
2210 '-m', '--merge', action='store_true',
2211 help='After tracing, merge the results back in the .isolate file')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002212 parser.add_option(
2213 '--skip-refresh', action='store_true',
2214 help='Skip reading .isolate file and do not refresh the sha1 of '
2215 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002216 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00002217
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002218 complete_state = load_complete_state(
2219 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002220 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002221 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00002222 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002223 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002224 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002225 unicode(complete_state.root_dir),
2226 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00002227 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
2228 if not os.path.isfile(cmd[0]):
2229 raise ExecutionError(
2230 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002231 logging.info('Running %s, cwd=%s' % (cmd, cwd))
2232 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002233 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002234 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002235 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002236 try:
2237 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002238 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002239 cmd,
2240 cwd,
2241 'default',
2242 True)
2243 except trace_inputs.TracingFailure, e:
2244 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
2245
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002246 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002247 logging.error(
2248 'Tracer exited with %d, which means the tests probably failed so the '
2249 'trace is probably incomplete.', result)
2250 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002251
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002252 complete_state.save_files()
2253
2254 if options.merge:
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002255 blacklist = trace_inputs.gen_blacklist(options.trace_blacklist)
2256 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002257
2258 return result
2259
2260
maruel@chromium.org712454d2013-04-04 17:52:34 +00002261def _process_variable_arg(_option, _opt, _value, parser):
2262 if not parser.rargs:
2263 raise optparse.OptionValueError(
2264 'Please use --variable FOO=BAR or --variable FOO BAR')
2265 k = parser.rargs.pop(0)
2266 if '=' in k:
2267 parser.values.variables.append(tuple(k.split('=', 1)))
2268 else:
2269 if not parser.rargs:
2270 raise optparse.OptionValueError(
2271 'Please use --variable FOO=BAR or --variable FOO BAR')
2272 v = parser.rargs.pop(0)
2273 parser.values.variables.append((k, v))
2274
2275
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002276def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002277 """Adds --isolated and --variable to an OptionParser."""
2278 parser.add_option(
2279 '-s', '--isolated',
2280 metavar='FILE',
2281 help='.isolated file to generate or read')
2282 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002283 parser.add_option(
2284 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002285 dest='isolated',
2286 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002287 default_variables = [('OS', get_flavor())]
2288 if sys.platform in ('win32', 'cygwin'):
2289 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
2290 else:
2291 default_variables.append(('EXECUTABLE_SUFFIX', ''))
2292 parser.add_option(
2293 '-V', '--variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00002294 action='callback',
2295 callback=_process_variable_arg,
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002296 default=default_variables,
2297 dest='variables',
2298 metavar='FOO BAR',
2299 help='Variables to process in the .isolate file, default: %default. '
2300 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002301 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002302
2303
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002304def add_trace_option(parser):
2305 """Adds --trace-blacklist to the parser."""
2306 parser.add_option(
2307 '--trace-blacklist',
2308 action='append', default=list(DEFAULT_BLACKLIST),
2309 help='List of regexp to use as blacklist filter for files to consider '
2310 'important, not to be confused with --blacklist which blacklists '
2311 'test case.')
2312
2313
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002314def parse_isolated_option(parser, options, cwd, require_isolated):
2315 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002316 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002317 options.isolated = os.path.normpath(
2318 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002319 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00002320 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002321 if options.isolated and not options.isolated.endswith('.isolated'):
2322 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002323
2324
2325def parse_variable_option(options):
2326 """Processes --variable."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002327 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
2328 # but it wouldn't be backward compatible.
2329 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002330 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002331 try:
2332 return int(s)
2333 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002334 return s.decode('utf-8')
benrg@chromium.org609b7982013-02-07 16:44:46 +00002335 options.variables = dict((k, try_make_int(v)) for k, v in options.variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002336
2337
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002338class OptionParserIsolate(tools.OptionParserWithLogging):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002339 """Adds automatic --isolate, --isolated, --out and --variable handling."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00002340 # Set it to False if it is not required, e.g. it can be passed on but do not
2341 # fail if not given.
2342 require_isolated = True
2343
2344 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002345 tools.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00002346 self,
2347 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
2348 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002349 group = optparse.OptionGroup(self, "Common options")
2350 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002351 '-i', '--isolate',
2352 metavar='FILE',
2353 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002354 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002355 group.add_option(
2356 '-o', '--outdir', metavar='DIR',
2357 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002358 'Defaults: run|remap: a /tmp subdirectory, others: '
2359 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00002360 group.add_option(
2361 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002362 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
2363 help='Indicates that invalid entries in the isolated file to be '
2364 'only be logged and not stop processing. Defaults to True if '
2365 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002366 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002367
2368 def parse_args(self, *args, **kwargs):
2369 """Makes sure the paths make sense.
2370
2371 On Windows, / and \ are often mixed together in a path.
2372 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002373 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002374 self, *args, **kwargs)
2375 if not self.allow_interspersed_args and args:
2376 self.error('Unsupported argument: %s' % args)
2377
maruel@chromium.orga3da9122013-03-28 13:27:09 +00002378 cwd = trace_inputs.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002379 parse_isolated_option(self, options, cwd, self.require_isolated)
2380 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002381
2382 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002383 # TODO(maruel): Work with non-ASCII.
2384 # The path must be in native path case for tracing purposes.
2385 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
2386 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
2387 options.isolate = trace_inputs.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002388
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002389 if options.outdir and not is_url(options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002390 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
2391 # outdir doesn't need native path case since tracing is never done from
2392 # there.
2393 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002394
2395 return options, args
2396
2397
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002398def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00002399 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002400 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00002401 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002402 except (
2403 ExecutionError,
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002404 run_isolated.MappingError,
2405 run_isolated.ConfigError) as e:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002406 sys.stderr.write('\nError: ')
2407 sys.stderr.write(str(e))
2408 sys.stderr.write('\n')
2409 return 1
2410
2411
2412if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00002413 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002414 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00002415 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002416 sys.exit(main(sys.argv[1:]))