blob: 22406012b96e604c4c8499ecd5ec3c7fca456403 [file] [log] [blame]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2012 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that 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
maruel0ddcad62016-10-27 15:11:24 -070011 https://github.com/luci/luci-py/tree/master/appengine/isolate/doc/client
12 https://github.com/luci/luci-py/blob/master/appengine/isolate/doc/Design.md#isolated-file-format
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 Ruelb2cef0f2017-10-31 10:51:23 -040016__version__ = '0.4.5'
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
maruel8e4e40c2016-05-30 06:21:07 -070040from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000041from utils import tools
42
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000043
Vadim Shtayuraea38c572014-10-06 16:57:16 -070044# Exit code of 'archive' and 'batcharchive' if the command fails due to an error
45# in *.isolate file (format error, or some referenced files are missing, etc.)
46EXIT_CODE_ISOLATE_ERROR = 1
47
48
49# Exit code of 'archive' and 'batcharchive' if the command fails due to
50# a network or server issue. It is an infrastructure failure.
51EXIT_CODE_UPLOAD_ERROR = 101
52
53
Vadim Shtayurafddb1432014-09-30 18:32:41 -070054# Supported version of *.isolated.gen.json files consumed by CMDbatcharchive.
55ISOLATED_GEN_JSON_VERSION = 1
56
57
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000058class ExecutionError(Exception):
59 """A generic error occurred."""
60 def __str__(self):
61 return self.args[0]
62
63
64### Path handling code.
65
66
maruel@chromium.org7b844a62013-09-17 13:04:59 +000067def recreate_tree(outdir, indir, infiles, action, as_hash):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000068 """Creates a new tree with only the input files in it.
69
70 Arguments:
71 outdir: Output directory to create the files in.
72 indir: Root directory the infiles are based in.
73 infiles: dict of files to map from |indir| to |outdir|.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040074 action: One of accepted action of file_path.link_file().
maruel@chromium.org7b844a62013-09-17 13:04:59 +000075 as_hash: Output filename is the hash instead of relfile.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000076 """
77 logging.info(
maruel@chromium.org7b844a62013-09-17 13:04:59 +000078 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
79 (outdir, indir, len(infiles), action, as_hash))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000080
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000081 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000082 if not os.path.isdir(outdir):
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +000083 logging.info('Creating %s' % outdir)
maruel12e30012015-10-09 11:55:35 -070084 fs.makedirs(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000085
86 for relfile, metadata in infiles.iteritems():
87 infile = os.path.join(indir, relfile)
maruel@chromium.org7b844a62013-09-17 13:04:59 +000088 if as_hash:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000089 # Do the hashtable specific checks.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000090 if 'l' in metadata:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000091 # Skip links when storing a hashtable.
92 continue
maruel@chromium.orge5c17132012-11-21 18:18:46 +000093 outfile = os.path.join(outdir, metadata['h'])
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +000094 if os.path.isfile(outfile):
95 # Just do a quick check that the file size matches. No need to stat()
96 # again the input file, grab the value from the dict.
maruel@chromium.orge5c17132012-11-21 18:18:46 +000097 if not 's' in metadata:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -040098 raise isolated_format.MappingError(
maruel@chromium.org861a5e72012-10-09 14:49:42 +000099 'Misconfigured item %s: %s' % (relfile, metadata))
maruel12e30012015-10-09 11:55:35 -0700100 if metadata['s'] == fs.stat(outfile).st_size:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000101 continue
102 else:
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000103 logging.warn('Overwritting %s' % metadata['h'])
maruel12e30012015-10-09 11:55:35 -0700104 fs.remove(outfile)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000105 else:
106 outfile = os.path.join(outdir, relfile)
nodire5028a92016-04-29 14:38:21 -0700107 file_path.ensure_tree(os.path.dirname(outfile))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000108
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.
maruel12e30012015-10-09 11:55:35 -0700113 fs.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
Adrian Ludwinb5b05312017-09-13 07:46:24 -0400264 # time of writing 'sha-1'.
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000265 '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
maruel0ddcad62016-10-27 15:11:24 -0700372 https://github.com/luci/luci-py/blob/master/appengine/isolate/doc/Design.md#isolated-file-format
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 }
Marc-Antoine Ruelb2cef0f2017-10-31 10:51:23 -0400386 out['read_only'] = self.read_only if self.read_only is not None else 1
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000387 if self.command:
388 out['command'] = self.command
Marc-Antoine Ruelb2cef0f2017-10-31 10:51:23 -0400389 if self.relative_cwd:
390 # Only set relative_cwd if a command was also specified. This reduce the
391 # noise for Swarming tasks where the command is specified as part of the
392 # Swarming task request and not thru the isolated file.
393 out['relative_cwd'] = self.relative_cwd
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000394 return out
395
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000396 @property
397 def isolate_filepath(self):
398 """Returns the absolute path of self.isolate_file."""
399 return os.path.normpath(
400 os.path.join(self.isolated_basedir, self.isolate_file))
401
402 # Arguments number differs from overridden method
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000403 @classmethod
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000404 def load(cls, data, isolated_basedir): # pylint: disable=W0221
405 """Special case loading to disallow different OS.
406
407 It is not possible to load a .isolated.state files from a different OS, this
408 file is saved in OS-specific format.
409 """
410 out = super(SavedState, cls).load(data, isolated_basedir)
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400411 if data.get('OS') != sys.platform:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400412 raise isolated_format.IsolatedError('Unexpected OS %s', data.get('OS'))
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000413
414 # Converts human readable form back into the proper class type.
Marc-Antoine Ruel05199462014-03-13 15:40:48 -0400415 algo = data.get('algo')
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400416 if not algo in isolated_format.SUPPORTED_ALGOS:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400417 raise isolated_format.IsolatedError('Unknown algo \'%s\'' % out.algo)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400418 out.algo = isolated_format.SUPPORTED_ALGOS[algo]
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000419
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500420 # Refuse the load non-exact version, even minor difference. This is unlike
421 # isolateserver.load_isolated(). This is because .isolated.state could have
422 # changed significantly even in minor version difference.
Marc-Antoine Ruel226ab802014-03-29 16:22:36 -0400423 if out.version != cls.EXPECTED_VERSION:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400424 raise isolated_format.IsolatedError(
maruel@chromium.org999a1fd2013-09-20 17:41:07 +0000425 'Unsupported version \'%s\'' % out.version)
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000426
Marc-Antoine Ruel16ebc2e2014-02-13 15:39:15 -0500427 # The .isolate file must be valid. If it is not present anymore, zap the
428 # value as if it was not noted, so .isolate_file can safely be overriden
429 # later.
maruel12e30012015-10-09 11:55:35 -0700430 if out.isolate_file and not fs.isfile(out.isolate_filepath):
Marc-Antoine Ruel16ebc2e2014-02-13 15:39:15 -0500431 out.isolate_file = None
432 if out.isolate_file:
433 # It could be absolute on Windows if the drive containing the .isolate and
434 # the drive containing the .isolated files differ, .e.g .isolate is on
435 # C:\\ and .isolated is on D:\\ .
436 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
maruel12e30012015-10-09 11:55:35 -0700437 assert fs.isfile(out.isolate_filepath), out.isolate_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000438 return out
439
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000440 def flatten(self):
441 """Makes sure 'algo' is in human readable form."""
442 out = super(SavedState, self).flatten()
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400443 out['algo'] = isolated_format.SUPPORTED_ALGOS_REVERSE[out['algo']]
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000444 return out
445
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000446 def __str__(self):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500447 def dict_to_str(d):
448 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
449
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000450 out = '%s(\n' % self.__class__.__name__
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000451 out += ' command: %s\n' % self.command
452 out += ' files: %d\n' % len(self.files)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000453 out += ' isolate_file: %s\n' % self.isolate_file
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000454 out += ' read_only: %s\n' % self.read_only
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000455 out += ' relative_cwd: %s\n' % self.relative_cwd
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000456 out += ' child_isolated_files: %s\n' % self.child_isolated_files
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500457 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
458 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500459 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000460 return out
461
462
463class CompleteState(object):
464 """Contains all the state to run the task at hand."""
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000465 def __init__(self, isolated_filepath, saved_state):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000466 super(CompleteState, self).__init__()
maruel@chromium.org29029882013-08-30 12:15:40 +0000467 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000468 self.isolated_filepath = isolated_filepath
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000469 # Contains the data to ease developer's use-case but that is not strictly
470 # necessary.
471 self.saved_state = saved_state
472
473 @classmethod
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000474 def load_files(cls, isolated_filepath):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000475 """Loads state from disk."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000476 assert os.path.isabs(isolated_filepath), isolated_filepath
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000477 isolated_basedir = os.path.dirname(isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000478 return cls(
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000479 isolated_filepath,
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000480 SavedState.load_file(
481 isolatedfile_to_state(isolated_filepath), isolated_basedir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000482
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500483 def load_isolate(
484 self, cwd, isolate_file, path_variables, config_variables,
kjlubick80596f02017-04-28 08:13:19 -0700485 extra_variables, blacklist, ignore_broken_items, collapse_symlinks):
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000486 """Updates self.isolated and self.saved_state with information loaded from a
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000487 .isolate file.
488
489 Processes the loaded data, deduce root_dir, relative_cwd.
490 """
491 # Make sure to not depend on os.getcwd().
492 assert os.path.isabs(isolate_file), isolate_file
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000493 isolate_file = file_path.get_native_path_case(isolate_file)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000494 logging.info(
kjlubick80596f02017-04-28 08:13:19 -0700495 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s, %s)',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500496 cwd, isolate_file, path_variables, config_variables, extra_variables,
kjlubick80596f02017-04-28 08:13:19 -0700497 ignore_broken_items, collapse_symlinks)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000498
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400499 # Config variables are not affected by the paths and must be used to
500 # retrieve the paths, so update them first.
501 self.saved_state.update_config(config_variables)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000502
maruel12e30012015-10-09 11:55:35 -0700503 with fs.open(isolate_file, 'r') as f:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000504 # At that point, variables are not replaced yet in command and infiles.
505 # infiles may contain directory entries and is in posix style.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400506 command, infiles, read_only, isolate_cmd_dir = (
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500507 isolate_format.load_isolate_for_config(
508 os.path.dirname(isolate_file), f.read(),
509 self.saved_state.config_variables))
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500510
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400511 # Processes the variables with the new found relative root. Note that 'cwd'
512 # is used when path variables are used.
513 path_variables = normalize_path_variables(
514 cwd, path_variables, isolate_cmd_dir)
515 # Update the rest of the saved state.
516 self.saved_state.update(isolate_file, path_variables, extra_variables)
517
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500518 total_variables = self.saved_state.path_variables.copy()
519 total_variables.update(self.saved_state.config_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500520 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500521 command = [
522 isolate_format.eval_variables(i, total_variables) for i in command
523 ]
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -0500524
525 total_variables = self.saved_state.path_variables.copy()
526 total_variables.update(self.saved_state.extra_variables)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500527 infiles = [
528 isolate_format.eval_variables(f, total_variables) for f in infiles
529 ]
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000530 # root_dir is automatically determined by the deepest root accessed with the
maruel@chromium.org75584e22013-06-20 01:40:24 +0000531 # form '../../foo/bar'. Note that path variables must be taken in account
532 # too, add them as if they were input files.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400533 self.saved_state.root_dir = isolate_format.determine_root_dir(
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400534 isolate_cmd_dir, infiles + self.saved_state.path_variables.values())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000535 # The relative directory is automatically determined by the relative path
536 # between root_dir and the directory containing the .isolate file,
537 # isolate_base_dir.
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400538 relative_cwd = os.path.relpath(isolate_cmd_dir, self.saved_state.root_dir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500539 # Now that we know where the root is, check that the path_variables point
benrg@chromium.org9ae72862013-02-11 05:05:51 +0000540 # inside it.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500541 for k, v in self.saved_state.path_variables.iteritems():
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400542 dest = os.path.join(isolate_cmd_dir, relative_cwd, v)
543 if not file_path.path_starts_with(self.saved_state.root_dir, dest):
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400544 raise isolated_format.MappingError(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400545 'Path variable %s=%r points outside the inferred root directory '
546 '%s; %s'
547 % (k, v, self.saved_state.root_dir, dest))
548 # Normalize the files based to self.saved_state.root_dir. It is important to
549 # keep the trailing os.path.sep at that step.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000550 infiles = [
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500551 file_path.relpath(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400552 file_path.normpath(os.path.join(isolate_cmd_dir, f)),
553 self.saved_state.root_dir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000554 for f in infiles
555 ]
kjlubick80596f02017-04-28 08:13:19 -0700556 follow_symlinks = False
557 if not collapse_symlinks:
558 follow_symlinks = sys.platform != 'win32'
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000559 # Expand the directories by listing each file inside. Up to now, trailing
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400560 # os.path.sep must be kept.
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400561 infiles = isolated_format.expand_directories_and_symlinks(
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400562 self.saved_state.root_dir,
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000563 infiles,
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500564 tools.gen_blacklist(blacklist),
csharp@chromium.org84d2e2e2013-03-27 13:38:42 +0000565 follow_symlinks,
csharp@chromium.org01856802012-11-12 17:48:13 +0000566 ignore_broken_items)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000567
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000568 # Finally, update the new data to be able to generate the foo.isolated file,
569 # the file that is used by run_isolated.py.
Marc-Antoine Ruel6b1084e2014-09-30 15:14:58 -0400570 self.saved_state.update_isolated(command, infiles, read_only, relative_cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000571 logging.debug(self)
572
kjlubick80596f02017-04-28 08:13:19 -0700573 def files_to_metadata(self, subdir, collapse_symlinks):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000574 """Updates self.saved_state.files with the files' mode and hash.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000575
maruel@chromium.org9268f042012-10-17 17:36:41 +0000576 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
577 file is tainted.
578
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400579 See isolated_format.file_to_metadata() for more information.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000580 """
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000581 for infile in sorted(self.saved_state.files):
maruel@chromium.org9268f042012-10-17 17:36:41 +0000582 if subdir and not infile.startswith(subdir):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000583 self.saved_state.files.pop(infile)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000584 else:
585 filepath = os.path.join(self.root_dir, infile)
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400586 self.saved_state.files[infile] = isolated_format.file_to_metadata(
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000587 filepath,
588 self.saved_state.files[infile],
maruel@chromium.orgbaa108d2013-03-28 13:24:51 +0000589 self.saved_state.read_only,
kjlubick80596f02017-04-28 08:13:19 -0700590 self.saved_state.algo,
591 collapse_symlinks)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000592
593 def save_files(self):
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000594 """Saves self.saved_state and creates a .isolated file."""
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000595 logging.debug('Dumping to %s' % self.isolated_filepath)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000596 self.saved_state.child_isolated_files = chromium_save_isolated(
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000597 self.isolated_filepath,
598 self.saved_state.to_isolated(),
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500599 self.saved_state.path_variables,
maruel@chromium.org385d73d2013-09-19 18:33:21 +0000600 self.saved_state.algo)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000601 total_bytes = sum(
602 i.get('s', 0) for i in self.saved_state.files.itervalues())
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000603 if total_bytes:
maruel@chromium.orgd3a17762012-12-13 14:17:15 +0000604 # TODO(maruel): Stats are missing the .isolated files.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000605 logging.debug('Total size: %d bytes' % total_bytes)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000606 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000607 logging.debug('Dumping to %s' % saved_state_file)
Marc-Antoine Ruelde011802013-11-12 15:19:47 -0500608 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000609
610 @property
611 def root_dir(self):
Marc-Antoine Rueledf28952014-03-31 19:55:47 -0400612 return self.saved_state.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000613
614 def __str__(self):
615 def indent(data, indent_length):
616 """Indents text."""
617 spacing = ' ' * indent_length
618 return ''.join(spacing + l for l in str(data).splitlines(True))
619
620 out = '%s(\n' % self.__class__.__name__
621 out += ' root_dir: %s\n' % self.root_dir
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000622 out += ' saved_state: %s)' % indent(self.saved_state, 2)
623 return out
624
625
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000626def load_complete_state(options, cwd, subdir, skip_update):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000627 """Loads a CompleteState.
628
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000629 This includes data from .isolate and .isolated.state files. Never reads the
630 .isolated file.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000631
632 Arguments:
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700633 options: Options instance generated with process_isolate_options. For either
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000634 options.isolate and options.isolated, if the value is set, it is an
635 absolute path.
636 cwd: base directory to be used when loading the .isolate file.
637 subdir: optional argument to only process file in the subdirectory, relative
638 to CompleteState.root_dir.
639 skip_update: Skip trying to load the .isolate file and processing the
640 dependencies. It is useful when not needed, like when tracing.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000641 """
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000642 assert not options.isolate or os.path.isabs(options.isolate)
643 assert not options.isolated or os.path.isabs(options.isolated)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000644 cwd = file_path.get_native_path_case(unicode(cwd))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000645 if options.isolated:
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000646 # Load the previous state if it was present. Namely, "foo.isolated.state".
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000647 # Note: this call doesn't load the .isolate file.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000648 complete_state = CompleteState.load_files(options.isolated)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000649 else:
650 # Constructs a dummy object that cannot be saved. Useful for temporary
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400651 # commands like 'run'. There is no directory containing a .isolated file so
652 # specify the current working directory as a valid directory.
653 complete_state = CompleteState(None, SavedState(os.getcwd()))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000654
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000655 if not options.isolate:
656 if not complete_state.saved_state.isolate_file:
657 if not skip_update:
658 raise ExecutionError('A .isolate file is required.')
659 isolate = None
660 else:
661 isolate = complete_state.saved_state.isolate_filepath
662 else:
663 isolate = options.isolate
664 if complete_state.saved_state.isolate_file:
Marc-Antoine Ruel37989932013-11-19 16:28:08 -0500665 rel_isolate = file_path.safe_relpath(
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000666 options.isolate, complete_state.saved_state.isolated_basedir)
667 if rel_isolate != complete_state.saved_state.isolate_file:
Marc-Antoine Ruel8472efa2014-03-18 14:32:50 -0400668 # This happens if the .isolate file was moved for example. In this case,
669 # discard the saved state.
670 logging.warning(
671 '--isolated %s != %s as saved in %s. Discarding saved state',
672 rel_isolate,
673 complete_state.saved_state.isolate_file,
674 isolatedfile_to_state(options.isolated))
675 complete_state = CompleteState(
676 options.isolated,
677 SavedState(complete_state.saved_state.isolated_basedir))
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000678
679 if not skip_update:
680 # Then load the .isolate and expands directories.
681 complete_state.load_isolate(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500682 cwd, isolate, options.path_variables, options.config_variables,
kjlubick80596f02017-04-28 08:13:19 -0700683 options.extra_variables, options.blacklist, options.ignore_broken_items,
684 options.collapse_symlinks)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000685
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +0000686 # Regenerate complete_state.saved_state.files.
maruel@chromium.org9268f042012-10-17 17:36:41 +0000687 if subdir:
maruel@chromium.org306e0e72012-11-02 18:22:03 +0000688 subdir = unicode(subdir)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -0500689 # This is tricky here. If it is a path, take it from the root_dir. If
690 # it is a variable, it must be keyed from the directory containing the
691 # .isolate file. So translate all variables first.
692 translated_path_variables = dict(
693 (k,
694 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
695 v)))
696 for k, v in complete_state.saved_state.path_variables.iteritems())
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -0500697 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000698 subdir = subdir.replace('/', os.path.sep)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +0000699
700 if not skip_update:
kjlubick80596f02017-04-28 08:13:19 -0700701 complete_state.files_to_metadata(subdir, options.collapse_symlinks)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000702 return complete_state
703
704
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500705def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
706 """Creates a isolated tree usable for test execution.
707
708 Returns the current working directory where the isolated command should be
709 started in.
710 """
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500711 # Forcibly copy when the tree has to be read only. Otherwise the inode is
712 # modified, and this cause real problems because the user's source tree
713 # becomes read only. On the other hand, the cost of doing file copy is huge.
714 if read_only not in (0, None):
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400715 action = file_path.COPY
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500716 else:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400717 action = file_path.HARDLINK_WITH_FALLBACK
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500718
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500719 recreate_tree(
720 outdir=outdir,
721 indir=root_dir,
722 infiles=files,
Marc-Antoine Ruel361bfda2014-01-15 15:26:39 -0500723 action=action,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500724 as_hash=False)
725 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
nodire5028a92016-04-29 14:38:21 -0700726
727 # cwd may not exist when no files are mapped from the directory containing the
728 # .isolate file. But the directory must exist to be the current working
729 # directory.
730 file_path.ensure_tree(cwd)
731
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500732 run_isolated.change_tree_read_only(outdir, read_only)
733 return cwd
734
735
Vadim Shtayurac28b74f2014-10-06 20:00:08 -0700736@tools.profile
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500737def prepare_for_archival(options, cwd):
738 """Loads the isolated file and create 'infiles' for archival."""
739 complete_state = load_complete_state(
740 options, cwd, options.subdir, False)
741 # Make sure that complete_state isn't modified until save_files() is
742 # called, because any changes made to it here will propagate to the files
743 # created (which is probably not intended).
744 complete_state.save_files()
745
746 infiles = complete_state.saved_state.files
747 # Add all the .isolated files.
748 isolated_hash = []
749 isolated_files = [
750 options.isolated,
751 ] + complete_state.saved_state.child_isolated_files
752 for item in isolated_files:
753 item_path = os.path.join(
754 os.path.dirname(complete_state.isolated_filepath), item)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400755 # Do not use isolated_format.hash_file() here because the file is
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500756 # likely smallish (under 500kb) and its file size is needed.
maruel12e30012015-10-09 11:55:35 -0700757 with fs.open(item_path, 'rb') as f:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500758 content = f.read()
759 isolated_hash.append(
760 complete_state.saved_state.algo(content).hexdigest())
761 isolated_metadata = {
762 'h': isolated_hash[-1],
763 's': len(content),
764 'priority': '0'
765 }
766 infiles[item_path] = isolated_metadata
767 return complete_state, infiles, isolated_hash
768
769
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700770def isolate_and_archive(trees, isolate_server, namespace):
771 """Isolates and uploads a bunch of isolated trees.
maruel@chromium.org29029882013-08-30 12:15:40 +0000772
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700773 Args:
774 trees: list of pairs (Options, working directory) that describe what tree
775 to isolate. Options are processed by 'process_isolate_options'.
776 isolate_server: URL of Isolate Server to upload to.
777 namespace: namespace to upload to.
778
779 Returns a dict {target name -> isolate hash or None}, where target name is
780 a name of *.isolated file without an extension (e.g. 'base_unittests').
781
782 Have multiple failure modes:
783 * If the upload fails due to server or network error returns None.
784 * If some *.isolate file is incorrect (but rest of them are fine and were
785 successfully uploaded), returns a dict where the value of the entry
786 corresponding to invalid *.isolate file is None.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000787 """
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700788 if not trees:
789 return {}
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000790
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700791 # Helper generator to avoid materializing the full (huge) list of files until
792 # the very end (in upload_tree).
793 def emit_files(root_dir, files):
794 for path, meta in files.iteritems():
795 yield (os.path.join(root_dir, path), meta)
796
797 # Process all *.isolate files, it involves parsing, file system traversal and
798 # hashing. The result is a list of generators that produce files to upload
799 # and the mapping {target name -> hash of *.isolated file} to return from
800 # this function.
801 files_generators = []
802 isolated_hashes = {}
803 with tools.Profiler('Isolate'):
804 for opts, cwd in trees:
805 target_name = os.path.splitext(os.path.basename(opts.isolated))[0]
806 try:
807 complete_state, files, isolated_hash = prepare_for_archival(opts, cwd)
808 files_generators.append(emit_files(complete_state.root_dir, files))
809 isolated_hashes[target_name] = isolated_hash[0]
810 print('%s %s' % (isolated_hash[0], target_name))
811 except Exception:
812 logging.exception('Exception when isolating %s', target_name)
813 isolated_hashes[target_name] = None
814
815 # All bad? Nothing to upload.
816 if all(v is None for v in isolated_hashes.itervalues()):
817 return isolated_hashes
818
819 # Now upload all necessary files at once.
820 with tools.Profiler('Upload'):
821 try:
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500822 isolateserver.upload_tree(
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700823 base_url=isolate_server,
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700824 infiles=itertools.chain(*files_generators),
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700825 namespace=namespace)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700826 except Exception:
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700827 logging.exception('Exception while uploading files')
828 return None
829
830 return isolated_hashes
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000831
832
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700833def parse_archive_command_line(args, cwd):
834 """Given list of arguments for 'archive' command returns parsed options.
835
836 Used by CMDbatcharchive to parse options passed via JSON. See also CMDarchive.
837 """
838 parser = optparse.OptionParser()
839 add_isolate_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500840 add_subdir_option(parser)
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000841 options, args = parser.parse_args(args)
842 if args:
843 parser.error('Unsupported argument: %s' % args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700844 process_isolate_options(parser, options, cwd)
845 return options
846
847
848### Commands.
849
850
851def CMDarchive(parser, args):
852 """Creates a .isolated file and uploads the tree to an isolate server.
853
854 All the files listed in the .isolated file are put in the isolate server
855 cache via isolateserver.py.
856 """
857 add_isolate_options(parser)
858 add_subdir_option(parser)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500859 isolateserver.add_isolate_server_options(parser)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700860 auth.add_auth_options(parser)
861 options, args = parser.parse_args(args)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500862 if args:
863 parser.error('Unsupported argument: %s' % args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700864 process_isolate_options(parser, options)
865 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700866 isolateserver.process_isolate_server_options(parser, options, True, True)
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700867 result = isolate_and_archive(
maruel12e30012015-10-09 11:55:35 -0700868 [(options, unicode(os.getcwd()))],
869 options.isolate_server,
870 options.namespace)
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700871 if result is None:
872 return EXIT_CODE_UPLOAD_ERROR
873 assert len(result) == 1, result
874 if result.values()[0] is None:
875 return EXIT_CODE_ISOLATE_ERROR
876 return 0
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700877
878
879@subcommand.usage('-- GEN_JSON_1 GEN_JSON_2 ...')
880def CMDbatcharchive(parser, args):
881 """Archives multiple isolated trees at once.
882
883 Using single command instead of multiple sequential invocations allows to cut
884 redundant work when isolated trees share common files (e.g. file hashes are
885 checked only once, their presence on the server is checked only once, and
886 so on).
887
888 Takes a list of paths to *.isolated.gen.json files that describe what trees to
889 isolate. Format of files is:
890 {
Andrew Wang83648552014-11-14 13:26:49 -0800891 "version": 1,
892 "dir": <absolute path to a directory all other paths are relative to>,
893 "args": [list of command line arguments for single 'archive' command]
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700894 }
895 """
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500896 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -0500897 isolateserver.add_archive_options(parser)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700898 auth.add_auth_options(parser)
Vadim Shtayuraf4e9ccb2014-10-01 21:24:53 -0700899 parser.add_option(
900 '--dump-json',
901 metavar='FILE',
902 help='Write isolated hashes of archived trees to this file as JSON')
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700903 options, args = parser.parse_args(args)
904 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700905 isolateserver.process_isolate_server_options(parser, options, True, True)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700906
907 # Validate all incoming options, prepare what needs to be archived as a list
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700908 # of tuples (archival options, working directory).
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700909 work_units = []
910 for gen_json_path in args:
911 # Validate JSON format of a *.isolated.gen.json file.
maruel0b908f12016-01-20 17:09:44 -0800912 try:
913 data = tools.read_json(gen_json_path)
914 except IOError as e:
915 parser.error('Failed to open %s: %s' % (gen_json_path, e))
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700916 if data.get('version') != ISOLATED_GEN_JSON_VERSION:
917 parser.error('Invalid version in %s' % gen_json_path)
918 cwd = data.get('dir')
maruel12e30012015-10-09 11:55:35 -0700919 if not isinstance(cwd, unicode) or not fs.isdir(cwd):
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700920 parser.error('Invalid dir in %s' % gen_json_path)
921 args = data.get('args')
922 if (not isinstance(args, list) or
923 not all(isinstance(x, unicode) for x in args)):
924 parser.error('Invalid args in %s' % gen_json_path)
925 # Convert command line (embedded in JSON) to Options object.
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700926 work_units.append((parse_archive_command_line(args, cwd), cwd))
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700927
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700928 # Perform the archival, all at once.
929 isolated_hashes = isolate_and_archive(
930 work_units, options.isolate_server, options.namespace)
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700931
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700932 # TODO(vadimsh): isolate_and_archive returns None on upload failure, there's
933 # no way currently to figure out what *.isolated file from a batch were
934 # successfully uploaded, so consider them all failed (and emit empty dict
935 # as JSON result).
Vadim Shtayuraf4e9ccb2014-10-01 21:24:53 -0700936 if options.dump_json:
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700937 tools.write_json(options.dump_json, isolated_hashes or {}, False)
Vadim Shtayuraf4e9ccb2014-10-01 21:24:53 -0700938
Vadim Shtayuraea38c572014-10-06 16:57:16 -0700939 if isolated_hashes is None:
940 return EXIT_CODE_UPLOAD_ERROR
941
942 # isolated_hashes[x] is None if 'x.isolate' contains a error.
943 if not all(isolated_hashes.itervalues()):
944 return EXIT_CODE_ISOLATE_ERROR
945
946 return 0
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700947
948
949def CMDcheck(parser, args):
950 """Checks that all the inputs are present and generates .isolated."""
951 add_isolate_options(parser)
952 add_subdir_option(parser)
953 options, args = parser.parse_args(args)
954 if args:
955 parser.error('Unsupported argument: %s' % args)
956 process_isolate_options(parser, options)
maruel@chromium.org2f952d82013-09-13 01:53:17 +0000957
958 complete_state = load_complete_state(
959 options, os.getcwd(), options.subdir, False)
960
961 # Nothing is done specifically. Just store the result and state.
962 complete_state.save_files()
963 return 0
964
965
maruel@chromium.orge5322512013-08-19 20:17:57 +0000966def CMDremap(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000967 """Creates a directory with all the dependencies mapped into it.
968
969 Useful to test manually why a test is failing. The target executable is not
970 run.
971 """
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700972 add_isolate_options(parser)
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -0400973 add_outdir_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500974 add_skip_refresh_option(parser)
maruel@chromium.org9268f042012-10-17 17:36:41 +0000975 options, args = parser.parse_args(args)
976 if args:
977 parser.error('Unsupported argument: %s' % args)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500978 cwd = os.getcwd()
Vadim Shtayurafddb1432014-09-30 18:32:41 -0700979 process_isolate_options(parser, options, cwd, require_isolated=False)
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -0400980 process_outdir_options(parser, options, cwd)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500981 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000982
nodire5028a92016-04-29 14:38:21 -0700983 file_path.ensure_tree(options.outdir)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500984 print('Remapping into %s' % options.outdir)
maruel12e30012015-10-09 11:55:35 -0700985 if fs.listdir(options.outdir):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000986 raise ExecutionError('Can\'t remap in a non-empty directory')
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000987
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500988 create_isolate_tree(
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -0500989 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -0500990 complete_state.saved_state.relative_cwd,
991 complete_state.saved_state.read_only)
maruel@chromium.org4b57f692012-10-05 20:33:09 +0000992 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000993 complete_state.save_files()
994 return 0
995
996
maruel@chromium.org29029882013-08-30 12:15:40 +0000997@subcommand.usage('-- [extra arguments]')
maruel@chromium.orge5322512013-08-19 20:17:57 +0000998def CMDrun(parser, args):
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +0000999 """Runs the test executable in an isolated (temporary) directory.
1000
1001 All the dependencies are mapped into the temporary directory and the
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001002 directory is cleaned up after the target exits.
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001003
maruel@chromium.org29029882013-08-30 12:15:40 +00001004 Argument processing stops at -- and these arguments are appended to the
1005 command line of the target to run. For example, use:
1006 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001007 """
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001008 add_isolate_options(parser)
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001009 add_skip_refresh_option(parser)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001010 options, args = parser.parse_args(args)
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001011 process_isolate_options(parser, options, require_isolated=False)
maruel@chromium.org8abec8b2013-04-16 19:34:20 +00001012 complete_state = load_complete_state(
1013 options, os.getcwd(), None, options.skip_refresh)
maruel@chromium.orgf75b0cb2012-12-11 21:39:00 +00001014 cmd = complete_state.saved_state.command + args
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001015 if not cmd:
maruel@chromium.org29029882013-08-30 12:15:40 +00001016 raise ExecutionError('No command to run.')
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001017 cmd = tools.fix_python_path(cmd)
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001018
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001019 outdir = run_isolated.make_temp_dir(
Marc-Antoine Ruel3c979cb2015-03-11 13:43:28 -04001020 u'isolate-%s' % datetime.date.today(),
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001021 os.path.dirname(complete_state.root_dir))
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001022 try:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -05001023 # TODO(maruel): Use run_isolated.run_tha_test().
Marc-Antoine Ruel1e9ad0c2014-01-14 15:20:33 -05001024 cwd = create_isolate_tree(
1025 outdir, complete_state.root_dir, complete_state.saved_state.files,
1026 complete_state.saved_state.relative_cwd,
1027 complete_state.saved_state.read_only)
John Abd-El-Malek3f998682014-09-17 17:48:09 -07001028 file_path.ensure_command_has_abs_path(cmd, cwd)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001029 logging.info('Running %s, cwd=%s' % (cmd, cwd))
Marc-Antoine Ruel926dccd2014-09-17 13:40:24 -04001030 try:
1031 result = subprocess.call(cmd, cwd=cwd)
1032 except OSError:
1033 sys.stderr.write(
1034 'Failed to executed the command; executable is missing, maybe you\n'
1035 'forgot to map it in the .isolate file?\n %s\n in %s\n' %
1036 (' '.join(cmd), cwd))
1037 result = 1
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001038 finally:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -04001039 file_path.rmtree(outdir)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001040
maruel@chromium.org4b57f692012-10-05 20:33:09 +00001041 if complete_state.isolated_filepath:
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001042 complete_state.save_files()
1043 return result
1044
1045
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001046def _process_variable_arg(option, opt, _value, parser):
1047 """Called by OptionParser to process a --<foo>-variable argument."""
maruel@chromium.org712454d2013-04-04 17:52:34 +00001048 if not parser.rargs:
1049 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001050 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001051 k = parser.rargs.pop(0)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001052 variables = getattr(parser.values, option.dest)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001053 if '=' in k:
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001054 k, v = k.split('=', 1)
maruel@chromium.org712454d2013-04-04 17:52:34 +00001055 else:
1056 if not parser.rargs:
1057 raise optparse.OptionValueError(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001058 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001059 v = parser.rargs.pop(0)
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001060 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001061 raise optparse.OptionValueError(
Marc-Antoine Ruela5a36222014-01-09 10:35:45 -05001062 'Variable \'%s\' doesn\'t respect format \'%s\'' %
1063 (k, isolate_format.VALID_VARIABLE))
Marc-Antoine Ruel9cc42c32013-12-11 09:35:55 -05001064 variables.append((k, v.decode('utf-8')))
maruel@chromium.org712454d2013-04-04 17:52:34 +00001065
1066
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001067def add_variable_option(parser):
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001068 """Adds --isolated and --<foo>-variable to an OptionParser."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001069 parser.add_option(
1070 '-s', '--isolated',
1071 metavar='FILE',
1072 help='.isolated file to generate or read')
1073 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001074 parser.add_option(
1075 '-r', '--result',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001076 dest='isolated',
1077 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001078 is_win = sys.platform in ('win32', 'cygwin')
1079 # There is really 3 kind of variables:
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001080 # - path variables, like DEPTH or PRODUCT_DIR that should be
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001081 # replaced opportunistically when tracing tests.
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001082 # - extraneous things like EXECUTABE_SUFFIX.
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001083 # - configuration variables that are to be used in deducing the matrix to
1084 # reduce.
1085 # - unrelated variables that are used as command flags for example.
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001086 parser.add_option(
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001087 '--config-variable',
maruel@chromium.org712454d2013-04-04 17:52:34 +00001088 action='callback',
1089 callback=_process_variable_arg,
Marc-Antoine Ruel05199462014-03-13 15:40:48 -04001090 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001091 dest='config_variables',
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001092 metavar='FOO BAR',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001093 help='Config variables are used to determine which conditions should be '
1094 'matched when loading a .isolate file, default: %default. '
1095 'All 3 kinds of variables are persistent accross calls, they are '
1096 'saved inside <.isolated>.state')
1097 parser.add_option(
1098 '--path-variable',
1099 action='callback',
1100 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001101 default=[],
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001102 dest='path_variables',
1103 metavar='FOO BAR',
1104 help='Path variables are used to replace file paths when loading a '
1105 '.isolate file, default: %default')
1106 parser.add_option(
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001107 '--extra-variable',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001108 action='callback',
1109 callback=_process_variable_arg,
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001110 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1111 dest='extra_variables',
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001112 metavar='FOO BAR',
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001113 help='Extraneous variables are replaced on the \'command\' entry and on '
1114 'paths in the .isolate file but are not considered relative paths.')
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001115
1116
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001117def add_isolate_options(parser):
1118 """Adds --isolate, --isolated, --out and --<foo>-variable options."""
Marc-Antoine Ruel1f8ba352014-11-04 15:55:03 -05001119 isolateserver.add_archive_options(parser)
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001120 group = optparse.OptionGroup(parser, 'Common options')
1121 group.add_option(
1122 '-i', '--isolate',
1123 metavar='FILE',
1124 help='.isolate file to load the dependency data from')
1125 add_variable_option(group)
1126 group.add_option(
1127 '--ignore_broken_items', action='store_true',
1128 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1129 help='Indicates that invalid entries in the isolated file to be '
1130 'only be logged and not stop processing. Defaults to True if '
1131 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
kjlubick80596f02017-04-28 08:13:19 -07001132 group.add_option(
1133 '-L', '--collapse_symlinks', action='store_true',
1134 help='Treat any symlinks as if they were the normal underlying file')
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001135 parser.add_option_group(group)
1136
1137
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001138def add_subdir_option(parser):
1139 parser.add_option(
1140 '--subdir',
1141 help='Filters to a subdirectory. Its behavior changes depending if it '
1142 'is a relative path as a string or as a path variable. Path '
1143 'variables are always keyed from the directory containing the '
1144 '.isolate file. Anything else is keyed on the root directory.')
1145
1146
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001147def add_skip_refresh_option(parser):
1148 parser.add_option(
1149 '--skip-refresh', action='store_true',
1150 help='Skip reading .isolate file and do not refresh the hash of '
1151 'dependencies')
1152
Marc-Antoine Ruelf9538ee2014-01-30 10:43:54 -05001153
Marc-Antoine Ruele236b5c2014-09-08 18:40:40 -04001154def add_outdir_options(parser):
1155 """Adds --outdir, which is orthogonal to --isolate-server.
1156
1157 Note: On upload, separate commands are used between 'archive' and 'hashtable'.
1158 On 'download', the same command can download from either an isolate server or
1159 a file system.
1160 """
1161 parser.add_option(
1162 '-o', '--outdir', metavar='DIR',
1163 help='Directory used to recreate the tree.')
1164
1165
1166def process_outdir_options(parser, options, cwd):
1167 if not options.outdir:
1168 parser.error('--outdir is required.')
1169 if file_path.is_url(options.outdir):
1170 parser.error('Can\'t use an URL for --outdir.')
1171 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1172 # outdir doesn't need native path case since tracing is never done from there.
1173 options.outdir = os.path.abspath(
1174 os.path.normpath(os.path.join(cwd, options.outdir)))
1175 # In theory, we'd create the directory outdir right away. Defer doing it in
1176 # case there's errors in the command line.
1177
1178
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001179def process_isolate_options(parser, options, cwd=None, require_isolated=True):
1180 """Handles options added with 'add_isolate_options'.
1181
1182 Mutates |options| in place, by normalizing path to isolate file, values of
1183 variables, etc.
1184 """
1185 cwd = file_path.get_native_path_case(unicode(cwd or os.getcwd()))
1186
1187 # Parse --isolated option.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001188 if options.isolated:
maruel12e30012015-10-09 11:55:35 -07001189 options.isolated = os.path.abspath(
1190 os.path.join(cwd, unicode(options.isolated).replace('/', os.path.sep)))
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001191 if require_isolated and not options.isolated:
maruel@chromium.org75c05b42013-07-25 15:51:48 +00001192 parser.error('--isolated is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001193 if options.isolated and not options.isolated.endswith('.isolated'):
1194 parser.error('--isolated value must end with \'.isolated\'')
maruel@chromium.org715e3fb2013-03-19 15:44:06 +00001195
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001196 # Processes all the --<foo>-variable flags.
benrg@chromium.org609b7982013-02-07 16:44:46 +00001197 def try_make_int(s):
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001198 """Converts a value to int if possible, converts to unicode otherwise."""
benrg@chromium.org609b7982013-02-07 16:44:46 +00001199 try:
1200 return int(s)
1201 except ValueError:
maruel@chromium.orge83215b2013-02-21 14:16:59 +00001202 return s.decode('utf-8')
Marc-Antoine Ruel1c1edd62013-12-06 09:13:13 -05001203 options.config_variables = dict(
1204 (k, try_make_int(v)) for k, v in options.config_variables)
1205 options.path_variables = dict(options.path_variables)
Marc-Antoine Ruelf3589b12013-12-06 14:26:16 -05001206 options.extra_variables = dict(options.extra_variables)
maruel@chromium.orgb253fb82012-10-16 21:44:48 +00001207
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001208 # Normalize the path in --isolate.
1209 if options.isolate:
1210 # TODO(maruel): Work with non-ASCII.
1211 # The path must be in native path case for tracing purposes.
1212 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
maruel12e30012015-10-09 11:55:35 -07001213 options.isolate = os.path.abspath(os.path.join(cwd, options.isolate))
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001214 options.isolate = file_path.get_native_path_case(options.isolate)
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001215
1216
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001217def main(argv):
maruel@chromium.orge5322512013-08-19 20:17:57 +00001218 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001219 parser = logging_utils.OptionParserWithLogging(
Vadim Shtayurafddb1432014-09-30 18:32:41 -07001220 version=__version__, verbose=int(os.environ.get('ISOLATE_DEBUG', 0)))
Marc-Antoine Ruel2ca67aa2015-01-23 21:37:53 -05001221 try:
1222 return dispatcher.execute(parser, argv)
1223 except isolated_format.MappingError as e:
1224 print >> sys.stderr, 'Failed to find an input file: %s' % e
1225 return 1
1226 except ExecutionError as e:
1227 print >> sys.stderr, 'Execution failure: %s' % e
1228 return 1
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001229
1230
1231if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001232 subprocess42.inhibit_os_error_reporting()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001233 fix_encoding.fix_encoding()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001234 tools.disable_buffering()
maruel@chromium.orge5322512013-08-19 20:17:57 +00001235 colorama.init()
maruel@chromium.org8fb47fe2012-10-03 20:13:15 +00001236 sys.exit(main(sys.argv[1:]))