blob: bf6047e32e45bb2d2d29c10cef9a6854ef88f870 [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
Vadim Shtayurafddb1432014-09-30 18:32:41 -070016__version__ = '0.4.1'
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040017
Marc-Antoine Ruel9dfdcc22014-01-08 14:14:18 -050018import datetime
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000019import logging
20import optparse
21import os
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000022import re
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000023import subprocess
24import sys
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000025
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080026import auth
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050027import isolate_format
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040028import isolated_format
maruel@chromium.orgfb78d432013-08-28 21:22:40 +000029import isolateserver
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000030import run_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000031
maruel@chromium.orge5322512013-08-19 20:17:57 +000032from third_party import colorama
33from third_party.depot_tools import fix_encoding
34from third_party.depot_tools import subcommand
35
maruel@chromium.org561d4b22013-09-26 21:08:08 +000036from utils import file_path
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000037from utils import tools
38
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000039
Vadim Shtayurafddb1432014-09-30 18:32:41 -070040# Supported version of *.isolated.gen.json files consumed by CMDbatcharchive.
41ISOLATED_GEN_JSON_VERSION = 1
42
43
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000044class ExecutionError(Exception):
45 """A generic error occurred."""
46 def __str__(self):
47 return self.args[0]
48
49
50### Path handling code.
51
52
maruel@chromium.org7b844a62013-09-17 13:04:59 +000053def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000054 """Creates a new tree with only the input files in it.
55
56 Arguments:
57 outdir: Output directory to create the files in.
58 indir: Root directory the infiles are based in.
59 infiles: dict of files to map from |indir| to |outdir|.
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000060 action: One of accepted action of run_isolated.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +000061 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000062 """
63 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +000064 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
65 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000066
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000067 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000068 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000069 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000070 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000071
72 for relfile, metadata in infiles.iteritems():
73 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +000074 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000075 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000076 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000077 # Skip links when storing a hashtable.
78 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +000079 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000080 if os.path.isfile(outfile):
81 # Just do a quick check that the file size matches. No need to stat()
82 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000083 if not 's' in metadata:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -040084 raise isolated_format.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +000085 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +000086 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000087 continue
88 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +000089 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000090 os.remove(outfile)
91 else:
92 outfile = os.path.join(outdir, relfile)
93 outsubdir = os.path.dirname(outfile)
94 if not os.path.isdir(outsubdir):
95 os.makedirs(outsubdir)
96
maruel@chromium.orge5c17132012-11-21 18:18:46 +000097 if 'l' in metadata:
98 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000099 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000100 # symlink doesn't exist on Windows.
101 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000102 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000103 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000104
105
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000106### Variable stuff.
107
108
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500109def _normalize_path_variable(cwd, relative_base_dir, key, value):
110 """Normalizes a path variable into a relative directory.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500111 """
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500112 # Variables could contain / or \ on windows. Always normalize to
113 # os.path.sep.
114 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
115 normalized = file_path.get_native_path_case(os.path.normpath(x))
116 if not os.path.isdir(normalized):
117 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
118
119 # All variables are relative to the .isolate file.
120 normalized = os.path.relpath(normalized, relative_base_dir)
121 logging.debug(
122 'Translated variable %s from %s to %s', key, value, normalized)
123 return normalized
124
125
126def normalize_path_variables(cwd, path_variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000127 """Processes path variables as a special case and returns a copy of the dict.
128
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000129 For each 'path' variable: first normalizes it based on |cwd|, verifies it
130 exists then sets it as relative to relative_base_dir.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500131 """
132 logging.info(
133 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
134 relative_base_dir)
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -0500135 assert isinstance(cwd, unicode), cwd
136 assert isinstance(relative_base_dir, unicode), relative_base_dir
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500137 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
138 return dict(
139 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
140 for k, v in path_variables.iteritems())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000141
142
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500143### Internal state files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000144
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500145
146def isolatedfile_to_state(filename):
147 """For a '.isolate' file, returns the path to the saved '.state' file."""
148 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000149
150
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500151def chromium_save_isolated(isolated, data, path_variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000152 """Writes one or many .isolated files.
153
154 This slightly increases the cold cache cost but greatly reduce the warm cache
155 cost by splitting low-churn files off the master .isolated file. It also
156 reduces overall isolateserver memcache consumption.
157 """
158 slaves = []
159
160 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000161 new_slave = {
162 'algo': data['algo'],
163 'files': {},
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000164 'version': data['version'],
165 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000166 for f in data['files'].keys():
167 if f.startswith(prefix):
168 new_slave['files'][f] = data['files'].pop(f)
169 if new_slave['files']:
170 slaves.append(new_slave)
171
172 # Split test/data/ in its own .isolated file.
173 extract_into_included_isolated(os.path.join('test', 'data', ''))
174
175 # Split everything out of PRODUCT_DIR in its own .isolated file.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500176 if path_variables.get('PRODUCT_DIR'):
177 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000178
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000179 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000180 for index, f in enumerate(slaves):
181 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500182 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000183 data.setdefault('includes', []).append(
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400184 isolated_format.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000185 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000186
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400187 files.extend(isolated_format.save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000188 return files
189
190
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000191class Flattenable(object):
192 """Represents data that can be represented as a json file."""
193 MEMBERS = ()
194
195 def flatten(self):
196 """Returns a json-serializable version of itself.
197
198 Skips None entries.
199 """
200 items = ((member, getattr(self, member)) for member in self.MEMBERS)
201 return dict((member, value) for member, value in items if value is not None)
202
203 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000204 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000205 """Loads a flattened version."""
206 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000207 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000208 for member in out.MEMBERS:
209 if member in data:
210 # Access to a protected member XXX of a client class
211 # pylint: disable=W0212
212 out._load_member(member, data.pop(member))
213 if data:
214 raise ValueError(
215 'Found unexpected entry %s while constructing an object %s' %
216 (data, cls.__name__), data, cls.__name__)
217 return out
218
219 def _load_member(self, member, value):
220 """Loads a member into self."""
221 setattr(self, member, value)
222
223 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000224 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000225 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000226 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500227 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000228 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000229 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000230 # On failure, loads the default instance.
231 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000232 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000233 return out
234
235
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000236class SavedState(Flattenable):
237 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000238
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000239 This file caches the items calculated by this script and is used to increase
240 the performance of the script. This file is not loaded by run_isolated.py.
241 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000242
243 It is important to note that the 'files' dict keys are using native OS path
244 separator instead of '/' used in .isolate file.
245 """
246 MEMBERS = (
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400247 # Value of sys.platform so that the file is rejected if loaded from a
248 # different OS. While this should never happen in practice, users are ...
249 # "creative".
250 'OS',
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000251 # Algorithm used to generate the hash. The only supported value is at the
252 # time of writting 'sha-1'.
253 'algo',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400254 # List of included .isolated files. Used to support/remember 'slave'
255 # .isolated files. Relative path to isolated_basedir.
256 'child_isolated_files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000257 # Cache of the processed command. This value is saved because .isolated
258 # files are never loaded by isolate.py so it's the only way to load the
259 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000260 'command',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500261 # GYP variables that are used to generate conditions. The most frequent
262 # example is 'OS'.
263 'config_variables',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500264 # GYP variables that will be replaced in 'command' and paths but will not be
265 # considered a relative directory.
266 'extra_variables',
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000267 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000268 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000269 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000270 'isolate_file',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400271 # GYP variables used to generate the .isolated files paths based on path
272 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
273 'path_variables',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000274 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000275 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000276 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000277 'relative_cwd',
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400278 # Root directory the files are mapped from.
279 'root_dir',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400280 # Version of the saved state file format. Any breaking change must update
281 # the value.
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000282 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000283 )
284
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400285 # Bump this version whenever the saved state changes. It is also keyed on the
286 # .isolated file version so any change in the generator will invalidate .state
287 # files.
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400288 EXPECTED_VERSION = isolated_format.ISOLATED_FILE_VERSION + '.2'
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400289
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000290 def __init__(self, isolated_basedir):
291 """Creates an empty SavedState.
292
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400293 Arguments:
294 isolated_basedir: the directory where the .isolated and .isolated.state
295 files are saved.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000296 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000297 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000298 assert os.path.isabs(isolated_basedir), isolated_basedir
299 assert os.path.isdir(isolated_basedir), isolated_basedir
300 self.isolated_basedir = isolated_basedir
301
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000302 # The default algorithm used.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400303 self.OS = sys.platform
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400304 self.algo = isolated_format.SUPPORTED_ALGOS['sha-1']
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500305 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000306 self.command = []
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500307 self.config_variables = {}
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500308 self.extra_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000309 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000310 self.isolate_file = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500311 self.path_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000312 self.read_only = None
313 self.relative_cwd = None
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400314 self.root_dir = None
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400315 self.version = self.EXPECTED_VERSION
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000316
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400317 def update_config(self, config_variables):
318 """Updates the saved state with only config variables."""
319 self.config_variables.update(config_variables)
320
321 def update(self, isolate_file, path_variables, extra_variables):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000322 """Updates the saved state with new data to keep GYP variables and internal
323 reference to the original .isolate file.
324 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +0000325 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000326 # Convert back to a relative path. On Windows, if the isolate and
327 # isolated files are on different drives, isolate_file will stay an absolute
328 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500329 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000330
331 # The same .isolate file should always be used to generate the .isolated and
332 # .isolated.state.
333 assert isolate_file == self.isolate_file or not self.isolate_file, (
334 isolate_file, self.isolate_file)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500335 self.extra_variables.update(extra_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000336 self.isolate_file = isolate_file
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500337 self.path_variables.update(path_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000338
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400339 def update_isolated(self, command, infiles, read_only, relative_cwd):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000340 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000341
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000342 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000343 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000344 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000345 self.command = command
346 # Add new files.
347 for f in infiles:
348 self.files.setdefault(f, {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000349 # Prune extraneous files that are not a dependency anymore.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400350 for f in set(self.files).difference(set(infiles)):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000351 del self.files[f]
352 if read_only is not None:
353 self.read_only = read_only
354 self.relative_cwd = relative_cwd
355
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000356 def to_isolated(self):
357 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000358
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000359 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000360 """
361 def strip(data):
362 """Returns a 'files' entry with only the whitelisted keys."""
363 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
364
365 out = {
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400366 'algo': isolated_format.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000367 'files': dict(
368 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400369 # The version of the .state file is different than the one of the
370 # .isolated file.
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400371 'version': isolated_format.ISOLATED_FILE_VERSION,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000372 }
373 if self.command:
374 out['command'] = self.command
375 if self.read_only is not None:
376 out['read_only'] = self.read_only
377 if self.relative_cwd:
378 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000379 return out
380
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000381 @property
382 def isolate_filepath(self):
383 """Returns the absolute path of self.isolate_file."""
384 return os.path.normpath(
385 os.path.join(self.isolated_basedir, self.isolate_file))
386
387 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000388 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000389 def load(cls, data, isolated_basedir): # pylint: disable=W0221
390 """Special case loading to disallow different OS.
391
392 It is not possible to load a .isolated.state files from a different OS, this
393 file is saved in OS-specific format.
394 """
395 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400396 if data.get('OS') != sys.platform:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400397 raise isolated_format.IsolatedError('Unexpected OS %s', data.get('OS'))
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000398
399 # Converts human readable form back into the proper class type.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400400 algo = data.get('algo')
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400401 if not algo in isolated_format.SUPPORTED_ALGOS:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400402 raise isolated_format.IsolatedError('Unknown algo \'%s\'' % out.algo)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400403 out.algo = isolated_format.SUPPORTED_ALGOS[algo]
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000404
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500405 # Refuse the load non-exact version, even minor difference. This is unlike
406 # isolateserver.load_isolated(). This is because .isolated.state could have
407 # changed significantly even in minor version difference.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400408 if out.version != cls.EXPECTED_VERSION:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400409 raise isolated_format.IsolatedError(
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000410 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000411
Marc-Antoine Ruel16ebc2e2014-02-13 15:39:15 -0500412 # The .isolate file must be valid. If it is not present anymore, zap the
413 # value as if it was not noted, so .isolate_file can safely be overriden
414 # later.
415 if out.isolate_file and not os.path.isfile(out.isolate_filepath):
416 out.isolate_file = None
417 if out.isolate_file:
418 # It could be absolute on Windows if the drive containing the .isolate and
419 # the drive containing the .isolated files differ, .e.g .isolate is on
420 # C:\\ and .isolated is on D:\\ .
421 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
422 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000423 return out
424
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000425 def flatten(self):
426 """Makes sure 'algo' is in human readable form."""
427 out = super(SavedState, self).flatten()
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400428 out['algo'] = isolated_format.SUPPORTED_ALGOS_REVERSE[out['algo']]
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000429 return out
430
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000431 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500432 def dict_to_str(d):
433 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
434
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000435 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000436 out += ' command: %s\n' % self.command
437 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000438 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000439 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000440 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000441 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500442 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
443 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500444 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000445 return out
446
447
448class CompleteState(object):
449 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000450 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000451 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +0000452 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000453 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000454 # Contains the data to ease developer's use-case but that is not strictly
455 # necessary.
456 self.saved_state = saved_state
457
458 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000459 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000460 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000461 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000462 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000463 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000464 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000465 SavedState.load_file(
466 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000467
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500468 def load_isolate(
469 self, cwd, isolate_file, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500470 extra_variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000471 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000472 .isolate file.
473
474 Processes the loaded data, deduce root_dir, relative_cwd.
475 """
476 # Make sure to not depend on os.getcwd().
477 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000478 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000479 logging.info(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500480 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500481 cwd, isolate_file, path_variables, config_variables, extra_variables,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500482 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000483
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400484 # Config variables are not affected by the paths and must be used to
485 # retrieve the paths, so update them first.
486 self.saved_state.update_config(config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000487
488 with open(isolate_file, 'r') as f:
489 # At that point, variables are not replaced yet in command and infiles.
490 # infiles may contain directory entries and is in posix style.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400491 command, infiles, read_only, isolate_cmd_dir = (
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500492 isolate_format.load_isolate_for_config(
493 os.path.dirname(isolate_file), f.read(),
494 self.saved_state.config_variables))
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500495
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400496 # Processes the variables with the new found relative root. Note that 'cwd'
497 # is used when path variables are used.
498 path_variables = normalize_path_variables(
499 cwd, path_variables, isolate_cmd_dir)
500 # Update the rest of the saved state.
501 self.saved_state.update(isolate_file, path_variables, extra_variables)
502
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500503 total_variables = self.saved_state.path_variables.copy()
504 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500505 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500506 command = [
507 isolate_format.eval_variables(i, total_variables) for i in command
508 ]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500509
510 total_variables = self.saved_state.path_variables.copy()
511 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500512 infiles = [
513 isolate_format.eval_variables(f, total_variables) for f in infiles
514 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000515 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +0000516 # form '../../foo/bar'. Note that path variables must be taken in account
517 # too, add them as if they were input files.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400518 self.saved_state.root_dir = isolate_format.determine_root_dir(
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400519 isolate_cmd_dir, infiles + self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000520 # The relative directory is automatically determined by the relative path
521 # between root_dir and the directory containing the .isolate file,
522 # isolate_base_dir.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400523 relative_cwd = os.path.relpath(isolate_cmd_dir, self.saved_state.root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500524 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000525 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500526 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400527 dest = os.path.join(isolate_cmd_dir, relative_cwd, v)
528 if not file_path.path_starts_with(self.saved_state.root_dir, dest):
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400529 raise isolated_format.MappingError(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400530 'Path variable %s=%r points outside the inferred root directory '
531 '%s; %s'
532 % (k, v, self.saved_state.root_dir, dest))
533 # Normalize the files based to self.saved_state.root_dir. It is important to
534 # keep the trailing os.path.sep at that step.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000535 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500536 file_path.relpath(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400537 file_path.normpath(os.path.join(isolate_cmd_dir, f)),
538 self.saved_state.root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000539 for f in infiles
540 ]
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400541 follow_symlinks = sys.platform != 'win32'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000542 # Expand the directories by listing each file inside. Up to now, trailing
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400543 # os.path.sep must be kept.
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400544 infiles = isolated_format.expand_directories_and_symlinks(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400545 self.saved_state.root_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000546 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +0000547 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000548 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +0000549 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000550
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000551 # Finally, update the new data to be able to generate the foo.isolated file,
552 # the file that is used by run_isolated.py.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400553 self.saved_state.update_isolated(command, infiles, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000554 logging.debug(self)
555
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400556 def files_to_metadata(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000557 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000558
maruel@chromium.org9268f042012-10-17 17:36:41 +0000559 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
560 file is tainted.
561
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400562 See isolated_format.file_to_metadata() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000563 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000564 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +0000565 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000566 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000567 else:
568 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400569 self.saved_state.files[infile] = isolated_format.file_to_metadata(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000570 filepath,
571 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000572 self.saved_state.read_only,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000573 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000574
575 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000576 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000577 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000578 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000579 self.isolated_filepath,
580 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500581 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000582 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000583 total_bytes = sum(
584 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000585 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000586 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000587 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000588 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000589 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500590 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000591
592 @property
593 def root_dir(self):
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400594 return self.saved_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000595
596 def __str__(self):
597 def indent(data, indent_length):
598 """Indents text."""
599 spacing = ' ' * indent_length
600 return ''.join(spacing + l for l in str(data).splitlines(True))
601
602 out = '%s(\n' % self.__class__.__name__
603 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000604 out += ' saved_state: %s)' % indent(self.saved_state, 2)
605 return out
606
607
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000608def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000609 """Loads a CompleteState.
610
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000611 This includes data from .isolate and .isolated.state files. Never reads the
612 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000613
614 Arguments:
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700615 options: Options instance generated with process_isolate_options. For either
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000616 options.isolate and options.isolated, if the value is set, it is an
617 absolute path.
618 cwd: base directory to be used when loading the .isolate file.
619 subdir: optional argument to only process file in the subdirectory, relative
620 to CompleteState.root_dir.
621 skip_update: Skip trying to load the .isolate file and processing the
622 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000623 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000624 assert not options.isolate or os.path.isabs(options.isolate)
625 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000626 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000627 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000628 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000629 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000630 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000631 else:
632 # Constructs a dummy object that cannot be saved. Useful for temporary
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400633 # commands like 'run'. There is no directory containing a .isolated file so
634 # specify the current working directory as a valid directory.
635 complete_state = CompleteState(None, SavedState(os.getcwd()))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000636
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000637 if not options.isolate:
638 if not complete_state.saved_state.isolate_file:
639 if not skip_update:
640 raise ExecutionError('A .isolate file is required.')
641 isolate = None
642 else:
643 isolate = complete_state.saved_state.isolate_filepath
644 else:
645 isolate = options.isolate
646 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500647 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000648 options.isolate, complete_state.saved_state.isolated_basedir)
649 if rel_isolate != complete_state.saved_state.isolate_file:
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400650 # This happens if the .isolate file was moved for example. In this case,
651 # discard the saved state.
652 logging.warning(
653 '--isolated %s != %s as saved in %s. Discarding saved state',
654 rel_isolate,
655 complete_state.saved_state.isolate_file,
656 isolatedfile_to_state(options.isolated))
657 complete_state = CompleteState(
658 options.isolated,
659 SavedState(complete_state.saved_state.isolated_basedir))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000660
661 if not skip_update:
662 # Then load the .isolate and expands directories.
663 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500664 cwd, isolate, options.path_variables, options.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500665 options.extra_variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000666
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000667 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +0000668 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000669 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500670 # This is tricky here. If it is a path, take it from the root_dir. If
671 # it is a variable, it must be keyed from the directory containing the
672 # .isolate file. So translate all variables first.
673 translated_path_variables = dict(
674 (k,
675 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
676 v)))
677 for k, v in complete_state.saved_state.path_variables.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500678 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000679 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000680
681 if not skip_update:
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400682 complete_state.files_to_metadata(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000683 return complete_state
684
685
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500686def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
687 """Creates a isolated tree usable for test execution.
688
689 Returns the current working directory where the isolated command should be
690 started in.
691 """
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500692 # Forcibly copy when the tree has to be read only. Otherwise the inode is
693 # modified, and this cause real problems because the user's source tree
694 # becomes read only. On the other hand, the cost of doing file copy is huge.
695 if read_only not in (0, None):
696 action = run_isolated.COPY
697 else:
698 action = run_isolated.HARDLINK_WITH_FALLBACK
699
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500700 recreate_tree(
701 outdir=outdir,
702 indir=root_dir,
703 infiles=files,
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500704 action=action,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500705 as_hash=False)
706 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
707 if not os.path.isdir(cwd):
708 # It can happen when no files are mapped from the directory containing the
709 # .isolate file. But the directory must exist to be the current working
710 # directory.
711 os.makedirs(cwd)
712 run_isolated.change_tree_read_only(outdir, read_only)
713 return cwd
714
715
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500716def prepare_for_archival(options, cwd):
717 """Loads the isolated file and create 'infiles' for archival."""
718 complete_state = load_complete_state(
719 options, cwd, options.subdir, False)
720 # Make sure that complete_state isn't modified until save_files() is
721 # called, because any changes made to it here will propagate to the files
722 # created (which is probably not intended).
723 complete_state.save_files()
724
725 infiles = complete_state.saved_state.files
726 # Add all the .isolated files.
727 isolated_hash = []
728 isolated_files = [
729 options.isolated,
730 ] + complete_state.saved_state.child_isolated_files
731 for item in isolated_files:
732 item_path = os.path.join(
733 os.path.dirname(complete_state.isolated_filepath), item)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400734 # Do not use isolated_format.hash_file() here because the file is
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500735 # likely smallish (under 500kb) and its file size is needed.
736 with open(item_path, 'rb') as f:
737 content = f.read()
738 isolated_hash.append(
739 complete_state.saved_state.algo(content).hexdigest())
740 isolated_metadata = {
741 'h': isolated_hash[-1],
742 's': len(content),
743 'priority': '0'
744 }
745 infiles[item_path] = isolated_metadata
746 return complete_state, infiles, isolated_hash
747
748
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700749def isolate_and_archive(options, cwd, isolate_server, namespace):
750 """Isolates and uploads isolated tree.
maruel@chromium.org29029882013-08-30 12:15:40 +0000751
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700752 Uses parsed options object.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000753 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000754 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000755 success = False
756 try:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500757 complete_state, infiles, isolated_hash = prepare_for_archival(
758 options, cwd)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000759 logging.info('Creating content addressed object store with %d item',
760 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000761
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500762 isolateserver.upload_tree(
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700763 base_url=isolate_server,
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500764 indir=complete_state.root_dir,
765 infiles=infiles,
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700766 namespace=namespace)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000767 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000768 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700769 except Exception:
770 logging.exception(
771 'Exception when archiving %s', os.path.basename(options.isolated))
772 success = False
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000773 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000774 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000775 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000776 if not success and os.path.isfile(options.isolated):
777 os.remove(options.isolated)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700778 return success
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000779
780
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700781def parse_archive_command_line(args, cwd):
782 """Given list of arguments for 'archive' command returns parsed options.
783
784 Used by CMDbatcharchive to parse options passed via JSON. See also CMDarchive.
785 """
786 parser = optparse.OptionParser()
787 add_isolate_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500788 add_subdir_option(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000789 options, args = parser.parse_args(args)
790 if args:
791 parser.error('Unsupported argument: %s' % args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700792 process_isolate_options(parser, options, cwd)
793 return options
794
795
796### Commands.
797
798
799def CMDarchive(parser, args):
800 """Creates a .isolated file and uploads the tree to an isolate server.
801
802 All the files listed in the .isolated file are put in the isolate server
803 cache via isolateserver.py.
804 """
805 add_isolate_options(parser)
806 add_subdir_option(parser)
807 isolateserver.add_isolate_server_options(parser, False)
808 auth.add_auth_options(parser)
809 options, args = parser.parse_args(args)
810 process_isolate_options(parser, options)
811 auth.process_auth_options(parser, options)
812 isolateserver.process_isolate_server_options(parser, options)
813 if args:
814 parser.error('Unsupported argument: %s' % args)
815 if not file_path.is_url(options.isolate_server):
816 parser.error('Not a valid server URL: %s' % options.isolate_server)
817 auth.ensure_logged_in(options.isolate_server)
818 success = isolate_and_archive(
819 options, os.getcwd(), options.isolate_server, options.namespace)
820 return int(not success)
821
822
823@subcommand.usage('-- GEN_JSON_1 GEN_JSON_2 ...')
824def CMDbatcharchive(parser, args):
825 """Archives multiple isolated trees at once.
826
827 Using single command instead of multiple sequential invocations allows to cut
828 redundant work when isolated trees share common files (e.g. file hashes are
829 checked only once, their presence on the server is checked only once, and
830 so on).
831
832 Takes a list of paths to *.isolated.gen.json files that describe what trees to
833 isolate. Format of files is:
834 {
835 'version': 1,
836 'dir': <absolute path to a directory all other paths are relative to>,
837 'arg': [list of command line arguments for single 'archive' command]
838 }
839 """
840 isolateserver.add_isolate_server_options(parser, False)
841 auth.add_auth_options(parser)
842 options, args = parser.parse_args(args)
843 auth.process_auth_options(parser, options)
844 isolateserver.process_isolate_server_options(parser, options)
845 if not file_path.is_url(options.isolate_server):
846 parser.error('Not a valid server URL: %s' % options.isolate_server)
847 auth.ensure_logged_in(options.isolate_server)
848
849 # Validate all incoming options, prepare what needs to be archived as a list
850 # of tuples (working directory, archival options).
851 work_units = []
852 for gen_json_path in args:
853 # Validate JSON format of a *.isolated.gen.json file.
854 data = tools.read_json(gen_json_path)
855 if data.get('version') != ISOLATED_GEN_JSON_VERSION:
856 parser.error('Invalid version in %s' % gen_json_path)
857 cwd = data.get('dir')
858 if not isinstance(cwd, unicode) or not os.path.isdir(cwd):
859 parser.error('Invalid dir in %s' % gen_json_path)
860 args = data.get('args')
861 if (not isinstance(args, list) or
862 not all(isinstance(x, unicode) for x in args)):
863 parser.error('Invalid args in %s' % gen_json_path)
864 # Convert command line (embedded in JSON) to Options object.
865 work_units.append((cwd, parse_archive_command_line(args, cwd)))
866
867 # Perform the archival.
868 # TODO(vadimsh): Start optimizing this by removing redundant work.
869 failed = False
870 for cwd, opts in work_units:
871 success = isolate_and_archive(
872 opts, cwd, options.isolate_server, options.namespace)
873 failed = failed or not success
874
875 return int(failed)
876
877
878def CMDcheck(parser, args):
879 """Checks that all the inputs are present and generates .isolated."""
880 add_isolate_options(parser)
881 add_subdir_option(parser)
882 options, args = parser.parse_args(args)
883 if args:
884 parser.error('Unsupported argument: %s' % args)
885 process_isolate_options(parser, options)
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000886
887 complete_state = load_complete_state(
888 options, os.getcwd(), options.subdir, False)
889
890 # Nothing is done specifically. Just store the result and state.
891 complete_state.save_files()
892 return 0
893
894
maruel@chromium.orge5322512013-08-19 20:17:57 +0000895def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000896 """Creates a directory with all the dependencies mapped into it.
897
898 Useful to test manually why a test is failing. The target executable is not
899 run.
900 """
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700901 add_isolate_options(parser)
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -0400902 add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500903 add_skip_refresh_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000904 options, args = parser.parse_args(args)
905 if args:
906 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500907 cwd = os.getcwd()
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700908 process_isolate_options(parser, options, cwd, require_isolated=False)
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -0400909 process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500910 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000911
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500912 if not os.path.isdir(options.outdir):
913 os.makedirs(options.outdir)
914 print('Remapping into %s' % options.outdir)
915 if os.listdir(options.outdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000916 raise ExecutionError('Can\'t remap in a non-empty directory')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000917
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500918 create_isolate_tree(
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500919 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500920 complete_state.saved_state.relative_cwd,
921 complete_state.saved_state.read_only)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000922 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000923 complete_state.save_files()
924 return 0
925
926
maruel@chromium.org29029882013-08-30 12:15:40 +0000927@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +0000928def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000929 """Runs the test executable in an isolated (temporary) directory.
930
931 All the dependencies are mapped into the temporary directory and the
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500932 directory is cleaned up after the target exits.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000933
maruel@chromium.org29029882013-08-30 12:15:40 +0000934 Argument processing stops at -- and these arguments are appended to the
935 command line of the target to run. For example, use:
936 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000937 """
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700938 add_isolate_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500939 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000940 options, args = parser.parse_args(args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700941 process_isolate_options(parser, options, require_isolated=False)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000942 complete_state = load_complete_state(
943 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000944 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000945 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +0000946 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000947 cmd = tools.fix_python_path(cmd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500948
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500949 outdir = run_isolated.make_temp_dir(
950 'isolate-%s' % datetime.date.today(),
951 os.path.dirname(complete_state.root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000952 try:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500953 # TODO(maruel): Use run_isolated.run_tha_test().
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500954 cwd = create_isolate_tree(
955 outdir, complete_state.root_dir, complete_state.saved_state.files,
956 complete_state.saved_state.relative_cwd,
957 complete_state.saved_state.read_only)
John Abd-El-Malek3f998682014-09-17 17:48:09 -0700958 file_path.ensure_command_has_abs_path(cmd, cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000959 logging.info('Running %s, cwd=%s' % (cmd, cwd))
Marc-Antoine Ruel926dccd2014-09-17 13:40:24 -0400960 try:
961 result = subprocess.call(cmd, cwd=cwd)
962 except OSError:
963 sys.stderr.write(
964 'Failed to executed the command; executable is missing, maybe you\n'
965 'forgot to map it in the .isolate file?\n %s\n in %s\n' %
966 (' '.join(cmd), cwd))
967 result = 1
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000968 finally:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500969 run_isolated.rmtree(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000970
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000971 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000972 complete_state.save_files()
973 return result
974
975
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500976def _process_variable_arg(option, opt, _value, parser):
977 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +0000978 if not parser.rargs:
979 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500980 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +0000981 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500982 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +0000983 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500984 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +0000985 else:
986 if not parser.rargs:
987 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500988 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +0000989 v = parser.rargs.pop(0)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500990 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500991 raise optparse.OptionValueError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500992 'Variable \'%s\' doesn\'t respect format \'%s\'' %
993 (k, isolate_format.VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -0500994 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +0000995
996
maruel@chromium.orgb253fb82012-10-16 21:44:48 +0000997def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500998 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000999 parser.add_option(
1000 '-s', '--isolated',
1001 metavar='FILE',
1002 help='.isolated file to generate or read')
1003 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001004 parser.add_option(
1005 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001006 dest='isolated',
1007 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001008 is_win = sys.platform in ('win32', 'cygwin')
1009 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001010 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001011 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001012 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001013 # - configuration variables that are to be used in deducing the matrix to
1014 # reduce.
1015 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001016 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001017 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00001018 action='callback',
1019 callback=_process_variable_arg,
Marc-Antoine Ruel05199462014-03-13 15:40:48 -04001020 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001021 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001022 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001023 help='Config variables are used to determine which conditions should be '
1024 'matched when loading a .isolate file, default: %default. '
1025 'All 3 kinds of variables are persistent accross calls, they are '
1026 'saved inside <.isolated>.state')
1027 parser.add_option(
1028 '--path-variable',
1029 action='callback',
1030 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001031 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001032 dest='path_variables',
1033 metavar='FOO BAR',
1034 help='Path variables are used to replace file paths when loading a '
1035 '.isolate file, default: %default')
1036 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001037 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001038 action='callback',
1039 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001040 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1041 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001042 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001043 help='Extraneous variables are replaced on the \'command\' entry and on '
1044 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001045
1046
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001047def add_isolate_options(parser):
1048 """Adds --isolate, --isolated, --out and --<foo>-variable options."""
1049 group = optparse.OptionGroup(parser, 'Common options')
1050 group.add_option(
1051 '-i', '--isolate',
1052 metavar='FILE',
1053 help='.isolate file to load the dependency data from')
1054 add_variable_option(group)
1055 group.add_option(
1056 '--ignore_broken_items', action='store_true',
1057 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1058 help='Indicates that invalid entries in the isolated file to be '
1059 'only be logged and not stop processing. Defaults to True if '
1060 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
1061 parser.add_option_group(group)
1062
1063
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001064def add_subdir_option(parser):
1065 parser.add_option(
1066 '--subdir',
1067 help='Filters to a subdirectory. Its behavior changes depending if it '
1068 'is a relative path as a string or as a path variable. Path '
1069 'variables are always keyed from the directory containing the '
1070 '.isolate file. Anything else is keyed on the root directory.')
1071
1072
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001073def add_skip_refresh_option(parser):
1074 parser.add_option(
1075 '--skip-refresh', action='store_true',
1076 help='Skip reading .isolate file and do not refresh the hash of '
1077 'dependencies')
1078
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001079
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -04001080def add_outdir_options(parser):
1081 """Adds --outdir, which is orthogonal to --isolate-server.
1082
1083 Note: On upload, separate commands are used between 'archive' and 'hashtable'.
1084 On 'download', the same command can download from either an isolate server or
1085 a file system.
1086 """
1087 parser.add_option(
1088 '-o', '--outdir', metavar='DIR',
1089 help='Directory used to recreate the tree.')
1090
1091
1092def process_outdir_options(parser, options, cwd):
1093 if not options.outdir:
1094 parser.error('--outdir is required.')
1095 if file_path.is_url(options.outdir):
1096 parser.error('Can\'t use an URL for --outdir.')
1097 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1098 # outdir doesn't need native path case since tracing is never done from there.
1099 options.outdir = os.path.abspath(
1100 os.path.normpath(os.path.join(cwd, options.outdir)))
1101 # In theory, we'd create the directory outdir right away. Defer doing it in
1102 # case there's errors in the command line.
1103
1104
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001105def process_isolate_options(parser, options, cwd=None, require_isolated=True):
1106 """Handles options added with 'add_isolate_options'.
1107
1108 Mutates |options| in place, by normalizing path to isolate file, values of
1109 variables, etc.
1110 """
1111 cwd = file_path.get_native_path_case(unicode(cwd or os.getcwd()))
1112
1113 # Parse --isolated option.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001114 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001115 options.isolated = os.path.normpath(
1116 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001117 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001118 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001119 if options.isolated and not options.isolated.endswith('.isolated'):
1120 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001121
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001122 # Processes all the --<foo>-variable flags.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001123 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001124 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001125 try:
1126 return int(s)
1127 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001128 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001129 options.config_variables = dict(
1130 (k, try_make_int(v)) for k, v in options.config_variables)
1131 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001132 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001133
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001134 # Normalize the path in --isolate.
1135 if options.isolate:
1136 # TODO(maruel): Work with non-ASCII.
1137 # The path must be in native path case for tracing purposes.
1138 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1139 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
1140 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001141
1142
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001143def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00001144 dispatcher = subcommand.CommandDispatcher(__name__)
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001145 parser = tools.OptionParserWithLogging(
1146 version=__version__, verbose=int(os.environ.get('ISOLATE_DEBUG', 0)))
1147 return dispatcher.execute(parser, argv)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001148
1149
1150if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00001151 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001152 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001153 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001154 sys.exit(main(sys.argv[1:]))