blob: 1b9a5bddab29d31b61712045c0f33f27340fc9e6 [file] [log] [blame]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001#!/usr/bin/env python
Marc-Antoine Ruel8add1242013-11-05 17:28:27 -05002# Copyright 2012 The Swarming Authors. All rights reserved.
Marc-Antoine Ruele98b1122013-11-05 20:27:57 -05003# Use of this source code is governed under the Apache License, Version 2.0 that
4# can be found in the LICENSE file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00005
maruel@chromium.orge5322512013-08-19 20:17:57 +00006"""Front end tool to operate on .isolate files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00007
maruel@chromium.orge5322512013-08-19 20:17:57 +00008This includes creating, merging or compiling them to generate a .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00009
10See more information at
maruel@chromium.orge5322512013-08-19 20:17:57 +000011 https://code.google.com/p/swarming/wiki/IsolateDesign
12 https://code.google.com/p/swarming/wiki/IsolateUserGuide
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000013"""
maruel@chromium.orge5322512013-08-19 20:17:57 +000014# Run ./isolate.py --help for more detailed information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000015
Marc-Antoine Ruel9dfdcc22014-01-08 14:14:18 -050016import datetime
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000017import logging
18import optparse
19import os
20import posixpath
21import re
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000022import subprocess
23import sys
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000024
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080025import auth
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050026import isolate_format
maruel@chromium.orgfb78d432013-08-28 21:22:40 +000027import isolateserver
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000028import run_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000029import trace_inputs
30
31# Import here directly so isolate is easier to use as a library.
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000032from run_isolated import get_flavor
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000033
maruel@chromium.orge5322512013-08-19 20:17:57 +000034from third_party import colorama
35from third_party.depot_tools import fix_encoding
36from third_party.depot_tools import subcommand
37
maruel@chromium.org561d4b22013-09-26 21:08:08 +000038from utils import file_path
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000039from utils import tools
40
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000041
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050042__version__ = '0.3.1'
maruel@chromium.org3d671992013-08-20 00:38:27 +000043
44
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000045class ExecutionError(Exception):
46 """A generic error occurred."""
47 def __str__(self):
48 return self.args[0]
49
50
51### Path handling code.
52
53
csharp@chromium.org01856802012-11-12 17:48:13 +000054def expand_directories_and_symlinks(indir, infiles, blacklist,
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +000055 follow_symlinks, ignore_broken_items):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000056 """Expands the directories and the symlinks, applies the blacklist and
57 verifies files exist.
58
59 Files are specified in os native path separator.
60 """
61 outfiles = []
62 for relfile in infiles:
csharp@chromium.org01856802012-11-12 17:48:13 +000063 try:
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -050064 outfiles.extend(
65 isolateserver.expand_directory_and_symlink(
66 indir, relfile, blacklist, follow_symlinks))
maruel@chromium.org9958e4a2013-09-17 00:01:48 +000067 except isolateserver.MappingError as e:
csharp@chromium.org01856802012-11-12 17:48:13 +000068 if ignore_broken_items:
69 logging.info('warning: %s', e)
70 else:
71 raise
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000072 return outfiles
73
74
maruel@chromium.org7b844a62013-09-17 13:04:59 +000075def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000076 """Creates a new tree with only the input files in it.
77
78 Arguments:
79 outdir: Output directory to create the files in.
80 indir: Root directory the infiles are based in.
81 infiles: dict of files to map from |indir| to |outdir|.
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000082 action: One of accepted action of run_isolated.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +000083 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000084 """
85 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +000086 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
87 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000088
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000089 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000090 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000091 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000092 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000093
94 for relfile, metadata in infiles.iteritems():
95 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +000096 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000097 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000098 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000099 # Skip links when storing a hashtable.
100 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000101 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000102 if os.path.isfile(outfile):
103 # Just do a quick check that the file size matches. No need to stat()
104 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000105 if not 's' in metadata:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000106 raise isolateserver.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +0000107 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000108 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000109 continue
110 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000111 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000112 os.remove(outfile)
113 else:
114 outfile = os.path.join(outdir, relfile)
115 outsubdir = os.path.dirname(outfile)
116 if not os.path.isdir(outsubdir):
117 os.makedirs(outsubdir)
118
119 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000120 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000121 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000122 if 'l' in metadata:
123 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000124 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000125 # symlink doesn't exist on Windows.
126 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000127 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000128 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000129
130
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000131### Variable stuff.
132
133
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500134def _normalize_path_variable(cwd, relative_base_dir, key, value):
135 """Normalizes a path variable into a relative directory.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500136 """
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500137 # Variables could contain / or \ on windows. Always normalize to
138 # os.path.sep.
139 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
140 normalized = file_path.get_native_path_case(os.path.normpath(x))
141 if not os.path.isdir(normalized):
142 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
143
144 # All variables are relative to the .isolate file.
145 normalized = os.path.relpath(normalized, relative_base_dir)
146 logging.debug(
147 'Translated variable %s from %s to %s', key, value, normalized)
148 return normalized
149
150
151def normalize_path_variables(cwd, path_variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000152 """Processes path variables as a special case and returns a copy of the dict.
153
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000154 For each 'path' variable: first normalizes it based on |cwd|, verifies it
155 exists then sets it as relative to relative_base_dir.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500156 """
157 logging.info(
158 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
159 relative_base_dir)
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -0500160 assert isinstance(cwd, unicode), cwd
161 assert isinstance(relative_base_dir, unicode), relative_base_dir
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500162 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
163 return dict(
164 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
165 for k, v in path_variables.iteritems())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000166
167
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500168### Internal state files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000169
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500170
171def isolatedfile_to_state(filename):
172 """For a '.isolate' file, returns the path to the saved '.state' file."""
173 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000174
175
176def classify_files(root_dir, tracked, untracked):
177 """Converts the list of files into a .isolate 'variables' dictionary.
178
179 Arguments:
180 - tracked: list of files names to generate a dictionary out of that should
181 probably be tracked.
182 - untracked: list of files names that must not be tracked.
183 """
184 # These directories are not guaranteed to be always present on every builder.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500185 CHROMIUM_OPTIONAL_DIRECTORIES = (
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000186 'test/data/plugin',
187 'third_party/WebKit/LayoutTests',
188 )
189
190 new_tracked = []
191 new_untracked = list(untracked)
192
193 def should_be_tracked(filepath):
194 """Returns True if it is a file without whitespace in a non-optional
195 directory that has no symlink in its path.
196 """
197 if filepath.endswith('/'):
198 return False
199 if ' ' in filepath:
200 return False
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500201 if any(i in filepath for i in CHROMIUM_OPTIONAL_DIRECTORIES):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000202 return False
203 # Look if any element in the path is a symlink.
204 split = filepath.split('/')
205 for i in range(len(split)):
206 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
207 return False
208 return True
209
210 for filepath in sorted(tracked):
211 if should_be_tracked(filepath):
212 new_tracked.append(filepath)
213 else:
214 # Anything else.
215 new_untracked.append(filepath)
216
217 variables = {}
218 if new_tracked:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500219 variables[isolate_format.KEY_TRACKED] = sorted(new_tracked)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000220 if new_untracked:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500221 variables[isolate_format.KEY_UNTRACKED] = sorted(new_untracked)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000222 return variables
223
224
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500225def chromium_fix(f, variables):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500226 """Fixes an isolate dependency with Chromium-specific fixes."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000227 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
228 # separator.
229 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000230 # Ignored items.
231 IGNORED_ITEMS = (
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000232 # http://crbug.com/160539, on Windows, it's in chrome/.
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000233 'Media Cache/',
maruel@chromium.orgd37462e2012-11-16 14:58:58 +0000234 'chrome/Media Cache/',
maruel@chromium.orga5c1c902012-11-15 18:47:53 +0000235 # 'First Run' is not created by the compile, but by the test itself.
236 '<(PRODUCT_DIR)/First Run')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000237
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000238 # Blacklist logs and other unimportant files.
239 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
240 logging.debug('Ignoring %s', f)
241 return None
242
maruel@chromium.org7650e422012-11-16 21:56:42 +0000243 EXECUTABLE = re.compile(
244 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500245 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
maruel@chromium.org7650e422012-11-16 21:56:42 +0000246 r'$')
247 match = EXECUTABLE.match(f)
248 if match:
249 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
250
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000251 if sys.platform == 'darwin':
252 # On OSX, the name of the output is dependent on gyp define, it can be
253 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
254 # Framework.framework'. Furthermore, they are versioned with a gyp
255 # variable. To lower the complexity of the .isolate file, remove all the
256 # individual entries that show up under any of the 4 entries and replace
257 # them with the directory itself. Overall, this results in a bit more
258 # files than strictly necessary.
259 OSX_BUNDLES = (
260 '<(PRODUCT_DIR)/Chromium Framework.framework/',
261 '<(PRODUCT_DIR)/Chromium.app/',
262 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
263 '<(PRODUCT_DIR)/Google Chrome.app/',
264 )
265 for prefix in OSX_BUNDLES:
266 if f.startswith(prefix):
267 # Note this result in duplicate values, so the a set() must be used to
268 # remove duplicates.
269 return prefix
270 return f
271
272
273def generate_simplified(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500274 tracked, untracked, touched, root_dir, path_variables, extra_variables,
275 relative_cwd, trace_blacklist):
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000276 """Generates a clean and complete .isolate 'variables' dictionary.
277
278 Cleans up and extracts only files from within root_dir then processes
279 variables and relative_cwd.
280 """
281 root_dir = os.path.realpath(root_dir)
282 logging.info(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500283 'generate_simplified(%d files, %s, %s, %s, %s)' %
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000284 (len(tracked) + len(untracked) + len(touched),
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500285 root_dir, path_variables, extra_variables, relative_cwd))
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000286
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000287 # Preparation work.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500288 relative_cwd = file_path.cleanup_path(relative_cwd)
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000289 assert not os.path.isabs(relative_cwd), relative_cwd
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500290
291 # Normalizes to posix path. .isolate files are using posix paths on all OSes
292 # for coherency.
maruel@chromium.org136b05a2012-11-20 18:49:44 +0000293 path_variables = dict(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500294 (k, v.replace(os.path.sep, '/')) for k, v in path_variables.iteritems())
295 # Contains normalized path_variables plus extra_variables.
296 total_variables = path_variables.copy()
297 total_variables.update(extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000298
299 # Actual work: Process the files.
300 # TODO(maruel): if all the files in a directory are in part tracked and in
301 # part untracked, the directory will not be extracted. Tracked files should be
302 # 'promoted' to be untracked as needed.
303 tracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000304 root_dir, tracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000305 untracked = trace_inputs.extract_directories(
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000306 root_dir, untracked, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000307 # touched is not compressed, otherwise it would result in files to be archived
308 # that we don't need.
309
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000310 root_dir_posix = root_dir.replace(os.path.sep, '/')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000311 def fix(f):
312 """Bases the file on the most restrictive variable."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000313 # Important, GYP stores the files with / and not \.
314 f = f.replace(os.path.sep, '/')
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000315 logging.debug('fix(%s)' % f)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000316 # If it's not already a variable.
317 if not f.startswith('<'):
318 # relative_cwd is usually the directory containing the gyp file. It may be
319 # empty if the whole directory containing the gyp file is needed.
maruel@chromium.org8b056ba2012-10-16 14:04:49 +0000320 # Use absolute paths in case cwd_dir is outside of root_dir.
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000321 # Convert the whole thing to / since it's isolate's speak.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500322 f = file_path.posix_relpath(
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000323 posixpath.join(root_dir_posix, f),
324 posixpath.join(root_dir_posix, relative_cwd)) or './'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000325
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500326 # Use the longest value first.
327 for key, value in sorted(
328 path_variables.iteritems(), key=lambda x: -len(x[1])):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500329 if f.startswith(value):
330 f = '<(%s)%s' % (key, f[len(value):])
331 logging.debug('Converted to %s' % f)
332 break
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000333 return f
334
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000335 def fix_all(items):
336 """Reduces the items to convert variables, removes unneeded items, apply
337 chromium-specific fixes and only return unique items.
338 """
339 variables_converted = (fix(f.path) for f in items)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500340 chromium_fixed = (
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500341 chromium_fix(f, total_variables) for f in variables_converted)
maruel@chromium.org2bbc9cd2012-11-15 19:35:32 +0000342 return set(f for f in chromium_fixed if f)
343
344 tracked = fix_all(tracked)
345 untracked = fix_all(untracked)
346 touched = fix_all(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000347 out = classify_files(root_dir, tracked, untracked)
348 if touched:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500349 out[isolate_format.KEY_TOUCHED] = sorted(touched)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000350 return out
351
352
353def generate_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500354 tracked, untracked, touched, root_dir, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500355 extra_variables, relative_cwd, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000356 """Generates a clean and complete .isolate file."""
benrg@chromium.org609b7982013-02-07 16:44:46 +0000357 dependencies = generate_simplified(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500358 tracked, untracked, touched, root_dir, path_variables, extra_variables,
359 relative_cwd, trace_blacklist)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000360 config_variable_names, config_values = zip(
361 *sorted(config_variables.iteritems()))
Marc-Antoine Ruel67d3c0a2014-01-10 09:12:39 -0500362 out = isolate_format.Configs(None, config_variable_names)
Marc-Antoine Ruel3ae9e6e2014-01-13 15:42:16 -0500363 # TODO(maruel): Create a public interface in Configs to add a ConfigSettings.
364 # pylint: disable=W0212
365 out._by_config[config_values] = isolate_format.ConfigSettings(dependencies)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000366 return out.make_isolate_file()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000367
368
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500369def chromium_save_isolated(isolated, data, path_variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000370 """Writes one or many .isolated files.
371
372 This slightly increases the cold cache cost but greatly reduce the warm cache
373 cost by splitting low-churn files off the master .isolated file. It also
374 reduces overall isolateserver memcache consumption.
375 """
376 slaves = []
377
378 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000379 new_slave = {
380 'algo': data['algo'],
381 'files': {},
382 'os': data['os'],
383 'version': data['version'],
384 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000385 for f in data['files'].keys():
386 if f.startswith(prefix):
387 new_slave['files'][f] = data['files'].pop(f)
388 if new_slave['files']:
389 slaves.append(new_slave)
390
391 # Split test/data/ in its own .isolated file.
392 extract_into_included_isolated(os.path.join('test', 'data', ''))
393
394 # Split everything out of PRODUCT_DIR in its own .isolated file.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500395 if path_variables.get('PRODUCT_DIR'):
396 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000397
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000398 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000399 for index, f in enumerate(slaves):
400 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500401 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000402 data.setdefault('includes', []).append(
403 isolateserver.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000404 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000405
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500406 files.extend(isolateserver.save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000407 return files
408
409
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000410class Flattenable(object):
411 """Represents data that can be represented as a json file."""
412 MEMBERS = ()
413
414 def flatten(self):
415 """Returns a json-serializable version of itself.
416
417 Skips None entries.
418 """
419 items = ((member, getattr(self, member)) for member in self.MEMBERS)
420 return dict((member, value) for member, value in items if value is not None)
421
422 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000423 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000424 """Loads a flattened version."""
425 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000426 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000427 for member in out.MEMBERS:
428 if member in data:
429 # Access to a protected member XXX of a client class
430 # pylint: disable=W0212
431 out._load_member(member, data.pop(member))
432 if data:
433 raise ValueError(
434 'Found unexpected entry %s while constructing an object %s' %
435 (data, cls.__name__), data, cls.__name__)
436 return out
437
438 def _load_member(self, member, value):
439 """Loads a member into self."""
440 setattr(self, member, value)
441
442 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000443 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000444 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000445 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500446 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000447 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000448 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000449 # On failure, loads the default instance.
450 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000451 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000452 return out
453
454
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000455class SavedState(Flattenable):
456 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000457
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000458 This file caches the items calculated by this script and is used to increase
459 the performance of the script. This file is not loaded by run_isolated.py.
460 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000461
462 It is important to note that the 'files' dict keys are using native OS path
463 separator instead of '/' used in .isolate file.
464 """
465 MEMBERS = (
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000466 # Algorithm used to generate the hash. The only supported value is at the
467 # time of writting 'sha-1'.
468 'algo',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000469 # Cache of the processed command. This value is saved because .isolated
470 # files are never loaded by isolate.py so it's the only way to load the
471 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000472 'command',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500473 # GYP variables that are used to generate conditions. The most frequent
474 # example is 'OS'.
475 'config_variables',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500476 # GYP variables that will be replaced in 'command' and paths but will not be
477 # considered a relative directory.
478 'extra_variables',
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000479 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000480 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000481 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000482 'isolate_file',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000483 # List of included .isolated files. Used to support/remember 'slave'
484 # .isolated files. Relative path to isolated_basedir.
485 'child_isolated_files',
486 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000487 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000488 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000489 'relative_cwd',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500490 # GYP variables used to generate the .isolated files paths based on path
491 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
492 'path_variables',
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000493 # Version of the file format in format 'major.minor'. Any non-breaking
494 # change must update minor. Any breaking change must update major.
495 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000496 )
497
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000498 def __init__(self, isolated_basedir):
499 """Creates an empty SavedState.
500
501 |isolated_basedir| is the directory where the .isolated and .isolated.state
502 files are saved.
503 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000504 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000505 assert os.path.isabs(isolated_basedir), isolated_basedir
506 assert os.path.isdir(isolated_basedir), isolated_basedir
507 self.isolated_basedir = isolated_basedir
508
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000509 # The default algorithm used.
510 self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500511 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000512 self.command = []
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500513 self.config_variables = {}
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500514 self.extra_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000515 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000516 self.isolate_file = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500517 self.path_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000518 self.read_only = None
519 self.relative_cwd = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500520 self.version = isolateserver.ISOLATED_FILE_VERSION
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000521
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500522 def update(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500523 self, isolate_file, path_variables, config_variables, extra_variables):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000524 """Updates the saved state with new data to keep GYP variables and internal
525 reference to the original .isolate file.
526 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +0000527 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000528 # Convert back to a relative path. On Windows, if the isolate and
529 # isolated files are on different drives, isolate_file will stay an absolute
530 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500531 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000532
533 # The same .isolate file should always be used to generate the .isolated and
534 # .isolated.state.
535 assert isolate_file == self.isolate_file or not self.isolate_file, (
536 isolate_file, self.isolate_file)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500537 self.config_variables.update(config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500538 self.extra_variables.update(extra_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000539 self.isolate_file = isolate_file
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500540 self.path_variables.update(path_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000541
542 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
543 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000544
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000545 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000546 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000547 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000548 self.command = command
549 # Add new files.
550 for f in infiles:
551 self.files.setdefault(f, {})
552 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000553 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000554 # Prune extraneous files that are not a dependency anymore.
555 for f in set(self.files).difference(set(infiles).union(touched)):
556 del self.files[f]
557 if read_only is not None:
558 self.read_only = read_only
559 self.relative_cwd = relative_cwd
560
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000561 def to_isolated(self):
562 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000563
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000564 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000565 """
566 def strip(data):
567 """Returns a 'files' entry with only the whitelisted keys."""
568 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
569
570 out = {
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000571 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000572 'files': dict(
573 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000574 'version': self.version,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000575 }
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500576 if self.config_variables.get('OS'):
577 out['os'] = self.config_variables['OS']
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000578 if self.command:
579 out['command'] = self.command
580 if self.read_only is not None:
581 out['read_only'] = self.read_only
582 if self.relative_cwd:
583 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000584 return out
585
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000586 @property
587 def isolate_filepath(self):
588 """Returns the absolute path of self.isolate_file."""
589 return os.path.normpath(
590 os.path.join(self.isolated_basedir, self.isolate_file))
591
592 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000593 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000594 def load(cls, data, isolated_basedir): # pylint: disable=W0221
595 """Special case loading to disallow different OS.
596
597 It is not possible to load a .isolated.state files from a different OS, this
598 file is saved in OS-specific format.
599 """
600 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500601 if data.get('os'):
602 out.config_variables['OS'] = data['os']
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000603
604 # Converts human readable form back into the proper class type.
605 algo = data.get('algo', 'sha-1')
606 if not algo in isolateserver.SUPPORTED_ALGOS:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000607 raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000608 out.algo = isolateserver.SUPPORTED_ALGOS[algo]
609
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500610 # Refuse the load non-exact version, even minor difference. This is unlike
611 # isolateserver.load_isolated(). This is because .isolated.state could have
612 # changed significantly even in minor version difference.
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000613 if not re.match(r'^(\d+)\.(\d+)$', out.version):
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000614 raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500615 if out.version != isolateserver.ISOLATED_FILE_VERSION:
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000616 raise isolateserver.ConfigError(
617 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000618
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000619 # The .isolate file must be valid. It could be absolute on Windows if the
620 # drive containing the .isolate and the drive containing the .isolated files
621 # differ.
622 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
623 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000624 return out
625
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000626 def flatten(self):
627 """Makes sure 'algo' is in human readable form."""
628 out = super(SavedState, self).flatten()
629 out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
630 return out
631
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000632 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500633 def dict_to_str(d):
634 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
635
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000636 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000637 out += ' command: %s\n' % self.command
638 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000639 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000640 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000641 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000642 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500643 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
644 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500645 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000646 return out
647
648
649class CompleteState(object):
650 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000651 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000652 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +0000653 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000654 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000655 # Contains the data to ease developer's use-case but that is not strictly
656 # necessary.
657 self.saved_state = saved_state
658
659 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000660 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000661 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000662 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000663 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000664 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000665 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000666 SavedState.load_file(
667 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000668
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500669 def load_isolate(
670 self, cwd, isolate_file, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500671 extra_variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000672 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000673 .isolate file.
674
675 Processes the loaded data, deduce root_dir, relative_cwd.
676 """
677 # Make sure to not depend on os.getcwd().
678 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000679 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000680 logging.info(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500681 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500682 cwd, isolate_file, path_variables, config_variables, extra_variables,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500683 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000684 relative_base_dir = os.path.dirname(isolate_file)
685
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500686 # Processes the variables.
687 path_variables = normalize_path_variables(
688 cwd, path_variables, relative_base_dir)
689 # Update the saved state.
690 self.saved_state.update(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500691 isolate_file, path_variables, config_variables, extra_variables)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500692 path_variables = self.saved_state.path_variables
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000693
694 with open(isolate_file, 'r') as f:
695 # At that point, variables are not replaced yet in command and infiles.
696 # infiles may contain directory entries and is in posix style.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500697 command, infiles, touched, read_only = (
698 isolate_format.load_isolate_for_config(
699 os.path.dirname(isolate_file), f.read(),
700 self.saved_state.config_variables))
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500701
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500702 total_variables = self.saved_state.path_variables.copy()
703 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500704 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500705 command = [
706 isolate_format.eval_variables(i, total_variables) for i in command
707 ]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500708
709 total_variables = self.saved_state.path_variables.copy()
710 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500711 infiles = [
712 isolate_format.eval_variables(f, total_variables) for f in infiles
713 ]
714 touched = [
715 isolate_format.eval_variables(f, total_variables) for f in touched
716 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000717 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +0000718 # form '../../foo/bar'. Note that path variables must be taken in account
719 # too, add them as if they were input files.
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500720 root_dir = isolate_format.determine_root_dir(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500721 relative_base_dir, infiles + touched +
722 self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000723 # The relative directory is automatically determined by the relative path
724 # between root_dir and the directory containing the .isolate file,
725 # isolate_base_dir.
726 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500727 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000728 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500729 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500730 if not file_path.path_starts_with(
731 root_dir, os.path.join(relative_base_dir, v)):
732 raise isolateserver.MappingError(
733 'Path variable %s=%r points outside the inferred root directory %s'
734 % (k, v, root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000735 # Normalize the files based to root_dir. It is important to keep the
736 # trailing os.path.sep at that step.
737 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500738 file_path.relpath(
739 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000740 for f in infiles
741 ]
742 touched = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500743 file_path.relpath(
744 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000745 for f in touched
746 ]
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500747 follow_symlinks = config_variables['OS'] != 'win'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000748 # Expand the directories by listing each file inside. Up to now, trailing
749 # os.path.sep must be kept. Do not expand 'touched'.
750 infiles = expand_directories_and_symlinks(
751 root_dir,
752 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +0000753 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000754 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +0000755 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000756
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000757 # If we ignore broken items then remove any missing touched items.
758 if ignore_broken_items:
759 original_touched_count = len(touched)
760 touched = [touch for touch in touched if os.path.exists(touch)]
761
762 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +0000763 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000764 len(touched) - original_touched_count)
765
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000766 # Finally, update the new data to be able to generate the foo.isolated file,
767 # the file that is used by run_isolated.py.
768 self.saved_state.update_isolated(
769 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000770 logging.debug(self)
771
maruel@chromium.org9268f042012-10-17 17:36:41 +0000772 def process_inputs(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000773 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000774
maruel@chromium.org9268f042012-10-17 17:36:41 +0000775 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
776 file is tainted.
777
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500778 See isolateserver.process_input() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000779 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000780 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +0000781 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000782 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000783 else:
784 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruelfcc3cd82013-11-19 16:31:38 -0500785 self.saved_state.files[infile] = isolateserver.process_input(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000786 filepath,
787 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000788 self.saved_state.read_only,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500789 self.saved_state.config_variables['OS'],
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000790 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000791
792 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000793 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000794 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000795 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000796 self.isolated_filepath,
797 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500798 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000799 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000800 total_bytes = sum(
801 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000802 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000803 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000804 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000805 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000806 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500807 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000808
809 @property
810 def root_dir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000811 """Returns the absolute path of the root_dir to reference the .isolate file
812 via relative_cwd.
813
814 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
815 to isolate_filepath.
816 """
maruel@chromium.orgb253fb82012-10-16 21:44:48 +0000817 if not self.saved_state.isolate_file:
818 raise ExecutionError('Please specify --isolate')
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000819 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000820 # Special case '.'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000821 if self.saved_state.relative_cwd == '.':
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000822 root_dir = isolate_dir
823 else:
maruel@chromium.org87557b92013-10-16 18:04:11 +0000824 if not isolate_dir.endswith(self.saved_state.relative_cwd):
825 raise ExecutionError(
826 ('Make sure the .isolate file is in the directory that will be '
827 'used as the relative directory. It is currently in %s and should '
828 'be in %s') % (isolate_dir, self.saved_state.relative_cwd))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000829 # Walk back back to the root directory.
830 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000831 return file_path.get_native_path_case(root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000832
833 @property
834 def resultdir(self):
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000835 """Returns the absolute path containing the .isolated file.
836
837 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
838 path as the value.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000839 """
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000840 return os.path.dirname(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000841
842 def __str__(self):
843 def indent(data, indent_length):
844 """Indents text."""
845 spacing = ' ' * indent_length
846 return ''.join(spacing + l for l in str(data).splitlines(True))
847
848 out = '%s(\n' % self.__class__.__name__
849 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000850 out += ' saved_state: %s)' % indent(self.saved_state, 2)
851 return out
852
853
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000854def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000855 """Loads a CompleteState.
856
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000857 This includes data from .isolate and .isolated.state files. Never reads the
858 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000859
860 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000861 options: Options instance generated with OptionParserIsolate. For either
862 options.isolate and options.isolated, if the value is set, it is an
863 absolute path.
864 cwd: base directory to be used when loading the .isolate file.
865 subdir: optional argument to only process file in the subdirectory, relative
866 to CompleteState.root_dir.
867 skip_update: Skip trying to load the .isolate file and processing the
868 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000869 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000870 assert not options.isolate or os.path.isabs(options.isolate)
871 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000872 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000873 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000874 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000875 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000876 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000877 else:
878 # Constructs a dummy object that cannot be saved. Useful for temporary
879 # commands like 'run'.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000880 complete_state = CompleteState(None, SavedState())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000881
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000882 if not options.isolate:
883 if not complete_state.saved_state.isolate_file:
884 if not skip_update:
885 raise ExecutionError('A .isolate file is required.')
886 isolate = None
887 else:
888 isolate = complete_state.saved_state.isolate_filepath
889 else:
890 isolate = options.isolate
891 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500892 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000893 options.isolate, complete_state.saved_state.isolated_basedir)
894 if rel_isolate != complete_state.saved_state.isolate_file:
895 raise ExecutionError(
896 '%s and %s do not match.' % (
897 options.isolate, complete_state.saved_state.isolate_file))
898
899 if not skip_update:
900 # Then load the .isolate and expands directories.
901 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500902 cwd, isolate, options.path_variables, options.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500903 options.extra_variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000904
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000905 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +0000906 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000907 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500908 # This is tricky here. If it is a path, take it from the root_dir. If
909 # it is a variable, it must be keyed from the directory containing the
910 # .isolate file. So translate all variables first.
911 translated_path_variables = dict(
912 (k,
913 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
914 v)))
915 for k, v in complete_state.saved_state.path_variables.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500916 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000917 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000918
919 if not skip_update:
920 complete_state.process_inputs(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000921 return complete_state
922
923
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000924def read_trace_as_isolate_dict(complete_state, trace_blacklist):
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000925 """Reads a trace and returns the .isolate dictionary.
926
927 Returns exceptions during the log parsing so it can be re-raised.
928 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000929 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000930 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000931 if not os.path.isfile(logfile):
932 raise ExecutionError(
933 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
934 try:
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000935 data = api.parse_log(logfile, trace_blacklist, None)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000936 exceptions = [i['exception'] for i in data if 'exception' in i]
937 results = (i['results'] for i in data if 'results' in i)
938 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
939 files = set(sum((result.existent for result in results_stripped), []))
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500940 tracked, touched = isolate_format.split_touched(files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000941 value = generate_isolate(
942 tracked,
943 [],
944 touched,
945 complete_state.root_dir,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500946 complete_state.saved_state.path_variables,
947 complete_state.saved_state.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500948 complete_state.saved_state.extra_variables,
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000949 complete_state.saved_state.relative_cwd,
950 trace_blacklist)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000951 return value, exceptions
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000952 except trace_inputs.TracingFailure, e:
953 raise ExecutionError(
954 'Reading traces failed for: %s\n%s' %
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000955 (' '.join(complete_state.saved_state.command), str(e)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000956
957
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000958def merge(complete_state, trace_blacklist):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000959 """Reads a trace and merges it back into the source .isolate file."""
maruel@chromium.org3683afe2013-07-27 00:09:27 +0000960 value, exceptions = read_trace_as_isolate_dict(
961 complete_state, trace_blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000962
963 # Now take that data and union it into the original .isolate file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000964 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000965 prev_content = f.read()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000966 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500967 prev_config = isolate_format.load_isolate_as_config(
maruel@chromium.org8007b8f2012-12-14 15:45:18 +0000968 isolate_dir,
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500969 isolate_format.eval_content(prev_content),
970 isolate_format.extract_comment(prev_content))
971 new_config = isolate_format.load_isolate_as_config(isolate_dir, value, '')
972 config = isolate_format.union(prev_config, new_config)
benrg@chromium.org609b7982013-02-07 16:44:46 +0000973 data = config.make_isolate_file()
maruel@chromium.orgec91af12012-10-18 20:45:57 +0000974 print('Updating %s' % complete_state.saved_state.isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000975 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500976 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.orge27c65f2012-10-22 13:56:53 +0000977 if exceptions:
978 # It got an exception, raise the first one.
979 raise \
980 exceptions[0][0], \
981 exceptions[0][1], \
982 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000983
984
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500985def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
986 """Creates a isolated tree usable for test execution.
987
988 Returns the current working directory where the isolated command should be
989 started in.
990 """
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500991 # Forcibly copy when the tree has to be read only. Otherwise the inode is
992 # modified, and this cause real problems because the user's source tree
993 # becomes read only. On the other hand, the cost of doing file copy is huge.
994 if read_only not in (0, None):
995 action = run_isolated.COPY
996 else:
997 action = run_isolated.HARDLINK_WITH_FALLBACK
998
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500999 recreate_tree(
1000 outdir=outdir,
1001 indir=root_dir,
1002 infiles=files,
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -05001003 action=action,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001004 as_hash=False)
1005 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
1006 if not os.path.isdir(cwd):
1007 # It can happen when no files are mapped from the directory containing the
1008 # .isolate file. But the directory must exist to be the current working
1009 # directory.
1010 os.makedirs(cwd)
1011 run_isolated.change_tree_read_only(outdir, read_only)
1012 return cwd
1013
1014
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001015def prepare_for_archival(options, cwd):
1016 """Loads the isolated file and create 'infiles' for archival."""
1017 complete_state = load_complete_state(
1018 options, cwd, options.subdir, False)
1019 # Make sure that complete_state isn't modified until save_files() is
1020 # called, because any changes made to it here will propagate to the files
1021 # created (which is probably not intended).
1022 complete_state.save_files()
1023
1024 infiles = complete_state.saved_state.files
1025 # Add all the .isolated files.
1026 isolated_hash = []
1027 isolated_files = [
1028 options.isolated,
1029 ] + complete_state.saved_state.child_isolated_files
1030 for item in isolated_files:
1031 item_path = os.path.join(
1032 os.path.dirname(complete_state.isolated_filepath), item)
1033 # Do not use isolateserver.hash_file() here because the file is
1034 # likely smallish (under 500kb) and its file size is needed.
1035 with open(item_path, 'rb') as f:
1036 content = f.read()
1037 isolated_hash.append(
1038 complete_state.saved_state.algo(content).hexdigest())
1039 isolated_metadata = {
1040 'h': isolated_hash[-1],
1041 's': len(content),
1042 'priority': '0'
1043 }
1044 infiles[item_path] = isolated_metadata
1045 return complete_state, infiles, isolated_hash
1046
1047
maruel@chromium.org29029882013-08-30 12:15:40 +00001048### Commands.
1049
1050
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001051def CMDarchive(parser, args):
1052 """Creates a .isolated file and uploads the tree to an isolate server.
maruel@chromium.org29029882013-08-30 12:15:40 +00001053
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001054 All the files listed in the .isolated file are put in the isolate server
1055 cache via isolateserver.py.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001056 """
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001057 add_subdir_option(parser)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001058 isolateserver.add_isolate_server_options(parser)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001059 auth.add_auth_options(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001060 options, args = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001061 auth.process_auth_options(parser, options)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001062 isolateserver.process_isolate_server_options(parser, options)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001063 if args:
1064 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001065 cwd = os.getcwd()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001066 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001067 success = False
1068 try:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001069 complete_state, infiles, isolated_hash = prepare_for_archival(
1070 options, cwd)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +00001071 logging.info('Creating content addressed object store with %d item',
1072 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001073
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001074 isolateserver.upload_tree(
1075 base_url=options.isolate_server,
1076 indir=complete_state.root_dir,
1077 infiles=infiles,
1078 namespace=options.namespace)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001079 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001080 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001081 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001082 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001083 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001084 if not success and os.path.isfile(options.isolated):
1085 os.remove(options.isolated)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001086 return int(not success)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001087
1088
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001089def CMDcheck(parser, args):
1090 """Checks that all the inputs are present and generates .isolated."""
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001091 add_subdir_option(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001092 options, args = parser.parse_args(args)
1093 if args:
1094 parser.error('Unsupported argument: %s' % args)
1095
1096 complete_state = load_complete_state(
1097 options, os.getcwd(), options.subdir, False)
1098
1099 # Nothing is done specifically. Just store the result and state.
1100 complete_state.save_files()
1101 return 0
1102
1103
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001104def CMDhashtable(parser, args):
1105 """Creates a .isolated file and stores the contains in a directory.
1106
1107 All the files listed in the .isolated file are put in the directory with their
1108 sha-1 as their file name. When using an NFS/CIFS server, the files can then be
1109 shared accross slaves without an isolate server.
1110 """
1111 add_subdir_option(parser)
1112 add_outdir_option(parser)
1113 add_skip_refresh_option(parser)
1114 options, args = parser.parse_args(args)
1115 if args:
1116 parser.error('Unsupported argument: %s' % args)
1117 cwd = os.getcwd()
1118 process_outdir(parser, options, cwd)
1119
1120 success = False
1121 try:
1122 complete_state, infiles, isolated_hash = prepare_for_archival(options, cwd)
1123 logging.info('Creating content addressed object store with %d item',
1124 len(infiles))
1125 if not os.path.isdir(options.outdir):
1126 os.makedirs(options.outdir)
1127
1128 # TODO(maruel): Make the files read-only?
1129 recreate_tree(
1130 outdir=options.outdir,
1131 indir=complete_state.root_dir,
1132 infiles=infiles,
1133 action=run_isolated.HARDLINK_WITH_FALLBACK,
1134 as_hash=True)
1135 success = True
1136 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
1137 finally:
1138 # If the command failed, delete the .isolated file if it exists. This is
1139 # important so no stale swarm job is executed.
1140 if not success and os.path.isfile(options.isolated):
1141 os.remove(options.isolated)
1142 return int(not success)
maruel@chromium.org2f952d82013-09-13 01:53:17 +00001143
1144
maruel@chromium.orge5322512013-08-19 20:17:57 +00001145def CMDmerge(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001146 """Reads and merges the data from the trace back into the original .isolate.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001147 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001148 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001149 add_trace_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001150 options, args = parser.parse_args(args)
1151 if args:
1152 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001153
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001154 complete_state = load_complete_state(options, os.getcwd(), None, False)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001155 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001156 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001157 return 0
1158
1159
maruel@chromium.orge5322512013-08-19 20:17:57 +00001160def CMDread(parser, args):
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001161 """Reads the trace file generated with command 'trace'."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001162 parser.require_isolated = False
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001163 add_trace_option(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001164 add_skip_refresh_option(parser)
maruel@chromium.org29029882013-08-30 12:15:40 +00001165 parser.add_option(
1166 '-m', '--merge', action='store_true',
1167 help='merge the results back in the .isolate file instead of printing')
maruel@chromium.org9268f042012-10-17 17:36:41 +00001168 options, args = parser.parse_args(args)
1169 if args:
1170 parser.error('Unsupported argument: %s' % args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001171
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001172 complete_state = load_complete_state(
1173 options, os.getcwd(), None, options.skip_refresh)
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001174 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001175 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
maruel@chromium.org29029882013-08-30 12:15:40 +00001176 if options.merge:
1177 merge(complete_state, blacklist)
1178 else:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001179 isolate_format.pretty_print(value, sys.stdout)
maruel@chromium.org29029882013-08-30 12:15:40 +00001180
maruel@chromium.orge27c65f2012-10-22 13:56:53 +00001181 if exceptions:
1182 # It got an exception, raise the first one.
1183 raise \
1184 exceptions[0][0], \
1185 exceptions[0][1], \
1186 exceptions[0][2]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001187 return 0
1188
1189
maruel@chromium.orge5322512013-08-19 20:17:57 +00001190def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001191 """Creates a directory with all the dependencies mapped into it.
1192
1193 Useful to test manually why a test is failing. The target executable is not
1194 run.
1195 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001196 parser.require_isolated = False
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001197 add_outdir_option(parser)
1198 add_skip_refresh_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +00001199 options, args = parser.parse_args(args)
1200 if args:
1201 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001202 cwd = os.getcwd()
1203 process_outdir(parser, options, cwd)
1204 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001205
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001206 if not os.path.isdir(options.outdir):
1207 os.makedirs(options.outdir)
1208 print('Remapping into %s' % options.outdir)
1209 if os.listdir(options.outdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001210 raise ExecutionError('Can\'t remap in a non-empty directory')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001211
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001212 create_isolate_tree(
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001213 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001214 complete_state.saved_state.relative_cwd,
1215 complete_state.saved_state.read_only)
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001216 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001217 complete_state.save_files()
1218 return 0
1219
1220
maruel@chromium.orge5322512013-08-19 20:17:57 +00001221def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001222 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +00001223 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001224 options, args = parser.parse_args(args)
1225 if args:
1226 parser.error('Unsupported argument: %s' % args)
1227
1228 if options.isolated:
1229 # Load the previous state if it was present. Namely, "foo.isolated.state".
1230 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001231 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001232 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001233 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001234 if not isolate:
maruel@chromium.org29029882013-08-30 12:15:40 +00001235 parser.error('--isolate is required.')
1236
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001237 with open(isolate, 'r') as f:
1238 content = f.read()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001239 config = isolate_format.load_isolate_as_config(
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001240 os.path.dirname(os.path.abspath(isolate)),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001241 isolate_format.eval_content(content),
1242 isolate_format.extract_comment(content))
benrg@chromium.org609b7982013-02-07 16:44:46 +00001243 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001244 print('Updating %s' % isolate)
1245 with open(isolate, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001246 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +00001247 return 0
1248
1249
maruel@chromium.org29029882013-08-30 12:15:40 +00001250@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001251def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001252 """Runs the test executable in an isolated (temporary) directory.
1253
1254 All the dependencies are mapped into the temporary directory and the
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001255 directory is cleaned up after the target exits.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001256
maruel@chromium.org29029882013-08-30 12:15:40 +00001257 Argument processing stops at -- and these arguments are appended to the
1258 command line of the target to run. For example, use:
1259 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001260 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001261 parser.require_isolated = False
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001262 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001263 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001264
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001265 complete_state = load_complete_state(
1266 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001267 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001268 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001269 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001270 cmd = tools.fix_python_path(cmd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001271
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001272 outdir = run_isolated.make_temp_dir(
1273 'isolate-%s' % datetime.date.today(),
1274 os.path.dirname(complete_state.root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001275 try:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -05001276 # TODO(maruel): Use run_isolated.run_tha_test().
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001277 cwd = create_isolate_tree(
1278 outdir, complete_state.root_dir, complete_state.saved_state.files,
1279 complete_state.saved_state.relative_cwd,
1280 complete_state.saved_state.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001281 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1282 result = subprocess.call(cmd, cwd=cwd)
1283 finally:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001284 run_isolated.rmtree(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001285
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001286 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001287 complete_state.save_files()
1288 return result
1289
1290
maruel@chromium.org29029882013-08-30 12:15:40 +00001291@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +00001292def CMDtrace(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001293 """Traces the target using trace_inputs.py.
1294
1295 It runs the executable without remapping it, and traces all the files it and
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001296 its child processes access. Then the 'merge' command can be used to generate
1297 an updated .isolate file out of it or the 'read' command to print it out to
1298 stdout.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001299
maruel@chromium.org29029882013-08-30 12:15:40 +00001300 Argument processing stops at -- and these arguments are appended to the
1301 command line of the target to run. For example, use:
1302 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001303 """
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001304 add_trace_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001305 parser.add_option(
1306 '-m', '--merge', action='store_true',
1307 help='After tracing, merge the results back in the .isolate file')
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001308 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001309 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +00001310
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001311 complete_state = load_complete_state(
1312 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001313 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001314 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001315 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001316 cmd = tools.fix_python_path(cmd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001317 cwd = os.path.normpath(os.path.join(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001318 unicode(complete_state.root_dir),
1319 complete_state.saved_state.relative_cwd))
maruel@chromium.org808f6af2012-10-11 14:08:08 +00001320 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1321 if not os.path.isfile(cmd[0]):
1322 raise ExecutionError(
1323 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001324 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1325 api = trace_inputs.get_api()
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001326 logfile = complete_state.isolated_filepath + '.log'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001327 api.clean_trace(logfile)
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001328 out = None
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001329 try:
1330 with api.get_tracer(logfile) as tracer:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001331 result, out = tracer.trace(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001332 cmd,
1333 cwd,
1334 'default',
1335 True)
1336 except trace_inputs.TracingFailure, e:
1337 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1338
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001339 if result:
maruel@chromium.orgb9322142013-01-22 18:49:46 +00001340 logging.error(
1341 'Tracer exited with %d, which means the tests probably failed so the '
1342 'trace is probably incomplete.', result)
1343 logging.info(out)
csharp@chromium.org5ab1ca92012-10-25 13:37:14 +00001344
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001345 complete_state.save_files()
1346
1347 if options.merge:
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001348 blacklist = tools.gen_blacklist(options.trace_blacklist)
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001349 merge(complete_state, blacklist)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001350
1351 return result
1352
1353
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001354def _process_variable_arg(option, opt, _value, parser):
1355 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +00001356 if not parser.rargs:
1357 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001358 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001359 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001360 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001361 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001362 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001363 else:
1364 if not parser.rargs:
1365 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001366 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001367 v = parser.rargs.pop(0)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001368 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001369 raise optparse.OptionValueError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001370 'Variable \'%s\' doesn\'t respect format \'%s\'' %
1371 (k, isolate_format.VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -05001372 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001373
1374
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001375def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001376 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001377 parser.add_option(
1378 '-s', '--isolated',
1379 metavar='FILE',
1380 help='.isolated file to generate or read')
1381 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001382 parser.add_option(
1383 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001384 dest='isolated',
1385 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001386 is_win = sys.platform in ('win32', 'cygwin')
1387 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001388 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001389 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001390 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001391 # - configuration variables that are to be used in deducing the matrix to
1392 # reduce.
1393 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001394 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001395 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00001396 action='callback',
1397 callback=_process_variable_arg,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001398 default=[('OS', get_flavor())],
1399 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001400 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001401 help='Config variables are used to determine which conditions should be '
1402 'matched when loading a .isolate file, default: %default. '
1403 'All 3 kinds of variables are persistent accross calls, they are '
1404 'saved inside <.isolated>.state')
1405 parser.add_option(
1406 '--path-variable',
1407 action='callback',
1408 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001409 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001410 dest='path_variables',
1411 metavar='FOO BAR',
1412 help='Path variables are used to replace file paths when loading a '
1413 '.isolate file, default: %default')
1414 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001415 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001416 action='callback',
1417 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001418 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1419 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001420 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001421 help='Extraneous variables are replaced on the \'command\' entry and on '
1422 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001423
1424
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001425def add_subdir_option(parser):
1426 parser.add_option(
1427 '--subdir',
1428 help='Filters to a subdirectory. Its behavior changes depending if it '
1429 'is a relative path as a string or as a path variable. Path '
1430 'variables are always keyed from the directory containing the '
1431 '.isolate file. Anything else is keyed on the root directory.')
1432
1433
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001434def add_trace_option(parser):
1435 """Adds --trace-blacklist to the parser."""
1436 parser.add_option(
1437 '--trace-blacklist',
Marc-Antoine Ruelac54cb42013-11-18 14:05:35 -05001438 action='append', default=list(isolateserver.DEFAULT_BLACKLIST),
maruel@chromium.org3683afe2013-07-27 00:09:27 +00001439 help='List of regexp to use as blacklist filter for files to consider '
1440 'important, not to be confused with --blacklist which blacklists '
1441 'test case.')
1442
1443
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001444def add_outdir_option(parser):
1445 parser.add_option(
1446 '-o', '--outdir', metavar='DIR',
1447 help='Directory used to recreate the tree.')
1448
1449
1450def add_skip_refresh_option(parser):
1451 parser.add_option(
1452 '--skip-refresh', action='store_true',
1453 help='Skip reading .isolate file and do not refresh the hash of '
1454 'dependencies')
1455
1456def process_outdir(parser, options, cwd):
1457 if not options.outdir:
1458 parser.error('--outdir is required.')
1459 if file_path.is_url(options.outdir):
1460 parser.error('Can\'t use an URL for --outdir with mode remap.')
1461 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1462 # outdir doesn't need native path case since tracing is never done from there.
1463 options.outdir = os.path.abspath(
1464 os.path.normpath(os.path.join(cwd, options.outdir)))
1465 # In theory, we'd create the directory outdir right away. Defer doing it in
1466 # case there's errors in the command line.
1467
1468
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001469def parse_isolated_option(parser, options, cwd, require_isolated):
1470 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001471 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001472 options.isolated = os.path.normpath(
1473 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001474 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001475 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001476 if options.isolated and not options.isolated.endswith('.isolated'):
1477 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001478
1479
1480def parse_variable_option(options):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001481 """Processes all the --<foo>-variable flags."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001482 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
1483 # but it wouldn't be backward compatible.
1484 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001485 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001486 try:
1487 return int(s)
1488 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001489 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001490 options.config_variables = dict(
1491 (k, try_make_int(v)) for k, v in options.config_variables)
1492 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001493 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001494
1495
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001496class OptionParserIsolate(tools.OptionParserWithLogging):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001497 """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
1498 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001499 # Set it to False if it is not required, e.g. it can be passed on but do not
1500 # fail if not given.
1501 require_isolated = True
1502
1503 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001504 tools.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00001505 self,
1506 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1507 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001508 group = optparse.OptionGroup(self, "Common options")
1509 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001510 '-i', '--isolate',
1511 metavar='FILE',
1512 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001513 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001514 group.add_option(
csharp@chromium.org01856802012-11-12 17:48:13 +00001515 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00001516 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1517 help='Indicates that invalid entries in the isolated file to be '
1518 'only be logged and not stop processing. Defaults to True if '
1519 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001520 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001521
1522 def parse_args(self, *args, **kwargs):
1523 """Makes sure the paths make sense.
1524
1525 On Windows, / and \ are often mixed together in a path.
1526 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001527 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001528 self, *args, **kwargs)
1529 if not self.allow_interspersed_args and args:
1530 self.error('Unsupported argument: %s' % args)
1531
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001532 cwd = file_path.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001533 parse_isolated_option(self, options, cwd, self.require_isolated)
1534 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001535
1536 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001537 # TODO(maruel): Work with non-ASCII.
1538 # The path must be in native path case for tracing purposes.
1539 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1540 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001541 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001542
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001543 return options, args
1544
1545
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001546def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00001547 dispatcher = subcommand.CommandDispatcher(__name__)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001548 try:
maruel@chromium.org3d671992013-08-20 00:38:27 +00001549 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001550 except Exception as e:
1551 tools.report_error(e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001552 return 1
1553
1554
1555if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00001556 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001557 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001558 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001559 sys.exit(main(sys.argv[1:]))