blob: 2c2b57fd79cebc220d750a0859f86dc2a9630fa1 [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
maruel@chromium.org29029882013-08-30 12:15:40 +000043__version__ = '0.1.1'
maruel@chromium.org3d671992013-08-20 00:38:27 +000044
45
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000046PATH_VARIABLES = ('DEPTH', 'PRODUCT_DIR')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000047
48# Files that should be 0-length when mapped.
49KEY_TOUCHED = 'isolate_dependency_touched'
50# Files that should be tracked by the build tool.
51KEY_TRACKED = 'isolate_dependency_tracked'
52# Files that should not be tracked by the build tool.
53KEY_UNTRACKED = 'isolate_dependency_untracked'
54
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000055
56class ExecutionError(Exception):
57 """A generic error occurred."""
58 def __str__(self):
59 return self.args[0]
60
61
62### Path handling code.
63
64
csharp@chromium.org01856802012-11-12 17:48:13 +000065def expand_directories_and_symlinks(indir, infiles, blacklist,
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +000066 follow_symlinks, ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000067 """Expands the directories and the symlinks, applies the blacklist and
68 verifies files exist.
69
70 Files are specified in os native path separator.
71 """
72 outfiles = []
73 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +000074 try:
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -050075 outfiles.extend(
76 isolateserver.expand_directory_and_symlink(
77 indir, relfile, blacklist, follow_symlinks))
maruel@chromium.org9958e4a2013-09-17 00:01:48 +000078 except isolateserver.MappingError as e:
csharp@chromium.org01856802012-11-12 17:48:13 +000079 if ignore_broken_items:
80 logging.info('warning: %s', e)
81 else:
82 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000083 return outfiles
84
85
maruel@chromium.org7b844a62013-09-17 13:04:59 +000086def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000087 """Creates a new tree with only the input files in it.
88
89 Arguments:
90 outdir: Output directory to create the files in.
91 indir: Root directory the infiles are based in.
92 infiles: dict of files to map from |indir| to |outdir|.
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000093 action: One of accepted action of run_isolated.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +000094 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000095 """
96 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +000097 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
98 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000099
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000100 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000101 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000102 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000103 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000104
105 for relfile, metadata in infiles.iteritems():
106 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000107 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000108 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000109 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000110 # Skip links when storing a hashtable.
111 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000112 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000113 if os.path.isfile(outfile):
114 # Just do a quick check that the file size matches. No need to stat()
115 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000116 if not 's' in metadata:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000117 raise isolateserver.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000118 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000119 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000120 continue
121 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000122 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000123 os.remove(outfile)
124 else:
125 outfile = os.path.join(outdir, relfile)
126 outsubdir = os.path.dirname(outfile)
127 if not os.path.isdir(outsubdir):
128 os.makedirs(outsubdir)
129
130 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000131 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000132 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000133 if 'l' in metadata:
134 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000135 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000136 # symlink doesn't exist on Windows.
137 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000138 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000139 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000140
141
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000142### Variable stuff.
143
144
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000145def isolatedfile_to_state(filename):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000146 """Replaces the file's extension."""
maruel@chromium.org4d52ce42012-10-05 12:22:35 +0000147 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000148
149
150def determine_root_dir(relative_root, infiles):
151 """For a list of infiles, determines the deepest root directory that is
152 referenced indirectly.
153
154 All arguments must be using os.path.sep.
155 """
156 # The trick used to determine the root directory is to look at "how far" back
157 # up it is looking up.
158 deepest_root = relative_root
159 for i in infiles:
160 x = relative_root
161 while i.startswith('..' + os.path.sep):
162 i = i[3:]
163 assert not i.startswith(os.path.sep)
164 x = os.path.dirname(x)
165 if deepest_root.startswith(x):
166 deepest_root = x
167 logging.debug(
168 'determine_root_dir(%s, %d files) -> %s' % (
169 relative_root, len(infiles), deepest_root))
170 return deepest_root
171
172
173def replace_variable(part, variables):
174 m = re.match(r'<\(([A-Z_]+)\)', part)
175 if m:
176 if m.group(1) not in variables:
177 raise ExecutionError(
178 'Variable "%s" was not found in %s.\nDid you forget to specify '
179 '--variable?' % (m.group(1), variables))
180 return variables[m.group(1)]
181 return part
182
183
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000184def process_variables(cwd, variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000185 """Processes path variables as a special case and returns a copy of the dict.
186
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000187 For each 'path' variable: first normalizes it based on |cwd|, verifies it
188 exists then sets it as relative to relative_base_dir.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000189 """
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000190 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000191 variables = variables.copy()
192 for i in PATH_VARIABLES:
193 if i not in variables:
194 continue
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000195 variable = variables[i].strip()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000196 # Variables could contain / or \ on windows. Always normalize to
197 # os.path.sep.
csharp@chromium.orgdd23b172013-03-15 16:00:27 +0000198 variable = variable.replace('/', os.path.sep)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000199 variable = os.path.join(cwd, variable)
200 variable = os.path.normpath(variable)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000201 variable = file_path.get_native_path_case(variable)
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000202 if not os.path.isdir(variable):
203 raise ExecutionError('%s=%s is not a directory' % (i, variable))
204
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000205 # All variables are relative to the .isolate file.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000206 variable = os.path.relpath(variable, relative_base_dir)
207 logging.debug(
208 'Translated variable %s from %s to %s', i, variables[i], variable)
209 variables[i] = variable
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000210 return variables
211
212
213def eval_variables(item, variables):
214 """Replaces the .isolate variables in a string item.
215
216 Note that the .isolate format is a subset of the .gyp dialect.
217 """
218 return ''.join(
219 replace_variable(p, variables) for p in re.split(r'(<\([A-Z_]+\))', item))
220
221
222def classify_files(root_dir, tracked, untracked):
223 """Converts the list of files into a .isolate 'variables' dictionary.
224
225 Arguments:
226 - tracked: list of files names to generate a dictionary out of that should
227 probably be tracked.
228 - untracked: list of files names that must not be tracked.
229 """
230 # These directories are not guaranteed to be always present on every builder.
231 OPTIONAL_DIRECTORIES = (
232 'test/data/plugin',
233 'third_party/WebKit/LayoutTests',
234 )
235
236 new_tracked = []
237 new_untracked = list(untracked)
238
239 def should_be_tracked(filepath):
240 """Returns True if it is a file without whitespace in a non-optional
241 directory that has no symlink in its path.
242 """
243 if filepath.endswith('/'):
244 return False
245 if ' ' in filepath:
246 return False
247 if any(i in filepath for i in OPTIONAL_DIRECTORIES):
248 return False
249 # Look if any element in the path is a symlink.
250 split = filepath.split('/')
251 for i in range(len(split)):
252 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
253 return False
254 return True
255
256 for filepath in sorted(tracked):
257 if should_be_tracked(filepath):
258 new_tracked.append(filepath)
259 else:
260 # Anything else.
261 new_untracked.append(filepath)
262
263 variables = {}
264 if new_tracked:
265 variables[KEY_TRACKED] = sorted(new_tracked)
266 if new_untracked:
267 variables[KEY_UNTRACKED] = sorted(new_untracked)
268 return variables
269
270
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000271def chromium_fix(f, variables):
272 """Fixes an isolate dependnecy with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000273 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
274 # separator.
275 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000276 # Ignored items.
277 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000278 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000279 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000280 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000281 # 'First Run' is not created by the compile, but by the test itself.
282 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000283
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000284 # Blacklist logs and other unimportant files.
285 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
286 logging.debug('Ignoring %s', f)
287 return None
288
maruel@chromium.org7650e422012-11-16 21:56:42 +0000289 EXECUTABLE = re.compile(
290 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
291 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
292 r'$')
293 match = EXECUTABLE.match(f)
294 if match:
295 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
296
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000297 if sys.platform == 'darwin':
298 # On OSX, the name of the output is dependent on gyp define, it can be
299 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
300 # Framework.framework'. Furthermore, they are versioned with a gyp
301 # variable. To lower the complexity of the .isolate file, remove all the
302 # individual entries that show up under any of the 4 entries and replace
303 # them with the directory itself. Overall, this results in a bit more
304 # files than strictly necessary.
305 OSX_BUNDLES = (
306 '<(PRODUCT_DIR)/Chromium Framework.framework/',
307 '<(PRODUCT_DIR)/Chromium.app/',
308 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
309 '<(PRODUCT_DIR)/Google Chrome.app/',
310 )
311 for prefix in OSX_BUNDLES:
312 if f.startswith(prefix):
313 # Note this result in duplicate values, so the a set() must be used to
314 # remove duplicates.
315 return prefix
316 return f
317
318
319def generate_simplified(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000320 tracked, untracked, touched, root_dir, variables, relative_cwd,
321 trace_blacklist):
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000322 """Generates a clean and complete .isolate 'variables' dictionary.
323
324 Cleans up and extracts only files from within root_dir then processes
325 variables and relative_cwd.
326 """
327 root_dir = os.path.realpath(root_dir)
328 logging.info(
329 'generate_simplified(%d files, %s, %s, %s)' %
330 (len(tracked) + len(untracked) + len(touched),
331 root_dir, variables, relative_cwd))
332
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000333 # Preparation work.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500334 relative_cwd = file_path.cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000335 assert not os.path.isabs(relative_cwd), relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000336 # Creates the right set of variables here. We only care about PATH_VARIABLES.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000337 path_variables = dict(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000338 ('<(%s)' % k, variables[k].replace(os.path.sep, '/'))
339 for k in PATH_VARIABLES if k in variables)
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000340 variables = variables.copy()
341 variables.update(path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000342
343 # Actual work: Process the files.
344 # TODO(maruel): if all the files in a directory are in part tracked and in
345 # part untracked, the directory will not be extracted. Tracked files should be
346 # 'promoted' to be untracked as needed.
347 tracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000348 root_dir, tracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000349 untracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000350 root_dir, untracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000351 # touched is not compressed, otherwise it would result in files to be archived
352 # that we don't need.
353
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000354 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000355 def fix(f):
356 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000357 # Important, GYP stores the files with / and not \.
358 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000359 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000360 # If it's not already a variable.
361 if not f.startswith('<'):
362 # relative_cwd is usually the directory containing the gyp file. It may be
363 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000364 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000365 # Convert the whole thing to / since it's isolate's speak.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500366 f = file_path.posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000367 posixpath.join(root_dir_posix, f),
368 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000369
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000370 for variable, root_path in path_variables.iteritems():
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000371 if f.startswith(root_path):
372 f = variable + f[len(root_path):]
maruel@chromium.org6b365dc2012-10-18 19:17:56 +0000373 logging.debug('Converted to %s' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000374 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000375 return f
376
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000377 def fix_all(items):
378 """Reduces the items to convert variables, removes unneeded items, apply
379 chromium-specific fixes and only return unique items.
380 """
381 variables_converted = (fix(f.path) for f in items)
382 chromium_fixed = (chromium_fix(f, variables) for f in variables_converted)
383 return set(f for f in chromium_fixed if f)
384
385 tracked = fix_all(tracked)
386 untracked = fix_all(untracked)
387 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000388 out = classify_files(root_dir, tracked, untracked)
389 if touched:
390 out[KEY_TOUCHED] = sorted(touched)
391 return out
392
393
benrg@chromium.org609b7982013-02-07 16:44:46 +0000394def chromium_filter_flags(variables):
395 """Filters out build flags used in Chromium that we don't want to treat as
396 configuration variables.
397 """
398 # TODO(benrg): Need a better way to determine this.
399 blacklist = set(PATH_VARIABLES + ('EXECUTABLE_SUFFIX', 'FLAG'))
400 return dict((k, v) for k, v in variables.iteritems() if k not in blacklist)
401
402
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000403def generate_isolate(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000404 tracked, untracked, touched, root_dir, variables, relative_cwd,
405 trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000406 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000407 dependencies = generate_simplified(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000408 tracked, untracked, touched, root_dir, variables, relative_cwd,
409 trace_blacklist)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000410 config_variables = chromium_filter_flags(variables)
411 config_variable_names, config_values = zip(
412 *sorted(config_variables.iteritems()))
413 out = Configs(None)
414 # The new dependencies apply to just one configuration, namely config_values.
415 out.merge_dependencies(dependencies, config_variable_names, [config_values])
416 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000417
418
419def split_touched(files):
420 """Splits files that are touched vs files that are read."""
421 tracked = []
422 touched = []
423 for f in files:
424 if f.size:
425 tracked.append(f)
426 else:
427 touched.append(f)
428 return tracked, touched
429
430
431def pretty_print(variables, stdout):
432 """Outputs a gyp compatible list from the decoded variables.
433
434 Similar to pprint.print() but with NIH syndrome.
435 """
436 # Order the dictionary keys by these keys in priority.
437 ORDER = (
438 'variables', 'condition', 'command', 'relative_cwd', 'read_only',
439 KEY_TRACKED, KEY_UNTRACKED)
440
441 def sorting_key(x):
442 """Gives priority to 'most important' keys before the others."""
443 if x in ORDER:
444 return str(ORDER.index(x))
445 return x
446
447 def loop_list(indent, items):
448 for item in items:
449 if isinstance(item, basestring):
450 stdout.write('%s\'%s\',\n' % (indent, item))
451 elif isinstance(item, dict):
452 stdout.write('%s{\n' % indent)
453 loop_dict(indent + ' ', item)
454 stdout.write('%s},\n' % indent)
455 elif isinstance(item, list):
456 # A list inside a list will write the first item embedded.
457 stdout.write('%s[' % indent)
458 for index, i in enumerate(item):
459 if isinstance(i, basestring):
460 stdout.write(
461 '\'%s\', ' % i.replace('\\', '\\\\').replace('\'', '\\\''))
462 elif isinstance(i, dict):
463 stdout.write('{\n')
464 loop_dict(indent + ' ', i)
465 if index != len(item) - 1:
466 x = ', '
467 else:
468 x = ''
469 stdout.write('%s}%s' % (indent, x))
470 else:
471 assert False
472 stdout.write('],\n')
473 else:
474 assert False
475
476 def loop_dict(indent, items):
477 for key in sorted(items, key=sorting_key):
478 item = items[key]
479 stdout.write("%s'%s': " % (indent, key))
480 if isinstance(item, dict):
481 stdout.write('{\n')
482 loop_dict(indent + ' ', item)
483 stdout.write(indent + '},\n')
484 elif isinstance(item, list):
485 stdout.write('[\n')
486 loop_list(indent + ' ', item)
487 stdout.write(indent + '],\n')
488 elif isinstance(item, basestring):
489 stdout.write(
490 '\'%s\',\n' % item.replace('\\', '\\\\').replace('\'', '\\\''))
491 elif item in (True, False, None):
492 stdout.write('%s\n' % item)
493 else:
494 assert False, item
495
496 stdout.write('{\n')
497 loop_dict(' ', variables)
498 stdout.write('}\n')
499
500
501def union(lhs, rhs):
502 """Merges two compatible datastructures composed of dict/list/set."""
503 assert lhs is not None or rhs is not None
504 if lhs is None:
505 return copy.deepcopy(rhs)
506 if rhs is None:
507 return copy.deepcopy(lhs)
508 assert type(lhs) == type(rhs), (lhs, rhs)
509 if hasattr(lhs, 'union'):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000510 # Includes set, ConfigSettings and Configs.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000511 return lhs.union(rhs)
512 if isinstance(lhs, dict):
513 return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs))
514 elif isinstance(lhs, list):
515 # Do not go inside the list.
516 return lhs + rhs
517 assert False, type(lhs)
518
519
520def extract_comment(content):
521 """Extracts file level comment."""
522 out = []
523 for line in content.splitlines(True):
524 if line.startswith('#'):
525 out.append(line)
526 else:
527 break
528 return ''.join(out)
529
530
531def eval_content(content):
532 """Evaluates a python file and return the value defined in it.
533
534 Used in practice for .isolate files.
535 """
536 globs = {'__builtins__': None}
537 locs = {}
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000538 try:
539 value = eval(content, globs, locs)
540 except TypeError as e:
541 e.args = list(e.args) + [content]
542 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000543 assert locs == {}, locs
544 assert globs == {'__builtins__': None}, globs
545 return value
546
547
benrg@chromium.org609b7982013-02-07 16:44:46 +0000548def match_configs(expr, config_variables, all_configs):
549 """Returns the configs from |all_configs| that match the |expr|, where
550 the elements of |all_configs| are tuples of values for the |config_variables|.
551 Example:
552 >>> match_configs(expr = "(foo==1 or foo==2) and bar=='b'",
553 config_variables = ["foo", "bar"],
554 all_configs = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
555 [(1, 'b'), (2, 'b')]
556 """
557 return [
558 config for config in all_configs
559 if eval(expr, dict(zip(config_variables, config)))
560 ]
561
562
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000563def verify_variables(variables):
564 """Verifies the |variables| dictionary is in the expected format."""
565 VALID_VARIABLES = [
566 KEY_TOUCHED,
567 KEY_TRACKED,
568 KEY_UNTRACKED,
569 'command',
570 'read_only',
571 ]
572 assert isinstance(variables, dict), variables
573 assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys()
574 for name, value in variables.iteritems():
575 if name == 'read_only':
576 assert value in (True, False, None), value
577 else:
578 assert isinstance(value, list), value
579 assert all(isinstance(i, basestring) for i in value), value
580
581
benrg@chromium.org609b7982013-02-07 16:44:46 +0000582def verify_ast(expr, variables_and_values):
583 """Verifies that |expr| is of the form
584 expr ::= expr ( "or" | "and" ) expr
585 | identifier "==" ( string | int )
586 Also collects the variable identifiers and string/int values in the dict
587 |variables_and_values|, in the form {'var': set([val1, val2, ...]), ...}.
588 """
589 assert isinstance(expr, (ast.BoolOp, ast.Compare))
590 if isinstance(expr, ast.BoolOp):
591 assert isinstance(expr.op, (ast.And, ast.Or))
592 for subexpr in expr.values:
593 verify_ast(subexpr, variables_and_values)
594 else:
595 assert isinstance(expr.left.ctx, ast.Load)
596 assert len(expr.ops) == 1
597 assert isinstance(expr.ops[0], ast.Eq)
598 var_values = variables_and_values.setdefault(expr.left.id, set())
599 rhs = expr.comparators[0]
600 assert isinstance(rhs, (ast.Str, ast.Num))
601 var_values.add(rhs.n if isinstance(rhs, ast.Num) else rhs.s)
602
603
604def verify_condition(condition, variables_and_values):
605 """Verifies the |condition| dictionary is in the expected format.
606 See verify_ast() for the meaning of |variables_and_values|.
607 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000608 VALID_INSIDE_CONDITION = ['variables']
609 assert isinstance(condition, list), condition
benrg@chromium.org609b7982013-02-07 16:44:46 +0000610 assert len(condition) == 2, condition
611 expr, then = condition
612
613 test_ast = compile(expr, '<condition>', 'eval', ast.PyCF_ONLY_AST)
614 verify_ast(test_ast.body, variables_and_values)
615
616 assert isinstance(then, dict), then
617 assert set(VALID_INSIDE_CONDITION).issuperset(set(then)), then.keys()
618 verify_variables(then['variables'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000619
620
benrg@chromium.org609b7982013-02-07 16:44:46 +0000621def verify_root(value, variables_and_values):
622 """Verifies that |value| is the parsed form of a valid .isolate file.
623 See verify_ast() for the meaning of |variables_and_values|.
624 """
625 VALID_ROOTS = ['includes', 'conditions']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000626 assert isinstance(value, dict), value
627 assert set(VALID_ROOTS).issuperset(set(value)), value.keys()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000628
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000629 includes = value.get('includes', [])
630 assert isinstance(includes, list), includes
631 for include in includes:
632 assert isinstance(include, basestring), include
633
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000634 conditions = value.get('conditions', [])
635 assert isinstance(conditions, list), conditions
636 for condition in conditions:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000637 verify_condition(condition, variables_and_values)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000638
639
benrg@chromium.org609b7982013-02-07 16:44:46 +0000640def remove_weak_dependencies(values, key, item, item_configs):
641 """Removes any configs from this key if the item is already under a
642 strong key.
643 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000644 if key == KEY_TOUCHED:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000645 item_configs = set(item_configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000646 for stronger_key in (KEY_TRACKED, KEY_UNTRACKED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000647 try:
648 item_configs -= values[stronger_key][item]
649 except KeyError:
650 pass
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000651
benrg@chromium.org609b7982013-02-07 16:44:46 +0000652 return item_configs
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000653
654
benrg@chromium.org609b7982013-02-07 16:44:46 +0000655def remove_repeated_dependencies(folders, key, item, item_configs):
656 """Removes any configs from this key if the item is in a folder that is
657 already included."""
csharp@chromium.org31176252012-11-02 13:04:40 +0000658
659 if key in (KEY_UNTRACKED, KEY_TRACKED, KEY_TOUCHED):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000660 item_configs = set(item_configs)
661 for (folder, configs) in folders.iteritems():
csharp@chromium.org31176252012-11-02 13:04:40 +0000662 if folder != item and item.startswith(folder):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000663 item_configs -= configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000664
benrg@chromium.org609b7982013-02-07 16:44:46 +0000665 return item_configs
csharp@chromium.org31176252012-11-02 13:04:40 +0000666
667
668def get_folders(values_dict):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000669 """Returns a dict of all the folders in the given value_dict."""
670 return dict(
671 (item, configs) for (item, configs) in values_dict.iteritems()
672 if item.endswith('/')
673 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000674
675
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000676def invert_map(variables):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000677 """Converts {config: {deptype: list(depvals)}} to
678 {deptype: {depval: set(configs)}}.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000679 """
680 KEYS = (
681 KEY_TOUCHED,
682 KEY_TRACKED,
683 KEY_UNTRACKED,
684 'command',
685 'read_only',
686 )
687 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000688 for config, values in variables.iteritems():
689 for key in KEYS:
690 if key == 'command':
691 items = [tuple(values[key])] if key in values else []
692 elif key == 'read_only':
693 items = [values[key]] if key in values else []
694 else:
695 assert key in (KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED)
696 items = values.get(key, [])
697 for item in items:
698 out[key].setdefault(item, set()).add(config)
699 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000700
701
benrg@chromium.org609b7982013-02-07 16:44:46 +0000702def reduce_inputs(values):
703 """Reduces the output of invert_map() to the strictest minimum list.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000704
benrg@chromium.org609b7982013-02-07 16:44:46 +0000705 Looks at each individual file and directory, maps where they are used and
706 reconstructs the inverse dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000707
benrg@chromium.org609b7982013-02-07 16:44:46 +0000708 Returns the minimized dictionary.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000709 """
710 KEYS = (
711 KEY_TOUCHED,
712 KEY_TRACKED,
713 KEY_UNTRACKED,
714 'command',
715 'read_only',
716 )
csharp@chromium.org31176252012-11-02 13:04:40 +0000717
718 # Folders can only live in KEY_UNTRACKED.
719 folders = get_folders(values.get(KEY_UNTRACKED, {}))
720
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000721 out = dict((key, {}) for key in KEYS)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000722 for key in KEYS:
723 for item, item_configs in values.get(key, {}).iteritems():
724 item_configs = remove_weak_dependencies(values, key, item, item_configs)
725 item_configs = remove_repeated_dependencies(
726 folders, key, item, item_configs)
727 if item_configs:
728 out[key][item] = item_configs
729 return out
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000730
731
benrg@chromium.org609b7982013-02-07 16:44:46 +0000732def convert_map_to_isolate_dict(values, config_variables):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000733 """Regenerates back a .isolate configuration dict from files and dirs
734 mappings generated from reduce_inputs().
735 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000736 # Gather a list of configurations for set inversion later.
737 all_mentioned_configs = set()
738 for configs_by_item in values.itervalues():
739 for configs in configs_by_item.itervalues():
740 all_mentioned_configs.update(configs)
741
742 # Invert the mapping to make it dict first.
743 conditions = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000744 for key in values:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000745 for item, configs in values[key].iteritems():
746 then = conditions.setdefault(frozenset(configs), {})
747 variables = then.setdefault('variables', {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000748
benrg@chromium.org609b7982013-02-07 16:44:46 +0000749 if item in (True, False):
750 # One-off for read_only.
751 variables[key] = item
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000752 else:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000753 assert item
754 if isinstance(item, tuple):
755 # One-off for command.
756 # Do not merge lists and do not sort!
757 # Note that item is a tuple.
758 assert key not in variables
759 variables[key] = list(item)
760 else:
761 # The list of items (files or dirs). Append the new item and keep
762 # the list sorted.
763 l = variables.setdefault(key, [])
764 l.append(item)
765 l.sort()
766
767 if all_mentioned_configs:
768 config_values = map(set, zip(*all_mentioned_configs))
769 sef = short_expression_finder.ShortExpressionFinder(
770 zip(config_variables, config_values))
771
772 conditions = sorted(
773 [sef.get_expr(configs), then] for configs, then in conditions.iteritems())
774 return {'conditions': conditions}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000775
776
777### Internal state files.
778
779
benrg@chromium.org609b7982013-02-07 16:44:46 +0000780class ConfigSettings(object):
781 """Represents the dependency variables for a single build configuration.
782 The structure is immutable.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000783 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000784 def __init__(self, config, values):
785 self.config = config
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000786 verify_variables(values)
787 self.touched = sorted(values.get(KEY_TOUCHED, []))
788 self.tracked = sorted(values.get(KEY_TRACKED, []))
789 self.untracked = sorted(values.get(KEY_UNTRACKED, []))
790 self.command = values.get('command', [])[:]
791 self.read_only = values.get('read_only')
792
793 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000794 assert not (self.config and rhs.config) or (self.config == rhs.config)
maruel@chromium.org669edcb2012-11-02 19:16:14 +0000795 assert not (self.command and rhs.command) or (self.command == rhs.command)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000796 var = {
797 KEY_TOUCHED: sorted(self.touched + rhs.touched),
798 KEY_TRACKED: sorted(self.tracked + rhs.tracked),
799 KEY_UNTRACKED: sorted(self.untracked + rhs.untracked),
800 'command': self.command or rhs.command,
801 'read_only': rhs.read_only if self.read_only is None else self.read_only,
802 }
benrg@chromium.org609b7982013-02-07 16:44:46 +0000803 return ConfigSettings(self.config or rhs.config, var)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000804
805 def flatten(self):
806 out = {}
807 if self.command:
808 out['command'] = self.command
809 if self.touched:
810 out[KEY_TOUCHED] = self.touched
811 if self.tracked:
812 out[KEY_TRACKED] = self.tracked
813 if self.untracked:
814 out[KEY_UNTRACKED] = self.untracked
815 if self.read_only is not None:
816 out['read_only'] = self.read_only
817 return out
818
819
820class Configs(object):
821 """Represents a processed .isolate file.
822
benrg@chromium.org609b7982013-02-07 16:44:46 +0000823 Stores the file in a processed way, split by configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000824 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000825 def __init__(self, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000826 self.file_comment = file_comment
benrg@chromium.org609b7982013-02-07 16:44:46 +0000827 # The keys of by_config are tuples of values for the configuration
828 # variables. The names of the variables (which must be the same for
829 # every by_config key) are kept in config_variables. Initially by_config
830 # is empty and we don't know what configuration variables will be used,
831 # so config_variables also starts out empty. It will be set by the first
832 # call to union() or merge_dependencies().
833 self.by_config = {}
834 self.config_variables = ()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000835
836 def union(self, rhs):
benrg@chromium.org609b7982013-02-07 16:44:46 +0000837 """Adds variables from rhs (a Configs) to the existing variables.
838 """
839 config_variables = self.config_variables
840 if not config_variables:
841 config_variables = rhs.config_variables
842 else:
843 # We can't proceed if this isn't true since we don't know the correct
844 # default values for extra variables. The variables are sorted so we
845 # don't need to worry about permutations.
846 if rhs.config_variables and rhs.config_variables != config_variables:
847 raise ExecutionError(
848 'Variables in merged .isolate files do not match: %r and %r' % (
849 config_variables, rhs.config_variables))
850
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000851 # Takes the first file comment, prefering lhs.
benrg@chromium.org609b7982013-02-07 16:44:46 +0000852 out = Configs(self.file_comment or rhs.file_comment)
853 out.config_variables = config_variables
854 for config in set(self.by_config) | set(rhs.by_config):
855 out.by_config[config] = union(
856 self.by_config.get(config), rhs.by_config.get(config))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000857 return out
858
benrg@chromium.org609b7982013-02-07 16:44:46 +0000859 def merge_dependencies(self, values, config_variables, configs):
860 """Adds new dependencies to this object for the given configurations.
861 Arguments:
862 values: A variables dict as found in a .isolate file, e.g.,
863 {KEY_TOUCHED: [...], 'command': ...}.
864 config_variables: An ordered list of configuration variables, e.g.,
865 ["OS", "chromeos"]. If this object already contains any dependencies,
866 the configuration variables must match.
867 configs: a list of tuples of values of the configuration variables,
868 e.g., [("mac", 0), ("linux", 1)]. The dependencies in |values|
869 are added to all of these configurations, and other configurations
870 are unchanged.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000871 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000872 if not values:
873 return
874
875 if not self.config_variables:
876 self.config_variables = config_variables
877 else:
878 # See comment in Configs.union().
879 assert self.config_variables == config_variables
880
881 for config in configs:
882 self.by_config[config] = union(
883 self.by_config.get(config), ConfigSettings(config, values))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000884
885 def flatten(self):
886 """Returns a flat dictionary representation of the configuration.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000887 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000888 return dict((k, v.flatten()) for k, v in self.by_config.iteritems())
889
890 def make_isolate_file(self):
891 """Returns a dictionary suitable for writing to a .isolate file.
892 """
893 dependencies_by_config = self.flatten()
894 configs_by_dependency = reduce_inputs(invert_map(dependencies_by_config))
895 return convert_map_to_isolate_dict(configs_by_dependency,
896 self.config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000897
898
benrg@chromium.org609b7982013-02-07 16:44:46 +0000899# TODO(benrg): Remove this function when no old-format files are left.
900def convert_old_to_new_format(value):
901 """Converts from the old .isolate format, which only has one variable (OS),
902 always includes 'linux', 'mac' and 'win' in the set of valid values for OS,
903 and allows conditions that depend on the set of all OSes, to the new format,
904 which allows any set of variables, has no hardcoded values, and only allows
905 explicit positive tests of variable values.
906 """
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +0000907 conditions = value.get('conditions', [])
benrg@chromium.org609b7982013-02-07 16:44:46 +0000908 if 'variables' not in value and all(len(cond) == 2 for cond in conditions):
909 return value # Nothing to change
910
911 def parse_condition(cond):
912 return re.match(r'OS=="(\w+)"\Z', cond[0]).group(1)
913
914 oses = set(map(parse_condition, conditions))
915 default_oses = set(['linux', 'mac', 'win'])
916 oses = sorted(oses | default_oses)
917
918 def if_not_os(not_os, then):
919 expr = ' or '.join('OS=="%s"' % os for os in oses if os != not_os)
920 return [expr, then]
921
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +0000922 conditions = [
923 cond[:2] for cond in conditions if cond[1]
924 ] + [
925 if_not_os(parse_condition(cond), cond[2])
benrg@chromium.org609b7982013-02-07 16:44:46 +0000926 for cond in conditions if len(cond) == 3
927 ]
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +0000928
benrg@chromium.org609b7982013-02-07 16:44:46 +0000929 if 'variables' in value:
930 conditions.append(if_not_os(None, {'variables': value.pop('variables')}))
931 conditions.sort()
932
benrg@chromium.org7e8e97b2013-02-09 03:16:48 +0000933 value = value.copy()
934 value['conditions'] = conditions
benrg@chromium.org609b7982013-02-07 16:44:46 +0000935 return value
936
937
938def load_isolate_as_config(isolate_dir, value, file_comment):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000939 """Parses one .isolate file and returns a Configs() instance.
940
941 |value| is the loaded dictionary that was defined in the gyp file.
942
943 The expected format is strict, anything diverting from the format below will
944 throw an assert:
945 {
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000946 'includes': [
947 'foo.isolate',
948 ],
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000949 'conditions': [
benrg@chromium.org609b7982013-02-07 16:44:46 +0000950 ['OS=="vms" and foo=42', {
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000951 'variables': {
benrg@chromium.org609b7982013-02-07 16:44:46 +0000952 'command': [
953 ...
954 ],
955 'isolate_dependency_tracked': [
956 ...
957 ],
958 'isolate_dependency_untracked': [
959 ...
960 ],
961 'read_only': False,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000962 },
963 }],
964 ...
965 ],
966 }
967 """
benrg@chromium.org609b7982013-02-07 16:44:46 +0000968 value = convert_old_to_new_format(value)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000969
benrg@chromium.org609b7982013-02-07 16:44:46 +0000970 variables_and_values = {}
971 verify_root(value, variables_and_values)
972 if variables_and_values:
973 config_variables, config_values = zip(
974 *sorted(variables_and_values.iteritems()))
975 all_configs = list(itertools.product(*config_values))
976 else:
977 config_variables = None
978 all_configs = []
979
980 isolate = Configs(file_comment)
981
982 # Add configuration-specific variables.
983 for expr, then in value.get('conditions', []):
984 configs = match_configs(expr, config_variables, all_configs)
985 isolate.merge_dependencies(then['variables'], config_variables, configs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000986
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000987 # Load the includes.
988 for include in value.get('includes', []):
989 if os.path.isabs(include):
990 raise ExecutionError(
991 'Failed to load configuration; absolute include path \'%s\'' %
992 include)
993 included_isolate = os.path.normpath(os.path.join(isolate_dir, include))
994 with open(included_isolate, 'r') as f:
benrg@chromium.org609b7982013-02-07 16:44:46 +0000995 included_isolate = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000996 os.path.dirname(included_isolate),
997 eval_content(f.read()),
benrg@chromium.org609b7982013-02-07 16:44:46 +0000998 None)
999 isolate = union(isolate, included_isolate)
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001000
benrg@chromium.org609b7982013-02-07 16:44:46 +00001001 return isolate
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001002
1003
benrg@chromium.org609b7982013-02-07 16:44:46 +00001004def load_isolate_for_config(isolate_dir, content, variables):
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001005 """Loads the .isolate file and returns the information unprocessed but
1006 filtered for the specific OS.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001007
1008 Returns the command, dependencies and read_only flag. The dependencies are
1009 fixed to use os.path.sep.
1010 """
1011 # Load the .isolate file, process its conditions, retrieve the command and
1012 # dependencies.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001013 isolate = load_isolate_as_config(isolate_dir, eval_content(content), None)
1014 try:
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001015 config_name = tuple(variables[var] for var in isolate.config_variables)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001016 except KeyError:
1017 raise ExecutionError(
1018 'These configuration variables were missing from the command line: %s' %
1019 ', '.join(sorted(set(isolate.config_variables) - set(variables))))
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001020 config = isolate.by_config.get(config_name)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001021 if not config:
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001022 raise ExecutionError(
1023 'Failed to load configuration for variable \'%s\' for config(s) \'%s\''
1024 '\nAvailable configs: %s' %
1025 (', '.join(isolate.config_variables),
1026 ', '.join(config_name),
1027 ', '.join(str(s) for s in isolate.by_config)))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001028 # Merge tracked and untracked variables, isolate.py doesn't care about the
1029 # trackability of the variables, only the build tool does.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001030 dependencies = [
1031 f.replace('/', os.path.sep) for f in config.tracked + config.untracked
1032 ]
1033 touched = [f.replace('/', os.path.sep) for f in config.touched]
1034 return config.command, dependencies, touched, config.read_only
1035
1036
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001037def chromium_save_isolated(isolated, data, variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001038 """Writes one or many .isolated files.
1039
1040 This slightly increases the cold cache cost but greatly reduce the warm cache
1041 cost by splitting low-churn files off the master .isolated file. It also
1042 reduces overall isolateserver memcache consumption.
1043 """
1044 slaves = []
1045
1046 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001047 new_slave = {
1048 'algo': data['algo'],
1049 'files': {},
1050 'os': data['os'],
1051 'version': data['version'],
1052 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001053 for f in data['files'].keys():
1054 if f.startswith(prefix):
1055 new_slave['files'][f] = data['files'].pop(f)
1056 if new_slave['files']:
1057 slaves.append(new_slave)
1058
1059 # Split test/data/ in its own .isolated file.
1060 extract_into_included_isolated(os.path.join('test', 'data', ''))
1061
1062 # Split everything out of PRODUCT_DIR in its own .isolated file.
1063 if variables.get('PRODUCT_DIR'):
1064 extract_into_included_isolated(variables['PRODUCT_DIR'])
1065
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001066 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001067 for index, f in enumerate(slaves):
1068 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05001069 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001070 data.setdefault('includes', []).append(
1071 isolateserver.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001072 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001073
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -05001074 files.extend(isolateserver.save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001075 return files
1076
1077
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001078class Flattenable(object):
1079 """Represents data that can be represented as a json file."""
1080 MEMBERS = ()
1081
1082 def flatten(self):
1083 """Returns a json-serializable version of itself.
1084
1085 Skips None entries.
1086 """
1087 items = ((member, getattr(self, member)) for member in self.MEMBERS)
1088 return dict((member, value) for member, value in items if value is not None)
1089
1090 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001091 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001092 """Loads a flattened version."""
1093 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001094 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001095 for member in out.MEMBERS:
1096 if member in data:
1097 # Access to a protected member XXX of a client class
1098 # pylint: disable=W0212
1099 out._load_member(member, data.pop(member))
1100 if data:
1101 raise ValueError(
1102 'Found unexpected entry %s while constructing an object %s' %
1103 (data, cls.__name__), data, cls.__name__)
1104 return out
1105
1106 def _load_member(self, member, value):
1107 """Loads a member into self."""
1108 setattr(self, member, value)
1109
1110 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001111 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001112 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001113 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05001114 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001115 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001116 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001117 # On failure, loads the default instance.
1118 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001119 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001120 return out
1121
1122
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001123class SavedState(Flattenable):
1124 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001125
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001126 This file caches the items calculated by this script and is used to increase
1127 the performance of the script. This file is not loaded by run_isolated.py.
1128 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001129
1130 It is important to note that the 'files' dict keys are using native OS path
1131 separator instead of '/' used in .isolate file.
1132 """
1133 MEMBERS = (
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001134 # Algorithm used to generate the hash. The only supported value is at the
1135 # time of writting 'sha-1'.
1136 'algo',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001137 # Cache of the processed command. This value is saved because .isolated
1138 # files are never loaded by isolate.py so it's the only way to load the
1139 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001140 'command',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001141 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001142 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001143 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001144 'isolate_file',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001145 # List of included .isolated files. Used to support/remember 'slave'
1146 # .isolated files. Relative path to isolated_basedir.
1147 'child_isolated_files',
1148 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001149 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001150 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001151 'relative_cwd',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001152 # GYP variables used to generate the .isolated file. Variables are saved so
1153 # a user can use isolate.py after building and the GYP variables are still
1154 # defined.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001155 'variables',
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001156 # Version of the file format in format 'major.minor'. Any non-breaking
1157 # change must update minor. Any breaking change must update major.
1158 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001159 )
1160
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001161 def __init__(self, isolated_basedir):
1162 """Creates an empty SavedState.
1163
1164 |isolated_basedir| is the directory where the .isolated and .isolated.state
1165 files are saved.
1166 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001167 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001168 assert os.path.isabs(isolated_basedir), isolated_basedir
1169 assert os.path.isdir(isolated_basedir), isolated_basedir
1170 self.isolated_basedir = isolated_basedir
1171
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001172 # The default algorithm used.
1173 self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001174 self.command = []
1175 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001176 self.isolate_file = None
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001177 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001178 self.read_only = None
1179 self.relative_cwd = None
benrg@chromium.org609b7982013-02-07 16:44:46 +00001180 self.variables = {'OS': get_flavor()}
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001181 # The current version.
1182 self.version = '1.0'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001183
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001184 def update(self, isolate_file, variables):
1185 """Updates the saved state with new data to keep GYP variables and internal
1186 reference to the original .isolate file.
1187 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +00001188 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001189 # Convert back to a relative path. On Windows, if the isolate and
1190 # isolated files are on different drives, isolate_file will stay an absolute
1191 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001192 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001193
1194 # The same .isolate file should always be used to generate the .isolated and
1195 # .isolated.state.
1196 assert isolate_file == self.isolate_file or not self.isolate_file, (
1197 isolate_file, self.isolate_file)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001198 self.isolate_file = isolate_file
1199 self.variables.update(variables)
1200
1201 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
1202 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001203
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001204 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001205 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001206 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001207 self.command = command
1208 # Add new files.
1209 for f in infiles:
1210 self.files.setdefault(f, {})
1211 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001212 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001213 # Prune extraneous files that are not a dependency anymore.
1214 for f in set(self.files).difference(set(infiles).union(touched)):
1215 del self.files[f]
1216 if read_only is not None:
1217 self.read_only = read_only
1218 self.relative_cwd = relative_cwd
1219
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001220 def to_isolated(self):
1221 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001222
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001223 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001224 """
1225 def strip(data):
1226 """Returns a 'files' entry with only the whitelisted keys."""
1227 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
1228
1229 out = {
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001230 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001231 'files': dict(
1232 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001233 'os': self.variables['OS'],
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001234 'version': self.version,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001235 }
1236 if self.command:
1237 out['command'] = self.command
1238 if self.read_only is not None:
1239 out['read_only'] = self.read_only
1240 if self.relative_cwd:
1241 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001242 return out
1243
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001244 @property
1245 def isolate_filepath(self):
1246 """Returns the absolute path of self.isolate_file."""
1247 return os.path.normpath(
1248 os.path.join(self.isolated_basedir, self.isolate_file))
1249
1250 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001251 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001252 def load(cls, data, isolated_basedir): # pylint: disable=W0221
1253 """Special case loading to disallow different OS.
1254
1255 It is not possible to load a .isolated.state files from a different OS, this
1256 file is saved in OS-specific format.
1257 """
1258 out = super(SavedState, cls).load(data, isolated_basedir)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001259 if 'os' in data:
1260 out.variables['OS'] = data['os']
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001261
1262 # Converts human readable form back into the proper class type.
1263 algo = data.get('algo', 'sha-1')
1264 if not algo in isolateserver.SUPPORTED_ALGOS:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001265 raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001266 out.algo = isolateserver.SUPPORTED_ALGOS[algo]
1267
1268 # For example, 1.1 is guaranteed to be backward compatible with 1.0 code.
1269 if not re.match(r'^(\d+)\.(\d+)$', out.version):
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001270 raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001271 if out.version.split('.', 1)[0] != '1':
maruel@chromium.org999a1fd2013-09-20 17:41:07 +00001272 raise isolateserver.ConfigError(
1273 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001274
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001275 # The .isolate file must be valid. It could be absolute on Windows if the
1276 # drive containing the .isolate and the drive containing the .isolated files
1277 # differ.
1278 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
1279 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001280 return out
1281
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001282 def flatten(self):
1283 """Makes sure 'algo' is in human readable form."""
1284 out = super(SavedState, self).flatten()
1285 out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
1286 return out
1287
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001288 def __str__(self):
1289 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001290 out += ' command: %s\n' % self.command
1291 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001292 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001293 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +00001294 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001295 out += ' child_isolated_files: %s\n' % self.child_isolated_files
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001296 out += ' variables: %s' % ''.join(
1297 '\n %s=%s' % (k, self.variables[k]) for k in sorted(self.variables))
1298 out += ')'
1299 return out
1300
1301
1302class CompleteState(object):
1303 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001304 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001305 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +00001306 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001307 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001308 # Contains the data to ease developer's use-case but that is not strictly
1309 # necessary.
1310 self.saved_state = saved_state
1311
1312 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001313 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001314 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001315 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001316 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001317 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001318 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001319 SavedState.load_file(
1320 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001321
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001322 def load_isolate(self, cwd, isolate_file, variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001323 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001324 .isolate file.
1325
1326 Processes the loaded data, deduce root_dir, relative_cwd.
1327 """
1328 # Make sure to not depend on os.getcwd().
1329 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001330 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001331 logging.info(
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001332 'CompleteState.load_isolate(%s, %s, %s, %s)',
1333 cwd, isolate_file, variables, ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001334 relative_base_dir = os.path.dirname(isolate_file)
1335
1336 # Processes the variables and update the saved state.
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001337 variables = process_variables(cwd, variables, relative_base_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001338 self.saved_state.update(isolate_file, variables)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001339 variables = self.saved_state.variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001340
1341 with open(isolate_file, 'r') as f:
1342 # At that point, variables are not replaced yet in command and infiles.
1343 # infiles may contain directory entries and is in posix style.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001344 command, infiles, touched, read_only = load_isolate_for_config(
1345 os.path.dirname(isolate_file), f.read(), variables)
1346 command = [eval_variables(i, variables) for i in command]
1347 infiles = [eval_variables(f, variables) for f in infiles]
1348 touched = [eval_variables(f, variables) for f in touched]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001349 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +00001350 # form '../../foo/bar'. Note that path variables must be taken in account
1351 # too, add them as if they were input files.
1352 path_variables = [variables[v] for v in PATH_VARIABLES if v in variables]
1353 root_dir = determine_root_dir(
1354 relative_base_dir, infiles + touched + path_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001355 # The relative directory is automatically determined by the relative path
1356 # between root_dir and the directory containing the .isolate file,
1357 # isolate_base_dir.
1358 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
benrg@chromium.org9ae72862013-02-11 05:05:51 +00001359 # Now that we know where the root is, check that the PATH_VARIABLES point
1360 # inside it.
1361 for i in PATH_VARIABLES:
1362 if i in variables:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001363 if not file_path.path_starts_with(
benrg@chromium.org9ae72862013-02-11 05:05:51 +00001364 root_dir, os.path.join(relative_base_dir, variables[i])):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +00001365 raise isolateserver.MappingError(
maruel@chromium.org75584e22013-06-20 01:40:24 +00001366 'Path variable %s=%r points outside the inferred root directory'
1367 ' %s' % (i, variables[i], root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001368 # Normalize the files based to root_dir. It is important to keep the
1369 # trailing os.path.sep at that step.
1370 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001371 file_path.relpath(
1372 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001373 for f in infiles
1374 ]
1375 touched = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001376 file_path.relpath(
1377 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001378 for f in touched
1379 ]
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001380 follow_symlinks = variables['OS'] != 'win'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001381 # Expand the directories by listing each file inside. Up to now, trailing
1382 # os.path.sep must be kept. Do not expand 'touched'.
1383 infiles = expand_directories_and_symlinks(
1384 root_dir,
1385 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +00001386 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +00001387 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +00001388 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001389
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001390 # If we ignore broken items then remove any missing touched items.
1391 if ignore_broken_items:
1392 original_touched_count = len(touched)
1393 touched = [touch for touch in touched if os.path.exists(touch)]
1394
1395 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +00001396 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +00001397 len(touched) - original_touched_count)
1398
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001399 # Finally, update the new data to be able to generate the foo.isolated file,
1400 # the file that is used by run_isolated.py.
1401 self.saved_state.update_isolated(
1402 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001403 logging.debug(self)
1404
maruel@chromium.org9268f042012-10-17 17:36:41 +00001405 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001406 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001407
maruel@chromium.org9268f042012-10-17 17:36:41 +00001408 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
1409 file is tainted.
1410
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -05001411 See isolateserver.process_input() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001412 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001413 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +00001414 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001415 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001416 else:
1417 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -05001418 self.saved_state.files[infile] = isolateserver.process_input(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001419 filepath,
1420 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +00001421 self.saved_state.read_only,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001422 self.saved_state.variables['OS'],
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001423 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001424
1425 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001426 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001427 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001428 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001429 self.isolated_filepath,
1430 self.saved_state.to_isolated(),
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001431 self.saved_state.variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001432 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001433 total_bytes = sum(
1434 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001435 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001436 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001437 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001438 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001439 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -05001440 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001441
1442 @property
1443 def root_dir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001444 """Returns the absolute path of the root_dir to reference the .isolate file
1445 via relative_cwd.
1446
1447 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
1448 to isolate_filepath.
1449 """
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001450 if not self.saved_state.isolate_file:
1451 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001452 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001453 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001454 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001455 root_dir = isolate_dir
1456 else:
maruel@chromium.org87557b92013-10-16 18:04:11 +00001457 if not isolate_dir.endswith(self.saved_state.relative_cwd):
1458 raise ExecutionError(
1459 ('Make sure the .isolate file is in the directory that will be '
1460 'used as the relative directory. It is currently in %s and should '
1461 'be in %s') % (isolate_dir, self.saved_state.relative_cwd))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001462 # Walk back back to the root directory.
1463 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001464 return file_path.get_native_path_case(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001465
1466 @property
1467 def resultdir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001468 """Returns the absolute path containing the .isolated file.
1469
1470 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
1471 path as the value.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001472 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001473 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001474
1475 def __str__(self):
1476 def indent(data, indent_length):
1477 """Indents text."""
1478 spacing = ' ' * indent_length
1479 return ''.join(spacing + l for l in str(data).splitlines(True))
1480
1481 out = '%s(\n' % self.__class__.__name__
1482 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001483 out += ' saved_state: %s)' % indent(self.saved_state, 2)
1484 return out
1485
1486
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001487def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001488 """Loads a CompleteState.
1489
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001490 This includes data from .isolate and .isolated.state files. Never reads the
1491 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001492
1493 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001494 options: Options instance generated with OptionParserIsolate. For either
1495 options.isolate and options.isolated, if the value is set, it is an
1496 absolute path.
1497 cwd: base directory to be used when loading the .isolate file.
1498 subdir: optional argument to only process file in the subdirectory, relative
1499 to CompleteState.root_dir.
1500 skip_update: Skip trying to load the .isolate file and processing the
1501 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001502 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001503 assert not options.isolate or os.path.isabs(options.isolate)
1504 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001505 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001506 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001507 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001508 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001509 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 else:
1511 # Constructs a dummy object that cannot be saved. Useful for temporary
1512 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001513 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001514
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001515 if not options.isolate:
1516 if not complete_state.saved_state.isolate_file:
1517 if not skip_update:
1518 raise ExecutionError('A .isolate file is required.')
1519 isolate = None
1520 else:
1521 isolate = complete_state.saved_state.isolate_filepath
1522 else:
1523 isolate = options.isolate
1524 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001525 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001526 options.isolate, complete_state.saved_state.isolated_basedir)
1527 if rel_isolate != complete_state.saved_state.isolate_file:
1528 raise ExecutionError(
1529 '%s and %s do not match.' % (
1530 options.isolate, complete_state.saved_state.isolate_file))
1531
1532 if not skip_update:
1533 # Then load the .isolate and expands directories.
1534 complete_state.load_isolate(
1535 cwd, isolate, options.variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001536
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001537 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +00001538 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +00001539 subdir = unicode(subdir)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001540 subdir = eval_variables(subdir, complete_state.saved_state.variables)
1541 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001542
1543 if not skip_update:
1544 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001545 return complete_state
1546
1547
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001548def read_trace_as_isolate_dict(complete_state, trace_blacklist):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001549 """Reads a trace and returns the .isolate dictionary.
1550
1551 Returns exceptions during the log parsing so it can be re-raised.
1552 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001553 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001554 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001555 if not os.path.isfile(logfile):
1556 raise ExecutionError(
1557 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
1558 try:
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001559 data = api.parse_log(logfile, trace_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001560 exceptions = [i['exception'] for i in data if 'exception' in i]
1561 results = (i['results'] for i in data if 'results' in i)
1562 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
1563 files = set(sum((result.existent for result in results_stripped), []))
1564 tracked, touched = split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001565 value = generate_isolate(
1566 tracked,
1567 [],
1568 touched,
1569 complete_state.root_dir,
1570 complete_state.saved_state.variables,
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001571 complete_state.saved_state.relative_cwd,
1572 trace_blacklist)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001573 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001574 except trace_inputs.TracingFailure, e:
1575 raise ExecutionError(
1576 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001577 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001578
1579
1580def print_all(comment, data, stream):
1581 """Prints a complete .isolate file and its top-level file comment into a
1582 stream.
1583 """
1584 if comment:
1585 stream.write(comment)
1586 pretty_print(data, stream)
1587
1588
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001589def merge(complete_state, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001590 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001591 value, exceptions = read_trace_as_isolate_dict(
1592 complete_state, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001593
1594 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001595 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001596 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001597 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001598 prev_config = load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +00001599 isolate_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001600 eval_content(prev_content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001601 extract_comment(prev_content))
1602 new_config = load_isolate_as_config(isolate_dir, value, '')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001603 config = union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +00001604 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001605 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001606 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001607 print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001608 if exceptions:
1609 # It got an exception, raise the first one.
1610 raise \
1611 exceptions[0][0], \
1612 exceptions[0][1], \
1613 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001614
1615
maruel@chromium.org29029882013-08-30 12:15:40 +00001616### Commands.
1617
1618
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001619def CMDarchive(parser, args):
1620 """Creates a .isolated file and uploads the tree to an isolate server.
maruel@chromium.org29029882013-08-30 12:15:40 +00001621
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001622 All the files listed in the .isolated file are put in the isolate server
1623 cache via isolateserver.py.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001624 """
maruel@chromium.org9268f042012-10-17 17:36:41 +00001625 parser.add_option('--subdir', help='Filters to a subdirectory')
1626 options, args = parser.parse_args(args)
1627 if args:
1628 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001629
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001630 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001631 success = False
1632 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001633 complete_state = load_complete_state(
1634 options, os.getcwd(), options.subdir, False)
1635 if not options.outdir:
1636 options.outdir = os.path.join(
1637 os.path.dirname(complete_state.isolated_filepath), 'hashtable')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001638 # Make sure that complete_state isn't modified until save_files() is
1639 # called, because any changes made to it here will propagate to the files
1640 # created (which is probably not intended).
1641 complete_state.save_files()
1642
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001643 infiles = complete_state.saved_state.files
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001644 # Add all the .isolated files.
maruel@chromium.org87f11962013-04-10 21:27:28 +00001645 isolated_hash = []
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001646 isolated_files = [
1647 options.isolated,
1648 ] + complete_state.saved_state.child_isolated_files
1649 for item in isolated_files:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001650 item_path = os.path.join(
1651 os.path.dirname(complete_state.isolated_filepath), item)
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001652 # Do not use isolateserver.hash_file() here because the file is
maruel@chromium.org87f11962013-04-10 21:27:28 +00001653 # likely smallish (under 500kb) and its file size is needed.
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001654 with open(item_path, 'rb') as f:
1655 content = f.read()
maruel@chromium.org385d73d2013-09-19 18:33:21 +00001656 isolated_hash.append(
1657 complete_state.saved_state.algo(content).hexdigest())
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001658 isolated_metadata = {
maruel@chromium.org87f11962013-04-10 21:27:28 +00001659 'h': isolated_hash[-1],
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001660 's': len(content),
1661 'priority': '0'
1662 }
1663 infiles[item_path] = isolated_metadata
1664
1665 logging.info('Creating content addressed object store with %d item',
1666 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001667
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001668 if file_path.is_url(options.outdir):
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001669 isolateserver.upload_tree(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001670 base_url=options.outdir,
1671 indir=complete_state.root_dir,
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +00001672 infiles=infiles,
1673 namespace='default-gzip')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001674 else:
1675 recreate_tree(
1676 outdir=options.outdir,
1677 indir=complete_state.root_dir,
1678 infiles=infiles,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00001679 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001680 as_hash=True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001681 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001682 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001683 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001684 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001685 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001686 if not success and os.path.isfile(options.isolated):
1687 os.remove(options.isolated)
maruel@chromium.org87f11962013-04-10 21:27:28 +00001688 return not success
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001689
1690
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001691def CMDcheck(parser, args):
1692 """Checks that all the inputs are present and generates .isolated."""
1693 parser.add_option('--subdir', help='Filters to a subdirectory')
1694 options, args = parser.parse_args(args)
1695 if args:
1696 parser.error('Unsupported argument: %s' % args)
1697
1698 complete_state = load_complete_state(
1699 options, os.getcwd(), options.subdir, False)
1700
1701 # Nothing is done specifically. Just store the result and state.
1702 complete_state.save_files()
1703 return 0
1704
1705
1706CMDhashtable = CMDarchive
1707
1708
maruel@chromium.orge5322512013-08-19 20:17:57 +00001709def CMDmerge(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001710 """Reads and merges the data from the trace back into the original .isolate.
1711
1712 Ignores --outdir.
1713 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001714 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001715 add_trace_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001716 options, args = parser.parse_args(args)
1717 if args:
1718 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001719
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001720 complete_state = load_complete_state(options, os.getcwd(), None, False)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001721 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001722 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001723 return 0
1724
1725
maruel@chromium.orge5322512013-08-19 20:17:57 +00001726def CMDread(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001727 """Reads the trace file generated with command 'trace'.
1728
1729 Ignores --outdir.
1730 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001731 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001732 add_trace_option(parser)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001733 parser.add_option(
1734 '--skip-refresh', action='store_true',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001735 help='Skip reading .isolate file and do not refresh the hash of '
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001736 'dependencies')
maruel@chromium.org29029882013-08-30 12:15:40 +00001737 parser.add_option(
1738 '-m', '--merge', action='store_true',
1739 help='merge the results back in the .isolate file instead of printing')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001740 options, args = parser.parse_args(args)
1741 if args:
1742 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001743
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001744 complete_state = load_complete_state(
1745 options, os.getcwd(), None, options.skip_refresh)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001746 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001747 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
maruel@chromium.org29029882013-08-30 12:15:40 +00001748 if options.merge:
1749 merge(complete_state, blacklist)
1750 else:
1751 pretty_print(value, sys.stdout)
1752
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001753 if exceptions:
1754 # It got an exception, raise the first one.
1755 raise \
1756 exceptions[0][0], \
1757 exceptions[0][1], \
1758 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001759 return 0
1760
1761
maruel@chromium.orge5322512013-08-19 20:17:57 +00001762def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001763 """Creates a directory with all the dependencies mapped into it.
1764
1765 Useful to test manually why a test is failing. The target executable is not
1766 run.
1767 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001768 parser.require_isolated = False
maruel@chromium.org9268f042012-10-17 17:36:41 +00001769 options, args = parser.parse_args(args)
1770 if args:
1771 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001772 complete_state = load_complete_state(options, os.getcwd(), None, False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001773
1774 if not options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001775 options.outdir = run_isolated.make_temp_dir(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001776 'isolate', complete_state.root_dir)
1777 else:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001778 if file_path.is_url(options.outdir):
maruel@chromium.org29029882013-08-30 12:15:40 +00001779 parser.error('Can\'t use url for --outdir with mode remap.')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001780 if not os.path.isdir(options.outdir):
1781 os.makedirs(options.outdir)
maruel@chromium.orgec91af12012-10-18 20:45:57 +00001782 print('Remapping into %s' % options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001783 if len(os.listdir(options.outdir)):
1784 raise ExecutionError('Can\'t remap in a non-empty directory')
1785 recreate_tree(
1786 outdir=options.outdir,
1787 indir=complete_state.root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001788 infiles=complete_state.saved_state.files,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00001789 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001790 as_hash=False)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001791 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001792 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001793
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001794 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001795 complete_state.save_files()
1796 return 0
1797
1798
maruel@chromium.orge5322512013-08-19 20:17:57 +00001799def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001800 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001801 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001802 options, args = parser.parse_args(args)
1803 if args:
1804 parser.error('Unsupported argument: %s' % args)
1805
1806 if options.isolated:
1807 # Load the previous state if it was present. Namely, "foo.isolated.state".
1808 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001809 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001810 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001811 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001812 if not isolate:
maruel@chromium.org29029882013-08-30 12:15:40 +00001813 parser.error('--isolate is required.')
1814
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001815 with open(isolate, 'r') as f:
1816 content = f.read()
1817 config = load_isolate_as_config(
1818 os.path.dirname(os.path.abspath(isolate)),
1819 eval_content(content),
benrg@chromium.org609b7982013-02-07 16:44:46 +00001820 extract_comment(content))
1821 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001822 print('Updating %s' % isolate)
1823 with open(isolate, 'wb') as f:
1824 print_all(config.file_comment, data, f)
1825 return 0
1826
1827
maruel@chromium.org29029882013-08-30 12:15:40 +00001828@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001829def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001830 """Runs the test executable in an isolated (temporary) directory.
1831
1832 All the dependencies are mapped into the temporary directory and the
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001833 directory is cleaned up after the target exits. Warning: if --outdir is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001834 specified, it is deleted upon exit.
1835
maruel@chromium.org29029882013-08-30 12:15:40 +00001836 Argument processing stops at -- and these arguments are appended to the
1837 command line of the target to run. For example, use:
1838 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001839 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001840 parser.require_isolated = False
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001841 parser.add_option(
1842 '--skip-refresh', action='store_true',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001843 help='Skip reading .isolate file and do not refresh the hash of '
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001844 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001845 options, args = parser.parse_args(args)
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05001846 if options.outdir and file_path.is_url(options.outdir):
maruel@chromium.org29029882013-08-30 12:15:40 +00001847 parser.error('Can\'t use url for --outdir with mode run.')
1848
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001849 complete_state = load_complete_state(
1850 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001851 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001852 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001853 raise ExecutionError('No command to run.')
maruel@chromium.orgb9520b02013-03-13 18:00:03 +00001854
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001855 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001856 try:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001857 root_dir = complete_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001858 if not options.outdir:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001859 if not os.path.isabs(root_dir):
1860 root_dir = os.path.join(os.path.dirname(options.isolated), root_dir)
1861 options.outdir = run_isolated.make_temp_dir('isolate', root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001862 else:
1863 if not os.path.isdir(options.outdir):
1864 os.makedirs(options.outdir)
1865 recreate_tree(
1866 outdir=options.outdir,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001867 indir=root_dir,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001868 infiles=complete_state.saved_state.files,
maruel@chromium.orgba6489b2013-07-11 20:23:33 +00001869 action=run_isolated.HARDLINK_WITH_FALLBACK,
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001870 as_hash=False)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001871 cwd = os.path.normpath(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001872 os.path.join(options.outdir, complete_state.saved_state.relative_cwd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001873 if not os.path.isdir(cwd):
1874 # It can happen when no files are mapped from the directory containing the
1875 # .isolate file. But the directory must exist to be the current working
1876 # directory.
1877 os.makedirs(cwd)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001878 if complete_state.saved_state.read_only:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001879 run_isolated.make_writable(options.outdir, True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001880 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1881 result = subprocess.call(cmd, cwd=cwd)
1882 finally:
1883 if options.outdir:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +00001884 run_isolated.rmtree(options.outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001885
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001886 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001887 complete_state.save_files()
1888 return result
1889
1890
maruel@chromium.org29029882013-08-30 12:15:40 +00001891@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001892def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001893 """Traces the target using trace_inputs.py.
1894
1895 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001896 its child processes access. Then the 'merge' command can be used to generate
1897 an updated .isolate file out of it or the 'read' command to print it out to
1898 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001899
maruel@chromium.org29029882013-08-30 12:15:40 +00001900 Argument processing stops at -- and these arguments are appended to the
1901 command line of the target to run. For example, use:
1902 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001903 """
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001904 add_trace_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001905 parser.add_option(
1906 '-m', '--merge', action='store_true',
1907 help='After tracing, merge the results back in the .isolate file')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001908 parser.add_option(
1909 '--skip-refresh', action='store_true',
maruel@chromium.org7b844a62013-09-17 13:04:59 +00001910 help='Skip reading .isolate file and do not refresh the hash of '
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001911 'dependencies')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001912 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001913
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001914 complete_state = load_complete_state(
1915 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001916 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001917 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001918 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001919 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001920 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001921 unicode(complete_state.root_dir),
1922 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001923 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1924 if not os.path.isfile(cmd[0]):
1925 raise ExecutionError(
1926 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001927 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1928 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001929 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001930 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001931 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001932 try:
1933 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001934 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001935 cmd,
1936 cwd,
1937 'default',
1938 True)
1939 except trace_inputs.TracingFailure, e:
1940 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1941
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001942 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001943 logging.error(
1944 'Tracer exited with %d, which means the tests probably failed so the '
1945 'trace is probably incomplete.', result)
1946 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001947
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001948 complete_state.save_files()
1949
1950 if options.merge:
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001951 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001952 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001953
1954 return result
1955
1956
maruel@chromium.org712454d2013-04-04 17:52:34 +00001957def _process_variable_arg(_option, _opt, _value, parser):
1958 if not parser.rargs:
1959 raise optparse.OptionValueError(
1960 'Please use --variable FOO=BAR or --variable FOO BAR')
1961 k = parser.rargs.pop(0)
1962 if '=' in k:
1963 parser.values.variables.append(tuple(k.split('=', 1)))
1964 else:
1965 if not parser.rargs:
1966 raise optparse.OptionValueError(
1967 'Please use --variable FOO=BAR or --variable FOO BAR')
1968 v = parser.rargs.pop(0)
1969 parser.values.variables.append((k, v))
1970
1971
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001972def add_variable_option(parser):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001973 """Adds --isolated and --variable to an OptionParser."""
1974 parser.add_option(
1975 '-s', '--isolated',
1976 metavar='FILE',
1977 help='.isolated file to generate or read')
1978 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001979 parser.add_option(
1980 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001981 dest='isolated',
1982 help=optparse.SUPPRESS_HELP)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001983 default_variables = [('OS', get_flavor())]
1984 if sys.platform in ('win32', 'cygwin'):
1985 default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
1986 else:
1987 default_variables.append(('EXECUTABLE_SUFFIX', ''))
1988 parser.add_option(
1989 '-V', '--variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00001990 action='callback',
1991 callback=_process_variable_arg,
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001992 default=default_variables,
1993 dest='variables',
1994 metavar='FOO BAR',
1995 help='Variables to process in the .isolate file, default: %default. '
1996 'Variables are persistent accross calls, they are saved inside '
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001997 '<.isolated>.state')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001998
1999
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002000def add_trace_option(parser):
2001 """Adds --trace-blacklist to the parser."""
2002 parser.add_option(
2003 '--trace-blacklist',
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05002004 action='append', default=list(isolateserver.DEFAULT_BLACKLIST),
maruel@chromium.org3683afe2013-07-27 00:09:27 +00002005 help='List of regexp to use as blacklist filter for files to consider '
2006 'important, not to be confused with --blacklist which blacklists '
2007 'test case.')
2008
2009
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002010def parse_isolated_option(parser, options, cwd, require_isolated):
2011 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002012 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002013 options.isolated = os.path.normpath(
2014 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002015 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00002016 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002017 if options.isolated and not options.isolated.endswith('.isolated'):
2018 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002019
2020
2021def parse_variable_option(options):
2022 """Processes --variable."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002023 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
2024 # but it wouldn't be backward compatible.
2025 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002026 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00002027 try:
2028 return int(s)
2029 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00002030 return s.decode('utf-8')
benrg@chromium.org609b7982013-02-07 16:44:46 +00002031 options.variables = dict((k, try_make_int(v)) for k, v in options.variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002032
2033
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002034class OptionParserIsolate(tools.OptionParserWithLogging):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00002035 """Adds automatic --isolate, --isolated, --out and --variable handling."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00002036 # Set it to False if it is not required, e.g. it can be passed on but do not
2037 # fail if not given.
2038 require_isolated = True
2039
2040 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002041 tools.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00002042 self,
2043 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
2044 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002045 group = optparse.OptionGroup(self, "Common options")
2046 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002047 '-i', '--isolate',
2048 metavar='FILE',
2049 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00002050 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002051 group.add_option(
2052 '-o', '--outdir', metavar='DIR',
2053 help='Directory used to recreate the tree or store the hash table. '
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002054 'Defaults: run|remap: a /tmp subdirectory, others: '
2055 'defaults to the directory containing --isolated')
csharp@chromium.org01856802012-11-12 17:48:13 +00002056 group.add_option(
2057 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00002058 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
2059 help='Indicates that invalid entries in the isolated file to be '
2060 'only be logged and not stop processing. Defaults to True if '
2061 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002062 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002063
2064 def parse_args(self, *args, **kwargs):
2065 """Makes sure the paths make sense.
2066
2067 On Windows, / and \ are often mixed together in a path.
2068 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002069 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002070 self, *args, **kwargs)
2071 if not self.allow_interspersed_args and args:
2072 self.error('Unsupported argument: %s' % args)
2073
maruel@chromium.org561d4b22013-09-26 21:08:08 +00002074 cwd = file_path.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00002075 parse_isolated_option(self, options, cwd, self.require_isolated)
2076 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002077
2078 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002079 # TODO(maruel): Work with non-ASCII.
2080 # The path must be in native path case for tracing purposes.
2081 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
2082 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
maruel@chromium.org561d4b22013-09-26 21:08:08 +00002083 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002084
Marc-Antoine Ruel37989932013-11-19 16:28:08 -05002085 if options.outdir and not file_path.is_url(options.outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00002086 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
2087 # outdir doesn't need native path case since tracing is never done from
2088 # there.
2089 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002090
2091 return options, args
2092
2093
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002094def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00002095 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002096 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00002097 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00002098 except Exception as e:
2099 tools.report_error(e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002100 return 1
2101
2102
2103if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00002104 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00002105 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00002106 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00002107 sys.exit(main(sys.argv[1:]))