blob: 63a5bd21c003f6b19874479de1e1492f49d03da5 [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',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000455 # Cache of the processed command. This value is saved because .isolated
456 # files are never loaded by isolate.py so it's the only way to load the
457 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000458 'command',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500459 # GYP variables that are used to generate conditions. The most frequent
460 # example is 'OS'.
461 'config_variables',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500462 # GYP variables that will be replaced in 'command' and paths but will not be
463 # considered a relative directory.
464 'extra_variables',
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000465 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000466 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000467 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000468 'isolate_file',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000469 # List of included .isolated files. Used to support/remember 'slave'
470 # .isolated files. Relative path to isolated_basedir.
471 'child_isolated_files',
472 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000473 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000474 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000475 'relative_cwd',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500476 # GYP variables used to generate the .isolated files paths based on path
477 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
478 'path_variables',
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000479 # Version of the file format in format 'major.minor'. Any non-breaking
480 # change must update minor. Any breaking change must update major.
481 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000482 )
483
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000484 def __init__(self, isolated_basedir):
485 """Creates an empty SavedState.
486
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400487 Arguments:
488 isolated_basedir: the directory where the .isolated and .isolated.state
489 files are saved.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000490 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000491 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000492 assert os.path.isabs(isolated_basedir), isolated_basedir
493 assert os.path.isdir(isolated_basedir), isolated_basedir
494 self.isolated_basedir = isolated_basedir
495
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000496 # The default algorithm used.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400497 self.OS = sys.platform
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000498 self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500499 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000500 self.command = []
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500501 self.config_variables = {}
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500502 self.extra_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000503 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000504 self.isolate_file = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500505 self.path_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000506 self.read_only = None
507 self.relative_cwd = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500508 self.version = isolateserver.ISOLATED_FILE_VERSION
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000509
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500510 def update(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500511 self, isolate_file, path_variables, config_variables, extra_variables):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000512 """Updates the saved state with new data to keep GYP variables and internal
513 reference to the original .isolate file.
514 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +0000515 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000516 # Convert back to a relative path. On Windows, if the isolate and
517 # isolated files are on different drives, isolate_file will stay an absolute
518 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500519 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000520
521 # The same .isolate file should always be used to generate the .isolated and
522 # .isolated.state.
523 assert isolate_file == self.isolate_file or not self.isolate_file, (
524 isolate_file, self.isolate_file)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500525 self.config_variables.update(config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500526 self.extra_variables.update(extra_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000527 self.isolate_file = isolate_file
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500528 self.path_variables.update(path_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000529
530 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
531 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000532
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000533 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000534 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000535 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000536 self.command = command
537 # Add new files.
538 for f in infiles:
539 self.files.setdefault(f, {})
540 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000541 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000542 # Prune extraneous files that are not a dependency anymore.
543 for f in set(self.files).difference(set(infiles).union(touched)):
544 del self.files[f]
545 if read_only is not None:
546 self.read_only = read_only
547 self.relative_cwd = relative_cwd
548
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000549 def to_isolated(self):
550 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000551
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000552 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000553 """
554 def strip(data):
555 """Returns a 'files' entry with only the whitelisted keys."""
556 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
557
558 out = {
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000559 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000560 'files': dict(
561 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000562 'version': self.version,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000563 }
564 if self.command:
565 out['command'] = self.command
566 if self.read_only is not None:
567 out['read_only'] = self.read_only
568 if self.relative_cwd:
569 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000570 return out
571
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000572 @property
573 def isolate_filepath(self):
574 """Returns the absolute path of self.isolate_file."""
575 return os.path.normpath(
576 os.path.join(self.isolated_basedir, self.isolate_file))
577
578 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000579 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000580 def load(cls, data, isolated_basedir): # pylint: disable=W0221
581 """Special case loading to disallow different OS.
582
583 It is not possible to load a .isolated.state files from a different OS, this
584 file is saved in OS-specific format.
585 """
586 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400587 if data.get('OS') != sys.platform:
588 raise isolateserver.ConfigError('Unexpected OS %s', data.get('OS'))
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000589
590 # Converts human readable form back into the proper class type.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400591 algo = data.get('algo')
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000592 if not algo in isolateserver.SUPPORTED_ALGOS:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000593 raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000594 out.algo = isolateserver.SUPPORTED_ALGOS[algo]
595
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500596 # Refuse the load non-exact version, even minor difference. This is unlike
597 # isolateserver.load_isolated(). This is because .isolated.state could have
598 # changed significantly even in minor version difference.
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000599 if not re.match(r'^(\d+)\.(\d+)$', out.version):
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000600 raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500601 if out.version != isolateserver.ISOLATED_FILE_VERSION:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000602 raise isolateserver.ConfigError(
603 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000604
Marc-Antoine Ruel16ebc2e2014-02-13 15:39:15 -0500605 # The .isolate file must be valid. If it is not present anymore, zap the
606 # value as if it was not noted, so .isolate_file can safely be overriden
607 # later.
608 if out.isolate_file and not os.path.isfile(out.isolate_filepath):
609 out.isolate_file = None
610 if out.isolate_file:
611 # It could be absolute on Windows if the drive containing the .isolate and
612 # the drive containing the .isolated files differ, .e.g .isolate is on
613 # C:\\ and .isolated is on D:\\ .
614 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
615 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000616 return out
617
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000618 def flatten(self):
619 """Makes sure 'algo' is in human readable form."""
620 out = super(SavedState, self).flatten()
621 out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
622 return out
623
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000624 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500625 def dict_to_str(d):
626 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
627
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000628 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000629 out += ' command: %s\n' % self.command
630 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000631 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000632 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000633 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000634 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500635 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
636 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500637 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000638 return out
639
640
641class CompleteState(object):
642 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000643 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000644 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +0000645 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000646 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000647 # Contains the data to ease developer's use-case but that is not strictly
648 # necessary.
649 self.saved_state = saved_state
650
651 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000652 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000653 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000654 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000655 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000656 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000657 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000658 SavedState.load_file(
659 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000660
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500661 def load_isolate(
662 self, cwd, isolate_file, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500663 extra_variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000664 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000665 .isolate file.
666
667 Processes the loaded data, deduce root_dir, relative_cwd.
668 """
669 # Make sure to not depend on os.getcwd().
670 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000671 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000672 logging.info(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500673 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500674 cwd, isolate_file, path_variables, config_variables, extra_variables,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500675 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000676 relative_base_dir = os.path.dirname(isolate_file)
677
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500678 # Processes the variables.
679 path_variables = normalize_path_variables(
680 cwd, path_variables, relative_base_dir)
681 # Update the saved state.
682 self.saved_state.update(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500683 isolate_file, path_variables, config_variables, extra_variables)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500684 path_variables = self.saved_state.path_variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000685
686 with open(isolate_file, 'r') as f:
687 # At that point, variables are not replaced yet in command and infiles.
688 # infiles may contain directory entries and is in posix style.
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400689 command, infiles, touched, read_only, isolate_cmd_dir = (
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500690 isolate_format.load_isolate_for_config(
691 os.path.dirname(isolate_file), f.read(),
692 self.saved_state.config_variables))
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500693
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500694 total_variables = self.saved_state.path_variables.copy()
695 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500696 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500697 command = [
698 isolate_format.eval_variables(i, total_variables) for i in command
699 ]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500700
701 total_variables = self.saved_state.path_variables.copy()
702 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500703 infiles = [
704 isolate_format.eval_variables(f, total_variables) for f in infiles
705 ]
706 touched = [
707 isolate_format.eval_variables(f, total_variables) for f in touched
708 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000709 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +0000710 # form '../../foo/bar'. Note that path variables must be taken in account
711 # too, add them as if they were input files.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500712 root_dir = isolate_format.determine_root_dir(
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400713 isolate_cmd_dir, infiles + touched +
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500714 self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000715 # The relative directory is automatically determined by the relative path
716 # between root_dir and the directory containing the .isolate file,
717 # isolate_base_dir.
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400718 relative_cwd = os.path.relpath(isolate_cmd_dir, root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500719 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000720 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500721 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500722 if not file_path.path_starts_with(
723 root_dir, os.path.join(relative_base_dir, v)):
724 raise isolateserver.MappingError(
725 'Path variable %s=%r points outside the inferred root directory %s'
726 % (k, v, root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000727 # Normalize the files based to root_dir. It is important to keep the
728 # trailing os.path.sep at that step.
729 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500730 file_path.relpath(
731 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000732 for f in infiles
733 ]
734 touched = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500735 file_path.relpath(
736 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000737 for f in touched
738 ]
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400739 follow_symlinks = sys.platform != 'win32'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000740 # Expand the directories by listing each file inside. Up to now, trailing
741 # os.path.sep must be kept. Do not expand 'touched'.
742 infiles = expand_directories_and_symlinks(
743 root_dir,
744 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +0000745 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000746 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +0000747 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000748
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000749 # If we ignore broken items then remove any missing touched items.
750 if ignore_broken_items:
751 original_touched_count = len(touched)
752 touched = [touch for touch in touched if os.path.exists(touch)]
753
754 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +0000755 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000756 len(touched) - original_touched_count)
757
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000758 # Finally, update the new data to be able to generate the foo.isolated file,
759 # the file that is used by run_isolated.py.
760 self.saved_state.update_isolated(
761 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000762 logging.debug(self)
763
maruel@chromium.org9268f042012-10-17 17:36:41 +0000764 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000765 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000766
maruel@chromium.org9268f042012-10-17 17:36:41 +0000767 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
768 file is tainted.
769
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500770 See isolateserver.process_input() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000771 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000772 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +0000773 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000774 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000775 else:
776 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500777 self.saved_state.files[infile] = isolateserver.process_input(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000778 filepath,
779 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000780 self.saved_state.read_only,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000781 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000782
783 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000784 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000785 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000786 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000787 self.isolated_filepath,
788 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500789 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000790 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000791 total_bytes = sum(
792 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000793 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000794 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000795 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000796 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000797 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500798 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000799
800 @property
801 def root_dir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000802 """Returns the absolute path of the root_dir to reference the .isolate file
803 via relative_cwd.
804
805 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
806 to isolate_filepath.
807 """
maruel@chromium.orgb253fb82012-10-16 21:44:48 +0000808 if not self.saved_state.isolate_file:
809 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000810 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000811 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000812 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000813 root_dir = isolate_dir
814 else:
maruel@chromium.org87557b92013-10-16 18:04:11 +0000815 if not isolate_dir.endswith(self.saved_state.relative_cwd):
816 raise ExecutionError(
817 ('Make sure the .isolate file is in the directory that will be '
818 'used as the relative directory. It is currently in %s and should '
819 'be in %s') % (isolate_dir, self.saved_state.relative_cwd))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000820 # Walk back back to the root directory.
821 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000822 return file_path.get_native_path_case(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000823
824 @property
825 def resultdir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000826 """Returns the absolute path containing the .isolated file.
827
828 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
829 path as the value.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000830 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000831 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000832
833 def __str__(self):
834 def indent(data, indent_length):
835 """Indents text."""
836 spacing = ' ' * indent_length
837 return ''.join(spacing + l for l in str(data).splitlines(True))
838
839 out = '%s(\n' % self.__class__.__name__
840 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000841 out += ' saved_state: %s)' % indent(self.saved_state, 2)
842 return out
843
844
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000845def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000846 """Loads a CompleteState.
847
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000848 This includes data from .isolate and .isolated.state files. Never reads the
849 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000850
851 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000852 options: Options instance generated with OptionParserIsolate. For either
853 options.isolate and options.isolated, if the value is set, it is an
854 absolute path.
855 cwd: base directory to be used when loading the .isolate file.
856 subdir: optional argument to only process file in the subdirectory, relative
857 to CompleteState.root_dir.
858 skip_update: Skip trying to load the .isolate file and processing the
859 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000860 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000861 assert not options.isolate or os.path.isabs(options.isolate)
862 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000863 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000864 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000865 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000866 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000867 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000868 else:
869 # Constructs a dummy object that cannot be saved. Useful for temporary
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400870 # commands like 'run'. There is no directory containing a .isolated file so
871 # specify the current working directory as a valid directory.
872 complete_state = CompleteState(None, SavedState(os.getcwd()))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000873
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000874 if not options.isolate:
875 if not complete_state.saved_state.isolate_file:
876 if not skip_update:
877 raise ExecutionError('A .isolate file is required.')
878 isolate = None
879 else:
880 isolate = complete_state.saved_state.isolate_filepath
881 else:
882 isolate = options.isolate
883 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500884 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000885 options.isolate, complete_state.saved_state.isolated_basedir)
886 if rel_isolate != complete_state.saved_state.isolate_file:
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400887 # This happens if the .isolate file was moved for example. In this case,
888 # discard the saved state.
889 logging.warning(
890 '--isolated %s != %s as saved in %s. Discarding saved state',
891 rel_isolate,
892 complete_state.saved_state.isolate_file,
893 isolatedfile_to_state(options.isolated))
894 complete_state = CompleteState(
895 options.isolated,
896 SavedState(complete_state.saved_state.isolated_basedir))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000897
898 if not skip_update:
899 # Then load the .isolate and expands directories.
900 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500901 cwd, isolate, options.path_variables, options.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500902 options.extra_variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000903
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000904 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +0000905 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000906 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500907 # This is tricky here. If it is a path, take it from the root_dir. If
908 # it is a variable, it must be keyed from the directory containing the
909 # .isolate file. So translate all variables first.
910 translated_path_variables = dict(
911 (k,
912 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
913 v)))
914 for k, v in complete_state.saved_state.path_variables.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500915 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000916 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000917
918 if not skip_update:
919 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000920 return complete_state
921
922
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000923def read_trace_as_isolate_dict(complete_state, trace_blacklist):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000924 """Reads a trace and returns the .isolate dictionary.
925
926 Returns exceptions during the log parsing so it can be re-raised.
927 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000928 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000929 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000930 if not os.path.isfile(logfile):
931 raise ExecutionError(
932 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
933 try:
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000934 data = api.parse_log(logfile, trace_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000935 exceptions = [i['exception'] for i in data if 'exception' in i]
936 results = (i['results'] for i in data if 'results' in i)
937 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
938 files = set(sum((result.existent for result in results_stripped), []))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500939 tracked, touched = isolate_format.split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000940 value = generate_isolate(
941 tracked,
942 [],
943 touched,
944 complete_state.root_dir,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500945 complete_state.saved_state.path_variables,
946 complete_state.saved_state.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500947 complete_state.saved_state.extra_variables,
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000948 complete_state.saved_state.relative_cwd,
949 trace_blacklist)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000950 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000951 except trace_inputs.TracingFailure, e:
952 raise ExecutionError(
953 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000954 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000955
956
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000957def merge(complete_state, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000958 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000959 value, exceptions = read_trace_as_isolate_dict(
960 complete_state, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000961
962 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000963 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000964 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000965 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500966 prev_config = isolate_format.load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000967 isolate_dir,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500968 isolate_format.eval_content(prev_content),
969 isolate_format.extract_comment(prev_content))
970 new_config = isolate_format.load_isolate_as_config(isolate_dir, value, '')
971 config = isolate_format.union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000972 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000973 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000974 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500975 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000976 if exceptions:
977 # It got an exception, raise the first one.
978 raise \
979 exceptions[0][0], \
980 exceptions[0][1], \
981 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000982
983
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500984def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
985 """Creates a isolated tree usable for test execution.
986
987 Returns the current working directory where the isolated command should be
988 started in.
989 """
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500990 # Forcibly copy when the tree has to be read only. Otherwise the inode is
991 # modified, and this cause real problems because the user's source tree
992 # becomes read only. On the other hand, the cost of doing file copy is huge.
993 if read_only not in (0, None):
994 action = run_isolated.COPY
995 else:
996 action = run_isolated.HARDLINK_WITH_FALLBACK
997
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500998 recreate_tree(
999 outdir=outdir,
1000 indir=root_dir,
1001 infiles=files,
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -05001002 action=action,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001003 as_hash=False)
1004 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
1005 if not os.path.isdir(cwd):
1006 # It can happen when no files are mapped from the directory containing the
1007 # .isolate file. But the directory must exist to be the current working
1008 # directory.
1009 os.makedirs(cwd)
1010 run_isolated.change_tree_read_only(outdir, read_only)
1011 return cwd
1012
1013
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001014def prepare_for_archival(options, cwd):
1015 """Loads the isolated file and create 'infiles' for archival."""
1016 complete_state = load_complete_state(
1017 options, cwd, options.subdir, False)
1018 # Make sure that complete_state isn't modified until save_files() is
1019 # called, because any changes made to it here will propagate to the files
1020 # created (which is probably not intended).
1021 complete_state.save_files()
1022
1023 infiles = complete_state.saved_state.files
1024 # Add all the .isolated files.
1025 isolated_hash = []
1026 isolated_files = [
1027 options.isolated,
1028 ] + complete_state.saved_state.child_isolated_files
1029 for item in isolated_files:
1030 item_path = os.path.join(
1031 os.path.dirname(complete_state.isolated_filepath), item)
1032 # Do not use isolateserver.hash_file() here because the file is
1033 # likely smallish (under 500kb) and its file size is needed.
1034 with open(item_path, 'rb') as f:
1035 content = f.read()
1036 isolated_hash.append(
1037 complete_state.saved_state.algo(content).hexdigest())
1038 isolated_metadata = {
1039 'h': isolated_hash[-1],
1040 's': len(content),
1041 'priority': '0'
1042 }
1043 infiles[item_path] = isolated_metadata
1044 return complete_state, infiles, isolated_hash
1045
1046
maruel@chromium.org29029882013-08-30 12:15:40 +00001047### Commands.
1048
1049
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001050def CMDarchive(parser, args):
1051 """Creates a .isolated file and uploads the tree to an isolate server.
maruel@chromium.org29029882013-08-30 12:15:40 +00001052
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001053 All the files listed in the .isolated file are put in the isolate server
1054 cache via isolateserver.py.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001055 """
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001056 add_subdir_option(parser)
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001057 isolateserver.add_isolate_server_options(parser, False)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001058 auth.add_auth_options(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001059 options, args = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001060 auth.process_auth_options(parser, options)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001061 isolateserver.process_isolate_server_options(parser, options)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001062 if args:
1063 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001064 cwd = os.getcwd()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001065 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001066 success = False
1067 try:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001068 complete_state, infiles, isolated_hash = prepare_for_archival(
1069 options, cwd)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001070 logging.info('Creating content addressed object store with %d item',
1071 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001072
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001073 isolateserver.upload_tree(
1074 base_url=options.isolate_server,
1075 indir=complete_state.root_dir,
1076 infiles=infiles,
1077 namespace=options.namespace)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001078 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001079 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001080 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001081 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001082 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001083 if not success and os.path.isfile(options.isolated):
1084 os.remove(options.isolated)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001085 return int(not success)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001086
1087
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001088def CMDcheck(parser, args):
1089 """Checks that all the inputs are present and generates .isolated."""
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001090 add_subdir_option(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001091 options, args = parser.parse_args(args)
1092 if args:
1093 parser.error('Unsupported argument: %s' % args)
1094
1095 complete_state = load_complete_state(
1096 options, os.getcwd(), options.subdir, False)
1097
1098 # Nothing is done specifically. Just store the result and state.
1099 complete_state.save_files()
1100 return 0
1101
1102
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001103def CMDhashtable(parser, args):
1104 """Creates a .isolated file and stores the contains in a directory.
1105
1106 All the files listed in the .isolated file are put in the directory with their
1107 sha-1 as their file name. When using an NFS/CIFS server, the files can then be
1108 shared accross slaves without an isolate server.
1109 """
1110 add_subdir_option(parser)
Marc-Antoine Ruel488ce8f2014-02-09 11:25:04 -05001111 isolateserver.add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001112 add_skip_refresh_option(parser)
1113 options, args = parser.parse_args(args)
1114 if args:
1115 parser.error('Unsupported argument: %s' % args)
1116 cwd = os.getcwd()
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001117 isolateserver.process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001118
1119 success = False
1120 try:
1121 complete_state, infiles, isolated_hash = prepare_for_archival(options, cwd)
1122 logging.info('Creating content addressed object store with %d item',
1123 len(infiles))
1124 if not os.path.isdir(options.outdir):
1125 os.makedirs(options.outdir)
1126
1127 # TODO(maruel): Make the files read-only?
1128 recreate_tree(
1129 outdir=options.outdir,
1130 indir=complete_state.root_dir,
1131 infiles=infiles,
1132 action=run_isolated.HARDLINK_WITH_FALLBACK,
1133 as_hash=True)
1134 success = True
1135 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
1136 finally:
1137 # If the command failed, delete the .isolated file if it exists. This is
1138 # important so no stale swarm job is executed.
1139 if not success and os.path.isfile(options.isolated):
1140 os.remove(options.isolated)
1141 return int(not success)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001142
1143
maruel@chromium.orge5322512013-08-19 20:17:57 +00001144def CMDmerge(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001145 """Reads and merges the data from the trace back into the original .isolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001146 """
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)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001149 options, args = parser.parse_args(args)
1150 if args:
1151 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001152
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001153 complete_state = load_complete_state(options, os.getcwd(), None, False)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001154 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001155 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001156 return 0
1157
1158
maruel@chromium.orge5322512013-08-19 20:17:57 +00001159def CMDread(parser, args):
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001160 """Reads the trace file generated with command 'trace'."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001161 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001162 add_trace_option(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001163 add_skip_refresh_option(parser)
maruel@chromium.org29029882013-08-30 12:15:40 +00001164 parser.add_option(
1165 '-m', '--merge', action='store_true',
1166 help='merge the results back in the .isolate file instead of printing')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001167 options, args = parser.parse_args(args)
1168 if args:
1169 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001170
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001171 complete_state = load_complete_state(
1172 options, os.getcwd(), None, options.skip_refresh)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001173 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001174 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
maruel@chromium.org29029882013-08-30 12:15:40 +00001175 if options.merge:
1176 merge(complete_state, blacklist)
1177 else:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001178 isolate_format.pretty_print(value, sys.stdout)
maruel@chromium.org29029882013-08-30 12:15:40 +00001179
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001180 if exceptions:
1181 # It got an exception, raise the first one.
1182 raise \
1183 exceptions[0][0], \
1184 exceptions[0][1], \
1185 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001186 return 0
1187
1188
maruel@chromium.orge5322512013-08-19 20:17:57 +00001189def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001190 """Creates a directory with all the dependencies mapped into it.
1191
1192 Useful to test manually why a test is failing. The target executable is not
1193 run.
1194 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001195 parser.require_isolated = False
Marc-Antoine Ruel488ce8f2014-02-09 11:25:04 -05001196 isolateserver.add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001197 add_skip_refresh_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001198 options, args = parser.parse_args(args)
1199 if args:
1200 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001201 cwd = os.getcwd()
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001202 isolateserver.process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001203 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001204
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001205 if not os.path.isdir(options.outdir):
1206 os.makedirs(options.outdir)
1207 print('Remapping into %s' % options.outdir)
1208 if os.listdir(options.outdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001209 raise ExecutionError('Can\'t remap in a non-empty directory')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001210
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001211 create_isolate_tree(
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001212 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001213 complete_state.saved_state.relative_cwd,
1214 complete_state.saved_state.read_only)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001215 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001216 complete_state.save_files()
1217 return 0
1218
1219
maruel@chromium.orge5322512013-08-19 20:17:57 +00001220def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001221 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001222 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001223 options, args = parser.parse_args(args)
1224 if args:
1225 parser.error('Unsupported argument: %s' % args)
1226
1227 if options.isolated:
1228 # Load the previous state if it was present. Namely, "foo.isolated.state".
1229 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001230 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001231 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001232 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001233 if not isolate:
maruel@chromium.org29029882013-08-30 12:15:40 +00001234 parser.error('--isolate is required.')
1235
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001236 with open(isolate, 'r') as f:
1237 content = f.read()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001238 config = isolate_format.load_isolate_as_config(
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001239 os.path.dirname(os.path.abspath(isolate)),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001240 isolate_format.eval_content(content),
1241 isolate_format.extract_comment(content))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001242 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001243 print('Updating %s' % isolate)
1244 with open(isolate, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001245 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001246 return 0
1247
1248
maruel@chromium.org29029882013-08-30 12:15:40 +00001249@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001250def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001251 """Runs the test executable in an isolated (temporary) directory.
1252
1253 All the dependencies are mapped into the temporary directory and the
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001254 directory is cleaned up after the target exits.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001255
maruel@chromium.org29029882013-08-30 12:15:40 +00001256 Argument processing stops at -- and these arguments are appended to the
1257 command line of the target to run. For example, use:
1258 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001259 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001260 parser.require_isolated = False
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001261 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001262 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001263
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001264 complete_state = load_complete_state(
1265 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001266 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001267 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001268 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001269 cmd = tools.fix_python_path(cmd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001270
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001271 outdir = run_isolated.make_temp_dir(
1272 'isolate-%s' % datetime.date.today(),
1273 os.path.dirname(complete_state.root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001274 try:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -05001275 # TODO(maruel): Use run_isolated.run_tha_test().
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001276 cwd = create_isolate_tree(
1277 outdir, complete_state.root_dir, complete_state.saved_state.files,
1278 complete_state.saved_state.relative_cwd,
1279 complete_state.saved_state.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001280 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1281 result = subprocess.call(cmd, cwd=cwd)
1282 finally:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001283 run_isolated.rmtree(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001284
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001285 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001286 complete_state.save_files()
1287 return result
1288
1289
maruel@chromium.org29029882013-08-30 12:15:40 +00001290@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001291def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001292 """Traces the target using trace_inputs.py.
1293
1294 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001295 its child processes access. Then the 'merge' command can be used to generate
1296 an updated .isolate file out of it or the 'read' command to print it out to
1297 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001298
maruel@chromium.org29029882013-08-30 12:15:40 +00001299 Argument processing stops at -- and these arguments are appended to the
1300 command line of the target to run. For example, use:
1301 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001302 """
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001303 add_trace_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001304 parser.add_option(
1305 '-m', '--merge', action='store_true',
1306 help='After tracing, merge the results back in the .isolate file')
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001307 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001308 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001309
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001310 complete_state = load_complete_state(
1311 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001312 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001313 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001314 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001315 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001316 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001317 unicode(complete_state.root_dir),
1318 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001319 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1320 if not os.path.isfile(cmd[0]):
1321 raise ExecutionError(
1322 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001323 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1324 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001325 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001326 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001327 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001328 try:
1329 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001330 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001331 cmd,
1332 cwd,
1333 'default',
1334 True)
1335 except trace_inputs.TracingFailure, e:
1336 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1337
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001338 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001339 logging.error(
1340 'Tracer exited with %d, which means the tests probably failed so the '
1341 'trace is probably incomplete.', result)
1342 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001343
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001344 complete_state.save_files()
1345
1346 if options.merge:
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001347 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001348 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001349
1350 return result
1351
1352
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001353def _process_variable_arg(option, opt, _value, parser):
1354 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +00001355 if not parser.rargs:
1356 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001357 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001358 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001359 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001360 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001361 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001362 else:
1363 if not parser.rargs:
1364 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001365 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001366 v = parser.rargs.pop(0)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001367 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001368 raise optparse.OptionValueError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001369 'Variable \'%s\' doesn\'t respect format \'%s\'' %
1370 (k, isolate_format.VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -05001371 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001372
1373
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001374def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001375 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001376 parser.add_option(
1377 '-s', '--isolated',
1378 metavar='FILE',
1379 help='.isolated file to generate or read')
1380 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001381 parser.add_option(
1382 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001383 dest='isolated',
1384 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001385 is_win = sys.platform in ('win32', 'cygwin')
1386 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001387 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001388 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001389 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001390 # - configuration variables that are to be used in deducing the matrix to
1391 # reduce.
1392 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001393 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001394 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00001395 action='callback',
1396 callback=_process_variable_arg,
Marc-Antoine Ruel05199462014-03-13 15:40:48 -04001397 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001398 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001399 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001400 help='Config variables are used to determine which conditions should be '
1401 'matched when loading a .isolate file, default: %default. '
1402 'All 3 kinds of variables are persistent accross calls, they are '
1403 'saved inside <.isolated>.state')
1404 parser.add_option(
1405 '--path-variable',
1406 action='callback',
1407 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001408 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001409 dest='path_variables',
1410 metavar='FOO BAR',
1411 help='Path variables are used to replace file paths when loading a '
1412 '.isolate file, default: %default')
1413 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001414 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001415 action='callback',
1416 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001417 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1418 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001419 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001420 help='Extraneous variables are replaced on the \'command\' entry and on '
1421 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001422
1423
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001424def add_subdir_option(parser):
1425 parser.add_option(
1426 '--subdir',
1427 help='Filters to a subdirectory. Its behavior changes depending if it '
1428 'is a relative path as a string or as a path variable. Path '
1429 'variables are always keyed from the directory containing the '
1430 '.isolate file. Anything else is keyed on the root directory.')
1431
1432
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001433def add_trace_option(parser):
1434 """Adds --trace-blacklist to the parser."""
1435 parser.add_option(
1436 '--trace-blacklist',
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001437 action='append', default=list(isolateserver.DEFAULT_BLACKLIST),
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001438 help='List of regexp to use as blacklist filter for files to consider '
1439 'important, not to be confused with --blacklist which blacklists '
1440 'test case.')
1441
1442
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001443def add_skip_refresh_option(parser):
1444 parser.add_option(
1445 '--skip-refresh', action='store_true',
1446 help='Skip reading .isolate file and do not refresh the hash of '
1447 'dependencies')
1448
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001449
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001450def parse_isolated_option(parser, options, cwd, require_isolated):
1451 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001452 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001453 options.isolated = os.path.normpath(
1454 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001455 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001456 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001457 if options.isolated and not options.isolated.endswith('.isolated'):
1458 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001459
1460
1461def parse_variable_option(options):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001462 """Processes all the --<foo>-variable flags."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001463 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
1464 # but it wouldn't be backward compatible.
1465 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001466 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001467 try:
1468 return int(s)
1469 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001470 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001471 options.config_variables = dict(
1472 (k, try_make_int(v)) for k, v in options.config_variables)
1473 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001474 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001475
1476
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001477class OptionParserIsolate(tools.OptionParserWithLogging):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001478 """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
1479 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001480 # Set it to False if it is not required, e.g. it can be passed on but do not
1481 # fail if not given.
1482 require_isolated = True
1483
1484 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001485 tools.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00001486 self,
1487 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1488 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001489 group = optparse.OptionGroup(self, "Common options")
1490 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001491 '-i', '--isolate',
1492 metavar='FILE',
1493 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001494 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001495 group.add_option(
csharp@chromium.org01856802012-11-12 17:48:13 +00001496 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00001497 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1498 help='Indicates that invalid entries in the isolated file to be '
1499 'only be logged and not stop processing. Defaults to True if '
1500 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001501 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001502
1503 def parse_args(self, *args, **kwargs):
1504 """Makes sure the paths make sense.
1505
1506 On Windows, / and \ are often mixed together in a path.
1507 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001508 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001509 self, *args, **kwargs)
1510 if not self.allow_interspersed_args and args:
1511 self.error('Unsupported argument: %s' % args)
1512
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001513 cwd = file_path.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001514 parse_isolated_option(self, options, cwd, self.require_isolated)
1515 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001516
1517 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001518 # TODO(maruel): Work with non-ASCII.
1519 # The path must be in native path case for tracing purposes.
1520 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1521 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001522 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001523
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001524 return options, args
1525
1526
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001527def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00001528 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001529 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00001530 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001531 except Exception as e:
1532 tools.report_error(e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001533 return 1
1534
1535
1536if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00001537 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001538 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001539 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001540 sys.exit(main(sys.argv[1:]))