blob: eb5980151db50155ac205d2744476f2b252e1b2a [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 Ruel1f8ba352014-11-04 15:55:03 -050016__version__ = '0.4.3'
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040017
Marc-Antoine Ruel9dfdcc22014-01-08 14:14:18 -050018import datetime
Vadim Shtayuraea38c572014-10-06 16:57:16 -070019import itertools
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000020import logging
21import optparse
22import os
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000023import re
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000024import subprocess
25import sys
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000026
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080027import auth
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -050028import isolate_format
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040029import isolated_format
maruel@chromium.orgfb78d432013-08-28 21:22:40 +000030import isolateserver
maruel@chromium.orgb8375c22012-10-05 18:10:01 +000031import run_isolated
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000032
maruel@chromium.orge5322512013-08-19 20:17:57 +000033from third_party import colorama
34from third_party.depot_tools import fix_encoding
35from third_party.depot_tools import subcommand
36
maruel@chromium.org561d4b22013-09-26 21:08:08 +000037from utils import file_path
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000038from utils import tools
39
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000040
Vadim Shtayuraea38c572014-10-06 16:57:16 -070041# Exit code of 'archive' and 'batcharchive' if the command fails due to an error
42# in *.isolate file (format error, or some referenced files are missing, etc.)
43EXIT_CODE_ISOLATE_ERROR = 1
44
45
46# Exit code of 'archive' and 'batcharchive' if the command fails due to
47# a network or server issue. It is an infrastructure failure.
48EXIT_CODE_UPLOAD_ERROR = 101
49
50
Vadim Shtayurafddb1432014-09-30 18:32:41 -070051# Supported version of *.isolated.gen.json files consumed by CMDbatcharchive.
52ISOLATED_GEN_JSON_VERSION = 1
53
54
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000055class ExecutionError(Exception):
56 """A generic error occurred."""
57 def __str__(self):
58 return self.args[0]
59
60
61### Path handling code.
62
63
maruel@chromium.org7b844a62013-09-17 13:04:59 +000064def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000065 """Creates a new tree with only the input files in it.
66
67 Arguments:
68 outdir: Output directory to create the files in.
69 indir: Root directory the infiles are based in.
70 infiles: dict of files to map from |indir| to |outdir|.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040071 action: One of accepted action of file_path.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +000072 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000073 """
74 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +000075 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
76 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000077
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000078 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000079 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000080 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000081 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000082
83 for relfile, metadata in infiles.iteritems():
84 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +000085 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000086 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000087 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000088 # Skip links when storing a hashtable.
89 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +000090 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000091 if os.path.isfile(outfile):
92 # Just do a quick check that the file size matches. No need to stat()
93 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000094 if not 's' in metadata:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -040095 raise isolated_format.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +000096 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +000097 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000098 continue
99 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000100 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000101 os.remove(outfile)
102 else:
103 outfile = os.path.join(outdir, relfile)
104 outsubdir = os.path.dirname(outfile)
105 if not os.path.isdir(outsubdir):
106 os.makedirs(outsubdir)
107
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000108 if 'l' in metadata:
109 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000110 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000111 # symlink doesn't exist on Windows.
112 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000113 else:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400114 file_path.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000115
116
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000117### Variable stuff.
118
119
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500120def _normalize_path_variable(cwd, relative_base_dir, key, value):
121 """Normalizes a path variable into a relative directory.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500122 """
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500123 # Variables could contain / or \ on windows. Always normalize to
124 # os.path.sep.
125 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
126 normalized = file_path.get_native_path_case(os.path.normpath(x))
127 if not os.path.isdir(normalized):
128 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
129
130 # All variables are relative to the .isolate file.
131 normalized = os.path.relpath(normalized, relative_base_dir)
132 logging.debug(
133 'Translated variable %s from %s to %s', key, value, normalized)
134 return normalized
135
136
137def normalize_path_variables(cwd, path_variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000138 """Processes path variables as a special case and returns a copy of the dict.
139
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000140 For each 'path' variable: first normalizes it based on |cwd|, verifies it
141 exists then sets it as relative to relative_base_dir.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500142 """
143 logging.info(
144 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
145 relative_base_dir)
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -0500146 assert isinstance(cwd, unicode), cwd
147 assert isinstance(relative_base_dir, unicode), relative_base_dir
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500148 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
149 return dict(
150 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
151 for k, v in path_variables.iteritems())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000152
153
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500154### Internal state files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000155
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500156
157def isolatedfile_to_state(filename):
158 """For a '.isolate' file, returns the path to the saved '.state' file."""
159 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000160
161
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500162def chromium_save_isolated(isolated, data, path_variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000163 """Writes one or many .isolated files.
164
165 This slightly increases the cold cache cost but greatly reduce the warm cache
166 cost by splitting low-churn files off the master .isolated file. It also
167 reduces overall isolateserver memcache consumption.
168 """
169 slaves = []
170
171 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000172 new_slave = {
173 'algo': data['algo'],
174 'files': {},
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000175 'version': data['version'],
176 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000177 for f in data['files'].keys():
178 if f.startswith(prefix):
179 new_slave['files'][f] = data['files'].pop(f)
180 if new_slave['files']:
181 slaves.append(new_slave)
182
183 # Split test/data/ in its own .isolated file.
184 extract_into_included_isolated(os.path.join('test', 'data', ''))
185
186 # Split everything out of PRODUCT_DIR in its own .isolated file.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500187 if path_variables.get('PRODUCT_DIR'):
188 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000189
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000190 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000191 for index, f in enumerate(slaves):
192 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500193 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000194 data.setdefault('includes', []).append(
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400195 isolated_format.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000196 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000197
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400198 files.extend(isolated_format.save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000199 return files
200
201
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000202class Flattenable(object):
203 """Represents data that can be represented as a json file."""
204 MEMBERS = ()
205
206 def flatten(self):
207 """Returns a json-serializable version of itself.
208
209 Skips None entries.
210 """
211 items = ((member, getattr(self, member)) for member in self.MEMBERS)
212 return dict((member, value) for member, value in items if value is not None)
213
214 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000215 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000216 """Loads a flattened version."""
217 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000218 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000219 for member in out.MEMBERS:
220 if member in data:
221 # Access to a protected member XXX of a client class
222 # pylint: disable=W0212
223 out._load_member(member, data.pop(member))
224 if data:
225 raise ValueError(
226 'Found unexpected entry %s while constructing an object %s' %
227 (data, cls.__name__), data, cls.__name__)
228 return out
229
230 def _load_member(self, member, value):
231 """Loads a member into self."""
232 setattr(self, member, value)
233
234 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000235 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000236 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000237 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500238 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000239 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000240 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000241 # On failure, loads the default instance.
242 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000243 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000244 return out
245
246
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000247class SavedState(Flattenable):
248 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000249
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000250 This file caches the items calculated by this script and is used to increase
251 the performance of the script. This file is not loaded by run_isolated.py.
252 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000253
254 It is important to note that the 'files' dict keys are using native OS path
255 separator instead of '/' used in .isolate file.
256 """
257 MEMBERS = (
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400258 # Value of sys.platform so that the file is rejected if loaded from a
259 # different OS. While this should never happen in practice, users are ...
260 # "creative".
261 'OS',
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000262 # Algorithm used to generate the hash. The only supported value is at the
263 # time of writting 'sha-1'.
264 'algo',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400265 # List of included .isolated files. Used to support/remember 'slave'
266 # .isolated files. Relative path to isolated_basedir.
267 'child_isolated_files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000268 # Cache of the processed command. This value is saved because .isolated
269 # files are never loaded by isolate.py so it's the only way to load the
270 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000271 'command',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500272 # GYP variables that are used to generate conditions. The most frequent
273 # example is 'OS'.
274 'config_variables',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500275 # GYP variables that will be replaced in 'command' and paths but will not be
276 # considered a relative directory.
277 'extra_variables',
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000278 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000279 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000280 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000281 'isolate_file',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400282 # GYP variables used to generate the .isolated files paths based on path
283 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
284 'path_variables',
Marc-Antoine Ruel33d442a2014-10-03 14:41:51 -0400285 # If the generated directory tree should be read-only. Defaults to 1.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000286 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000287 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000288 'relative_cwd',
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400289 # Root directory the files are mapped from.
290 'root_dir',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400291 # Version of the saved state file format. Any breaking change must update
292 # the value.
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000293 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000294 )
295
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400296 # Bump this version whenever the saved state changes. It is also keyed on the
297 # .isolated file version so any change in the generator will invalidate .state
298 # files.
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400299 EXPECTED_VERSION = isolated_format.ISOLATED_FILE_VERSION + '.2'
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400300
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000301 def __init__(self, isolated_basedir):
302 """Creates an empty SavedState.
303
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400304 Arguments:
305 isolated_basedir: the directory where the .isolated and .isolated.state
306 files are saved.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000307 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000308 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000309 assert os.path.isabs(isolated_basedir), isolated_basedir
310 assert os.path.isdir(isolated_basedir), isolated_basedir
311 self.isolated_basedir = isolated_basedir
312
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000313 # The default algorithm used.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400314 self.OS = sys.platform
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400315 self.algo = isolated_format.SUPPORTED_ALGOS['sha-1']
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500316 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000317 self.command = []
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500318 self.config_variables = {}
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500319 self.extra_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000320 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000321 self.isolate_file = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500322 self.path_variables = {}
Marc-Antoine Ruel33d442a2014-10-03 14:41:51 -0400323 # Defaults to 1 when compiling to .isolated.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000324 self.read_only = None
325 self.relative_cwd = None
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400326 self.root_dir = None
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400327 self.version = self.EXPECTED_VERSION
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000328
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400329 def update_config(self, config_variables):
330 """Updates the saved state with only config variables."""
331 self.config_variables.update(config_variables)
332
333 def update(self, isolate_file, path_variables, extra_variables):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000334 """Updates the saved state with new data to keep GYP variables and internal
335 reference to the original .isolate file.
336 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +0000337 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000338 # Convert back to a relative path. On Windows, if the isolate and
339 # isolated files are on different drives, isolate_file will stay an absolute
340 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500341 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000342
343 # The same .isolate file should always be used to generate the .isolated and
344 # .isolated.state.
345 assert isolate_file == self.isolate_file or not self.isolate_file, (
346 isolate_file, self.isolate_file)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500347 self.extra_variables.update(extra_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000348 self.isolate_file = isolate_file
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500349 self.path_variables.update(path_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000350
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400351 def update_isolated(self, command, infiles, read_only, relative_cwd):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000352 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000353
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000354 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000355 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000356 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000357 self.command = command
358 # Add new files.
359 for f in infiles:
360 self.files.setdefault(f, {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000361 # Prune extraneous files that are not a dependency anymore.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400362 for f in set(self.files).difference(set(infiles)):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000363 del self.files[f]
364 if read_only is not None:
365 self.read_only = read_only
366 self.relative_cwd = relative_cwd
367
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000368 def to_isolated(self):
369 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000370
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000371 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000372 """
373 def strip(data):
374 """Returns a 'files' entry with only the whitelisted keys."""
375 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
376
377 out = {
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400378 'algo': isolated_format.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000379 'files': dict(
380 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400381 # The version of the .state file is different than the one of the
382 # .isolated file.
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400383 'version': isolated_format.ISOLATED_FILE_VERSION,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000384 }
385 if self.command:
386 out['command'] = self.command
Marc-Antoine Ruel33d442a2014-10-03 14:41:51 -0400387 out['read_only'] = self.read_only if self.read_only is not None else 1
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000388 if self.relative_cwd:
389 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000390 return out
391
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000392 @property
393 def isolate_filepath(self):
394 """Returns the absolute path of self.isolate_file."""
395 return os.path.normpath(
396 os.path.join(self.isolated_basedir, self.isolate_file))
397
398 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000399 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000400 def load(cls, data, isolated_basedir): # pylint: disable=W0221
401 """Special case loading to disallow different OS.
402
403 It is not possible to load a .isolated.state files from a different OS, this
404 file is saved in OS-specific format.
405 """
406 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400407 if data.get('OS') != sys.platform:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400408 raise isolated_format.IsolatedError('Unexpected OS %s', data.get('OS'))
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000409
410 # Converts human readable form back into the proper class type.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400411 algo = data.get('algo')
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400412 if not algo in isolated_format.SUPPORTED_ALGOS:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400413 raise isolated_format.IsolatedError('Unknown algo \'%s\'' % out.algo)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400414 out.algo = isolated_format.SUPPORTED_ALGOS[algo]
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000415
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500416 # Refuse the load non-exact version, even minor difference. This is unlike
417 # isolateserver.load_isolated(). This is because .isolated.state could have
418 # changed significantly even in minor version difference.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400419 if out.version != cls.EXPECTED_VERSION:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400420 raise isolated_format.IsolatedError(
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000421 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000422
Marc-Antoine Ruel16ebc2e2014-02-13 15:39:15 -0500423 # The .isolate file must be valid. If it is not present anymore, zap the
424 # value as if it was not noted, so .isolate_file can safely be overriden
425 # later.
426 if out.isolate_file and not os.path.isfile(out.isolate_filepath):
427 out.isolate_file = None
428 if out.isolate_file:
429 # It could be absolute on Windows if the drive containing the .isolate and
430 # the drive containing the .isolated files differ, .e.g .isolate is on
431 # C:\\ and .isolated is on D:\\ .
432 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
433 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000434 return out
435
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000436 def flatten(self):
437 """Makes sure 'algo' is in human readable form."""
438 out = super(SavedState, self).flatten()
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400439 out['algo'] = isolated_format.SUPPORTED_ALGOS_REVERSE[out['algo']]
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000440 return out
441
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000442 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500443 def dict_to_str(d):
444 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
445
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000446 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000447 out += ' command: %s\n' % self.command
448 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000449 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000450 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000451 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000452 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500453 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
454 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500455 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000456 return out
457
458
459class CompleteState(object):
460 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000461 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000462 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +0000463 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000464 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000465 # Contains the data to ease developer's use-case but that is not strictly
466 # necessary.
467 self.saved_state = saved_state
468
469 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000470 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000471 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000472 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000473 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000474 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000475 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000476 SavedState.load_file(
477 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000478
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500479 def load_isolate(
480 self, cwd, isolate_file, path_variables, config_variables,
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500481 extra_variables, blacklist, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000482 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000483 .isolate file.
484
485 Processes the loaded data, deduce root_dir, relative_cwd.
486 """
487 # Make sure to not depend on os.getcwd().
488 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000489 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000490 logging.info(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500491 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500492 cwd, isolate_file, path_variables, config_variables, extra_variables,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500493 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000494
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400495 # Config variables are not affected by the paths and must be used to
496 # retrieve the paths, so update them first.
497 self.saved_state.update_config(config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000498
499 with open(isolate_file, 'r') as f:
500 # At that point, variables are not replaced yet in command and infiles.
501 # infiles may contain directory entries and is in posix style.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400502 command, infiles, read_only, isolate_cmd_dir = (
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500503 isolate_format.load_isolate_for_config(
504 os.path.dirname(isolate_file), f.read(),
505 self.saved_state.config_variables))
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500506
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400507 # Processes the variables with the new found relative root. Note that 'cwd'
508 # is used when path variables are used.
509 path_variables = normalize_path_variables(
510 cwd, path_variables, isolate_cmd_dir)
511 # Update the rest of the saved state.
512 self.saved_state.update(isolate_file, path_variables, extra_variables)
513
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500514 total_variables = self.saved_state.path_variables.copy()
515 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500516 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500517 command = [
518 isolate_format.eval_variables(i, total_variables) for i in command
519 ]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500520
521 total_variables = self.saved_state.path_variables.copy()
522 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500523 infiles = [
524 isolate_format.eval_variables(f, total_variables) for f in infiles
525 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000526 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +0000527 # form '../../foo/bar'. Note that path variables must be taken in account
528 # too, add them as if they were input files.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400529 self.saved_state.root_dir = isolate_format.determine_root_dir(
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400530 isolate_cmd_dir, infiles + self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000531 # The relative directory is automatically determined by the relative path
532 # between root_dir and the directory containing the .isolate file,
533 # isolate_base_dir.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400534 relative_cwd = os.path.relpath(isolate_cmd_dir, self.saved_state.root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500535 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000536 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500537 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400538 dest = os.path.join(isolate_cmd_dir, relative_cwd, v)
539 if not file_path.path_starts_with(self.saved_state.root_dir, dest):
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400540 raise isolated_format.MappingError(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400541 'Path variable %s=%r points outside the inferred root directory '
542 '%s; %s'
543 % (k, v, self.saved_state.root_dir, dest))
544 # Normalize the files based to self.saved_state.root_dir. It is important to
545 # keep the trailing os.path.sep at that step.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000546 infiles = [
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 infiles
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
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400554 # os.path.sep must be kept.
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,
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500558 tools.gen_blacklist(blacklist),
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
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000562 # Finally, update the new data to be able to generate the foo.isolated file,
563 # the file that is used by run_isolated.py.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400564 self.saved_state.update_isolated(command, infiles, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000565 logging.debug(self)
566
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400567 def files_to_metadata(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000568 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000569
maruel@chromium.org9268f042012-10-17 17:36:41 +0000570 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
571 file is tainted.
572
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400573 See isolated_format.file_to_metadata() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000574 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000575 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +0000576 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000577 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000578 else:
579 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400580 self.saved_state.files[infile] = isolated_format.file_to_metadata(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000581 filepath,
582 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000583 self.saved_state.read_only,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000584 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000585
586 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000587 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000588 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000589 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000590 self.isolated_filepath,
591 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500592 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000593 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000594 total_bytes = sum(
595 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000596 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000597 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000598 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000599 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000600 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500601 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000602
603 @property
604 def root_dir(self):
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400605 return self.saved_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000606
607 def __str__(self):
608 def indent(data, indent_length):
609 """Indents text."""
610 spacing = ' ' * indent_length
611 return ''.join(spacing + l for l in str(data).splitlines(True))
612
613 out = '%s(\n' % self.__class__.__name__
614 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000615 out += ' saved_state: %s)' % indent(self.saved_state, 2)
616 return out
617
618
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000619def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000620 """Loads a CompleteState.
621
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000622 This includes data from .isolate and .isolated.state files. Never reads the
623 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000624
625 Arguments:
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700626 options: Options instance generated with process_isolate_options. For either
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000627 options.isolate and options.isolated, if the value is set, it is an
628 absolute path.
629 cwd: base directory to be used when loading the .isolate file.
630 subdir: optional argument to only process file in the subdirectory, relative
631 to CompleteState.root_dir.
632 skip_update: Skip trying to load the .isolate file and processing the
633 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000634 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000635 assert not options.isolate or os.path.isabs(options.isolate)
636 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000637 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000638 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000639 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000640 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000641 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000642 else:
643 # Constructs a dummy object that cannot be saved. Useful for temporary
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400644 # commands like 'run'. There is no directory containing a .isolated file so
645 # specify the current working directory as a valid directory.
646 complete_state = CompleteState(None, SavedState(os.getcwd()))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000647
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000648 if not options.isolate:
649 if not complete_state.saved_state.isolate_file:
650 if not skip_update:
651 raise ExecutionError('A .isolate file is required.')
652 isolate = None
653 else:
654 isolate = complete_state.saved_state.isolate_filepath
655 else:
656 isolate = options.isolate
657 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500658 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000659 options.isolate, complete_state.saved_state.isolated_basedir)
660 if rel_isolate != complete_state.saved_state.isolate_file:
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400661 # This happens if the .isolate file was moved for example. In this case,
662 # discard the saved state.
663 logging.warning(
664 '--isolated %s != %s as saved in %s. Discarding saved state',
665 rel_isolate,
666 complete_state.saved_state.isolate_file,
667 isolatedfile_to_state(options.isolated))
668 complete_state = CompleteState(
669 options.isolated,
670 SavedState(complete_state.saved_state.isolated_basedir))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000671
672 if not skip_update:
673 # Then load the .isolate and expands directories.
674 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500675 cwd, isolate, options.path_variables, options.config_variables,
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500676 options.extra_variables, options.blacklist, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000677
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000678 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +0000679 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000680 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500681 # This is tricky here. If it is a path, take it from the root_dir. If
682 # it is a variable, it must be keyed from the directory containing the
683 # .isolate file. So translate all variables first.
684 translated_path_variables = dict(
685 (k,
686 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
687 v)))
688 for k, v in complete_state.saved_state.path_variables.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500689 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000690 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000691
692 if not skip_update:
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400693 complete_state.files_to_metadata(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000694 return complete_state
695
696
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500697def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
698 """Creates a isolated tree usable for test execution.
699
700 Returns the current working directory where the isolated command should be
701 started in.
702 """
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500703 # Forcibly copy when the tree has to be read only. Otherwise the inode is
704 # modified, and this cause real problems because the user's source tree
705 # becomes read only. On the other hand, the cost of doing file copy is huge.
706 if read_only not in (0, None):
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400707 action = file_path.COPY
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500708 else:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400709 action = file_path.HARDLINK_WITH_FALLBACK
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500710
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500711 recreate_tree(
712 outdir=outdir,
713 indir=root_dir,
714 infiles=files,
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500715 action=action,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500716 as_hash=False)
717 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
718 if not os.path.isdir(cwd):
719 # It can happen when no files are mapped from the directory containing the
720 # .isolate file. But the directory must exist to be the current working
721 # directory.
722 os.makedirs(cwd)
723 run_isolated.change_tree_read_only(outdir, read_only)
724 return cwd
725
726
Vadim Shtayurac28b74f2014-10-06 20:00:08 -0700727@tools.profile
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500728def prepare_for_archival(options, cwd):
729 """Loads the isolated file and create 'infiles' for archival."""
730 complete_state = load_complete_state(
731 options, cwd, options.subdir, False)
732 # Make sure that complete_state isn't modified until save_files() is
733 # called, because any changes made to it here will propagate to the files
734 # created (which is probably not intended).
735 complete_state.save_files()
736
737 infiles = complete_state.saved_state.files
738 # Add all the .isolated files.
739 isolated_hash = []
740 isolated_files = [
741 options.isolated,
742 ] + complete_state.saved_state.child_isolated_files
743 for item in isolated_files:
744 item_path = os.path.join(
745 os.path.dirname(complete_state.isolated_filepath), item)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400746 # Do not use isolated_format.hash_file() here because the file is
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500747 # likely smallish (under 500kb) and its file size is needed.
748 with open(item_path, 'rb') as f:
749 content = f.read()
750 isolated_hash.append(
751 complete_state.saved_state.algo(content).hexdigest())
752 isolated_metadata = {
753 'h': isolated_hash[-1],
754 's': len(content),
755 'priority': '0'
756 }
757 infiles[item_path] = isolated_metadata
758 return complete_state, infiles, isolated_hash
759
760
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700761def isolate_and_archive(trees, isolate_server, namespace):
762 """Isolates and uploads a bunch of isolated trees.
maruel@chromium.org29029882013-08-30 12:15:40 +0000763
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700764 Args:
765 trees: list of pairs (Options, working directory) that describe what tree
766 to isolate. Options are processed by 'process_isolate_options'.
767 isolate_server: URL of Isolate Server to upload to.
768 namespace: namespace to upload to.
769
770 Returns a dict {target name -> isolate hash or None}, where target name is
771 a name of *.isolated file without an extension (e.g. 'base_unittests').
772
773 Have multiple failure modes:
774 * If the upload fails due to server or network error returns None.
775 * If some *.isolate file is incorrect (but rest of them are fine and were
776 successfully uploaded), returns a dict where the value of the entry
777 corresponding to invalid *.isolate file is None.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000778 """
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700779 if not trees:
780 return {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000781
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700782 # Helper generator to avoid materializing the full (huge) list of files until
783 # the very end (in upload_tree).
784 def emit_files(root_dir, files):
785 for path, meta in files.iteritems():
786 yield (os.path.join(root_dir, path), meta)
787
788 # Process all *.isolate files, it involves parsing, file system traversal and
789 # hashing. The result is a list of generators that produce files to upload
790 # and the mapping {target name -> hash of *.isolated file} to return from
791 # this function.
792 files_generators = []
793 isolated_hashes = {}
794 with tools.Profiler('Isolate'):
795 for opts, cwd in trees:
796 target_name = os.path.splitext(os.path.basename(opts.isolated))[0]
797 try:
798 complete_state, files, isolated_hash = prepare_for_archival(opts, cwd)
799 files_generators.append(emit_files(complete_state.root_dir, files))
800 isolated_hashes[target_name] = isolated_hash[0]
801 print('%s %s' % (isolated_hash[0], target_name))
802 except Exception:
803 logging.exception('Exception when isolating %s', target_name)
804 isolated_hashes[target_name] = None
805
806 # All bad? Nothing to upload.
807 if all(v is None for v in isolated_hashes.itervalues()):
808 return isolated_hashes
809
810 # Now upload all necessary files at once.
811 with tools.Profiler('Upload'):
812 try:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500813 isolateserver.upload_tree(
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700814 base_url=isolate_server,
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700815 infiles=itertools.chain(*files_generators),
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700816 namespace=namespace)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700817 except Exception:
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700818 logging.exception('Exception while uploading files')
819 return None
820
821 return isolated_hashes
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000822
823
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700824def parse_archive_command_line(args, cwd):
825 """Given list of arguments for 'archive' command returns parsed options.
826
827 Used by CMDbatcharchive to parse options passed via JSON. See also CMDarchive.
828 """
829 parser = optparse.OptionParser()
830 add_isolate_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500831 add_subdir_option(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000832 options, args = parser.parse_args(args)
833 if args:
834 parser.error('Unsupported argument: %s' % args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700835 process_isolate_options(parser, options, cwd)
836 return options
837
838
839### Commands.
840
841
842def CMDarchive(parser, args):
843 """Creates a .isolated file and uploads the tree to an isolate server.
844
845 All the files listed in the .isolated file are put in the isolate server
846 cache via isolateserver.py.
847 """
848 add_isolate_options(parser)
849 add_subdir_option(parser)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500850 isolateserver.add_isolate_server_options(parser)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700851 auth.add_auth_options(parser)
852 options, args = parser.parse_args(args)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500853 if args:
854 parser.error('Unsupported argument: %s' % args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700855 process_isolate_options(parser, options)
856 auth.process_auth_options(parser, options)
857 isolateserver.process_isolate_server_options(parser, options)
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700858 result = isolate_and_archive(
859 [(options, os.getcwd())], options.isolate_server, options.namespace)
860 if result is None:
861 return EXIT_CODE_UPLOAD_ERROR
862 assert len(result) == 1, result
863 if result.values()[0] is None:
864 return EXIT_CODE_ISOLATE_ERROR
865 return 0
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700866
867
868@subcommand.usage('-- GEN_JSON_1 GEN_JSON_2 ...')
869def CMDbatcharchive(parser, args):
870 """Archives multiple isolated trees at once.
871
872 Using single command instead of multiple sequential invocations allows to cut
873 redundant work when isolated trees share common files (e.g. file hashes are
874 checked only once, their presence on the server is checked only once, and
875 so on).
876
877 Takes a list of paths to *.isolated.gen.json files that describe what trees to
878 isolate. Format of files is:
879 {
Andrew Wang83648552014-11-14 13:26:49 -0800880 "version": 1,
881 "dir": <absolute path to a directory all other paths are relative to>,
882 "args": [list of command line arguments for single 'archive' command]
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700883 }
884 """
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500885 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500886 isolateserver.add_archive_options(parser)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700887 auth.add_auth_options(parser)
Vadim Shtayuraf4e9ccb2014-10-01 21:24:53 -0700888 parser.add_option(
889 '--dump-json',
890 metavar='FILE',
891 help='Write isolated hashes of archived trees to this file as JSON')
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700892 options, args = parser.parse_args(args)
893 auth.process_auth_options(parser, options)
894 isolateserver.process_isolate_server_options(parser, options)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700895
896 # Validate all incoming options, prepare what needs to be archived as a list
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700897 # of tuples (archival options, working directory).
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700898 work_units = []
899 for gen_json_path in args:
900 # Validate JSON format of a *.isolated.gen.json file.
901 data = tools.read_json(gen_json_path)
902 if data.get('version') != ISOLATED_GEN_JSON_VERSION:
903 parser.error('Invalid version in %s' % gen_json_path)
904 cwd = data.get('dir')
905 if not isinstance(cwd, unicode) or not os.path.isdir(cwd):
906 parser.error('Invalid dir in %s' % gen_json_path)
907 args = data.get('args')
908 if (not isinstance(args, list) or
909 not all(isinstance(x, unicode) for x in args)):
910 parser.error('Invalid args in %s' % gen_json_path)
911 # Convert command line (embedded in JSON) to Options object.
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700912 work_units.append((parse_archive_command_line(args, cwd), cwd))
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700913
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700914 # Perform the archival, all at once.
915 isolated_hashes = isolate_and_archive(
916 work_units, options.isolate_server, options.namespace)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700917
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700918 # TODO(vadimsh): isolate_and_archive returns None on upload failure, there's
919 # no way currently to figure out what *.isolated file from a batch were
920 # successfully uploaded, so consider them all failed (and emit empty dict
921 # as JSON result).
Vadim Shtayuraf4e9ccb2014-10-01 21:24:53 -0700922 if options.dump_json:
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700923 tools.write_json(options.dump_json, isolated_hashes or {}, False)
Vadim Shtayuraf4e9ccb2014-10-01 21:24:53 -0700924
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700925 if isolated_hashes is None:
926 return EXIT_CODE_UPLOAD_ERROR
927
928 # isolated_hashes[x] is None if 'x.isolate' contains a error.
929 if not all(isolated_hashes.itervalues()):
930 return EXIT_CODE_ISOLATE_ERROR
931
932 return 0
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700933
934
935def CMDcheck(parser, args):
936 """Checks that all the inputs are present and generates .isolated."""
937 add_isolate_options(parser)
938 add_subdir_option(parser)
939 options, args = parser.parse_args(args)
940 if args:
941 parser.error('Unsupported argument: %s' % args)
942 process_isolate_options(parser, options)
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000943
944 complete_state = load_complete_state(
945 options, os.getcwd(), options.subdir, False)
946
947 # Nothing is done specifically. Just store the result and state.
948 complete_state.save_files()
949 return 0
950
951
maruel@chromium.orge5322512013-08-19 20:17:57 +0000952def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000953 """Creates a directory with all the dependencies mapped into it.
954
955 Useful to test manually why a test is failing. The target executable is not
956 run.
957 """
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700958 add_isolate_options(parser)
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -0400959 add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500960 add_skip_refresh_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000961 options, args = parser.parse_args(args)
962 if args:
963 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500964 cwd = os.getcwd()
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700965 process_isolate_options(parser, options, cwd, require_isolated=False)
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -0400966 process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500967 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000968
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500969 if not os.path.isdir(options.outdir):
970 os.makedirs(options.outdir)
971 print('Remapping into %s' % options.outdir)
972 if os.listdir(options.outdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000973 raise ExecutionError('Can\'t remap in a non-empty directory')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000974
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500975 create_isolate_tree(
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500976 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500977 complete_state.saved_state.relative_cwd,
978 complete_state.saved_state.read_only)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000979 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000980 complete_state.save_files()
981 return 0
982
983
maruel@chromium.org29029882013-08-30 12:15:40 +0000984@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +0000985def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000986 """Runs the test executable in an isolated (temporary) directory.
987
988 All the dependencies are mapped into the temporary directory and the
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500989 directory is cleaned up after the target exits.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000990
maruel@chromium.org29029882013-08-30 12:15:40 +0000991 Argument processing stops at -- and these arguments are appended to the
992 command line of the target to run. For example, use:
993 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000994 """
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700995 add_isolate_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500996 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000997 options, args = parser.parse_args(args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700998 process_isolate_options(parser, options, require_isolated=False)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000999 complete_state = load_complete_state(
1000 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001001 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001002 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001003 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001004 cmd = tools.fix_python_path(cmd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001005
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001006 outdir = run_isolated.make_temp_dir(
1007 'isolate-%s' % datetime.date.today(),
1008 os.path.dirname(complete_state.root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001009 try:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -05001010 # TODO(maruel): Use run_isolated.run_tha_test().
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001011 cwd = create_isolate_tree(
1012 outdir, complete_state.root_dir, complete_state.saved_state.files,
1013 complete_state.saved_state.relative_cwd,
1014 complete_state.saved_state.read_only)
John Abd-El-Malek3f998682014-09-17 17:48:09 -07001015 file_path.ensure_command_has_abs_path(cmd, cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001016 logging.info('Running %s, cwd=%s' % (cmd, cwd))
Marc-Antoine Ruel926dccd2014-09-17 13:40:24 -04001017 try:
1018 result = subprocess.call(cmd, cwd=cwd)
1019 except OSError:
1020 sys.stderr.write(
1021 'Failed to executed the command; executable is missing, maybe you\n'
1022 'forgot to map it in the .isolate file?\n %s\n in %s\n' %
1023 (' '.join(cmd), cwd))
1024 result = 1
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001025 finally:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -04001026 file_path.rmtree(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001027
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001028 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001029 complete_state.save_files()
1030 return result
1031
1032
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001033def _process_variable_arg(option, opt, _value, parser):
1034 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +00001035 if not parser.rargs:
1036 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001037 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001038 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001039 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001040 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001041 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001042 else:
1043 if not parser.rargs:
1044 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001045 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001046 v = parser.rargs.pop(0)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001047 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001048 raise optparse.OptionValueError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001049 'Variable \'%s\' doesn\'t respect format \'%s\'' %
1050 (k, isolate_format.VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -05001051 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001052
1053
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001054def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001055 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001056 parser.add_option(
1057 '-s', '--isolated',
1058 metavar='FILE',
1059 help='.isolated file to generate or read')
1060 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001061 parser.add_option(
1062 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001063 dest='isolated',
1064 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001065 is_win = sys.platform in ('win32', 'cygwin')
1066 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001067 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001068 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001069 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001070 # - configuration variables that are to be used in deducing the matrix to
1071 # reduce.
1072 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001073 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001074 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00001075 action='callback',
1076 callback=_process_variable_arg,
Marc-Antoine Ruel05199462014-03-13 15:40:48 -04001077 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001078 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001079 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001080 help='Config variables are used to determine which conditions should be '
1081 'matched when loading a .isolate file, default: %default. '
1082 'All 3 kinds of variables are persistent accross calls, they are '
1083 'saved inside <.isolated>.state')
1084 parser.add_option(
1085 '--path-variable',
1086 action='callback',
1087 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001088 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001089 dest='path_variables',
1090 metavar='FOO BAR',
1091 help='Path variables are used to replace file paths when loading a '
1092 '.isolate file, default: %default')
1093 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001094 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001095 action='callback',
1096 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001097 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1098 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001099 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001100 help='Extraneous variables are replaced on the \'command\' entry and on '
1101 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001102
1103
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001104def add_isolate_options(parser):
1105 """Adds --isolate, --isolated, --out and --<foo>-variable options."""
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -05001106 isolateserver.add_archive_options(parser)
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001107 group = optparse.OptionGroup(parser, 'Common options')
1108 group.add_option(
1109 '-i', '--isolate',
1110 metavar='FILE',
1111 help='.isolate file to load the dependency data from')
1112 add_variable_option(group)
1113 group.add_option(
1114 '--ignore_broken_items', action='store_true',
1115 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1116 help='Indicates that invalid entries in the isolated file to be '
1117 'only be logged and not stop processing. Defaults to True if '
1118 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
1119 parser.add_option_group(group)
1120
1121
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001122def add_subdir_option(parser):
1123 parser.add_option(
1124 '--subdir',
1125 help='Filters to a subdirectory. Its behavior changes depending if it '
1126 'is a relative path as a string or as a path variable. Path '
1127 'variables are always keyed from the directory containing the '
1128 '.isolate file. Anything else is keyed on the root directory.')
1129
1130
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001131def add_skip_refresh_option(parser):
1132 parser.add_option(
1133 '--skip-refresh', action='store_true',
1134 help='Skip reading .isolate file and do not refresh the hash of '
1135 'dependencies')
1136
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001137
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -04001138def add_outdir_options(parser):
1139 """Adds --outdir, which is orthogonal to --isolate-server.
1140
1141 Note: On upload, separate commands are used between 'archive' and 'hashtable'.
1142 On 'download', the same command can download from either an isolate server or
1143 a file system.
1144 """
1145 parser.add_option(
1146 '-o', '--outdir', metavar='DIR',
1147 help='Directory used to recreate the tree.')
1148
1149
1150def process_outdir_options(parser, options, cwd):
1151 if not options.outdir:
1152 parser.error('--outdir is required.')
1153 if file_path.is_url(options.outdir):
1154 parser.error('Can\'t use an URL for --outdir.')
1155 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1156 # outdir doesn't need native path case since tracing is never done from there.
1157 options.outdir = os.path.abspath(
1158 os.path.normpath(os.path.join(cwd, options.outdir)))
1159 # In theory, we'd create the directory outdir right away. Defer doing it in
1160 # case there's errors in the command line.
1161
1162
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001163def process_isolate_options(parser, options, cwd=None, require_isolated=True):
1164 """Handles options added with 'add_isolate_options'.
1165
1166 Mutates |options| in place, by normalizing path to isolate file, values of
1167 variables, etc.
1168 """
1169 cwd = file_path.get_native_path_case(unicode(cwd or os.getcwd()))
1170
1171 # Parse --isolated option.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001172 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001173 options.isolated = os.path.normpath(
1174 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001175 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001176 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001177 if options.isolated and not options.isolated.endswith('.isolated'):
1178 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001179
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001180 # Processes all the --<foo>-variable flags.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001181 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001182 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001183 try:
1184 return int(s)
1185 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001186 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001187 options.config_variables = dict(
1188 (k, try_make_int(v)) for k, v in options.config_variables)
1189 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001190 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001191
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001192 # Normalize the path in --isolate.
1193 if options.isolate:
1194 # TODO(maruel): Work with non-ASCII.
1195 # The path must be in native path case for tracing purposes.
1196 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1197 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
1198 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001199
1200
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001201def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00001202 dispatcher = subcommand.CommandDispatcher(__name__)
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001203 parser = tools.OptionParserWithLogging(
1204 version=__version__, verbose=int(os.environ.get('ISOLATE_DEBUG', 0)))
1205 return dispatcher.execute(parser, argv)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001206
1207
1208if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00001209 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001210 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001211 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001212 sys.exit(main(sys.argv[1:]))