blob: a3a4db306d6bfd272feaeab6127d8018c7e7aeae [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
maruel12e30012015-10-09 11:55:35 -070016__version__ = '0.4.4'
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
maruel12e30012015-10-09 11:55:35 -070039from utils import fs
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000040from utils import tools
41
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000042
Vadim Shtayuraea38c572014-10-06 16:57:16 -070043# Exit code of 'archive' and 'batcharchive' if the command fails due to an error
44# in *.isolate file (format error, or some referenced files are missing, etc.)
45EXIT_CODE_ISOLATE_ERROR = 1
46
47
48# Exit code of 'archive' and 'batcharchive' if the command fails due to
49# a network or server issue. It is an infrastructure failure.
50EXIT_CODE_UPLOAD_ERROR = 101
51
52
Vadim Shtayurafddb1432014-09-30 18:32:41 -070053# Supported version of *.isolated.gen.json files consumed by CMDbatcharchive.
54ISOLATED_GEN_JSON_VERSION = 1
55
56
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000057class ExecutionError(Exception):
58 """A generic error occurred."""
59 def __str__(self):
60 return self.args[0]
61
62
63### Path handling code.
64
65
maruel@chromium.org7b844a62013-09-17 13:04:59 +000066def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000067 """Creates a new tree with only the input files in it.
68
69 Arguments:
70 outdir: Output directory to create the files in.
71 indir: Root directory the infiles are based in.
72 infiles: dict of files to map from |indir| to |outdir|.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040073 action: One of accepted action of file_path.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +000074 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000075 """
76 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +000077 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
78 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000079
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000080 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000081 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000082 logging.info('Creating %s' % outdir)
maruel12e30012015-10-09 11:55:35 -070083 fs.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000084
85 for relfile, metadata in infiles.iteritems():
86 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +000087 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000088 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000089 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000090 # Skip links when storing a hashtable.
91 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +000092 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000093 if os.path.isfile(outfile):
94 # Just do a quick check that the file size matches. No need to stat()
95 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000096 if not 's' in metadata:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -040097 raise isolated_format.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +000098 'Misconfigured item %s: %s' % (relfile, metadata))
maruel12e30012015-10-09 11:55:35 -070099 if metadata['s'] == fs.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000100 continue
101 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000102 logging.warn('Overwritting %s' % metadata['h'])
maruel12e30012015-10-09 11:55:35 -0700103 fs.remove(outfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000104 else:
105 outfile = os.path.join(outdir, relfile)
106 outsubdir = os.path.dirname(outfile)
107 if not os.path.isdir(outsubdir):
maruel12e30012015-10-09 11:55:35 -0700108 fs.makedirs(outsubdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000109
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000110 if 'l' in metadata:
111 pointed = metadata['l']
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000112 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000113 # symlink doesn't exist on Windows.
maruel12e30012015-10-09 11:55:35 -0700114 fs.symlink(pointed, outfile) # pylint: disable=E1101
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000115 else:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400116 file_path.link_file(outfile, infile, action)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000117
118
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000119### Variable stuff.
120
121
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500122def _normalize_path_variable(cwd, relative_base_dir, key, value):
123 """Normalizes a path variable into a relative directory.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500124 """
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500125 # Variables could contain / or \ on windows. Always normalize to
126 # os.path.sep.
127 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
128 normalized = file_path.get_native_path_case(os.path.normpath(x))
129 if not os.path.isdir(normalized):
130 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
131
132 # All variables are relative to the .isolate file.
133 normalized = os.path.relpath(normalized, relative_base_dir)
134 logging.debug(
135 'Translated variable %s from %s to %s', key, value, normalized)
136 return normalized
137
138
139def normalize_path_variables(cwd, path_variables, relative_base_dir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000140 """Processes path variables as a special case and returns a copy of the dict.
141
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000142 For each 'path' variable: first normalizes it based on |cwd|, verifies it
143 exists then sets it as relative to relative_base_dir.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500144 """
145 logging.info(
146 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
147 relative_base_dir)
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -0500148 assert isinstance(cwd, unicode), cwd
149 assert isinstance(relative_base_dir, unicode), relative_base_dir
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500150 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
151 return dict(
152 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
153 for k, v in path_variables.iteritems())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000154
155
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500156### Internal state files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000157
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500158
159def isolatedfile_to_state(filename):
160 """For a '.isolate' file, returns the path to the saved '.state' file."""
161 return filename + '.state'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000162
163
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500164def chromium_save_isolated(isolated, data, path_variables, algo):
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000165 """Writes one or many .isolated files.
166
167 This slightly increases the cold cache cost but greatly reduce the warm cache
168 cost by splitting low-churn files off the master .isolated file. It also
169 reduces overall isolateserver memcache consumption.
170 """
171 slaves = []
172
173 def extract_into_included_isolated(prefix):
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000174 new_slave = {
175 'algo': data['algo'],
176 'files': {},
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000177 'version': data['version'],
178 }
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000179 for f in data['files'].keys():
180 if f.startswith(prefix):
181 new_slave['files'][f] = data['files'].pop(f)
182 if new_slave['files']:
183 slaves.append(new_slave)
184
185 # Split test/data/ in its own .isolated file.
186 extract_into_included_isolated(os.path.join('test', 'data', ''))
187
188 # Split everything out of PRODUCT_DIR in its own .isolated file.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500189 if path_variables.get('PRODUCT_DIR'):
190 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000191
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000192 files = []
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000193 for index, f in enumerate(slaves):
194 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500195 tools.write_json(slavepath, f, True)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000196 data.setdefault('includes', []).append(
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400197 isolated_format.hash_file(slavepath, algo))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000198 files.append(os.path.basename(slavepath))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000199
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400200 files.extend(isolated_format.save_isolated(isolated, data))
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000201 return files
202
203
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000204class Flattenable(object):
205 """Represents data that can be represented as a json file."""
206 MEMBERS = ()
207
208 def flatten(self):
209 """Returns a json-serializable version of itself.
210
211 Skips None entries.
212 """
213 items = ((member, getattr(self, member)) for member in self.MEMBERS)
214 return dict((member, value) for member, value in items if value is not None)
215
216 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000217 def load(cls, data, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000218 """Loads a flattened version."""
219 data = data.copy()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000220 out = cls(*args, **kwargs)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000221 for member in out.MEMBERS:
222 if member in data:
223 # Access to a protected member XXX of a client class
224 # pylint: disable=W0212
225 out._load_member(member, data.pop(member))
226 if data:
227 raise ValueError(
228 'Found unexpected entry %s while constructing an object %s' %
229 (data, cls.__name__), data, cls.__name__)
230 return out
231
232 def _load_member(self, member, value):
233 """Loads a member into self."""
234 setattr(self, member, value)
235
236 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000237 def load_file(cls, filename, *args, **kwargs):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000238 """Loads the data from a file or return an empty instance."""
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000239 try:
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500240 out = cls.load(tools.read_json(filename), *args, **kwargs)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000241 logging.debug('Loaded %s(%s)', cls.__name__, filename)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000242 except (IOError, ValueError) as e:
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000243 # On failure, loads the default instance.
244 out = cls(*args, **kwargs)
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000245 logging.warn('Failed to load %s: %s', filename, e)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000246 return out
247
248
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000249class SavedState(Flattenable):
250 """Describes the content of a .state file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000251
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000252 This file caches the items calculated by this script and is used to increase
253 the performance of the script. This file is not loaded by run_isolated.py.
254 This file can always be safely removed.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000255
256 It is important to note that the 'files' dict keys are using native OS path
257 separator instead of '/' used in .isolate file.
258 """
259 MEMBERS = (
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400260 # Value of sys.platform so that the file is rejected if loaded from a
261 # different OS. While this should never happen in practice, users are ...
262 # "creative".
263 'OS',
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000264 # Algorithm used to generate the hash. The only supported value is at the
265 # time of writting 'sha-1'.
266 'algo',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400267 # List of included .isolated files. Used to support/remember 'slave'
268 # .isolated files. Relative path to isolated_basedir.
269 'child_isolated_files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000270 # Cache of the processed command. This value is saved because .isolated
271 # files are never loaded by isolate.py so it's the only way to load the
272 # command safely.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000273 'command',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500274 # GYP variables that are used to generate conditions. The most frequent
275 # example is 'OS'.
276 'config_variables',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500277 # GYP variables that will be replaced in 'command' and paths but will not be
278 # considered a relative directory.
279 'extra_variables',
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000280 # Cache of the files found so the next run can skip hash calculation.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000281 'files',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000282 # Path of the original .isolate file. Relative path to isolated_basedir.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000283 'isolate_file',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400284 # GYP variables used to generate the .isolated files paths based on path
285 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
286 'path_variables',
Marc-Antoine Ruel33d442a2014-10-03 14:41:51 -0400287 # If the generated directory tree should be read-only. Defaults to 1.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000288 'read_only',
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000289 # Relative cwd to use to start the command.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000290 'relative_cwd',
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400291 # Root directory the files are mapped from.
292 'root_dir',
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400293 # Version of the saved state file format. Any breaking change must update
294 # the value.
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000295 'version',
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000296 )
297
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400298 # Bump this version whenever the saved state changes. It is also keyed on the
299 # .isolated file version so any change in the generator will invalidate .state
300 # files.
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400301 EXPECTED_VERSION = isolated_format.ISOLATED_FILE_VERSION + '.2'
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400302
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000303 def __init__(self, isolated_basedir):
304 """Creates an empty SavedState.
305
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400306 Arguments:
307 isolated_basedir: the directory where the .isolated and .isolated.state
308 files are saved.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000309 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000310 super(SavedState, self).__init__()
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000311 assert os.path.isabs(isolated_basedir), isolated_basedir
312 assert os.path.isdir(isolated_basedir), isolated_basedir
313 self.isolated_basedir = isolated_basedir
314
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000315 # The default algorithm used.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400316 self.OS = sys.platform
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400317 self.algo = isolated_format.SUPPORTED_ALGOS['sha-1']
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500318 self.child_isolated_files = []
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000319 self.command = []
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500320 self.config_variables = {}
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500321 self.extra_variables = {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000322 self.files = {}
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000323 self.isolate_file = None
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500324 self.path_variables = {}
Marc-Antoine Ruel33d442a2014-10-03 14:41:51 -0400325 # Defaults to 1 when compiling to .isolated.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000326 self.read_only = None
327 self.relative_cwd = None
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400328 self.root_dir = None
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400329 self.version = self.EXPECTED_VERSION
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000330
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400331 def update_config(self, config_variables):
332 """Updates the saved state with only config variables."""
333 self.config_variables.update(config_variables)
334
335 def update(self, isolate_file, path_variables, extra_variables):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000336 """Updates the saved state with new data to keep GYP variables and internal
337 reference to the original .isolate file.
338 """
maruel@chromium.orge99c1512013-04-09 20:24:11 +0000339 assert os.path.isabs(isolate_file)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000340 # Convert back to a relative path. On Windows, if the isolate and
341 # isolated files are on different drives, isolate_file will stay an absolute
342 # path.
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500343 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000344
345 # The same .isolate file should always be used to generate the .isolated and
346 # .isolated.state.
347 assert isolate_file == self.isolate_file or not self.isolate_file, (
348 isolate_file, self.isolate_file)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500349 self.extra_variables.update(extra_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000350 self.isolate_file = isolate_file
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500351 self.path_variables.update(path_variables)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000352
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400353 def update_isolated(self, command, infiles, read_only, relative_cwd):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000354 """Updates the saved state with data necessary to generate a .isolated file.
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000355
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000356 The new files in |infiles| are added to self.files dict but their hash is
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000357 not calculated here.
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000358 """
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000359 self.command = command
360 # Add new files.
361 for f in infiles:
362 self.files.setdefault(f, {})
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000363 # Prune extraneous files that are not a dependency anymore.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400364 for f in set(self.files).difference(set(infiles)):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000365 del self.files[f]
366 if read_only is not None:
367 self.read_only = read_only
368 self.relative_cwd = relative_cwd
369
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000370 def to_isolated(self):
371 """Creates a .isolated dictionary out of the saved state.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000372
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000373 https://code.google.com/p/swarming/wiki/IsolatedDesign
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000374 """
375 def strip(data):
376 """Returns a 'files' entry with only the whitelisted keys."""
377 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
378
379 out = {
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400380 'algo': isolated_format.SUPPORTED_ALGOS_REVERSE[self.algo],
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000381 'files': dict(
382 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400383 # The version of the .state file is different than the one of the
384 # .isolated file.
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400385 'version': isolated_format.ISOLATED_FILE_VERSION,
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000386 }
387 if self.command:
388 out['command'] = self.command
Marc-Antoine Ruel33d442a2014-10-03 14:41:51 -0400389 out['read_only'] = self.read_only if self.read_only is not None else 1
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000390 if self.relative_cwd:
391 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000392 return out
393
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000394 @property
395 def isolate_filepath(self):
396 """Returns the absolute path of self.isolate_file."""
397 return os.path.normpath(
398 os.path.join(self.isolated_basedir, self.isolate_file))
399
400 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000401 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000402 def load(cls, data, isolated_basedir): # pylint: disable=W0221
403 """Special case loading to disallow different OS.
404
405 It is not possible to load a .isolated.state files from a different OS, this
406 file is saved in OS-specific format.
407 """
408 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400409 if data.get('OS') != sys.platform:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400410 raise isolated_format.IsolatedError('Unexpected OS %s', data.get('OS'))
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000411
412 # Converts human readable form back into the proper class type.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400413 algo = data.get('algo')
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400414 if not algo in isolated_format.SUPPORTED_ALGOS:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400415 raise isolated_format.IsolatedError('Unknown algo \'%s\'' % out.algo)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400416 out.algo = isolated_format.SUPPORTED_ALGOS[algo]
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000417
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500418 # Refuse the load non-exact version, even minor difference. This is unlike
419 # isolateserver.load_isolated(). This is because .isolated.state could have
420 # changed significantly even in minor version difference.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400421 if out.version != cls.EXPECTED_VERSION:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400422 raise isolated_format.IsolatedError(
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000423 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000424
Marc-Antoine Ruel16ebc2e2014-02-13 15:39:15 -0500425 # The .isolate file must be valid. If it is not present anymore, zap the
426 # value as if it was not noted, so .isolate_file can safely be overriden
427 # later.
maruel12e30012015-10-09 11:55:35 -0700428 if out.isolate_file and not fs.isfile(out.isolate_filepath):
Marc-Antoine Ruel16ebc2e2014-02-13 15:39:15 -0500429 out.isolate_file = None
430 if out.isolate_file:
431 # It could be absolute on Windows if the drive containing the .isolate and
432 # the drive containing the .isolated files differ, .e.g .isolate is on
433 # C:\\ and .isolated is on D:\\ .
434 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
maruel12e30012015-10-09 11:55:35 -0700435 assert fs.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000436 return out
437
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000438 def flatten(self):
439 """Makes sure 'algo' is in human readable form."""
440 out = super(SavedState, self).flatten()
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400441 out['algo'] = isolated_format.SUPPORTED_ALGOS_REVERSE[out['algo']]
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000442 return out
443
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000444 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500445 def dict_to_str(d):
446 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
447
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000448 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000449 out += ' command: %s\n' % self.command
450 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000451 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000452 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000453 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000454 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500455 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
456 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500457 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000458 return out
459
460
461class CompleteState(object):
462 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000463 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000464 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +0000465 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000466 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000467 # Contains the data to ease developer's use-case but that is not strictly
468 # necessary.
469 self.saved_state = saved_state
470
471 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000472 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000473 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000474 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000475 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000476 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000477 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000478 SavedState.load_file(
479 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000480
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500481 def load_isolate(
482 self, cwd, isolate_file, path_variables, config_variables,
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500483 extra_variables, blacklist, ignore_broken_items):
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000484 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000485 .isolate file.
486
487 Processes the loaded data, deduce root_dir, relative_cwd.
488 """
489 # Make sure to not depend on os.getcwd().
490 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000491 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000492 logging.info(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500493 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500494 cwd, isolate_file, path_variables, config_variables, extra_variables,
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500495 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000496
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400497 # Config variables are not affected by the paths and must be used to
498 # retrieve the paths, so update them first.
499 self.saved_state.update_config(config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000500
maruel12e30012015-10-09 11:55:35 -0700501 with fs.open(isolate_file, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000502 # At that point, variables are not replaced yet in command and infiles.
503 # infiles may contain directory entries and is in posix style.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400504 command, infiles, read_only, isolate_cmd_dir = (
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500505 isolate_format.load_isolate_for_config(
506 os.path.dirname(isolate_file), f.read(),
507 self.saved_state.config_variables))
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500508
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400509 # Processes the variables with the new found relative root. Note that 'cwd'
510 # is used when path variables are used.
511 path_variables = normalize_path_variables(
512 cwd, path_variables, isolate_cmd_dir)
513 # Update the rest of the saved state.
514 self.saved_state.update(isolate_file, path_variables, extra_variables)
515
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500516 total_variables = self.saved_state.path_variables.copy()
517 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500518 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500519 command = [
520 isolate_format.eval_variables(i, total_variables) for i in command
521 ]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500522
523 total_variables = self.saved_state.path_variables.copy()
524 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500525 infiles = [
526 isolate_format.eval_variables(f, total_variables) for f in infiles
527 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000528 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +0000529 # form '../../foo/bar'. Note that path variables must be taken in account
530 # too, add them as if they were input files.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400531 self.saved_state.root_dir = isolate_format.determine_root_dir(
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400532 isolate_cmd_dir, infiles + self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000533 # The relative directory is automatically determined by the relative path
534 # between root_dir and the directory containing the .isolate file,
535 # isolate_base_dir.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400536 relative_cwd = os.path.relpath(isolate_cmd_dir, self.saved_state.root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500537 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000538 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500539 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400540 dest = os.path.join(isolate_cmd_dir, relative_cwd, v)
541 if not file_path.path_starts_with(self.saved_state.root_dir, dest):
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400542 raise isolated_format.MappingError(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400543 'Path variable %s=%r points outside the inferred root directory '
544 '%s; %s'
545 % (k, v, self.saved_state.root_dir, dest))
546 # Normalize the files based to self.saved_state.root_dir. It is important to
547 # keep the trailing os.path.sep at that step.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000548 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500549 file_path.relpath(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400550 file_path.normpath(os.path.join(isolate_cmd_dir, f)),
551 self.saved_state.root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000552 for f in infiles
553 ]
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400554 follow_symlinks = sys.platform != 'win32'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000555 # Expand the directories by listing each file inside. Up to now, trailing
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400556 # os.path.sep must be kept.
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400557 infiles = isolated_format.expand_directories_and_symlinks(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400558 self.saved_state.root_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000559 infiles,
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500560 tools.gen_blacklist(blacklist),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000561 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +0000562 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000563
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000564 # Finally, update the new data to be able to generate the foo.isolated file,
565 # the file that is used by run_isolated.py.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400566 self.saved_state.update_isolated(command, infiles, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000567 logging.debug(self)
568
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400569 def files_to_metadata(self, subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000570 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000571
maruel@chromium.org9268f042012-10-17 17:36:41 +0000572 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
573 file is tainted.
574
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400575 See isolated_format.file_to_metadata() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000576 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000577 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +0000578 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000579 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000580 else:
581 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400582 self.saved_state.files[infile] = isolated_format.file_to_metadata(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000583 filepath,
584 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000585 self.saved_state.read_only,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000586 self.saved_state.algo)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000587
588 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000589 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000590 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000591 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000592 self.isolated_filepath,
593 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500594 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000595 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000596 total_bytes = sum(
597 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000598 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000599 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000600 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000601 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000602 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500603 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000604
605 @property
606 def root_dir(self):
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400607 return self.saved_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000608
609 def __str__(self):
610 def indent(data, indent_length):
611 """Indents text."""
612 spacing = ' ' * indent_length
613 return ''.join(spacing + l for l in str(data).splitlines(True))
614
615 out = '%s(\n' % self.__class__.__name__
616 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000617 out += ' saved_state: %s)' % indent(self.saved_state, 2)
618 return out
619
620
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000621def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000622 """Loads a CompleteState.
623
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000624 This includes data from .isolate and .isolated.state files. Never reads the
625 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000626
627 Arguments:
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700628 options: Options instance generated with process_isolate_options. For either
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000629 options.isolate and options.isolated, if the value is set, it is an
630 absolute path.
631 cwd: base directory to be used when loading the .isolate file.
632 subdir: optional argument to only process file in the subdirectory, relative
633 to CompleteState.root_dir.
634 skip_update: Skip trying to load the .isolate file and processing the
635 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000636 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000637 assert not options.isolate or os.path.isabs(options.isolate)
638 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000639 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000640 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000641 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000642 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000643 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000644 else:
645 # Constructs a dummy object that cannot be saved. Useful for temporary
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400646 # commands like 'run'. There is no directory containing a .isolated file so
647 # specify the current working directory as a valid directory.
648 complete_state = CompleteState(None, SavedState(os.getcwd()))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000649
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000650 if not options.isolate:
651 if not complete_state.saved_state.isolate_file:
652 if not skip_update:
653 raise ExecutionError('A .isolate file is required.')
654 isolate = None
655 else:
656 isolate = complete_state.saved_state.isolate_filepath
657 else:
658 isolate = options.isolate
659 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500660 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000661 options.isolate, complete_state.saved_state.isolated_basedir)
662 if rel_isolate != complete_state.saved_state.isolate_file:
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400663 # This happens if the .isolate file was moved for example. In this case,
664 # discard the saved state.
665 logging.warning(
666 '--isolated %s != %s as saved in %s. Discarding saved state',
667 rel_isolate,
668 complete_state.saved_state.isolate_file,
669 isolatedfile_to_state(options.isolated))
670 complete_state = CompleteState(
671 options.isolated,
672 SavedState(complete_state.saved_state.isolated_basedir))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000673
674 if not skip_update:
675 # Then load the .isolate and expands directories.
676 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500677 cwd, isolate, options.path_variables, options.config_variables,
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500678 options.extra_variables, options.blacklist, options.ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000679
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000680 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +0000681 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000682 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500683 # This is tricky here. If it is a path, take it from the root_dir. If
684 # it is a variable, it must be keyed from the directory containing the
685 # .isolate file. So translate all variables first.
686 translated_path_variables = dict(
687 (k,
688 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
689 v)))
690 for k, v in complete_state.saved_state.path_variables.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500691 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000692 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000693
694 if not skip_update:
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400695 complete_state.files_to_metadata(subdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000696 return complete_state
697
698
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500699def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
700 """Creates a isolated tree usable for test execution.
701
702 Returns the current working directory where the isolated command should be
703 started in.
704 """
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500705 # Forcibly copy when the tree has to be read only. Otherwise the inode is
706 # modified, and this cause real problems because the user's source tree
707 # becomes read only. On the other hand, the cost of doing file copy is huge.
708 if read_only not in (0, None):
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400709 action = file_path.COPY
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500710 else:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400711 action = file_path.HARDLINK_WITH_FALLBACK
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500712
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500713 recreate_tree(
714 outdir=outdir,
715 indir=root_dir,
716 infiles=files,
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500717 action=action,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500718 as_hash=False)
719 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
maruel12e30012015-10-09 11:55:35 -0700720 if not fs.isdir(cwd):
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500721 # It can happen when no files are mapped from the directory containing the
722 # .isolate file. But the directory must exist to be the current working
723 # directory.
maruel12e30012015-10-09 11:55:35 -0700724 fs.makedirs(cwd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500725 run_isolated.change_tree_read_only(outdir, read_only)
726 return cwd
727
728
Vadim Shtayurac28b74f2014-10-06 20:00:08 -0700729@tools.profile
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500730def prepare_for_archival(options, cwd):
731 """Loads the isolated file and create 'infiles' for archival."""
732 complete_state = load_complete_state(
733 options, cwd, options.subdir, False)
734 # Make sure that complete_state isn't modified until save_files() is
735 # called, because any changes made to it here will propagate to the files
736 # created (which is probably not intended).
737 complete_state.save_files()
738
739 infiles = complete_state.saved_state.files
740 # Add all the .isolated files.
741 isolated_hash = []
742 isolated_files = [
743 options.isolated,
744 ] + complete_state.saved_state.child_isolated_files
745 for item in isolated_files:
746 item_path = os.path.join(
747 os.path.dirname(complete_state.isolated_filepath), item)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400748 # Do not use isolated_format.hash_file() here because the file is
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500749 # likely smallish (under 500kb) and its file size is needed.
maruel12e30012015-10-09 11:55:35 -0700750 with fs.open(item_path, 'rb') as f:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500751 content = f.read()
752 isolated_hash.append(
753 complete_state.saved_state.algo(content).hexdigest())
754 isolated_metadata = {
755 'h': isolated_hash[-1],
756 's': len(content),
757 'priority': '0'
758 }
759 infiles[item_path] = isolated_metadata
760 return complete_state, infiles, isolated_hash
761
762
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700763def isolate_and_archive(trees, isolate_server, namespace):
764 """Isolates and uploads a bunch of isolated trees.
maruel@chromium.org29029882013-08-30 12:15:40 +0000765
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700766 Args:
767 trees: list of pairs (Options, working directory) that describe what tree
768 to isolate. Options are processed by 'process_isolate_options'.
769 isolate_server: URL of Isolate Server to upload to.
770 namespace: namespace to upload to.
771
772 Returns a dict {target name -> isolate hash or None}, where target name is
773 a name of *.isolated file without an extension (e.g. 'base_unittests').
774
775 Have multiple failure modes:
776 * If the upload fails due to server or network error returns None.
777 * If some *.isolate file is incorrect (but rest of them are fine and were
778 successfully uploaded), returns a dict where the value of the entry
779 corresponding to invalid *.isolate file is None.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000780 """
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700781 if not trees:
782 return {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000783
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700784 # Helper generator to avoid materializing the full (huge) list of files until
785 # the very end (in upload_tree).
786 def emit_files(root_dir, files):
787 for path, meta in files.iteritems():
788 yield (os.path.join(root_dir, path), meta)
789
790 # Process all *.isolate files, it involves parsing, file system traversal and
791 # hashing. The result is a list of generators that produce files to upload
792 # and the mapping {target name -> hash of *.isolated file} to return from
793 # this function.
794 files_generators = []
795 isolated_hashes = {}
796 with tools.Profiler('Isolate'):
797 for opts, cwd in trees:
798 target_name = os.path.splitext(os.path.basename(opts.isolated))[0]
799 try:
800 complete_state, files, isolated_hash = prepare_for_archival(opts, cwd)
801 files_generators.append(emit_files(complete_state.root_dir, files))
802 isolated_hashes[target_name] = isolated_hash[0]
803 print('%s %s' % (isolated_hash[0], target_name))
804 except Exception:
805 logging.exception('Exception when isolating %s', target_name)
806 isolated_hashes[target_name] = None
807
808 # All bad? Nothing to upload.
809 if all(v is None for v in isolated_hashes.itervalues()):
810 return isolated_hashes
811
812 # Now upload all necessary files at once.
813 with tools.Profiler('Upload'):
814 try:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500815 isolateserver.upload_tree(
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700816 base_url=isolate_server,
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700817 infiles=itertools.chain(*files_generators),
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700818 namespace=namespace)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700819 except Exception:
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700820 logging.exception('Exception while uploading files')
821 return None
822
823 return isolated_hashes
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000824
825
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700826def parse_archive_command_line(args, cwd):
827 """Given list of arguments for 'archive' command returns parsed options.
828
829 Used by CMDbatcharchive to parse options passed via JSON. See also CMDarchive.
830 """
831 parser = optparse.OptionParser()
832 add_isolate_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500833 add_subdir_option(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000834 options, args = parser.parse_args(args)
835 if args:
836 parser.error('Unsupported argument: %s' % args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700837 process_isolate_options(parser, options, cwd)
838 return options
839
840
841### Commands.
842
843
844def CMDarchive(parser, args):
845 """Creates a .isolated file and uploads the tree to an isolate server.
846
847 All the files listed in the .isolated file are put in the isolate server
848 cache via isolateserver.py.
849 """
850 add_isolate_options(parser)
851 add_subdir_option(parser)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500852 isolateserver.add_isolate_server_options(parser)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700853 auth.add_auth_options(parser)
854 options, args = parser.parse_args(args)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500855 if args:
856 parser.error('Unsupported argument: %s' % args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700857 process_isolate_options(parser, options)
858 auth.process_auth_options(parser, options)
Marc-Antoine Ruele290ada2014-12-10 19:48:49 -0500859 isolateserver.process_isolate_server_options(parser, options, True)
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700860 result = isolate_and_archive(
maruel12e30012015-10-09 11:55:35 -0700861 [(options, unicode(os.getcwd()))],
862 options.isolate_server,
863 options.namespace)
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700864 if result is None:
865 return EXIT_CODE_UPLOAD_ERROR
866 assert len(result) == 1, result
867 if result.values()[0] is None:
868 return EXIT_CODE_ISOLATE_ERROR
869 return 0
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700870
871
872@subcommand.usage('-- GEN_JSON_1 GEN_JSON_2 ...')
873def CMDbatcharchive(parser, args):
874 """Archives multiple isolated trees at once.
875
876 Using single command instead of multiple sequential invocations allows to cut
877 redundant work when isolated trees share common files (e.g. file hashes are
878 checked only once, their presence on the server is checked only once, and
879 so on).
880
881 Takes a list of paths to *.isolated.gen.json files that describe what trees to
882 isolate. Format of files is:
883 {
Andrew Wang83648552014-11-14 13:26:49 -0800884 "version": 1,
885 "dir": <absolute path to a directory all other paths are relative to>,
886 "args": [list of command line arguments for single 'archive' command]
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700887 }
888 """
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500889 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500890 isolateserver.add_archive_options(parser)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700891 auth.add_auth_options(parser)
Vadim Shtayuraf4e9ccb2014-10-01 21:24:53 -0700892 parser.add_option(
893 '--dump-json',
894 metavar='FILE',
895 help='Write isolated hashes of archived trees to this file as JSON')
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700896 options, args = parser.parse_args(args)
897 auth.process_auth_options(parser, options)
Marc-Antoine Ruele290ada2014-12-10 19:48:49 -0500898 isolateserver.process_isolate_server_options(parser, options, True)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700899
900 # Validate all incoming options, prepare what needs to be archived as a list
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700901 # of tuples (archival options, working directory).
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700902 work_units = []
903 for gen_json_path in args:
904 # Validate JSON format of a *.isolated.gen.json file.
905 data = tools.read_json(gen_json_path)
906 if data.get('version') != ISOLATED_GEN_JSON_VERSION:
907 parser.error('Invalid version in %s' % gen_json_path)
908 cwd = data.get('dir')
maruel12e30012015-10-09 11:55:35 -0700909 if not isinstance(cwd, unicode) or not fs.isdir(cwd):
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700910 parser.error('Invalid dir in %s' % gen_json_path)
911 args = data.get('args')
912 if (not isinstance(args, list) or
913 not all(isinstance(x, unicode) for x in args)):
914 parser.error('Invalid args in %s' % gen_json_path)
915 # Convert command line (embedded in JSON) to Options object.
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700916 work_units.append((parse_archive_command_line(args, cwd), cwd))
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700917
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700918 # Perform the archival, all at once.
919 isolated_hashes = isolate_and_archive(
920 work_units, options.isolate_server, options.namespace)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700921
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700922 # TODO(vadimsh): isolate_and_archive returns None on upload failure, there's
923 # no way currently to figure out what *.isolated file from a batch were
924 # successfully uploaded, so consider them all failed (and emit empty dict
925 # as JSON result).
Vadim Shtayuraf4e9ccb2014-10-01 21:24:53 -0700926 if options.dump_json:
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700927 tools.write_json(options.dump_json, isolated_hashes or {}, False)
Vadim Shtayuraf4e9ccb2014-10-01 21:24:53 -0700928
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700929 if isolated_hashes is None:
930 return EXIT_CODE_UPLOAD_ERROR
931
932 # isolated_hashes[x] is None if 'x.isolate' contains a error.
933 if not all(isolated_hashes.itervalues()):
934 return EXIT_CODE_ISOLATE_ERROR
935
936 return 0
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700937
938
939def CMDcheck(parser, args):
940 """Checks that all the inputs are present and generates .isolated."""
941 add_isolate_options(parser)
942 add_subdir_option(parser)
943 options, args = parser.parse_args(args)
944 if args:
945 parser.error('Unsupported argument: %s' % args)
946 process_isolate_options(parser, options)
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000947
948 complete_state = load_complete_state(
949 options, os.getcwd(), options.subdir, False)
950
951 # Nothing is done specifically. Just store the result and state.
952 complete_state.save_files()
953 return 0
954
955
maruel@chromium.orge5322512013-08-19 20:17:57 +0000956def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000957 """Creates a directory with all the dependencies mapped into it.
958
959 Useful to test manually why a test is failing. The target executable is not
960 run.
961 """
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700962 add_isolate_options(parser)
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -0400963 add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500964 add_skip_refresh_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000965 options, args = parser.parse_args(args)
966 if args:
967 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500968 cwd = os.getcwd()
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700969 process_isolate_options(parser, options, cwd, require_isolated=False)
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -0400970 process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500971 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000972
maruel12e30012015-10-09 11:55:35 -0700973 if not fs.isdir(options.outdir):
974 fs.makedirs(options.outdir)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500975 print('Remapping into %s' % options.outdir)
maruel12e30012015-10-09 11:55:35 -0700976 if fs.listdir(options.outdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000977 raise ExecutionError('Can\'t remap in a non-empty directory')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000978
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500979 create_isolate_tree(
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500980 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500981 complete_state.saved_state.relative_cwd,
982 complete_state.saved_state.read_only)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000983 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000984 complete_state.save_files()
985 return 0
986
987
maruel@chromium.org29029882013-08-30 12:15:40 +0000988@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +0000989def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000990 """Runs the test executable in an isolated (temporary) directory.
991
992 All the dependencies are mapped into the temporary directory and the
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500993 directory is cleaned up after the target exits.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000994
maruel@chromium.org29029882013-08-30 12:15:40 +0000995 Argument processing stops at -- and these arguments are appended to the
996 command line of the target to run. For example, use:
997 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000998 """
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700999 add_isolate_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001000 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001001 options, args = parser.parse_args(args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001002 process_isolate_options(parser, options, require_isolated=False)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001003 complete_state = load_complete_state(
1004 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001005 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001006 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001007 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001008 cmd = tools.fix_python_path(cmd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001009
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001010 outdir = run_isolated.make_temp_dir(
Marc-Antoine Ruel3c979cb2015-03-11 13:43:28 -04001011 u'isolate-%s' % datetime.date.today(),
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001012 os.path.dirname(complete_state.root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001013 try:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -05001014 # TODO(maruel): Use run_isolated.run_tha_test().
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001015 cwd = create_isolate_tree(
1016 outdir, complete_state.root_dir, complete_state.saved_state.files,
1017 complete_state.saved_state.relative_cwd,
1018 complete_state.saved_state.read_only)
John Abd-El-Malek3f998682014-09-17 17:48:09 -07001019 file_path.ensure_command_has_abs_path(cmd, cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001020 logging.info('Running %s, cwd=%s' % (cmd, cwd))
Marc-Antoine Ruel926dccd2014-09-17 13:40:24 -04001021 try:
1022 result = subprocess.call(cmd, cwd=cwd)
1023 except OSError:
1024 sys.stderr.write(
1025 'Failed to executed the command; executable is missing, maybe you\n'
1026 'forgot to map it in the .isolate file?\n %s\n in %s\n' %
1027 (' '.join(cmd), cwd))
1028 result = 1
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001029 finally:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -04001030 file_path.rmtree(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001031
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001032 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001033 complete_state.save_files()
1034 return result
1035
1036
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001037def _process_variable_arg(option, opt, _value, parser):
1038 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +00001039 if not parser.rargs:
1040 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001041 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001042 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001043 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001044 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001045 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001046 else:
1047 if not parser.rargs:
1048 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001049 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001050 v = parser.rargs.pop(0)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001051 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001052 raise optparse.OptionValueError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001053 'Variable \'%s\' doesn\'t respect format \'%s\'' %
1054 (k, isolate_format.VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -05001055 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001056
1057
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001058def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001059 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001060 parser.add_option(
1061 '-s', '--isolated',
1062 metavar='FILE',
1063 help='.isolated file to generate or read')
1064 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001065 parser.add_option(
1066 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001067 dest='isolated',
1068 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001069 is_win = sys.platform in ('win32', 'cygwin')
1070 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001071 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001072 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001073 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001074 # - configuration variables that are to be used in deducing the matrix to
1075 # reduce.
1076 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001077 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001078 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00001079 action='callback',
1080 callback=_process_variable_arg,
Marc-Antoine Ruel05199462014-03-13 15:40:48 -04001081 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001082 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001083 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001084 help='Config variables are used to determine which conditions should be '
1085 'matched when loading a .isolate file, default: %default. '
1086 'All 3 kinds of variables are persistent accross calls, they are '
1087 'saved inside <.isolated>.state')
1088 parser.add_option(
1089 '--path-variable',
1090 action='callback',
1091 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001092 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001093 dest='path_variables',
1094 metavar='FOO BAR',
1095 help='Path variables are used to replace file paths when loading a '
1096 '.isolate file, default: %default')
1097 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001098 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001099 action='callback',
1100 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001101 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1102 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001103 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001104 help='Extraneous variables are replaced on the \'command\' entry and on '
1105 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001106
1107
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001108def add_isolate_options(parser):
1109 """Adds --isolate, --isolated, --out and --<foo>-variable options."""
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -05001110 isolateserver.add_archive_options(parser)
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001111 group = optparse.OptionGroup(parser, 'Common options')
1112 group.add_option(
1113 '-i', '--isolate',
1114 metavar='FILE',
1115 help='.isolate file to load the dependency data from')
1116 add_variable_option(group)
1117 group.add_option(
1118 '--ignore_broken_items', action='store_true',
1119 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1120 help='Indicates that invalid entries in the isolated file to be '
1121 'only be logged and not stop processing. Defaults to True if '
1122 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
1123 parser.add_option_group(group)
1124
1125
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001126def add_subdir_option(parser):
1127 parser.add_option(
1128 '--subdir',
1129 help='Filters to a subdirectory. Its behavior changes depending if it '
1130 'is a relative path as a string or as a path variable. Path '
1131 'variables are always keyed from the directory containing the '
1132 '.isolate file. Anything else is keyed on the root directory.')
1133
1134
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001135def add_skip_refresh_option(parser):
1136 parser.add_option(
1137 '--skip-refresh', action='store_true',
1138 help='Skip reading .isolate file and do not refresh the hash of '
1139 'dependencies')
1140
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001141
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -04001142def add_outdir_options(parser):
1143 """Adds --outdir, which is orthogonal to --isolate-server.
1144
1145 Note: On upload, separate commands are used between 'archive' and 'hashtable'.
1146 On 'download', the same command can download from either an isolate server or
1147 a file system.
1148 """
1149 parser.add_option(
1150 '-o', '--outdir', metavar='DIR',
1151 help='Directory used to recreate the tree.')
1152
1153
1154def process_outdir_options(parser, options, cwd):
1155 if not options.outdir:
1156 parser.error('--outdir is required.')
1157 if file_path.is_url(options.outdir):
1158 parser.error('Can\'t use an URL for --outdir.')
1159 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1160 # outdir doesn't need native path case since tracing is never done from there.
1161 options.outdir = os.path.abspath(
1162 os.path.normpath(os.path.join(cwd, options.outdir)))
1163 # In theory, we'd create the directory outdir right away. Defer doing it in
1164 # case there's errors in the command line.
1165
1166
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001167def process_isolate_options(parser, options, cwd=None, require_isolated=True):
1168 """Handles options added with 'add_isolate_options'.
1169
1170 Mutates |options| in place, by normalizing path to isolate file, values of
1171 variables, etc.
1172 """
1173 cwd = file_path.get_native_path_case(unicode(cwd or os.getcwd()))
1174
1175 # Parse --isolated option.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001176 if options.isolated:
maruel12e30012015-10-09 11:55:35 -07001177 options.isolated = os.path.abspath(
1178 os.path.join(cwd, unicode(options.isolated).replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001179 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001180 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001181 if options.isolated and not options.isolated.endswith('.isolated'):
1182 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001183
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001184 # Processes all the --<foo>-variable flags.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001185 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001186 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001187 try:
1188 return int(s)
1189 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001190 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001191 options.config_variables = dict(
1192 (k, try_make_int(v)) for k, v in options.config_variables)
1193 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001194 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001195
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001196 # Normalize the path in --isolate.
1197 if options.isolate:
1198 # TODO(maruel): Work with non-ASCII.
1199 # The path must be in native path case for tracing purposes.
1200 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
maruel12e30012015-10-09 11:55:35 -07001201 options.isolate = os.path.abspath(os.path.join(cwd, options.isolate))
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001202 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001203
1204
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001205def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00001206 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001207 parser = logging_utils.OptionParserWithLogging(
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001208 version=__version__, verbose=int(os.environ.get('ISOLATE_DEBUG', 0)))
Marc-Antoine Ruel2ca67aa2015-01-23 21:37:53 -05001209 try:
1210 return dispatcher.execute(parser, argv)
1211 except isolated_format.MappingError as e:
1212 print >> sys.stderr, 'Failed to find an input file: %s' % e
1213 return 1
1214 except ExecutionError as e:
1215 print >> sys.stderr, 'Execution failure: %s' % e
1216 return 1
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001217
1218
1219if __name__ == '__main__':
maruel@chromium.orge5322512013-08-19 20:17:57 +00001220 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001221 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001222 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001223 sys.exit(main(sys.argv[1:]))