blob: 9e5cf2d7707b796b6b363133ada58a2b948734d8 [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
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050025import isolate_format
maruel@chromium.orgfb78d432013-08-28 21:22:40 +000026import isolateserver
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000027import run_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000028import trace_inputs
29
30# Import here directly so isolate is easier to use as a library.
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000031from run_isolated import get_flavor
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000032
maruel@chromium.orge5322512013-08-19 20:17:57 +000033from third_party import colorama
34from third_party.depot_tools import fix_encoding
35from third_party.depot_tools import subcommand
36
maruel@chromium.org561d4b22013-09-26 21:08:08 +000037from utils import file_path
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000038from utils import tools
39
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000040
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -050041__version__ = '0.3'
maruel@chromium.org3d671992013-08-20 00:38:27 +000042
43
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000044class ExecutionError(Exception):
45 """A generic error occurred."""
46 def __str__(self):
47 return self.args[0]
48
49
50### Path handling code.
51
52
csharp@chromium.org01856802012-11-12 17:48:13 +000053def expand_directories_and_symlinks(indir, infiles, blacklist,
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +000054 follow_symlinks, ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000055 """Expands the directories and the symlinks, applies the blacklist and
56 verifies files exist.
57
58 Files are specified in os native path separator.
59 """
60 outfiles = []
61 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +000062 try:
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -050063 outfiles.extend(
64 isolateserver.expand_directory_and_symlink(
65 indir, relfile, blacklist, follow_symlinks))
maruel@chromium.org9958e4a2013-09-17 00:01:48 +000066 except isolateserver.MappingError as e:
csharp@chromium.org01856802012-11-12 17:48:13 +000067 if ignore_broken_items:
68 logging.info('warning: %s', e)
69 else:
70 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000071 return outfiles
72
73
maruel@chromium.org7b844a62013-09-17 13:04:59 +000074def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000075 """Creates a new tree with only the input files in it.
76
77 Arguments:
78 outdir: Output directory to create the files in.
79 indir: Root directory the infiles are based in.
80 infiles: dict of files to map from |indir| to |outdir|.
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000081 action: One of accepted action of run_isolated.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +000082 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000083 """
84 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +000085 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
86 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000087
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000088 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000089 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000090 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000091 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000092
93 for relfile, metadata in infiles.iteritems():
94 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +000095 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000096 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000097 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000098 # Skip links when storing a hashtable.
99 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000100 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000101 if os.path.isfile(outfile):
102 # Just do a quick check that the file size matches. No need to stat()
103 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000104 if not 's' in metadata:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000105 raise isolateserver.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000106 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000107 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000108 continue
109 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000110 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000111 os.remove(outfile)
112 else:
113 outfile = os.path.join(outdir, relfile)
114 outsubdir = os.path.dirname(outfile)
115 if not os.path.isdir(outsubdir):
116 os.makedirs(outsubdir)
117
118 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000119 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000120 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000121 if 'l' in metadata:
122 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000123 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000124 # symlink doesn't exist on Windows.
125 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000126 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000127 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000128
129
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000130### Variable stuff.
131
132
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500133def _normalize_path_variable(cwd, relative_base_dir, key, value):
134 """Normalizes a path variable into a relative directory.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500135 """
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500136 # Variables could contain / or \ on windows. Always normalize to
137 # os.path.sep.
138 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
139 normalized = file_path.get_native_path_case(os.path.normpath(x))
140 if not os.path.isdir(normalized):
141 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
142
143 # All variables are relative to the .isolate file.
144 normalized = os.path.relpath(normalized, relative_base_dir)
145 logging.debug(
146 'Translated variable %s from %s to %s', key, value, normalized)
147 return normalized
148
149
150def normalize_path_variables(cwd, path_variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000151 """Processes path variables as a special case and returns a copy of the dict.
152
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000153 For each 'path' variable: first normalizes it based on |cwd|, verifies it
154 exists then sets it as relative to relative_base_dir.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500155 """
156 logging.info(
157 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
158 relative_base_dir)
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -0500159 assert isinstance(cwd, unicode), cwd
160 assert isinstance(relative_base_dir, unicode), relative_base_dir
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500161 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
162 return dict(
163 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
164 for k, v in path_variables.iteritems())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000165
166
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500167### Internal state files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000168
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500169
170def isolatedfile_to_state(filename):
171 """For a '.isolate' file, returns the path to the saved '.state' file."""
172 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000173
174
175def classify_files(root_dir, tracked, untracked):
176 """Converts the list of files into a .isolate 'variables' dictionary.
177
178 Arguments:
179 - tracked: list of files names to generate a dictionary out of that should
180 probably be tracked.
181 - untracked: list of files names that must not be tracked.
182 """
183 # These directories are not guaranteed to be always present on every builder.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500184 CHROMIUM_OPTIONAL_DIRECTORIES = (
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000185 'test/data/plugin',
186 'third_party/WebKit/LayoutTests',
187 )
188
189 new_tracked = []
190 new_untracked = list(untracked)
191
192 def should_be_tracked(filepath):
193 """Returns True if it is a file without whitespace in a non-optional
194 directory that has no symlink in its path.
195 """
196 if filepath.endswith('/'):
197 return False
198 if ' ' in filepath:
199 return False
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500200 if any(i in filepath for i in CHROMIUM_OPTIONAL_DIRECTORIES):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000201 return False
202 # Look if any element in the path is a symlink.
203 split = filepath.split('/')
204 for i in range(len(split)):
205 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
206 return False
207 return True
208
209 for filepath in sorted(tracked):
210 if should_be_tracked(filepath):
211 new_tracked.append(filepath)
212 else:
213 # Anything else.
214 new_untracked.append(filepath)
215
216 variables = {}
217 if new_tracked:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500218 variables[isolate_format.KEY_TRACKED] = sorted(new_tracked)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000219 if new_untracked:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500220 variables[isolate_format.KEY_UNTRACKED] = sorted(new_untracked)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000221 return variables
222
223
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500224def chromium_fix(f, variables):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500225 """Fixes an isolate dependency with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000226 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
227 # separator.
228 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000229 # Ignored items.
230 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000231 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000232 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000233 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000234 # 'First Run' is not created by the compile, but by the test itself.
235 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000236
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000237 # Blacklist logs and other unimportant files.
238 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
239 logging.debug('Ignoring %s', f)
240 return None
241
maruel@chromium.org7650e422012-11-16 21:56:42 +0000242 EXECUTABLE = re.compile(
243 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500244 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
maruel@chromium.org7650e422012-11-16 21:56:42 +0000245 r'$')
246 match = EXECUTABLE.match(f)
247 if match:
248 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
249
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000250 if sys.platform == 'darwin':
251 # On OSX, the name of the output is dependent on gyp define, it can be
252 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
253 # Framework.framework'. Furthermore, they are versioned with a gyp
254 # variable. To lower the complexity of the .isolate file, remove all the
255 # individual entries that show up under any of the 4 entries and replace
256 # them with the directory itself. Overall, this results in a bit more
257 # files than strictly necessary.
258 OSX_BUNDLES = (
259 '<(PRODUCT_DIR)/Chromium Framework.framework/',
260 '<(PRODUCT_DIR)/Chromium.app/',
261 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
262 '<(PRODUCT_DIR)/Google Chrome.app/',
263 )
264 for prefix in OSX_BUNDLES:
265 if f.startswith(prefix):
266 # Note this result in duplicate values, so the a set() must be used to
267 # remove duplicates.
268 return prefix
269 return f
270
271
272def generate_simplified(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500273 tracked, untracked, touched, root_dir, path_variables, extra_variables,
274 relative_cwd, trace_blacklist):
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000275 """Generates a clean and complete .isolate 'variables' dictionary.
276
277 Cleans up and extracts only files from within root_dir then processes
278 variables and relative_cwd.
279 """
280 root_dir = os.path.realpath(root_dir)
281 logging.info(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500282 'generate_simplified(%d files, %s, %s, %s, %s)' %
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000283 (len(tracked) + len(untracked) + len(touched),
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500284 root_dir, path_variables, extra_variables, relative_cwd))
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000285
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000286 # Preparation work.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500287 relative_cwd = file_path.cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000288 assert not os.path.isabs(relative_cwd), relative_cwd
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500289
290 # Normalizes to posix path. .isolate files are using posix paths on all OSes
291 # for coherency.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000292 path_variables = dict(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500293 (k, v.replace(os.path.sep, '/')) for k, v in path_variables.iteritems())
294 # Contains normalized path_variables plus extra_variables.
295 total_variables = path_variables.copy()
296 total_variables.update(extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000297
298 # Actual work: Process the files.
299 # TODO(maruel): if all the files in a directory are in part tracked and in
300 # part untracked, the directory will not be extracted. Tracked files should be
301 # 'promoted' to be untracked as needed.
302 tracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000303 root_dir, tracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000304 untracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000305 root_dir, untracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000306 # touched is not compressed, otherwise it would result in files to be archived
307 # that we don't need.
308
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000309 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000310 def fix(f):
311 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000312 # Important, GYP stores the files with / and not \.
313 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000314 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000315 # If it's not already a variable.
316 if not f.startswith('<'):
317 # relative_cwd is usually the directory containing the gyp file. It may be
318 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000319 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000320 # Convert the whole thing to / since it's isolate's speak.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500321 f = file_path.posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000322 posixpath.join(root_dir_posix, f),
323 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000324
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500325 # Use the longest value first.
326 for key, value in sorted(
327 path_variables.iteritems(), key=lambda x: -len(x[1])):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500328 if f.startswith(value):
329 f = '<(%s)%s' % (key, f[len(value):])
330 logging.debug('Converted to %s' % f)
331 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000332 return f
333
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000334 def fix_all(items):
335 """Reduces the items to convert variables, removes unneeded items, apply
336 chromium-specific fixes and only return unique items.
337 """
338 variables_converted = (fix(f.path) for f in items)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500339 chromium_fixed = (
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500340 chromium_fix(f, total_variables) for f in variables_converted)
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000341 return set(f for f in chromium_fixed if f)
342
343 tracked = fix_all(tracked)
344 untracked = fix_all(untracked)
345 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000346 out = classify_files(root_dir, tracked, untracked)
347 if touched:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500348 out[isolate_format.KEY_TOUCHED] = sorted(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000349 return out
350
351
352def generate_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500353 tracked, untracked, touched, root_dir, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500354 extra_variables, relative_cwd, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000355 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000356 dependencies = generate_simplified(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500357 tracked, untracked, touched, root_dir, path_variables, extra_variables,
358 relative_cwd, trace_blacklist)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000359 config_variable_names, config_values = zip(
360 *sorted(config_variables.iteritems()))
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500361 out = isolate_format.Configs(None, config_variable_names)
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500362 # TODO(maruel): Create a public interface in Configs to add a ConfigSettings.
363 # pylint: disable=W0212
364 out._by_config[config_values] = isolate_format.ConfigSettings(dependencies)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000365 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000366
367
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500368def chromium_save_isolated(isolated, data, path_variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000369 """Writes one or many .isolated files.
370
371 This slightly increases the cold cache cost but greatly reduce the warm cache
372 cost by splitting low-churn files off the master .isolated file. It also
373 reduces overall isolateserver memcache consumption.
374 """
375 slaves = []
376
377 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000378 new_slave = {
379 'algo': data['algo'],
380 'files': {},
381 'os': data['os'],
382 'version': data['version'],
383 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000384 for f in data['files'].keys():
385 if f.startswith(prefix):
386 new_slave['files'][f] = data['files'].pop(f)
387 if new_slave['files']:
388 slaves.append(new_slave)
389
390 # Split test/data/ in its own .isolated file.
391 extract_into_included_isolated(os.path.join('test', 'data', ''))
392
393 # Split everything out of PRODUCT_DIR in its own .isolated file.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500394 if path_variables.get('PRODUCT_DIR'):
395 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000396
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000397 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000398 for index, f in enumerate(slaves):
399 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500400 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000401 data.setdefault('includes', []).append(
402 isolateserver.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000403 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000404
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500405 files.extend(isolateserver.save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000406 return files
407
408
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000409class Flattenable(object):
410 """Represents data that can be represented as a json file."""
411 MEMBERS = ()
412
413 def flatten(self):
414 """Returns a json-serializable version of itself.
415
416 Skips None entries.
417 """
418 items = ((member, getattr(self, member)) for member in self.MEMBERS)
419 return dict((member, value) for member, value in items if value is not None)
420
421 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000422 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000423 """Loads a flattened version."""
424 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000425 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000426 for member in out.MEMBERS:
427 if member in data:
428 # Access to a protected member XXX of a client class
429 # pylint: disable=W0212
430 out._load_member(member, data.pop(member))
431 if data:
432 raise ValueError(
433 'Found unexpected entry %s while constructing an object %s' %
434 (data, cls.__name__), data, cls.__name__)
435 return out
436
437 def _load_member(self, member, value):
438 """Loads a member into self."""
439 setattr(self, member, value)
440
441 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000442 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000443 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000444 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500445 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000446 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000447 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000448 # On failure, loads the default instance.
449 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000450 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000451 return out
452
453
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000454class SavedState(Flattenable):
455 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000456
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000457 This file caches the items calculated by this script and is used to increase
458 the performance of the script. This file is not loaded by run_isolated.py.
459 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000460
461 It is important to note that the 'files' dict keys are using native OS path
462 separator instead of '/' used in .isolate file.
463 """
464 MEMBERS = (
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000465 # Algorithm used to generate the hash. The only supported value is at the
466 # time of writting 'sha-1'.
467 'algo',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000468 # Cache of the processed command. This value is saved because .isolated
469 # files are never loaded by isolate.py so it's the only way to load the
470 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000471 'command',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500472 # GYP variables that are used to generate conditions. The most frequent
473 # example is 'OS'.
474 'config_variables',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500475 # GYP variables that will be replaced in 'command' and paths but will not be
476 # considered a relative directory.
477 'extra_variables',
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000478 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000479 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000480 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000481 'isolate_file',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000482 # List of included .isolated files. Used to support/remember 'slave'
483 # .isolated files. Relative path to isolated_basedir.
484 'child_isolated_files',
485 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000486 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000487 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000488 'relative_cwd',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500489 # GYP variables used to generate the .isolated files paths based on path
490 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
491 'path_variables',
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000492 # Version of the file format in format 'major.minor'. Any non-breaking
493 # change must update minor. Any breaking change must update major.
494 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000495 )
496
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000497 def __init__(self, isolated_basedir):
498 """Creates an empty SavedState.
499
500 |isolated_basedir| is the directory where the .isolated and .isolated.state
501 files are saved.
502 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000503 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000504 assert os.path.isabs(isolated_basedir), isolated_basedir
505 assert os.path.isdir(isolated_basedir), isolated_basedir
506 self.isolated_basedir = isolated_basedir
507
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000508 # The default algorithm used.
509 self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500510 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000511 self.command = []
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500512 self.config_variables = {}
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500513 self.extra_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000514 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000515 self.isolate_file = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500516 self.path_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000517 self.read_only = None
518 self.relative_cwd = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500519 self.version = isolateserver.ISOLATED_FILE_VERSION
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000520
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500521 def update(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500522 self, isolate_file, path_variables, config_variables, extra_variables):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000523 """Updates the saved state with new data to keep GYP variables and internal
524 reference to the original .isolate file.
525 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +0000526 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000527 # Convert back to a relative path. On Windows, if the isolate and
528 # isolated files are on different drives, isolate_file will stay an absolute
529 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500530 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000531
532 # The same .isolate file should always be used to generate the .isolated and
533 # .isolated.state.
534 assert isolate_file == self.isolate_file or not self.isolate_file, (
535 isolate_file, self.isolate_file)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500536 self.config_variables.update(config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500537 self.extra_variables.update(extra_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000538 self.isolate_file = isolate_file
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500539 self.path_variables.update(path_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000540
541 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
542 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000543
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000544 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000545 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000546 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000547 self.command = command
548 # Add new files.
549 for f in infiles:
550 self.files.setdefault(f, {})
551 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000552 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000553 # Prune extraneous files that are not a dependency anymore.
554 for f in set(self.files).difference(set(infiles).union(touched)):
555 del self.files[f]
556 if read_only is not None:
557 self.read_only = read_only
558 self.relative_cwd = relative_cwd
559
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000560 def to_isolated(self):
561 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000562
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000563 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000564 """
565 def strip(data):
566 """Returns a 'files' entry with only the whitelisted keys."""
567 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
568
569 out = {
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000570 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000571 'files': dict(
572 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000573 'version': self.version,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000574 }
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500575 if self.config_variables.get('OS'):
576 out['os'] = self.config_variables['OS']
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000577 if self.command:
578 out['command'] = self.command
579 if self.read_only is not None:
580 out['read_only'] = self.read_only
581 if self.relative_cwd:
582 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000583 return out
584
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000585 @property
586 def isolate_filepath(self):
587 """Returns the absolute path of self.isolate_file."""
588 return os.path.normpath(
589 os.path.join(self.isolated_basedir, self.isolate_file))
590
591 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000592 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000593 def load(cls, data, isolated_basedir): # pylint: disable=W0221
594 """Special case loading to disallow different OS.
595
596 It is not possible to load a .isolated.state files from a different OS, this
597 file is saved in OS-specific format.
598 """
599 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500600 if data.get('os'):
601 out.config_variables['OS'] = data['os']
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000602
603 # Converts human readable form back into the proper class type.
604 algo = data.get('algo', 'sha-1')
605 if not algo in isolateserver.SUPPORTED_ALGOS:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000606 raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000607 out.algo = isolateserver.SUPPORTED_ALGOS[algo]
608
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500609 # Refuse the load non-exact version, even minor difference. This is unlike
610 # isolateserver.load_isolated(). This is because .isolated.state could have
611 # changed significantly even in minor version difference.
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000612 if not re.match(r'^(\d+)\.(\d+)$', out.version):
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000613 raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500614 if out.version != isolateserver.ISOLATED_FILE_VERSION:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000615 raise isolateserver.ConfigError(
616 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000617
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000618 # The .isolate file must be valid. It could be absolute on Windows if the
619 # drive containing the .isolate and the drive containing the .isolated files
620 # differ.
621 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
622 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000623 return out
624
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000625 def flatten(self):
626 """Makes sure 'algo' is in human readable form."""
627 out = super(SavedState, self).flatten()
628 out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
629 return out
630
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000631 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500632 def dict_to_str(d):
633 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
634
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000635 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000636 out += ' command: %s\n' % self.command
637 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000638 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000639 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000640 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000641 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500642 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
643 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500644 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000645 return out
646
647
648class CompleteState(object):
649 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000650 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000651 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +0000652 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000653 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000654 # Contains the data to ease developer's use-case but that is not strictly
655 # necessary.
656 self.saved_state = saved_state
657
658 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000659 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000660 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000661 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000662 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000663 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000664 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000665 SavedState.load_file(
666 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000667
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500668 def load_isolate(
669 self, cwd, isolate_file, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500670 extra_variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000671 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000672 .isolate file.
673
674 Processes the loaded data, deduce root_dir, relative_cwd.
675 """
676 # Make sure to not depend on os.getcwd().
677 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000678 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000679 logging.info(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500680 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500681 cwd, isolate_file, path_variables, config_variables, extra_variables,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500682 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000683 relative_base_dir = os.path.dirname(isolate_file)
684
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500685 # Processes the variables.
686 path_variables = normalize_path_variables(
687 cwd, path_variables, relative_base_dir)
688 # Update the saved state.
689 self.saved_state.update(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500690 isolate_file, path_variables, config_variables, extra_variables)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500691 path_variables = self.saved_state.path_variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000692
693 with open(isolate_file, 'r') as f:
694 # At that point, variables are not replaced yet in command and infiles.
695 # infiles may contain directory entries and is in posix style.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500696 command, infiles, touched, read_only = (
697 isolate_format.load_isolate_for_config(
698 os.path.dirname(isolate_file), f.read(),
699 self.saved_state.config_variables))
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500700
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500701 total_variables = self.saved_state.path_variables.copy()
702 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500703 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500704 command = [
705 isolate_format.eval_variables(i, total_variables) for i in command
706 ]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500707
708 total_variables = self.saved_state.path_variables.copy()
709 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500710 infiles = [
711 isolate_format.eval_variables(f, total_variables) for f in infiles
712 ]
713 touched = [
714 isolate_format.eval_variables(f, total_variables) for f in touched
715 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000716 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +0000717 # form '../../foo/bar'. Note that path variables must be taken in account
718 # too, add them as if they were input files.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500719 root_dir = isolate_format.determine_root_dir(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500720 relative_base_dir, infiles + touched +
721 self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000722 # The relative directory is automatically determined by the relative path
723 # between root_dir and the directory containing the .isolate file,
724 # isolate_base_dir.
725 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500726 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000727 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500728 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500729 if not file_path.path_starts_with(
730 root_dir, os.path.join(relative_base_dir, v)):
731 raise isolateserver.MappingError(
732 'Path variable %s=%r points outside the inferred root directory %s'
733 % (k, v, root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000734 # Normalize the files based to root_dir. It is important to keep the
735 # trailing os.path.sep at that step.
736 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500737 file_path.relpath(
738 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000739 for f in infiles
740 ]
741 touched = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500742 file_path.relpath(
743 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000744 for f in touched
745 ]
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500746 follow_symlinks = config_variables['OS'] != 'win'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000747 # Expand the directories by listing each file inside. Up to now, trailing
748 # os.path.sep must be kept. Do not expand 'touched'.
749 infiles = expand_directories_and_symlinks(
750 root_dir,
751 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +0000752 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000753 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +0000754 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000755
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000756 # If we ignore broken items then remove any missing touched items.
757 if ignore_broken_items:
758 original_touched_count = len(touched)
759 touched = [touch for touch in touched if os.path.exists(touch)]
760
761 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +0000762 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000763 len(touched) - original_touched_count)
764
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000765 # Finally, update the new data to be able to generate the foo.isolated file,
766 # the file that is used by run_isolated.py.
767 self.saved_state.update_isolated(
768 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000769 logging.debug(self)
770
maruel@chromium.org9268f042012-10-17 17:36:41 +0000771 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000772 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000773
maruel@chromium.org9268f042012-10-17 17:36:41 +0000774 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
775 file is tainted.
776
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500777 See isolateserver.process_input() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000778 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000779 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +0000780 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000781 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000782 else:
783 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500784 self.saved_state.files[infile] = isolateserver.process_input(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000785 filepath,
786 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000787 self.saved_state.read_only,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500788 self.saved_state.config_variables['OS'],
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000789 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000790
791 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000792 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000793 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000794 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000795 self.isolated_filepath,
796 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500797 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000798 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000799 total_bytes = sum(
800 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000801 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000802 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000803 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000804 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000805 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500806 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000807
808 @property
809 def root_dir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000810 """Returns the absolute path of the root_dir to reference the .isolate file
811 via relative_cwd.
812
813 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
814 to isolate_filepath.
815 """
maruel@chromium.orgb253fb82012-10-16 21:44:48 +0000816 if not self.saved_state.isolate_file:
817 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000818 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000819 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000820 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000821 root_dir = isolate_dir
822 else:
maruel@chromium.org87557b92013-10-16 18:04:11 +0000823 if not isolate_dir.endswith(self.saved_state.relative_cwd):
824 raise ExecutionError(
825 ('Make sure the .isolate file is in the directory that will be '
826 'used as the relative directory. It is currently in %s and should '
827 'be in %s') % (isolate_dir, self.saved_state.relative_cwd))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000828 # Walk back back to the root directory.
829 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000830 return file_path.get_native_path_case(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000831
832 @property
833 def resultdir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000834 """Returns the absolute path containing the .isolated file.
835
836 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
837 path as the value.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000838 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000839 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000840
841 def __str__(self):
842 def indent(data, indent_length):
843 """Indents text."""
844 spacing = ' ' * indent_length
845 return ''.join(spacing + l for l in str(data).splitlines(True))
846
847 out = '%s(\n' % self.__class__.__name__
848 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000849 out += ' saved_state: %s)' % indent(self.saved_state, 2)
850 return out
851
852
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000853def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000854 """Loads a CompleteState.
855
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000856 This includes data from .isolate and .isolated.state files. Never reads the
857 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000858
859 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000860 options: Options instance generated with OptionParserIsolate. For either
861 options.isolate and options.isolated, if the value is set, it is an
862 absolute path.
863 cwd: base directory to be used when loading the .isolate file.
864 subdir: optional argument to only process file in the subdirectory, relative
865 to CompleteState.root_dir.
866 skip_update: Skip trying to load the .isolate file and processing the
867 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000868 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000869 assert not options.isolate or os.path.isabs(options.isolate)
870 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000871 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000872 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000873 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000874 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000875 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000876 else:
877 # Constructs a dummy object that cannot be saved. Useful for temporary
878 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000879 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000880
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000881 if not options.isolate:
882 if not complete_state.saved_state.isolate_file:
883 if not skip_update:
884 raise ExecutionError('A .isolate file is required.')
885 isolate = None
886 else:
887 isolate = complete_state.saved_state.isolate_filepath
888 else:
889 isolate = options.isolate
890 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500891 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000892 options.isolate, complete_state.saved_state.isolated_basedir)
893 if rel_isolate != complete_state.saved_state.isolate_file:
894 raise ExecutionError(
895 '%s and %s do not match.' % (
896 options.isolate, complete_state.saved_state.isolate_file))
897
898 if not skip_update:
899 # Then load the .isolate and expands directories.
900 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500901 cwd, isolate, options.path_variables, options.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500902 options.extra_variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000903
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000904 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +0000905 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000906 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500907 # This is tricky here. If it is a path, take it from the root_dir. If
908 # it is a variable, it must be keyed from the directory containing the
909 # .isolate file. So translate all variables first.
910 translated_path_variables = dict(
911 (k,
912 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
913 v)))
914 for k, v in complete_state.saved_state.path_variables.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500915 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000916 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000917
918 if not skip_update:
919 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000920 return complete_state
921
922
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000923def read_trace_as_isolate_dict(complete_state, trace_blacklist):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000924 """Reads a trace and returns the .isolate dictionary.
925
926 Returns exceptions during the log parsing so it can be re-raised.
927 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000928 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000929 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000930 if not os.path.isfile(logfile):
931 raise ExecutionError(
932 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
933 try:
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000934 data = api.parse_log(logfile, trace_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000935 exceptions = [i['exception'] for i in data if 'exception' in i]
936 results = (i['results'] for i in data if 'results' in i)
937 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
938 files = set(sum((result.existent for result in results_stripped), []))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500939 tracked, touched = isolate_format.split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000940 value = generate_isolate(
941 tracked,
942 [],
943 touched,
944 complete_state.root_dir,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500945 complete_state.saved_state.path_variables,
946 complete_state.saved_state.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500947 complete_state.saved_state.extra_variables,
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000948 complete_state.saved_state.relative_cwd,
949 trace_blacklist)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000950 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000951 except trace_inputs.TracingFailure, e:
952 raise ExecutionError(
953 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000954 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000955
956
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000957def merge(complete_state, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000958 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000959 value, exceptions = read_trace_as_isolate_dict(
960 complete_state, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000961
962 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000963 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000964 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000965 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500966 prev_config = isolate_format.load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000967 isolate_dir,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500968 isolate_format.eval_content(prev_content),
969 isolate_format.extract_comment(prev_content))
970 new_config = isolate_format.load_isolate_as_config(isolate_dir, value, '')
971 config = isolate_format.union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000972 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000973 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000974 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500975 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000976 if exceptions:
977 # It got an exception, raise the first one.
978 raise \
979 exceptions[0][0], \
980 exceptions[0][1], \
981 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000982
983
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500984def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
985 """Creates a isolated tree usable for test execution.
986
987 Returns the current working directory where the isolated command should be
988 started in.
989 """
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500990 # Forcibly copy when the tree has to be read only. Otherwise the inode is
991 # modified, and this cause real problems because the user's source tree
992 # becomes read only. On the other hand, the cost of doing file copy is huge.
993 if read_only not in (0, None):
994 action = run_isolated.COPY
995 else:
996 action = run_isolated.HARDLINK_WITH_FALLBACK
997
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500998 recreate_tree(
999 outdir=outdir,
1000 indir=root_dir,
1001 infiles=files,
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -05001002 action=action,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001003 as_hash=False)
1004 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
1005 if not os.path.isdir(cwd):
1006 # It can happen when no files are mapped from the directory containing the
1007 # .isolate file. But the directory must exist to be the current working
1008 # directory.
1009 os.makedirs(cwd)
1010 run_isolated.change_tree_read_only(outdir, read_only)
1011 return cwd
1012
1013
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001014def prepare_for_archival(options, cwd):
1015 """Loads the isolated file and create 'infiles' for archival."""
1016 complete_state = load_complete_state(
1017 options, cwd, options.subdir, False)
1018 # Make sure that complete_state isn't modified until save_files() is
1019 # called, because any changes made to it here will propagate to the files
1020 # created (which is probably not intended).
1021 complete_state.save_files()
1022
1023 infiles = complete_state.saved_state.files
1024 # Add all the .isolated files.
1025 isolated_hash = []
1026 isolated_files = [
1027 options.isolated,
1028 ] + complete_state.saved_state.child_isolated_files
1029 for item in isolated_files:
1030 item_path = os.path.join(
1031 os.path.dirname(complete_state.isolated_filepath), item)
1032 # Do not use isolateserver.hash_file() here because the file is
1033 # likely smallish (under 500kb) and its file size is needed.
1034 with open(item_path, 'rb') as f:
1035 content = f.read()
1036 isolated_hash.append(
1037 complete_state.saved_state.algo(content).hexdigest())
1038 isolated_metadata = {
1039 'h': isolated_hash[-1],
1040 's': len(content),
1041 'priority': '0'
1042 }
1043 infiles[item_path] = isolated_metadata
1044 return complete_state, infiles, isolated_hash
1045
1046
maruel@chromium.org29029882013-08-30 12:15:40 +00001047### Commands.
1048
1049
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001050def CMDarchive(parser, args):
1051 """Creates a .isolated file and uploads the tree to an isolate server.
maruel@chromium.org29029882013-08-30 12:15:40 +00001052
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001053 All the files listed in the .isolated file are put in the isolate server
1054 cache via isolateserver.py.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001055 """
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001056 add_subdir_option(parser)
1057 parser.add_option(
1058 '-I', '--isolate-server', metavar='URL',
1059 default=os.environ.get('ISOLATE_SERVER', ''),
1060 help='URL of the isolate server')
1061 parser.add_option(
1062 '--namespace', metavar='NAME', default='default-gzip',
1063 help='Namespace to use on the isolate server')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001064 options, args = parser.parse_args(args)
1065 if args:
1066 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001067 if not options.isolate_server:
1068 parser.error('--isolate-server is required.')
1069 cwd = os.getcwd()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001070 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001071 success = False
1072 try:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001073 complete_state, infiles, isolated_hash = prepare_for_archival(
1074 options, cwd)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001075 logging.info('Creating content addressed object store with %d item',
1076 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001077
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001078 isolateserver.upload_tree(
1079 base_url=options.isolate_server,
1080 indir=complete_state.root_dir,
1081 infiles=infiles,
1082 namespace=options.namespace)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001083 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001084 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001085 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001086 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001087 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001088 if not success and os.path.isfile(options.isolated):
1089 os.remove(options.isolated)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001090 return int(not success)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001091
1092
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001093def CMDcheck(parser, args):
1094 """Checks that all the inputs are present and generates .isolated."""
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001095 add_subdir_option(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001096 options, args = parser.parse_args(args)
1097 if args:
1098 parser.error('Unsupported argument: %s' % args)
1099
1100 complete_state = load_complete_state(
1101 options, os.getcwd(), options.subdir, False)
1102
1103 # Nothing is done specifically. Just store the result and state.
1104 complete_state.save_files()
1105 return 0
1106
1107
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001108def CMDhashtable(parser, args):
1109 """Creates a .isolated file and stores the contains in a directory.
1110
1111 All the files listed in the .isolated file are put in the directory with their
1112 sha-1 as their file name. When using an NFS/CIFS server, the files can then be
1113 shared accross slaves without an isolate server.
1114 """
1115 add_subdir_option(parser)
1116 add_outdir_option(parser)
1117 add_skip_refresh_option(parser)
1118 options, args = parser.parse_args(args)
1119 if args:
1120 parser.error('Unsupported argument: %s' % args)
1121 cwd = os.getcwd()
1122 process_outdir(parser, options, cwd)
1123
1124 success = False
1125 try:
1126 complete_state, infiles, isolated_hash = prepare_for_archival(options, cwd)
1127 logging.info('Creating content addressed object store with %d item',
1128 len(infiles))
1129 if not os.path.isdir(options.outdir):
1130 os.makedirs(options.outdir)
1131
1132 # TODO(maruel): Make the files read-only?
1133 recreate_tree(
1134 outdir=options.outdir,
1135 indir=complete_state.root_dir,
1136 infiles=infiles,
1137 action=run_isolated.HARDLINK_WITH_FALLBACK,
1138 as_hash=True)
1139 success = True
1140 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
1141 finally:
1142 # If the command failed, delete the .isolated file if it exists. This is
1143 # important so no stale swarm job is executed.
1144 if not success and os.path.isfile(options.isolated):
1145 os.remove(options.isolated)
1146 return int(not success)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001147
1148
maruel@chromium.orge5322512013-08-19 20:17:57 +00001149def CMDmerge(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001150 """Reads and merges the data from the trace back into the original .isolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001151 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001152 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001153 add_trace_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001154 options, args = parser.parse_args(args)
1155 if args:
1156 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001157
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001158 complete_state = load_complete_state(options, os.getcwd(), None, False)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001159 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001160 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001161 return 0
1162
1163
maruel@chromium.orge5322512013-08-19 20:17:57 +00001164def CMDread(parser, args):
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001165 """Reads the trace file generated with command 'trace'."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001166 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001167 add_trace_option(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001168 add_skip_refresh_option(parser)
maruel@chromium.org29029882013-08-30 12:15:40 +00001169 parser.add_option(
1170 '-m', '--merge', action='store_true',
1171 help='merge the results back in the .isolate file instead of printing')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001172 options, args = parser.parse_args(args)
1173 if args:
1174 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001175
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001176 complete_state = load_complete_state(
1177 options, os.getcwd(), None, options.skip_refresh)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001178 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001179 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
maruel@chromium.org29029882013-08-30 12:15:40 +00001180 if options.merge:
1181 merge(complete_state, blacklist)
1182 else:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001183 isolate_format.pretty_print(value, sys.stdout)
maruel@chromium.org29029882013-08-30 12:15:40 +00001184
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001185 if exceptions:
1186 # It got an exception, raise the first one.
1187 raise \
1188 exceptions[0][0], \
1189 exceptions[0][1], \
1190 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001191 return 0
1192
1193
maruel@chromium.orge5322512013-08-19 20:17:57 +00001194def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001195 """Creates a directory with all the dependencies mapped into it.
1196
1197 Useful to test manually why a test is failing. The target executable is not
1198 run.
1199 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001200 parser.require_isolated = False
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001201 add_outdir_option(parser)
1202 add_skip_refresh_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001203 options, args = parser.parse_args(args)
1204 if args:
1205 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001206 cwd = os.getcwd()
1207 process_outdir(parser, options, cwd)
1208 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001209
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001210 if not os.path.isdir(options.outdir):
1211 os.makedirs(options.outdir)
1212 print('Remapping into %s' % options.outdir)
1213 if os.listdir(options.outdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001214 raise ExecutionError('Can\'t remap in a non-empty directory')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001215
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001216 create_isolate_tree(
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001217 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001218 complete_state.saved_state.relative_cwd,
1219 complete_state.saved_state.read_only)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001220 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001221 complete_state.save_files()
1222 return 0
1223
1224
maruel@chromium.orge5322512013-08-19 20:17:57 +00001225def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001226 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001227 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001228 options, args = parser.parse_args(args)
1229 if args:
1230 parser.error('Unsupported argument: %s' % args)
1231
1232 if options.isolated:
1233 # Load the previous state if it was present. Namely, "foo.isolated.state".
1234 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001235 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001236 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001237 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001238 if not isolate:
maruel@chromium.org29029882013-08-30 12:15:40 +00001239 parser.error('--isolate is required.')
1240
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001241 with open(isolate, 'r') as f:
1242 content = f.read()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001243 config = isolate_format.load_isolate_as_config(
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001244 os.path.dirname(os.path.abspath(isolate)),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001245 isolate_format.eval_content(content),
1246 isolate_format.extract_comment(content))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001247 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001248 print('Updating %s' % isolate)
1249 with open(isolate, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001250 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001251 return 0
1252
1253
maruel@chromium.org29029882013-08-30 12:15:40 +00001254@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001255def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001256 """Runs the test executable in an isolated (temporary) directory.
1257
1258 All the dependencies are mapped into the temporary directory and the
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001259 directory is cleaned up after the target exits.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001260
maruel@chromium.org29029882013-08-30 12:15:40 +00001261 Argument processing stops at -- and these arguments are appended to the
1262 command line of the target to run. For example, use:
1263 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001264 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001265 parser.require_isolated = False
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001266 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001267 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001268
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001269 complete_state = load_complete_state(
1270 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001271 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001272 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001273 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001274 cmd = tools.fix_python_path(cmd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001275
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001276 outdir = run_isolated.make_temp_dir(
1277 'isolate-%s' % datetime.date.today(),
1278 os.path.dirname(complete_state.root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001279 try:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -05001280 # TODO(maruel): Use run_isolated.run_tha_test().
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001281 cwd = create_isolate_tree(
1282 outdir, complete_state.root_dir, complete_state.saved_state.files,
1283 complete_state.saved_state.relative_cwd,
1284 complete_state.saved_state.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001285 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1286 result = subprocess.call(cmd, cwd=cwd)
1287 finally:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001288 run_isolated.rmtree(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001289
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001290 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001291 complete_state.save_files()
1292 return result
1293
1294
maruel@chromium.org29029882013-08-30 12:15:40 +00001295@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001296def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001297 """Traces the target using trace_inputs.py.
1298
1299 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001300 its child processes access. Then the 'merge' command can be used to generate
1301 an updated .isolate file out of it or the 'read' command to print it out to
1302 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001303
maruel@chromium.org29029882013-08-30 12:15:40 +00001304 Argument processing stops at -- and these arguments are appended to the
1305 command line of the target to run. For example, use:
1306 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001307 """
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001308 add_trace_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001309 parser.add_option(
1310 '-m', '--merge', action='store_true',
1311 help='After tracing, merge the results back in the .isolate file')
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001312 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001313 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001314
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001315 complete_state = load_complete_state(
1316 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001317 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001318 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001319 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001320 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001321 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001322 unicode(complete_state.root_dir),
1323 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001324 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1325 if not os.path.isfile(cmd[0]):
1326 raise ExecutionError(
1327 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001328 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1329 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001330 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001331 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001332 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001333 try:
1334 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001335 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001336 cmd,
1337 cwd,
1338 'default',
1339 True)
1340 except trace_inputs.TracingFailure, e:
1341 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1342
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001343 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001344 logging.error(
1345 'Tracer exited with %d, which means the tests probably failed so the '
1346 'trace is probably incomplete.', result)
1347 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001348
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001349 complete_state.save_files()
1350
1351 if options.merge:
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001352 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001353 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001354
1355 return result
1356
1357
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001358def _process_variable_arg(option, opt, _value, parser):
1359 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +00001360 if not parser.rargs:
1361 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001362 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001363 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001364 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001365 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001366 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001367 else:
1368 if not parser.rargs:
1369 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001370 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001371 v = parser.rargs.pop(0)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001372 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001373 raise optparse.OptionValueError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001374 'Variable \'%s\' doesn\'t respect format \'%s\'' %
1375 (k, isolate_format.VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -05001376 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001377
1378
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001379def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001380 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001381 parser.add_option(
1382 '-s', '--isolated',
1383 metavar='FILE',
1384 help='.isolated file to generate or read')
1385 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001386 parser.add_option(
1387 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001388 dest='isolated',
1389 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001390 is_win = sys.platform in ('win32', 'cygwin')
1391 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001392 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001393 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001394 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001395 # - configuration variables that are to be used in deducing the matrix to
1396 # reduce.
1397 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001398 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001399 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00001400 action='callback',
1401 callback=_process_variable_arg,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001402 default=[('OS', get_flavor())],
1403 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001404 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001405 help='Config variables are used to determine which conditions should be '
1406 'matched when loading a .isolate file, default: %default. '
1407 'All 3 kinds of variables are persistent accross calls, they are '
1408 'saved inside <.isolated>.state')
1409 parser.add_option(
1410 '--path-variable',
1411 action='callback',
1412 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001413 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001414 dest='path_variables',
1415 metavar='FOO BAR',
1416 help='Path variables are used to replace file paths when loading a '
1417 '.isolate file, default: %default')
1418 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001419 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001420 action='callback',
1421 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001422 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1423 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001424 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001425 help='Extraneous variables are replaced on the \'command\' entry and on '
1426 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001427
1428
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001429def add_subdir_option(parser):
1430 parser.add_option(
1431 '--subdir',
1432 help='Filters to a subdirectory. Its behavior changes depending if it '
1433 'is a relative path as a string or as a path variable. Path '
1434 'variables are always keyed from the directory containing the '
1435 '.isolate file. Anything else is keyed on the root directory.')
1436
1437
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001438def add_trace_option(parser):
1439 """Adds --trace-blacklist to the parser."""
1440 parser.add_option(
1441 '--trace-blacklist',
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001442 action='append', default=list(isolateserver.DEFAULT_BLACKLIST),
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001443 help='List of regexp to use as blacklist filter for files to consider '
1444 'important, not to be confused with --blacklist which blacklists '
1445 'test case.')
1446
1447
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001448def add_outdir_option(parser):
1449 parser.add_option(
1450 '-o', '--outdir', metavar='DIR',
1451 help='Directory used to recreate the tree.')
1452
1453
1454def add_skip_refresh_option(parser):
1455 parser.add_option(
1456 '--skip-refresh', action='store_true',
1457 help='Skip reading .isolate file and do not refresh the hash of '
1458 'dependencies')
1459
1460def process_outdir(parser, options, cwd):
1461 if not options.outdir:
1462 parser.error('--outdir is required.')
1463 if file_path.is_url(options.outdir):
1464 parser.error('Can\'t use an URL for --outdir with mode remap.')
1465 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1466 # outdir doesn't need native path case since tracing is never done from there.
1467 options.outdir = os.path.abspath(
1468 os.path.normpath(os.path.join(cwd, options.outdir)))
1469 # In theory, we'd create the directory outdir right away. Defer doing it in
1470 # case there's errors in the command line.
1471
1472
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001473def parse_isolated_option(parser, options, cwd, require_isolated):
1474 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001475 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001476 options.isolated = os.path.normpath(
1477 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001478 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001479 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001480 if options.isolated and not options.isolated.endswith('.isolated'):
1481 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001482
1483
1484def parse_variable_option(options):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001485 """Processes all the --<foo>-variable flags."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001486 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
1487 # but it wouldn't be backward compatible.
1488 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001489 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001490 try:
1491 return int(s)
1492 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001493 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001494 options.config_variables = dict(
1495 (k, try_make_int(v)) for k, v in options.config_variables)
1496 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001497 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001498
1499
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001500class OptionParserIsolate(tools.OptionParserWithLogging):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001501 """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
1502 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001503 # Set it to False if it is not required, e.g. it can be passed on but do not
1504 # fail if not given.
1505 require_isolated = True
1506
1507 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001508 tools.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00001509 self,
1510 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1511 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001512 group = optparse.OptionGroup(self, "Common options")
1513 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001514 '-i', '--isolate',
1515 metavar='FILE',
1516 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001517 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001518 group.add_option(
csharp@chromium.org01856802012-11-12 17:48:13 +00001519 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00001520 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1521 help='Indicates that invalid entries in the isolated file to be '
1522 'only be logged and not stop processing. Defaults to True if '
1523 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001524 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001525
1526 def parse_args(self, *args, **kwargs):
1527 """Makes sure the paths make sense.
1528
1529 On Windows, / and \ are often mixed together in a path.
1530 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001531 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001532 self, *args, **kwargs)
1533 if not self.allow_interspersed_args and args:
1534 self.error('Unsupported argument: %s' % args)
1535
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001536 cwd = file_path.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001537 parse_isolated_option(self, options, cwd, self.require_isolated)
1538 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001539
1540 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001541 # TODO(maruel): Work with non-ASCII.
1542 # The path must be in native path case for tracing purposes.
1543 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1544 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001545 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001546
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001547 return options, args
1548
1549
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001550def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00001551 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001552 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00001553 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001554 except Exception as e:
1555 tools.report_error(e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001556 return 1
1557
1558
1559if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00001560 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001561 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001562 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001563 sys.exit(main(sys.argv[1:]))