blob: 731585a4ff5955da8ecb93a1aaf45effb9a7990d [file] [log] [blame]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001#!/usr/bin/env python
Marc-Antoine Ruel8add1242013-11-05 17:28:27 -05002# Copyright 2012 The Swarming Authors. All rights reserved.
Marc-Antoine Ruele98b1122013-11-05 20:27:57 -05003# Use of this source code is governed under the Apache License, Version 2.0 that
4# can be found in the LICENSE file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00005
maruel@chromium.orge5322512013-08-19 20:17:57 +00006"""Front end tool to operate on .isolate files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00007
maruel@chromium.orge5322512013-08-19 20:17:57 +00008This includes creating, merging or compiling them to generate a .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00009
10See more information at
maruel@chromium.orge5322512013-08-19 20:17:57 +000011 https://code.google.com/p/swarming/wiki/IsolateDesign
12 https://code.google.com/p/swarming/wiki/IsolateUserGuide
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000013"""
maruel@chromium.orge5322512013-08-19 20:17:57 +000014# Run ./isolate.py --help for more detailed information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000015
benrg@chromium.org609b7982013-02-07 16:44:46 +000016import ast
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000017import copy
benrg@chromium.org609b7982013-02-07 16:44:46 +000018import itertools
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000019import logging
20import optparse
21import os
22import posixpath
23import re
24import stat
25import subprocess
26import sys
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000027
maruel@chromium.orgfb78d432013-08-28 21:22:40 +000028import isolateserver
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000029import run_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000030import trace_inputs
31
32# Import here directly so isolate is easier to use as a library.
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000033from run_isolated import get_flavor
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000034
maruel@chromium.orge5322512013-08-19 20:17:57 +000035from third_party import colorama
36from third_party.depot_tools import fix_encoding
37from third_party.depot_tools import subcommand
38
maruel@chromium.org561d4b22013-09-26 21:08:08 +000039from utils import file_path
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
benrg@chromium.org9ae72862013-02-11 05:05:51 +000066def expand_symlinks(indir, relfile):
67 """Follows symlinks in |relfile|, but treating symlinks that point outside the
68 build tree as if they were ordinary directories/files. Returns the final
69 symlink-free target and a list of paths to symlinks encountered in the
70 process.
71
72 The rule about symlinks outside the build tree is for the benefit of the
73 Chromium OS ebuild, which symlinks the output directory to an unrelated path
74 in the chroot.
75
76 Fails when a directory loop is detected, although in theory we could support
77 that case.
78 """
benrg@chromium.org9ae72862013-02-11 05:05:51 +000079 is_directory = relfile.endswith(os.path.sep)
80 done = indir
81 todo = relfile.strip(os.path.sep)
82 symlinks = []
83
84 while todo:
maruel@chromium.org561d4b22013-09-26 21:08:08 +000085 pre_symlink, symlink, post_symlink = file_path.split_at_symlink(
benrg@chromium.org9ae72862013-02-11 05:05:51 +000086 done, todo)
87 if not symlink:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -050088 todo = file_path.fix_native_path_case(done, todo)
benrg@chromium.org9ae72862013-02-11 05:05:51 +000089 done = os.path.join(done, todo)
90 break
91 symlink_path = os.path.join(done, pre_symlink, symlink)
92 post_symlink = post_symlink.lstrip(os.path.sep)
93 # readlink doesn't exist on Windows.
94 # pylint: disable=E1101
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +000095 target = os.path.normpath(os.path.join(done, pre_symlink))
96 symlink_target = os.readlink(symlink_path)
maruel@chromium.org28c19672013-04-29 18:51:09 +000097 if os.path.isabs(symlink_target):
98 # Absolute path are considered a normal directories. The use case is
99 # generally someone who puts the output directory on a separate drive.
100 target = symlink_target
101 else:
102 # The symlink itself could be using the wrong path case.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500103 target = file_path.fix_native_path_case(target, symlink_target)
csharp@chromium.orgf2eacff2013-04-04 14:20:20 +0000104
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000105 if not os.path.exists(target):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000106 raise isolateserver.MappingError(
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000107 'Symlink target doesn\'t exist: %s -> %s' % (symlink_path, target))
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000108 target = file_path.get_native_path_case(target)
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500109 if not file_path.path_starts_with(indir, target):
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000110 done = symlink_path
111 todo = post_symlink
112 continue
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500113 if file_path.path_starts_with(target, symlink_path):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000114 raise isolateserver.MappingError(
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000115 'Can\'t map recursive symlink reference %s -> %s' %
116 (symlink_path, target))
117 logging.info('Found symlink: %s -> %s', symlink_path, target)
118 symlinks.append(os.path.relpath(symlink_path, indir))
119 # Treat the common prefix of the old and new paths as done, and start
120 # scanning again.
121 target = target.split(os.path.sep)
122 symlink_path = symlink_path.split(os.path.sep)
123 prefix_length = 0
124 for target_piece, symlink_path_piece in zip(target, symlink_path):
125 if target_piece == symlink_path_piece:
126 prefix_length += 1
127 else:
128 break
129 done = os.path.sep.join(target[:prefix_length])
130 todo = os.path.join(
131 os.path.sep.join(target[prefix_length:]), post_symlink)
132
133 relfile = os.path.relpath(done, indir)
134 relfile = relfile.rstrip(os.path.sep) + is_directory * os.path.sep
135 return relfile, symlinks
136
137
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000138def expand_directory_and_symlink(indir, relfile, blacklist, follow_symlinks):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000139 """Expands a single input. It can result in multiple outputs.
140
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000141 This function is recursive when relfile is a directory.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000142
143 Note: this code doesn't properly handle recursive symlink like one created
144 with:
145 ln -s .. foo
146 """
147 if os.path.isabs(relfile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000148 raise isolateserver.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000149 'Can\'t map absolute path %s' % relfile)
150
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500151 infile = file_path.normpath(os.path.join(indir, relfile))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000152 if not infile.startswith(indir):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000153 raise isolateserver.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000154 'Can\'t map file %s outside %s' % (infile, indir))
155
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000156 filepath = os.path.join(indir, relfile)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000157 native_filepath = file_path.get_native_path_case(filepath)
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000158 if filepath != native_filepath:
maruel@chromium.org59bb2a32013-03-21 17:08:39 +0000159 # Special case './'.
160 if filepath != native_filepath + '.' + os.path.sep:
maruel@chromium.org7f66a982013-06-06 15:58:59 +0000161 # Give up enforcing strict path case on OSX. Really, it's that sad. The
162 # case where it happens is very specific and hard to reproduce:
163 # get_native_path_case(
164 # u'Foo.framework/Versions/A/Resources/Something.nib') will return
165 # u'Foo.framework/Versions/A/resources/Something.nib', e.g. lowercase 'r'.
166 #
167 # Note that this is really something deep in OSX because running
168 # ls Foo.framework/Versions/A
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000169 # will print out 'Resources', while file_path.get_native_path_case()
maruel@chromium.org7f66a982013-06-06 15:58:59 +0000170 # returns a lower case 'r'.
171 #
172 # So *something* is happening under the hood resulting in the command 'ls'
173 # and Carbon.File.FSPathMakeRef('path').FSRefMakePath() to disagree. We
174 # have no idea why.
175 if sys.platform != 'darwin':
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000176 raise isolateserver.MappingError(
maruel@chromium.org7f66a982013-06-06 15:58:59 +0000177 'File path doesn\'t equal native file path\n%s != %s' %
178 (filepath, native_filepath))
csharp@chromium.orgf972d932013-03-05 19:29:31 +0000179
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000180 symlinks = []
181 if follow_symlinks:
182 relfile, symlinks = expand_symlinks(indir, relfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000183
184 if relfile.endswith(os.path.sep):
185 if not os.path.isdir(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000186 raise isolateserver.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000187 '%s is not a directory but ends with "%s"' % (infile, os.path.sep))
188
maruel@chromium.org59bb2a32013-03-21 17:08:39 +0000189 # Special case './'.
190 if relfile.startswith('.' + os.path.sep):
191 relfile = relfile[2:]
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000192 outfiles = symlinks
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000193 try:
194 for filename in os.listdir(infile):
195 inner_relfile = os.path.join(relfile, filename)
196 if blacklist(inner_relfile):
197 continue
198 if os.path.isdir(os.path.join(indir, inner_relfile)):
199 inner_relfile += os.path.sep
200 outfiles.extend(
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000201 expand_directory_and_symlink(indir, inner_relfile, blacklist,
202 follow_symlinks))
csharp@chromium.org63a96d92013-01-16 19:50:14 +0000203 return outfiles
204 except OSError as e:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000205 raise isolateserver.MappingError(
maruel@chromium.org1cd786e2013-04-26 18:48:40 +0000206 'Unable to iterate over directory %s.\n%s' % (infile, e))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000207 else:
208 # Always add individual files even if they were blacklisted.
209 if os.path.isdir(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000210 raise isolateserver.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000211 'Input directory %s must have a trailing slash' % infile)
212
213 if not os.path.isfile(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000214 raise isolateserver.MappingError(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000215 'Input file %s doesn\'t exist' % infile)
216
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000217 return symlinks + [relfile]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000218
219
csharp@chromium.org01856802012-11-12 17:48:13 +0000220def expand_directories_and_symlinks(indir, infiles, blacklist,
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000221 follow_symlinks, ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000222 """Expands the directories and the symlinks, applies the blacklist and
223 verifies files exist.
224
225 Files are specified in os native path separator.
226 """
227 outfiles = []
228 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +0000229 try:
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000230 outfiles.extend(expand_directory_and_symlink(indir, relfile, blacklist,
231 follow_symlinks))
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000232 except isolateserver.MappingError as e:
csharp@chromium.org01856802012-11-12 17:48:13 +0000233 if ignore_broken_items:
234 logging.info('warning: %s', e)
235 else:
236 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000237 return outfiles
238
239
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000240def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000241 """Creates a new tree with only the input files in it.
242
243 Arguments:
244 outdir: Output directory to create the files in.
245 indir: Root directory the infiles are based in.
246 infiles: dict of files to map from |indir| to |outdir|.
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000247 action: One of accepted action of run_isolated.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000248 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000249 """
250 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000251 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
252 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000253
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000254 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000255 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000256 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000257 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000258
259 for relfile, metadata in infiles.iteritems():
260 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000261 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000262 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000263 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000264 # Skip links when storing a hashtable.
265 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000266 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000267 if os.path.isfile(outfile):
268 # Just do a quick check that the file size matches. No need to stat()
269 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000270 if not 's' in metadata:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000271 raise isolateserver.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000272 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000273 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000274 continue
275 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000276 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000277 os.remove(outfile)
278 else:
279 outfile = os.path.join(outdir, relfile)
280 outsubdir = os.path.dirname(outfile)
281 if not os.path.isdir(outsubdir):
282 os.makedirs(outsubdir)
283
284 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000285 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000286 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000287 if 'l' in metadata:
288 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000289 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000290 # symlink doesn't exist on Windows.
291 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000292 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000293 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000294
295
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000296def process_input(filepath, prevdict, read_only, flavor, algo):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000297 """Processes an input file, a dependency, and return meta data about it.
298
299 Arguments:
300 - filepath: File to act on.
301 - prevdict: the previous dictionary. It is used to retrieve the cached sha-1
302 to skip recalculating the hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000303 - read_only: If True, the file mode is manipulated. In practice, only save
304 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
305 windows, mode is not set since all files are 'executable' by
306 default.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000307 - algo: Hashing algorithm used.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000308
309 Behaviors:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000310 - Retrieves the file mode, file size, file timestamp, file link
311 destination if it is a file link and calcultate the SHA-1 of the file's
312 content if the path points to a file and not a symlink.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000313 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000314 out = {}
315 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000316 # if prevdict.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000317 # # The file's content is ignored. Skip the time and hard code mode.
318 # if get_flavor() != 'win':
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000319 # out['m'] = stat.S_IRUSR | stat.S_IRGRP
320 # out['s'] = 0
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000321 # out['h'] = algo().hexdigest()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000322 # out['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000323 # return out
324
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000325 # Always check the file stat and check if it is a link. The timestamp is used
326 # to know if the file's content/symlink destination should be looked into.
327 # E.g. only reuse from prevdict if the timestamp hasn't changed.
328 # There is the risk of the file's timestamp being reset to its last value
329 # manually while its content changed. We don't protect against that use case.
330 try:
331 filestats = os.lstat(filepath)
332 except OSError:
333 # The file is not present.
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000334 raise isolateserver.MappingError('%s is missing' % filepath)
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000335 is_link = stat.S_ISLNK(filestats.st_mode)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000336
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000337 if flavor != 'win':
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000338 # Ignore file mode on Windows since it's not really useful there.
339 filemode = stat.S_IMODE(filestats.st_mode)
340 # Remove write access for group and all access to 'others'.
341 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
342 if read_only:
343 filemode &= ~stat.S_IWUSR
344 if filemode & stat.S_IXUSR:
345 filemode |= stat.S_IXGRP
346 else:
347 filemode &= ~stat.S_IXGRP
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000348 if not is_link:
349 out['m'] = filemode
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000350
351 # Used to skip recalculating the hash or link destination. Use the most recent
352 # update time.
353 # TODO(maruel): Save it in the .state file instead of .isolated so the
354 # .isolated file is deterministic.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000355 out['t'] = int(round(filestats.st_mtime))
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000356
357 if not is_link:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000358 out['s'] = filestats.st_size
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000359 # If the timestamp wasn't updated and the file size is still the same, carry
360 # on the sha-1.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000361 if (prevdict.get('t') == out['t'] and
362 prevdict.get('s') == out['s']):
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000363 # Reuse the previous hash if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000364 out['h'] = prevdict.get('h')
365 if not out.get('h'):
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000366 out['h'] = isolateserver.hash_file(filepath, algo)
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000367 else:
368 # If the timestamp wasn't updated, carry on the link destination.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000369 if prevdict.get('t') == out['t']:
maruel@chromium.orgf2826a32012-10-16 18:26:17 +0000370 # Reuse the previous link destination if available.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000371 out['l'] = prevdict.get('l')
372 if out.get('l') is None:
maruel@chromium.org8d159e32013-04-18 15:29:50 +0000373 # The link could be in an incorrect path case. In practice, this only
374 # happen on OSX on case insensitive HFS.
375 # TODO(maruel): It'd be better if it was only done once, in
376 # expand_directory_and_symlink(), so it would not be necessary to do again
377 # here.
378 symlink_value = os.readlink(filepath) # pylint: disable=E1101
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000379 filedir = file_path.get_native_path_case(os.path.dirname(filepath))
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500380 native_dest = file_path.fix_native_path_case(filedir, symlink_value)
maruel@chromium.org8d159e32013-04-18 15:29:50 +0000381 out['l'] = os.path.relpath(native_dest, filedir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000382 return out
383
384
385### Variable stuff.
386
387
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000388def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000389 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000390 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000391
392
393def determine_root_dir(relative_root, infiles):
394 """For a list of infiles, determines the deepest root directory that is
395 referenced indirectly.
396
397 All arguments must be using os.path.sep.
398 """
399 # The trick used to determine the root directory is to look at "how far" back
400 # up it is looking up.
401 deepest_root = relative_root
402 for i in infiles:
403 x = relative_root
404 while i.startswith('..' + os.path.sep):
405 i = i[3:]
406 assert not i.startswith(os.path.sep)
407 x = os.path.dirname(x)
408 if deepest_root.startswith(x):
409 deepest_root = x
410 logging.debug(
411 'determine_root_dir(%s, %d files) -> %s' % (
412 relative_root, len(infiles), deepest_root))
413 return deepest_root
414
415
416def replace_variable(part, variables):
417 m = re.match(r'<\(([A-Z_]+)\)', part)
418 if m:
419 if m.group(1) not in variables:
420 raise ExecutionError(
421 'Variable "%s" was not found in %s.\nDid you forget to specify '
422 '--variable?' % (m.group(1), variables))
423 return variables[m.group(1)]
424 return part
425
426
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000427def process_variables(cwd, variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000428 """Processes path variables as a special case and returns a copy of the dict.
429
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000430 For each 'path' variable: first normalizes it based on |cwd|, verifies it
431 exists then sets it as relative to relative_base_dir.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000432 """
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000433 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000434 variables = variables.copy()
435 for i in PATH_VARIABLES:
436 if i not in variables:
437 continue
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000438 variable = variables[i].strip()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000439 # Variables could contain / or \ on windows. Always normalize to
440 # os.path.sep.
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000441 variable = variable.replace('/', os.path.sep)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000442 variable = os.path.join(cwd, variable)
443 variable = os.path.normpath(variable)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000444 variable = file_path.get_native_path_case(variable)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000445 if not os.path.isdir(variable):
446 raise ExecutionError('%s=%s is not a directory' % (i, variable))
447
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000448 # All variables are relative to the .isolate file.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000449 variable = os.path.relpath(variable, relative_base_dir)
450 logging.debug(
451 'Translated variable %s from %s to %s', i, variables[i], variable)
452 variables[i] = variable
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000453 return variables
454
455
456def eval_variables(item, variables):
457 """Replaces the .isolate variables in a string item.
458
459 Note that the .isolate format is a subset of the .gyp dialect.
460 """
461 return ''.join(
462 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
463
464
465def classify_files(root_dir, tracked, untracked):
466 """Converts the list of files into a .isolate 'variables' dictionary.
467
468 Arguments:
469 - tracked: list of files names to generate a dictionary out of that should
470 probably be tracked.
471 - untracked: list of files names that must not be tracked.
472 """
473 # These directories are not guaranteed to be always present on every builder.
474 OPTIONAL_DIRECTORIES = (
475 'test/data/plugin',
476 'third_party/WebKit/LayoutTests',
477 )
478
479 new_tracked = []
480 new_untracked = list(untracked)
481
482 def should_be_tracked(filepath):
483 """Returns True if it is a file without whitespace in a non-optional
484 directory that has no symlink in its path.
485 """
486 if filepath.endswith('/'):
487 return False
488 if ' ' in filepath:
489 return False
490 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
491 return False
492 # Look if any element in the path is a symlink.
493 split = filepath.split('/')
494 for i in range(len(split)):
495 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
496 return False
497 return True
498
499 for filepath in sorted(tracked):
500 if should_be_tracked(filepath):
501 new_tracked.append(filepath)
502 else:
503 # Anything else.
504 new_untracked.append(filepath)
505
506 variables = {}
507 if new_tracked:
508 variables[KEY_TRACKED] = sorted(new_tracked)
509 if new_untracked:
510 variables[KEY_UNTRACKED] = sorted(new_untracked)
511 return variables
512
513
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000514def chromium_fix(f, variables):
515 """Fixes an isolate dependnecy with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000516 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
517 # separator.
518 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000519 # Ignored items.
520 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000521 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000522 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000523 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000524 # 'First Run' is not created by the compile, but by the test itself.
525 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000526
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000527 # Blacklist logs and other unimportant files.
528 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
529 logging.debug('Ignoring %s', f)
530 return None
531
maruel@chromium.org7650e422012-11-16 21:56:42 +0000532 EXECUTABLE = re.compile(
533 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
534 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
535 r'$')
536 match = EXECUTABLE.match(f)
537 if match:
538 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
539
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000540 if sys.platform == 'darwin':
541 # On OSX, the name of the output is dependent on gyp define, it can be
542 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
543 # Framework.framework'. Furthermore, they are versioned with a gyp
544 # variable. To lower the complexity of the .isolate file, remove all the
545 # individual entries that show up under any of the 4 entries and replace
546 # them with the directory itself. Overall, this results in a bit more
547 # files than strictly necessary.
548 OSX_BUNDLES = (
549 '<(PRODUCT_DIR)/Chromium Framework.framework/',
550 '<(PRODUCT_DIR)/Chromium.app/',
551 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
552 '<(PRODUCT_DIR)/Google Chrome.app/',
553 )
554 for prefix in OSX_BUNDLES:
555 if f.startswith(prefix):
556 # Note this result in duplicate values, so the a set() must be used to
557 # remove duplicates.
558 return prefix
559 return f
560
561
562def generate_simplified(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000563 tracked, untracked, touched, root_dir, variables, relative_cwd,
564 trace_blacklist):
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000565 """Generates a clean and complete .isolate 'variables' dictionary.
566
567 Cleans up and extracts only files from within root_dir then processes
568 variables and relative_cwd.
569 """
570 root_dir = os.path.realpath(root_dir)
571 logging.info(
572 'generate_simplified(%d files, %s, %s, %s)' %
573 (len(tracked) + len(untracked) + len(touched),
574 root_dir, variables, relative_cwd))
575
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000576 # Preparation work.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500577 relative_cwd = file_path.cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000578 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000579 # Creates the right set of variables here. We only care about PATH_VARIABLES.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000580 path_variables = dict(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000581 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
582 for k in PATH_VARIABLES if k in variables)
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000583 variables = variables.copy()
584 variables.update(path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000585
586 # Actual work: Process the files.
587 # TODO(maruel): if all the files in a directory are in part tracked and in
588 # part untracked, the directory will not be extracted. Tracked files should be
589 # 'promoted' to be untracked as needed.
590 tracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000591 root_dir, tracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000592 untracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000593 root_dir, untracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000594 # touched is not compressed, otherwise it would result in files to be archived
595 # that we don't need.
596
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000597 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000598 def fix(f):
599 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000600 # Important, GYP stores the files with / and not \.
601 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000602 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000603 # If it's not already a variable.
604 if not f.startswith('<'):
605 # relative_cwd is usually the directory containing the gyp file. It may be
606 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000607 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000608 # Convert the whole thing to / since it's isolate's speak.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500609 f = file_path.posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000610 posixpath.join(root_dir_posix, f),
611 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000612
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000613 for variable, root_path in path_variables.iteritems():
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000614 if f.startswith(root_path):
615 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000616 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000617 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000618 return f
619
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000620 def fix_all(items):
621 """Reduces the items to convert variables, removes unneeded items, apply
622 chromium-specific fixes and only return unique items.
623 """
624 variables_converted = (fix(f.path) for f in items)
625 chromium_fixed = (chromium_fix(f, variables) for f in variables_converted)
626 return set(f for f in chromium_fixed if f)
627
628 tracked = fix_all(tracked)
629 untracked = fix_all(untracked)
630 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000631 out = classify_files(root_dir, tracked, untracked)
632 if touched:
633 out[KEY_TOUCHED] = sorted(touched)
634 return out
635
636
benrg@chromium.org609b7982013-02-07 16:44:46 +0000637def chromium_filter_flags(variables):
638 """Filters out build flags used in Chromium that we don't want to treat as
639 configuration variables.
640 """
641 # TODO(benrg): Need a better way to determine this.
642 blacklist = set(PATH_VARIABLES + ('EXECUTABLE_SUFFIX', 'FLAG'))
643 return dict((k, v) for k, v in variables.iteritems() if k not in blacklist)
644
645
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000646def generate_isolate(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000647 tracked, untracked, touched, root_dir, variables, relative_cwd,
648 trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000649 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000650 dependencies = generate_simplified(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000651 tracked, untracked, touched, root_dir, variables, relative_cwd,
652 trace_blacklist)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000653 config_variables = chromium_filter_flags(variables)
654 config_variable_names, config_values = zip(
655 *sorted(config_variables.iteritems()))
656 out = Configs(None)
657 # The new dependencies apply to just one configuration, namely config_values.
658 out.merge_dependencies(dependencies, config_variable_names, [config_values])
659 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000660
661
662def split_touched(files):
663 """Splits files that are touched vs files that are read."""
664 tracked = []
665 touched = []
666 for f in files:
667 if f.size:
668 tracked.append(f)
669 else:
670 touched.append(f)
671 return tracked, touched
672
673
674def pretty_print(variables, stdout):
675 """Outputs a gyp compatible list from the decoded variables.
676
677 Similar to pprint.print() but with NIH syndrome.
678 """
679 # Order the dictionary keys by these keys in priority.
680 ORDER = (
681 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
682 KEY_TRACKED, KEY_UNTRACKED)
683
684 def sorting_key(x):
685 """Gives priority to 'most important' keys before the others."""
686 if x in ORDER:
687 return str(ORDER.index(x))
688 return x
689
690 def loop_list(indent, items):
691 for item in items:
692 if isinstance(item, basestring):
693 stdout.write('%s\'%s\',\n' % (indent, item))
694 elif isinstance(item, dict):
695 stdout.write('%s{\n' % indent)
696 loop_dict(indent + ' ', item)
697 stdout.write('%s},\n' % indent)
698 elif isinstance(item, list):
699 # A list inside a list will write the first item embedded.
700 stdout.write('%s[' % indent)
701 for index, i in enumerate(item):
702 if isinstance(i, basestring):
703 stdout.write(
704 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
705 elif isinstance(i, dict):
706 stdout.write('{\n')
707 loop_dict(indent + ' ', i)
708 if index != len(item) - 1:
709 x = ', '
710 else:
711 x = ''
712 stdout.write('%s}%s' % (indent, x))
713 else:
714 assert False
715 stdout.write('],\n')
716 else:
717 assert False
718
719 def loop_dict(indent, items):
720 for key in sorted(items, key=sorting_key):
721 item = items[key]
722 stdout.write("%s'%s': " % (indent, key))
723 if isinstance(item, dict):
724 stdout.write('{\n')
725 loop_dict(indent + ' ', item)
726 stdout.write(indent + '},\n')
727 elif isinstance(item, list):
728 stdout.write('[\n')
729 loop_list(indent + ' ', item)
730 stdout.write(indent + '],\n')
731 elif isinstance(item, basestring):
732 stdout.write(
733 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
734 elif item in (True, False, None):
735 stdout.write('%s\n' % item)
736 else:
737 assert False, item
738
739 stdout.write('{\n')
740 loop_dict(' ', variables)
741 stdout.write('}\n')
742
743
744def union(lhs, rhs):
745 """Merges two compatible datastructures composed of dict/list/set."""
746 assert lhs is not None or rhs is not None
747 if lhs is None:
748 return copy.deepcopy(rhs)
749 if rhs is None:
750 return copy.deepcopy(lhs)
751 assert type(lhs) == type(rhs), (lhs, rhs)
752 if hasattr(lhs, 'union'):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000753 # Includes set, ConfigSettings and Configs.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000754 return lhs.union(rhs)
755 if isinstance(lhs, dict):
756 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
757 elif isinstance(lhs, list):
758 # Do not go inside the list.
759 return lhs + rhs
760 assert False, type(lhs)
761
762
763def extract_comment(content):
764 """Extracts file level comment."""
765 out = []
766 for line in content.splitlines(True):
767 if line.startswith('#'):
768 out.append(line)
769 else:
770 break
771 return ''.join(out)
772
773
774def eval_content(content):
775 """Evaluates a python file and return the value defined in it.
776
777 Used in practice for .isolate files.
778 """
779 globs = {'__builtins__': None}
780 locs = {}
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000781 try:
782 value = eval(content, globs, locs)
783 except TypeError as e:
784 e.args = list(e.args) + [content]
785 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000786 assert locs == {}, locs
787 assert globs == {'__builtins__': None}, globs
788 return value
789
790
benrg@chromium.org609b7982013-02-07 16:44:46 +0000791def match_configs(expr, config_variables, all_configs):
792 """Returns the configs from |all_configs| that match the |expr|, where
793 the elements of |all_configs| are tuples of values for the |config_variables|.
794 Example:
795 >>> match_configs(expr = "(foo==1 or foo==2) and bar=='b'",
796 config_variables = ["foo", "bar"],
797 all_configs = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
798 [(1, 'b'), (2, 'b')]
799 """
800 return [
801 config for config in all_configs
802 if eval(expr, dict(zip(config_variables, config)))
803 ]
804
805
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000806def verify_variables(variables):
807 """Verifies the |variables| dictionary is in the expected format."""
808 VALID_VARIABLES = [
809 KEY_TOUCHED,
810 KEY_TRACKED,
811 KEY_UNTRACKED,
812 'command',
813 'read_only',
814 ]
815 assert isinstance(variables, dict), variables
816 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
817 for name, value in variables.iteritems():
818 if name == 'read_only':
819 assert value in (True, False, None), value
820 else:
821 assert isinstance(value, list), value
822 assert all(isinstance(i, basestring) for i in value), value
823
824
benrg@chromium.org609b7982013-02-07 16:44:46 +0000825def verify_ast(expr, variables_and_values):
826 """Verifies that |expr| is of the form
827 expr ::= expr ( "or" | "and" ) expr
828 | identifier "==" ( string | int )
829 Also collects the variable identifiers and string/int values in the dict
830 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
831 """
832 assert isinstance(expr, (ast.BoolOp, ast.Compare))
833 if isinstance(expr, ast.BoolOp):
834 assert isinstance(expr.op, (ast.And, ast.Or))
835 for subexpr in expr.values:
836 verify_ast(subexpr, variables_and_values)
837 else:
838 assert isinstance(expr.left.ctx, ast.Load)
839 assert len(expr.ops) == 1
840 assert isinstance(expr.ops[0], ast.Eq)
841 var_values = variables_and_values.setdefault(expr.left.id, set())
842 rhs = expr.comparators[0]
843 assert isinstance(rhs, (ast.Str, ast.Num))
844 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
845
846
847def verify_condition(condition, variables_and_values):
848 """Verifies the |condition| dictionary is in the expected format.
849 See verify_ast() for the meaning of |variables_and_values|.
850 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000851 VALID_INSIDE_CONDITION = ['variables']
852 assert isinstance(condition, list), condition
benrg@chromium.org609b7982013-02-07 16:44:46 +0000853 assert len(condition) == 2, condition
854 expr, then = condition
855
856 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
857 verify_ast(test_ast.body, variables_and_values)
858
859 assert isinstance(then, dict), then
860 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
861 verify_variables(then['variables'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000862
863
benrg@chromium.org609b7982013-02-07 16:44:46 +0000864def verify_root(value, variables_and_values):
865 """Verifies that |value| is the parsed form of a valid .isolate file.
866 See verify_ast() for the meaning of |variables_and_values|.
867 """
868 VALID_ROOTS = ['includes', 'conditions']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000869 assert isinstance(value, dict), value
870 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000871
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000872 includes = value.get('includes', [])
873 assert isinstance(includes, list), includes
874 for include in includes:
875 assert isinstance(include, basestring), include
876
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000877 conditions = value.get('conditions', [])
878 assert isinstance(conditions, list), conditions
879 for condition in conditions:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000880 verify_condition(condition, variables_and_values)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000881
882
benrg@chromium.org609b7982013-02-07 16:44:46 +0000883def remove_weak_dependencies(values, key, item, item_configs):
884 """Removes any configs from this key if the item is already under a
885 strong key.
886 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000887 if key == KEY_TOUCHED:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000888 item_configs = set(item_configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000889 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000890 try:
891 item_configs -= values[stronger_key][item]
892 except KeyError:
893 pass
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000894
benrg@chromium.org609b7982013-02-07 16:44:46 +0000895 return item_configs
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000896
897
benrg@chromium.org609b7982013-02-07 16:44:46 +0000898def remove_repeated_dependencies(folders, key, item, item_configs):
899 """Removes any configs from this key if the item is in a folder that is
900 already included."""
csharp@chromium.org31176252012-11-02 13:04:40 +0000901
902 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000903 item_configs = set(item_configs)
904 for (folder, configs) in folders.iteritems():
csharp@chromium.org31176252012-11-02 13:04:40 +0000905 if folder != item and item.startswith(folder):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000906 item_configs -= configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000907
benrg@chromium.org609b7982013-02-07 16:44:46 +0000908 return item_configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000909
910
911def get_folders(values_dict):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000912 """Returns a dict of all the folders in the given value_dict."""
913 return dict(
914 (item, configs) for (item, configs) in values_dict.iteritems()
915 if item.endswith('/')
916 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000917
918
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000919def invert_map(variables):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000920 """Converts {config: {deptype: list(depvals)}} to
921 {deptype: {depval: set(configs)}}.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000922 """
923 KEYS = (
924 KEY_TOUCHED,
925 KEY_TRACKED,
926 KEY_UNTRACKED,
927 'command',
928 'read_only',
929 )
930 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000931 for config, values in variables.iteritems():
932 for key in KEYS:
933 if key == 'command':
934 items = [tuple(values[key])] if key in values else []
935 elif key == 'read_only':
936 items = [values[key]] if key in values else []
937 else:
938 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
939 items = values.get(key, [])
940 for item in items:
941 out[key].setdefault(item, set()).add(config)
942 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000943
944
benrg@chromium.org609b7982013-02-07 16:44:46 +0000945def reduce_inputs(values):
946 """Reduces the output of invert_map() to the strictest minimum list.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000947
benrg@chromium.org609b7982013-02-07 16:44:46 +0000948 Looks at each individual file and directory, maps where they are used and
949 reconstructs the inverse dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000950
benrg@chromium.org609b7982013-02-07 16:44:46 +0000951 Returns the minimized dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000952 """
953 KEYS = (
954 KEY_TOUCHED,
955 KEY_TRACKED,
956 KEY_UNTRACKED,
957 'command',
958 'read_only',
959 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000960
961 # Folders can only live in KEY_UNTRACKED.
962 folders = get_folders(values.get(KEY_UNTRACKED, {}))
963
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000964 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000965 for key in KEYS:
966 for item, item_configs in values.get(key, {}).iteritems():
967 item_configs = remove_weak_dependencies(values, key, item, item_configs)
968 item_configs = remove_repeated_dependencies(
969 folders, key, item, item_configs)
970 if item_configs:
971 out[key][item] = item_configs
972 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000973
974
benrg@chromium.org609b7982013-02-07 16:44:46 +0000975def convert_map_to_isolate_dict(values, config_variables):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000976 """Regenerates back a .isolate configuration dict from files and dirs
977 mappings generated from reduce_inputs().
978 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000979 # Gather a list of configurations for set inversion later.
980 all_mentioned_configs = set()
981 for configs_by_item in values.itervalues():
982 for configs in configs_by_item.itervalues():
983 all_mentioned_configs.update(configs)
984
985 # Invert the mapping to make it dict first.
986 conditions = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000987 for key in values:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000988 for item, configs in values[key].iteritems():
989 then = conditions.setdefault(frozenset(configs), {})
990 variables = then.setdefault('variables', {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000991
benrg@chromium.org609b7982013-02-07 16:44:46 +0000992 if item in (True, False):
993 # One-off for read_only.
994 variables[key] = item
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000995 else:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000996 assert item
997 if isinstance(item, tuple):
998 # One-off for command.
999 # Do not merge lists and do not sort!
1000 # Note that item is a tuple.
1001 assert key not in variables
1002 variables[key] = list(item)
1003 else:
1004 # The list of items (files or dirs). Append the new item and keep
1005 # the list sorted.
1006 l = variables.setdefault(key, [])
1007 l.append(item)
1008 l.sort()
1009
1010 if all_mentioned_configs:
1011 config_values = map(set, zip(*all_mentioned_configs))
1012 sef = short_expression_finder.ShortExpressionFinder(
1013 zip(config_variables, config_values))
1014
1015 conditions = sorted(
1016 [sef.get_expr(configs), then] for configs, then in conditions.iteritems())
1017 return {'conditions': conditions}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001018
1019
1020### Internal state files.
1021
1022
benrg@chromium.org609b7982013-02-07 16:44:46 +00001023class ConfigSettings(object):
1024 """Represents the dependency variables for a single build configuration.
1025 The structure is immutable.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001026 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001027 def __init__(self, config, values):
1028 self.config = config
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001029 verify_variables(values)
1030 self.touched = sorted(values.get(KEY_TOUCHED, []))
1031 self.tracked = sorted(values.get(KEY_TRACKED, []))
1032 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
1033 self.command = values.get('command', [])[:]
1034 self.read_only = values.get('read_only')
1035
1036 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001037 assert not (self.config and rhs.config) or (self.config == rhs.config)
maruel@chromium.org669edcb2012-11-02 19:16:14 +00001038 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001039 var = {
1040 KEY_TOUCHED: sorted(self.touched + rhs.touched),
1041 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
1042 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
1043 'command': self.command or rhs.command,
1044 'read_only': rhs.read_only if self.read_only is None else self.read_only,
1045 }
benrg@chromium.org609b7982013-02-07 16:44:46 +00001046 return ConfigSettings(self.config or rhs.config, var)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001047
1048 def flatten(self):
1049 out = {}
1050 if self.command:
1051 out['command'] = self.command
1052 if self.touched:
1053 out[KEY_TOUCHED] = self.touched
1054 if self.tracked:
1055 out[KEY_TRACKED] = self.tracked
1056 if self.untracked:
1057 out[KEY_UNTRACKED] = self.untracked
1058 if self.read_only is not None:
1059 out['read_only'] = self.read_only
1060 return out
1061
1062
1063class Configs(object):
1064 """Represents a processed .isolate file.
1065
benrg@chromium.org609b7982013-02-07 16:44:46 +00001066 Stores the file in a processed way, split by configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001067 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001068 def __init__(self, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001069 self.file_comment = file_comment
benrg@chromium.org609b7982013-02-07 16:44:46 +00001070 # The keys of by_config are tuples of values for the configuration
1071 # variables. The names of the variables (which must be the same for
1072 # every by_config key) are kept in config_variables. Initially by_config
1073 # is empty and we don't know what configuration variables will be used,
1074 # so config_variables also starts out empty. It will be set by the first
1075 # call to union() or merge_dependencies().
1076 self.by_config = {}
1077 self.config_variables = ()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001078
1079 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +00001080 """Adds variables from rhs (a Configs) to the existing variables.
1081 """
1082 config_variables = self.config_variables
1083 if not config_variables:
1084 config_variables = rhs.config_variables
1085 else:
1086 # We can't proceed if this isn't true since we don't know the correct
1087 # default values for extra variables. The variables are sorted so we
1088 # don't need to worry about permutations.
1089 if rhs.config_variables and rhs.config_variables != config_variables:
1090 raise ExecutionError(
1091 'Variables in merged .isolate files do not match: %r and %r' % (
1092 config_variables, rhs.config_variables))
1093
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001094 # Takes the first file comment, prefering lhs.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001095 out = Configs(self.file_comment or rhs.file_comment)
1096 out.config_variables = config_variables
1097 for config in set(self.by_config) | set(rhs.by_config):
1098 out.by_config[config] = union(
1099 self.by_config.get(config), rhs.by_config.get(config))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001100 return out
1101
benrg@chromium.org609b7982013-02-07 16:44:46 +00001102 def merge_dependencies(self, values, config_variables, configs):
1103 """Adds new dependencies to this object for the given configurations.
1104 Arguments:
1105 values: A variables dict as found in a .isolate file, e.g.,
1106 {KEY_TOUCHED: [...], 'command': ...}.
1107 config_variables: An ordered list of configuration variables, e.g.,
1108 ["OS", "chromeos"]. If this object already contains any dependencies,
1109 the configuration variables must match.
1110 configs: a list of tuples of values of the configuration variables,
1111 e.g., [("mac", 0), ("linux", 1)]. The dependencies in |values|
1112 are added to all of these configurations, and other configurations
1113 are unchanged.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001114 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001115 if not values:
1116 return
1117
1118 if not self.config_variables:
1119 self.config_variables = config_variables
1120 else:
1121 # See comment in Configs.union().
1122 assert self.config_variables == config_variables
1123
1124 for config in configs:
1125 self.by_config[config] = union(
1126 self.by_config.get(config), ConfigSettings(config, values))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001127
1128 def flatten(self):
1129 """Returns a flat dictionary representation of the configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001130 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001131 return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
1132
1133 def make_isolate_file(self):
1134 """Returns a dictionary suitable for writing to a .isolate file.
1135 """
1136 dependencies_by_config = self.flatten()
1137 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
1138 return convert_map_to_isolate_dict(configs_by_dependency,
1139 self.config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001140
1141
benrg@chromium.org609b7982013-02-07 16:44:46 +00001142# TODO(benrg): Remove this function when no old-format files are left.
1143def convert_old_to_new_format(value):
1144 """Converts from the old .isolate format, which only has one variable (OS),
1145 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
1146 and allows conditions that depend on the set of all OSes, to the new format,
1147 which allows any set of variables, has no hardcoded values, and only allows
1148 explicit positive tests of variable values.
1149 """
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001150 conditions = value.get('conditions', [])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001151 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
1152 return value # Nothing to change
1153
1154 def parse_condition(cond):
1155 return re.match(r'OS=="(\w+)"\Z', cond[0]).group(1)
1156
1157 oses = set(map(parse_condition, conditions))
1158 default_oses = set(['linux', 'mac', 'win'])
1159 oses = sorted(oses | default_oses)
1160
1161 def if_not_os(not_os, then):
1162 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
1163 return [expr, then]
1164
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001165 conditions = [
1166 cond[:2] for cond in conditions if cond[1]
1167 ] + [
1168 if_not_os(parse_condition(cond), cond[2])
benrg@chromium.org609b7982013-02-07 16:44:46 +00001169 for cond in conditions if len(cond) == 3
1170 ]
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001171
benrg@chromium.org609b7982013-02-07 16:44:46 +00001172 if 'variables' in value:
1173 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
1174 conditions.sort()
1175
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +00001176 value = value.copy()
1177 value['conditions'] = conditions
benrg@chromium.org609b7982013-02-07 16:44:46 +00001178 return value
1179
1180
1181def load_isolate_as_config(isolate_dir, value, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001182 """Parses one .isolate file and returns a Configs() instance.
1183
1184 |value| is the loaded dictionary that was defined in the gyp file.
1185
1186 The expected format is strict, anything diverting from the format below will
1187 throw an assert:
1188 {
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001189 'includes': [
1190 'foo.isolate',
1191 ],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001192 'conditions': [
benrg@chromium.org609b7982013-02-07 16:44:46 +00001193 ['OS=="vms" and foo=42', {
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001194 'variables': {
benrg@chromium.org609b7982013-02-07 16:44:46 +00001195 'command': [
1196 ...
1197 ],
1198 'isolate_dependency_tracked': [
1199 ...
1200 ],
1201 'isolate_dependency_untracked': [
1202 ...
1203 ],
1204 'read_only': False,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001205 },
1206 }],
1207 ...
1208 ],
1209 }
1210 """
benrg@chromium.org609b7982013-02-07 16:44:46 +00001211 value = convert_old_to_new_format(value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001212
benrg@chromium.org609b7982013-02-07 16:44:46 +00001213 variables_and_values = {}
1214 verify_root(value, variables_and_values)
1215 if variables_and_values:
1216 config_variables, config_values = zip(
1217 *sorted(variables_and_values.iteritems()))
1218 all_configs = list(itertools.product(*config_values))
1219 else:
1220 config_variables = None
1221 all_configs = []
1222
1223 isolate = Configs(file_comment)
1224
1225 # Add configuration-specific variables.
1226 for expr, then in value.get('conditions', []):
1227 configs = match_configs(expr, config_variables, all_configs)
1228 isolate.merge_dependencies(then['variables'], config_variables, configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001229
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001230 # Load the includes.
1231 for include in value.get('includes', []):
1232 if os.path.isabs(include):
1233 raise ExecutionError(
1234 'Failed to load configuration; absolute include path \'%s\'' %
1235 include)
1236 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
1237 with open(included_isolate, 'r') as f:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001238 included_isolate = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001239 os.path.dirname(included_isolate),
1240 eval_content(f.read()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001241 None)
1242 isolate = union(isolate, included_isolate)
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001243
benrg@chromium.org609b7982013-02-07 16:44:46 +00001244 return isolate
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001245
1246
benrg@chromium.org609b7982013-02-07 16:44:46 +00001247def load_isolate_for_config(isolate_dir, content, variables):
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001248 """Loads the .isolate file and returns the information unprocessed but
1249 filtered for the specific OS.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001250
1251 Returns the command, dependencies and read_only flag. The dependencies are
1252 fixed to use os.path.sep.
1253 """
1254 # Load the .isolate file, process its conditions, retrieve the command and
1255 # dependencies.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001256 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
1257 try:
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001258 config_name = tuple(variables[var] for var in isolate.config_variables)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001259 except KeyError:
1260 raise ExecutionError(
1261 'These configuration variables were missing from the command line: %s' %
1262 ', '.join(sorted(set(isolate.config_variables) - set(variables))))
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001263 config = isolate.by_config.get(config_name)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001264 if not config:
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001265 raise ExecutionError(
1266 'Failed to load configuration for variable \'%s\' for config(s) \'%s\''
1267 '\nAvailable configs: %s' %
1268 (', '.join(isolate.config_variables),
1269 ', '.join(config_name),
1270 ', '.join(str(s) for s in isolate.by_config)))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001271 # Merge tracked and untracked variables, isolate.py doesn't care about the
1272 # trackability of the variables, only the build tool does.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001273 dependencies = [
1274 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1275 ]
1276 touched = [f.replace('/', os.path.sep) for f in config.touched]
1277 return config.command, dependencies, touched, config.read_only
1278
1279
maruel@chromium.orgdcdbfc82013-07-25 18:54:57 +00001280def save_isolated(isolated, data):
1281 """Writes one or multiple .isolated files.
1282
1283 Note: this reference implementation does not create child .isolated file so it
1284 always returns an empty list.
1285
1286 Returns the list of child isolated files that are included by |isolated|.
1287 """
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05001288 tools.write_json(isolated, data, True)
maruel@chromium.orgdcdbfc82013-07-25 18:54:57 +00001289 return []
1290
1291
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001292def chromium_save_isolated(isolated, data, variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001293 """Writes one or many .isolated files.
1294
1295 This slightly increases the cold cache cost but greatly reduce the warm cache
1296 cost by splitting low-churn files off the master .isolated file. It also
1297 reduces overall isolateserver memcache consumption.
1298 """
1299 slaves = []
1300
1301 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001302 new_slave = {
1303 'algo': data['algo'],
1304 'files': {},
1305 'os': data['os'],
1306 'version': data['version'],
1307 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001308 for f in data['files'].keys():
1309 if f.startswith(prefix):
1310 new_slave['files'][f] = data['files'].pop(f)
1311 if new_slave['files']:
1312 slaves.append(new_slave)
1313
1314 # Split test/data/ in its own .isolated file.
1315 extract_into_included_isolated(os.path.join('test', 'data', ''))
1316
1317 # Split everything out of PRODUCT_DIR in its own .isolated file.
1318 if variables.get('PRODUCT_DIR'):
1319 extract_into_included_isolated(variables['PRODUCT_DIR'])
1320
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001321 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001322 for index, f in enumerate(slaves):
1323 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05001324 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001325 data.setdefault('includes', []).append(
1326 isolateserver.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001327 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001328
maruel@chromium.orgdcdbfc82013-07-25 18:54:57 +00001329 files.extend(save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001330 return files
1331
1332
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001333class Flattenable(object):
1334 """Represents data that can be represented as a json file."""
1335 MEMBERS = ()
1336
1337 def flatten(self):
1338 """Returns a json-serializable version of itself.
1339
1340 Skips None entries.
1341 """
1342 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1343 return dict((member, value) for member, value in items if value is not None)
1344
1345 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001346 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001347 """Loads a flattened version."""
1348 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001349 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001350 for member in out.MEMBERS:
1351 if member in data:
1352 # Access to a protected member XXX of a client class
1353 # pylint: disable=W0212
1354 out._load_member(member, data.pop(member))
1355 if data:
1356 raise ValueError(
1357 'Found unexpected entry %s while constructing an object %s' %
1358 (data, cls.__name__), data, cls.__name__)
1359 return out
1360
1361 def _load_member(self, member, value):
1362 """Loads a member into self."""
1363 setattr(self, member, value)
1364
1365 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001366 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001367 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001368 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05001369 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001370 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001371 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001372 # On failure, loads the default instance.
1373 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001374 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001375 return out
1376
1377
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001378class SavedState(Flattenable):
1379 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001380
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001381 This file caches the items calculated by this script and is used to increase
1382 the performance of the script. This file is not loaded by run_isolated.py.
1383 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001384
1385 It is important to note that the 'files' dict keys are using native OS path
1386 separator instead of '/' used in .isolate file.
1387 """
1388 MEMBERS = (
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001389 # Algorithm used to generate the hash. The only supported value is at the
1390 # time of writting 'sha-1'.
1391 'algo',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001392 # Cache of the processed command. This value is saved because .isolated
1393 # files are never loaded by isolate.py so it's the only way to load the
1394 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001395 'command',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001396 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001397 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001398 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001399 'isolate_file',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001400 # List of included .isolated files. Used to support/remember 'slave'
1401 # .isolated files. Relative path to isolated_basedir.
1402 'child_isolated_files',
1403 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001404 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001405 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001406 'relative_cwd',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001407 # GYP variables used to generate the .isolated file. Variables are saved so
1408 # a user can use isolate.py after building and the GYP variables are still
1409 # defined.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001410 'variables',
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001411 # Version of the file format in format 'major.minor'. Any non-breaking
1412 # change must update minor. Any breaking change must update major.
1413 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001414 )
1415
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001416 def __init__(self, isolated_basedir):
1417 """Creates an empty SavedState.
1418
1419 |isolated_basedir| is the directory where the .isolated and .isolated.state
1420 files are saved.
1421 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001422 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001423 assert os.path.isabs(isolated_basedir), isolated_basedir
1424 assert os.path.isdir(isolated_basedir), isolated_basedir
1425 self.isolated_basedir = isolated_basedir
1426
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001427 # The default algorithm used.
1428 self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001429 self.command = []
1430 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001431 self.isolate_file = None
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001432 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001433 self.read_only = None
1434 self.relative_cwd = None
benrg@chromium.org609b7982013-02-07 16:44:46 +00001435 self.variables = {'OS': get_flavor()}
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001436 # The current version.
1437 self.version = '1.0'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001438
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001439 def update(self, isolate_file, variables):
1440 """Updates the saved state with new data to keep GYP variables and internal
1441 reference to the original .isolate file.
1442 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +00001443 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001444 # Convert back to a relative path. On Windows, if the isolate and
1445 # isolated files are on different drives, isolate_file will stay an absolute
1446 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001447 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001448
1449 # The same .isolate file should always be used to generate the .isolated and
1450 # .isolated.state.
1451 assert isolate_file == self.isolate_file or not self.isolate_file, (
1452 isolate_file, self.isolate_file)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001453 self.isolate_file = isolate_file
1454 self.variables.update(variables)
1455
1456 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
1457 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001458
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001459 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001460 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001461 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001462 self.command = command
1463 # Add new files.
1464 for f in infiles:
1465 self.files.setdefault(f, {})
1466 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001467 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001468 # Prune extraneous files that are not a dependency anymore.
1469 for f in set(self.files).difference(set(infiles).union(touched)):
1470 del self.files[f]
1471 if read_only is not None:
1472 self.read_only = read_only
1473 self.relative_cwd = relative_cwd
1474
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001475 def to_isolated(self):
1476 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001477
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001478 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001479 """
1480 def strip(data):
1481 """Returns a 'files' entry with only the whitelisted keys."""
1482 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
1483
1484 out = {
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001485 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001486 'files': dict(
1487 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001488 'os': self.variables['OS'],
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001489 'version': self.version,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001490 }
1491 if self.command:
1492 out['command'] = self.command
1493 if self.read_only is not None:
1494 out['read_only'] = self.read_only
1495 if self.relative_cwd:
1496 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001497 return out
1498
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001499 @property
1500 def isolate_filepath(self):
1501 """Returns the absolute path of self.isolate_file."""
1502 return os.path.normpath(
1503 os.path.join(self.isolated_basedir, self.isolate_file))
1504
1505 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001506 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001507 def load(cls, data, isolated_basedir): # pylint: disable=W0221
1508 """Special case loading to disallow different OS.
1509
1510 It is not possible to load a .isolated.state files from a different OS, this
1511 file is saved in OS-specific format.
1512 """
1513 out = super(SavedState, cls).load(data, isolated_basedir)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001514 if 'os' in data:
1515 out.variables['OS'] = data['os']
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001516
1517 # Converts human readable form back into the proper class type.
1518 algo = data.get('algo', 'sha-1')
1519 if not algo in isolateserver.SUPPORTED_ALGOS:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001520 raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001521 out.algo = isolateserver.SUPPORTED_ALGOS[algo]
1522
1523 # For example, 1.1 is guaranteed to be backward compatible with 1.0 code.
1524 if not re.match(r'^(\d+)\.(\d+)$', out.version):
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001525 raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001526 if out.version.split('.', 1)[0] != '1':
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001527 raise isolateserver.ConfigError(
1528 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001529
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001530 # The .isolate file must be valid. It could be absolute on Windows if the
1531 # drive containing the .isolate and the drive containing the .isolated files
1532 # differ.
1533 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
1534 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001535 return out
1536
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001537 def flatten(self):
1538 """Makes sure 'algo' is in human readable form."""
1539 out = super(SavedState, self).flatten()
1540 out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
1541 return out
1542
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001543 def __str__(self):
1544 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001545 out += ' command: %s\n' % self.command
1546 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001547 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001548 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +00001549 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001550 out += ' child_isolated_files: %s\n' % self.child_isolated_files
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001551 out += ' variables: %s' % ''.join(
1552 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1553 out += ')'
1554 return out
1555
1556
1557class CompleteState(object):
1558 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001559 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001560 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +00001561 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001562 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001563 # Contains the data to ease developer's use-case but that is not strictly
1564 # necessary.
1565 self.saved_state = saved_state
1566
1567 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001568 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001569 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001570 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001571 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001572 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001573 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001574 SavedState.load_file(
1575 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001576
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001577 def load_isolate(self, cwd, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001578 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001579 .isolate file.
1580
1581 Processes the loaded data, deduce root_dir, relative_cwd.
1582 """
1583 # Make sure to not depend on os.getcwd().
1584 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001585 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001586 logging.info(
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001587 'CompleteState.load_isolate(%s, %s, %s, %s)',
1588 cwd, isolate_file, variables, ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001589 relative_base_dir = os.path.dirname(isolate_file)
1590
1591 # Processes the variables and update the saved state.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001592 variables = process_variables(cwd, variables, relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001593 self.saved_state.update(isolate_file, variables)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001594 variables = self.saved_state.variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001595
1596 with open(isolate_file, 'r') as f:
1597 # At that point, variables are not replaced yet in command and infiles.
1598 # infiles may contain directory entries and is in posix style.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001599 command, infiles, touched, read_only = load_isolate_for_config(
1600 os.path.dirname(isolate_file), f.read(), variables)
1601 command = [eval_variables(i, variables) for i in command]
1602 infiles = [eval_variables(f, variables) for f in infiles]
1603 touched = [eval_variables(f, variables) for f in touched]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001604 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +00001605 # form '../../foo/bar'. Note that path variables must be taken in account
1606 # too, add them as if they were input files.
1607 path_variables = [variables[v] for v in PATH_VARIABLES if v in variables]
1608 root_dir = determine_root_dir(
1609 relative_base_dir, infiles + touched + path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001610 # The relative directory is automatically determined by the relative path
1611 # between root_dir and the directory containing the .isolate file,
1612 # isolate_base_dir.
1613 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
benrg@chromium.org9ae72862013-02-11 05:05:51 +00001614 # Now that we know where the root is, check that the PATH_VARIABLES point
1615 # inside it.
1616 for i in PATH_VARIABLES:
1617 if i in variables:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001618 if not file_path.path_starts_with(
benrg@chromium.org9ae72862013-02-11 05:05:51 +00001619 root_dir, os.path.join(relative_base_dir, variables[i])):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +00001620 raise isolateserver.MappingError(
maruel@chromium.org75584e22013-06-20 01:40:24 +00001621 'Path variable %s=%r points outside the inferred root directory'
1622 ' %s' % (i, variables[i], root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001623 # Normalize the files based to root_dir. It is important to keep the
1624 # trailing os.path.sep at that step.
1625 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001626 file_path.relpath(
1627 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001628 for f in infiles
1629 ]
1630 touched = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001631 file_path.relpath(
1632 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001633 for f in touched
1634 ]
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001635 follow_symlinks = variables['OS'] != 'win'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001636 # Expand the directories by listing each file inside. Up to now, trailing
1637 # os.path.sep must be kept. Do not expand 'touched'.
1638 infiles = expand_directories_and_symlinks(
1639 root_dir,
1640 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001641 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001642 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +00001643 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001644
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001645 # If we ignore broken items then remove any missing touched items.
1646 if ignore_broken_items:
1647 original_touched_count = len(touched)
1648 touched = [touch for touch in touched if os.path.exists(touch)]
1649
1650 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +00001651 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001652 len(touched) - original_touched_count)
1653
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001654 # Finally, update the new data to be able to generate the foo.isolated file,
1655 # the file that is used by run_isolated.py.
1656 self.saved_state.update_isolated(
1657 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001658 logging.debug(self)
1659
maruel@chromium.org9268f042012-10-17 17:36:41 +00001660 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001661 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001662
maruel@chromium.org9268f042012-10-17 17:36:41 +00001663 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1664 file is tainted.
1665
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001666 See process_input() for more information.
1667 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001668 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001669 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001670 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001671 else:
1672 filepath = os.path.join(self.root_dir, infile)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001673 self.saved_state.files[infile] = process_input(
1674 filepath,
1675 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +00001676 self.saved_state.read_only,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001677 self.saved_state.variables['OS'],
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001678 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001679
1680 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001681 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001682 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001683 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001684 self.isolated_filepath,
1685 self.saved_state.to_isolated(),
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001686 self.saved_state.variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001687 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001688 total_bytes = sum(
1689 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001690 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001691 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001692 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001693 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001694 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05001695 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001696
1697 @property
1698 def root_dir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001699 """Returns the absolute path of the root_dir to reference the .isolate file
1700 via relative_cwd.
1701
1702 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
1703 to isolate_filepath.
1704 """
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001705 if not self.saved_state.isolate_file:
1706 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001707 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001708 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001709 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001710 root_dir = isolate_dir
1711 else:
maruel@chromium.org87557b92013-10-16 18:04:11 +00001712 if not isolate_dir.endswith(self.saved_state.relative_cwd):
1713 raise ExecutionError(
1714 ('Make sure the .isolate file is in the directory that will be '
1715 'used as the relative directory. It is currently in %s and should '
1716 'be in %s') % (isolate_dir, self.saved_state.relative_cwd))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001717 # Walk back back to the root directory.
1718 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001719 return file_path.get_native_path_case(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001720
1721 @property
1722 def resultdir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001723 """Returns the absolute path containing the .isolated file.
1724
1725 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
1726 path as the value.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001727 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001728 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001729
1730 def __str__(self):
1731 def indent(data, indent_length):
1732 """Indents text."""
1733 spacing = ' ' * indent_length
1734 return ''.join(spacing + l for l in str(data).splitlines(True))
1735
1736 out = '%s(\n' % self.__class__.__name__
1737 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001738 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1739 return out
1740
1741
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001742def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001743 """Loads a CompleteState.
1744
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001745 This includes data from .isolate and .isolated.state files. Never reads the
1746 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001747
1748 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001749 options: Options instance generated with OptionParserIsolate. For either
1750 options.isolate and options.isolated, if the value is set, it is an
1751 absolute path.
1752 cwd: base directory to be used when loading the .isolate file.
1753 subdir: optional argument to only process file in the subdirectory, relative
1754 to CompleteState.root_dir.
1755 skip_update: Skip trying to load the .isolate file and processing the
1756 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001757 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001758 assert not options.isolate or os.path.isabs(options.isolate)
1759 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001760 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001761 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001762 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001763 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001764 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001765 else:
1766 # Constructs a dummy object that cannot be saved. Useful for temporary
1767 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001768 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001769
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001770 if not options.isolate:
1771 if not complete_state.saved_state.isolate_file:
1772 if not skip_update:
1773 raise ExecutionError('A .isolate file is required.')
1774 isolate = None
1775 else:
1776 isolate = complete_state.saved_state.isolate_filepath
1777 else:
1778 isolate = options.isolate
1779 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001780 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001781 options.isolate, complete_state.saved_state.isolated_basedir)
1782 if rel_isolate != complete_state.saved_state.isolate_file:
1783 raise ExecutionError(
1784 '%s and %s do not match.' % (
1785 options.isolate, complete_state.saved_state.isolate_file))
1786
1787 if not skip_update:
1788 # Then load the .isolate and expands directories.
1789 complete_state.load_isolate(
1790 cwd, isolate, options.variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001791
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001792 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001793 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001794 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001795 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1796 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001797
1798 if not skip_update:
1799 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001800 return complete_state
1801
1802
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001803def read_trace_as_isolate_dict(complete_state, trace_blacklist):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001804 """Reads a trace and returns the .isolate dictionary.
1805
1806 Returns exceptions during the log parsing so it can be re-raised.
1807 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001808 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001809 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001810 if not os.path.isfile(logfile):
1811 raise ExecutionError(
1812 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1813 try:
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001814 data = api.parse_log(logfile, trace_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001815 exceptions = [i['exception'] for i in data if 'exception' in i]
1816 results = (i['results'] for i in data if 'results' in i)
1817 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1818 files = set(sum((result.existent for result in results_stripped), []))
1819 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001820 value = generate_isolate(
1821 tracked,
1822 [],
1823 touched,
1824 complete_state.root_dir,
1825 complete_state.saved_state.variables,
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001826 complete_state.saved_state.relative_cwd,
1827 trace_blacklist)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001828 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001829 except trace_inputs.TracingFailure, e:
1830 raise ExecutionError(
1831 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001832 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001833
1834
1835def print_all(comment, data, stream):
1836 """Prints a complete .isolate file and its top-level file comment into a
1837 stream.
1838 """
1839 if comment:
1840 stream.write(comment)
1841 pretty_print(data, stream)
1842
1843
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001844def merge(complete_state, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001845 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001846 value, exceptions = read_trace_as_isolate_dict(
1847 complete_state, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001848
1849 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001850 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001851 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001852 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001853 prev_config = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001854 isolate_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001855 eval_content(prev_content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001856 extract_comment(prev_content))
1857 new_config = load_isolate_as_config(isolate_dir, value, '')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001858 config = union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001859 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001860 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001861 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001862 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001863 if exceptions:
1864 # It got an exception, raise the first one.
1865 raise \
1866 exceptions[0][0], \
1867 exceptions[0][1], \
1868 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001869
1870
maruel@chromium.org29029882013-08-30 12:15:40 +00001871### Commands.
1872
1873
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001874def CMDarchive(parser, args):
1875 """Creates a .isolated file and uploads the tree to an isolate server.
maruel@chromium.org29029882013-08-30 12:15:40 +00001876
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001877 All the files listed in the .isolated file are put in the isolate server
1878 cache via isolateserver.py.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001879 """
maruel@chromium.org9268f042012-10-17 17:36:41 +00001880 parser.add_option('--subdir', help='Filters to a subdirectory')
1881 options, args = parser.parse_args(args)
1882 if args:
1883 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001884
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001885 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001886 success = False
1887 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001888 complete_state = load_complete_state(
1889 options, os.getcwd(), options.subdir, False)
1890 if not options.outdir:
1891 options.outdir = os.path.join(
1892 os.path.dirname(complete_state.isolated_filepath), 'hashtable')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001893 # Make sure that complete_state isn't modified until save_files() is
1894 # called, because any changes made to it here will propagate to the files
1895 # created (which is probably not intended).
1896 complete_state.save_files()
1897
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001898 infiles = complete_state.saved_state.files
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001899 # Add all the .isolated files.
maruel@chromium.org87f11962013-04-10 21:27:28 +00001900 isolated_hash = []
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001901 isolated_files = [
1902 options.isolated,
1903 ] + complete_state.saved_state.child_isolated_files
1904 for item in isolated_files:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001905 item_path = os.path.join(
1906 os.path.dirname(complete_state.isolated_filepath), item)
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001907 # Do not use isolateserver.hash_file() here because the file is
maruel@chromium.org87f11962013-04-10 21:27:28 +00001908 # likely smallish (under 500kb) and its file size is needed.
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001909 with open(item_path, 'rb') as f:
1910 content = f.read()
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001911 isolated_hash.append(
1912 complete_state.saved_state.algo(content).hexdigest())
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001913 isolated_metadata = {
maruel@chromium.org87f11962013-04-10 21:27:28 +00001914 'h': isolated_hash[-1],
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001915 's': len(content),
1916 'priority': '0'
1917 }
1918 infiles[item_path] = isolated_metadata
1919
1920 logging.info('Creating content addressed object store with %d item',
1921 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001922
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001923 if file_path.is_url(options.outdir):
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001924 isolateserver.upload_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001925 base_url=options.outdir,
1926 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00001927 infiles=infiles,
1928 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001929 else:
1930 recreate_tree(
1931 outdir=options.outdir,
1932 indir=complete_state.root_dir,
1933 infiles=infiles,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00001934 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001935 as_hash=True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001936 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001937 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001938 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001939 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001940 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001941 if not success and os.path.isfile(options.isolated):
1942 os.remove(options.isolated)
maruel@chromium.org87f11962013-04-10 21:27:28 +00001943 return not success
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001944
1945
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001946def CMDcheck(parser, args):
1947 """Checks that all the inputs are present and generates .isolated."""
1948 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)
1952
1953 complete_state = load_complete_state(
1954 options, os.getcwd(), options.subdir, False)
1955
1956 # Nothing is done specifically. Just store the result and state.
1957 complete_state.save_files()
1958 return 0
1959
1960
1961CMDhashtable = CMDarchive
1962
1963
maruel@chromium.orge5322512013-08-19 20:17:57 +00001964def CMDmerge(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001965 """Reads and merges the data from the trace back into the original .isolate.
1966
1967 Ignores --outdir.
1968 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001969 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001970 add_trace_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001971 options, args = parser.parse_args(args)
1972 if args:
1973 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001974
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001975 complete_state = load_complete_state(options, os.getcwd(), None, False)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001976 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001977 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001978 return 0
1979
1980
maruel@chromium.orge5322512013-08-19 20:17:57 +00001981def CMDread(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001982 """Reads the trace file generated with command 'trace'.
1983
1984 Ignores --outdir.
1985 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001986 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001987 add_trace_option(parser)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001988 parser.add_option(
1989 '--skip-refresh', action='store_true',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001990 help='Skip reading .isolate file and do not refresh the hash of '
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001991 'dependencies')
maruel@chromium.org29029882013-08-30 12:15:40 +00001992 parser.add_option(
1993 '-m', '--merge', action='store_true',
1994 help='merge the results back in the .isolate file instead of printing')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001995 options, args = parser.parse_args(args)
1996 if args:
1997 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001998
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001999 complete_state = load_complete_state(
2000 options, os.getcwd(), None, options.skip_refresh)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05002001 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002002 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
maruel@chromium.org29029882013-08-30 12:15:40 +00002003 if options.merge:
2004 merge(complete_state, blacklist)
2005 else:
2006 pretty_print(value, sys.stdout)
2007
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00002008 if exceptions:
2009 # It got an exception, raise the first one.
2010 raise \
2011 exceptions[0][0], \
2012 exceptions[0][1], \
2013 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002014 return 0
2015
2016
maruel@chromium.orge5322512013-08-19 20:17:57 +00002017def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002018 """Creates a directory with all the dependencies mapped into it.
2019
2020 Useful to test manually why a test is failing. The target executable is not
2021 run.
2022 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002023 parser.require_isolated = False
maruel@chromium.org9268f042012-10-17 17:36:41 +00002024 options, args = parser.parse_args(args)
2025 if args:
2026 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002027 complete_state = load_complete_state(options, os.getcwd(), None, False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002028
2029 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002030 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002031 'isolate', complete_state.root_dir)
2032 else:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05002033 if file_path.is_url(options.outdir):
maruel@chromium.org29029882013-08-30 12:15:40 +00002034 parser.error('Can\'t use url for --outdir with mode remap.')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002035 if not os.path.isdir(options.outdir):
2036 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00002037 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002038 if len(os.listdir(options.outdir)):
2039 raise ExecutionError('Can\'t remap in a non-empty directory')
2040 recreate_tree(
2041 outdir=options.outdir,
2042 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002043 infiles=complete_state.saved_state.files,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00002044 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00002045 as_hash=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002046 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002047 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002048
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002049 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002050 complete_state.save_files()
2051 return 0
2052
2053
maruel@chromium.orge5322512013-08-19 20:17:57 +00002054def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002055 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00002056 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002057 options, args = parser.parse_args(args)
2058 if args:
2059 parser.error('Unsupported argument: %s' % args)
2060
2061 if options.isolated:
2062 # Load the previous state if it was present. Namely, "foo.isolated.state".
2063 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002064 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002065 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002066 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002067 if not isolate:
maruel@chromium.org29029882013-08-30 12:15:40 +00002068 parser.error('--isolate is required.')
2069
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002070 with open(isolate, 'r') as f:
2071 content = f.read()
2072 config = load_isolate_as_config(
2073 os.path.dirname(os.path.abspath(isolate)),
2074 eval_content(content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00002075 extract_comment(content))
2076 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00002077 print('Updating %s' % isolate)
2078 with open(isolate, 'wb') as f:
2079 print_all(config.file_comment, data, f)
2080 return 0
2081
2082
maruel@chromium.org29029882013-08-30 12:15:40 +00002083@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00002084def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002085 """Runs the test executable in an isolated (temporary) directory.
2086
2087 All the dependencies are mapped into the temporary directory and the
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002088 directory is cleaned up after the target exits. Warning: if --outdir is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002089 specified, it is deleted upon exit.
2090
maruel@chromium.org29029882013-08-30 12:15:40 +00002091 Argument processing stops at -- and these arguments are appended to the
2092 command line of the target to run. For example, use:
2093 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002094 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002095 parser.require_isolated = False
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002096 parser.add_option(
2097 '--skip-refresh', action='store_true',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00002098 help='Skip reading .isolate file and do not refresh the hash of '
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002099 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002100 options, args = parser.parse_args(args)
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05002101 if options.outdir and file_path.is_url(options.outdir):
maruel@chromium.org29029882013-08-30 12:15:40 +00002102 parser.error('Can\'t use url for --outdir with mode run.')
2103
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002104 complete_state = load_complete_state(
2105 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002106 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002107 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00002108 raise ExecutionError('No command to run.')
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00002109
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002110 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002111 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002112 root_dir = complete_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002113 if not options.outdir:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002114 if not os.path.isabs(root_dir):
2115 root_dir = os.path.join(os.path.dirname(options.isolated), root_dir)
2116 options.outdir = run_isolated.make_temp_dir('isolate', root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002117 else:
2118 if not os.path.isdir(options.outdir):
2119 os.makedirs(options.outdir)
2120 recreate_tree(
2121 outdir=options.outdir,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002122 indir=root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002123 infiles=complete_state.saved_state.files,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00002124 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00002125 as_hash=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002126 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002127 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002128 if not os.path.isdir(cwd):
2129 # It can happen when no files are mapped from the directory containing the
2130 # .isolate file. But the directory must exist to be the current working
2131 # directory.
2132 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002133 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002134 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002135 logging.info('Running %s, cwd=%s' % (cmd, cwd))
2136 result = subprocess.call(cmd, cwd=cwd)
2137 finally:
2138 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00002139 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002140
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002141 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002142 complete_state.save_files()
2143 return result
2144
2145
maruel@chromium.org29029882013-08-30 12:15:40 +00002146@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00002147def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002148 """Traces the target using trace_inputs.py.
2149
2150 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002151 its child processes access. Then the 'merge' command can be used to generate
2152 an updated .isolate file out of it or the 'read' command to print it out to
2153 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002154
maruel@chromium.org29029882013-08-30 12:15:40 +00002155 Argument processing stops at -- and these arguments are appended to the
2156 command line of the target to run. For example, use:
2157 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002158 """
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002159 add_trace_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002160 parser.add_option(
2161 '-m', '--merge', action='store_true',
2162 help='After tracing, merge the results back in the .isolate file')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002163 parser.add_option(
2164 '--skip-refresh', action='store_true',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00002165 help='Skip reading .isolate file and do not refresh the hash of '
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002166 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002167 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00002168
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00002169 complete_state = load_complete_state(
2170 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002171 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002172 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00002173 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002174 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002175 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00002176 unicode(complete_state.root_dir),
2177 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00002178 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
2179 if not os.path.isfile(cmd[0]):
2180 raise ExecutionError(
2181 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002182 logging.info('Running %s, cwd=%s' % (cmd, cwd))
2183 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00002184 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002185 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002186 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002187 try:
2188 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002189 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002190 cmd,
2191 cwd,
2192 'default',
2193 True)
2194 except trace_inputs.TracingFailure, e:
2195 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
2196
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002197 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00002198 logging.error(
2199 'Tracer exited with %d, which means the tests probably failed so the '
2200 'trace is probably incomplete.', result)
2201 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002202
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002203 complete_state.save_files()
2204
2205 if options.merge:
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05002206 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002207 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002208
2209 return result
2210
2211
maruel@chromium.org712454d2013-04-04 17:52:34 +00002212def _process_variable_arg(_option, _opt, _value, parser):
2213 if not parser.rargs:
2214 raise optparse.OptionValueError(
2215 'Please use --variable FOO=BAR or --variable FOO BAR')
2216 k = parser.rargs.pop(0)
2217 if '=' in k:
2218 parser.values.variables.append(tuple(k.split('=', 1)))
2219 else:
2220 if not parser.rargs:
2221 raise optparse.OptionValueError(
2222 'Please use --variable FOO=BAR or --variable FOO BAR')
2223 v = parser.rargs.pop(0)
2224 parser.values.variables.append((k, v))
2225
2226
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002227def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002228 """Adds --isolated and --variable to an OptionParser."""
2229 parser.add_option(
2230 '-s', '--isolated',
2231 metavar='FILE',
2232 help='.isolated file to generate or read')
2233 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002234 parser.add_option(
2235 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002236 dest='isolated',
2237 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002238 default_variables = [('OS', get_flavor())]
2239 if sys.platform in ('win32', 'cygwin'):
2240 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
2241 else:
2242 default_variables.append(('EXECUTABLE_SUFFIX', ''))
2243 parser.add_option(
2244 '-V', '--variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00002245 action='callback',
2246 callback=_process_variable_arg,
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002247 default=default_variables,
2248 dest='variables',
2249 metavar='FOO BAR',
2250 help='Variables to process in the .isolate file, default: %default. '
2251 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002252 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002253
2254
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002255def add_trace_option(parser):
2256 """Adds --trace-blacklist to the parser."""
2257 parser.add_option(
2258 '--trace-blacklist',
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05002259 action='append', default=list(isolateserver.DEFAULT_BLACKLIST),
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002260 help='List of regexp to use as blacklist filter for files to consider '
2261 'important, not to be confused with --blacklist which blacklists '
2262 'test case.')
2263
2264
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002265def parse_isolated_option(parser, options, cwd, require_isolated):
2266 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002267 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002268 options.isolated = os.path.normpath(
2269 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002270 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00002271 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002272 if options.isolated and not options.isolated.endswith('.isolated'):
2273 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002274
2275
2276def parse_variable_option(options):
2277 """Processes --variable."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002278 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
2279 # but it wouldn't be backward compatible.
2280 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002281 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002282 try:
2283 return int(s)
2284 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002285 return s.decode('utf-8')
benrg@chromium.org609b7982013-02-07 16:44:46 +00002286 options.variables = dict((k, try_make_int(v)) for k, v in options.variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002287
2288
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002289class OptionParserIsolate(tools.OptionParserWithLogging):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002290 """Adds automatic --isolate, --isolated, --out and --variable handling."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00002291 # Set it to False if it is not required, e.g. it can be passed on but do not
2292 # fail if not given.
2293 require_isolated = True
2294
2295 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002296 tools.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00002297 self,
2298 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
2299 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002300 group = optparse.OptionGroup(self, "Common options")
2301 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002302 '-i', '--isolate',
2303 metavar='FILE',
2304 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002305 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002306 group.add_option(
2307 '-o', '--outdir', metavar='DIR',
2308 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002309 'Defaults: run|remap: a /tmp subdirectory, others: '
2310 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00002311 group.add_option(
2312 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002313 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
2314 help='Indicates that invalid entries in the isolated file to be '
2315 'only be logged and not stop processing. Defaults to True if '
2316 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002317 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002318
2319 def parse_args(self, *args, **kwargs):
2320 """Makes sure the paths make sense.
2321
2322 On Windows, / and \ are often mixed together in a path.
2323 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002324 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002325 self, *args, **kwargs)
2326 if not self.allow_interspersed_args and args:
2327 self.error('Unsupported argument: %s' % args)
2328
maruel@chromium.org561d4b22013-09-26 21:08:08 +00002329 cwd = file_path.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002330 parse_isolated_option(self, options, cwd, self.require_isolated)
2331 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002332
2333 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002334 # TODO(maruel): Work with non-ASCII.
2335 # The path must be in native path case for tracing purposes.
2336 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
2337 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
maruel@chromium.org561d4b22013-09-26 21:08:08 +00002338 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002339
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05002340 if options.outdir and not file_path.is_url(options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002341 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
2342 # outdir doesn't need native path case since tracing is never done from
2343 # there.
2344 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002345
2346 return options, args
2347
2348
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002349def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00002350 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002351 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00002352 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00002353 except Exception as e:
2354 tools.report_error(e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002355 return 1
2356
2357
2358if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00002359 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002360 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00002361 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002362 sys.exit(main(sys.argv[1:]))