blob: 1d3f24fee4ed0800bc83589379df4702a8640c7e [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
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000024import subprocess
25import sys
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000026
maruel@chromium.orgfb78d432013-08-28 21:22:40 +000027import isolateserver
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000028import run_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000029import trace_inputs
30
31# Import here directly so isolate is easier to use as a library.
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000032from run_isolated import get_flavor
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000033
maruel@chromium.orge5322512013-08-19 20:17:57 +000034from third_party import colorama
35from third_party.depot_tools import fix_encoding
36from third_party.depot_tools import subcommand
37
maruel@chromium.org561d4b22013-09-26 21:08:08 +000038from utils import file_path
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000039from utils import tools
maruel@chromium.orgb61979a2013-08-29 15:18:51 +000040from utils import short_expression_finder
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000041
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000042
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -050043__version__ = '0.2'
maruel@chromium.org3d671992013-08-20 00:38:27 +000044
45
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000046# Files that should be 0-length when mapped.
47KEY_TOUCHED = 'isolate_dependency_touched'
48# Files that should be tracked by the build tool.
49KEY_TRACKED = 'isolate_dependency_tracked'
50# Files that should not be tracked by the build tool.
51KEY_UNTRACKED = 'isolate_dependency_untracked'
52
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -050053# Valid variable name.
54VALID_VARIABLE = '[A-Za-z_]+'
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -050055
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
csharp@chromium.org01856802012-11-12 17:48:13 +000066def expand_directories_and_symlinks(indir, infiles, blacklist,
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +000067 follow_symlinks, ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000068 """Expands the directories and the symlinks, applies the blacklist and
69 verifies files exist.
70
71 Files are specified in os native path separator.
72 """
73 outfiles = []
74 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +000075 try:
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -050076 outfiles.extend(
77 isolateserver.expand_directory_and_symlink(
78 indir, relfile, blacklist, follow_symlinks))
maruel@chromium.org9958e4a2013-09-17 00:01:48 +000079 except isolateserver.MappingError as e:
csharp@chromium.org01856802012-11-12 17:48:13 +000080 if ignore_broken_items:
81 logging.info('warning: %s', e)
82 else:
83 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000084 return outfiles
85
86
maruel@chromium.org7b844a62013-09-17 13:04:59 +000087def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000088 """Creates a new tree with only the input files in it.
89
90 Arguments:
91 outdir: Output directory to create the files in.
92 indir: Root directory the infiles are based in.
93 infiles: dict of files to map from |indir| to |outdir|.
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000094 action: One of accepted action of run_isolated.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +000095 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000096 """
97 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +000098 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
99 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000100
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000101 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000102 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000103 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000104 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000105
106 for relfile, metadata in infiles.iteritems():
107 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000108 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000109 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000110 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000111 # Skip links when storing a hashtable.
112 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000113 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000114 if os.path.isfile(outfile):
115 # Just do a quick check that the file size matches. No need to stat()
116 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000117 if not 's' in metadata:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000118 raise isolateserver.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000119 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000120 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000121 continue
122 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000123 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000124 os.remove(outfile)
125 else:
126 outfile = os.path.join(outdir, relfile)
127 outsubdir = os.path.dirname(outfile)
128 if not os.path.isdir(outsubdir):
129 os.makedirs(outsubdir)
130
131 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000132 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000133 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000134 if 'l' in metadata:
135 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000136 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000137 # symlink doesn't exist on Windows.
138 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000139 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000140 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000141
142
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000143### Variable stuff.
144
145
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000146def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000147 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000148 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000149
150
151def determine_root_dir(relative_root, infiles):
152 """For a list of infiles, determines the deepest root directory that is
153 referenced indirectly.
154
155 All arguments must be using os.path.sep.
156 """
157 # The trick used to determine the root directory is to look at "how far" back
158 # up it is looking up.
159 deepest_root = relative_root
160 for i in infiles:
161 x = relative_root
162 while i.startswith('..' + os.path.sep):
163 i = i[3:]
164 assert not i.startswith(os.path.sep)
165 x = os.path.dirname(x)
166 if deepest_root.startswith(x):
167 deepest_root = x
168 logging.debug(
169 'determine_root_dir(%s, %d files) -> %s' % (
170 relative_root, len(infiles), deepest_root))
171 return deepest_root
172
173
174def replace_variable(part, variables):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500175 m = re.match(r'<\((' + VALID_VARIABLE + ')\)', part)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000176 if m:
177 if m.group(1) not in variables:
178 raise ExecutionError(
179 'Variable "%s" was not found in %s.\nDid you forget to specify '
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500180 '--path-variable?' % (m.group(1), variables))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000181 return variables[m.group(1)]
182 return part
183
184
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500185def _normalize_path_variable(cwd, relative_base_dir, key, value):
186 """Normalizes a path variable into a relative directory.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500187 """
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500188 # Variables could contain / or \ on windows. Always normalize to
189 # os.path.sep.
190 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
191 normalized = file_path.get_native_path_case(os.path.normpath(x))
192 if not os.path.isdir(normalized):
193 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
194
195 # All variables are relative to the .isolate file.
196 normalized = os.path.relpath(normalized, relative_base_dir)
197 logging.debug(
198 'Translated variable %s from %s to %s', key, value, normalized)
199 return normalized
200
201
202def normalize_path_variables(cwd, path_variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000203 """Processes path variables as a special case and returns a copy of the dict.
204
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000205 For each 'path' variable: first normalizes it based on |cwd|, verifies it
206 exists then sets it as relative to relative_base_dir.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500207 """
208 logging.info(
209 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
210 relative_base_dir)
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -0500211 assert isinstance(cwd, unicode), cwd
212 assert isinstance(relative_base_dir, unicode), relative_base_dir
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500213 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
214 return dict(
215 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
216 for k, v in path_variables.iteritems())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000217
218
219def eval_variables(item, variables):
220 """Replaces the .isolate variables in a string item.
221
222 Note that the .isolate format is a subset of the .gyp dialect.
223 """
224 return ''.join(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500225 replace_variable(p, variables)
226 for p in re.split(r'(<\(' + VALID_VARIABLE + '\))', item))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000227
228
229def classify_files(root_dir, tracked, untracked):
230 """Converts the list of files into a .isolate 'variables' dictionary.
231
232 Arguments:
233 - tracked: list of files names to generate a dictionary out of that should
234 probably be tracked.
235 - untracked: list of files names that must not be tracked.
236 """
237 # These directories are not guaranteed to be always present on every builder.
238 OPTIONAL_DIRECTORIES = (
239 'test/data/plugin',
240 'third_party/WebKit/LayoutTests',
241 )
242
243 new_tracked = []
244 new_untracked = list(untracked)
245
246 def should_be_tracked(filepath):
247 """Returns True if it is a file without whitespace in a non-optional
248 directory that has no symlink in its path.
249 """
250 if filepath.endswith('/'):
251 return False
252 if ' ' in filepath:
253 return False
254 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
255 return False
256 # Look if any element in the path is a symlink.
257 split = filepath.split('/')
258 for i in range(len(split)):
259 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
260 return False
261 return True
262
263 for filepath in sorted(tracked):
264 if should_be_tracked(filepath):
265 new_tracked.append(filepath)
266 else:
267 # Anything else.
268 new_untracked.append(filepath)
269
270 variables = {}
271 if new_tracked:
272 variables[KEY_TRACKED] = sorted(new_tracked)
273 if new_untracked:
274 variables[KEY_UNTRACKED] = sorted(new_untracked)
275 return variables
276
277
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500278def chromium_fix(f, variables):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500279 """Fixes an isolate dependency with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000280 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
281 # separator.
282 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000283 # Ignored items.
284 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000285 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000286 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000287 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000288 # 'First Run' is not created by the compile, but by the test itself.
289 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000290
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000291 # Blacklist logs and other unimportant files.
292 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
293 logging.debug('Ignoring %s', f)
294 return None
295
maruel@chromium.org7650e422012-11-16 21:56:42 +0000296 EXECUTABLE = re.compile(
297 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500298 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
maruel@chromium.org7650e422012-11-16 21:56:42 +0000299 r'$')
300 match = EXECUTABLE.match(f)
301 if match:
302 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
303
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000304 if sys.platform == 'darwin':
305 # On OSX, the name of the output is dependent on gyp define, it can be
306 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
307 # Framework.framework'. Furthermore, they are versioned with a gyp
308 # variable. To lower the complexity of the .isolate file, remove all the
309 # individual entries that show up under any of the 4 entries and replace
310 # them with the directory itself. Overall, this results in a bit more
311 # files than strictly necessary.
312 OSX_BUNDLES = (
313 '<(PRODUCT_DIR)/Chromium Framework.framework/',
314 '<(PRODUCT_DIR)/Chromium.app/',
315 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
316 '<(PRODUCT_DIR)/Google Chrome.app/',
317 )
318 for prefix in OSX_BUNDLES:
319 if f.startswith(prefix):
320 # Note this result in duplicate values, so the a set() must be used to
321 # remove duplicates.
322 return prefix
323 return f
324
325
326def generate_simplified(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500327 tracked, untracked, touched, root_dir, path_variables, extra_variables,
328 relative_cwd, trace_blacklist):
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000329 """Generates a clean and complete .isolate 'variables' dictionary.
330
331 Cleans up and extracts only files from within root_dir then processes
332 variables and relative_cwd.
333 """
334 root_dir = os.path.realpath(root_dir)
335 logging.info(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500336 'generate_simplified(%d files, %s, %s, %s, %s)' %
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000337 (len(tracked) + len(untracked) + len(touched),
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500338 root_dir, path_variables, extra_variables, relative_cwd))
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000339
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000340 # Preparation work.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500341 relative_cwd = file_path.cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000342 assert not os.path.isabs(relative_cwd), relative_cwd
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500343
344 # Normalizes to posix path. .isolate files are using posix paths on all OSes
345 # for coherency.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000346 path_variables = dict(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500347 (k, v.replace(os.path.sep, '/')) for k, v in path_variables.iteritems())
348 # Contains normalized path_variables plus extra_variables.
349 total_variables = path_variables.copy()
350 total_variables.update(extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000351
352 # Actual work: Process the files.
353 # TODO(maruel): if all the files in a directory are in part tracked and in
354 # part untracked, the directory will not be extracted. Tracked files should be
355 # 'promoted' to be untracked as needed.
356 tracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000357 root_dir, tracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000358 untracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000359 root_dir, untracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000360 # touched is not compressed, otherwise it would result in files to be archived
361 # that we don't need.
362
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000363 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000364 def fix(f):
365 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000366 # Important, GYP stores the files with / and not \.
367 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000368 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000369 # If it's not already a variable.
370 if not f.startswith('<'):
371 # relative_cwd is usually the directory containing the gyp file. It may be
372 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000373 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000374 # Convert the whole thing to / since it's isolate's speak.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500375 f = file_path.posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000376 posixpath.join(root_dir_posix, f),
377 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000378
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500379 # Use the longest value first.
380 for key, value in sorted(
381 path_variables.iteritems(), key=lambda x: -len(x[1])):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500382 if f.startswith(value):
383 f = '<(%s)%s' % (key, f[len(value):])
384 logging.debug('Converted to %s' % f)
385 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000386 return f
387
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000388 def fix_all(items):
389 """Reduces the items to convert variables, removes unneeded items, apply
390 chromium-specific fixes and only return unique items.
391 """
392 variables_converted = (fix(f.path) for f in items)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500393 chromium_fixed = (
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500394 chromium_fix(f, total_variables) for f in variables_converted)
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000395 return set(f for f in chromium_fixed if f)
396
397 tracked = fix_all(tracked)
398 untracked = fix_all(untracked)
399 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000400 out = classify_files(root_dir, tracked, untracked)
401 if touched:
402 out[KEY_TOUCHED] = sorted(touched)
403 return out
404
405
406def generate_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500407 tracked, untracked, touched, root_dir, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500408 extra_variables, relative_cwd, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000409 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000410 dependencies = generate_simplified(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500411 tracked, untracked, touched, root_dir, path_variables, extra_variables,
412 relative_cwd, trace_blacklist)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000413 config_variable_names, config_values = zip(
414 *sorted(config_variables.iteritems()))
415 out = Configs(None)
416 # The new dependencies apply to just one configuration, namely config_values.
417 out.merge_dependencies(dependencies, config_variable_names, [config_values])
418 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000419
420
421def split_touched(files):
422 """Splits files that are touched vs files that are read."""
423 tracked = []
424 touched = []
425 for f in files:
426 if f.size:
427 tracked.append(f)
428 else:
429 touched.append(f)
430 return tracked, touched
431
432
433def pretty_print(variables, stdout):
434 """Outputs a gyp compatible list from the decoded variables.
435
436 Similar to pprint.print() but with NIH syndrome.
437 """
438 # Order the dictionary keys by these keys in priority.
439 ORDER = (
440 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
441 KEY_TRACKED, KEY_UNTRACKED)
442
443 def sorting_key(x):
444 """Gives priority to 'most important' keys before the others."""
445 if x in ORDER:
446 return str(ORDER.index(x))
447 return x
448
449 def loop_list(indent, items):
450 for item in items:
451 if isinstance(item, basestring):
452 stdout.write('%s\'%s\',\n' % (indent, item))
453 elif isinstance(item, dict):
454 stdout.write('%s{\n' % indent)
455 loop_dict(indent + ' ', item)
456 stdout.write('%s},\n' % indent)
457 elif isinstance(item, list):
458 # A list inside a list will write the first item embedded.
459 stdout.write('%s[' % indent)
460 for index, i in enumerate(item):
461 if isinstance(i, basestring):
462 stdout.write(
463 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
464 elif isinstance(i, dict):
465 stdout.write('{\n')
466 loop_dict(indent + ' ', i)
467 if index != len(item) - 1:
468 x = ', '
469 else:
470 x = ''
471 stdout.write('%s}%s' % (indent, x))
472 else:
473 assert False
474 stdout.write('],\n')
475 else:
476 assert False
477
478 def loop_dict(indent, items):
479 for key in sorted(items, key=sorting_key):
480 item = items[key]
481 stdout.write("%s'%s': " % (indent, key))
482 if isinstance(item, dict):
483 stdout.write('{\n')
484 loop_dict(indent + ' ', item)
485 stdout.write(indent + '},\n')
486 elif isinstance(item, list):
487 stdout.write('[\n')
488 loop_list(indent + ' ', item)
489 stdout.write(indent + '],\n')
490 elif isinstance(item, basestring):
491 stdout.write(
492 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
493 elif item in (True, False, None):
494 stdout.write('%s\n' % item)
495 else:
496 assert False, item
497
498 stdout.write('{\n')
499 loop_dict(' ', variables)
500 stdout.write('}\n')
501
502
503def union(lhs, rhs):
504 """Merges two compatible datastructures composed of dict/list/set."""
505 assert lhs is not None or rhs is not None
506 if lhs is None:
507 return copy.deepcopy(rhs)
508 if rhs is None:
509 return copy.deepcopy(lhs)
510 assert type(lhs) == type(rhs), (lhs, rhs)
511 if hasattr(lhs, 'union'):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000512 # Includes set, ConfigSettings and Configs.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000513 return lhs.union(rhs)
514 if isinstance(lhs, dict):
515 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
516 elif isinstance(lhs, list):
517 # Do not go inside the list.
518 return lhs + rhs
519 assert False, type(lhs)
520
521
522def extract_comment(content):
523 """Extracts file level comment."""
524 out = []
525 for line in content.splitlines(True):
526 if line.startswith('#'):
527 out.append(line)
528 else:
529 break
530 return ''.join(out)
531
532
533def eval_content(content):
534 """Evaluates a python file and return the value defined in it.
535
536 Used in practice for .isolate files.
537 """
538 globs = {'__builtins__': None}
539 locs = {}
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000540 try:
541 value = eval(content, globs, locs)
542 except TypeError as e:
543 e.args = list(e.args) + [content]
544 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000545 assert locs == {}, locs
546 assert globs == {'__builtins__': None}, globs
547 return value
548
549
benrg@chromium.org609b7982013-02-07 16:44:46 +0000550def match_configs(expr, config_variables, all_configs):
551 """Returns the configs from |all_configs| that match the |expr|, where
552 the elements of |all_configs| are tuples of values for the |config_variables|.
553 Example:
554 >>> match_configs(expr = "(foo==1 or foo==2) and bar=='b'",
555 config_variables = ["foo", "bar"],
556 all_configs = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
557 [(1, 'b'), (2, 'b')]
558 """
559 return [
560 config for config in all_configs
561 if eval(expr, dict(zip(config_variables, config)))
562 ]
563
564
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000565def verify_variables(variables):
566 """Verifies the |variables| dictionary is in the expected format."""
567 VALID_VARIABLES = [
568 KEY_TOUCHED,
569 KEY_TRACKED,
570 KEY_UNTRACKED,
571 'command',
572 'read_only',
573 ]
574 assert isinstance(variables, dict), variables
575 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
576 for name, value in variables.iteritems():
577 if name == 'read_only':
578 assert value in (True, False, None), value
579 else:
580 assert isinstance(value, list), value
581 assert all(isinstance(i, basestring) for i in value), value
582
583
benrg@chromium.org609b7982013-02-07 16:44:46 +0000584def verify_ast(expr, variables_and_values):
585 """Verifies that |expr| is of the form
586 expr ::= expr ( "or" | "and" ) expr
587 | identifier "==" ( string | int )
588 Also collects the variable identifiers and string/int values in the dict
589 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
590 """
591 assert isinstance(expr, (ast.BoolOp, ast.Compare))
592 if isinstance(expr, ast.BoolOp):
593 assert isinstance(expr.op, (ast.And, ast.Or))
594 for subexpr in expr.values:
595 verify_ast(subexpr, variables_and_values)
596 else:
597 assert isinstance(expr.left.ctx, ast.Load)
598 assert len(expr.ops) == 1
599 assert isinstance(expr.ops[0], ast.Eq)
600 var_values = variables_and_values.setdefault(expr.left.id, set())
601 rhs = expr.comparators[0]
602 assert isinstance(rhs, (ast.Str, ast.Num))
603 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
604
605
606def verify_condition(condition, variables_and_values):
607 """Verifies the |condition| dictionary is in the expected format.
608 See verify_ast() for the meaning of |variables_and_values|.
609 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000610 VALID_INSIDE_CONDITION = ['variables']
611 assert isinstance(condition, list), condition
benrg@chromium.org609b7982013-02-07 16:44:46 +0000612 assert len(condition) == 2, condition
613 expr, then = condition
614
615 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
616 verify_ast(test_ast.body, variables_and_values)
617
618 assert isinstance(then, dict), then
619 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500620 if not 'variables' in then:
621 raise isolateserver.ConfigError('Missing \'variables\' in condition %s' %
622 condition)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000623 verify_variables(then['variables'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000624
625
benrg@chromium.org609b7982013-02-07 16:44:46 +0000626def verify_root(value, variables_and_values):
627 """Verifies that |value| is the parsed form of a valid .isolate file.
628 See verify_ast() for the meaning of |variables_and_values|.
629 """
630 VALID_ROOTS = ['includes', 'conditions']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000631 assert isinstance(value, dict), value
632 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000633
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000634 includes = value.get('includes', [])
635 assert isinstance(includes, list), includes
636 for include in includes:
637 assert isinstance(include, basestring), include
638
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000639 conditions = value.get('conditions', [])
640 assert isinstance(conditions, list), conditions
641 for condition in conditions:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000642 verify_condition(condition, variables_and_values)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000643
644
benrg@chromium.org609b7982013-02-07 16:44:46 +0000645def remove_weak_dependencies(values, key, item, item_configs):
646 """Removes any configs from this key if the item is already under a
647 strong key.
648 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000649 if key == KEY_TOUCHED:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000650 item_configs = set(item_configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000651 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000652 try:
653 item_configs -= values[stronger_key][item]
654 except KeyError:
655 pass
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000656
benrg@chromium.org609b7982013-02-07 16:44:46 +0000657 return item_configs
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000658
659
benrg@chromium.org609b7982013-02-07 16:44:46 +0000660def remove_repeated_dependencies(folders, key, item, item_configs):
661 """Removes any configs from this key if the item is in a folder that is
662 already included."""
csharp@chromium.org31176252012-11-02 13:04:40 +0000663
664 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000665 item_configs = set(item_configs)
666 for (folder, configs) in folders.iteritems():
csharp@chromium.org31176252012-11-02 13:04:40 +0000667 if folder != item and item.startswith(folder):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000668 item_configs -= configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000669
benrg@chromium.org609b7982013-02-07 16:44:46 +0000670 return item_configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000671
672
673def get_folders(values_dict):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000674 """Returns a dict of all the folders in the given value_dict."""
675 return dict(
676 (item, configs) for (item, configs) in values_dict.iteritems()
677 if item.endswith('/')
678 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000679
680
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000681def invert_map(variables):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000682 """Converts {config: {deptype: list(depvals)}} to
683 {deptype: {depval: set(configs)}}.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000684 """
685 KEYS = (
686 KEY_TOUCHED,
687 KEY_TRACKED,
688 KEY_UNTRACKED,
689 'command',
690 'read_only',
691 )
692 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000693 for config, values in variables.iteritems():
694 for key in KEYS:
695 if key == 'command':
696 items = [tuple(values[key])] if key in values else []
697 elif key == 'read_only':
698 items = [values[key]] if key in values else []
699 else:
700 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
701 items = values.get(key, [])
702 for item in items:
703 out[key].setdefault(item, set()).add(config)
704 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000705
706
benrg@chromium.org609b7982013-02-07 16:44:46 +0000707def reduce_inputs(values):
708 """Reduces the output of invert_map() to the strictest minimum list.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000709
benrg@chromium.org609b7982013-02-07 16:44:46 +0000710 Looks at each individual file and directory, maps where they are used and
711 reconstructs the inverse dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000712
benrg@chromium.org609b7982013-02-07 16:44:46 +0000713 Returns the minimized dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000714 """
715 KEYS = (
716 KEY_TOUCHED,
717 KEY_TRACKED,
718 KEY_UNTRACKED,
719 'command',
720 'read_only',
721 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000722
723 # Folders can only live in KEY_UNTRACKED.
724 folders = get_folders(values.get(KEY_UNTRACKED, {}))
725
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000726 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000727 for key in KEYS:
728 for item, item_configs in values.get(key, {}).iteritems():
729 item_configs = remove_weak_dependencies(values, key, item, item_configs)
730 item_configs = remove_repeated_dependencies(
731 folders, key, item, item_configs)
732 if item_configs:
733 out[key][item] = item_configs
734 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000735
736
benrg@chromium.org609b7982013-02-07 16:44:46 +0000737def convert_map_to_isolate_dict(values, config_variables):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000738 """Regenerates back a .isolate configuration dict from files and dirs
739 mappings generated from reduce_inputs().
740 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000741 # Gather a list of configurations for set inversion later.
742 all_mentioned_configs = set()
743 for configs_by_item in values.itervalues():
744 for configs in configs_by_item.itervalues():
745 all_mentioned_configs.update(configs)
746
747 # Invert the mapping to make it dict first.
748 conditions = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000749 for key in values:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000750 for item, configs in values[key].iteritems():
751 then = conditions.setdefault(frozenset(configs), {})
752 variables = then.setdefault('variables', {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000753
benrg@chromium.org609b7982013-02-07 16:44:46 +0000754 if item in (True, False):
755 # One-off for read_only.
756 variables[key] = item
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000757 else:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000758 assert item
759 if isinstance(item, tuple):
760 # One-off for command.
761 # Do not merge lists and do not sort!
762 # Note that item is a tuple.
763 assert key not in variables
764 variables[key] = list(item)
765 else:
766 # The list of items (files or dirs). Append the new item and keep
767 # the list sorted.
768 l = variables.setdefault(key, [])
769 l.append(item)
770 l.sort()
771
772 if all_mentioned_configs:
773 config_values = map(set, zip(*all_mentioned_configs))
774 sef = short_expression_finder.ShortExpressionFinder(
775 zip(config_variables, config_values))
776
777 conditions = sorted(
778 [sef.get_expr(configs), then] for configs, then in conditions.iteritems())
779 return {'conditions': conditions}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000780
781
782### Internal state files.
783
784
benrg@chromium.org609b7982013-02-07 16:44:46 +0000785class ConfigSettings(object):
786 """Represents the dependency variables for a single build configuration.
787 The structure is immutable.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000788 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000789 def __init__(self, config, values):
790 self.config = config
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000791 verify_variables(values)
792 self.touched = sorted(values.get(KEY_TOUCHED, []))
793 self.tracked = sorted(values.get(KEY_TRACKED, []))
794 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
795 self.command = values.get('command', [])[:]
796 self.read_only = values.get('read_only')
797
798 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000799 assert not (self.config and rhs.config) or (self.config == rhs.config)
maruel@chromium.org669edcb2012-11-02 19:16:14 +0000800 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000801 var = {
802 KEY_TOUCHED: sorted(self.touched + rhs.touched),
803 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
804 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
805 'command': self.command or rhs.command,
806 'read_only': rhs.read_only if self.read_only is None else self.read_only,
807 }
benrg@chromium.org609b7982013-02-07 16:44:46 +0000808 return ConfigSettings(self.config or rhs.config, var)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000809
810 def flatten(self):
811 out = {}
812 if self.command:
813 out['command'] = self.command
814 if self.touched:
815 out[KEY_TOUCHED] = self.touched
816 if self.tracked:
817 out[KEY_TRACKED] = self.tracked
818 if self.untracked:
819 out[KEY_UNTRACKED] = self.untracked
820 if self.read_only is not None:
821 out['read_only'] = self.read_only
822 return out
823
824
825class Configs(object):
826 """Represents a processed .isolate file.
827
benrg@chromium.org609b7982013-02-07 16:44:46 +0000828 Stores the file in a processed way, split by configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000829 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000830 def __init__(self, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000831 self.file_comment = file_comment
benrg@chromium.org609b7982013-02-07 16:44:46 +0000832 # The keys of by_config are tuples of values for the configuration
833 # variables. The names of the variables (which must be the same for
834 # every by_config key) are kept in config_variables. Initially by_config
835 # is empty and we don't know what configuration variables will be used,
836 # so config_variables also starts out empty. It will be set by the first
837 # call to union() or merge_dependencies().
838 self.by_config = {}
839 self.config_variables = ()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000840
841 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000842 """Adds variables from rhs (a Configs) to the existing variables.
843 """
844 config_variables = self.config_variables
845 if not config_variables:
846 config_variables = rhs.config_variables
847 else:
848 # We can't proceed if this isn't true since we don't know the correct
849 # default values for extra variables. The variables are sorted so we
850 # don't need to worry about permutations.
851 if rhs.config_variables and rhs.config_variables != config_variables:
852 raise ExecutionError(
853 'Variables in merged .isolate files do not match: %r and %r' % (
854 config_variables, rhs.config_variables))
855
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000856 # Takes the first file comment, prefering lhs.
benrg@chromium.org609b7982013-02-07 16:44:46 +0000857 out = Configs(self.file_comment or rhs.file_comment)
858 out.config_variables = config_variables
859 for config in set(self.by_config) | set(rhs.by_config):
860 out.by_config[config] = union(
861 self.by_config.get(config), rhs.by_config.get(config))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000862 return out
863
benrg@chromium.org609b7982013-02-07 16:44:46 +0000864 def merge_dependencies(self, values, config_variables, configs):
865 """Adds new dependencies to this object for the given configurations.
866 Arguments:
867 values: A variables dict as found in a .isolate file, e.g.,
868 {KEY_TOUCHED: [...], 'command': ...}.
869 config_variables: An ordered list of configuration variables, e.g.,
870 ["OS", "chromeos"]. If this object already contains any dependencies,
871 the configuration variables must match.
872 configs: a list of tuples of values of the configuration variables,
873 e.g., [("mac", 0), ("linux", 1)]. The dependencies in |values|
874 are added to all of these configurations, and other configurations
875 are unchanged.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000876 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000877 if not values:
878 return
879
880 if not self.config_variables:
881 self.config_variables = config_variables
882 else:
883 # See comment in Configs.union().
884 assert self.config_variables == config_variables
885
886 for config in configs:
887 self.by_config[config] = union(
888 self.by_config.get(config), ConfigSettings(config, values))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000889
890 def flatten(self):
891 """Returns a flat dictionary representation of the configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000892 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000893 return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
894
895 def make_isolate_file(self):
896 """Returns a dictionary suitable for writing to a .isolate file.
897 """
898 dependencies_by_config = self.flatten()
899 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
900 return convert_map_to_isolate_dict(configs_by_dependency,
901 self.config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000902
903
benrg@chromium.org609b7982013-02-07 16:44:46 +0000904# TODO(benrg): Remove this function when no old-format files are left.
905def convert_old_to_new_format(value):
906 """Converts from the old .isolate format, which only has one variable (OS),
907 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
908 and allows conditions that depend on the set of all OSes, to the new format,
909 which allows any set of variables, has no hardcoded values, and only allows
910 explicit positive tests of variable values.
911 """
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +0000912 conditions = value.get('conditions', [])
benrg@chromium.org609b7982013-02-07 16:44:46 +0000913 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
914 return value # Nothing to change
915
916 def parse_condition(cond):
917 return re.match(r'OS=="(\w+)"\Z', cond[0]).group(1)
918
919 oses = set(map(parse_condition, conditions))
920 default_oses = set(['linux', 'mac', 'win'])
921 oses = sorted(oses | default_oses)
922
923 def if_not_os(not_os, then):
924 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
925 return [expr, then]
926
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +0000927 conditions = [
928 cond[:2] for cond in conditions if cond[1]
929 ] + [
930 if_not_os(parse_condition(cond), cond[2])
benrg@chromium.org609b7982013-02-07 16:44:46 +0000931 for cond in conditions if len(cond) == 3
932 ]
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +0000933
benrg@chromium.org609b7982013-02-07 16:44:46 +0000934 if 'variables' in value:
935 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
936 conditions.sort()
937
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +0000938 value = value.copy()
939 value['conditions'] = conditions
benrg@chromium.org609b7982013-02-07 16:44:46 +0000940 return value
941
942
943def load_isolate_as_config(isolate_dir, value, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000944 """Parses one .isolate file and returns a Configs() instance.
945
946 |value| is the loaded dictionary that was defined in the gyp file.
947
948 The expected format is strict, anything diverting from the format below will
949 throw an assert:
950 {
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000951 'includes': [
952 'foo.isolate',
953 ],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000954 'conditions': [
benrg@chromium.org609b7982013-02-07 16:44:46 +0000955 ['OS=="vms" and foo=42', {
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000956 'variables': {
benrg@chromium.org609b7982013-02-07 16:44:46 +0000957 'command': [
958 ...
959 ],
960 'isolate_dependency_tracked': [
961 ...
962 ],
963 'isolate_dependency_untracked': [
964 ...
965 ],
966 'read_only': False,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000967 },
968 }],
969 ...
970 ],
971 }
972 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000973 value = convert_old_to_new_format(value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000974
benrg@chromium.org609b7982013-02-07 16:44:46 +0000975 variables_and_values = {}
976 verify_root(value, variables_and_values)
977 if variables_and_values:
978 config_variables, config_values = zip(
979 *sorted(variables_and_values.iteritems()))
980 all_configs = list(itertools.product(*config_values))
981 else:
982 config_variables = None
983 all_configs = []
984
985 isolate = Configs(file_comment)
986
987 # Add configuration-specific variables.
988 for expr, then in value.get('conditions', []):
989 configs = match_configs(expr, config_variables, all_configs)
990 isolate.merge_dependencies(then['variables'], config_variables, configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000991
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000992 # Load the includes.
993 for include in value.get('includes', []):
994 if os.path.isabs(include):
995 raise ExecutionError(
996 'Failed to load configuration; absolute include path \'%s\'' %
997 include)
998 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
999 with open(included_isolate, 'r') as f:
benrg@chromium.org609b7982013-02-07 16:44:46 +00001000 included_isolate = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001001 os.path.dirname(included_isolate),
1002 eval_content(f.read()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001003 None)
1004 isolate = union(isolate, included_isolate)
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001005
benrg@chromium.org609b7982013-02-07 16:44:46 +00001006 return isolate
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001007
1008
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001009def load_isolate_for_config(isolate_dir, content, config_variables):
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001010 """Loads the .isolate file and returns the information unprocessed but
1011 filtered for the specific OS.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001012
1013 Returns the command, dependencies and read_only flag. The dependencies are
1014 fixed to use os.path.sep.
1015 """
1016 # Load the .isolate file, process its conditions, retrieve the command and
1017 # dependencies.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001018 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
1019 try:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001020 config_name = tuple(
1021 config_variables[var] for var in isolate.config_variables)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001022 except KeyError:
1023 raise ExecutionError(
1024 'These configuration variables were missing from the command line: %s' %
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001025 ', '.join(
1026 sorted(set(isolate.config_variables) - set(config_variables))))
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001027 config = isolate.by_config.get(config_name)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001028 if not config:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001029 raise isolateserver.ConfigError(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001030 'Failed to load configuration for variable \'%s\' for config(s) \'%s\''
1031 '\nAvailable configs: %s' %
1032 (', '.join(isolate.config_variables),
1033 ', '.join(config_name),
1034 ', '.join(str(s) for s in isolate.by_config)))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001035 # Merge tracked and untracked variables, isolate.py doesn't care about the
1036 # trackability of the variables, only the build tool does.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001037 dependencies = [
1038 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1039 ]
1040 touched = [f.replace('/', os.path.sep) for f in config.touched]
1041 return config.command, dependencies, touched, config.read_only
1042
1043
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001044def chromium_save_isolated(isolated, data, path_variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001045 """Writes one or many .isolated files.
1046
1047 This slightly increases the cold cache cost but greatly reduce the warm cache
1048 cost by splitting low-churn files off the master .isolated file. It also
1049 reduces overall isolateserver memcache consumption.
1050 """
1051 slaves = []
1052
1053 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001054 new_slave = {
1055 'algo': data['algo'],
1056 'files': {},
1057 'os': data['os'],
1058 'version': data['version'],
1059 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001060 for f in data['files'].keys():
1061 if f.startswith(prefix):
1062 new_slave['files'][f] = data['files'].pop(f)
1063 if new_slave['files']:
1064 slaves.append(new_slave)
1065
1066 # Split test/data/ in its own .isolated file.
1067 extract_into_included_isolated(os.path.join('test', 'data', ''))
1068
1069 # Split everything out of PRODUCT_DIR in its own .isolated file.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001070 if path_variables.get('PRODUCT_DIR'):
1071 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001072
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001073 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001074 for index, f in enumerate(slaves):
1075 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05001076 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001077 data.setdefault('includes', []).append(
1078 isolateserver.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001079 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001080
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -05001081 files.extend(isolateserver.save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001082 return files
1083
1084
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001085class Flattenable(object):
1086 """Represents data that can be represented as a json file."""
1087 MEMBERS = ()
1088
1089 def flatten(self):
1090 """Returns a json-serializable version of itself.
1091
1092 Skips None entries.
1093 """
1094 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1095 return dict((member, value) for member, value in items if value is not None)
1096
1097 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001098 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001099 """Loads a flattened version."""
1100 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001101 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001102 for member in out.MEMBERS:
1103 if member in data:
1104 # Access to a protected member XXX of a client class
1105 # pylint: disable=W0212
1106 out._load_member(member, data.pop(member))
1107 if data:
1108 raise ValueError(
1109 'Found unexpected entry %s while constructing an object %s' %
1110 (data, cls.__name__), data, cls.__name__)
1111 return out
1112
1113 def _load_member(self, member, value):
1114 """Loads a member into self."""
1115 setattr(self, member, value)
1116
1117 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001118 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001119 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001120 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05001121 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001122 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001123 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001124 # On failure, loads the default instance.
1125 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001126 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001127 return out
1128
1129
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001130class SavedState(Flattenable):
1131 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001132
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001133 This file caches the items calculated by this script and is used to increase
1134 the performance of the script. This file is not loaded by run_isolated.py.
1135 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001136
1137 It is important to note that the 'files' dict keys are using native OS path
1138 separator instead of '/' used in .isolate file.
1139 """
1140 MEMBERS = (
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001141 # Algorithm used to generate the hash. The only supported value is at the
1142 # time of writting 'sha-1'.
1143 'algo',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001144 # Cache of the processed command. This value is saved because .isolated
1145 # files are never loaded by isolate.py so it's the only way to load the
1146 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001147 'command',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001148 # GYP variables that are used to generate conditions. The most frequent
1149 # example is 'OS'.
1150 'config_variables',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001151 # GYP variables that will be replaced in 'command' and paths but will not be
1152 # considered a relative directory.
1153 'extra_variables',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001154 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001155 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001156 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001157 'isolate_file',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001158 # List of included .isolated files. Used to support/remember 'slave'
1159 # .isolated files. Relative path to isolated_basedir.
1160 'child_isolated_files',
1161 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001162 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001163 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001164 'relative_cwd',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001165 # GYP variables used to generate the .isolated files paths based on path
1166 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
1167 'path_variables',
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001168 # Version of the file format in format 'major.minor'. Any non-breaking
1169 # change must update minor. Any breaking change must update major.
1170 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001171 )
1172
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001173 def __init__(self, isolated_basedir):
1174 """Creates an empty SavedState.
1175
1176 |isolated_basedir| is the directory where the .isolated and .isolated.state
1177 files are saved.
1178 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001179 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001180 assert os.path.isabs(isolated_basedir), isolated_basedir
1181 assert os.path.isdir(isolated_basedir), isolated_basedir
1182 self.isolated_basedir = isolated_basedir
1183
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001184 # The default algorithm used.
1185 self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001186 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001187 self.command = []
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001188 self.config_variables = {}
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001189 self.extra_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001190 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001191 self.isolate_file = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001192 self.path_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001193 self.read_only = None
1194 self.relative_cwd = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001195 self.version = isolateserver.ISOLATED_FILE_VERSION
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001196
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001197 def update(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001198 self, isolate_file, path_variables, config_variables, extra_variables):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001199 """Updates the saved state with new data to keep GYP variables and internal
1200 reference to the original .isolate file.
1201 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +00001202 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001203 # Convert back to a relative path. On Windows, if the isolate and
1204 # isolated files are on different drives, isolate_file will stay an absolute
1205 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001206 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001207
1208 # The same .isolate file should always be used to generate the .isolated and
1209 # .isolated.state.
1210 assert isolate_file == self.isolate_file or not self.isolate_file, (
1211 isolate_file, self.isolate_file)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001212 self.config_variables.update(config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001213 self.extra_variables.update(extra_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001214 self.isolate_file = isolate_file
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001215 self.path_variables.update(path_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001216
1217 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
1218 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001219
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001220 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001221 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001222 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001223 self.command = command
1224 # Add new files.
1225 for f in infiles:
1226 self.files.setdefault(f, {})
1227 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001228 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001229 # Prune extraneous files that are not a dependency anymore.
1230 for f in set(self.files).difference(set(infiles).union(touched)):
1231 del self.files[f]
1232 if read_only is not None:
1233 self.read_only = read_only
1234 self.relative_cwd = relative_cwd
1235
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001236 def to_isolated(self):
1237 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001238
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001239 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001240 """
1241 def strip(data):
1242 """Returns a 'files' entry with only the whitelisted keys."""
1243 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
1244
1245 out = {
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001246 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001247 'files': dict(
1248 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001249 'version': self.version,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001250 }
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001251 if self.config_variables.get('OS'):
1252 out['os'] = self.config_variables['OS']
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001253 if self.command:
1254 out['command'] = self.command
1255 if self.read_only is not None:
1256 out['read_only'] = self.read_only
1257 if self.relative_cwd:
1258 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001259 return out
1260
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001261 @property
1262 def isolate_filepath(self):
1263 """Returns the absolute path of self.isolate_file."""
1264 return os.path.normpath(
1265 os.path.join(self.isolated_basedir, self.isolate_file))
1266
1267 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001268 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001269 def load(cls, data, isolated_basedir): # pylint: disable=W0221
1270 """Special case loading to disallow different OS.
1271
1272 It is not possible to load a .isolated.state files from a different OS, this
1273 file is saved in OS-specific format.
1274 """
1275 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001276 if data.get('os'):
1277 out.config_variables['OS'] = data['os']
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001278
1279 # Converts human readable form back into the proper class type.
1280 algo = data.get('algo', 'sha-1')
1281 if not algo in isolateserver.SUPPORTED_ALGOS:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001282 raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001283 out.algo = isolateserver.SUPPORTED_ALGOS[algo]
1284
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001285 # Refuse the load non-exact version, even minor difference. This is unlike
1286 # isolateserver.load_isolated(). This is because .isolated.state could have
1287 # changed significantly even in minor version difference.
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001288 if not re.match(r'^(\d+)\.(\d+)$', out.version):
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001289 raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001290 if out.version != isolateserver.ISOLATED_FILE_VERSION:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001291 raise isolateserver.ConfigError(
1292 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001293
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001294 # The .isolate file must be valid. It could be absolute on Windows if the
1295 # drive containing the .isolate and the drive containing the .isolated files
1296 # differ.
1297 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
1298 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001299 return out
1300
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001301 def flatten(self):
1302 """Makes sure 'algo' is in human readable form."""
1303 out = super(SavedState, self).flatten()
1304 out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
1305 return out
1306
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001307 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001308 def dict_to_str(d):
1309 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
1310
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001311 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001312 out += ' command: %s\n' % self.command
1313 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001314 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001315 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +00001316 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001317 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001318 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
1319 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001320 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001321 return out
1322
1323
1324class CompleteState(object):
1325 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001326 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001327 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +00001328 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001329 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001330 # Contains the data to ease developer's use-case but that is not strictly
1331 # necessary.
1332 self.saved_state = saved_state
1333
1334 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001335 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001336 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001337 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001338 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001339 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001340 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001341 SavedState.load_file(
1342 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001343
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001344 def load_isolate(
1345 self, cwd, isolate_file, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001346 extra_variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001347 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001348 .isolate file.
1349
1350 Processes the loaded data, deduce root_dir, relative_cwd.
1351 """
1352 # Make sure to not depend on os.getcwd().
1353 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001354 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001355 logging.info(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001356 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001357 cwd, isolate_file, path_variables, config_variables, extra_variables,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001358 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001359 relative_base_dir = os.path.dirname(isolate_file)
1360
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001361 # Processes the variables.
1362 path_variables = normalize_path_variables(
1363 cwd, path_variables, relative_base_dir)
1364 # Update the saved state.
1365 self.saved_state.update(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001366 isolate_file, path_variables, config_variables, extra_variables)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001367 path_variables = self.saved_state.path_variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001368
1369 with open(isolate_file, 'r') as f:
1370 # At that point, variables are not replaced yet in command and infiles.
1371 # infiles may contain directory entries and is in posix style.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001372 command, infiles, touched, read_only = load_isolate_for_config(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001373 os.path.dirname(isolate_file), f.read(),
1374 self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001375
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001376 total_variables = self.saved_state.path_variables.copy()
1377 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001378 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001379 command = [eval_variables(i, total_variables) for i in command]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001380
1381 total_variables = self.saved_state.path_variables.copy()
1382 total_variables.update(self.saved_state.extra_variables)
1383 infiles = [eval_variables(f, total_variables) for f in infiles]
1384 touched = [eval_variables(f, total_variables) for f in touched]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001385 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +00001386 # form '../../foo/bar'. Note that path variables must be taken in account
1387 # too, add them as if they were input files.
maruel@chromium.org75584e22013-06-20 01:40:24 +00001388 root_dir = determine_root_dir(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001389 relative_base_dir, infiles + touched +
1390 self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001391 # The relative directory is automatically determined by the relative path
1392 # between root_dir and the directory containing the .isolate file,
1393 # isolate_base_dir.
1394 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001395 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +00001396 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001397 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001398 if not file_path.path_starts_with(
1399 root_dir, os.path.join(relative_base_dir, v)):
1400 raise isolateserver.MappingError(
1401 'Path variable %s=%r points outside the inferred root directory %s'
1402 % (k, v, root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001403 # Normalize the files based to root_dir. It is important to keep the
1404 # trailing os.path.sep at that step.
1405 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001406 file_path.relpath(
1407 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001408 for f in infiles
1409 ]
1410 touched = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001411 file_path.relpath(
1412 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001413 for f in touched
1414 ]
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001415 follow_symlinks = config_variables['OS'] != 'win'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001416 # Expand the directories by listing each file inside. Up to now, trailing
1417 # os.path.sep must be kept. Do not expand 'touched'.
1418 infiles = expand_directories_and_symlinks(
1419 root_dir,
1420 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001421 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001422 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +00001423 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001424
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001425 # If we ignore broken items then remove any missing touched items.
1426 if ignore_broken_items:
1427 original_touched_count = len(touched)
1428 touched = [touch for touch in touched if os.path.exists(touch)]
1429
1430 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +00001431 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001432 len(touched) - original_touched_count)
1433
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001434 # Finally, update the new data to be able to generate the foo.isolated file,
1435 # the file that is used by run_isolated.py.
1436 self.saved_state.update_isolated(
1437 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001438 logging.debug(self)
1439
maruel@chromium.org9268f042012-10-17 17:36:41 +00001440 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001441 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001442
maruel@chromium.org9268f042012-10-17 17:36:41 +00001443 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1444 file is tainted.
1445
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -05001446 See isolateserver.process_input() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001447 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001448 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001449 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001450 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001451 else:
1452 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -05001453 self.saved_state.files[infile] = isolateserver.process_input(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001454 filepath,
1455 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +00001456 self.saved_state.read_only,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001457 self.saved_state.config_variables['OS'],
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001458 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001459
1460 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001461 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001462 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001463 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001464 self.isolated_filepath,
1465 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001466 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001467 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001468 total_bytes = sum(
1469 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001470 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001471 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001472 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001473 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001474 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05001475 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001476
1477 @property
1478 def root_dir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001479 """Returns the absolute path of the root_dir to reference the .isolate file
1480 via relative_cwd.
1481
1482 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
1483 to isolate_filepath.
1484 """
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001485 if not self.saved_state.isolate_file:
1486 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001487 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001488 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001489 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001490 root_dir = isolate_dir
1491 else:
maruel@chromium.org87557b92013-10-16 18:04:11 +00001492 if not isolate_dir.endswith(self.saved_state.relative_cwd):
1493 raise ExecutionError(
1494 ('Make sure the .isolate file is in the directory that will be '
1495 'used as the relative directory. It is currently in %s and should '
1496 'be in %s') % (isolate_dir, self.saved_state.relative_cwd))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001497 # Walk back back to the root directory.
1498 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001499 return file_path.get_native_path_case(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001500
1501 @property
1502 def resultdir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001503 """Returns the absolute path containing the .isolated file.
1504
1505 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
1506 path as the value.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001507 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001508 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001509
1510 def __str__(self):
1511 def indent(data, indent_length):
1512 """Indents text."""
1513 spacing = ' ' * indent_length
1514 return ''.join(spacing + l for l in str(data).splitlines(True))
1515
1516 out = '%s(\n' % self.__class__.__name__
1517 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001518 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1519 return out
1520
1521
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001522def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001523 """Loads a CompleteState.
1524
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001525 This includes data from .isolate and .isolated.state files. Never reads the
1526 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001527
1528 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001529 options: Options instance generated with OptionParserIsolate. For either
1530 options.isolate and options.isolated, if the value is set, it is an
1531 absolute path.
1532 cwd: base directory to be used when loading the .isolate file.
1533 subdir: optional argument to only process file in the subdirectory, relative
1534 to CompleteState.root_dir.
1535 skip_update: Skip trying to load the .isolate file and processing the
1536 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001537 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001538 assert not options.isolate or os.path.isabs(options.isolate)
1539 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001540 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001541 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001542 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001543 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001544 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001545 else:
1546 # Constructs a dummy object that cannot be saved. Useful for temporary
1547 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001548 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001549
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001550 if not options.isolate:
1551 if not complete_state.saved_state.isolate_file:
1552 if not skip_update:
1553 raise ExecutionError('A .isolate file is required.')
1554 isolate = None
1555 else:
1556 isolate = complete_state.saved_state.isolate_filepath
1557 else:
1558 isolate = options.isolate
1559 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001560 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001561 options.isolate, complete_state.saved_state.isolated_basedir)
1562 if rel_isolate != complete_state.saved_state.isolate_file:
1563 raise ExecutionError(
1564 '%s and %s do not match.' % (
1565 options.isolate, complete_state.saved_state.isolate_file))
1566
1567 if not skip_update:
1568 # Then load the .isolate and expands directories.
1569 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001570 cwd, isolate, options.path_variables, options.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001571 options.extra_variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001572
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001573 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001574 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001575 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001576 # This is tricky here. If it is a path, take it from the root_dir. If
1577 # it is a variable, it must be keyed from the directory containing the
1578 # .isolate file. So translate all variables first.
1579 translated_path_variables = dict(
1580 (k,
1581 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
1582 v)))
1583 for k, v in complete_state.saved_state.path_variables.iteritems())
1584 subdir = eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001585 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001586
1587 if not skip_update:
1588 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001589 return complete_state
1590
1591
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001592def read_trace_as_isolate_dict(complete_state, trace_blacklist):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001593 """Reads a trace and returns the .isolate dictionary.
1594
1595 Returns exceptions during the log parsing so it can be re-raised.
1596 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001597 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001598 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001599 if not os.path.isfile(logfile):
1600 raise ExecutionError(
1601 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1602 try:
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001603 data = api.parse_log(logfile, trace_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001604 exceptions = [i['exception'] for i in data if 'exception' in i]
1605 results = (i['results'] for i in data if 'results' in i)
1606 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1607 files = set(sum((result.existent for result in results_stripped), []))
1608 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001609 value = generate_isolate(
1610 tracked,
1611 [],
1612 touched,
1613 complete_state.root_dir,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001614 complete_state.saved_state.path_variables,
1615 complete_state.saved_state.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001616 complete_state.saved_state.extra_variables,
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001617 complete_state.saved_state.relative_cwd,
1618 trace_blacklist)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001619 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001620 except trace_inputs.TracingFailure, e:
1621 raise ExecutionError(
1622 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001623 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001624
1625
1626def print_all(comment, data, stream):
1627 """Prints a complete .isolate file and its top-level file comment into a
1628 stream.
1629 """
1630 if comment:
1631 stream.write(comment)
1632 pretty_print(data, stream)
1633
1634
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001635def merge(complete_state, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001636 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001637 value, exceptions = read_trace_as_isolate_dict(
1638 complete_state, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001639
1640 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001641 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001642 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001643 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001644 prev_config = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001645 isolate_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001646 eval_content(prev_content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001647 extract_comment(prev_content))
1648 new_config = load_isolate_as_config(isolate_dir, value, '')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001649 config = union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001650 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001651 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001652 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001653 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001654 if exceptions:
1655 # It got an exception, raise the first one.
1656 raise \
1657 exceptions[0][0], \
1658 exceptions[0][1], \
1659 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001660
1661
maruel@chromium.org29029882013-08-30 12:15:40 +00001662### Commands.
1663
1664
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001665def add_subdir_flag(parser):
1666 parser.add_option(
1667 '--subdir',
1668 help='Filters to a subdirectory. Its behavior changes depending if it '
1669 'is a relative path as a string or as a path variable. Path '
1670 'variables are always keyed from the directory containing the '
1671 '.isolate file. Anything else is keyed on the root directory.')
1672
1673
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001674def CMDarchive(parser, args):
1675 """Creates a .isolated file and uploads the tree to an isolate server.
maruel@chromium.org29029882013-08-30 12:15:40 +00001676
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001677 All the files listed in the .isolated file are put in the isolate server
1678 cache via isolateserver.py.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001679 """
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001680 add_subdir_flag(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001681 options, args = parser.parse_args(args)
1682 if args:
1683 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001684
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001685 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001686 success = False
1687 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001688 complete_state = load_complete_state(
1689 options, os.getcwd(), options.subdir, False)
1690 if not options.outdir:
1691 options.outdir = os.path.join(
1692 os.path.dirname(complete_state.isolated_filepath), 'hashtable')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001693 # Make sure that complete_state isn't modified until save_files() is
1694 # called, because any changes made to it here will propagate to the files
1695 # created (which is probably not intended).
1696 complete_state.save_files()
1697
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001698 infiles = complete_state.saved_state.files
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001699 # Add all the .isolated files.
maruel@chromium.org87f11962013-04-10 21:27:28 +00001700 isolated_hash = []
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001701 isolated_files = [
1702 options.isolated,
1703 ] + complete_state.saved_state.child_isolated_files
1704 for item in isolated_files:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001705 item_path = os.path.join(
1706 os.path.dirname(complete_state.isolated_filepath), item)
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001707 # Do not use isolateserver.hash_file() here because the file is
maruel@chromium.org87f11962013-04-10 21:27:28 +00001708 # likely smallish (under 500kb) and its file size is needed.
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001709 with open(item_path, 'rb') as f:
1710 content = f.read()
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001711 isolated_hash.append(
1712 complete_state.saved_state.algo(content).hexdigest())
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001713 isolated_metadata = {
maruel@chromium.org87f11962013-04-10 21:27:28 +00001714 'h': isolated_hash[-1],
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001715 's': len(content),
1716 'priority': '0'
1717 }
1718 infiles[item_path] = isolated_metadata
1719
1720 logging.info('Creating content addressed object store with %d item',
1721 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001722
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001723 if file_path.is_url(options.outdir):
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001724 isolateserver.upload_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001725 base_url=options.outdir,
1726 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00001727 infiles=infiles,
1728 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001729 else:
1730 recreate_tree(
1731 outdir=options.outdir,
1732 indir=complete_state.root_dir,
1733 infiles=infiles,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00001734 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001735 as_hash=True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001736 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001737 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001738 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001739 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001740 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001741 if not success and os.path.isfile(options.isolated):
1742 os.remove(options.isolated)
maruel@chromium.org87f11962013-04-10 21:27:28 +00001743 return not success
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001744
1745
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001746def CMDcheck(parser, args):
1747 """Checks that all the inputs are present and generates .isolated."""
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001748 add_subdir_flag(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001749 options, args = parser.parse_args(args)
1750 if args:
1751 parser.error('Unsupported argument: %s' % args)
1752
1753 complete_state = load_complete_state(
1754 options, os.getcwd(), options.subdir, False)
1755
1756 # Nothing is done specifically. Just store the result and state.
1757 complete_state.save_files()
1758 return 0
1759
1760
1761CMDhashtable = CMDarchive
1762
1763
maruel@chromium.orge5322512013-08-19 20:17:57 +00001764def CMDmerge(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001765 """Reads and merges the data from the trace back into the original .isolate.
1766
1767 Ignores --outdir.
1768 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001769 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001770 add_trace_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001771 options, args = parser.parse_args(args)
1772 if args:
1773 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001774
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001775 complete_state = load_complete_state(options, os.getcwd(), None, False)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001776 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001777 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001778 return 0
1779
1780
maruel@chromium.orge5322512013-08-19 20:17:57 +00001781def CMDread(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001782 """Reads the trace file generated with command 'trace'.
1783
1784 Ignores --outdir.
1785 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001786 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001787 add_trace_option(parser)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001788 parser.add_option(
1789 '--skip-refresh', action='store_true',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001790 help='Skip reading .isolate file and do not refresh the hash of '
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001791 'dependencies')
maruel@chromium.org29029882013-08-30 12:15:40 +00001792 parser.add_option(
1793 '-m', '--merge', action='store_true',
1794 help='merge the results back in the .isolate file instead of printing')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001795 options, args = parser.parse_args(args)
1796 if args:
1797 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001798
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001799 complete_state = load_complete_state(
1800 options, os.getcwd(), None, options.skip_refresh)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001801 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001802 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
maruel@chromium.org29029882013-08-30 12:15:40 +00001803 if options.merge:
1804 merge(complete_state, blacklist)
1805 else:
1806 pretty_print(value, sys.stdout)
1807
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001808 if exceptions:
1809 # It got an exception, raise the first one.
1810 raise \
1811 exceptions[0][0], \
1812 exceptions[0][1], \
1813 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001814 return 0
1815
1816
maruel@chromium.orge5322512013-08-19 20:17:57 +00001817def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001818 """Creates a directory with all the dependencies mapped into it.
1819
1820 Useful to test manually why a test is failing. The target executable is not
1821 run.
1822 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001823 parser.require_isolated = False
maruel@chromium.org9268f042012-10-17 17:36:41 +00001824 options, args = parser.parse_args(args)
1825 if args:
1826 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001827 complete_state = load_complete_state(options, os.getcwd(), None, False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001828
1829 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001830 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001831 'isolate', complete_state.root_dir)
1832 else:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001833 if file_path.is_url(options.outdir):
maruel@chromium.org29029882013-08-30 12:15:40 +00001834 parser.error('Can\'t use url for --outdir with mode remap.')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001835 if not os.path.isdir(options.outdir):
1836 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001837 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001838 if len(os.listdir(options.outdir)):
1839 raise ExecutionError('Can\'t remap in a non-empty directory')
1840 recreate_tree(
1841 outdir=options.outdir,
1842 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001843 infiles=complete_state.saved_state.files,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00001844 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001845 as_hash=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001846 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001847 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001848
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001849 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001850 complete_state.save_files()
1851 return 0
1852
1853
maruel@chromium.orge5322512013-08-19 20:17:57 +00001854def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001855 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001856 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001857 options, args = parser.parse_args(args)
1858 if args:
1859 parser.error('Unsupported argument: %s' % args)
1860
1861 if options.isolated:
1862 # Load the previous state if it was present. Namely, "foo.isolated.state".
1863 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001864 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001865 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001866 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001867 if not isolate:
maruel@chromium.org29029882013-08-30 12:15:40 +00001868 parser.error('--isolate is required.')
1869
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001870 with open(isolate, 'r') as f:
1871 content = f.read()
1872 config = load_isolate_as_config(
1873 os.path.dirname(os.path.abspath(isolate)),
1874 eval_content(content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001875 extract_comment(content))
1876 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001877 print('Updating %s' % isolate)
1878 with open(isolate, 'wb') as f:
1879 print_all(config.file_comment, data, f)
1880 return 0
1881
1882
maruel@chromium.org29029882013-08-30 12:15:40 +00001883@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001884def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001885 """Runs the test executable in an isolated (temporary) directory.
1886
1887 All the dependencies are mapped into the temporary directory and the
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001888 directory is cleaned up after the target exits. Warning: if --outdir is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001889 specified, it is deleted upon exit.
1890
maruel@chromium.org29029882013-08-30 12:15:40 +00001891 Argument processing stops at -- and these arguments are appended to the
1892 command line of the target to run. For example, use:
1893 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001894 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001895 parser.require_isolated = False
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001896 parser.add_option(
1897 '--skip-refresh', action='store_true',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001898 help='Skip reading .isolate file and do not refresh the hash of '
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001899 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001900 options, args = parser.parse_args(args)
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001901 if options.outdir and file_path.is_url(options.outdir):
maruel@chromium.org29029882013-08-30 12:15:40 +00001902 parser.error('Can\'t use url for --outdir with mode run.')
1903
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001904 complete_state = load_complete_state(
1905 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001906 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001907 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001908 raise ExecutionError('No command to run.')
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001909
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001910 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001911 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001912 root_dir = complete_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001913 if not options.outdir:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001914 if not os.path.isabs(root_dir):
1915 root_dir = os.path.join(os.path.dirname(options.isolated), root_dir)
1916 options.outdir = run_isolated.make_temp_dir('isolate', root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001917 else:
1918 if not os.path.isdir(options.outdir):
1919 os.makedirs(options.outdir)
1920 recreate_tree(
1921 outdir=options.outdir,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001922 indir=root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001923 infiles=complete_state.saved_state.files,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00001924 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001925 as_hash=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001926 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001927 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001928 if not os.path.isdir(cwd):
1929 # It can happen when no files are mapped from the directory containing the
1930 # .isolate file. But the directory must exist to be the current working
1931 # directory.
1932 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001933 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001934 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001935 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1936 result = subprocess.call(cmd, cwd=cwd)
1937 finally:
1938 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001939 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001940
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001941 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001942 complete_state.save_files()
1943 return result
1944
1945
maruel@chromium.org29029882013-08-30 12:15:40 +00001946@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001947def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001948 """Traces the target using trace_inputs.py.
1949
1950 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001951 its child processes access. Then the 'merge' command can be used to generate
1952 an updated .isolate file out of it or the 'read' command to print it out to
1953 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001954
maruel@chromium.org29029882013-08-30 12:15:40 +00001955 Argument processing stops at -- and these arguments are appended to the
1956 command line of the target to run. For example, use:
1957 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001958 """
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001959 add_trace_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001960 parser.add_option(
1961 '-m', '--merge', action='store_true',
1962 help='After tracing, merge the results back in the .isolate file')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001963 parser.add_option(
1964 '--skip-refresh', action='store_true',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001965 help='Skip reading .isolate file and do not refresh the hash of '
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001966 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001967 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001968
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001969 complete_state = load_complete_state(
1970 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001971 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001972 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001973 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001974 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001975 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001976 unicode(complete_state.root_dir),
1977 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001978 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1979 if not os.path.isfile(cmd[0]):
1980 raise ExecutionError(
1981 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001982 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1983 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001984 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001985 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001986 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001987 try:
1988 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001989 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001990 cmd,
1991 cwd,
1992 'default',
1993 True)
1994 except trace_inputs.TracingFailure, e:
1995 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1996
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001997 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001998 logging.error(
1999 'Tracer exited with %d, which means the tests probably failed so the '
2000 'trace is probably incomplete.', result)
2001 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00002002
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002003 complete_state.save_files()
2004
2005 if options.merge:
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05002006 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002007 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002008
2009 return result
2010
2011
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002012def _process_variable_arg(option, opt, _value, parser):
2013 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +00002014 if not parser.rargs:
2015 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002016 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00002017 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002018 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +00002019 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002020 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +00002021 else:
2022 if not parser.rargs:
2023 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002024 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00002025 v = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002026 if not re.match('^' + VALID_VARIABLE + '$', k):
2027 raise optparse.OptionValueError(
2028 'Variable \'%s\' doesn\'t respect format \'%s\'' % (k, VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -05002029 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +00002030
2031
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002032def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002033 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002034 parser.add_option(
2035 '-s', '--isolated',
2036 metavar='FILE',
2037 help='.isolated file to generate or read')
2038 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002039 parser.add_option(
2040 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002041 dest='isolated',
2042 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002043 is_win = sys.platform in ('win32', 'cygwin')
2044 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05002045 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002046 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05002047 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002048 # - configuration variables that are to be used in deducing the matrix to
2049 # reduce.
2050 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002051 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002052 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00002053 action='callback',
2054 callback=_process_variable_arg,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002055 default=[('OS', get_flavor())],
2056 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002057 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002058 help='Config variables are used to determine which conditions should be '
2059 'matched when loading a .isolate file, default: %default. '
2060 'All 3 kinds of variables are persistent accross calls, they are '
2061 'saved inside <.isolated>.state')
2062 parser.add_option(
2063 '--path-variable',
2064 action='callback',
2065 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05002066 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002067 dest='path_variables',
2068 metavar='FOO BAR',
2069 help='Path variables are used to replace file paths when loading a '
2070 '.isolate file, default: %default')
2071 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05002072 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002073 action='callback',
2074 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05002075 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
2076 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002077 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05002078 help='Extraneous variables are replaced on the \'command\' entry and on '
2079 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002080
2081
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002082def add_trace_option(parser):
2083 """Adds --trace-blacklist to the parser."""
2084 parser.add_option(
2085 '--trace-blacklist',
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05002086 action='append', default=list(isolateserver.DEFAULT_BLACKLIST),
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002087 help='List of regexp to use as blacklist filter for files to consider '
2088 'important, not to be confused with --blacklist which blacklists '
2089 'test case.')
2090
2091
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002092def parse_isolated_option(parser, options, cwd, require_isolated):
2093 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002094 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002095 options.isolated = os.path.normpath(
2096 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002097 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00002098 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002099 if options.isolated and not options.isolated.endswith('.isolated'):
2100 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002101
2102
2103def parse_variable_option(options):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002104 """Processes all the --<foo>-variable flags."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002105 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
2106 # but it wouldn't be backward compatible.
2107 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002108 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002109 try:
2110 return int(s)
2111 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002112 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002113 options.config_variables = dict(
2114 (k, try_make_int(v)) for k, v in options.config_variables)
2115 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05002116 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002117
2118
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002119class OptionParserIsolate(tools.OptionParserWithLogging):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05002120 """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
2121 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00002122 # Set it to False if it is not required, e.g. it can be passed on but do not
2123 # fail if not given.
2124 require_isolated = True
2125
2126 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002127 tools.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00002128 self,
2129 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
2130 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002131 group = optparse.OptionGroup(self, "Common options")
2132 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002133 '-i', '--isolate',
2134 metavar='FILE',
2135 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002136 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002137 group.add_option(
2138 '-o', '--outdir', metavar='DIR',
2139 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002140 'Defaults: run|remap: a /tmp subdirectory, others: '
2141 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00002142 group.add_option(
2143 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002144 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
2145 help='Indicates that invalid entries in the isolated file to be '
2146 'only be logged and not stop processing. Defaults to True if '
2147 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002148 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002149
2150 def parse_args(self, *args, **kwargs):
2151 """Makes sure the paths make sense.
2152
2153 On Windows, / and \ are often mixed together in a path.
2154 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002155 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002156 self, *args, **kwargs)
2157 if not self.allow_interspersed_args and args:
2158 self.error('Unsupported argument: %s' % args)
2159
maruel@chromium.org561d4b22013-09-26 21:08:08 +00002160 cwd = file_path.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002161 parse_isolated_option(self, options, cwd, self.require_isolated)
2162 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002163
2164 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002165 # TODO(maruel): Work with non-ASCII.
2166 # The path must be in native path case for tracing purposes.
2167 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
2168 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
maruel@chromium.org561d4b22013-09-26 21:08:08 +00002169 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002170
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05002171 if options.outdir and not file_path.is_url(options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002172 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
2173 # outdir doesn't need native path case since tracing is never done from
2174 # there.
2175 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002176
2177 return options, args
2178
2179
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002180def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00002181 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002182 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00002183 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00002184 except Exception as e:
2185 tools.report_error(e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002186 return 1
2187
2188
2189if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00002190 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002191 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00002192 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002193 sys.exit(main(sys.argv[1:]))