blob: 382ed09dc13025393b827e2d1f219b81bed4ebcb [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
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040037from utils import logging_utils
maruel@chromium.org561d4b22013-09-26 21:08:08 +000038from utils import file_path
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000039from utils import tools
40
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000041
Vadim Shtayuraea38c572014-10-06 16:57:16 -070042# Exit code of 'archive' and 'batcharchive' if the command fails due to an error
43# in *.isolate file (format error, or some referenced files are missing, etc.)
44EXIT_CODE_ISOLATE_ERROR = 1
45
46
47# Exit code of 'archive' and 'batcharchive' if the command fails due to
48# a network or server issue. It is an infrastructure failure.
49EXIT_CODE_UPLOAD_ERROR = 101
50
51
Vadim Shtayurafddb1432014-09-30 18:32:41 -070052# Supported version of *.isolated.gen.json files consumed by CMDbatcharchive.
53ISOLATED_GEN_JSON_VERSION = 1
54
55
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000056class ExecutionError(Exception):
57 """A generic error occurred."""
58 def __str__(self):
59 return self.args[0]
60
61
62### Path handling code.
63
64
maruel@chromium.org7b844a62013-09-17 13:04:59 +000065def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000066 """Creates a new tree with only the input files in it.
67
68 Arguments:
69 outdir: Output directory to create the files in.
70 indir: Root directory the infiles are based in.
71 infiles: dict of files to map from |indir| to |outdir|.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040072 action: One of accepted action of file_path.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +000073 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000074 """
75 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +000076 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
77 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000078
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000079 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000080 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000081 logging.info('Creating %s' % outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000082 os.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000083
84 for relfile, metadata in infiles.iteritems():
85 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +000086 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000087 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000088 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000089 # Skip links when storing a hashtable.
90 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +000091 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000092 if os.path.isfile(outfile):
93 # Just do a quick check that the file size matches. No need to stat()
94 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000095 if not 's' in metadata:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -040096 raise isolated_format.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +000097 'Misconfigured item %s: %s' % (relfile, metadata))
maruel@chromium.orge5c17132012-11-21 18:18:46 +000098 if metadata['s'] == os.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000099 continue
100 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000101 logging.warn('Overwritting %s' % metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000102 os.remove(outfile)
103 else:
104 outfile = os.path.join(outdir, relfile)
105 outsubdir = os.path.dirname(outfile)
106 if not os.path.isdir(outsubdir):
107 os.makedirs(outsubdir)
108
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000109 if 'l' in metadata:
110 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000111 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000112 # symlink doesn't exist on Windows.
113 os.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000114 else:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400115 file_path.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000116
117
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000118### Variable stuff.
119
120
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500121def _normalize_path_variable(cwd, relative_base_dir, key, value):
122 """Normalizes a path variable into a relative directory.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500123 """
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500124 # Variables could contain / or \ on windows. Always normalize to
125 # os.path.sep.
126 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
127 normalized = file_path.get_native_path_case(os.path.normpath(x))
128 if not os.path.isdir(normalized):
129 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
130
131 # All variables are relative to the .isolate file.
132 normalized = os.path.relpath(normalized, relative_base_dir)
133 logging.debug(
134 'Translated variable %s from %s to %s', key, value, normalized)
135 return normalized
136
137
138def normalize_path_variables(cwd, path_variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000139 """Processes path variables as a special case and returns a copy of the dict.
140
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000141 For each 'path' variable: first normalizes it based on |cwd|, verifies it
142 exists then sets it as relative to relative_base_dir.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500143 """
144 logging.info(
145 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
146 relative_base_dir)
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -0500147 assert isinstance(cwd, unicode), cwd
148 assert isinstance(relative_base_dir, unicode), relative_base_dir
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500149 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
150 return dict(
151 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
152 for k, v in path_variables.iteritems())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000153
154
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500155### Internal state files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000156
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500157
158def isolatedfile_to_state(filename):
159 """For a '.isolate' file, returns the path to the saved '.state' file."""
160 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000161
162
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500163def chromium_save_isolated(isolated, data, path_variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000164 """Writes one or many .isolated files.
165
166 This slightly increases the cold cache cost but greatly reduce the warm cache
167 cost by splitting low-churn files off the master .isolated file. It also
168 reduces overall isolateserver memcache consumption.
169 """
170 slaves = []
171
172 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000173 new_slave = {
174 'algo': data['algo'],
175 'files': {},
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000176 'version': data['version'],
177 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000178 for f in data['files'].keys():
179 if f.startswith(prefix):
180 new_slave['files'][f] = data['files'].pop(f)
181 if new_slave['files']:
182 slaves.append(new_slave)
183
184 # Split test/data/ in its own .isolated file.
185 extract_into_included_isolated(os.path.join('test', 'data', ''))
186
187 # Split everything out of PRODUCT_DIR in its own .isolated file.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500188 if path_variables.get('PRODUCT_DIR'):
189 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000190
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000191 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000192 for index, f in enumerate(slaves):
193 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500194 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000195 data.setdefault('includes', []).append(
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400196 isolated_format.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000197 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000198
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400199 files.extend(isolated_format.save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000200 return files
201
202
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000203class Flattenable(object):
204 """Represents data that can be represented as a json file."""
205 MEMBERS = ()
206
207 def flatten(self):
208 """Returns a json-serializable version of itself.
209
210 Skips None entries.
211 """
212 items = ((member, getattr(self, member)) for member in self.MEMBERS)
213 return dict((member, value) for member, value in items if value is not None)
214
215 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000216 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000217 """Loads a flattened version."""
218 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000219 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000220 for member in out.MEMBERS:
221 if member in data:
222 # Access to a protected member XXX of a client class
223 # pylint: disable=W0212
224 out._load_member(member, data.pop(member))
225 if data:
226 raise ValueError(
227 'Found unexpected entry %s while constructing an object %s' %
228 (data, cls.__name__), data, cls.__name__)
229 return out
230
231 def _load_member(self, member, value):
232 """Loads a member into self."""
233 setattr(self, member, value)
234
235 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000236 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000237 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000238 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500239 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000240 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000241 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000242 # On failure, loads the default instance.
243 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000244 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000245 return out
246
247
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000248class SavedState(Flattenable):
249 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000250
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000251 This file caches the items calculated by this script and is used to increase
252 the performance of the script. This file is not loaded by run_isolated.py.
253 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000254
255 It is important to note that the 'files' dict keys are using native OS path
256 separator instead of '/' used in .isolate file.
257 """
258 MEMBERS = (
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400259 # Value of sys.platform so that the file is rejected if loaded from a
260 # different OS. While this should never happen in practice, users are ...
261 # "creative".
262 'OS',
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000263 # Algorithm used to generate the hash. The only supported value is at the
264 # time of writting 'sha-1'.
265 'algo',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400266 # List of included .isolated files. Used to support/remember 'slave'
267 # .isolated files. Relative path to isolated_basedir.
268 'child_isolated_files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000269 # Cache of the processed command. This value is saved because .isolated
270 # files are never loaded by isolate.py so it's the only way to load the
271 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000272 'command',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500273 # GYP variables that are used to generate conditions. The most frequent
274 # example is 'OS'.
275 'config_variables',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500276 # GYP variables that will be replaced in 'command' and paths but will not be
277 # considered a relative directory.
278 'extra_variables',
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000279 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000280 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000281 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000282 'isolate_file',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400283 # GYP variables used to generate the .isolated files paths based on path
284 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
285 'path_variables',
Marc-Antoine Ruel33d442a2014-10-03 14:41:51 -0400286 # If the generated directory tree should be read-only. Defaults to 1.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000287 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000288 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000289 'relative_cwd',
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400290 # Root directory the files are mapped from.
291 'root_dir',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400292 # Version of the saved state file format. Any breaking change must update
293 # the value.
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000294 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000295 )
296
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400297 # Bump this version whenever the saved state changes. It is also keyed on the
298 # .isolated file version so any change in the generator will invalidate .state
299 # files.
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400300 EXPECTED_VERSION = isolated_format.ISOLATED_FILE_VERSION + '.2'
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400301
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000302 def __init__(self, isolated_basedir):
303 """Creates an empty SavedState.
304
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400305 Arguments:
306 isolated_basedir: the directory where the .isolated and .isolated.state
307 files are saved.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000308 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000309 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000310 assert os.path.isabs(isolated_basedir), isolated_basedir
311 assert os.path.isdir(isolated_basedir), isolated_basedir
312 self.isolated_basedir = isolated_basedir
313
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000314 # The default algorithm used.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400315 self.OS = sys.platform
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400316 self.algo = isolated_format.SUPPORTED_ALGOS['sha-1']
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500317 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000318 self.command = []
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500319 self.config_variables = {}
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500320 self.extra_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000321 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000322 self.isolate_file = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500323 self.path_variables = {}
Marc-Antoine Ruel33d442a2014-10-03 14:41:51 -0400324 # Defaults to 1 when compiling to .isolated.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000325 self.read_only = None
326 self.relative_cwd = None
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400327 self.root_dir = None
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400328 self.version = self.EXPECTED_VERSION
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000329
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400330 def update_config(self, config_variables):
331 """Updates the saved state with only config variables."""
332 self.config_variables.update(config_variables)
333
334 def update(self, isolate_file, path_variables, extra_variables):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000335 """Updates the saved state with new data to keep GYP variables and internal
336 reference to the original .isolate file.
337 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +0000338 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000339 # Convert back to a relative path. On Windows, if the isolate and
340 # isolated files are on different drives, isolate_file will stay an absolute
341 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500342 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000343
344 # The same .isolate file should always be used to generate the .isolated and
345 # .isolated.state.
346 assert isolate_file == self.isolate_file or not self.isolate_file, (
347 isolate_file, self.isolate_file)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500348 self.extra_variables.update(extra_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000349 self.isolate_file = isolate_file
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500350 self.path_variables.update(path_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000351
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400352 def update_isolated(self, command, infiles, read_only, relative_cwd):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000353 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000354
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000355 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000356 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000357 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000358 self.command = command
359 # Add new files.
360 for f in infiles:
361 self.files.setdefault(f, {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000362 # Prune extraneous files that are not a dependency anymore.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400363 for f in set(self.files).difference(set(infiles)):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000364 del self.files[f]
365 if read_only is not None:
366 self.read_only = read_only
367 self.relative_cwd = relative_cwd
368
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000369 def to_isolated(self):
370 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000371
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000372 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000373 """
374 def strip(data):
375 """Returns a 'files' entry with only the whitelisted keys."""
376 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
377
378 out = {
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400379 'algo': isolated_format.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000380 'files': dict(
381 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400382 # The version of the .state file is different than the one of the
383 # .isolated file.
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400384 'version': isolated_format.ISOLATED_FILE_VERSION,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000385 }
386 if self.command:
387 out['command'] = self.command
Marc-Antoine Ruel33d442a2014-10-03 14:41:51 -0400388 out['read_only'] = self.read_only if self.read_only is not None else 1
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000389 if self.relative_cwd:
390 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000391 return out
392
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000393 @property
394 def isolate_filepath(self):
395 """Returns the absolute path of self.isolate_file."""
396 return os.path.normpath(
397 os.path.join(self.isolated_basedir, self.isolate_file))
398
399 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000400 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000401 def load(cls, data, isolated_basedir): # pylint: disable=W0221
402 """Special case loading to disallow different OS.
403
404 It is not possible to load a .isolated.state files from a different OS, this
405 file is saved in OS-specific format.
406 """
407 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400408 if data.get('OS') != sys.platform:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400409 raise isolated_format.IsolatedError('Unexpected OS %s', data.get('OS'))
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000410
411 # Converts human readable form back into the proper class type.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400412 algo = data.get('algo')
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400413 if not algo in isolated_format.SUPPORTED_ALGOS:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400414 raise isolated_format.IsolatedError('Unknown algo \'%s\'' % out.algo)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400415 out.algo = isolated_format.SUPPORTED_ALGOS[algo]
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000416
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500417 # Refuse the load non-exact version, even minor difference. This is unlike
418 # isolateserver.load_isolated(). This is because .isolated.state could have
419 # changed significantly even in minor version difference.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400420 if out.version != cls.EXPECTED_VERSION:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400421 raise isolated_format.IsolatedError(
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000422 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000423
Marc-Antoine Ruel16ebc2e2014-02-13 15:39:15 -0500424 # The .isolate file must be valid. If it is not present anymore, zap the
425 # value as if it was not noted, so .isolate_file can safely be overriden
426 # later.
427 if out.isolate_file and not os.path.isfile(out.isolate_filepath):
428 out.isolate_file = None
429 if out.isolate_file:
430 # It could be absolute on Windows if the drive containing the .isolate and
431 # the drive containing the .isolated files differ, .e.g .isolate is on
432 # C:\\ and .isolated is on D:\\ .
433 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
434 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000435 return out
436
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000437 def flatten(self):
438 """Makes sure 'algo' is in human readable form."""
439 out = super(SavedState, self).flatten()
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400440 out['algo'] = isolated_format.SUPPORTED_ALGOS_REVERSE[out['algo']]
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000441 return out
442
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000443 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500444 def dict_to_str(d):
445 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
446
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000447 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000448 out += ' command: %s\n' % self.command
449 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000450 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000451 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000452 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000453 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500454 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
455 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500456 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000457 return out
458
459
460class CompleteState(object):
461 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000462 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000463 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +0000464 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000465 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000466 # Contains the data to ease developer's use-case but that is not strictly
467 # necessary.
468 self.saved_state = saved_state
469
470 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000471 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000472 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000473 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000474 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000475 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000476 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000477 SavedState.load_file(
478 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000479
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500480 def load_isolate(
481 self, cwd, isolate_file, path_variables, config_variables,
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500482 extra_variables, blacklist, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000483 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000484 .isolate file.
485
486 Processes the loaded data, deduce root_dir, relative_cwd.
487 """
488 # Make sure to not depend on os.getcwd().
489 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000490 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000491 logging.info(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500492 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500493 cwd, isolate_file, path_variables, config_variables, extra_variables,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500494 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000495
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400496 # Config variables are not affected by the paths and must be used to
497 # retrieve the paths, so update them first.
498 self.saved_state.update_config(config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000499
500 with open(isolate_file, 'r') as f:
501 # At that point, variables are not replaced yet in command and infiles.
502 # infiles may contain directory entries and is in posix style.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400503 command, infiles, read_only, isolate_cmd_dir = (
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500504 isolate_format.load_isolate_for_config(
505 os.path.dirname(isolate_file), f.read(),
506 self.saved_state.config_variables))
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500507
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400508 # Processes the variables with the new found relative root. Note that 'cwd'
509 # is used when path variables are used.
510 path_variables = normalize_path_variables(
511 cwd, path_variables, isolate_cmd_dir)
512 # Update the rest of the saved state.
513 self.saved_state.update(isolate_file, path_variables, extra_variables)
514
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500515 total_variables = self.saved_state.path_variables.copy()
516 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500517 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500518 command = [
519 isolate_format.eval_variables(i, total_variables) for i in command
520 ]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500521
522 total_variables = self.saved_state.path_variables.copy()
523 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500524 infiles = [
525 isolate_format.eval_variables(f, total_variables) for f in infiles
526 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000527 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +0000528 # form '../../foo/bar'. Note that path variables must be taken in account
529 # too, add them as if they were input files.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400530 self.saved_state.root_dir = isolate_format.determine_root_dir(
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400531 isolate_cmd_dir, infiles + self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000532 # The relative directory is automatically determined by the relative path
533 # between root_dir and the directory containing the .isolate file,
534 # isolate_base_dir.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400535 relative_cwd = os.path.relpath(isolate_cmd_dir, self.saved_state.root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500536 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000537 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500538 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400539 dest = os.path.join(isolate_cmd_dir, relative_cwd, v)
540 if not file_path.path_starts_with(self.saved_state.root_dir, dest):
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400541 raise isolated_format.MappingError(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400542 'Path variable %s=%r points outside the inferred root directory '
543 '%s; %s'
544 % (k, v, self.saved_state.root_dir, dest))
545 # Normalize the files based to self.saved_state.root_dir. It is important to
546 # keep the trailing os.path.sep at that step.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000547 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500548 file_path.relpath(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400549 file_path.normpath(os.path.join(isolate_cmd_dir, f)),
550 self.saved_state.root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000551 for f in infiles
552 ]
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400553 follow_symlinks = sys.platform != 'win32'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000554 # Expand the directories by listing each file inside. Up to now, trailing
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400555 # os.path.sep must be kept.
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400556 infiles = isolated_format.expand_directories_and_symlinks(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400557 self.saved_state.root_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000558 infiles,
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500559 tools.gen_blacklist(blacklist),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000560 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +0000561 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000562
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000563 # Finally, update the new data to be able to generate the foo.isolated file,
564 # the file that is used by run_isolated.py.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400565 self.saved_state.update_isolated(command, infiles, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000566 logging.debug(self)
567
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400568 def files_to_metadata(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000569 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000570
maruel@chromium.org9268f042012-10-17 17:36:41 +0000571 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
572 file is tainted.
573
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400574 See isolated_format.file_to_metadata() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000575 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000576 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +0000577 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000578 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000579 else:
580 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400581 self.saved_state.files[infile] = isolated_format.file_to_metadata(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000582 filepath,
583 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000584 self.saved_state.read_only,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000585 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000586
587 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000588 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000589 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000590 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000591 self.isolated_filepath,
592 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500593 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000594 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000595 total_bytes = sum(
596 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000597 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000598 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000599 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000600 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000601 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500602 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000603
604 @property
605 def root_dir(self):
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400606 return self.saved_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000607
608 def __str__(self):
609 def indent(data, indent_length):
610 """Indents text."""
611 spacing = ' ' * indent_length
612 return ''.join(spacing + l for l in str(data).splitlines(True))
613
614 out = '%s(\n' % self.__class__.__name__
615 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000616 out += ' saved_state: %s)' % indent(self.saved_state, 2)
617 return out
618
619
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000620def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000621 """Loads a CompleteState.
622
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000623 This includes data from .isolate and .isolated.state files. Never reads the
624 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000625
626 Arguments:
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700627 options: Options instance generated with process_isolate_options. For either
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000628 options.isolate and options.isolated, if the value is set, it is an
629 absolute path.
630 cwd: base directory to be used when loading the .isolate file.
631 subdir: optional argument to only process file in the subdirectory, relative
632 to CompleteState.root_dir.
633 skip_update: Skip trying to load the .isolate file and processing the
634 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000635 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000636 assert not options.isolate or os.path.isabs(options.isolate)
637 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000638 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000639 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000640 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000641 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000642 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000643 else:
644 # Constructs a dummy object that cannot be saved. Useful for temporary
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400645 # commands like 'run'. There is no directory containing a .isolated file so
646 # specify the current working directory as a valid directory.
647 complete_state = CompleteState(None, SavedState(os.getcwd()))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000648
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000649 if not options.isolate:
650 if not complete_state.saved_state.isolate_file:
651 if not skip_update:
652 raise ExecutionError('A .isolate file is required.')
653 isolate = None
654 else:
655 isolate = complete_state.saved_state.isolate_filepath
656 else:
657 isolate = options.isolate
658 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500659 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000660 options.isolate, complete_state.saved_state.isolated_basedir)
661 if rel_isolate != complete_state.saved_state.isolate_file:
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400662 # This happens if the .isolate file was moved for example. In this case,
663 # discard the saved state.
664 logging.warning(
665 '--isolated %s != %s as saved in %s. Discarding saved state',
666 rel_isolate,
667 complete_state.saved_state.isolate_file,
668 isolatedfile_to_state(options.isolated))
669 complete_state = CompleteState(
670 options.isolated,
671 SavedState(complete_state.saved_state.isolated_basedir))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000672
673 if not skip_update:
674 # Then load the .isolate and expands directories.
675 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500676 cwd, isolate, options.path_variables, options.config_variables,
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500677 options.extra_variables, options.blacklist, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000678
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000679 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +0000680 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000681 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500682 # This is tricky here. If it is a path, take it from the root_dir. If
683 # it is a variable, it must be keyed from the directory containing the
684 # .isolate file. So translate all variables first.
685 translated_path_variables = dict(
686 (k,
687 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
688 v)))
689 for k, v in complete_state.saved_state.path_variables.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500690 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000691 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000692
693 if not skip_update:
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400694 complete_state.files_to_metadata(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000695 return complete_state
696
697
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500698def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
699 """Creates a isolated tree usable for test execution.
700
701 Returns the current working directory where the isolated command should be
702 started in.
703 """
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500704 # Forcibly copy when the tree has to be read only. Otherwise the inode is
705 # modified, and this cause real problems because the user's source tree
706 # becomes read only. On the other hand, the cost of doing file copy is huge.
707 if read_only not in (0, None):
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400708 action = file_path.COPY
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500709 else:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400710 action = file_path.HARDLINK_WITH_FALLBACK
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500711
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500712 recreate_tree(
713 outdir=outdir,
714 indir=root_dir,
715 infiles=files,
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500716 action=action,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500717 as_hash=False)
718 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
719 if not os.path.isdir(cwd):
720 # It can happen when no files are mapped from the directory containing the
721 # .isolate file. But the directory must exist to be the current working
722 # directory.
723 os.makedirs(cwd)
724 run_isolated.change_tree_read_only(outdir, read_only)
725 return cwd
726
727
Vadim Shtayurac28b74f2014-10-06 20:00:08 -0700728@tools.profile
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500729def prepare_for_archival(options, cwd):
730 """Loads the isolated file and create 'infiles' for archival."""
731 complete_state = load_complete_state(
732 options, cwd, options.subdir, False)
733 # Make sure that complete_state isn't modified until save_files() is
734 # called, because any changes made to it here will propagate to the files
735 # created (which is probably not intended).
736 complete_state.save_files()
737
738 infiles = complete_state.saved_state.files
739 # Add all the .isolated files.
740 isolated_hash = []
741 isolated_files = [
742 options.isolated,
743 ] + complete_state.saved_state.child_isolated_files
744 for item in isolated_files:
745 item_path = os.path.join(
746 os.path.dirname(complete_state.isolated_filepath), item)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400747 # Do not use isolated_format.hash_file() here because the file is
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500748 # likely smallish (under 500kb) and its file size is needed.
749 with open(item_path, 'rb') as f:
750 content = f.read()
751 isolated_hash.append(
752 complete_state.saved_state.algo(content).hexdigest())
753 isolated_metadata = {
754 'h': isolated_hash[-1],
755 's': len(content),
756 'priority': '0'
757 }
758 infiles[item_path] = isolated_metadata
759 return complete_state, infiles, isolated_hash
760
761
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700762def isolate_and_archive(trees, isolate_server, namespace):
763 """Isolates and uploads a bunch of isolated trees.
maruel@chromium.org29029882013-08-30 12:15:40 +0000764
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700765 Args:
766 trees: list of pairs (Options, working directory) that describe what tree
767 to isolate. Options are processed by 'process_isolate_options'.
768 isolate_server: URL of Isolate Server to upload to.
769 namespace: namespace to upload to.
770
771 Returns a dict {target name -> isolate hash or None}, where target name is
772 a name of *.isolated file without an extension (e.g. 'base_unittests').
773
774 Have multiple failure modes:
775 * If the upload fails due to server or network error returns None.
776 * If some *.isolate file is incorrect (but rest of them are fine and were
777 successfully uploaded), returns a dict where the value of the entry
778 corresponding to invalid *.isolate file is None.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000779 """
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700780 if not trees:
781 return {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000782
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700783 # Helper generator to avoid materializing the full (huge) list of files until
784 # the very end (in upload_tree).
785 def emit_files(root_dir, files):
786 for path, meta in files.iteritems():
787 yield (os.path.join(root_dir, path), meta)
788
789 # Process all *.isolate files, it involves parsing, file system traversal and
790 # hashing. The result is a list of generators that produce files to upload
791 # and the mapping {target name -> hash of *.isolated file} to return from
792 # this function.
793 files_generators = []
794 isolated_hashes = {}
795 with tools.Profiler('Isolate'):
796 for opts, cwd in trees:
797 target_name = os.path.splitext(os.path.basename(opts.isolated))[0]
798 try:
799 complete_state, files, isolated_hash = prepare_for_archival(opts, cwd)
800 files_generators.append(emit_files(complete_state.root_dir, files))
801 isolated_hashes[target_name] = isolated_hash[0]
802 print('%s %s' % (isolated_hash[0], target_name))
803 except Exception:
804 logging.exception('Exception when isolating %s', target_name)
805 isolated_hashes[target_name] = None
806
807 # All bad? Nothing to upload.
808 if all(v is None for v in isolated_hashes.itervalues()):
809 return isolated_hashes
810
811 # Now upload all necessary files at once.
812 with tools.Profiler('Upload'):
813 try:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500814 isolateserver.upload_tree(
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700815 base_url=isolate_server,
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700816 infiles=itertools.chain(*files_generators),
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700817 namespace=namespace)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700818 except Exception:
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700819 logging.exception('Exception while uploading files')
820 return None
821
822 return isolated_hashes
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000823
824
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700825def parse_archive_command_line(args, cwd):
826 """Given list of arguments for 'archive' command returns parsed options.
827
828 Used by CMDbatcharchive to parse options passed via JSON. See also CMDarchive.
829 """
830 parser = optparse.OptionParser()
831 add_isolate_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500832 add_subdir_option(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000833 options, args = parser.parse_args(args)
834 if args:
835 parser.error('Unsupported argument: %s' % args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700836 process_isolate_options(parser, options, cwd)
837 return options
838
839
840### Commands.
841
842
843def CMDarchive(parser, args):
844 """Creates a .isolated file and uploads the tree to an isolate server.
845
846 All the files listed in the .isolated file are put in the isolate server
847 cache via isolateserver.py.
848 """
849 add_isolate_options(parser)
850 add_subdir_option(parser)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500851 isolateserver.add_isolate_server_options(parser)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700852 auth.add_auth_options(parser)
853 options, args = parser.parse_args(args)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500854 if args:
855 parser.error('Unsupported argument: %s' % args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700856 process_isolate_options(parser, options)
857 auth.process_auth_options(parser, options)
Marc-Antoine Ruele290ada2014-12-10 19:48:49 -0500858 isolateserver.process_isolate_server_options(parser, options, True)
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700859 result = isolate_and_archive(
860 [(options, os.getcwd())], options.isolate_server, options.namespace)
861 if result is None:
862 return EXIT_CODE_UPLOAD_ERROR
863 assert len(result) == 1, result
864 if result.values()[0] is None:
865 return EXIT_CODE_ISOLATE_ERROR
866 return 0
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700867
868
869@subcommand.usage('-- GEN_JSON_1 GEN_JSON_2 ...')
870def CMDbatcharchive(parser, args):
871 """Archives multiple isolated trees at once.
872
873 Using single command instead of multiple sequential invocations allows to cut
874 redundant work when isolated trees share common files (e.g. file hashes are
875 checked only once, their presence on the server is checked only once, and
876 so on).
877
878 Takes a list of paths to *.isolated.gen.json files that describe what trees to
879 isolate. Format of files is:
880 {
Andrew Wang83648552014-11-14 13:26:49 -0800881 "version": 1,
882 "dir": <absolute path to a directory all other paths are relative to>,
883 "args": [list of command line arguments for single 'archive' command]
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700884 }
885 """
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500886 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500887 isolateserver.add_archive_options(parser)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700888 auth.add_auth_options(parser)
Vadim Shtayuraf4e9ccb2014-10-01 21:24:53 -0700889 parser.add_option(
890 '--dump-json',
891 metavar='FILE',
892 help='Write isolated hashes of archived trees to this file as JSON')
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700893 options, args = parser.parse_args(args)
894 auth.process_auth_options(parser, options)
Marc-Antoine Ruele290ada2014-12-10 19:48:49 -0500895 isolateserver.process_isolate_server_options(parser, options, True)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700896
897 # Validate all incoming options, prepare what needs to be archived as a list
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700898 # of tuples (archival options, working directory).
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700899 work_units = []
900 for gen_json_path in args:
901 # Validate JSON format of a *.isolated.gen.json file.
902 data = tools.read_json(gen_json_path)
903 if data.get('version') != ISOLATED_GEN_JSON_VERSION:
904 parser.error('Invalid version in %s' % gen_json_path)
905 cwd = data.get('dir')
906 if not isinstance(cwd, unicode) or not os.path.isdir(cwd):
907 parser.error('Invalid dir in %s' % gen_json_path)
908 args = data.get('args')
909 if (not isinstance(args, list) or
910 not all(isinstance(x, unicode) for x in args)):
911 parser.error('Invalid args in %s' % gen_json_path)
912 # Convert command line (embedded in JSON) to Options object.
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700913 work_units.append((parse_archive_command_line(args, cwd), cwd))
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700914
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700915 # Perform the archival, all at once.
916 isolated_hashes = isolate_and_archive(
917 work_units, options.isolate_server, options.namespace)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700918
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700919 # TODO(vadimsh): isolate_and_archive returns None on upload failure, there's
920 # no way currently to figure out what *.isolated file from a batch were
921 # successfully uploaded, so consider them all failed (and emit empty dict
922 # as JSON result).
Vadim Shtayuraf4e9ccb2014-10-01 21:24:53 -0700923 if options.dump_json:
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700924 tools.write_json(options.dump_json, isolated_hashes or {}, False)
Vadim Shtayuraf4e9ccb2014-10-01 21:24:53 -0700925
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700926 if isolated_hashes is None:
927 return EXIT_CODE_UPLOAD_ERROR
928
929 # isolated_hashes[x] is None if 'x.isolate' contains a error.
930 if not all(isolated_hashes.itervalues()):
931 return EXIT_CODE_ISOLATE_ERROR
932
933 return 0
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700934
935
936def CMDcheck(parser, args):
937 """Checks that all the inputs are present and generates .isolated."""
938 add_isolate_options(parser)
939 add_subdir_option(parser)
940 options, args = parser.parse_args(args)
941 if args:
942 parser.error('Unsupported argument: %s' % args)
943 process_isolate_options(parser, options)
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000944
945 complete_state = load_complete_state(
946 options, os.getcwd(), options.subdir, False)
947
948 # Nothing is done specifically. Just store the result and state.
949 complete_state.save_files()
950 return 0
951
952
maruel@chromium.orge5322512013-08-19 20:17:57 +0000953def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000954 """Creates a directory with all the dependencies mapped into it.
955
956 Useful to test manually why a test is failing. The target executable is not
957 run.
958 """
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700959 add_isolate_options(parser)
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -0400960 add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500961 add_skip_refresh_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000962 options, args = parser.parse_args(args)
963 if args:
964 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500965 cwd = os.getcwd()
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700966 process_isolate_options(parser, options, cwd, require_isolated=False)
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -0400967 process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500968 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000969
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500970 if not os.path.isdir(options.outdir):
971 os.makedirs(options.outdir)
972 print('Remapping into %s' % options.outdir)
973 if os.listdir(options.outdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000974 raise ExecutionError('Can\'t remap in a non-empty directory')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000975
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500976 create_isolate_tree(
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500977 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500978 complete_state.saved_state.relative_cwd,
979 complete_state.saved_state.read_only)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000980 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000981 complete_state.save_files()
982 return 0
983
984
maruel@chromium.org29029882013-08-30 12:15:40 +0000985@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +0000986def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000987 """Runs the test executable in an isolated (temporary) directory.
988
989 All the dependencies are mapped into the temporary directory and the
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500990 directory is cleaned up after the target exits.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000991
maruel@chromium.org29029882013-08-30 12:15:40 +0000992 Argument processing stops at -- and these arguments are appended to the
993 command line of the target to run. For example, use:
994 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000995 """
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700996 add_isolate_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500997 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000998 options, args = parser.parse_args(args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700999 process_isolate_options(parser, options, require_isolated=False)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001000 complete_state = load_complete_state(
1001 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001002 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001003 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001004 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001005 cmd = tools.fix_python_path(cmd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001006
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001007 outdir = run_isolated.make_temp_dir(
Marc-Antoine Ruel3c979cb2015-03-11 13:43:28 -04001008 u'isolate-%s' % datetime.date.today(),
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001009 os.path.dirname(complete_state.root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001010 try:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -05001011 # TODO(maruel): Use run_isolated.run_tha_test().
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001012 cwd = create_isolate_tree(
1013 outdir, complete_state.root_dir, complete_state.saved_state.files,
1014 complete_state.saved_state.relative_cwd,
1015 complete_state.saved_state.read_only)
John Abd-El-Malek3f998682014-09-17 17:48:09 -07001016 file_path.ensure_command_has_abs_path(cmd, cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001017 logging.info('Running %s, cwd=%s' % (cmd, cwd))
Marc-Antoine Ruel926dccd2014-09-17 13:40:24 -04001018 try:
1019 result = subprocess.call(cmd, cwd=cwd)
1020 except OSError:
1021 sys.stderr.write(
1022 'Failed to executed the command; executable is missing, maybe you\n'
1023 'forgot to map it in the .isolate file?\n %s\n in %s\n' %
1024 (' '.join(cmd), cwd))
1025 result = 1
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001026 finally:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -04001027 file_path.rmtree(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001028
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001029 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001030 complete_state.save_files()
1031 return result
1032
1033
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001034def _process_variable_arg(option, opt, _value, parser):
1035 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +00001036 if not parser.rargs:
1037 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001038 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001039 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001040 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001041 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001042 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001043 else:
1044 if not parser.rargs:
1045 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001046 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001047 v = parser.rargs.pop(0)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001048 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001049 raise optparse.OptionValueError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001050 'Variable \'%s\' doesn\'t respect format \'%s\'' %
1051 (k, isolate_format.VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -05001052 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001053
1054
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001055def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001056 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001057 parser.add_option(
1058 '-s', '--isolated',
1059 metavar='FILE',
1060 help='.isolated file to generate or read')
1061 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001062 parser.add_option(
1063 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001064 dest='isolated',
1065 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001066 is_win = sys.platform in ('win32', 'cygwin')
1067 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001068 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001069 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001070 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001071 # - configuration variables that are to be used in deducing the matrix to
1072 # reduce.
1073 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001074 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001075 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00001076 action='callback',
1077 callback=_process_variable_arg,
Marc-Antoine Ruel05199462014-03-13 15:40:48 -04001078 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001079 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001080 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001081 help='Config variables are used to determine which conditions should be '
1082 'matched when loading a .isolate file, default: %default. '
1083 'All 3 kinds of variables are persistent accross calls, they are '
1084 'saved inside <.isolated>.state')
1085 parser.add_option(
1086 '--path-variable',
1087 action='callback',
1088 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001089 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001090 dest='path_variables',
1091 metavar='FOO BAR',
1092 help='Path variables are used to replace file paths when loading a '
1093 '.isolate file, default: %default')
1094 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001095 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001096 action='callback',
1097 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001098 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1099 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001100 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001101 help='Extraneous variables are replaced on the \'command\' entry and on '
1102 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001103
1104
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001105def add_isolate_options(parser):
1106 """Adds --isolate, --isolated, --out and --<foo>-variable options."""
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -05001107 isolateserver.add_archive_options(parser)
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001108 group = optparse.OptionGroup(parser, 'Common options')
1109 group.add_option(
1110 '-i', '--isolate',
1111 metavar='FILE',
1112 help='.isolate file to load the dependency data from')
1113 add_variable_option(group)
1114 group.add_option(
1115 '--ignore_broken_items', action='store_true',
1116 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1117 help='Indicates that invalid entries in the isolated file to be '
1118 'only be logged and not stop processing. Defaults to True if '
1119 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
1120 parser.add_option_group(group)
1121
1122
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001123def add_subdir_option(parser):
1124 parser.add_option(
1125 '--subdir',
1126 help='Filters to a subdirectory. Its behavior changes depending if it '
1127 'is a relative path as a string or as a path variable. Path '
1128 'variables are always keyed from the directory containing the '
1129 '.isolate file. Anything else is keyed on the root directory.')
1130
1131
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001132def add_skip_refresh_option(parser):
1133 parser.add_option(
1134 '--skip-refresh', action='store_true',
1135 help='Skip reading .isolate file and do not refresh the hash of '
1136 'dependencies')
1137
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001138
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -04001139def add_outdir_options(parser):
1140 """Adds --outdir, which is orthogonal to --isolate-server.
1141
1142 Note: On upload, separate commands are used between 'archive' and 'hashtable'.
1143 On 'download', the same command can download from either an isolate server or
1144 a file system.
1145 """
1146 parser.add_option(
1147 '-o', '--outdir', metavar='DIR',
1148 help='Directory used to recreate the tree.')
1149
1150
1151def process_outdir_options(parser, options, cwd):
1152 if not options.outdir:
1153 parser.error('--outdir is required.')
1154 if file_path.is_url(options.outdir):
1155 parser.error('Can\'t use an URL for --outdir.')
1156 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1157 # outdir doesn't need native path case since tracing is never done from there.
1158 options.outdir = os.path.abspath(
1159 os.path.normpath(os.path.join(cwd, options.outdir)))
1160 # In theory, we'd create the directory outdir right away. Defer doing it in
1161 # case there's errors in the command line.
1162
1163
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001164def process_isolate_options(parser, options, cwd=None, require_isolated=True):
1165 """Handles options added with 'add_isolate_options'.
1166
1167 Mutates |options| in place, by normalizing path to isolate file, values of
1168 variables, etc.
1169 """
1170 cwd = file_path.get_native_path_case(unicode(cwd or os.getcwd()))
1171
1172 # Parse --isolated option.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001173 if options.isolated:
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +00001174 options.isolated = os.path.normpath(
1175 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001176 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001177 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001178 if options.isolated and not options.isolated.endswith('.isolated'):
1179 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001180
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001181 # Processes all the --<foo>-variable flags.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001182 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001183 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001184 try:
1185 return int(s)
1186 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001187 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001188 options.config_variables = dict(
1189 (k, try_make_int(v)) for k, v in options.config_variables)
1190 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001191 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001192
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001193 # Normalize the path in --isolate.
1194 if options.isolate:
1195 # TODO(maruel): Work with non-ASCII.
1196 # The path must be in native path case for tracing purposes.
1197 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1198 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
1199 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001200
1201
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001202def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00001203 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001204 parser = logging_utils.OptionParserWithLogging(
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001205 version=__version__, verbose=int(os.environ.get('ISOLATE_DEBUG', 0)))
Marc-Antoine Ruel2ca67aa2015-01-23 21:37:53 -05001206 try:
1207 return dispatcher.execute(parser, argv)
1208 except isolated_format.MappingError as e:
1209 print >> sys.stderr, 'Failed to find an input file: %s' % e
1210 return 1
1211 except ExecutionError as e:
1212 print >> sys.stderr, 'Execution failure: %s' % e
1213 return 1
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001214
1215
1216if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00001217 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001218 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001219 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001220 sys.exit(main(sys.argv[1:]))