maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python |
maruel | ea586f3 | 2016-04-05 11:11:33 -0700 | [diff] [blame] | 2 | # Copyright 2012 The LUCI Authors. All rights reserved. |
maruel | f1f5e2a | 2016-05-25 17:10:39 -0700 | [diff] [blame] | 3 | # 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 5 | |
maruel@chromium.org | e532251 | 2013-08-19 20:17:57 +0000 | [diff] [blame] | 6 | """Front end tool to operate on .isolate files. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 7 | |
maruel@chromium.org | e532251 | 2013-08-19 20:17:57 +0000 | [diff] [blame] | 8 | This includes creating, merging or compiling them to generate a .isolated file. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 9 | |
| 10 | See more information at |
maruel | 0ddcad6 | 2016-10-27 15:11:24 -0700 | [diff] [blame] | 11 | 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 13 | """ |
maruel@chromium.org | e532251 | 2013-08-19 20:17:57 +0000 | [diff] [blame] | 14 | # Run ./isolate.py --help for more detailed information. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 15 | |
Marc-Antoine Ruel | b2cef0f | 2017-10-31 10:51:23 -0400 | [diff] [blame] | 16 | __version__ = '0.4.5' |
Marc-Antoine Ruel | cfb6085 | 2014-07-02 15:22:00 -0400 | [diff] [blame] | 17 | |
Marc-Antoine Ruel | 9dfdcc2 | 2014-01-08 14:14:18 -0500 | [diff] [blame] | 18 | import datetime |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 19 | import itertools |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 20 | import logging |
| 21 | import optparse |
| 22 | import os |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 23 | import re |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 24 | import subprocess |
| 25 | import sys |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 26 | |
Vadim Shtayura | e34e13a | 2014-02-02 11:23:26 -0800 | [diff] [blame] | 27 | import auth |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 28 | import isolate_format |
Marc-Antoine Ruel | 8bee66d | 2014-08-28 19:02:07 -0400 | [diff] [blame] | 29 | import isolated_format |
maruel@chromium.org | fb78d43 | 2013-08-28 21:22:40 +0000 | [diff] [blame] | 30 | import isolateserver |
maruel@chromium.org | b8375c2 | 2012-10-05 18:10:01 +0000 | [diff] [blame] | 31 | import run_isolated |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 32 | |
maruel@chromium.org | e532251 | 2013-08-19 20:17:57 +0000 | [diff] [blame] | 33 | from third_party import colorama |
| 34 | from third_party.depot_tools import fix_encoding |
| 35 | from third_party.depot_tools import subcommand |
| 36 | |
Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame] | 37 | from utils import logging_utils |
maruel@chromium.org | 561d4b2 | 2013-09-26 21:08:08 +0000 | [diff] [blame] | 38 | from utils import file_path |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 39 | from utils import fs |
maruel | 8e4e40c | 2016-05-30 06:21:07 -0700 | [diff] [blame] | 40 | from utils import subprocess42 |
vadimsh@chromium.org | a432647 | 2013-08-24 02:05:41 +0000 | [diff] [blame] | 41 | from utils import tools |
| 42 | |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 43 | |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 44 | # 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.) |
| 46 | EXIT_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. |
| 51 | EXIT_CODE_UPLOAD_ERROR = 101 |
| 52 | |
| 53 | |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 54 | # Supported version of *.isolated.gen.json files consumed by CMDbatcharchive. |
| 55 | ISOLATED_GEN_JSON_VERSION = 1 |
| 56 | |
| 57 | |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 58 | class 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.org | 7b844a6 | 2013-09-17 13:04:59 +0000 | [diff] [blame] | 67 | def recreate_tree(outdir, indir, infiles, action, as_hash): |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 68 | """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 Ruel | e4ad07e | 2014-10-15 20:22:29 -0400 | [diff] [blame] | 74 | action: One of accepted action of file_path.link_file(). |
maruel@chromium.org | 7b844a6 | 2013-09-17 13:04:59 +0000 | [diff] [blame] | 75 | as_hash: Output filename is the hash instead of relfile. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 76 | """ |
| 77 | logging.info( |
maruel@chromium.org | 7b844a6 | 2013-09-17 13:04:59 +0000 | [diff] [blame] | 78 | 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' % |
| 79 | (outdir, indir, len(infiles), action, as_hash)) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 80 | |
maruel@chromium.org | 61a9b3b | 2012-12-12 17:18:52 +0000 | [diff] [blame] | 81 | assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 82 | if not os.path.isdir(outdir): |
maruel@chromium.org | 61a9b3b | 2012-12-12 17:18:52 +0000 | [diff] [blame] | 83 | logging.info('Creating %s' % outdir) |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 84 | fs.makedirs(outdir) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 85 | |
| 86 | for relfile, metadata in infiles.iteritems(): |
| 87 | infile = os.path.join(indir, relfile) |
maruel@chromium.org | 7b844a6 | 2013-09-17 13:04:59 +0000 | [diff] [blame] | 88 | if as_hash: |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 89 | # Do the hashtable specific checks. |
maruel@chromium.org | e5c1713 | 2012-11-21 18:18:46 +0000 | [diff] [blame] | 90 | if 'l' in metadata: |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 91 | # Skip links when storing a hashtable. |
| 92 | continue |
maruel@chromium.org | e5c1713 | 2012-11-21 18:18:46 +0000 | [diff] [blame] | 93 | outfile = os.path.join(outdir, metadata['h']) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 94 | 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.org | e5c1713 | 2012-11-21 18:18:46 +0000 | [diff] [blame] | 97 | if not 's' in metadata: |
Marc-Antoine Ruel | 1e7658c | 2014-08-28 19:46:39 -0400 | [diff] [blame] | 98 | raise isolated_format.MappingError( |
maruel@chromium.org | 861a5e7 | 2012-10-09 14:49:42 +0000 | [diff] [blame] | 99 | 'Misconfigured item %s: %s' % (relfile, metadata)) |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 100 | if metadata['s'] == fs.stat(outfile).st_size: |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 101 | continue |
| 102 | else: |
maruel@chromium.org | e5c1713 | 2012-11-21 18:18:46 +0000 | [diff] [blame] | 103 | logging.warn('Overwritting %s' % metadata['h']) |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 104 | fs.remove(outfile) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 105 | else: |
| 106 | outfile = os.path.join(outdir, relfile) |
nodir | e5028a9 | 2016-04-29 14:38:21 -0700 | [diff] [blame] | 107 | file_path.ensure_tree(os.path.dirname(outfile)) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 108 | |
maruel@chromium.org | e5c1713 | 2012-11-21 18:18:46 +0000 | [diff] [blame] | 109 | if 'l' in metadata: |
| 110 | pointed = metadata['l'] |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 111 | logging.debug('Symlink: %s -> %s' % (outfile, pointed)) |
maruel@chromium.org | f43e68b | 2012-10-15 20:23:10 +0000 | [diff] [blame] | 112 | # symlink doesn't exist on Windows. |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 113 | fs.symlink(pointed, outfile) # pylint: disable=E1101 |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 114 | else: |
Marc-Antoine Ruel | e4ad07e | 2014-10-15 20:22:29 -0400 | [diff] [blame] | 115 | file_path.link_file(outfile, infile, action) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 116 | |
| 117 | |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 118 | ### Variable stuff. |
| 119 | |
| 120 | |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 121 | def _normalize_path_variable(cwd, relative_base_dir, key, value): |
| 122 | """Normalizes a path variable into a relative directory. |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 123 | """ |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 124 | # 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 | |
| 138 | def normalize_path_variables(cwd, path_variables, relative_base_dir): |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 139 | """Processes path variables as a special case and returns a copy of the dict. |
| 140 | |
maruel@chromium.org | 61a9b3b | 2012-12-12 17:18:52 +0000 | [diff] [blame] | 141 | 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 Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 143 | """ |
| 144 | logging.info( |
| 145 | 'normalize_path_variables(%s, %s, %s)', cwd, path_variables, |
| 146 | relative_base_dir) |
Marc-Antoine Ruel | 9cc42c3 | 2013-12-11 09:35:55 -0500 | [diff] [blame] | 147 | assert isinstance(cwd, unicode), cwd |
| 148 | assert isinstance(relative_base_dir, unicode), relative_base_dir |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 149 | 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 153 | |
| 154 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 155 | ### Internal state files. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 156 | |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 157 | |
| 158 | def isolatedfile_to_state(filename): |
| 159 | """For a '.isolate' file, returns the path to the saved '.state' file.""" |
| 160 | return filename + '.state' |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 161 | |
| 162 | |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 163 | def chromium_save_isolated(isolated, data, path_variables, algo): |
maruel@chromium.org | d3a1776 | 2012-12-13 14:17:15 +0000 | [diff] [blame] | 164 | """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.org | 385d73d | 2013-09-19 18:33:21 +0000 | [diff] [blame] | 173 | new_slave = { |
| 174 | 'algo': data['algo'], |
| 175 | 'files': {}, |
maruel@chromium.org | 385d73d | 2013-09-19 18:33:21 +0000 | [diff] [blame] | 176 | 'version': data['version'], |
| 177 | } |
maruel@chromium.org | d3a1776 | 2012-12-13 14:17:15 +0000 | [diff] [blame] | 178 | 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 Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 188 | if path_variables.get('PRODUCT_DIR'): |
| 189 | extract_into_included_isolated(path_variables['PRODUCT_DIR']) |
maruel@chromium.org | d3a1776 | 2012-12-13 14:17:15 +0000 | [diff] [blame] | 190 | |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 191 | files = [] |
maruel@chromium.org | d3a1776 | 2012-12-13 14:17:15 +0000 | [diff] [blame] | 192 | for index, f in enumerate(slaves): |
| 193 | slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index |
Marc-Antoine Ruel | de01180 | 2013-11-12 15:19:47 -0500 | [diff] [blame] | 194 | tools.write_json(slavepath, f, True) |
maruel@chromium.org | 7b844a6 | 2013-09-17 13:04:59 +0000 | [diff] [blame] | 195 | data.setdefault('includes', []).append( |
Marc-Antoine Ruel | 8bee66d | 2014-08-28 19:02:07 -0400 | [diff] [blame] | 196 | isolated_format.hash_file(slavepath, algo)) |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 197 | files.append(os.path.basename(slavepath)) |
maruel@chromium.org | d3a1776 | 2012-12-13 14:17:15 +0000 | [diff] [blame] | 198 | |
Marc-Antoine Ruel | 52436aa | 2014-08-28 21:57:57 -0400 | [diff] [blame] | 199 | files.extend(isolated_format.save_isolated(isolated, data)) |
maruel@chromium.org | d3a1776 | 2012-12-13 14:17:15 +0000 | [diff] [blame] | 200 | return files |
| 201 | |
| 202 | |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 203 | class 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.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 216 | def load(cls, data, *args, **kwargs): |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 217 | """Loads a flattened version.""" |
| 218 | data = data.copy() |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 219 | out = cls(*args, **kwargs) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 220 | 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.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 236 | def load_file(cls, filename, *args, **kwargs): |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 237 | """Loads the data from a file or return an empty instance.""" |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 238 | try: |
Marc-Antoine Ruel | de01180 | 2013-11-12 15:19:47 -0500 | [diff] [blame] | 239 | out = cls.load(tools.read_json(filename), *args, **kwargs) |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 240 | logging.debug('Loaded %s(%s)', cls.__name__, filename) |
maruel@chromium.org | e9403ab | 2013-09-20 18:03:49 +0000 | [diff] [blame] | 241 | except (IOError, ValueError) as e: |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 242 | # On failure, loads the default instance. |
| 243 | out = cls(*args, **kwargs) |
maruel@chromium.org | e9403ab | 2013-09-20 18:03:49 +0000 | [diff] [blame] | 244 | logging.warn('Failed to load %s: %s', filename, e) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 245 | return out |
| 246 | |
| 247 | |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 248 | class SavedState(Flattenable): |
| 249 | """Describes the content of a .state file. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 250 | |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 251 | 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 254 | |
| 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 Ruel | 0519946 | 2014-03-13 15:40:48 -0400 | [diff] [blame] | 259 | # 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.org | 385d73d | 2013-09-19 18:33:21 +0000 | [diff] [blame] | 263 | # Algorithm used to generate the hash. The only supported value is at the |
Adrian Ludwin | b5b0531 | 2017-09-13 07:46:24 -0400 | [diff] [blame] | 264 | # time of writing 'sha-1'. |
maruel@chromium.org | 385d73d | 2013-09-19 18:33:21 +0000 | [diff] [blame] | 265 | 'algo', |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 266 | # List of included .isolated files. Used to support/remember 'slave' |
| 267 | # .isolated files. Relative path to isolated_basedir. |
| 268 | 'child_isolated_files', |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 269 | # 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 272 | 'command', |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 273 | # GYP variables that are used to generate conditions. The most frequent |
| 274 | # example is 'OS'. |
| 275 | 'config_variables', |
Marc-Antoine Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 276 | # GYP variables that will be replaced in 'command' and paths but will not be |
| 277 | # considered a relative directory. |
| 278 | 'extra_variables', |
maruel@chromium.org | 7b844a6 | 2013-09-17 13:04:59 +0000 | [diff] [blame] | 279 | # Cache of the files found so the next run can skip hash calculation. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 280 | 'files', |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 281 | # Path of the original .isolate file. Relative path to isolated_basedir. |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 282 | 'isolate_file', |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 283 | # 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 Ruel | 33d442a | 2014-10-03 14:41:51 -0400 | [diff] [blame] | 286 | # If the generated directory tree should be read-only. Defaults to 1. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 287 | 'read_only', |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 288 | # Relative cwd to use to start the command. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 289 | 'relative_cwd', |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 290 | # Root directory the files are mapped from. |
| 291 | 'root_dir', |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 292 | # Version of the saved state file format. Any breaking change must update |
| 293 | # the value. |
maruel@chromium.org | 385d73d | 2013-09-19 18:33:21 +0000 | [diff] [blame] | 294 | 'version', |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 295 | ) |
| 296 | |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 297 | # 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 Ruel | 8bee66d | 2014-08-28 19:02:07 -0400 | [diff] [blame] | 300 | EXPECTED_VERSION = isolated_format.ISOLATED_FILE_VERSION + '.2' |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 301 | |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 302 | def __init__(self, isolated_basedir): |
| 303 | """Creates an empty SavedState. |
| 304 | |
Marc-Antoine Ruel | 8472efa | 2014-03-18 14:32:50 -0400 | [diff] [blame] | 305 | Arguments: |
| 306 | isolated_basedir: the directory where the .isolated and .isolated.state |
| 307 | files are saved. |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 308 | """ |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 309 | super(SavedState, self).__init__() |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 310 | 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.org | 385d73d | 2013-09-19 18:33:21 +0000 | [diff] [blame] | 314 | # The default algorithm used. |
Marc-Antoine Ruel | 0519946 | 2014-03-13 15:40:48 -0400 | [diff] [blame] | 315 | self.OS = sys.platform |
Marc-Antoine Ruel | 8bee66d | 2014-08-28 19:02:07 -0400 | [diff] [blame] | 316 | self.algo = isolated_format.SUPPORTED_ALGOS['sha-1'] |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 317 | self.child_isolated_files = [] |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 318 | self.command = [] |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 319 | self.config_variables = {} |
Marc-Antoine Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 320 | self.extra_variables = {} |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 321 | self.files = {} |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 322 | self.isolate_file = None |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 323 | self.path_variables = {} |
Marc-Antoine Ruel | 33d442a | 2014-10-03 14:41:51 -0400 | [diff] [blame] | 324 | # Defaults to 1 when compiling to .isolated. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 325 | self.read_only = None |
| 326 | self.relative_cwd = None |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 327 | self.root_dir = None |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 328 | self.version = self.EXPECTED_VERSION |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 329 | |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 330 | 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.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 335 | """Updates the saved state with new data to keep GYP variables and internal |
| 336 | reference to the original .isolate file. |
| 337 | """ |
maruel@chromium.org | e99c151 | 2013-04-09 20:24:11 +0000 | [diff] [blame] | 338 | assert os.path.isabs(isolate_file) |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 339 | # 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 Ruel | 3798993 | 2013-11-19 16:28:08 -0500 | [diff] [blame] | 342 | isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir) |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 343 | |
| 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 Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 348 | self.extra_variables.update(extra_variables) |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 349 | self.isolate_file = isolate_file |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 350 | self.path_variables.update(path_variables) |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 351 | |
Marc-Antoine Ruel | 6b1084e | 2014-09-30 15:14:58 -0400 | [diff] [blame] | 352 | def update_isolated(self, command, infiles, read_only, relative_cwd): |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 353 | """Updates the saved state with data necessary to generate a .isolated file. |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 354 | |
maruel@chromium.org | 7b844a6 | 2013-09-17 13:04:59 +0000 | [diff] [blame] | 355 | The new files in |infiles| are added to self.files dict but their hash is |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 356 | not calculated here. |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 357 | """ |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 358 | self.command = command |
| 359 | # Add new files. |
| 360 | for f in infiles: |
| 361 | self.files.setdefault(f, {}) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 362 | # Prune extraneous files that are not a dependency anymore. |
Marc-Antoine Ruel | 6b1084e | 2014-09-30 15:14:58 -0400 | [diff] [blame] | 363 | for f in set(self.files).difference(set(infiles)): |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 364 | 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.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 369 | def to_isolated(self): |
| 370 | """Creates a .isolated dictionary out of the saved state. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 371 | |
maruel | 0ddcad6 | 2016-10-27 15:11:24 -0700 | [diff] [blame] | 372 | https://github.com/luci/luci-py/blob/master/appengine/isolate/doc/Design.md#isolated-file-format |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 373 | """ |
| 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 Ruel | 8bee66d | 2014-08-28 19:02:07 -0400 | [diff] [blame] | 379 | 'algo': isolated_format.SUPPORTED_ALGOS_REVERSE[self.algo], |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 380 | 'files': dict( |
| 381 | (filepath, strip(data)) for filepath, data in self.files.iteritems()), |
Marc-Antoine Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 382 | # The version of the .state file is different than the one of the |
| 383 | # .isolated file. |
Marc-Antoine Ruel | 8bee66d | 2014-08-28 19:02:07 -0400 | [diff] [blame] | 384 | 'version': isolated_format.ISOLATED_FILE_VERSION, |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 385 | } |
Marc-Antoine Ruel | b2cef0f | 2017-10-31 10:51:23 -0400 | [diff] [blame] | 386 | out['read_only'] = self.read_only if self.read_only is not None else 1 |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 387 | if self.command: |
| 388 | out['command'] = self.command |
Marc-Antoine Ruel | b2cef0f | 2017-10-31 10:51:23 -0400 | [diff] [blame] | 389 | 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 394 | return out |
| 395 | |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 396 | @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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 403 | @classmethod |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 404 | 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 Ruel | 0519946 | 2014-03-13 15:40:48 -0400 | [diff] [blame] | 411 | if data.get('OS') != sys.platform: |
Marc-Antoine Ruel | 1e7658c | 2014-08-28 19:46:39 -0400 | [diff] [blame] | 412 | raise isolated_format.IsolatedError('Unexpected OS %s', data.get('OS')) |
maruel@chromium.org | 385d73d | 2013-09-19 18:33:21 +0000 | [diff] [blame] | 413 | |
| 414 | # Converts human readable form back into the proper class type. |
Marc-Antoine Ruel | 0519946 | 2014-03-13 15:40:48 -0400 | [diff] [blame] | 415 | algo = data.get('algo') |
Marc-Antoine Ruel | 8bee66d | 2014-08-28 19:02:07 -0400 | [diff] [blame] | 416 | if not algo in isolated_format.SUPPORTED_ALGOS: |
Marc-Antoine Ruel | 1e7658c | 2014-08-28 19:46:39 -0400 | [diff] [blame] | 417 | raise isolated_format.IsolatedError('Unknown algo \'%s\'' % out.algo) |
Marc-Antoine Ruel | 8bee66d | 2014-08-28 19:02:07 -0400 | [diff] [blame] | 418 | out.algo = isolated_format.SUPPORTED_ALGOS[algo] |
maruel@chromium.org | 385d73d | 2013-09-19 18:33:21 +0000 | [diff] [blame] | 419 | |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 420 | # 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 Ruel | 226ab80 | 2014-03-29 16:22:36 -0400 | [diff] [blame] | 423 | if out.version != cls.EXPECTED_VERSION: |
Marc-Antoine Ruel | 1e7658c | 2014-08-28 19:46:39 -0400 | [diff] [blame] | 424 | raise isolated_format.IsolatedError( |
maruel@chromium.org | 999a1fd | 2013-09-20 17:41:07 +0000 | [diff] [blame] | 425 | 'Unsupported version \'%s\'' % out.version) |
maruel@chromium.org | 385d73d | 2013-09-19 18:33:21 +0000 | [diff] [blame] | 426 | |
Marc-Antoine Ruel | 16ebc2e | 2014-02-13 15:39:15 -0500 | [diff] [blame] | 427 | # 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. |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 430 | if out.isolate_file and not fs.isfile(out.isolate_filepath): |
Marc-Antoine Ruel | 16ebc2e | 2014-02-13 15:39:15 -0500 | [diff] [blame] | 431 | 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' |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 437 | assert fs.isfile(out.isolate_filepath), out.isolate_filepath |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 438 | return out |
| 439 | |
maruel@chromium.org | 385d73d | 2013-09-19 18:33:21 +0000 | [diff] [blame] | 440 | def flatten(self): |
| 441 | """Makes sure 'algo' is in human readable form.""" |
| 442 | out = super(SavedState, self).flatten() |
Marc-Antoine Ruel | 8bee66d | 2014-08-28 19:02:07 -0400 | [diff] [blame] | 443 | out['algo'] = isolated_format.SUPPORTED_ALGOS_REVERSE[out['algo']] |
maruel@chromium.org | 385d73d | 2013-09-19 18:33:21 +0000 | [diff] [blame] | 444 | return out |
| 445 | |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 446 | def __str__(self): |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 447 | def dict_to_str(d): |
| 448 | return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d)) |
| 449 | |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 450 | out = '%s(\n' % self.__class__.__name__ |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 451 | out += ' command: %s\n' % self.command |
| 452 | out += ' files: %d\n' % len(self.files) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 453 | out += ' isolate_file: %s\n' % self.isolate_file |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 454 | out += ' read_only: %s\n' % self.read_only |
maruel@chromium.org | 9e9ceaa | 2013-04-05 15:42:42 +0000 | [diff] [blame] | 455 | out += ' relative_cwd: %s\n' % self.relative_cwd |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 456 | out += ' child_isolated_files: %s\n' % self.child_isolated_files |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 457 | 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 Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 459 | out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 460 | return out |
| 461 | |
| 462 | |
| 463 | class CompleteState(object): |
| 464 | """Contains all the state to run the task at hand.""" |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 465 | def __init__(self, isolated_filepath, saved_state): |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 466 | super(CompleteState, self).__init__() |
maruel@chromium.org | 2902988 | 2013-08-30 12:15:40 +0000 | [diff] [blame] | 467 | assert isolated_filepath is None or os.path.isabs(isolated_filepath) |
maruel@chromium.org | 4b57f69 | 2012-10-05 20:33:09 +0000 | [diff] [blame] | 468 | self.isolated_filepath = isolated_filepath |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 469 | # 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.org | 4b57f69 | 2012-10-05 20:33:09 +0000 | [diff] [blame] | 474 | def load_files(cls, isolated_filepath): |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 475 | """Loads state from disk.""" |
maruel@chromium.org | 4b57f69 | 2012-10-05 20:33:09 +0000 | [diff] [blame] | 476 | assert os.path.isabs(isolated_filepath), isolated_filepath |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 477 | isolated_basedir = os.path.dirname(isolated_filepath) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 478 | return cls( |
maruel@chromium.org | 4b57f69 | 2012-10-05 20:33:09 +0000 | [diff] [blame] | 479 | isolated_filepath, |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 480 | SavedState.load_file( |
| 481 | isolatedfile_to_state(isolated_filepath), isolated_basedir)) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 482 | |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 483 | def load_isolate( |
| 484 | self, cwd, isolate_file, path_variables, config_variables, |
kjlubick | 80596f0 | 2017-04-28 08:13:19 -0700 | [diff] [blame] | 485 | extra_variables, blacklist, ignore_broken_items, collapse_symlinks): |
maruel@chromium.org | 4b57f69 | 2012-10-05 20:33:09 +0000 | [diff] [blame] | 486 | """Updates self.isolated and self.saved_state with information loaded from a |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 487 | .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.org | 561d4b2 | 2013-09-26 21:08:08 +0000 | [diff] [blame] | 493 | isolate_file = file_path.get_native_path_case(isolate_file) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 494 | logging.info( |
kjlubick | 80596f0 | 2017-04-28 08:13:19 -0700 | [diff] [blame] | 495 | 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s, %s)', |
Marc-Antoine Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 496 | cwd, isolate_file, path_variables, config_variables, extra_variables, |
kjlubick | 80596f0 | 2017-04-28 08:13:19 -0700 | [diff] [blame] | 497 | ignore_broken_items, collapse_symlinks) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 498 | |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 499 | # 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 502 | |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 503 | with fs.open(isolate_file, 'r') as f: |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 504 | # 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 Ruel | 6b1084e | 2014-09-30 15:14:58 -0400 | [diff] [blame] | 506 | command, infiles, read_only, isolate_cmd_dir = ( |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 507 | isolate_format.load_isolate_for_config( |
| 508 | os.path.dirname(isolate_file), f.read(), |
| 509 | self.saved_state.config_variables)) |
Marc-Antoine Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 510 | |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 511 | # 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 Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 518 | total_variables = self.saved_state.path_variables.copy() |
| 519 | total_variables.update(self.saved_state.config_variables) |
Marc-Antoine Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 520 | total_variables.update(self.saved_state.extra_variables) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 521 | command = [ |
| 522 | isolate_format.eval_variables(i, total_variables) for i in command |
| 523 | ] |
Marc-Antoine Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 524 | |
| 525 | total_variables = self.saved_state.path_variables.copy() |
| 526 | total_variables.update(self.saved_state.extra_variables) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 527 | infiles = [ |
| 528 | isolate_format.eval_variables(f, total_variables) for f in infiles |
| 529 | ] |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 530 | # root_dir is automatically determined by the deepest root accessed with the |
maruel@chromium.org | 75584e2 | 2013-06-20 01:40:24 +0000 | [diff] [blame] | 531 | # form '../../foo/bar'. Note that path variables must be taken in account |
| 532 | # too, add them as if they were input files. |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 533 | self.saved_state.root_dir = isolate_format.determine_root_dir( |
Marc-Antoine Ruel | 6b1084e | 2014-09-30 15:14:58 -0400 | [diff] [blame] | 534 | isolate_cmd_dir, infiles + self.saved_state.path_variables.values()) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 535 | # 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 Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 538 | relative_cwd = os.path.relpath(isolate_cmd_dir, self.saved_state.root_dir) |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 539 | # Now that we know where the root is, check that the path_variables point |
benrg@chromium.org | 9ae7286 | 2013-02-11 05:05:51 +0000 | [diff] [blame] | 540 | # inside it. |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 541 | for k, v in self.saved_state.path_variables.iteritems(): |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 542 | 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 Ruel | 1e7658c | 2014-08-28 19:46:39 -0400 | [diff] [blame] | 544 | raise isolated_format.MappingError( |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 545 | '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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 550 | infiles = [ |
Marc-Antoine Ruel | 3798993 | 2013-11-19 16:28:08 -0500 | [diff] [blame] | 551 | file_path.relpath( |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 552 | file_path.normpath(os.path.join(isolate_cmd_dir, f)), |
| 553 | self.saved_state.root_dir) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 554 | for f in infiles |
| 555 | ] |
kjlubick | 80596f0 | 2017-04-28 08:13:19 -0700 | [diff] [blame] | 556 | follow_symlinks = False |
| 557 | if not collapse_symlinks: |
| 558 | follow_symlinks = sys.platform != 'win32' |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 559 | # Expand the directories by listing each file inside. Up to now, trailing |
Marc-Antoine Ruel | 6b1084e | 2014-09-30 15:14:58 -0400 | [diff] [blame] | 560 | # os.path.sep must be kept. |
Marc-Antoine Ruel | 9225779 | 2014-08-28 20:51:08 -0400 | [diff] [blame] | 561 | infiles = isolated_format.expand_directories_and_symlinks( |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 562 | self.saved_state.root_dir, |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 563 | infiles, |
Marc-Antoine Ruel | 1f8ba35 | 2014-11-04 15:55:03 -0500 | [diff] [blame] | 564 | tools.gen_blacklist(blacklist), |
csharp@chromium.org | 84d2e2e | 2013-03-27 13:38:42 +0000 | [diff] [blame] | 565 | follow_symlinks, |
csharp@chromium.org | 0185680 | 2012-11-12 17:48:13 +0000 | [diff] [blame] | 566 | ignore_broken_items) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 567 | |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 568 | # 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 Ruel | 6b1084e | 2014-09-30 15:14:58 -0400 | [diff] [blame] | 570 | self.saved_state.update_isolated(command, infiles, read_only, relative_cwd) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 571 | logging.debug(self) |
| 572 | |
kjlubick | 80596f0 | 2017-04-28 08:13:19 -0700 | [diff] [blame] | 573 | def files_to_metadata(self, subdir, collapse_symlinks): |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 574 | """Updates self.saved_state.files with the files' mode and hash. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 575 | |
maruel@chromium.org | 9268f04 | 2012-10-17 17:36:41 +0000 | [diff] [blame] | 576 | If |subdir| is specified, filters to a subdirectory. The resulting .isolated |
| 577 | file is tainted. |
| 578 | |
Marc-Antoine Ruel | 9225779 | 2014-08-28 20:51:08 -0400 | [diff] [blame] | 579 | See isolated_format.file_to_metadata() for more information. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 580 | """ |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 581 | for infile in sorted(self.saved_state.files): |
maruel@chromium.org | 9268f04 | 2012-10-17 17:36:41 +0000 | [diff] [blame] | 582 | if subdir and not infile.startswith(subdir): |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 583 | self.saved_state.files.pop(infile) |
maruel@chromium.org | 9268f04 | 2012-10-17 17:36:41 +0000 | [diff] [blame] | 584 | else: |
| 585 | filepath = os.path.join(self.root_dir, infile) |
Marc-Antoine Ruel | 9225779 | 2014-08-28 20:51:08 -0400 | [diff] [blame] | 586 | self.saved_state.files[infile] = isolated_format.file_to_metadata( |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 587 | filepath, |
| 588 | self.saved_state.files[infile], |
maruel@chromium.org | baa108d | 2013-03-28 13:24:51 +0000 | [diff] [blame] | 589 | self.saved_state.read_only, |
kjlubick | 80596f0 | 2017-04-28 08:13:19 -0700 | [diff] [blame] | 590 | self.saved_state.algo, |
| 591 | collapse_symlinks) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 592 | |
| 593 | def save_files(self): |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 594 | """Saves self.saved_state and creates a .isolated file.""" |
maruel@chromium.org | 4b57f69 | 2012-10-05 20:33:09 +0000 | [diff] [blame] | 595 | logging.debug('Dumping to %s' % self.isolated_filepath) |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 596 | self.saved_state.child_isolated_files = chromium_save_isolated( |
maruel@chromium.org | d3a1776 | 2012-12-13 14:17:15 +0000 | [diff] [blame] | 597 | self.isolated_filepath, |
| 598 | self.saved_state.to_isolated(), |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 599 | self.saved_state.path_variables, |
maruel@chromium.org | 385d73d | 2013-09-19 18:33:21 +0000 | [diff] [blame] | 600 | self.saved_state.algo) |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 601 | total_bytes = sum( |
| 602 | i.get('s', 0) for i in self.saved_state.files.itervalues()) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 603 | if total_bytes: |
maruel@chromium.org | d3a1776 | 2012-12-13 14:17:15 +0000 | [diff] [blame] | 604 | # TODO(maruel): Stats are missing the .isolated files. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 605 | logging.debug('Total size: %d bytes' % total_bytes) |
maruel@chromium.org | 4b57f69 | 2012-10-05 20:33:09 +0000 | [diff] [blame] | 606 | saved_state_file = isolatedfile_to_state(self.isolated_filepath) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 607 | logging.debug('Dumping to %s' % saved_state_file) |
Marc-Antoine Ruel | de01180 | 2013-11-12 15:19:47 -0500 | [diff] [blame] | 608 | tools.write_json(saved_state_file, self.saved_state.flatten(), True) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 609 | |
| 610 | @property |
| 611 | def root_dir(self): |
Marc-Antoine Ruel | edf2895 | 2014-03-31 19:55:47 -0400 | [diff] [blame] | 612 | return self.saved_state.root_dir |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 613 | |
| 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 622 | out += ' saved_state: %s)' % indent(self.saved_state, 2) |
| 623 | return out |
| 624 | |
| 625 | |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 626 | def load_complete_state(options, cwd, subdir, skip_update): |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 627 | """Loads a CompleteState. |
| 628 | |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 629 | This includes data from .isolate and .isolated.state files. Never reads the |
| 630 | .isolated file. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 631 | |
| 632 | Arguments: |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 633 | options: Options instance generated with process_isolate_options. For either |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 634 | 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 641 | """ |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 642 | assert not options.isolate or os.path.isabs(options.isolate) |
| 643 | assert not options.isolated or os.path.isabs(options.isolated) |
maruel@chromium.org | 561d4b2 | 2013-09-26 21:08:08 +0000 | [diff] [blame] | 644 | cwd = file_path.get_native_path_case(unicode(cwd)) |
maruel@chromium.org | 0cd0b18 | 2012-10-22 13:34:15 +0000 | [diff] [blame] | 645 | if options.isolated: |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 646 | # Load the previous state if it was present. Namely, "foo.isolated.state". |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 647 | # Note: this call doesn't load the .isolate file. |
maruel@chromium.org | 0cd0b18 | 2012-10-22 13:34:15 +0000 | [diff] [blame] | 648 | complete_state = CompleteState.load_files(options.isolated) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 649 | else: |
| 650 | # Constructs a dummy object that cannot be saved. Useful for temporary |
Marc-Antoine Ruel | 8472efa | 2014-03-18 14:32:50 -0400 | [diff] [blame] | 651 | # 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 654 | |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 655 | 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 Ruel | 3798993 | 2013-11-19 16:28:08 -0500 | [diff] [blame] | 665 | rel_isolate = file_path.safe_relpath( |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 666 | options.isolate, complete_state.saved_state.isolated_basedir) |
| 667 | if rel_isolate != complete_state.saved_state.isolate_file: |
Marc-Antoine Ruel | 8472efa | 2014-03-18 14:32:50 -0400 | [diff] [blame] | 668 | # 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.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 678 | |
| 679 | if not skip_update: |
| 680 | # Then load the .isolate and expands directories. |
| 681 | complete_state.load_isolate( |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 682 | cwd, isolate, options.path_variables, options.config_variables, |
kjlubick | 80596f0 | 2017-04-28 08:13:19 -0700 | [diff] [blame] | 683 | options.extra_variables, options.blacklist, options.ignore_broken_items, |
| 684 | options.collapse_symlinks) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 685 | |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 686 | # Regenerate complete_state.saved_state.files. |
maruel@chromium.org | 9268f04 | 2012-10-17 17:36:41 +0000 | [diff] [blame] | 687 | if subdir: |
maruel@chromium.org | 306e0e7 | 2012-11-02 18:22:03 +0000 | [diff] [blame] | 688 | subdir = unicode(subdir) |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 689 | # 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 Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 697 | subdir = isolate_format.eval_variables(subdir, translated_path_variables) |
maruel@chromium.org | 9268f04 | 2012-10-17 17:36:41 +0000 | [diff] [blame] | 698 | subdir = subdir.replace('/', os.path.sep) |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 699 | |
| 700 | if not skip_update: |
kjlubick | 80596f0 | 2017-04-28 08:13:19 -0700 | [diff] [blame] | 701 | complete_state.files_to_metadata(subdir, options.collapse_symlinks) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 702 | return complete_state |
| 703 | |
| 704 | |
Marc-Antoine Ruel | 1e9ad0c | 2014-01-14 15:20:33 -0500 | [diff] [blame] | 705 | def 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 Ruel | 361bfda | 2014-01-15 15:26:39 -0500 | [diff] [blame] | 711 | # 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 Ruel | e4ad07e | 2014-10-15 20:22:29 -0400 | [diff] [blame] | 715 | action = file_path.COPY |
Marc-Antoine Ruel | 361bfda | 2014-01-15 15:26:39 -0500 | [diff] [blame] | 716 | else: |
Marc-Antoine Ruel | e4ad07e | 2014-10-15 20:22:29 -0400 | [diff] [blame] | 717 | action = file_path.HARDLINK_WITH_FALLBACK |
Marc-Antoine Ruel | 361bfda | 2014-01-15 15:26:39 -0500 | [diff] [blame] | 718 | |
Marc-Antoine Ruel | 1e9ad0c | 2014-01-14 15:20:33 -0500 | [diff] [blame] | 719 | recreate_tree( |
| 720 | outdir=outdir, |
| 721 | indir=root_dir, |
| 722 | infiles=files, |
Marc-Antoine Ruel | 361bfda | 2014-01-15 15:26:39 -0500 | [diff] [blame] | 723 | action=action, |
Marc-Antoine Ruel | 1e9ad0c | 2014-01-14 15:20:33 -0500 | [diff] [blame] | 724 | as_hash=False) |
| 725 | cwd = os.path.normpath(os.path.join(outdir, relative_cwd)) |
nodir | e5028a9 | 2016-04-29 14:38:21 -0700 | [diff] [blame] | 726 | |
| 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 Ruel | 1e9ad0c | 2014-01-14 15:20:33 -0500 | [diff] [blame] | 732 | run_isolated.change_tree_read_only(outdir, read_only) |
| 733 | return cwd |
| 734 | |
| 735 | |
Vadim Shtayura | c28b74f | 2014-10-06 20:00:08 -0700 | [diff] [blame] | 736 | @tools.profile |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 737 | def 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 Ruel | 8bee66d | 2014-08-28 19:02:07 -0400 | [diff] [blame] | 755 | # Do not use isolated_format.hash_file() here because the file is |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 756 | # likely smallish (under 500kb) and its file size is needed. |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 757 | with fs.open(item_path, 'rb') as f: |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 758 | content = f.read() |
| 759 | isolated_hash.append( |
| 760 | complete_state.saved_state.algo(content).hexdigest()) |
| 761 | isolated_metadata = { |
| 762 | 'h': isolated_hash[-1], |
| 763 | 's': len(content), |
| 764 | 'priority': '0' |
| 765 | } |
| 766 | infiles[item_path] = isolated_metadata |
| 767 | return complete_state, infiles, isolated_hash |
| 768 | |
| 769 | |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 770 | def isolate_and_archive(trees, isolate_server, namespace): |
| 771 | """Isolates and uploads a bunch of isolated trees. |
maruel@chromium.org | 2902988 | 2013-08-30 12:15:40 +0000 | [diff] [blame] | 772 | |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 773 | 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 787 | """ |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 788 | if not trees: |
| 789 | return {} |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 790 | |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 791 | # 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 Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 822 | isolateserver.upload_tree( |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 823 | base_url=isolate_server, |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 824 | infiles=itertools.chain(*files_generators), |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 825 | namespace=namespace) |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 826 | except Exception: |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 827 | logging.exception('Exception while uploading files') |
| 828 | return None |
| 829 | |
| 830 | return isolated_hashes |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 831 | |
| 832 | |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 833 | def 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 Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 840 | add_subdir_option(parser) |
maruel@chromium.org | 2f952d8 | 2013-09-13 01:53:17 +0000 | [diff] [blame] | 841 | options, args = parser.parse_args(args) |
| 842 | if args: |
| 843 | parser.error('Unsupported argument: %s' % args) |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 844 | process_isolate_options(parser, options, cwd) |
| 845 | return options |
| 846 | |
| 847 | |
| 848 | ### Commands. |
| 849 | |
| 850 | |
| 851 | def 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 Ruel | f7d737d | 2014-12-10 15:36:29 -0500 | [diff] [blame] | 859 | isolateserver.add_isolate_server_options(parser) |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 860 | auth.add_auth_options(parser) |
| 861 | options, args = parser.parse_args(args) |
Marc-Antoine Ruel | f7d737d | 2014-12-10 15:36:29 -0500 | [diff] [blame] | 862 | if args: |
| 863 | parser.error('Unsupported argument: %s' % args) |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 864 | process_isolate_options(parser, options) |
| 865 | auth.process_auth_options(parser, options) |
nodir | 55be77b | 2016-05-03 09:39:57 -0700 | [diff] [blame] | 866 | isolateserver.process_isolate_server_options(parser, options, True, True) |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 867 | result = isolate_and_archive( |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 868 | [(options, unicode(os.getcwd()))], |
| 869 | options.isolate_server, |
| 870 | options.namespace) |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 871 | 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 Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 877 | |
| 878 | |
| 879 | @subcommand.usage('-- GEN_JSON_1 GEN_JSON_2 ...') |
| 880 | def 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 Wang | 8364855 | 2014-11-14 13:26:49 -0800 | [diff] [blame] | 891 | "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 Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 894 | } |
| 895 | """ |
Marc-Antoine Ruel | f7d737d | 2014-12-10 15:36:29 -0500 | [diff] [blame] | 896 | isolateserver.add_isolate_server_options(parser) |
Marc-Antoine Ruel | 1f8ba35 | 2014-11-04 15:55:03 -0500 | [diff] [blame] | 897 | isolateserver.add_archive_options(parser) |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 898 | auth.add_auth_options(parser) |
Vadim Shtayura | f4e9ccb | 2014-10-01 21:24:53 -0700 | [diff] [blame] | 899 | parser.add_option( |
| 900 | '--dump-json', |
| 901 | metavar='FILE', |
| 902 | help='Write isolated hashes of archived trees to this file as JSON') |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 903 | options, args = parser.parse_args(args) |
| 904 | auth.process_auth_options(parser, options) |
nodir | 55be77b | 2016-05-03 09:39:57 -0700 | [diff] [blame] | 905 | isolateserver.process_isolate_server_options(parser, options, True, True) |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 906 | |
| 907 | # Validate all incoming options, prepare what needs to be archived as a list |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 908 | # of tuples (archival options, working directory). |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 909 | work_units = [] |
| 910 | for gen_json_path in args: |
| 911 | # Validate JSON format of a *.isolated.gen.json file. |
maruel | 0b908f1 | 2016-01-20 17:09:44 -0800 | [diff] [blame] | 912 | 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 Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 916 | if data.get('version') != ISOLATED_GEN_JSON_VERSION: |
| 917 | parser.error('Invalid version in %s' % gen_json_path) |
| 918 | cwd = data.get('dir') |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 919 | if not isinstance(cwd, unicode) or not fs.isdir(cwd): |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 920 | 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 Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 926 | work_units.append((parse_archive_command_line(args, cwd), cwd)) |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 927 | |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 928 | # Perform the archival, all at once. |
| 929 | isolated_hashes = isolate_and_archive( |
| 930 | work_units, options.isolate_server, options.namespace) |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 931 | |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 932 | # 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 Shtayura | f4e9ccb | 2014-10-01 21:24:53 -0700 | [diff] [blame] | 936 | if options.dump_json: |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 937 | tools.write_json(options.dump_json, isolated_hashes or {}, False) |
Vadim Shtayura | f4e9ccb | 2014-10-01 21:24:53 -0700 | [diff] [blame] | 938 | |
Vadim Shtayura | ea38c57 | 2014-10-06 16:57:16 -0700 | [diff] [blame] | 939 | 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 Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 947 | |
| 948 | |
| 949 | def 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.org | 2f952d8 | 2013-09-13 01:53:17 +0000 | [diff] [blame] | 957 | |
| 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.org | e532251 | 2013-08-19 20:17:57 +0000 | [diff] [blame] | 966 | def CMDremap(parser, args): |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 967 | """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 Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 972 | add_isolate_options(parser) |
Marc-Antoine Ruel | e236b5c | 2014-09-08 18:40:40 -0400 | [diff] [blame] | 973 | add_outdir_options(parser) |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 974 | add_skip_refresh_option(parser) |
maruel@chromium.org | 9268f04 | 2012-10-17 17:36:41 +0000 | [diff] [blame] | 975 | options, args = parser.parse_args(args) |
| 976 | if args: |
| 977 | parser.error('Unsupported argument: %s' % args) |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 978 | cwd = os.getcwd() |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 979 | process_isolate_options(parser, options, cwd, require_isolated=False) |
Marc-Antoine Ruel | e236b5c | 2014-09-08 18:40:40 -0400 | [diff] [blame] | 980 | process_outdir_options(parser, options, cwd) |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 981 | complete_state = load_complete_state(options, cwd, None, options.skip_refresh) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 982 | |
nodir | e5028a9 | 2016-04-29 14:38:21 -0700 | [diff] [blame] | 983 | file_path.ensure_tree(options.outdir) |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 984 | print('Remapping into %s' % options.outdir) |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 985 | if fs.listdir(options.outdir): |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 986 | raise ExecutionError('Can\'t remap in a non-empty directory') |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 987 | |
Marc-Antoine Ruel | 1e9ad0c | 2014-01-14 15:20:33 -0500 | [diff] [blame] | 988 | create_isolate_tree( |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 989 | options.outdir, complete_state.root_dir, complete_state.saved_state.files, |
Marc-Antoine Ruel | 1e9ad0c | 2014-01-14 15:20:33 -0500 | [diff] [blame] | 990 | complete_state.saved_state.relative_cwd, |
| 991 | complete_state.saved_state.read_only) |
maruel@chromium.org | 4b57f69 | 2012-10-05 20:33:09 +0000 | [diff] [blame] | 992 | if complete_state.isolated_filepath: |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 993 | complete_state.save_files() |
| 994 | return 0 |
| 995 | |
| 996 | |
maruel@chromium.org | 2902988 | 2013-08-30 12:15:40 +0000 | [diff] [blame] | 997 | @subcommand.usage('-- [extra arguments]') |
maruel@chromium.org | e532251 | 2013-08-19 20:17:57 +0000 | [diff] [blame] | 998 | def CMDrun(parser, args): |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 999 | """Runs the test executable in an isolated (temporary) directory. |
| 1000 | |
| 1001 | All the dependencies are mapped into the temporary directory and the |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 1002 | directory is cleaned up after the target exits. |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1003 | |
maruel@chromium.org | 2902988 | 2013-08-30 12:15:40 +0000 | [diff] [blame] | 1004 | 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1007 | """ |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 1008 | add_isolate_options(parser) |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 1009 | add_skip_refresh_option(parser) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1010 | options, args = parser.parse_args(args) |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 1011 | process_isolate_options(parser, options, require_isolated=False) |
maruel@chromium.org | 8abec8b | 2013-04-16 19:34:20 +0000 | [diff] [blame] | 1012 | complete_state = load_complete_state( |
| 1013 | options, os.getcwd(), None, options.skip_refresh) |
maruel@chromium.org | f75b0cb | 2012-12-11 21:39:00 +0000 | [diff] [blame] | 1014 | cmd = complete_state.saved_state.command + args |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1015 | if not cmd: |
maruel@chromium.org | 2902988 | 2013-08-30 12:15:40 +0000 | [diff] [blame] | 1016 | raise ExecutionError('No command to run.') |
vadimsh@chromium.org | a432647 | 2013-08-24 02:05:41 +0000 | [diff] [blame] | 1017 | cmd = tools.fix_python_path(cmd) |
Marc-Antoine Ruel | 1e9ad0c | 2014-01-14 15:20:33 -0500 | [diff] [blame] | 1018 | |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 1019 | outdir = run_isolated.make_temp_dir( |
Marc-Antoine Ruel | 3c979cb | 2015-03-11 13:43:28 -0400 | [diff] [blame] | 1020 | u'isolate-%s' % datetime.date.today(), |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 1021 | os.path.dirname(complete_state.root_dir)) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1022 | try: |
Marc-Antoine Ruel | 7124e39 | 2014-01-09 11:49:21 -0500 | [diff] [blame] | 1023 | # TODO(maruel): Use run_isolated.run_tha_test(). |
Marc-Antoine Ruel | 1e9ad0c | 2014-01-14 15:20:33 -0500 | [diff] [blame] | 1024 | 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-Malek | 3f99868 | 2014-09-17 17:48:09 -0700 | [diff] [blame] | 1028 | file_path.ensure_command_has_abs_path(cmd, cwd) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1029 | logging.info('Running %s, cwd=%s' % (cmd, cwd)) |
Marc-Antoine Ruel | 926dccd | 2014-09-17 13:40:24 -0400 | [diff] [blame] | 1030 | 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1038 | finally: |
Marc-Antoine Ruel | e4ad07e | 2014-10-15 20:22:29 -0400 | [diff] [blame] | 1039 | file_path.rmtree(outdir) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1040 | |
maruel@chromium.org | 4b57f69 | 2012-10-05 20:33:09 +0000 | [diff] [blame] | 1041 | if complete_state.isolated_filepath: |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1042 | complete_state.save_files() |
| 1043 | return result |
| 1044 | |
| 1045 | |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1046 | def _process_variable_arg(option, opt, _value, parser): |
| 1047 | """Called by OptionParser to process a --<foo>-variable argument.""" |
maruel@chromium.org | 712454d | 2013-04-04 17:52:34 +0000 | [diff] [blame] | 1048 | if not parser.rargs: |
| 1049 | raise optparse.OptionValueError( |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1050 | 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt)) |
maruel@chromium.org | 712454d | 2013-04-04 17:52:34 +0000 | [diff] [blame] | 1051 | k = parser.rargs.pop(0) |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1052 | variables = getattr(parser.values, option.dest) |
maruel@chromium.org | 712454d | 2013-04-04 17:52:34 +0000 | [diff] [blame] | 1053 | if '=' in k: |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1054 | k, v = k.split('=', 1) |
maruel@chromium.org | 712454d | 2013-04-04 17:52:34 +0000 | [diff] [blame] | 1055 | else: |
| 1056 | if not parser.rargs: |
| 1057 | raise optparse.OptionValueError( |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1058 | 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt)) |
maruel@chromium.org | 712454d | 2013-04-04 17:52:34 +0000 | [diff] [blame] | 1059 | v = parser.rargs.pop(0) |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 1060 | if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k): |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1061 | raise optparse.OptionValueError( |
Marc-Antoine Ruel | a5a3622 | 2014-01-09 10:35:45 -0500 | [diff] [blame] | 1062 | 'Variable \'%s\' doesn\'t respect format \'%s\'' % |
| 1063 | (k, isolate_format.VALID_VARIABLE)) |
Marc-Antoine Ruel | 9cc42c3 | 2013-12-11 09:35:55 -0500 | [diff] [blame] | 1064 | variables.append((k, v.decode('utf-8'))) |
maruel@chromium.org | 712454d | 2013-04-04 17:52:34 +0000 | [diff] [blame] | 1065 | |
| 1066 | |
maruel@chromium.org | b253fb8 | 2012-10-16 21:44:48 +0000 | [diff] [blame] | 1067 | def add_variable_option(parser): |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1068 | """Adds --isolated and --<foo>-variable to an OptionParser.""" |
maruel@chromium.org | 0cd0b18 | 2012-10-22 13:34:15 +0000 | [diff] [blame] | 1069 | 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.org | b253fb8 | 2012-10-16 21:44:48 +0000 | [diff] [blame] | 1074 | parser.add_option( |
| 1075 | '-r', '--result', |
maruel@chromium.org | 0cd0b18 | 2012-10-22 13:34:15 +0000 | [diff] [blame] | 1076 | dest='isolated', |
| 1077 | help=optparse.SUPPRESS_HELP) |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1078 | is_win = sys.platform in ('win32', 'cygwin') |
| 1079 | # There is really 3 kind of variables: |
Marc-Antoine Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 1080 | # - path variables, like DEPTH or PRODUCT_DIR that should be |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1081 | # replaced opportunistically when tracing tests. |
Marc-Antoine Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 1082 | # - extraneous things like EXECUTABE_SUFFIX. |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1083 | # - 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.org | b253fb8 | 2012-10-16 21:44:48 +0000 | [diff] [blame] | 1086 | parser.add_option( |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1087 | '--config-variable', |
maruel@chromium.org | 712454d | 2013-04-04 17:52:34 +0000 | [diff] [blame] | 1088 | action='callback', |
| 1089 | callback=_process_variable_arg, |
Marc-Antoine Ruel | 0519946 | 2014-03-13 15:40:48 -0400 | [diff] [blame] | 1090 | default=[], |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1091 | dest='config_variables', |
maruel@chromium.org | b253fb8 | 2012-10-16 21:44:48 +0000 | [diff] [blame] | 1092 | metavar='FOO BAR', |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1093 | 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 Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 1101 | default=[], |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1102 | 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 Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 1107 | '--extra-variable', |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1108 | action='callback', |
| 1109 | callback=_process_variable_arg, |
Marc-Antoine Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 1110 | default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')], |
| 1111 | dest='extra_variables', |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1112 | metavar='FOO BAR', |
Marc-Antoine Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 1113 | 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.org | b253fb8 | 2012-10-16 21:44:48 +0000 | [diff] [blame] | 1115 | |
| 1116 | |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 1117 | def add_isolate_options(parser): |
| 1118 | """Adds --isolate, --isolated, --out and --<foo>-variable options.""" |
Marc-Antoine Ruel | 1f8ba35 | 2014-11-04 15:55:03 -0500 | [diff] [blame] | 1119 | isolateserver.add_archive_options(parser) |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 1120 | 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') |
kjlubick | 80596f0 | 2017-04-28 08:13:19 -0700 | [diff] [blame] | 1132 | group.add_option( |
| 1133 | '-L', '--collapse_symlinks', action='store_true', |
| 1134 | help='Treat any symlinks as if they were the normal underlying file') |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 1135 | parser.add_option_group(group) |
| 1136 | |
| 1137 | |
Marc-Antoine Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 1138 | def 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 Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 1147 | def 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 Ruel | f9538ee | 2014-01-30 10:43:54 -0500 | [diff] [blame] | 1153 | |
Marc-Antoine Ruel | e236b5c | 2014-09-08 18:40:40 -0400 | [diff] [blame] | 1154 | def 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 | |
| 1166 | def 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 Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 1179 | def 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.org | 0cd0b18 | 2012-10-22 13:34:15 +0000 | [diff] [blame] | 1188 | if options.isolated: |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 1189 | options.isolated = os.path.abspath( |
| 1190 | os.path.join(cwd, unicode(options.isolated).replace('/', os.path.sep))) |
maruel@chromium.org | 0cd0b18 | 2012-10-22 13:34:15 +0000 | [diff] [blame] | 1191 | if require_isolated and not options.isolated: |
maruel@chromium.org | 75c05b4 | 2013-07-25 15:51:48 +0000 | [diff] [blame] | 1192 | parser.error('--isolated is required.') |
maruel@chromium.org | 0cd0b18 | 2012-10-22 13:34:15 +0000 | [diff] [blame] | 1193 | if options.isolated and not options.isolated.endswith('.isolated'): |
| 1194 | parser.error('--isolated value must end with \'.isolated\'') |
maruel@chromium.org | 715e3fb | 2013-03-19 15:44:06 +0000 | [diff] [blame] | 1195 | |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 1196 | # Processes all the --<foo>-variable flags. |
benrg@chromium.org | 609b798 | 2013-02-07 16:44:46 +0000 | [diff] [blame] | 1197 | def try_make_int(s): |
maruel@chromium.org | e83215b | 2013-02-21 14:16:59 +0000 | [diff] [blame] | 1198 | """Converts a value to int if possible, converts to unicode otherwise.""" |
benrg@chromium.org | 609b798 | 2013-02-07 16:44:46 +0000 | [diff] [blame] | 1199 | try: |
| 1200 | return int(s) |
| 1201 | except ValueError: |
maruel@chromium.org | e83215b | 2013-02-21 14:16:59 +0000 | [diff] [blame] | 1202 | return s.decode('utf-8') |
Marc-Antoine Ruel | 1c1edd6 | 2013-12-06 09:13:13 -0500 | [diff] [blame] | 1203 | 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 Ruel | f3589b1 | 2013-12-06 14:26:16 -0500 | [diff] [blame] | 1206 | options.extra_variables = dict(options.extra_variables) |
maruel@chromium.org | b253fb8 | 2012-10-16 21:44:48 +0000 | [diff] [blame] | 1207 | |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 1208 | # 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) |
maruel | 12e3001 | 2015-10-09 11:55:35 -0700 | [diff] [blame] | 1213 | options.isolate = os.path.abspath(os.path.join(cwd, options.isolate)) |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 1214 | options.isolate = file_path.get_native_path_case(options.isolate) |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1215 | |
| 1216 | |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1217 | def main(argv): |
maruel@chromium.org | e532251 | 2013-08-19 20:17:57 +0000 | [diff] [blame] | 1218 | dispatcher = subcommand.CommandDispatcher(__name__) |
Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame] | 1219 | parser = logging_utils.OptionParserWithLogging( |
Vadim Shtayura | fddb143 | 2014-09-30 18:32:41 -0700 | [diff] [blame] | 1220 | version=__version__, verbose=int(os.environ.get('ISOLATE_DEBUG', 0))) |
Marc-Antoine Ruel | 2ca67aa | 2015-01-23 21:37:53 -0500 | [diff] [blame] | 1221 | 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.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1229 | |
| 1230 | |
| 1231 | if __name__ == '__main__': |
maruel | 8e4e40c | 2016-05-30 06:21:07 -0700 | [diff] [blame] | 1232 | subprocess42.inhibit_os_error_reporting() |
maruel@chromium.org | e532251 | 2013-08-19 20:17:57 +0000 | [diff] [blame] | 1233 | fix_encoding.fix_encoding() |
vadimsh@chromium.org | a432647 | 2013-08-24 02:05:41 +0000 | [diff] [blame] | 1234 | tools.disable_buffering() |
maruel@chromium.org | e532251 | 2013-08-19 20:17:57 +0000 | [diff] [blame] | 1235 | colorama.init() |
maruel@chromium.org | 8fb47fe | 2012-10-03 20:13:15 +0000 | [diff] [blame] | 1236 | sys.exit(main(sys.argv[1:])) |