blob: 0712d6df782a09b92324855e8e97d954b6f00bfe [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 Ruele236b5c2014-09-08 18:40:40 -040016__version__ = '0.4'
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
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000040class ExecutionError(Exception):
41 """A generic error occurred."""
42 def __str__(self):
43 return self.args[0]
44
45
46### Path handling code.
47
48
maruel@chromium.org7b844a62013-09-17 13:04:59 +000049def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000050 """Creates a new tree with only the input files in it.
51
52 Arguments:
53 outdir: Output directory to create the files in.
54 indir: Root directory the infiles are based in.
55 infiles: dict of files to map from |indir| to |outdir|.
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000056 action: One of accepted action of run_isolated.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +000057 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000058 """
59 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +000060 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
61 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000062
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000063 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000064 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000065 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000066 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000067
68 for relfile, metadata in infiles.iteritems():
69 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +000070 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000071 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000072 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000073 # Skip links when storing a hashtable.
74 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +000075 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000076 if os.path.isfile(outfile):
77 # Just do a quick check that the file size matches. No need to stat()
78 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000079 if not 's' in metadata:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -040080 raise isolated_format.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +000081 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +000082 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000083 continue
84 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +000085 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000086 os.remove(outfile)
87 else:
88 outfile = os.path.join(outdir, relfile)
89 outsubdir = os.path.dirname(outfile)
90 if not os.path.isdir(outsubdir):
91 os.makedirs(outsubdir)
92
93 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000094 # if metadata.get('T') == True:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000095 # open(outfile, 'ab').close()
maruel@chromium.orge5c17132012-11-21 18:18:46 +000096 if 'l' in metadata:
97 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000098 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +000099 # symlink doesn't exist on Windows.
100 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000101 else:
maruel@chromium.orgb8375c22012-10-05 18:10:01 +0000102 run_isolated.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000103
104
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000105### Variable stuff.
106
107
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500108def _normalize_path_variable(cwd, relative_base_dir, key, value):
109 """Normalizes a path variable into a relative directory.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500110 """
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500111 # Variables could contain / or \ on windows. Always normalize to
112 # os.path.sep.
113 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
114 normalized = file_path.get_native_path_case(os.path.normpath(x))
115 if not os.path.isdir(normalized):
116 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
117
118 # All variables are relative to the .isolate file.
119 normalized = os.path.relpath(normalized, relative_base_dir)
120 logging.debug(
121 'Translated variable %s from %s to %s', key, value, normalized)
122 return normalized
123
124
125def normalize_path_variables(cwd, path_variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000126 """Processes path variables as a special case and returns a copy of the dict.
127
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000128 For each 'path' variable: first normalizes it based on |cwd|, verifies it
129 exists then sets it as relative to relative_base_dir.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500130 """
131 logging.info(
132 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
133 relative_base_dir)
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -0500134 assert isinstance(cwd, unicode), cwd
135 assert isinstance(relative_base_dir, unicode), relative_base_dir
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500136 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
137 return dict(
138 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
139 for k, v in path_variables.iteritems())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000140
141
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500142### Internal state files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000143
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500144
145def isolatedfile_to_state(filename):
146 """For a '.isolate' file, returns the path to the saved '.state' file."""
147 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000148
149
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500150def chromium_save_isolated(isolated, data, path_variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000151 """Writes one or many .isolated files.
152
153 This slightly increases the cold cache cost but greatly reduce the warm cache
154 cost by splitting low-churn files off the master .isolated file. It also
155 reduces overall isolateserver memcache consumption.
156 """
157 slaves = []
158
159 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000160 new_slave = {
161 'algo': data['algo'],
162 'files': {},
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000163 'version': data['version'],
164 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000165 for f in data['files'].keys():
166 if f.startswith(prefix):
167 new_slave['files'][f] = data['files'].pop(f)
168 if new_slave['files']:
169 slaves.append(new_slave)
170
171 # Split test/data/ in its own .isolated file.
172 extract_into_included_isolated(os.path.join('test', 'data', ''))
173
174 # Split everything out of PRODUCT_DIR in its own .isolated file.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500175 if path_variables.get('PRODUCT_DIR'):
176 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000177
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000178 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000179 for index, f in enumerate(slaves):
180 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500181 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000182 data.setdefault('includes', []).append(
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400183 isolated_format.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000184 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000185
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400186 files.extend(isolated_format.save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000187 return files
188
189
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000190class Flattenable(object):
191 """Represents data that can be represented as a json file."""
192 MEMBERS = ()
193
194 def flatten(self):
195 """Returns a json-serializable version of itself.
196
197 Skips None entries.
198 """
199 items = ((member, getattr(self, member)) for member in self.MEMBERS)
200 return dict((member, value) for member, value in items if value is not None)
201
202 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000203 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000204 """Loads a flattened version."""
205 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000206 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000207 for member in out.MEMBERS:
208 if member in data:
209 # Access to a protected member XXX of a client class
210 # pylint: disable=W0212
211 out._load_member(member, data.pop(member))
212 if data:
213 raise ValueError(
214 'Found unexpected entry %s while constructing an object %s' %
215 (data, cls.__name__), data, cls.__name__)
216 return out
217
218 def _load_member(self, member, value):
219 """Loads a member into self."""
220 setattr(self, member, value)
221
222 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000223 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000224 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000225 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500226 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000227 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000228 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000229 # On failure, loads the default instance.
230 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000231 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000232 return out
233
234
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000235class SavedState(Flattenable):
236 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000237
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000238 This file caches the items calculated by this script and is used to increase
239 the performance of the script. This file is not loaded by run_isolated.py.
240 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000241
242 It is important to note that the 'files' dict keys are using native OS path
243 separator instead of '/' used in .isolate file.
244 """
245 MEMBERS = (
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400246 # Value of sys.platform so that the file is rejected if loaded from a
247 # different OS. While this should never happen in practice, users are ...
248 # "creative".
249 'OS',
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000250 # Algorithm used to generate the hash. The only supported value is at the
251 # time of writting 'sha-1'.
252 'algo',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400253 # List of included .isolated files. Used to support/remember 'slave'
254 # .isolated files. Relative path to isolated_basedir.
255 'child_isolated_files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000256 # Cache of the processed command. This value is saved because .isolated
257 # files are never loaded by isolate.py so it's the only way to load the
258 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000259 'command',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500260 # GYP variables that are used to generate conditions. The most frequent
261 # example is 'OS'.
262 'config_variables',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500263 # GYP variables that will be replaced in 'command' and paths but will not be
264 # considered a relative directory.
265 'extra_variables',
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000266 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000267 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000268 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000269 'isolate_file',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400270 # GYP variables used to generate the .isolated files paths based on path
271 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
272 'path_variables',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000273 # If the generated directory tree should be read-only.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000274 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000275 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000276 'relative_cwd',
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400277 # Root directory the files are mapped from.
278 'root_dir',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400279 # Version of the saved state file format. Any breaking change must update
280 # the value.
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000281 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000282 )
283
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400284 # Bump this version whenever the saved state changes. It is also keyed on the
285 # .isolated file version so any change in the generator will invalidate .state
286 # files.
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400287 EXPECTED_VERSION = isolated_format.ISOLATED_FILE_VERSION + '.2'
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400288
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000289 def __init__(self, isolated_basedir):
290 """Creates an empty SavedState.
291
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400292 Arguments:
293 isolated_basedir: the directory where the .isolated and .isolated.state
294 files are saved.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000295 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000296 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000297 assert os.path.isabs(isolated_basedir), isolated_basedir
298 assert os.path.isdir(isolated_basedir), isolated_basedir
299 self.isolated_basedir = isolated_basedir
300
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000301 # The default algorithm used.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400302 self.OS = sys.platform
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400303 self.algo = isolated_format.SUPPORTED_ALGOS['sha-1']
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500304 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000305 self.command = []
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500306 self.config_variables = {}
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500307 self.extra_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000308 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000309 self.isolate_file = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500310 self.path_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000311 self.read_only = None
312 self.relative_cwd = None
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400313 self.root_dir = None
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400314 self.version = self.EXPECTED_VERSION
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000315
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400316 def update_config(self, config_variables):
317 """Updates the saved state with only config variables."""
318 self.config_variables.update(config_variables)
319
320 def update(self, isolate_file, path_variables, extra_variables):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000321 """Updates the saved state with new data to keep GYP variables and internal
322 reference to the original .isolate file.
323 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +0000324 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000325 # Convert back to a relative path. On Windows, if the isolate and
326 # isolated files are on different drives, isolate_file will stay an absolute
327 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500328 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000329
330 # The same .isolate file should always be used to generate the .isolated and
331 # .isolated.state.
332 assert isolate_file == self.isolate_file or not self.isolate_file, (
333 isolate_file, self.isolate_file)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500334 self.extra_variables.update(extra_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000335 self.isolate_file = isolate_file
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500336 self.path_variables.update(path_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000337
338 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
339 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000340
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000341 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000342 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000343 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000344 self.command = command
345 # Add new files.
346 for f in infiles:
347 self.files.setdefault(f, {})
348 for f in touched:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000349 self.files.setdefault(f, {})['T'] = True
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000350 # Prune extraneous files that are not a dependency anymore.
351 for f in set(self.files).difference(set(infiles).union(touched)):
352 del self.files[f]
353 if read_only is not None:
354 self.read_only = read_only
355 self.relative_cwd = relative_cwd
356
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000357 def to_isolated(self):
358 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000359
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000360 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000361 """
362 def strip(data):
363 """Returns a 'files' entry with only the whitelisted keys."""
364 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
365
366 out = {
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400367 'algo': isolated_format.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000368 'files': dict(
369 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400370 # The version of the .state file is different than the one of the
371 # .isolated file.
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400372 'version': isolated_format.ISOLATED_FILE_VERSION,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000373 }
374 if self.command:
375 out['command'] = self.command
376 if self.read_only is not None:
377 out['read_only'] = self.read_only
378 if self.relative_cwd:
379 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000380 return out
381
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000382 @property
383 def isolate_filepath(self):
384 """Returns the absolute path of self.isolate_file."""
385 return os.path.normpath(
386 os.path.join(self.isolated_basedir, self.isolate_file))
387
388 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000389 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000390 def load(cls, data, isolated_basedir): # pylint: disable=W0221
391 """Special case loading to disallow different OS.
392
393 It is not possible to load a .isolated.state files from a different OS, this
394 file is saved in OS-specific format.
395 """
396 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400397 if data.get('OS') != sys.platform:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400398 raise isolated_format.IsolatedError('Unexpected OS %s', data.get('OS'))
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000399
400 # Converts human readable form back into the proper class type.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400401 algo = data.get('algo')
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400402 if not algo in isolated_format.SUPPORTED_ALGOS:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400403 raise isolated_format.IsolatedError('Unknown algo \'%s\'' % out.algo)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400404 out.algo = isolated_format.SUPPORTED_ALGOS[algo]
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000405
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500406 # Refuse the load non-exact version, even minor difference. This is unlike
407 # isolateserver.load_isolated(). This is because .isolated.state could have
408 # changed significantly even in minor version difference.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400409 if out.version != cls.EXPECTED_VERSION:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400410 raise isolated_format.IsolatedError(
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000411 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000412
Marc-Antoine Ruel16ebc2e2014-02-13 15:39:15 -0500413 # The .isolate file must be valid. If it is not present anymore, zap the
414 # value as if it was not noted, so .isolate_file can safely be overriden
415 # later.
416 if out.isolate_file and not os.path.isfile(out.isolate_filepath):
417 out.isolate_file = None
418 if out.isolate_file:
419 # It could be absolute on Windows if the drive containing the .isolate and
420 # the drive containing the .isolated files differ, .e.g .isolate is on
421 # C:\\ and .isolated is on D:\\ .
422 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
423 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000424 return out
425
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000426 def flatten(self):
427 """Makes sure 'algo' is in human readable form."""
428 out = super(SavedState, self).flatten()
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400429 out['algo'] = isolated_format.SUPPORTED_ALGOS_REVERSE[out['algo']]
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000430 return out
431
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000432 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500433 def dict_to_str(d):
434 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
435
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000436 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000437 out += ' command: %s\n' % self.command
438 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000439 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000440 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000441 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000442 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500443 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
444 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500445 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000446 return out
447
448
449class CompleteState(object):
450 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000451 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000452 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +0000453 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000454 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000455 # Contains the data to ease developer's use-case but that is not strictly
456 # necessary.
457 self.saved_state = saved_state
458
459 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000460 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000461 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000462 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000463 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000464 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000465 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000466 SavedState.load_file(
467 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000468
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500469 def load_isolate(
470 self, cwd, isolate_file, path_variables, config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500471 extra_variables, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000472 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000473 .isolate file.
474
475 Processes the loaded data, deduce root_dir, relative_cwd.
476 """
477 # Make sure to not depend on os.getcwd().
478 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000479 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000480 logging.info(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500481 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500482 cwd, isolate_file, path_variables, config_variables, extra_variables,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500483 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000484
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400485 # Config variables are not affected by the paths and must be used to
486 # retrieve the paths, so update them first.
487 self.saved_state.update_config(config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000488
489 with open(isolate_file, 'r') as f:
490 # At that point, variables are not replaced yet in command and infiles.
491 # infiles may contain directory entries and is in posix style.
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400492 command, infiles, touched, read_only, isolate_cmd_dir = (
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500493 isolate_format.load_isolate_for_config(
494 os.path.dirname(isolate_file), f.read(),
495 self.saved_state.config_variables))
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500496
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400497 # Processes the variables with the new found relative root. Note that 'cwd'
498 # is used when path variables are used.
499 path_variables = normalize_path_variables(
500 cwd, path_variables, isolate_cmd_dir)
501 # Update the rest of the saved state.
502 self.saved_state.update(isolate_file, path_variables, extra_variables)
503
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500504 total_variables = self.saved_state.path_variables.copy()
505 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500506 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500507 command = [
508 isolate_format.eval_variables(i, total_variables) for i in command
509 ]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500510
511 total_variables = self.saved_state.path_variables.copy()
512 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500513 infiles = [
514 isolate_format.eval_variables(f, total_variables) for f in infiles
515 ]
516 touched = [
517 isolate_format.eval_variables(f, total_variables) for f in touched
518 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000519 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +0000520 # form '../../foo/bar'. Note that path variables must be taken in account
521 # too, add them as if they were input files.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400522 self.saved_state.root_dir = isolate_format.determine_root_dir(
Marc-Antoine Ruelfdc9a552014-03-28 13:52:11 -0400523 isolate_cmd_dir, infiles + touched +
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500524 self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000525 # The relative directory is automatically determined by the relative path
526 # between root_dir and the directory containing the .isolate file,
527 # isolate_base_dir.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400528 relative_cwd = os.path.relpath(isolate_cmd_dir, self.saved_state.root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500529 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000530 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500531 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400532 dest = os.path.join(isolate_cmd_dir, relative_cwd, v)
533 if not file_path.path_starts_with(self.saved_state.root_dir, dest):
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400534 raise isolated_format.MappingError(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400535 'Path variable %s=%r points outside the inferred root directory '
536 '%s; %s'
537 % (k, v, self.saved_state.root_dir, dest))
538 # Normalize the files based to self.saved_state.root_dir. It is important to
539 # keep the trailing os.path.sep at that step.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000540 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500541 file_path.relpath(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400542 file_path.normpath(os.path.join(isolate_cmd_dir, f)),
543 self.saved_state.root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000544 for f in infiles
545 ]
546 touched = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500547 file_path.relpath(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400548 file_path.normpath(os.path.join(isolate_cmd_dir, f)),
549 self.saved_state.root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000550 for f in touched
551 ]
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400552 follow_symlinks = sys.platform != 'win32'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000553 # Expand the directories by listing each file inside. Up to now, trailing
554 # os.path.sep must be kept. Do not expand 'touched'.
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400555 infiles = isolated_format.expand_directories_and_symlinks(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400556 self.saved_state.root_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000557 infiles,
csharp@chromium.org01856802012-11-12 17:48:13 +0000558 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000559 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +0000560 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000561
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000562 # If we ignore broken items then remove any missing touched items.
563 if ignore_broken_items:
564 original_touched_count = len(touched)
565 touched = [touch for touch in touched if os.path.exists(touch)]
566
567 if len(touched) != original_touched_count:
maruel@chromium.org1d3a9132013-07-18 20:06:15 +0000568 logging.info('Removed %d invalid touched entries',
csharp@chromium.orgbc7c5d12013-03-21 16:39:15 +0000569 len(touched) - original_touched_count)
570
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000571 # Finally, update the new data to be able to generate the foo.isolated file,
572 # the file that is used by run_isolated.py.
573 self.saved_state.update_isolated(
574 command, infiles, touched, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000575 logging.debug(self)
576
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400577 def files_to_metadata(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000578 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000579
maruel@chromium.org9268f042012-10-17 17:36:41 +0000580 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
581 file is tainted.
582
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400583 See isolated_format.file_to_metadata() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000584 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000585 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +0000586 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000587 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000588 else:
589 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400590 self.saved_state.files[infile] = isolated_format.file_to_metadata(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000591 filepath,
592 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000593 self.saved_state.read_only,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000594 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000595
596 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000597 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000598 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000599 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000600 self.isolated_filepath,
601 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500602 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000603 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000604 total_bytes = sum(
605 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000606 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000607 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000608 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000609 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000610 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500611 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000612
613 @property
614 def root_dir(self):
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400615 return self.saved_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000616
617 def __str__(self):
618 def indent(data, indent_length):
619 """Indents text."""
620 spacing = ' ' * indent_length
621 return ''.join(spacing + l for l in str(data).splitlines(True))
622
623 out = '%s(\n' % self.__class__.__name__
624 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000625 out += ' saved_state: %s)' % indent(self.saved_state, 2)
626 return out
627
628
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000629def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000630 """Loads a CompleteState.
631
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000632 This includes data from .isolate and .isolated.state files. Never reads the
633 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000634
635 Arguments:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000636 options: Options instance generated with OptionParserIsolate. For either
637 options.isolate and options.isolated, if the value is set, it is an
638 absolute path.
639 cwd: base directory to be used when loading the .isolate file.
640 subdir: optional argument to only process file in the subdirectory, relative
641 to CompleteState.root_dir.
642 skip_update: Skip trying to load the .isolate file and processing the
643 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000644 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000645 assert not options.isolate or os.path.isabs(options.isolate)
646 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000647 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000648 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000649 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000650 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000651 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000652 else:
653 # Constructs a dummy object that cannot be saved. Useful for temporary
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400654 # commands like 'run'. There is no directory containing a .isolated file so
655 # specify the current working directory as a valid directory.
656 complete_state = CompleteState(None, SavedState(os.getcwd()))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000657
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000658 if not options.isolate:
659 if not complete_state.saved_state.isolate_file:
660 if not skip_update:
661 raise ExecutionError('A .isolate file is required.')
662 isolate = None
663 else:
664 isolate = complete_state.saved_state.isolate_filepath
665 else:
666 isolate = options.isolate
667 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500668 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000669 options.isolate, complete_state.saved_state.isolated_basedir)
670 if rel_isolate != complete_state.saved_state.isolate_file:
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400671 # This happens if the .isolate file was moved for example. In this case,
672 # discard the saved state.
673 logging.warning(
674 '--isolated %s != %s as saved in %s. Discarding saved state',
675 rel_isolate,
676 complete_state.saved_state.isolate_file,
677 isolatedfile_to_state(options.isolated))
678 complete_state = CompleteState(
679 options.isolated,
680 SavedState(complete_state.saved_state.isolated_basedir))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000681
682 if not skip_update:
683 # Then load the .isolate and expands directories.
684 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500685 cwd, isolate, options.path_variables, options.config_variables,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500686 options.extra_variables, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000687
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000688 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +0000689 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000690 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500691 # This is tricky here. If it is a path, take it from the root_dir. If
692 # it is a variable, it must be keyed from the directory containing the
693 # .isolate file. So translate all variables first.
694 translated_path_variables = dict(
695 (k,
696 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
697 v)))
698 for k, v in complete_state.saved_state.path_variables.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500699 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000700 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000701
702 if not skip_update:
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400703 complete_state.files_to_metadata(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000704 return complete_state
705
706
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500707def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
708 """Creates a isolated tree usable for test execution.
709
710 Returns the current working directory where the isolated command should be
711 started in.
712 """
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500713 # Forcibly copy when the tree has to be read only. Otherwise the inode is
714 # modified, and this cause real problems because the user's source tree
715 # becomes read only. On the other hand, the cost of doing file copy is huge.
716 if read_only not in (0, None):
717 action = run_isolated.COPY
718 else:
719 action = run_isolated.HARDLINK_WITH_FALLBACK
720
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500721 recreate_tree(
722 outdir=outdir,
723 indir=root_dir,
724 infiles=files,
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500725 action=action,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500726 as_hash=False)
727 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
728 if not os.path.isdir(cwd):
729 # It can happen when no files are mapped from the directory containing the
730 # .isolate file. But the directory must exist to be the current working
731 # directory.
732 os.makedirs(cwd)
733 run_isolated.change_tree_read_only(outdir, read_only)
734 return cwd
735
736
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500737def prepare_for_archival(options, cwd):
738 """Loads the isolated file and create 'infiles' for archival."""
739 complete_state = load_complete_state(
740 options, cwd, options.subdir, False)
741 # Make sure that complete_state isn't modified until save_files() is
742 # called, because any changes made to it here will propagate to the files
743 # created (which is probably not intended).
744 complete_state.save_files()
745
746 infiles = complete_state.saved_state.files
747 # Add all the .isolated files.
748 isolated_hash = []
749 isolated_files = [
750 options.isolated,
751 ] + complete_state.saved_state.child_isolated_files
752 for item in isolated_files:
753 item_path = os.path.join(
754 os.path.dirname(complete_state.isolated_filepath), item)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400755 # Do not use isolated_format.hash_file() here because the file is
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500756 # likely smallish (under 500kb) and its file size is needed.
757 with open(item_path, 'rb') as f:
758 content = f.read()
759 isolated_hash.append(
760 complete_state.saved_state.algo(content).hexdigest())
761 isolated_metadata = {
762 'h': isolated_hash[-1],
763 's': len(content),
764 'priority': '0'
765 }
766 infiles[item_path] = isolated_metadata
767 return complete_state, infiles, isolated_hash
768
769
maruel@chromium.org29029882013-08-30 12:15:40 +0000770### Commands.
771
772
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000773def CMDarchive(parser, args):
774 """Creates a .isolated file and uploads the tree to an isolate server.
maruel@chromium.org29029882013-08-30 12:15:40 +0000775
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000776 All the files listed in the .isolated file are put in the isolate server
777 cache via isolateserver.py.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000778 """
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500779 add_subdir_option(parser)
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500780 isolateserver.add_isolate_server_options(parser, False)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800781 auth.add_auth_options(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000782 options, args = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800783 auth.process_auth_options(parser, options)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500784 isolateserver.process_isolate_server_options(parser, options)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000785 if args:
786 parser.error('Unsupported argument: %s' % args)
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700787 if file_path.is_url(options.isolate_server):
788 auth.ensure_logged_in(options.isolate_server)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500789 cwd = os.getcwd()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000790 with tools.Profiler('GenerateHashtable'):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000791 success = False
792 try:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500793 complete_state, infiles, isolated_hash = prepare_for_archival(
794 options, cwd)
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000795 logging.info('Creating content addressed object store with %d item',
796 len(infiles))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000797
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500798 isolateserver.upload_tree(
799 base_url=options.isolate_server,
800 indir=complete_state.root_dir,
801 infiles=infiles,
802 namespace=options.namespace)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000803 success = True
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000804 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000805 finally:
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000806 # If the command failed, delete the .isolated file if it exists. This is
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000807 # important so no stale swarm job is executed.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000808 if not success and os.path.isfile(options.isolated):
809 os.remove(options.isolated)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500810 return int(not success)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000811
812
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000813def CMDcheck(parser, args):
814 """Checks that all the inputs are present and generates .isolated."""
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500815 add_subdir_option(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000816 options, args = parser.parse_args(args)
817 if args:
818 parser.error('Unsupported argument: %s' % args)
819
820 complete_state = load_complete_state(
821 options, os.getcwd(), options.subdir, False)
822
823 # Nothing is done specifically. Just store the result and state.
824 complete_state.save_files()
825 return 0
826
827
maruel@chromium.orge5322512013-08-19 20:17:57 +0000828def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000829 """Creates a directory with all the dependencies mapped into it.
830
831 Useful to test manually why a test is failing. The target executable is not
832 run.
833 """
maruel@chromium.orge5322512013-08-19 20:17:57 +0000834 parser.require_isolated = False
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -0400835 add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500836 add_skip_refresh_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000837 options, args = parser.parse_args(args)
838 if args:
839 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500840 cwd = os.getcwd()
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -0400841 process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500842 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000843
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500844 if not os.path.isdir(options.outdir):
845 os.makedirs(options.outdir)
846 print('Remapping into %s' % options.outdir)
847 if os.listdir(options.outdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000848 raise ExecutionError('Can\'t remap in a non-empty directory')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000849
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500850 create_isolate_tree(
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500851 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500852 complete_state.saved_state.relative_cwd,
853 complete_state.saved_state.read_only)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000854 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000855 complete_state.save_files()
856 return 0
857
858
maruel@chromium.orge5322512013-08-19 20:17:57 +0000859def CMDrewrite(parser, args):
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +0000860 """Rewrites a .isolate file into the canonical format."""
maruel@chromium.orge5322512013-08-19 20:17:57 +0000861 parser.require_isolated = False
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +0000862 options, args = parser.parse_args(args)
863 if args:
864 parser.error('Unsupported argument: %s' % args)
865
866 if options.isolated:
867 # Load the previous state if it was present. Namely, "foo.isolated.state".
868 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000869 isolate = options.isolate or complete_state.saved_state.isolate_filepath
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +0000870 else:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000871 isolate = options.isolate
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +0000872 if not isolate:
maruel@chromium.org29029882013-08-30 12:15:40 +0000873 parser.error('--isolate is required.')
874
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +0000875 with open(isolate, 'r') as f:
876 content = f.read()
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500877 config = isolate_format.load_isolate_as_config(
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +0000878 os.path.dirname(os.path.abspath(isolate)),
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500879 isolate_format.eval_content(content),
880 isolate_format.extract_comment(content))
benrg@chromium.org609b7982013-02-07 16:44:46 +0000881 data = config.make_isolate_file()
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +0000882 print('Updating %s' % isolate)
883 with open(isolate, 'wb') as f:
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500884 isolate_format.print_all(config.file_comment, data, f)
maruel@chromium.org9f7f6d42013-02-04 18:31:17 +0000885 return 0
886
887
maruel@chromium.org29029882013-08-30 12:15:40 +0000888@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +0000889def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000890 """Runs the test executable in an isolated (temporary) directory.
891
892 All the dependencies are mapped into the temporary directory and the
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500893 directory is cleaned up after the target exits.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000894
maruel@chromium.org29029882013-08-30 12:15:40 +0000895 Argument processing stops at -- and these arguments are appended to the
896 command line of the target to run. For example, use:
897 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000898 """
maruel@chromium.orge5322512013-08-19 20:17:57 +0000899 parser.require_isolated = False
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500900 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000901 options, args = parser.parse_args(args)
maruel@chromium.org29029882013-08-30 12:15:40 +0000902
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000903 complete_state = load_complete_state(
904 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000905 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000906 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +0000907 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000908 cmd = tools.fix_python_path(cmd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500909
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500910 outdir = run_isolated.make_temp_dir(
911 'isolate-%s' % datetime.date.today(),
912 os.path.dirname(complete_state.root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000913 try:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500914 # TODO(maruel): Use run_isolated.run_tha_test().
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500915 cwd = create_isolate_tree(
916 outdir, complete_state.root_dir, complete_state.saved_state.files,
917 complete_state.saved_state.relative_cwd,
918 complete_state.saved_state.read_only)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000919 logging.info('Running %s, cwd=%s' % (cmd, cwd))
Marc-Antoine Ruel926dccd2014-09-17 13:40:24 -0400920 try:
921 result = subprocess.call(cmd, cwd=cwd)
922 except OSError:
923 sys.stderr.write(
924 'Failed to executed the command; executable is missing, maybe you\n'
925 'forgot to map it in the .isolate file?\n %s\n in %s\n' %
926 (' '.join(cmd), cwd))
927 result = 1
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000928 finally:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500929 run_isolated.rmtree(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000930
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000931 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000932 complete_state.save_files()
933 return result
934
935
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500936def _process_variable_arg(option, opt, _value, parser):
937 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +0000938 if not parser.rargs:
939 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500940 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +0000941 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500942 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +0000943 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500944 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +0000945 else:
946 if not parser.rargs:
947 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500948 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +0000949 v = parser.rargs.pop(0)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500950 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500951 raise optparse.OptionValueError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500952 'Variable \'%s\' doesn\'t respect format \'%s\'' %
953 (k, isolate_format.VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -0500954 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +0000955
956
maruel@chromium.orgb253fb82012-10-16 21:44:48 +0000957def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500958 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000959 parser.add_option(
960 '-s', '--isolated',
961 metavar='FILE',
962 help='.isolated file to generate or read')
963 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +0000964 parser.add_option(
965 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000966 dest='isolated',
967 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500968 is_win = sys.platform in ('win32', 'cygwin')
969 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500970 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500971 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500972 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500973 # - configuration variables that are to be used in deducing the matrix to
974 # reduce.
975 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +0000976 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500977 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +0000978 action='callback',
979 callback=_process_variable_arg,
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400980 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500981 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +0000982 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500983 help='Config variables are used to determine which conditions should be '
984 'matched when loading a .isolate file, default: %default. '
985 'All 3 kinds of variables are persistent accross calls, they are '
986 'saved inside <.isolated>.state')
987 parser.add_option(
988 '--path-variable',
989 action='callback',
990 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500991 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500992 dest='path_variables',
993 metavar='FOO BAR',
994 help='Path variables are used to replace file paths when loading a '
995 '.isolate file, default: %default')
996 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500997 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500998 action='callback',
999 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001000 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1001 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001002 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001003 help='Extraneous variables are replaced on the \'command\' entry and on '
1004 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001005
1006
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001007def add_subdir_option(parser):
1008 parser.add_option(
1009 '--subdir',
1010 help='Filters to a subdirectory. Its behavior changes depending if it '
1011 'is a relative path as a string or as a path variable. Path '
1012 'variables are always keyed from the directory containing the '
1013 '.isolate file. Anything else is keyed on the root directory.')
1014
1015
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001016def add_skip_refresh_option(parser):
1017 parser.add_option(
1018 '--skip-refresh', action='store_true',
1019 help='Skip reading .isolate file and do not refresh the hash of '
1020 'dependencies')
1021
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001022
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -04001023def add_outdir_options(parser):
1024 """Adds --outdir, which is orthogonal to --isolate-server.
1025
1026 Note: On upload, separate commands are used between 'archive' and 'hashtable'.
1027 On 'download', the same command can download from either an isolate server or
1028 a file system.
1029 """
1030 parser.add_option(
1031 '-o', '--outdir', metavar='DIR',
1032 help='Directory used to recreate the tree.')
1033
1034
1035def process_outdir_options(parser, options, cwd):
1036 if not options.outdir:
1037 parser.error('--outdir is required.')
1038 if file_path.is_url(options.outdir):
1039 parser.error('Can\'t use an URL for --outdir.')
1040 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1041 # outdir doesn't need native path case since tracing is never done from there.
1042 options.outdir = os.path.abspath(
1043 os.path.normpath(os.path.join(cwd, options.outdir)))
1044 # In theory, we'd create the directory outdir right away. Defer doing it in
1045 # case there's errors in the command line.
1046
1047
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001048def parse_isolated_option(parser, options, cwd, require_isolated):
1049 """Processes --isolated."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001050 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001051 options.isolated = os.path.normpath(
1052 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001053 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001054 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001055 if options.isolated and not options.isolated.endswith('.isolated'):
1056 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001057
1058
1059def parse_variable_option(options):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001060 """Processes all the --<foo>-variable flags."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001061 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
1062 # but it wouldn't be backward compatible.
1063 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001064 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001065 try:
1066 return int(s)
1067 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001068 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001069 options.config_variables = dict(
1070 (k, try_make_int(v)) for k, v in options.config_variables)
1071 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001072 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001073
1074
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001075class OptionParserIsolate(tools.OptionParserWithLogging):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001076 """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
1077 """
maruel@chromium.orge5322512013-08-19 20:17:57 +00001078 # Set it to False if it is not required, e.g. it can be passed on but do not
1079 # fail if not given.
1080 require_isolated = True
1081
1082 def __init__(self, **kwargs):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001083 tools.OptionParserWithLogging.__init__(
maruel@chromium.org55276902012-10-05 20:56:19 +00001084 self,
1085 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1086 **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001087 group = optparse.OptionGroup(self, "Common options")
1088 group.add_option(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001089 '-i', '--isolate',
1090 metavar='FILE',
1091 help='.isolate file to load the dependency data from')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001092 add_variable_option(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001093 group.add_option(
csharp@chromium.org01856802012-11-12 17:48:13 +00001094 '--ignore_broken_items', action='store_true',
maruel@chromium.orgf347c3a2012-12-11 19:03:28 +00001095 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1096 help='Indicates that invalid entries in the isolated file to be '
1097 'only be logged and not stop processing. Defaults to True if '
1098 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001099 self.add_option_group(group)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001100
1101 def parse_args(self, *args, **kwargs):
1102 """Makes sure the paths make sense.
1103
1104 On Windows, / and \ are often mixed together in a path.
1105 """
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001106 options, args = tools.OptionParserWithLogging.parse_args(
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001107 self, *args, **kwargs)
1108 if not self.allow_interspersed_args and args:
1109 self.error('Unsupported argument: %s' % args)
1110
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001111 cwd = file_path.get_native_path_case(unicode(os.getcwd()))
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001112 parse_isolated_option(self, options, cwd, self.require_isolated)
1113 parse_variable_option(options)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001114
1115 if options.isolate:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001116 # TODO(maruel): Work with non-ASCII.
1117 # The path must be in native path case for tracing purposes.
1118 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1119 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001120 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001121
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001122 return options, args
1123
1124
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001125def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00001126 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001127 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001128
1129
1130if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00001131 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001132 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001133 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001134 sys.exit(main(sys.argv[1:]))