blob: 829d23b6187f9cbb078184f75a9094e408033f22 [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
31# Import here directly so isolate is easier to use as a library.
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000032from run_isolated import get_flavor
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000033
maruel@chromium.orge5322512013-08-19 20:17:57 +000034from third_party import colorama
35from third_party.depot_tools import fix_encoding
36from third_party.depot_tools import subcommand
37
maruel@chromium.org561d4b22013-09-26 21:08:08 +000038from utils import file_path
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000039from utils import tools
40
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000041
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050042__version__ = '0.3.1'
maruel@chromium.org3d671992013-08-20 00:38:27 +000043
44
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000045class ExecutionError(Exception):
46 """A generic error occurred."""
47 def __str__(self):
48 return self.args[0]
49
50
51### Path handling code.
52
53
csharp@chromium.org01856802012-11-12 17:48:13 +000054def expand_directories_and_symlinks(indir, infiles, blacklist,
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +000055 follow_symlinks, ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000056 """Expands the directories and the symlinks, applies the blacklist and
57 verifies files exist.
58
59 Files are specified in os native path separator.
60 """
61 outfiles = []
62 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +000063 try:
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -050064 outfiles.extend(
65 isolateserver.expand_directory_and_symlink(
66 indir, relfile, blacklist, follow_symlinks))
maruel@chromium.org9958e4a2013-09-17 00:01:48 +000067 except isolateserver.MappingError as e:
csharp@chromium.org01856802012-11-12 17:48:13 +000068 if ignore_broken_items:
69 logging.info('warning: %s', e)
70 else:
71 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000072 return outfiles
73
74
maruel@chromium.org7b844a62013-09-17 13:04:59 +000075def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000076 """Creates a new tree with only the input files in it.
77
78 Arguments:
79 outdir: Output directory to create the files in.
80 indir: Root directory the infiles are based in.
81 infiles: dict of files to map from |indir| to |outdir|.
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000082 action: One of accepted action of run_isolated.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +000083 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000084 """
85 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +000086 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
87 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000088
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000089 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000090 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000091 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000092 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000093
94 for relfile, metadata in infiles.iteritems():
95 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +000096 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000097 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000098 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000099 # Skip links when storing a hashtable.
100 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000101 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000102 if os.path.isfile(outfile):
103 # Just do a quick check that the file size matches. No need to stat()
104 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000105 if not 's' in metadata:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000106 raise isolateserver.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000107 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000108 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000109 continue
110 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000111 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000112 os.remove(outfile)
113 else:
114 outfile = os.path.join(outdir, relfile)
115 outsubdir = os.path.dirname(outfile)
116 if not os.path.isdir(outsubdir):
117 os.makedirs(outsubdir)
118
119 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000120 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000121 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000122 if 'l' in metadata:
123 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000124 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000125 # symlink doesn't exist on Windows.
126 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000127 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000128 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000129
130
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000131### Variable stuff.
132
133
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500134def _normalize_path_variable(cwd, relative_base_dir, key, value):
135 """Normalizes a path variable into a relative directory.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500136 """
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500137 # Variables could contain / or \ on windows. Always normalize to
138 # os.path.sep.
139 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
140 normalized = file_path.get_native_path_case(os.path.normpath(x))
141 if not os.path.isdir(normalized):
142 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
143
144 # All variables are relative to the .isolate file.
145 normalized = os.path.relpath(normalized, relative_base_dir)
146 logging.debug(
147 'Translated variable %s from %s to %s', key, value, normalized)
148 return normalized
149
150
151def normalize_path_variables(cwd, path_variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000152 """Processes path variables as a special case and returns a copy of the dict.
153
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000154 For each 'path' variable: first normalizes it based on |cwd|, verifies it
155 exists then sets it as relative to relative_base_dir.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500156 """
157 logging.info(
158 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
159 relative_base_dir)
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -0500160 assert isinstance(cwd, unicode), cwd
161 assert isinstance(relative_base_dir, unicode), relative_base_dir
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500162 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
163 return dict(
164 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
165 for k, v in path_variables.iteritems())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000166
167
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500168### Internal state files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000169
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500170
171def isolatedfile_to_state(filename):
172 """For a '.isolate' file, returns the path to the saved '.state' file."""
173 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000174
175
176def classify_files(root_dir, tracked, untracked):
177 """Converts the list of files into a .isolate 'variables' dictionary.
178
179 Arguments:
180 - tracked: list of files names to generate a dictionary out of that should
181 probably be tracked.
182 - untracked: list of files names that must not be tracked.
183 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000184 new_tracked = []
185 new_untracked = list(untracked)
186
187 def should_be_tracked(filepath):
188 """Returns True if it is a file without whitespace in a non-optional
189 directory that has no symlink in its path.
190 """
191 if filepath.endswith('/'):
192 return False
193 if ' ' in filepath:
194 return False
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000195 # Look if any element in the path is a symlink.
196 split = filepath.split('/')
197 for i in range(len(split)):
198 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
199 return False
200 return True
201
202 for filepath in sorted(tracked):
203 if should_be_tracked(filepath):
204 new_tracked.append(filepath)
205 else:
206 # Anything else.
207 new_untracked.append(filepath)
208
209 variables = {}
210 if new_tracked:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500211 variables[isolate_format.KEY_TRACKED] = sorted(new_tracked)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000212 if new_untracked:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500213 variables[isolate_format.KEY_UNTRACKED] = sorted(new_untracked)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000214 return variables
215
216
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500217def chromium_fix(f, variables):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500218 """Fixes an isolate dependency with Chromium-specific fixes."""
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000219 # Blacklist logs and other unimportant files.
Marc-Antoine Ruel4196cfc2014-02-21 15:43:18 -0500220 # - 'First Run' is not created by the compile but by the test itself.
221 # - Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
222 # separator at this point.
223 if (re.match(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$', f) or
224 f == '<(PRODUCT_DIR)/First Run'):
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000225 logging.debug('Ignoring %s', f)
226 return None
227
maruel@chromium.org7650e422012-11-16 21:56:42 +0000228 EXECUTABLE = re.compile(
229 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500230 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
maruel@chromium.org7650e422012-11-16 21:56:42 +0000231 r'$')
232 match = EXECUTABLE.match(f)
233 if match:
234 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
235
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000236 if sys.platform == 'darwin':
237 # On OSX, the name of the output is dependent on gyp define, it can be
238 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
239 # Framework.framework'. Furthermore, they are versioned with a gyp
240 # variable. To lower the complexity of the .isolate file, remove all the
241 # individual entries that show up under any of the 4 entries and replace
242 # them with the directory itself. Overall, this results in a bit more
243 # files than strictly necessary.
244 OSX_BUNDLES = (
245 '<(PRODUCT_DIR)/Chromium Framework.framework/',
246 '<(PRODUCT_DIR)/Chromium.app/',
247 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
248 '<(PRODUCT_DIR)/Google Chrome.app/',
249 )
250 for prefix in OSX_BUNDLES:
251 if f.startswith(prefix):
252 # Note this result in duplicate values, so the a set() must be used to
253 # remove duplicates.
254 return prefix
255 return f
256
257
258def generate_simplified(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500259 tracked, untracked, touched, root_dir, path_variables, extra_variables,
260 relative_cwd, trace_blacklist):
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000261 """Generates a clean and complete .isolate 'variables' dictionary.
262
263 Cleans up and extracts only files from within root_dir then processes
264 variables and relative_cwd.
265 """
266 root_dir = os.path.realpath(root_dir)
267 logging.info(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500268 'generate_simplified(%d files, %s, %s, %s, %s)' %
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000269 (len(tracked) + len(untracked) + len(touched),
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500270 root_dir, path_variables, extra_variables, relative_cwd))
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000271
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000272 # Preparation work.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500273 relative_cwd = file_path.cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000274 assert not os.path.isabs(relative_cwd), relative_cwd
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500275
276 # Normalizes to posix path. .isolate files are using posix paths on all OSes
277 # for coherency.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000278 path_variables = dict(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500279 (k, v.replace(os.path.sep, '/')) for k, v in path_variables.iteritems())
280 # Contains normalized path_variables plus extra_variables.
281 total_variables = path_variables.copy()
282 total_variables.update(extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000283
284 # Actual work: Process the files.
285 # TODO(maruel): if all the files in a directory are in part tracked and in
286 # part untracked, the directory will not be extracted. Tracked files should be
287 # 'promoted' to be untracked as needed.
288 tracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000289 root_dir, tracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000290 untracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000291 root_dir, untracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000292 # touched is not compressed, otherwise it would result in files to be archived
293 # that we don't need.
294
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000295 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000296 def fix(f):
297 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000298 # Important, GYP stores the files with / and not \.
299 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000300 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000301 # If it's not already a variable.
302 if not f.startswith('<'):
303 # relative_cwd is usually the directory containing the gyp file. It may be
304 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000305 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000306 # Convert the whole thing to / since it's isolate's speak.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500307 f = file_path.posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000308 posixpath.join(root_dir_posix, f),
309 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000310
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500311 # Use the longest value first.
312 for key, value in sorted(
313 path_variables.iteritems(), key=lambda x: -len(x[1])):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500314 if f.startswith(value):
315 f = '<(%s)%s' % (key, f[len(value):])
316 logging.debug('Converted to %s' % f)
317 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000318 return f
319
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000320 def fix_all(items):
321 """Reduces the items to convert variables, removes unneeded items, apply
322 chromium-specific fixes and only return unique items.
323 """
324 variables_converted = (fix(f.path) for f in items)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500325 chromium_fixed = (
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500326 chromium_fix(f, total_variables) for f in variables_converted)
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000327 return set(f for f in chromium_fixed if f)
328
329 tracked = fix_all(tracked)
330 untracked = fix_all(untracked)
331 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000332 out = classify_files(root_dir, tracked, untracked)
333 if touched:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500334 out[isolate_format.KEY_TOUCHED] = sorted(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000335 return out
336
337
338def generate_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500339 tracked, untracked, touched, root_dir, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500340 extra_variables, relative_cwd, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000341 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000342 dependencies = generate_simplified(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500343 tracked, untracked, touched, root_dir, path_variables, extra_variables,
344 relative_cwd, trace_blacklist)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000345 config_variable_names, config_values = zip(
346 *sorted(config_variables.iteritems()))
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500347 out = isolate_format.Configs(None, config_variable_names)
Marc-Antoine Ruel8170f492014-03-13 15:26:56 -0400348 out.set_config(config_values, isolate_format.ConfigSettings(dependencies))
benrg@chromium.org609b7982013-02-07 16:44:46 +0000349 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000350
351
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500352def chromium_save_isolated(isolated, data, path_variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000353 """Writes one or many .isolated files.
354
355 This slightly increases the cold cache cost but greatly reduce the warm cache
356 cost by splitting low-churn files off the master .isolated file. It also
357 reduces overall isolateserver memcache consumption.
358 """
359 slaves = []
360
361 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000362 new_slave = {
363 'algo': data['algo'],
364 'files': {},
365 'os': data['os'],
366 'version': data['version'],
367 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000368 for f in data['files'].keys():
369 if f.startswith(prefix):
370 new_slave['files'][f] = data['files'].pop(f)
371 if new_slave['files']:
372 slaves.append(new_slave)
373
374 # Split test/data/ in its own .isolated file.
375 extract_into_included_isolated(os.path.join('test', 'data', ''))
376
377 # Split everything out of PRODUCT_DIR in its own .isolated file.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500378 if path_variables.get('PRODUCT_DIR'):
379 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000380
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000381 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000382 for index, f in enumerate(slaves):
383 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500384 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000385 data.setdefault('includes', []).append(
386 isolateserver.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000387 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000388
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500389 files.extend(isolateserver.save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000390 return files
391
392
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000393class Flattenable(object):
394 """Represents data that can be represented as a json file."""
395 MEMBERS = ()
396
397 def flatten(self):
398 """Returns a json-serializable version of itself.
399
400 Skips None entries.
401 """
402 items = ((member, getattr(self, member)) for member in self.MEMBERS)
403 return dict((member, value) for member, value in items if value is not None)
404
405 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000406 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000407 """Loads a flattened version."""
408 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000409 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000410 for member in out.MEMBERS:
411 if member in data:
412 # Access to a protected member XXX of a client class
413 # pylint: disable=W0212
414 out._load_member(member, data.pop(member))
415 if data:
416 raise ValueError(
417 'Found unexpected entry %s while constructing an object %s' %
418 (data, cls.__name__), data, cls.__name__)
419 return out
420
421 def _load_member(self, member, value):
422 """Loads a member into self."""
423 setattr(self, member, value)
424
425 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000426 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000427 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000428 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500429 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000430 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000431 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000432 # On failure, loads the default instance.
433 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000434 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000435 return out
436
437
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000438class SavedState(Flattenable):
439 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000440
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000441 This file caches the items calculated by this script and is used to increase
442 the performance of the script. This file is not loaded by run_isolated.py.
443 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000444
445 It is important to note that the 'files' dict keys are using native OS path
446 separator instead of '/' used in .isolate file.
447 """
448 MEMBERS = (
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.
493 self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500494 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000495 self.command = []
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500496 self.config_variables = {}
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500497 self.extra_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000498 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000499 self.isolate_file = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500500 self.path_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000501 self.read_only = None
502 self.relative_cwd = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500503 self.version = isolateserver.ISOLATED_FILE_VERSION
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000504
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500505 def update(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500506 self, isolate_file, path_variables, config_variables, extra_variables):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000507 """Updates the saved state with new data to keep GYP variables and internal
508 reference to the original .isolate file.
509 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +0000510 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000511 # Convert back to a relative path. On Windows, if the isolate and
512 # isolated files are on different drives, isolate_file will stay an absolute
513 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500514 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000515
516 # The same .isolate file should always be used to generate the .isolated and
517 # .isolated.state.
518 assert isolate_file == self.isolate_file or not self.isolate_file, (
519 isolate_file, self.isolate_file)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500520 self.config_variables.update(config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500521 self.extra_variables.update(extra_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000522 self.isolate_file = isolate_file
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500523 self.path_variables.update(path_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000524
525 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
526 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000527
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000528 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000529 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000530 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000531 self.command = command
532 # Add new files.
533 for f in infiles:
534 self.files.setdefault(f, {})
535 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000536 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000537 # Prune extraneous files that are not a dependency anymore.
538 for f in set(self.files).difference(set(infiles).union(touched)):
539 del self.files[f]
540 if read_only is not None:
541 self.read_only = read_only
542 self.relative_cwd = relative_cwd
543
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000544 def to_isolated(self):
545 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000546
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000547 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000548 """
549 def strip(data):
550 """Returns a 'files' entry with only the whitelisted keys."""
551 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
552
553 out = {
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000554 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000555 'files': dict(
556 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000557 'version': self.version,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000558 }
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500559 if self.config_variables.get('OS'):
560 out['os'] = self.config_variables['OS']
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000561 if self.command:
562 out['command'] = self.command
563 if self.read_only is not None:
564 out['read_only'] = self.read_only
565 if self.relative_cwd:
566 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000567 return out
568
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000569 @property
570 def isolate_filepath(self):
571 """Returns the absolute path of self.isolate_file."""
572 return os.path.normpath(
573 os.path.join(self.isolated_basedir, self.isolate_file))
574
575 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000576 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000577 def load(cls, data, isolated_basedir): # pylint: disable=W0221
578 """Special case loading to disallow different OS.
579
580 It is not possible to load a .isolated.state files from a different OS, this
581 file is saved in OS-specific format.
582 """
583 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500584 if data.get('os'):
585 out.config_variables['OS'] = data['os']
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000586
587 # Converts human readable form back into the proper class type.
588 algo = data.get('algo', 'sha-1')
589 if not algo in isolateserver.SUPPORTED_ALGOS:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000590 raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000591 out.algo = isolateserver.SUPPORTED_ALGOS[algo]
592
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500593 # Refuse the load non-exact version, even minor difference. This is unlike
594 # isolateserver.load_isolated(). This is because .isolated.state could have
595 # changed significantly even in minor version difference.
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000596 if not re.match(r'^(\d+)\.(\d+)$', out.version):
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000597 raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500598 if out.version != isolateserver.ISOLATED_FILE_VERSION:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000599 raise isolateserver.ConfigError(
600 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000601
Marc-Antoine Ruel16ebc2e2014-02-13 15:39:15 -0500602 # The .isolate file must be valid. If it is not present anymore, zap the
603 # value as if it was not noted, so .isolate_file can safely be overriden
604 # later.
605 if out.isolate_file and not os.path.isfile(out.isolate_filepath):
606 out.isolate_file = None
607 if out.isolate_file:
608 # It could be absolute on Windows if the drive containing the .isolate and
609 # the drive containing the .isolated files differ, .e.g .isolate is on
610 # C:\\ and .isolated is on D:\\ .
611 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
612 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000613 return out
614
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000615 def flatten(self):
616 """Makes sure 'algo' is in human readable form."""
617 out = super(SavedState, self).flatten()
618 out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
619 return out
620
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000621 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500622 def dict_to_str(d):
623 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
624
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000625 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000626 out += ' command: %s\n' % self.command
627 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000628 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000629 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000630 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000631 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500632 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
633 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500634 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000635 return out
636
637
638class CompleteState(object):
639 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000640 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000641 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +0000642 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000643 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000644 # Contains the data to ease developer's use-case but that is not strictly
645 # necessary.
646 self.saved_state = saved_state
647
648 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000649 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000650 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000651 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000652 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000653 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000654 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000655 SavedState.load_file(
656 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000657
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500658 def load_isolate(
659 self, cwd, isolate_file, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500660 extra_variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000661 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000662 .isolate file.
663
664 Processes the loaded data, deduce root_dir, relative_cwd.
665 """
666 # Make sure to not depend on os.getcwd().
667 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000668 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000669 logging.info(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500670 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500671 cwd, isolate_file, path_variables, config_variables, extra_variables,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500672 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000673 relative_base_dir = os.path.dirname(isolate_file)
674
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500675 # Processes the variables.
676 path_variables = normalize_path_variables(
677 cwd, path_variables, relative_base_dir)
678 # Update the saved state.
679 self.saved_state.update(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500680 isolate_file, path_variables, config_variables, extra_variables)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500681 path_variables = self.saved_state.path_variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000682
683 with open(isolate_file, 'r') as f:
684 # At that point, variables are not replaced yet in command and infiles.
685 # infiles may contain directory entries and is in posix style.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500686 command, infiles, touched, read_only = (
687 isolate_format.load_isolate_for_config(
688 os.path.dirname(isolate_file), f.read(),
689 self.saved_state.config_variables))
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500690
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500691 total_variables = self.saved_state.path_variables.copy()
692 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500693 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500694 command = [
695 isolate_format.eval_variables(i, total_variables) for i in command
696 ]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500697
698 total_variables = self.saved_state.path_variables.copy()
699 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500700 infiles = [
701 isolate_format.eval_variables(f, total_variables) for f in infiles
702 ]
703 touched = [
704 isolate_format.eval_variables(f, total_variables) for f in touched
705 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000706 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +0000707 # form '../../foo/bar'. Note that path variables must be taken in account
708 # too, add them as if they were input files.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500709 root_dir = isolate_format.determine_root_dir(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500710 relative_base_dir, infiles + touched +
711 self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000712 # The relative directory is automatically determined by the relative path
713 # between root_dir and the directory containing the .isolate file,
714 # isolate_base_dir.
715 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500716 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000717 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500718 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500719 if not file_path.path_starts_with(
720 root_dir, os.path.join(relative_base_dir, v)):
721 raise isolateserver.MappingError(
722 'Path variable %s=%r points outside the inferred root directory %s'
723 % (k, v, root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000724 # Normalize the files based to root_dir. It is important to keep the
725 # trailing os.path.sep at that step.
726 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500727 file_path.relpath(
728 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000729 for f in infiles
730 ]
731 touched = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500732 file_path.relpath(
733 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000734 for f in touched
735 ]
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500736 follow_symlinks = config_variables['OS'] != 'win'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000737 # Expand the directories by listing each file inside. Up to now, trailing
738 # os.path.sep must be kept. Do not expand 'touched'.
739 infiles = expand_directories_and_symlinks(
740 root_dir,
741 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +0000742 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000743 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +0000744 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000745
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000746 # If we ignore broken items then remove any missing touched items.
747 if ignore_broken_items:
748 original_touched_count = len(touched)
749 touched = [touch for touch in touched if os.path.exists(touch)]
750
751 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +0000752 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000753 len(touched) - original_touched_count)
754
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000755 # Finally, update the new data to be able to generate the foo.isolated file,
756 # the file that is used by run_isolated.py.
757 self.saved_state.update_isolated(
758 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000759 logging.debug(self)
760
maruel@chromium.org9268f042012-10-17 17:36:41 +0000761 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000762 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000763
maruel@chromium.org9268f042012-10-17 17:36:41 +0000764 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
765 file is tainted.
766
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500767 See isolateserver.process_input() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000768 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000769 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +0000770 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000771 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000772 else:
773 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500774 self.saved_state.files[infile] = isolateserver.process_input(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000775 filepath,
776 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000777 self.saved_state.read_only,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500778 self.saved_state.config_variables['OS'],
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000779 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000780
781 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000782 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000783 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000784 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000785 self.isolated_filepath,
786 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500787 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000788 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000789 total_bytes = sum(
790 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000791 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000792 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000793 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000794 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000795 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500796 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000797
798 @property
799 def root_dir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000800 """Returns the absolute path of the root_dir to reference the .isolate file
801 via relative_cwd.
802
803 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
804 to isolate_filepath.
805 """
maruel@chromium.orgb253fb82012-10-16 21:44:48 +0000806 if not self.saved_state.isolate_file:
807 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000808 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000809 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000810 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000811 root_dir = isolate_dir
812 else:
maruel@chromium.org87557b92013-10-16 18:04:11 +0000813 if not isolate_dir.endswith(self.saved_state.relative_cwd):
814 raise ExecutionError(
815 ('Make sure the .isolate file is in the directory that will be '
816 'used as the relative directory. It is currently in %s and should '
817 'be in %s') % (isolate_dir, self.saved_state.relative_cwd))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000818 # Walk back back to the root directory.
819 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000820 return file_path.get_native_path_case(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000821
822 @property
823 def resultdir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000824 """Returns the absolute path containing the .isolated file.
825
826 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
827 path as the value.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000828 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000829 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000830
831 def __str__(self):
832 def indent(data, indent_length):
833 """Indents text."""
834 spacing = ' ' * indent_length
835 return ''.join(spacing + l for l in str(data).splitlines(True))
836
837 out = '%s(\n' % self.__class__.__name__
838 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000839 out += ' saved_state: %s)' % indent(self.saved_state, 2)
840 return out
841
842
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000843def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000844 """Loads a CompleteState.
845
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000846 This includes data from .isolate and .isolated.state files. Never reads the
847 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000848
849 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000850 options: Options instance generated with OptionParserIsolate. For either
851 options.isolate and options.isolated, if the value is set, it is an
852 absolute path.
853 cwd: base directory to be used when loading the .isolate file.
854 subdir: optional argument to only process file in the subdirectory, relative
855 to CompleteState.root_dir.
856 skip_update: Skip trying to load the .isolate file and processing the
857 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000858 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000859 assert not options.isolate or os.path.isabs(options.isolate)
860 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000861 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000862 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000863 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000864 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000865 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000866 else:
867 # Constructs a dummy object that cannot be saved. Useful for temporary
868 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000869 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000870
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000871 if not options.isolate:
872 if not complete_state.saved_state.isolate_file:
873 if not skip_update:
874 raise ExecutionError('A .isolate file is required.')
875 isolate = None
876 else:
877 isolate = complete_state.saved_state.isolate_filepath
878 else:
879 isolate = options.isolate
880 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500881 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000882 options.isolate, complete_state.saved_state.isolated_basedir)
883 if rel_isolate != complete_state.saved_state.isolate_file:
884 raise ExecutionError(
885 '%s and %s do not match.' % (
886 options.isolate, complete_state.saved_state.isolate_file))
887
888 if not skip_update:
889 # Then load the .isolate and expands directories.
890 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500891 cwd, isolate, options.path_variables, options.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500892 options.extra_variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000893
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000894 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +0000895 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000896 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500897 # This is tricky here. If it is a path, take it from the root_dir. If
898 # it is a variable, it must be keyed from the directory containing the
899 # .isolate file. So translate all variables first.
900 translated_path_variables = dict(
901 (k,
902 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
903 v)))
904 for k, v in complete_state.saved_state.path_variables.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500905 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000906 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000907
908 if not skip_update:
909 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000910 return complete_state
911
912
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000913def read_trace_as_isolate_dict(complete_state, trace_blacklist):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000914 """Reads a trace and returns the .isolate dictionary.
915
916 Returns exceptions during the log parsing so it can be re-raised.
917 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000918 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000919 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000920 if not os.path.isfile(logfile):
921 raise ExecutionError(
922 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
923 try:
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000924 data = api.parse_log(logfile, trace_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000925 exceptions = [i['exception'] for i in data if 'exception' in i]
926 results = (i['results'] for i in data if 'results' in i)
927 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
928 files = set(sum((result.existent for result in results_stripped), []))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500929 tracked, touched = isolate_format.split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000930 value = generate_isolate(
931 tracked,
932 [],
933 touched,
934 complete_state.root_dir,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500935 complete_state.saved_state.path_variables,
936 complete_state.saved_state.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500937 complete_state.saved_state.extra_variables,
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000938 complete_state.saved_state.relative_cwd,
939 trace_blacklist)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000940 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000941 except trace_inputs.TracingFailure, e:
942 raise ExecutionError(
943 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000944 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000945
946
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000947def merge(complete_state, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000948 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000949 value, exceptions = read_trace_as_isolate_dict(
950 complete_state, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000951
952 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000953 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000954 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000955 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500956 prev_config = isolate_format.load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000957 isolate_dir,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500958 isolate_format.eval_content(prev_content),
959 isolate_format.extract_comment(prev_content))
960 new_config = isolate_format.load_isolate_as_config(isolate_dir, value, '')
961 config = isolate_format.union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000962 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000963 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000964 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500965 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000966 if exceptions:
967 # It got an exception, raise the first one.
968 raise \
969 exceptions[0][0], \
970 exceptions[0][1], \
971 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000972
973
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500974def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
975 """Creates a isolated tree usable for test execution.
976
977 Returns the current working directory where the isolated command should be
978 started in.
979 """
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500980 # Forcibly copy when the tree has to be read only. Otherwise the inode is
981 # modified, and this cause real problems because the user's source tree
982 # becomes read only. On the other hand, the cost of doing file copy is huge.
983 if read_only not in (0, None):
984 action = run_isolated.COPY
985 else:
986 action = run_isolated.HARDLINK_WITH_FALLBACK
987
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500988 recreate_tree(
989 outdir=outdir,
990 indir=root_dir,
991 infiles=files,
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500992 action=action,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500993 as_hash=False)
994 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
995 if not os.path.isdir(cwd):
996 # It can happen when no files are mapped from the directory containing the
997 # .isolate file. But the directory must exist to be the current working
998 # directory.
999 os.makedirs(cwd)
1000 run_isolated.change_tree_read_only(outdir, read_only)
1001 return cwd
1002
1003
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001004def prepare_for_archival(options, cwd):
1005 """Loads the isolated file and create 'infiles' for archival."""
1006 complete_state = load_complete_state(
1007 options, cwd, options.subdir, False)
1008 # Make sure that complete_state isn't modified until save_files() is
1009 # called, because any changes made to it here will propagate to the files
1010 # created (which is probably not intended).
1011 complete_state.save_files()
1012
1013 infiles = complete_state.saved_state.files
1014 # Add all the .isolated files.
1015 isolated_hash = []
1016 isolated_files = [
1017 options.isolated,
1018 ] + complete_state.saved_state.child_isolated_files
1019 for item in isolated_files:
1020 item_path = os.path.join(
1021 os.path.dirname(complete_state.isolated_filepath), item)
1022 # Do not use isolateserver.hash_file() here because the file is
1023 # likely smallish (under 500kb) and its file size is needed.
1024 with open(item_path, 'rb') as f:
1025 content = f.read()
1026 isolated_hash.append(
1027 complete_state.saved_state.algo(content).hexdigest())
1028 isolated_metadata = {
1029 'h': isolated_hash[-1],
1030 's': len(content),
1031 'priority': '0'
1032 }
1033 infiles[item_path] = isolated_metadata
1034 return complete_state, infiles, isolated_hash
1035
1036
maruel@chromium.org29029882013-08-30 12:15:40 +00001037### Commands.
1038
1039
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001040def CMDarchive(parser, args):
1041 """Creates a .isolated file and uploads the tree to an isolate server.
maruel@chromium.org29029882013-08-30 12:15:40 +00001042
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001043 All the files listed in the .isolated file are put in the isolate server
1044 cache via isolateserver.py.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001045 """
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001046 add_subdir_option(parser)
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001047 isolateserver.add_isolate_server_options(parser, False)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001048 auth.add_auth_options(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001049 options, args = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001050 auth.process_auth_options(parser, options)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001051 isolateserver.process_isolate_server_options(parser, options)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001052 if args:
1053 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001054 cwd = os.getcwd()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001055 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001056 success = False
1057 try:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001058 complete_state, infiles, isolated_hash = prepare_for_archival(
1059 options, cwd)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001060 logging.info('Creating content addressed object store with %d item',
1061 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001062
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001063 isolateserver.upload_tree(
1064 base_url=options.isolate_server,
1065 indir=complete_state.root_dir,
1066 infiles=infiles,
1067 namespace=options.namespace)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001068 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001069 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001070 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001071 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001072 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001073 if not success and os.path.isfile(options.isolated):
1074 os.remove(options.isolated)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001075 return int(not success)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001076
1077
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001078def CMDcheck(parser, args):
1079 """Checks that all the inputs are present and generates .isolated."""
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001080 add_subdir_option(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001081 options, args = parser.parse_args(args)
1082 if args:
1083 parser.error('Unsupported argument: %s' % args)
1084
1085 complete_state = load_complete_state(
1086 options, os.getcwd(), options.subdir, False)
1087
1088 # Nothing is done specifically. Just store the result and state.
1089 complete_state.save_files()
1090 return 0
1091
1092
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001093def CMDhashtable(parser, args):
1094 """Creates a .isolated file and stores the contains in a directory.
1095
1096 All the files listed in the .isolated file are put in the directory with their
1097 sha-1 as their file name. When using an NFS/CIFS server, the files can then be
1098 shared accross slaves without an isolate server.
1099 """
1100 add_subdir_option(parser)
Marc-Antoine Ruel488ce8f2014-02-09 11:25:04 -05001101 isolateserver.add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001102 add_skip_refresh_option(parser)
1103 options, args = parser.parse_args(args)
1104 if args:
1105 parser.error('Unsupported argument: %s' % args)
1106 cwd = os.getcwd()
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001107 isolateserver.process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001108
1109 success = False
1110 try:
1111 complete_state, infiles, isolated_hash = prepare_for_archival(options, cwd)
1112 logging.info('Creating content addressed object store with %d item',
1113 len(infiles))
1114 if not os.path.isdir(options.outdir):
1115 os.makedirs(options.outdir)
1116
1117 # TODO(maruel): Make the files read-only?
1118 recreate_tree(
1119 outdir=options.outdir,
1120 indir=complete_state.root_dir,
1121 infiles=infiles,
1122 action=run_isolated.HARDLINK_WITH_FALLBACK,
1123 as_hash=True)
1124 success = True
1125 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
1126 finally:
1127 # If the command failed, delete the .isolated file if it exists. This is
1128 # important so no stale swarm job is executed.
1129 if not success and os.path.isfile(options.isolated):
1130 os.remove(options.isolated)
1131 return int(not success)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001132
1133
maruel@chromium.orge5322512013-08-19 20:17:57 +00001134def CMDmerge(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001135 """Reads and merges the data from the trace back into the original .isolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001136 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001137 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001138 add_trace_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001139 options, args = parser.parse_args(args)
1140 if args:
1141 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001142
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001143 complete_state = load_complete_state(options, os.getcwd(), None, False)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001144 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001145 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001146 return 0
1147
1148
maruel@chromium.orge5322512013-08-19 20:17:57 +00001149def CMDread(parser, args):
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001150 """Reads the trace file generated with command 'trace'."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001151 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001152 add_trace_option(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001153 add_skip_refresh_option(parser)
maruel@chromium.org29029882013-08-30 12:15:40 +00001154 parser.add_option(
1155 '-m', '--merge', action='store_true',
1156 help='merge the results back in the .isolate file instead of printing')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001157 options, args = parser.parse_args(args)
1158 if args:
1159 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001160
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001161 complete_state = load_complete_state(
1162 options, os.getcwd(), None, options.skip_refresh)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001163 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001164 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
maruel@chromium.org29029882013-08-30 12:15:40 +00001165 if options.merge:
1166 merge(complete_state, blacklist)
1167 else:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001168 isolate_format.pretty_print(value, sys.stdout)
maruel@chromium.org29029882013-08-30 12:15:40 +00001169
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001170 if exceptions:
1171 # It got an exception, raise the first one.
1172 raise \
1173 exceptions[0][0], \
1174 exceptions[0][1], \
1175 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001176 return 0
1177
1178
maruel@chromium.orge5322512013-08-19 20:17:57 +00001179def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001180 """Creates a directory with all the dependencies mapped into it.
1181
1182 Useful to test manually why a test is failing. The target executable is not
1183 run.
1184 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001185 parser.require_isolated = False
Marc-Antoine Ruel488ce8f2014-02-09 11:25:04 -05001186 isolateserver.add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001187 add_skip_refresh_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001188 options, args = parser.parse_args(args)
1189 if args:
1190 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001191 cwd = os.getcwd()
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001192 isolateserver.process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001193 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001194
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001195 if not os.path.isdir(options.outdir):
1196 os.makedirs(options.outdir)
1197 print('Remapping into %s' % options.outdir)
1198 if os.listdir(options.outdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001199 raise ExecutionError('Can\'t remap in a non-empty directory')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001200
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001201 create_isolate_tree(
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001202 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001203 complete_state.saved_state.relative_cwd,
1204 complete_state.saved_state.read_only)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001205 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001206 complete_state.save_files()
1207 return 0
1208
1209
maruel@chromium.orge5322512013-08-19 20:17:57 +00001210def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001211 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001212 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001213 options, args = parser.parse_args(args)
1214 if args:
1215 parser.error('Unsupported argument: %s' % args)
1216
1217 if options.isolated:
1218 # Load the previous state if it was present. Namely, "foo.isolated.state".
1219 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001220 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001221 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001222 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001223 if not isolate:
maruel@chromium.org29029882013-08-30 12:15:40 +00001224 parser.error('--isolate is required.')
1225
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001226 with open(isolate, 'r') as f:
1227 content = f.read()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001228 config = isolate_format.load_isolate_as_config(
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001229 os.path.dirname(os.path.abspath(isolate)),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001230 isolate_format.eval_content(content),
1231 isolate_format.extract_comment(content))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001232 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001233 print('Updating %s' % isolate)
1234 with open(isolate, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001235 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001236 return 0
1237
1238
maruel@chromium.org29029882013-08-30 12:15:40 +00001239@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001240def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001241 """Runs the test executable in an isolated (temporary) directory.
1242
1243 All the dependencies are mapped into the temporary directory and the
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001244 directory is cleaned up after the target exits.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001245
maruel@chromium.org29029882013-08-30 12:15:40 +00001246 Argument processing stops at -- and these arguments are appended to the
1247 command line of the target to run. For example, use:
1248 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001249 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001250 parser.require_isolated = False
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001251 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001252 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001253
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001254 complete_state = load_complete_state(
1255 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001256 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001257 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001258 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001259 cmd = tools.fix_python_path(cmd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001260
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001261 outdir = run_isolated.make_temp_dir(
1262 'isolate-%s' % datetime.date.today(),
1263 os.path.dirname(complete_state.root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001264 try:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -05001265 # TODO(maruel): Use run_isolated.run_tha_test().
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001266 cwd = create_isolate_tree(
1267 outdir, complete_state.root_dir, complete_state.saved_state.files,
1268 complete_state.saved_state.relative_cwd,
1269 complete_state.saved_state.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001270 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1271 result = subprocess.call(cmd, cwd=cwd)
1272 finally:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001273 run_isolated.rmtree(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001274
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001275 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001276 complete_state.save_files()
1277 return result
1278
1279
maruel@chromium.org29029882013-08-30 12:15:40 +00001280@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001281def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001282 """Traces the target using trace_inputs.py.
1283
1284 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001285 its child processes access. Then the 'merge' command can be used to generate
1286 an updated .isolate file out of it or the 'read' command to print it out to
1287 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001288
maruel@chromium.org29029882013-08-30 12:15:40 +00001289 Argument processing stops at -- and these arguments are appended to the
1290 command line of the target to run. For example, use:
1291 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001292 """
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001293 add_trace_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001294 parser.add_option(
1295 '-m', '--merge', action='store_true',
1296 help='After tracing, merge the results back in the .isolate file')
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001297 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001298 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001299
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001300 complete_state = load_complete_state(
1301 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001302 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001303 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001304 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001305 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001306 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001307 unicode(complete_state.root_dir),
1308 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001309 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1310 if not os.path.isfile(cmd[0]):
1311 raise ExecutionError(
1312 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001313 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1314 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001315 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001316 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001317 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001318 try:
1319 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001320 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001321 cmd,
1322 cwd,
1323 'default',
1324 True)
1325 except trace_inputs.TracingFailure, e:
1326 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1327
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001328 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001329 logging.error(
1330 'Tracer exited with %d, which means the tests probably failed so the '
1331 'trace is probably incomplete.', result)
1332 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001333
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001334 complete_state.save_files()
1335
1336 if options.merge:
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001337 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001338 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001339
1340 return result
1341
1342
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001343def _process_variable_arg(option, opt, _value, parser):
1344 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +00001345 if not parser.rargs:
1346 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001347 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001348 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001349 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001350 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001351 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001352 else:
1353 if not parser.rargs:
1354 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001355 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001356 v = parser.rargs.pop(0)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001357 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001358 raise optparse.OptionValueError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001359 'Variable \'%s\' doesn\'t respect format \'%s\'' %
1360 (k, isolate_format.VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -05001361 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001362
1363
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001364def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001365 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001366 parser.add_option(
1367 '-s', '--isolated',
1368 metavar='FILE',
1369 help='.isolated file to generate or read')
1370 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001371 parser.add_option(
1372 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001373 dest='isolated',
1374 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001375 is_win = sys.platform in ('win32', 'cygwin')
1376 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001377 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001378 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001379 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001380 # - configuration variables that are to be used in deducing the matrix to
1381 # reduce.
1382 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001383 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001384 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00001385 action='callback',
1386 callback=_process_variable_arg,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001387 default=[('OS', get_flavor())],
1388 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001389 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001390 help='Config variables are used to determine which conditions should be '
1391 'matched when loading a .isolate file, default: %default. '
1392 'All 3 kinds of variables are persistent accross calls, they are '
1393 'saved inside <.isolated>.state')
1394 parser.add_option(
1395 '--path-variable',
1396 action='callback',
1397 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001398 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001399 dest='path_variables',
1400 metavar='FOO BAR',
1401 help='Path variables are used to replace file paths when loading a '
1402 '.isolate file, default: %default')
1403 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001404 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001405 action='callback',
1406 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001407 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1408 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001409 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001410 help='Extraneous variables are replaced on the \'command\' entry and on '
1411 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001412
1413
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001414def add_subdir_option(parser):
1415 parser.add_option(
1416 '--subdir',
1417 help='Filters to a subdirectory. Its behavior changes depending if it '
1418 'is a relative path as a string or as a path variable. Path '
1419 'variables are always keyed from the directory containing the '
1420 '.isolate file. Anything else is keyed on the root directory.')
1421
1422
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001423def add_trace_option(parser):
1424 """Adds --trace-blacklist to the parser."""
1425 parser.add_option(
1426 '--trace-blacklist',
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001427 action='append', default=list(isolateserver.DEFAULT_BLACKLIST),
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001428 help='List of regexp to use as blacklist filter for files to consider '
1429 'important, not to be confused with --blacklist which blacklists '
1430 'test case.')
1431
1432
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001433def add_skip_refresh_option(parser):
1434 parser.add_option(
1435 '--skip-refresh', action='store_true',
1436 help='Skip reading .isolate file and do not refresh the hash of '
1437 'dependencies')
1438
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001439
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001440def parse_isolated_option(parser, options, cwd, require_isolated):
1441 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001442 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001443 options.isolated = os.path.normpath(
1444 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001445 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001446 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001447 if options.isolated and not options.isolated.endswith('.isolated'):
1448 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001449
1450
1451def parse_variable_option(options):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001452 """Processes all the --<foo>-variable flags."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001453 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
1454 # but it wouldn't be backward compatible.
1455 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001456 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001457 try:
1458 return int(s)
1459 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001460 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001461 options.config_variables = dict(
1462 (k, try_make_int(v)) for k, v in options.config_variables)
1463 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001464 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001465
1466
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001467class OptionParserIsolate(tools.OptionParserWithLogging):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001468 """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
1469 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001470 # Set it to False if it is not required, e.g. it can be passed on but do not
1471 # fail if not given.
1472 require_isolated = True
1473
1474 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001475 tools.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00001476 self,
1477 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1478 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001479 group = optparse.OptionGroup(self, "Common options")
1480 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001481 '-i', '--isolate',
1482 metavar='FILE',
1483 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001484 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001485 group.add_option(
csharp@chromium.org01856802012-11-12 17:48:13 +00001486 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00001487 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1488 help='Indicates that invalid entries in the isolated file to be '
1489 'only be logged and not stop processing. Defaults to True if '
1490 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001491 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001492
1493 def parse_args(self, *args, **kwargs):
1494 """Makes sure the paths make sense.
1495
1496 On Windows, / and \ are often mixed together in a path.
1497 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001498 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001499 self, *args, **kwargs)
1500 if not self.allow_interspersed_args and args:
1501 self.error('Unsupported argument: %s' % args)
1502
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001503 cwd = file_path.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001504 parse_isolated_option(self, options, cwd, self.require_isolated)
1505 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001506
1507 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001508 # TODO(maruel): Work with non-ASCII.
1509 # The path must be in native path case for tracing purposes.
1510 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1511 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001512 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001513
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001514 return options, args
1515
1516
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001517def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00001518 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001519 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00001520 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001521 except Exception as e:
1522 tools.report_error(e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001523 return 1
1524
1525
1526if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00001527 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001528 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001529 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001530 sys.exit(main(sys.argv[1:]))