blob: 5ae4d018ba14c1a757e45c4ebc22c538abece0f0 [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
Marc-Antoine Ruel9dfdcc22014-01-08 14:14:18 -050016import datetime
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000017import logging
18import optparse
19import os
20import posixpath
21import re
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000022import subprocess
23import sys
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000024
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080025import auth
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050026import isolate_format
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
maruel@chromium.orge5322512013-08-19 20:17:57 +000031from third_party import colorama
32from third_party.depot_tools import fix_encoding
33from third_party.depot_tools import subcommand
34
maruel@chromium.org561d4b22013-09-26 21:08:08 +000035from utils import file_path
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000036from utils import tools
37
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000038
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050039__version__ = '0.3.1'
maruel@chromium.org3d671992013-08-20 00:38:27 +000040
41
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000042class ExecutionError(Exception):
43 """A generic error occurred."""
44 def __str__(self):
45 return self.args[0]
46
47
48### Path handling code.
49
50
csharp@chromium.org01856802012-11-12 17:48:13 +000051def expand_directories_and_symlinks(indir, infiles, blacklist,
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +000052 follow_symlinks, ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000053 """Expands the directories and the symlinks, applies the blacklist and
54 verifies files exist.
55
56 Files are specified in os native path separator.
57 """
58 outfiles = []
59 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +000060 try:
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -050061 outfiles.extend(
62 isolateserver.expand_directory_and_symlink(
63 indir, relfile, blacklist, follow_symlinks))
maruel@chromium.org9958e4a2013-09-17 00:01:48 +000064 except isolateserver.MappingError as e:
csharp@chromium.org01856802012-11-12 17:48:13 +000065 if ignore_broken_items:
66 logging.info('warning: %s', e)
67 else:
68 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000069 return outfiles
70
71
maruel@chromium.org7b844a62013-09-17 13:04:59 +000072def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000073 """Creates a new tree with only the input files in it.
74
75 Arguments:
76 outdir: Output directory to create the files in.
77 indir: Root directory the infiles are based in.
78 infiles: dict of files to map from |indir| to |outdir|.
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000079 action: One of accepted action of run_isolated.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +000080 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000081 """
82 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +000083 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
84 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000085
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000086 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000087 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000088 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000089 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000090
91 for relfile, metadata in infiles.iteritems():
92 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +000093 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000094 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000095 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000096 # Skip links when storing a hashtable.
97 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +000098 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000099 if os.path.isfile(outfile):
100 # Just do a quick check that the file size matches. No need to stat()
101 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000102 if not 's' in metadata:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000103 raise isolateserver.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000104 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000105 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000106 continue
107 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000108 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000109 os.remove(outfile)
110 else:
111 outfile = os.path.join(outdir, relfile)
112 outsubdir = os.path.dirname(outfile)
113 if not os.path.isdir(outsubdir):
114 os.makedirs(outsubdir)
115
116 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000117 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000118 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000119 if 'l' in metadata:
120 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000121 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000122 # symlink doesn't exist on Windows.
123 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000124 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000125 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000126
127
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000128### Variable stuff.
129
130
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500131def _normalize_path_variable(cwd, relative_base_dir, key, value):
132 """Normalizes a path variable into a relative directory.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500133 """
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500134 # Variables could contain / or \ on windows. Always normalize to
135 # os.path.sep.
136 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
137 normalized = file_path.get_native_path_case(os.path.normpath(x))
138 if not os.path.isdir(normalized):
139 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
140
141 # All variables are relative to the .isolate file.
142 normalized = os.path.relpath(normalized, relative_base_dir)
143 logging.debug(
144 'Translated variable %s from %s to %s', key, value, normalized)
145 return normalized
146
147
148def normalize_path_variables(cwd, path_variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000149 """Processes path variables as a special case and returns a copy of the dict.
150
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000151 For each 'path' variable: first normalizes it based on |cwd|, verifies it
152 exists then sets it as relative to relative_base_dir.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500153 """
154 logging.info(
155 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
156 relative_base_dir)
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -0500157 assert isinstance(cwd, unicode), cwd
158 assert isinstance(relative_base_dir, unicode), relative_base_dir
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500159 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
160 return dict(
161 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
162 for k, v in path_variables.iteritems())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000163
164
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500165### Internal state files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000166
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500167
168def isolatedfile_to_state(filename):
169 """For a '.isolate' file, returns the path to the saved '.state' file."""
170 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000171
172
173def classify_files(root_dir, tracked, untracked):
174 """Converts the list of files into a .isolate 'variables' dictionary.
175
176 Arguments:
177 - tracked: list of files names to generate a dictionary out of that should
178 probably be tracked.
179 - untracked: list of files names that must not be tracked.
180 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000181 new_tracked = []
182 new_untracked = list(untracked)
183
184 def should_be_tracked(filepath):
185 """Returns True if it is a file without whitespace in a non-optional
186 directory that has no symlink in its path.
187 """
188 if filepath.endswith('/'):
189 return False
190 if ' ' in filepath:
191 return False
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000192 # Look if any element in the path is a symlink.
193 split = filepath.split('/')
194 for i in range(len(split)):
195 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
196 return False
197 return True
198
199 for filepath in sorted(tracked):
200 if should_be_tracked(filepath):
201 new_tracked.append(filepath)
202 else:
203 # Anything else.
204 new_untracked.append(filepath)
205
206 variables = {}
207 if new_tracked:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500208 variables[isolate_format.KEY_TRACKED] = sorted(new_tracked)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000209 if new_untracked:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500210 variables[isolate_format.KEY_UNTRACKED] = sorted(new_untracked)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000211 return variables
212
213
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500214def chromium_fix(f, variables):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500215 """Fixes an isolate dependency with Chromium-specific fixes."""
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000216 # Blacklist logs and other unimportant files.
Marc-Antoine Ruel4196cfc2014-02-21 15:43:18 -0500217 # - 'First Run' is not created by the compile but by the test itself.
218 # - Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
219 # separator at this point.
220 if (re.match(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$', f) or
221 f == '<(PRODUCT_DIR)/First Run'):
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000222 logging.debug('Ignoring %s', f)
223 return None
224
maruel@chromium.org7650e422012-11-16 21:56:42 +0000225 EXECUTABLE = re.compile(
226 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500227 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
maruel@chromium.org7650e422012-11-16 21:56:42 +0000228 r'$')
229 match = EXECUTABLE.match(f)
230 if match:
231 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
232
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000233 if sys.platform == 'darwin':
234 # On OSX, the name of the output is dependent on gyp define, it can be
235 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
236 # Framework.framework'. Furthermore, they are versioned with a gyp
237 # variable. To lower the complexity of the .isolate file, remove all the
238 # individual entries that show up under any of the 4 entries and replace
239 # them with the directory itself. Overall, this results in a bit more
240 # files than strictly necessary.
241 OSX_BUNDLES = (
242 '<(PRODUCT_DIR)/Chromium Framework.framework/',
243 '<(PRODUCT_DIR)/Chromium.app/',
244 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
245 '<(PRODUCT_DIR)/Google Chrome.app/',
246 )
247 for prefix in OSX_BUNDLES:
248 if f.startswith(prefix):
249 # Note this result in duplicate values, so the a set() must be used to
250 # remove duplicates.
251 return prefix
252 return f
253
254
255def generate_simplified(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500256 tracked, untracked, touched, root_dir, path_variables, extra_variables,
257 relative_cwd, trace_blacklist):
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000258 """Generates a clean and complete .isolate 'variables' dictionary.
259
260 Cleans up and extracts only files from within root_dir then processes
261 variables and relative_cwd.
262 """
263 root_dir = os.path.realpath(root_dir)
264 logging.info(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500265 'generate_simplified(%d files, %s, %s, %s, %s)' %
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000266 (len(tracked) + len(untracked) + len(touched),
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500267 root_dir, path_variables, extra_variables, relative_cwd))
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000268
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000269 # Preparation work.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500270 relative_cwd = file_path.cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000271 assert not os.path.isabs(relative_cwd), relative_cwd
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500272
273 # Normalizes to posix path. .isolate files are using posix paths on all OSes
274 # for coherency.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000275 path_variables = dict(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500276 (k, v.replace(os.path.sep, '/')) for k, v in path_variables.iteritems())
277 # Contains normalized path_variables plus extra_variables.
278 total_variables = path_variables.copy()
279 total_variables.update(extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000280
281 # Actual work: Process the files.
282 # TODO(maruel): if all the files in a directory are in part tracked and in
283 # part untracked, the directory will not be extracted. Tracked files should be
284 # 'promoted' to be untracked as needed.
285 tracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000286 root_dir, tracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000287 untracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000288 root_dir, untracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000289 # touched is not compressed, otherwise it would result in files to be archived
290 # that we don't need.
291
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000292 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000293 def fix(f):
294 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000295 # Important, GYP stores the files with / and not \.
296 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000297 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000298 # If it's not already a variable.
299 if not f.startswith('<'):
300 # relative_cwd is usually the directory containing the gyp file. It may be
301 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000302 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000303 # Convert the whole thing to / since it's isolate's speak.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500304 f = file_path.posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000305 posixpath.join(root_dir_posix, f),
306 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000307
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500308 # Use the longest value first.
309 for key, value in sorted(
310 path_variables.iteritems(), key=lambda x: -len(x[1])):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500311 if f.startswith(value):
312 f = '<(%s)%s' % (key, f[len(value):])
313 logging.debug('Converted to %s' % f)
314 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000315 return f
316
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000317 def fix_all(items):
318 """Reduces the items to convert variables, removes unneeded items, apply
319 chromium-specific fixes and only return unique items.
320 """
321 variables_converted = (fix(f.path) for f in items)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500322 chromium_fixed = (
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500323 chromium_fix(f, total_variables) for f in variables_converted)
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000324 return set(f for f in chromium_fixed if f)
325
326 tracked = fix_all(tracked)
327 untracked = fix_all(untracked)
328 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000329 out = classify_files(root_dir, tracked, untracked)
330 if touched:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500331 out[isolate_format.KEY_TOUCHED] = sorted(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000332 return out
333
334
335def generate_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500336 tracked, untracked, touched, root_dir, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500337 extra_variables, relative_cwd, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000338 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000339 dependencies = generate_simplified(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500340 tracked, untracked, touched, root_dir, path_variables, extra_variables,
341 relative_cwd, trace_blacklist)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000342 config_variable_names, config_values = zip(
343 *sorted(config_variables.iteritems()))
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500344 out = isolate_format.Configs(None, config_variable_names)
Marc-Antoine Ruelb53d0c12014-03-28 13:46:27 -0400345 out.set_config(
346 config_values,
347 isolate_format.ConfigSettings(
348 dependencies, os.path.abspath(relative_cwd)))
benrg@chromium.org609b7982013-02-07 16:44:46 +0000349 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000350
351
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500352def chromium_save_isolated(isolated, data, path_variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000353 """Writes one or many .isolated files.
354
355 This slightly increases the cold cache cost but greatly reduce the warm cache
356 cost by splitting low-churn files off the master .isolated file. It also
357 reduces overall isolateserver memcache consumption.
358 """
359 slaves = []
360
361 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000362 new_slave = {
363 'algo': data['algo'],
364 'files': {},
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000365 'version': data['version'],
366 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000367 for f in data['files'].keys():
368 if f.startswith(prefix):
369 new_slave['files'][f] = data['files'].pop(f)
370 if new_slave['files']:
371 slaves.append(new_slave)
372
373 # Split test/data/ in its own .isolated file.
374 extract_into_included_isolated(os.path.join('test', 'data', ''))
375
376 # Split everything out of PRODUCT_DIR in its own .isolated file.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500377 if path_variables.get('PRODUCT_DIR'):
378 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000379
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000380 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000381 for index, f in enumerate(slaves):
382 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500383 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000384 data.setdefault('includes', []).append(
385 isolateserver.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000386 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000387
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500388 files.extend(isolateserver.save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000389 return files
390
391
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000392class Flattenable(object):
393 """Represents data that can be represented as a json file."""
394 MEMBERS = ()
395
396 def flatten(self):
397 """Returns a json-serializable version of itself.
398
399 Skips None entries.
400 """
401 items = ((member, getattr(self, member)) for member in self.MEMBERS)
402 return dict((member, value) for member, value in items if value is not None)
403
404 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000405 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000406 """Loads a flattened version."""
407 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000408 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000409 for member in out.MEMBERS:
410 if member in data:
411 # Access to a protected member XXX of a client class
412 # pylint: disable=W0212
413 out._load_member(member, data.pop(member))
414 if data:
415 raise ValueError(
416 'Found unexpected entry %s while constructing an object %s' %
417 (data, cls.__name__), data, cls.__name__)
418 return out
419
420 def _load_member(self, member, value):
421 """Loads a member into self."""
422 setattr(self, member, value)
423
424 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000425 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000426 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000427 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500428 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000429 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000430 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000431 # On failure, loads the default instance.
432 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000433 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000434 return out
435
436
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000437class SavedState(Flattenable):
438 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000439
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000440 This file caches the items calculated by this script and is used to increase
441 the performance of the script. This file is not loaded by run_isolated.py.
442 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000443
444 It is important to note that the 'files' dict keys are using native OS path
445 separator instead of '/' used in .isolate file.
446 """
447 MEMBERS = (
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400448 # Value of sys.platform so that the file is rejected if loaded from a
449 # different OS. While this should never happen in practice, users are ...
450 # "creative".
451 'OS',
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000452 # Algorithm used to generate the hash. The only supported value is at the
453 # time of writting 'sha-1'.
454 'algo',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400455 # List of included .isolated files. Used to support/remember 'slave'
456 # .isolated files. Relative path to isolated_basedir.
457 'child_isolated_files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000458 # Cache of the processed command. This value is saved because .isolated
459 # files are never loaded by isolate.py so it's the only way to load the
460 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000461 'command',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500462 # GYP variables that are used to generate conditions. The most frequent
463 # example is 'OS'.
464 'config_variables',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500465 # GYP variables that will be replaced in 'command' and paths but will not be
466 # considered a relative directory.
467 'extra_variables',
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000468 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000469 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000470 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000471 'isolate_file',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400472 # GYP variables used to generate the .isolated files paths based on path
473 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
474 'path_variables',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000475 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000476 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000477 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000478 'relative_cwd',
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400479 # Root directory the files are mapped from.
480 'root_dir',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400481 # Version of the saved state file format. Any breaking change must update
482 # the value.
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000483 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000484 )
485
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400486 # Bump this version whenever the saved state changes. It is also keyed on the
487 # .isolated file version so any change in the generator will invalidate .state
488 # files.
489 EXPECTED_VERSION = isolateserver.ISOLATED_FILE_VERSION + '.2'
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400490
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000491 def __init__(self, isolated_basedir):
492 """Creates an empty SavedState.
493
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400494 Arguments:
495 isolated_basedir: the directory where the .isolated and .isolated.state
496 files are saved.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000497 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000498 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000499 assert os.path.isabs(isolated_basedir), isolated_basedir
500 assert os.path.isdir(isolated_basedir), isolated_basedir
501 self.isolated_basedir = isolated_basedir
502
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000503 # The default algorithm used.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400504 self.OS = sys.platform
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000505 self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500506 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000507 self.command = []
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500508 self.config_variables = {}
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500509 self.extra_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000510 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000511 self.isolate_file = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500512 self.path_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000513 self.read_only = None
514 self.relative_cwd = None
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400515 self.root_dir = None
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400516 self.version = self.EXPECTED_VERSION
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000517
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400518 def update_config(self, config_variables):
519 """Updates the saved state with only config variables."""
520 self.config_variables.update(config_variables)
521
522 def update(self, isolate_file, path_variables, extra_variables):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000523 """Updates the saved state with new data to keep GYP variables and internal
524 reference to the original .isolate file.
525 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +0000526 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000527 # Convert back to a relative path. On Windows, if the isolate and
528 # isolated files are on different drives, isolate_file will stay an absolute
529 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500530 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000531
532 # The same .isolate file should always be used to generate the .isolated and
533 # .isolated.state.
534 assert isolate_file == self.isolate_file or not self.isolate_file, (
535 isolate_file, self.isolate_file)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500536 self.extra_variables.update(extra_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000537 self.isolate_file = isolate_file
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500538 self.path_variables.update(path_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000539
540 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
541 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000542
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000543 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000544 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000545 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000546 self.command = command
547 # Add new files.
548 for f in infiles:
549 self.files.setdefault(f, {})
550 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000551 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000552 # Prune extraneous files that are not a dependency anymore.
553 for f in set(self.files).difference(set(infiles).union(touched)):
554 del self.files[f]
555 if read_only is not None:
556 self.read_only = read_only
557 self.relative_cwd = relative_cwd
558
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000559 def to_isolated(self):
560 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000561
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000562 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000563 """
564 def strip(data):
565 """Returns a 'files' entry with only the whitelisted keys."""
566 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
567
568 out = {
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000569 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000570 'files': dict(
571 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400572 # The version of the .state file is different than the one of the
573 # .isolated file.
574 'version': isolateserver.ISOLATED_FILE_VERSION,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000575 }
576 if self.command:
577 out['command'] = self.command
578 if self.read_only is not None:
579 out['read_only'] = self.read_only
580 if self.relative_cwd:
581 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000582 return out
583
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000584 @property
585 def isolate_filepath(self):
586 """Returns the absolute path of self.isolate_file."""
587 return os.path.normpath(
588 os.path.join(self.isolated_basedir, self.isolate_file))
589
590 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000591 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000592 def load(cls, data, isolated_basedir): # pylint: disable=W0221
593 """Special case loading to disallow different OS.
594
595 It is not possible to load a .isolated.state files from a different OS, this
596 file is saved in OS-specific format.
597 """
598 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400599 if data.get('OS') != sys.platform:
600 raise isolateserver.ConfigError('Unexpected OS %s', data.get('OS'))
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000601
602 # Converts human readable form back into the proper class type.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400603 algo = data.get('algo')
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000604 if not algo in isolateserver.SUPPORTED_ALGOS:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000605 raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000606 out.algo = isolateserver.SUPPORTED_ALGOS[algo]
607
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500608 # Refuse the load non-exact version, even minor difference. This is unlike
609 # isolateserver.load_isolated(). This is because .isolated.state could have
610 # changed significantly even in minor version difference.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400611 if out.version != cls.EXPECTED_VERSION:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000612 raise isolateserver.ConfigError(
613 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000614
Marc-Antoine Ruel16ebc2e2014-02-13 15:39:15 -0500615 # The .isolate file must be valid. If it is not present anymore, zap the
616 # value as if it was not noted, so .isolate_file can safely be overriden
617 # later.
618 if out.isolate_file and not os.path.isfile(out.isolate_filepath):
619 out.isolate_file = None
620 if out.isolate_file:
621 # It could be absolute on Windows if the drive containing the .isolate and
622 # the drive containing the .isolated files differ, .e.g .isolate is on
623 # C:\\ and .isolated is on D:\\ .
624 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
625 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000626 return out
627
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000628 def flatten(self):
629 """Makes sure 'algo' is in human readable form."""
630 out = super(SavedState, self).flatten()
631 out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
632 return out
633
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000634 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500635 def dict_to_str(d):
636 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
637
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000638 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000639 out += ' command: %s\n' % self.command
640 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000641 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000642 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000643 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000644 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500645 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
646 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500647 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000648 return out
649
650
651class CompleteState(object):
652 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000653 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000654 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +0000655 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000656 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000657 # Contains the data to ease developer's use-case but that is not strictly
658 # necessary.
659 self.saved_state = saved_state
660
661 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000662 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000663 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000664 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000665 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000666 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000667 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000668 SavedState.load_file(
669 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000670
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500671 def load_isolate(
672 self, cwd, isolate_file, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500673 extra_variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000674 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000675 .isolate file.
676
677 Processes the loaded data, deduce root_dir, relative_cwd.
678 """
679 # Make sure to not depend on os.getcwd().
680 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000681 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000682 logging.info(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500683 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500684 cwd, isolate_file, path_variables, config_variables, extra_variables,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500685 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000686
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400687 # Config variables are not affected by the paths and must be used to
688 # retrieve the paths, so update them first.
689 self.saved_state.update_config(config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000690
691 with open(isolate_file, 'r') as f:
692 # At that point, variables are not replaced yet in command and infiles.
693 # infiles may contain directory entries and is in posix style.
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400694 command, infiles, touched, read_only, isolate_cmd_dir = (
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500695 isolate_format.load_isolate_for_config(
696 os.path.dirname(isolate_file), f.read(),
697 self.saved_state.config_variables))
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500698
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400699 # Processes the variables with the new found relative root. Note that 'cwd'
700 # is used when path variables are used.
701 path_variables = normalize_path_variables(
702 cwd, path_variables, isolate_cmd_dir)
703 # Update the rest of the saved state.
704 self.saved_state.update(isolate_file, path_variables, extra_variables)
705
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500706 total_variables = self.saved_state.path_variables.copy()
707 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500708 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500709 command = [
710 isolate_format.eval_variables(i, total_variables) for i in command
711 ]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500712
713 total_variables = self.saved_state.path_variables.copy()
714 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500715 infiles = [
716 isolate_format.eval_variables(f, total_variables) for f in infiles
717 ]
718 touched = [
719 isolate_format.eval_variables(f, total_variables) for f in touched
720 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000721 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +0000722 # form '../../foo/bar'. Note that path variables must be taken in account
723 # too, add them as if they were input files.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400724 self.saved_state.root_dir = isolate_format.determine_root_dir(
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400725 isolate_cmd_dir, infiles + touched +
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500726 self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000727 # The relative directory is automatically determined by the relative path
728 # between root_dir and the directory containing the .isolate file,
729 # isolate_base_dir.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400730 relative_cwd = os.path.relpath(isolate_cmd_dir, self.saved_state.root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500731 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000732 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500733 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400734 dest = os.path.join(isolate_cmd_dir, relative_cwd, v)
735 if not file_path.path_starts_with(self.saved_state.root_dir, dest):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500736 raise isolateserver.MappingError(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400737 'Path variable %s=%r points outside the inferred root directory '
738 '%s; %s'
739 % (k, v, self.saved_state.root_dir, dest))
740 # Normalize the files based to self.saved_state.root_dir. It is important to
741 # keep the trailing os.path.sep at that step.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000742 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500743 file_path.relpath(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400744 file_path.normpath(os.path.join(isolate_cmd_dir, f)),
745 self.saved_state.root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000746 for f in infiles
747 ]
748 touched = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500749 file_path.relpath(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400750 file_path.normpath(os.path.join(isolate_cmd_dir, f)),
751 self.saved_state.root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000752 for f in touched
753 ]
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400754 follow_symlinks = sys.platform != 'win32'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000755 # Expand the directories by listing each file inside. Up to now, trailing
756 # os.path.sep must be kept. Do not expand 'touched'.
757 infiles = expand_directories_and_symlinks(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400758 self.saved_state.root_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000759 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +0000760 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000761 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +0000762 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000763
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000764 # If we ignore broken items then remove any missing touched items.
765 if ignore_broken_items:
766 original_touched_count = len(touched)
767 touched = [touch for touch in touched if os.path.exists(touch)]
768
769 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +0000770 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000771 len(touched) - original_touched_count)
772
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000773 # Finally, update the new data to be able to generate the foo.isolated file,
774 # the file that is used by run_isolated.py.
775 self.saved_state.update_isolated(
776 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000777 logging.debug(self)
778
maruel@chromium.org9268f042012-10-17 17:36:41 +0000779 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000780 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000781
maruel@chromium.org9268f042012-10-17 17:36:41 +0000782 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
783 file is tainted.
784
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500785 See isolateserver.process_input() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000786 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000787 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +0000788 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000789 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000790 else:
791 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500792 self.saved_state.files[infile] = isolateserver.process_input(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000793 filepath,
794 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000795 self.saved_state.read_only,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000796 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000797
798 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000799 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000800 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000801 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000802 self.isolated_filepath,
803 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500804 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000805 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000806 total_bytes = sum(
807 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000808 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000809 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000810 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000811 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000812 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500813 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000814
815 @property
816 def root_dir(self):
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400817 return self.saved_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000818
819 def __str__(self):
820 def indent(data, indent_length):
821 """Indents text."""
822 spacing = ' ' * indent_length
823 return ''.join(spacing + l for l in str(data).splitlines(True))
824
825 out = '%s(\n' % self.__class__.__name__
826 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000827 out += ' saved_state: %s)' % indent(self.saved_state, 2)
828 return out
829
830
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000831def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000832 """Loads a CompleteState.
833
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000834 This includes data from .isolate and .isolated.state files. Never reads the
835 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000836
837 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000838 options: Options instance generated with OptionParserIsolate. For either
839 options.isolate and options.isolated, if the value is set, it is an
840 absolute path.
841 cwd: base directory to be used when loading the .isolate file.
842 subdir: optional argument to only process file in the subdirectory, relative
843 to CompleteState.root_dir.
844 skip_update: Skip trying to load the .isolate file and processing the
845 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000846 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000847 assert not options.isolate or os.path.isabs(options.isolate)
848 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000849 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000850 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000851 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000852 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000853 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000854 else:
855 # Constructs a dummy object that cannot be saved. Useful for temporary
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400856 # commands like 'run'. There is no directory containing a .isolated file so
857 # specify the current working directory as a valid directory.
858 complete_state = CompleteState(None, SavedState(os.getcwd()))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000859
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000860 if not options.isolate:
861 if not complete_state.saved_state.isolate_file:
862 if not skip_update:
863 raise ExecutionError('A .isolate file is required.')
864 isolate = None
865 else:
866 isolate = complete_state.saved_state.isolate_filepath
867 else:
868 isolate = options.isolate
869 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500870 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000871 options.isolate, complete_state.saved_state.isolated_basedir)
872 if rel_isolate != complete_state.saved_state.isolate_file:
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400873 # This happens if the .isolate file was moved for example. In this case,
874 # discard the saved state.
875 logging.warning(
876 '--isolated %s != %s as saved in %s. Discarding saved state',
877 rel_isolate,
878 complete_state.saved_state.isolate_file,
879 isolatedfile_to_state(options.isolated))
880 complete_state = CompleteState(
881 options.isolated,
882 SavedState(complete_state.saved_state.isolated_basedir))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000883
884 if not skip_update:
885 # Then load the .isolate and expands directories.
886 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500887 cwd, isolate, options.path_variables, options.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500888 options.extra_variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000889
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000890 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +0000891 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000892 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500893 # This is tricky here. If it is a path, take it from the root_dir. If
894 # it is a variable, it must be keyed from the directory containing the
895 # .isolate file. So translate all variables first.
896 translated_path_variables = dict(
897 (k,
898 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
899 v)))
900 for k, v in complete_state.saved_state.path_variables.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500901 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000902 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000903
904 if not skip_update:
905 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000906 return complete_state
907
908
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000909def read_trace_as_isolate_dict(complete_state, trace_blacklist):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000910 """Reads a trace and returns the .isolate dictionary.
911
912 Returns exceptions during the log parsing so it can be re-raised.
913 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000914 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000915 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000916 if not os.path.isfile(logfile):
917 raise ExecutionError(
918 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
919 try:
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000920 data = api.parse_log(logfile, trace_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000921 exceptions = [i['exception'] for i in data if 'exception' in i]
922 results = (i['results'] for i in data if 'results' in i)
923 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
924 files = set(sum((result.existent for result in results_stripped), []))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500925 tracked, touched = isolate_format.split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000926 value = generate_isolate(
927 tracked,
928 [],
929 touched,
930 complete_state.root_dir,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500931 complete_state.saved_state.path_variables,
932 complete_state.saved_state.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500933 complete_state.saved_state.extra_variables,
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000934 complete_state.saved_state.relative_cwd,
935 trace_blacklist)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000936 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000937 except trace_inputs.TracingFailure, e:
938 raise ExecutionError(
939 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000940 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000941
942
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000943def merge(complete_state, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000944 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000945 value, exceptions = read_trace_as_isolate_dict(
946 complete_state, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000947
948 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000949 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000950 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000951 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500952 prev_config = isolate_format.load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000953 isolate_dir,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500954 isolate_format.eval_content(prev_content),
955 isolate_format.extract_comment(prev_content))
956 new_config = isolate_format.load_isolate_as_config(isolate_dir, value, '')
Marc-Antoine Ruelbd1b2842014-03-28 13:56:43 -0400957 config = prev_config.union(new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000958 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000959 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000960 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500961 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000962 if exceptions:
963 # It got an exception, raise the first one.
964 raise \
965 exceptions[0][0], \
966 exceptions[0][1], \
967 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000968
969
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500970def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
971 """Creates a isolated tree usable for test execution.
972
973 Returns the current working directory where the isolated command should be
974 started in.
975 """
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500976 # Forcibly copy when the tree has to be read only. Otherwise the inode is
977 # modified, and this cause real problems because the user's source tree
978 # becomes read only. On the other hand, the cost of doing file copy is huge.
979 if read_only not in (0, None):
980 action = run_isolated.COPY
981 else:
982 action = run_isolated.HARDLINK_WITH_FALLBACK
983
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500984 recreate_tree(
985 outdir=outdir,
986 indir=root_dir,
987 infiles=files,
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500988 action=action,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500989 as_hash=False)
990 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
991 if not os.path.isdir(cwd):
992 # It can happen when no files are mapped from the directory containing the
993 # .isolate file. But the directory must exist to be the current working
994 # directory.
995 os.makedirs(cwd)
996 run_isolated.change_tree_read_only(outdir, read_only)
997 return cwd
998
999
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001000def prepare_for_archival(options, cwd):
1001 """Loads the isolated file and create 'infiles' for archival."""
1002 complete_state = load_complete_state(
1003 options, cwd, options.subdir, False)
1004 # Make sure that complete_state isn't modified until save_files() is
1005 # called, because any changes made to it here will propagate to the files
1006 # created (which is probably not intended).
1007 complete_state.save_files()
1008
1009 infiles = complete_state.saved_state.files
1010 # Add all the .isolated files.
1011 isolated_hash = []
1012 isolated_files = [
1013 options.isolated,
1014 ] + complete_state.saved_state.child_isolated_files
1015 for item in isolated_files:
1016 item_path = os.path.join(
1017 os.path.dirname(complete_state.isolated_filepath), item)
1018 # Do not use isolateserver.hash_file() here because the file is
1019 # likely smallish (under 500kb) and its file size is needed.
1020 with open(item_path, 'rb') as f:
1021 content = f.read()
1022 isolated_hash.append(
1023 complete_state.saved_state.algo(content).hexdigest())
1024 isolated_metadata = {
1025 'h': isolated_hash[-1],
1026 's': len(content),
1027 'priority': '0'
1028 }
1029 infiles[item_path] = isolated_metadata
1030 return complete_state, infiles, isolated_hash
1031
1032
maruel@chromium.org29029882013-08-30 12:15:40 +00001033### Commands.
1034
1035
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001036def CMDarchive(parser, args):
1037 """Creates a .isolated file and uploads the tree to an isolate server.
maruel@chromium.org29029882013-08-30 12:15:40 +00001038
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001039 All the files listed in the .isolated file are put in the isolate server
1040 cache via isolateserver.py.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001041 """
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001042 add_subdir_option(parser)
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001043 isolateserver.add_isolate_server_options(parser, False)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001044 auth.add_auth_options(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001045 options, args = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001046 auth.process_auth_options(parser, options)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001047 isolateserver.process_isolate_server_options(parser, options)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001048 if args:
1049 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001050 cwd = os.getcwd()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001051 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001052 success = False
1053 try:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001054 complete_state, infiles, isolated_hash = prepare_for_archival(
1055 options, cwd)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001056 logging.info('Creating content addressed object store with %d item',
1057 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001058
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001059 isolateserver.upload_tree(
1060 base_url=options.isolate_server,
1061 indir=complete_state.root_dir,
1062 infiles=infiles,
1063 namespace=options.namespace)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001064 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001065 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001066 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001067 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001068 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001069 if not success and os.path.isfile(options.isolated):
1070 os.remove(options.isolated)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001071 return int(not success)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001072
1073
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001074def CMDcheck(parser, args):
1075 """Checks that all the inputs are present and generates .isolated."""
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001076 add_subdir_option(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001077 options, args = parser.parse_args(args)
1078 if args:
1079 parser.error('Unsupported argument: %s' % args)
1080
1081 complete_state = load_complete_state(
1082 options, os.getcwd(), options.subdir, False)
1083
1084 # Nothing is done specifically. Just store the result and state.
1085 complete_state.save_files()
1086 return 0
1087
1088
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001089def CMDhashtable(parser, args):
1090 """Creates a .isolated file and stores the contains in a directory.
1091
1092 All the files listed in the .isolated file are put in the directory with their
1093 sha-1 as their file name. When using an NFS/CIFS server, the files can then be
1094 shared accross slaves without an isolate server.
1095 """
1096 add_subdir_option(parser)
Marc-Antoine Ruel488ce8f2014-02-09 11:25:04 -05001097 isolateserver.add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001098 add_skip_refresh_option(parser)
1099 options, args = parser.parse_args(args)
1100 if args:
1101 parser.error('Unsupported argument: %s' % args)
1102 cwd = os.getcwd()
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001103 isolateserver.process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001104
1105 success = False
1106 try:
1107 complete_state, infiles, isolated_hash = prepare_for_archival(options, cwd)
1108 logging.info('Creating content addressed object store with %d item',
1109 len(infiles))
1110 if not os.path.isdir(options.outdir):
1111 os.makedirs(options.outdir)
1112
1113 # TODO(maruel): Make the files read-only?
1114 recreate_tree(
1115 outdir=options.outdir,
1116 indir=complete_state.root_dir,
1117 infiles=infiles,
1118 action=run_isolated.HARDLINK_WITH_FALLBACK,
1119 as_hash=True)
1120 success = True
1121 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
1122 finally:
1123 # If the command failed, delete the .isolated file if it exists. This is
1124 # important so no stale swarm job is executed.
1125 if not success and os.path.isfile(options.isolated):
1126 os.remove(options.isolated)
1127 return int(not success)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001128
1129
maruel@chromium.orge5322512013-08-19 20:17:57 +00001130def CMDmerge(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001131 """Reads and merges the data from the trace back into the original .isolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001132 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001133 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001134 add_trace_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001135 options, args = parser.parse_args(args)
1136 if args:
1137 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001138
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001139 complete_state = load_complete_state(options, os.getcwd(), None, False)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001140 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001141 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001142 return 0
1143
1144
maruel@chromium.orge5322512013-08-19 20:17:57 +00001145def CMDread(parser, args):
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001146 """Reads the trace file generated with command 'trace'."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001147 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001148 add_trace_option(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001149 add_skip_refresh_option(parser)
maruel@chromium.org29029882013-08-30 12:15:40 +00001150 parser.add_option(
1151 '-m', '--merge', action='store_true',
1152 help='merge the results back in the .isolate file instead of printing')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001153 options, args = parser.parse_args(args)
1154 if args:
1155 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001156
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001157 complete_state = load_complete_state(
1158 options, os.getcwd(), None, options.skip_refresh)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001159 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001160 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
maruel@chromium.org29029882013-08-30 12:15:40 +00001161 if options.merge:
1162 merge(complete_state, blacklist)
1163 else:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001164 isolate_format.pretty_print(value, sys.stdout)
maruel@chromium.org29029882013-08-30 12:15:40 +00001165
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001166 if exceptions:
1167 # It got an exception, raise the first one.
1168 raise \
1169 exceptions[0][0], \
1170 exceptions[0][1], \
1171 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001172 return 0
1173
1174
maruel@chromium.orge5322512013-08-19 20:17:57 +00001175def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001176 """Creates a directory with all the dependencies mapped into it.
1177
1178 Useful to test manually why a test is failing. The target executable is not
1179 run.
1180 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001181 parser.require_isolated = False
Marc-Antoine Ruel488ce8f2014-02-09 11:25:04 -05001182 isolateserver.add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001183 add_skip_refresh_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001184 options, args = parser.parse_args(args)
1185 if args:
1186 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001187 cwd = os.getcwd()
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001188 isolateserver.process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001189 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001190
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001191 if not os.path.isdir(options.outdir):
1192 os.makedirs(options.outdir)
1193 print('Remapping into %s' % options.outdir)
1194 if os.listdir(options.outdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001195 raise ExecutionError('Can\'t remap in a non-empty directory')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001196
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001197 create_isolate_tree(
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001198 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001199 complete_state.saved_state.relative_cwd,
1200 complete_state.saved_state.read_only)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001201 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001202 complete_state.save_files()
1203 return 0
1204
1205
maruel@chromium.orge5322512013-08-19 20:17:57 +00001206def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001207 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001208 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001209 options, args = parser.parse_args(args)
1210 if args:
1211 parser.error('Unsupported argument: %s' % args)
1212
1213 if options.isolated:
1214 # Load the previous state if it was present. Namely, "foo.isolated.state".
1215 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001216 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001217 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001218 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001219 if not isolate:
maruel@chromium.org29029882013-08-30 12:15:40 +00001220 parser.error('--isolate is required.')
1221
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001222 with open(isolate, 'r') as f:
1223 content = f.read()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001224 config = isolate_format.load_isolate_as_config(
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001225 os.path.dirname(os.path.abspath(isolate)),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001226 isolate_format.eval_content(content),
1227 isolate_format.extract_comment(content))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001228 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001229 print('Updating %s' % isolate)
1230 with open(isolate, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001231 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001232 return 0
1233
1234
maruel@chromium.org29029882013-08-30 12:15:40 +00001235@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001236def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001237 """Runs the test executable in an isolated (temporary) directory.
1238
1239 All the dependencies are mapped into the temporary directory and the
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001240 directory is cleaned up after the target exits.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001241
maruel@chromium.org29029882013-08-30 12:15:40 +00001242 Argument processing stops at -- and these arguments are appended to the
1243 command line of the target to run. For example, use:
1244 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001245 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001246 parser.require_isolated = False
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001247 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001248 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001249
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001250 complete_state = load_complete_state(
1251 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001252 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001253 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001254 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001255 cmd = tools.fix_python_path(cmd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001256
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001257 outdir = run_isolated.make_temp_dir(
1258 'isolate-%s' % datetime.date.today(),
1259 os.path.dirname(complete_state.root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001260 try:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -05001261 # TODO(maruel): Use run_isolated.run_tha_test().
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001262 cwd = create_isolate_tree(
1263 outdir, complete_state.root_dir, complete_state.saved_state.files,
1264 complete_state.saved_state.relative_cwd,
1265 complete_state.saved_state.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001266 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1267 result = subprocess.call(cmd, cwd=cwd)
1268 finally:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001269 run_isolated.rmtree(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001270
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001271 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001272 complete_state.save_files()
1273 return result
1274
1275
maruel@chromium.org29029882013-08-30 12:15:40 +00001276@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001277def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001278 """Traces the target using trace_inputs.py.
1279
1280 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001281 its child processes access. Then the 'merge' command can be used to generate
1282 an updated .isolate file out of it or the 'read' command to print it out to
1283 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001284
maruel@chromium.org29029882013-08-30 12:15:40 +00001285 Argument processing stops at -- and these arguments are appended to the
1286 command line of the target to run. For example, use:
1287 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001288 """
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001289 add_trace_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001290 parser.add_option(
1291 '-m', '--merge', action='store_true',
1292 help='After tracing, merge the results back in the .isolate file')
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001293 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001294 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001295
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001296 complete_state = load_complete_state(
1297 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001298 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001299 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001300 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001301 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001302 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001303 unicode(complete_state.root_dir),
1304 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001305 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1306 if not os.path.isfile(cmd[0]):
1307 raise ExecutionError(
1308 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001309 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1310 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001311 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001312 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001313 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001314 try:
1315 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001316 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001317 cmd,
1318 cwd,
1319 'default',
1320 True)
1321 except trace_inputs.TracingFailure, e:
1322 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1323
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001324 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001325 logging.error(
1326 'Tracer exited with %d, which means the tests probably failed so the '
1327 'trace is probably incomplete.', result)
1328 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001329
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001330 complete_state.save_files()
1331
1332 if options.merge:
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001333 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001334 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001335
1336 return result
1337
1338
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001339def _process_variable_arg(option, opt, _value, parser):
1340 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +00001341 if not parser.rargs:
1342 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001343 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001344 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001345 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001346 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001347 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001348 else:
1349 if not parser.rargs:
1350 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001351 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001352 v = parser.rargs.pop(0)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001353 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001354 raise optparse.OptionValueError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001355 'Variable \'%s\' doesn\'t respect format \'%s\'' %
1356 (k, isolate_format.VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -05001357 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001358
1359
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001360def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001361 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001362 parser.add_option(
1363 '-s', '--isolated',
1364 metavar='FILE',
1365 help='.isolated file to generate or read')
1366 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001367 parser.add_option(
1368 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001369 dest='isolated',
1370 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001371 is_win = sys.platform in ('win32', 'cygwin')
1372 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001373 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001374 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001375 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001376 # - configuration variables that are to be used in deducing the matrix to
1377 # reduce.
1378 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001379 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001380 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00001381 action='callback',
1382 callback=_process_variable_arg,
Marc-Antoine Ruel05199462014-03-13 15:40:48 -04001383 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001384 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001385 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001386 help='Config variables are used to determine which conditions should be '
1387 'matched when loading a .isolate file, default: %default. '
1388 'All 3 kinds of variables are persistent accross calls, they are '
1389 'saved inside <.isolated>.state')
1390 parser.add_option(
1391 '--path-variable',
1392 action='callback',
1393 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001394 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001395 dest='path_variables',
1396 metavar='FOO BAR',
1397 help='Path variables are used to replace file paths when loading a '
1398 '.isolate file, default: %default')
1399 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001400 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001401 action='callback',
1402 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001403 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1404 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001405 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001406 help='Extraneous variables are replaced on the \'command\' entry and on '
1407 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001408
1409
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001410def add_subdir_option(parser):
1411 parser.add_option(
1412 '--subdir',
1413 help='Filters to a subdirectory. Its behavior changes depending if it '
1414 'is a relative path as a string or as a path variable. Path '
1415 'variables are always keyed from the directory containing the '
1416 '.isolate file. Anything else is keyed on the root directory.')
1417
1418
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001419def add_trace_option(parser):
1420 """Adds --trace-blacklist to the parser."""
1421 parser.add_option(
1422 '--trace-blacklist',
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001423 action='append', default=list(isolateserver.DEFAULT_BLACKLIST),
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001424 help='List of regexp to use as blacklist filter for files to consider '
1425 'important, not to be confused with --blacklist which blacklists '
1426 'test case.')
1427
1428
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001429def add_skip_refresh_option(parser):
1430 parser.add_option(
1431 '--skip-refresh', action='store_true',
1432 help='Skip reading .isolate file and do not refresh the hash of '
1433 'dependencies')
1434
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001435
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001436def parse_isolated_option(parser, options, cwd, require_isolated):
1437 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001438 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001439 options.isolated = os.path.normpath(
1440 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001441 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001442 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001443 if options.isolated and not options.isolated.endswith('.isolated'):
1444 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001445
1446
1447def parse_variable_option(options):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001448 """Processes all the --<foo>-variable flags."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001449 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
1450 # but it wouldn't be backward compatible.
1451 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001452 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001453 try:
1454 return int(s)
1455 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001456 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001457 options.config_variables = dict(
1458 (k, try_make_int(v)) for k, v in options.config_variables)
1459 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001460 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001461
1462
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001463class OptionParserIsolate(tools.OptionParserWithLogging):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001464 """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
1465 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001466 # Set it to False if it is not required, e.g. it can be passed on but do not
1467 # fail if not given.
1468 require_isolated = True
1469
1470 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001471 tools.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00001472 self,
1473 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1474 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001475 group = optparse.OptionGroup(self, "Common options")
1476 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001477 '-i', '--isolate',
1478 metavar='FILE',
1479 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001480 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001481 group.add_option(
csharp@chromium.org01856802012-11-12 17:48:13 +00001482 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00001483 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1484 help='Indicates that invalid entries in the isolated file to be '
1485 'only be logged and not stop processing. Defaults to True if '
1486 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001487 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001488
1489 def parse_args(self, *args, **kwargs):
1490 """Makes sure the paths make sense.
1491
1492 On Windows, / and \ are often mixed together in a path.
1493 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001494 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001495 self, *args, **kwargs)
1496 if not self.allow_interspersed_args and args:
1497 self.error('Unsupported argument: %s' % args)
1498
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001499 cwd = file_path.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001500 parse_isolated_option(self, options, cwd, self.require_isolated)
1501 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001502
1503 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001504 # TODO(maruel): Work with non-ASCII.
1505 # The path must be in native path case for tracing purposes.
1506 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1507 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001508 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001509
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 return options, args
1511
1512
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001513def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00001514 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001515 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00001516 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001517 except Exception as e:
1518 tools.report_error(e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001519 return 1
1520
1521
1522if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00001523 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001524 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001525 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001526 sys.exit(main(sys.argv[1:]))