blob: 9fc1ac0d8801d50de32284c07bf30685642cb302 [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 Ruel8170f492014-03-13 15:26:56 -0400345 out.set_config(config_values, isolate_format.ConfigSettings(dependencies))
benrg@chromium.org609b7982013-02-07 16:44:46 +0000346 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000347
348
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500349def chromium_save_isolated(isolated, data, path_variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000350 """Writes one or many .isolated files.
351
352 This slightly increases the cold cache cost but greatly reduce the warm cache
353 cost by splitting low-churn files off the master .isolated file. It also
354 reduces overall isolateserver memcache consumption.
355 """
356 slaves = []
357
358 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000359 new_slave = {
360 'algo': data['algo'],
361 'files': {},
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000362 'version': data['version'],
363 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000364 for f in data['files'].keys():
365 if f.startswith(prefix):
366 new_slave['files'][f] = data['files'].pop(f)
367 if new_slave['files']:
368 slaves.append(new_slave)
369
370 # Split test/data/ in its own .isolated file.
371 extract_into_included_isolated(os.path.join('test', 'data', ''))
372
373 # Split everything out of PRODUCT_DIR in its own .isolated file.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500374 if path_variables.get('PRODUCT_DIR'):
375 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000376
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000377 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000378 for index, f in enumerate(slaves):
379 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500380 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000381 data.setdefault('includes', []).append(
382 isolateserver.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000383 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000384
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500385 files.extend(isolateserver.save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000386 return files
387
388
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000389class Flattenable(object):
390 """Represents data that can be represented as a json file."""
391 MEMBERS = ()
392
393 def flatten(self):
394 """Returns a json-serializable version of itself.
395
396 Skips None entries.
397 """
398 items = ((member, getattr(self, member)) for member in self.MEMBERS)
399 return dict((member, value) for member, value in items if value is not None)
400
401 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000402 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000403 """Loads a flattened version."""
404 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000405 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000406 for member in out.MEMBERS:
407 if member in data:
408 # Access to a protected member XXX of a client class
409 # pylint: disable=W0212
410 out._load_member(member, data.pop(member))
411 if data:
412 raise ValueError(
413 'Found unexpected entry %s while constructing an object %s' %
414 (data, cls.__name__), data, cls.__name__)
415 return out
416
417 def _load_member(self, member, value):
418 """Loads a member into self."""
419 setattr(self, member, value)
420
421 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000422 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000423 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000424 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500425 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000426 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000427 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000428 # On failure, loads the default instance.
429 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000430 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000431 return out
432
433
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000434class SavedState(Flattenable):
435 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000436
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000437 This file caches the items calculated by this script and is used to increase
438 the performance of the script. This file is not loaded by run_isolated.py.
439 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000440
441 It is important to note that the 'files' dict keys are using native OS path
442 separator instead of '/' used in .isolate file.
443 """
444 MEMBERS = (
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400445 # Value of sys.platform so that the file is rejected if loaded from a
446 # different OS. While this should never happen in practice, users are ...
447 # "creative".
448 'OS',
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000449 # Algorithm used to generate the hash. The only supported value is at the
450 # time of writting 'sha-1'.
451 'algo',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000452 # Cache of the processed command. This value is saved because .isolated
453 # files are never loaded by isolate.py so it's the only way to load the
454 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000455 'command',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500456 # GYP variables that are used to generate conditions. The most frequent
457 # example is 'OS'.
458 'config_variables',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500459 # GYP variables that will be replaced in 'command' and paths but will not be
460 # considered a relative directory.
461 'extra_variables',
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000462 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000463 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000464 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000465 'isolate_file',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000466 # List of included .isolated files. Used to support/remember 'slave'
467 # .isolated files. Relative path to isolated_basedir.
468 'child_isolated_files',
469 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000470 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000471 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000472 'relative_cwd',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500473 # GYP variables used to generate the .isolated files paths based on path
474 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
475 'path_variables',
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000476 # Version of the file format in format 'major.minor'. Any non-breaking
477 # change must update minor. Any breaking change must update major.
478 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000479 )
480
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000481 def __init__(self, isolated_basedir):
482 """Creates an empty SavedState.
483
484 |isolated_basedir| is the directory where the .isolated and .isolated.state
485 files are saved.
486 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000487 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000488 assert os.path.isabs(isolated_basedir), isolated_basedir
489 assert os.path.isdir(isolated_basedir), isolated_basedir
490 self.isolated_basedir = isolated_basedir
491
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000492 # The default algorithm used.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400493 self.OS = sys.platform
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000494 self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500495 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000496 self.command = []
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500497 self.config_variables = {}
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500498 self.extra_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000499 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000500 self.isolate_file = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500501 self.path_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000502 self.read_only = None
503 self.relative_cwd = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500504 self.version = isolateserver.ISOLATED_FILE_VERSION
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000505
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500506 def update(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500507 self, isolate_file, path_variables, config_variables, extra_variables):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000508 """Updates the saved state with new data to keep GYP variables and internal
509 reference to the original .isolate file.
510 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +0000511 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000512 # Convert back to a relative path. On Windows, if the isolate and
513 # isolated files are on different drives, isolate_file will stay an absolute
514 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500515 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000516
517 # The same .isolate file should always be used to generate the .isolated and
518 # .isolated.state.
519 assert isolate_file == self.isolate_file or not self.isolate_file, (
520 isolate_file, self.isolate_file)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500521 self.config_variables.update(config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500522 self.extra_variables.update(extra_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000523 self.isolate_file = isolate_file
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500524 self.path_variables.update(path_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000525
526 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
527 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000528
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000529 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000530 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000531 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000532 self.command = command
533 # Add new files.
534 for f in infiles:
535 self.files.setdefault(f, {})
536 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000537 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000538 # Prune extraneous files that are not a dependency anymore.
539 for f in set(self.files).difference(set(infiles).union(touched)):
540 del self.files[f]
541 if read_only is not None:
542 self.read_only = read_only
543 self.relative_cwd = relative_cwd
544
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000545 def to_isolated(self):
546 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000547
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000548 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000549 """
550 def strip(data):
551 """Returns a 'files' entry with only the whitelisted keys."""
552 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
553
554 out = {
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000555 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000556 'files': dict(
557 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000558 'version': self.version,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000559 }
560 if self.command:
561 out['command'] = self.command
562 if self.read_only is not None:
563 out['read_only'] = self.read_only
564 if self.relative_cwd:
565 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000566 return out
567
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000568 @property
569 def isolate_filepath(self):
570 """Returns the absolute path of self.isolate_file."""
571 return os.path.normpath(
572 os.path.join(self.isolated_basedir, self.isolate_file))
573
574 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000575 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000576 def load(cls, data, isolated_basedir): # pylint: disable=W0221
577 """Special case loading to disallow different OS.
578
579 It is not possible to load a .isolated.state files from a different OS, this
580 file is saved in OS-specific format.
581 """
582 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400583 if data.get('OS') != sys.platform:
584 raise isolateserver.ConfigError('Unexpected OS %s', data.get('OS'))
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000585
586 # Converts human readable form back into the proper class type.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400587 algo = data.get('algo')
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000588 if not algo in isolateserver.SUPPORTED_ALGOS:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000589 raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000590 out.algo = isolateserver.SUPPORTED_ALGOS[algo]
591
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500592 # Refuse the load non-exact version, even minor difference. This is unlike
593 # isolateserver.load_isolated(). This is because .isolated.state could have
594 # changed significantly even in minor version difference.
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000595 if not re.match(r'^(\d+)\.(\d+)$', out.version):
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000596 raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500597 if out.version != isolateserver.ISOLATED_FILE_VERSION:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000598 raise isolateserver.ConfigError(
599 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000600
Marc-Antoine Ruel16ebc2e2014-02-13 15:39:15 -0500601 # The .isolate file must be valid. If it is not present anymore, zap the
602 # value as if it was not noted, so .isolate_file can safely be overriden
603 # later.
604 if out.isolate_file and not os.path.isfile(out.isolate_filepath):
605 out.isolate_file = None
606 if out.isolate_file:
607 # It could be absolute on Windows if the drive containing the .isolate and
608 # the drive containing the .isolated files differ, .e.g .isolate is on
609 # C:\\ and .isolated is on D:\\ .
610 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
611 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000612 return out
613
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000614 def flatten(self):
615 """Makes sure 'algo' is in human readable form."""
616 out = super(SavedState, self).flatten()
617 out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
618 return out
619
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000620 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500621 def dict_to_str(d):
622 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
623
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000624 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000625 out += ' command: %s\n' % self.command
626 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000627 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000628 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000629 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000630 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500631 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
632 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500633 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000634 return out
635
636
637class CompleteState(object):
638 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000639 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000640 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +0000641 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000642 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000643 # Contains the data to ease developer's use-case but that is not strictly
644 # necessary.
645 self.saved_state = saved_state
646
647 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000648 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000649 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000650 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000651 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000652 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000653 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000654 SavedState.load_file(
655 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000656
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500657 def load_isolate(
658 self, cwd, isolate_file, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500659 extra_variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000660 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000661 .isolate file.
662
663 Processes the loaded data, deduce root_dir, relative_cwd.
664 """
665 # Make sure to not depend on os.getcwd().
666 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000667 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000668 logging.info(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500669 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500670 cwd, isolate_file, path_variables, config_variables, extra_variables,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500671 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000672 relative_base_dir = os.path.dirname(isolate_file)
673
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500674 # Processes the variables.
675 path_variables = normalize_path_variables(
676 cwd, path_variables, relative_base_dir)
677 # Update the saved state.
678 self.saved_state.update(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500679 isolate_file, path_variables, config_variables, extra_variables)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500680 path_variables = self.saved_state.path_variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000681
682 with open(isolate_file, 'r') as f:
683 # At that point, variables are not replaced yet in command and infiles.
684 # infiles may contain directory entries and is in posix style.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500685 command, infiles, touched, read_only = (
686 isolate_format.load_isolate_for_config(
687 os.path.dirname(isolate_file), f.read(),
688 self.saved_state.config_variables))
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500689
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500690 total_variables = self.saved_state.path_variables.copy()
691 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500692 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500693 command = [
694 isolate_format.eval_variables(i, total_variables) for i in command
695 ]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500696
697 total_variables = self.saved_state.path_variables.copy()
698 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500699 infiles = [
700 isolate_format.eval_variables(f, total_variables) for f in infiles
701 ]
702 touched = [
703 isolate_format.eval_variables(f, total_variables) for f in touched
704 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000705 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +0000706 # form '../../foo/bar'. Note that path variables must be taken in account
707 # too, add them as if they were input files.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500708 root_dir = isolate_format.determine_root_dir(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500709 relative_base_dir, infiles + touched +
710 self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000711 # The relative directory is automatically determined by the relative path
712 # between root_dir and the directory containing the .isolate file,
713 # isolate_base_dir.
714 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500715 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000716 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500717 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500718 if not file_path.path_starts_with(
719 root_dir, os.path.join(relative_base_dir, v)):
720 raise isolateserver.MappingError(
721 'Path variable %s=%r points outside the inferred root directory %s'
722 % (k, v, root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000723 # Normalize the files based to root_dir. It is important to keep the
724 # trailing os.path.sep at that step.
725 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500726 file_path.relpath(
727 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000728 for f in infiles
729 ]
730 touched = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500731 file_path.relpath(
732 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000733 for f in touched
734 ]
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400735 follow_symlinks = sys.platform != 'win32'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000736 # Expand the directories by listing each file inside. Up to now, trailing
737 # os.path.sep must be kept. Do not expand 'touched'.
738 infiles = expand_directories_and_symlinks(
739 root_dir,
740 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +0000741 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000742 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +0000743 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000744
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000745 # If we ignore broken items then remove any missing touched items.
746 if ignore_broken_items:
747 original_touched_count = len(touched)
748 touched = [touch for touch in touched if os.path.exists(touch)]
749
750 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +0000751 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000752 len(touched) - original_touched_count)
753
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000754 # Finally, update the new data to be able to generate the foo.isolated file,
755 # the file that is used by run_isolated.py.
756 self.saved_state.update_isolated(
757 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000758 logging.debug(self)
759
maruel@chromium.org9268f042012-10-17 17:36:41 +0000760 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000761 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000762
maruel@chromium.org9268f042012-10-17 17:36:41 +0000763 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
764 file is tainted.
765
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500766 See isolateserver.process_input() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000767 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000768 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +0000769 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000770 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000771 else:
772 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500773 self.saved_state.files[infile] = isolateserver.process_input(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000774 filepath,
775 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000776 self.saved_state.read_only,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000777 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000778
779 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000780 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000781 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000782 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000783 self.isolated_filepath,
784 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500785 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000786 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000787 total_bytes = sum(
788 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000789 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000790 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000791 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000792 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000793 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500794 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000795
796 @property
797 def root_dir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000798 """Returns the absolute path of the root_dir to reference the .isolate file
799 via relative_cwd.
800
801 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
802 to isolate_filepath.
803 """
maruel@chromium.orgb253fb82012-10-16 21:44:48 +0000804 if not self.saved_state.isolate_file:
805 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000806 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000807 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000808 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000809 root_dir = isolate_dir
810 else:
maruel@chromium.org87557b92013-10-16 18:04:11 +0000811 if not isolate_dir.endswith(self.saved_state.relative_cwd):
812 raise ExecutionError(
813 ('Make sure the .isolate file is in the directory that will be '
814 'used as the relative directory. It is currently in %s and should '
815 'be in %s') % (isolate_dir, self.saved_state.relative_cwd))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000816 # Walk back back to the root directory.
817 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000818 return file_path.get_native_path_case(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000819
820 @property
821 def resultdir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000822 """Returns the absolute path containing the .isolated file.
823
824 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
825 path as the value.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000826 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000827 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000828
829 def __str__(self):
830 def indent(data, indent_length):
831 """Indents text."""
832 spacing = ' ' * indent_length
833 return ''.join(spacing + l for l in str(data).splitlines(True))
834
835 out = '%s(\n' % self.__class__.__name__
836 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000837 out += ' saved_state: %s)' % indent(self.saved_state, 2)
838 return out
839
840
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000841def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000842 """Loads a CompleteState.
843
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000844 This includes data from .isolate and .isolated.state files. Never reads the
845 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000846
847 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000848 options: Options instance generated with OptionParserIsolate. For either
849 options.isolate and options.isolated, if the value is set, it is an
850 absolute path.
851 cwd: base directory to be used when loading the .isolate file.
852 subdir: optional argument to only process file in the subdirectory, relative
853 to CompleteState.root_dir.
854 skip_update: Skip trying to load the .isolate file and processing the
855 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000856 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000857 assert not options.isolate or os.path.isabs(options.isolate)
858 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000859 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000860 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000861 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000862 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000863 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000864 else:
865 # Constructs a dummy object that cannot be saved. Useful for temporary
866 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000867 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000868
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000869 if not options.isolate:
870 if not complete_state.saved_state.isolate_file:
871 if not skip_update:
872 raise ExecutionError('A .isolate file is required.')
873 isolate = None
874 else:
875 isolate = complete_state.saved_state.isolate_filepath
876 else:
877 isolate = options.isolate
878 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500879 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000880 options.isolate, complete_state.saved_state.isolated_basedir)
881 if rel_isolate != complete_state.saved_state.isolate_file:
882 raise ExecutionError(
883 '%s and %s do not match.' % (
884 options.isolate, complete_state.saved_state.isolate_file))
885
886 if not skip_update:
887 # Then load the .isolate and expands directories.
888 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500889 cwd, isolate, options.path_variables, options.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500890 options.extra_variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000891
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000892 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +0000893 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000894 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500895 # This is tricky here. If it is a path, take it from the root_dir. If
896 # it is a variable, it must be keyed from the directory containing the
897 # .isolate file. So translate all variables first.
898 translated_path_variables = dict(
899 (k,
900 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
901 v)))
902 for k, v in complete_state.saved_state.path_variables.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500903 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000904 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000905
906 if not skip_update:
907 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000908 return complete_state
909
910
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000911def read_trace_as_isolate_dict(complete_state, trace_blacklist):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000912 """Reads a trace and returns the .isolate dictionary.
913
914 Returns exceptions during the log parsing so it can be re-raised.
915 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000916 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000917 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000918 if not os.path.isfile(logfile):
919 raise ExecutionError(
920 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
921 try:
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000922 data = api.parse_log(logfile, trace_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000923 exceptions = [i['exception'] for i in data if 'exception' in i]
924 results = (i['results'] for i in data if 'results' in i)
925 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
926 files = set(sum((result.existent for result in results_stripped), []))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500927 tracked, touched = isolate_format.split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000928 value = generate_isolate(
929 tracked,
930 [],
931 touched,
932 complete_state.root_dir,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500933 complete_state.saved_state.path_variables,
934 complete_state.saved_state.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500935 complete_state.saved_state.extra_variables,
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000936 complete_state.saved_state.relative_cwd,
937 trace_blacklist)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000938 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000939 except trace_inputs.TracingFailure, e:
940 raise ExecutionError(
941 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000942 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000943
944
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000945def merge(complete_state, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000946 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000947 value, exceptions = read_trace_as_isolate_dict(
948 complete_state, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000949
950 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000951 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000952 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000953 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500954 prev_config = isolate_format.load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000955 isolate_dir,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500956 isolate_format.eval_content(prev_content),
957 isolate_format.extract_comment(prev_content))
958 new_config = isolate_format.load_isolate_as_config(isolate_dir, value, '')
959 config = isolate_format.union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000960 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000961 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000962 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500963 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000964 if exceptions:
965 # It got an exception, raise the first one.
966 raise \
967 exceptions[0][0], \
968 exceptions[0][1], \
969 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000970
971
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500972def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
973 """Creates a isolated tree usable for test execution.
974
975 Returns the current working directory where the isolated command should be
976 started in.
977 """
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500978 # Forcibly copy when the tree has to be read only. Otherwise the inode is
979 # modified, and this cause real problems because the user's source tree
980 # becomes read only. On the other hand, the cost of doing file copy is huge.
981 if read_only not in (0, None):
982 action = run_isolated.COPY
983 else:
984 action = run_isolated.HARDLINK_WITH_FALLBACK
985
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500986 recreate_tree(
987 outdir=outdir,
988 indir=root_dir,
989 infiles=files,
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500990 action=action,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500991 as_hash=False)
992 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
993 if not os.path.isdir(cwd):
994 # It can happen when no files are mapped from the directory containing the
995 # .isolate file. But the directory must exist to be the current working
996 # directory.
997 os.makedirs(cwd)
998 run_isolated.change_tree_read_only(outdir, read_only)
999 return cwd
1000
1001
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001002def prepare_for_archival(options, cwd):
1003 """Loads the isolated file and create 'infiles' for archival."""
1004 complete_state = load_complete_state(
1005 options, cwd, options.subdir, False)
1006 # Make sure that complete_state isn't modified until save_files() is
1007 # called, because any changes made to it here will propagate to the files
1008 # created (which is probably not intended).
1009 complete_state.save_files()
1010
1011 infiles = complete_state.saved_state.files
1012 # Add all the .isolated files.
1013 isolated_hash = []
1014 isolated_files = [
1015 options.isolated,
1016 ] + complete_state.saved_state.child_isolated_files
1017 for item in isolated_files:
1018 item_path = os.path.join(
1019 os.path.dirname(complete_state.isolated_filepath), item)
1020 # Do not use isolateserver.hash_file() here because the file is
1021 # likely smallish (under 500kb) and its file size is needed.
1022 with open(item_path, 'rb') as f:
1023 content = f.read()
1024 isolated_hash.append(
1025 complete_state.saved_state.algo(content).hexdigest())
1026 isolated_metadata = {
1027 'h': isolated_hash[-1],
1028 's': len(content),
1029 'priority': '0'
1030 }
1031 infiles[item_path] = isolated_metadata
1032 return complete_state, infiles, isolated_hash
1033
1034
maruel@chromium.org29029882013-08-30 12:15:40 +00001035### Commands.
1036
1037
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001038def CMDarchive(parser, args):
1039 """Creates a .isolated file and uploads the tree to an isolate server.
maruel@chromium.org29029882013-08-30 12:15:40 +00001040
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001041 All the files listed in the .isolated file are put in the isolate server
1042 cache via isolateserver.py.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001043 """
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001044 add_subdir_option(parser)
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001045 isolateserver.add_isolate_server_options(parser, False)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001046 auth.add_auth_options(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001047 options, args = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001048 auth.process_auth_options(parser, options)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001049 isolateserver.process_isolate_server_options(parser, options)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001050 if args:
1051 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001052 cwd = os.getcwd()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001053 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001054 success = False
1055 try:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001056 complete_state, infiles, isolated_hash = prepare_for_archival(
1057 options, cwd)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001058 logging.info('Creating content addressed object store with %d item',
1059 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001060
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001061 isolateserver.upload_tree(
1062 base_url=options.isolate_server,
1063 indir=complete_state.root_dir,
1064 infiles=infiles,
1065 namespace=options.namespace)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001066 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001067 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001068 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001069 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001070 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001071 if not success and os.path.isfile(options.isolated):
1072 os.remove(options.isolated)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001073 return int(not success)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001074
1075
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001076def CMDcheck(parser, args):
1077 """Checks that all the inputs are present and generates .isolated."""
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001078 add_subdir_option(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001079 options, args = parser.parse_args(args)
1080 if args:
1081 parser.error('Unsupported argument: %s' % args)
1082
1083 complete_state = load_complete_state(
1084 options, os.getcwd(), options.subdir, False)
1085
1086 # Nothing is done specifically. Just store the result and state.
1087 complete_state.save_files()
1088 return 0
1089
1090
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001091def CMDhashtable(parser, args):
1092 """Creates a .isolated file and stores the contains in a directory.
1093
1094 All the files listed in the .isolated file are put in the directory with their
1095 sha-1 as their file name. When using an NFS/CIFS server, the files can then be
1096 shared accross slaves without an isolate server.
1097 """
1098 add_subdir_option(parser)
Marc-Antoine Ruel488ce8f2014-02-09 11:25:04 -05001099 isolateserver.add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001100 add_skip_refresh_option(parser)
1101 options, args = parser.parse_args(args)
1102 if args:
1103 parser.error('Unsupported argument: %s' % args)
1104 cwd = os.getcwd()
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001105 isolateserver.process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001106
1107 success = False
1108 try:
1109 complete_state, infiles, isolated_hash = prepare_for_archival(options, cwd)
1110 logging.info('Creating content addressed object store with %d item',
1111 len(infiles))
1112 if not os.path.isdir(options.outdir):
1113 os.makedirs(options.outdir)
1114
1115 # TODO(maruel): Make the files read-only?
1116 recreate_tree(
1117 outdir=options.outdir,
1118 indir=complete_state.root_dir,
1119 infiles=infiles,
1120 action=run_isolated.HARDLINK_WITH_FALLBACK,
1121 as_hash=True)
1122 success = True
1123 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
1124 finally:
1125 # If the command failed, delete the .isolated file if it exists. This is
1126 # important so no stale swarm job is executed.
1127 if not success and os.path.isfile(options.isolated):
1128 os.remove(options.isolated)
1129 return int(not success)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001130
1131
maruel@chromium.orge5322512013-08-19 20:17:57 +00001132def CMDmerge(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001133 """Reads and merges the data from the trace back into the original .isolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001134 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001135 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001136 add_trace_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001137 options, args = parser.parse_args(args)
1138 if args:
1139 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001140
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001141 complete_state = load_complete_state(options, os.getcwd(), None, False)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001142 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001143 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001144 return 0
1145
1146
maruel@chromium.orge5322512013-08-19 20:17:57 +00001147def CMDread(parser, args):
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001148 """Reads the trace file generated with command 'trace'."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001149 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001150 add_trace_option(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001151 add_skip_refresh_option(parser)
maruel@chromium.org29029882013-08-30 12:15:40 +00001152 parser.add_option(
1153 '-m', '--merge', action='store_true',
1154 help='merge the results back in the .isolate file instead of printing')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001155 options, args = parser.parse_args(args)
1156 if args:
1157 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001158
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001159 complete_state = load_complete_state(
1160 options, os.getcwd(), None, options.skip_refresh)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001161 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001162 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
maruel@chromium.org29029882013-08-30 12:15:40 +00001163 if options.merge:
1164 merge(complete_state, blacklist)
1165 else:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001166 isolate_format.pretty_print(value, sys.stdout)
maruel@chromium.org29029882013-08-30 12:15:40 +00001167
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001168 if exceptions:
1169 # It got an exception, raise the first one.
1170 raise \
1171 exceptions[0][0], \
1172 exceptions[0][1], \
1173 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001174 return 0
1175
1176
maruel@chromium.orge5322512013-08-19 20:17:57 +00001177def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001178 """Creates a directory with all the dependencies mapped into it.
1179
1180 Useful to test manually why a test is failing. The target executable is not
1181 run.
1182 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001183 parser.require_isolated = False
Marc-Antoine Ruel488ce8f2014-02-09 11:25:04 -05001184 isolateserver.add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001185 add_skip_refresh_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001186 options, args = parser.parse_args(args)
1187 if args:
1188 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001189 cwd = os.getcwd()
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001190 isolateserver.process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001191 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001192
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001193 if not os.path.isdir(options.outdir):
1194 os.makedirs(options.outdir)
1195 print('Remapping into %s' % options.outdir)
1196 if os.listdir(options.outdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001197 raise ExecutionError('Can\'t remap in a non-empty directory')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001198
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001199 create_isolate_tree(
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001200 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001201 complete_state.saved_state.relative_cwd,
1202 complete_state.saved_state.read_only)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001203 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001204 complete_state.save_files()
1205 return 0
1206
1207
maruel@chromium.orge5322512013-08-19 20:17:57 +00001208def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001209 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001210 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001211 options, args = parser.parse_args(args)
1212 if args:
1213 parser.error('Unsupported argument: %s' % args)
1214
1215 if options.isolated:
1216 # Load the previous state if it was present. Namely, "foo.isolated.state".
1217 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001218 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001219 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001220 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001221 if not isolate:
maruel@chromium.org29029882013-08-30 12:15:40 +00001222 parser.error('--isolate is required.')
1223
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001224 with open(isolate, 'r') as f:
1225 content = f.read()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001226 config = isolate_format.load_isolate_as_config(
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001227 os.path.dirname(os.path.abspath(isolate)),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001228 isolate_format.eval_content(content),
1229 isolate_format.extract_comment(content))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001230 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001231 print('Updating %s' % isolate)
1232 with open(isolate, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001233 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001234 return 0
1235
1236
maruel@chromium.org29029882013-08-30 12:15:40 +00001237@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001238def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001239 """Runs the test executable in an isolated (temporary) directory.
1240
1241 All the dependencies are mapped into the temporary directory and the
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001242 directory is cleaned up after the target exits.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001243
maruel@chromium.org29029882013-08-30 12:15:40 +00001244 Argument processing stops at -- and these arguments are appended to the
1245 command line of the target to run. For example, use:
1246 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001247 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001248 parser.require_isolated = False
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001249 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001250 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001251
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001252 complete_state = load_complete_state(
1253 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001254 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001255 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001256 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001257 cmd = tools.fix_python_path(cmd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001258
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001259 outdir = run_isolated.make_temp_dir(
1260 'isolate-%s' % datetime.date.today(),
1261 os.path.dirname(complete_state.root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001262 try:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -05001263 # TODO(maruel): Use run_isolated.run_tha_test().
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001264 cwd = create_isolate_tree(
1265 outdir, complete_state.root_dir, complete_state.saved_state.files,
1266 complete_state.saved_state.relative_cwd,
1267 complete_state.saved_state.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001268 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1269 result = subprocess.call(cmd, cwd=cwd)
1270 finally:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001271 run_isolated.rmtree(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001272
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001273 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001274 complete_state.save_files()
1275 return result
1276
1277
maruel@chromium.org29029882013-08-30 12:15:40 +00001278@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001279def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001280 """Traces the target using trace_inputs.py.
1281
1282 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001283 its child processes access. Then the 'merge' command can be used to generate
1284 an updated .isolate file out of it or the 'read' command to print it out to
1285 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001286
maruel@chromium.org29029882013-08-30 12:15:40 +00001287 Argument processing stops at -- and these arguments are appended to the
1288 command line of the target to run. For example, use:
1289 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001290 """
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001291 add_trace_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001292 parser.add_option(
1293 '-m', '--merge', action='store_true',
1294 help='After tracing, merge the results back in the .isolate file')
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001295 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001296 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001297
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001298 complete_state = load_complete_state(
1299 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001300 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001301 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001302 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001303 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001304 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001305 unicode(complete_state.root_dir),
1306 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001307 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1308 if not os.path.isfile(cmd[0]):
1309 raise ExecutionError(
1310 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001311 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1312 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001313 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001314 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001315 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001316 try:
1317 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001318 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001319 cmd,
1320 cwd,
1321 'default',
1322 True)
1323 except trace_inputs.TracingFailure, e:
1324 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1325
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001326 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001327 logging.error(
1328 'Tracer exited with %d, which means the tests probably failed so the '
1329 'trace is probably incomplete.', result)
1330 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001331
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001332 complete_state.save_files()
1333
1334 if options.merge:
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001335 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001336 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001337
1338 return result
1339
1340
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001341def _process_variable_arg(option, opt, _value, parser):
1342 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +00001343 if not parser.rargs:
1344 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001345 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001346 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001347 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001348 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001349 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001350 else:
1351 if not parser.rargs:
1352 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001353 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001354 v = parser.rargs.pop(0)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001355 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001356 raise optparse.OptionValueError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001357 'Variable \'%s\' doesn\'t respect format \'%s\'' %
1358 (k, isolate_format.VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -05001359 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001360
1361
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001362def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001363 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001364 parser.add_option(
1365 '-s', '--isolated',
1366 metavar='FILE',
1367 help='.isolated file to generate or read')
1368 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001369 parser.add_option(
1370 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001371 dest='isolated',
1372 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001373 is_win = sys.platform in ('win32', 'cygwin')
1374 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001375 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001376 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001377 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001378 # - configuration variables that are to be used in deducing the matrix to
1379 # reduce.
1380 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001381 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001382 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00001383 action='callback',
1384 callback=_process_variable_arg,
Marc-Antoine Ruel05199462014-03-13 15:40:48 -04001385 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001386 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001387 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001388 help='Config variables are used to determine which conditions should be '
1389 'matched when loading a .isolate file, default: %default. '
1390 'All 3 kinds of variables are persistent accross calls, they are '
1391 'saved inside <.isolated>.state')
1392 parser.add_option(
1393 '--path-variable',
1394 action='callback',
1395 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001396 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001397 dest='path_variables',
1398 metavar='FOO BAR',
1399 help='Path variables are used to replace file paths when loading a '
1400 '.isolate file, default: %default')
1401 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001402 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001403 action='callback',
1404 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001405 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1406 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001407 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001408 help='Extraneous variables are replaced on the \'command\' entry and on '
1409 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001410
1411
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001412def add_subdir_option(parser):
1413 parser.add_option(
1414 '--subdir',
1415 help='Filters to a subdirectory. Its behavior changes depending if it '
1416 'is a relative path as a string or as a path variable. Path '
1417 'variables are always keyed from the directory containing the '
1418 '.isolate file. Anything else is keyed on the root directory.')
1419
1420
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001421def add_trace_option(parser):
1422 """Adds --trace-blacklist to the parser."""
1423 parser.add_option(
1424 '--trace-blacklist',
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001425 action='append', default=list(isolateserver.DEFAULT_BLACKLIST),
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001426 help='List of regexp to use as blacklist filter for files to consider '
1427 'important, not to be confused with --blacklist which blacklists '
1428 'test case.')
1429
1430
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001431def add_skip_refresh_option(parser):
1432 parser.add_option(
1433 '--skip-refresh', action='store_true',
1434 help='Skip reading .isolate file and do not refresh the hash of '
1435 'dependencies')
1436
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001437
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001438def parse_isolated_option(parser, options, cwd, require_isolated):
1439 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001440 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001441 options.isolated = os.path.normpath(
1442 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001443 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001444 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001445 if options.isolated and not options.isolated.endswith('.isolated'):
1446 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001447
1448
1449def parse_variable_option(options):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001450 """Processes all the --<foo>-variable flags."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001451 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
1452 # but it wouldn't be backward compatible.
1453 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001454 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001455 try:
1456 return int(s)
1457 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001458 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001459 options.config_variables = dict(
1460 (k, try_make_int(v)) for k, v in options.config_variables)
1461 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001462 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001463
1464
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001465class OptionParserIsolate(tools.OptionParserWithLogging):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001466 """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
1467 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001468 # Set it to False if it is not required, e.g. it can be passed on but do not
1469 # fail if not given.
1470 require_isolated = True
1471
1472 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001473 tools.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00001474 self,
1475 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1476 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001477 group = optparse.OptionGroup(self, "Common options")
1478 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001479 '-i', '--isolate',
1480 metavar='FILE',
1481 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001482 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001483 group.add_option(
csharp@chromium.org01856802012-11-12 17:48:13 +00001484 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00001485 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1486 help='Indicates that invalid entries in the isolated file to be '
1487 'only be logged and not stop processing. Defaults to True if '
1488 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001489 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001490
1491 def parse_args(self, *args, **kwargs):
1492 """Makes sure the paths make sense.
1493
1494 On Windows, / and \ are often mixed together in a path.
1495 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001496 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001497 self, *args, **kwargs)
1498 if not self.allow_interspersed_args and args:
1499 self.error('Unsupported argument: %s' % args)
1500
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001501 cwd = file_path.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001502 parse_isolated_option(self, options, cwd, self.require_isolated)
1503 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001504
1505 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001506 # TODO(maruel): Work with non-ASCII.
1507 # The path must be in native path case for tracing purposes.
1508 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1509 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001510 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001511
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001512 return options, args
1513
1514
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001515def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00001516 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001517 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00001518 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001519 except Exception as e:
1520 tools.report_error(e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001521 return 1
1522
1523
1524if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00001525 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001526 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001527 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001528 sys.exit(main(sys.argv[1:]))