2 # Copyright 2012 The Swarming Authors. All rights reserved.
3 # Use of this source code is governed under the Apache License, Version 2.0 that
4 # can be found in the LICENSE file.
6 """Front end tool to operate on .isolate files.
8 This includes creating, merging or compiling them to generate a .isolated file.
10 See more information at
11 https://code.google.com/p/swarming/wiki/IsolateDesign
12 https://code.google.com/p/swarming/wiki/IsolateUserGuide
14 # Run ./isolate.py --help for more detailed information.
28 import isolated_format
32 from third_party import colorama
33 from third_party.depot_tools import fix_encoding
34 from third_party.depot_tools import subcommand
36 from utils import file_path
37 from utils import tools
40 class ExecutionError(Exception):
41 """A generic error occurred."""
46 ### Path handling code.
49 def recreate_tree(outdir, indir, infiles, action, as_hash):
50 """Creates a new tree with only the input files in it.
53 outdir: Output directory to create the files in.
54 indir: Root directory the infiles are based in.
55 infiles: dict of files to map from |indir| to |outdir|.
56 action: One of accepted action of run_isolated.link_file().
57 as_hash: Output filename is the hash instead of relfile.
60 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
61 (outdir, indir, len(infiles), action, as_hash))
63 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
64 if not os.path.isdir(outdir):
65 logging.info('Creating %s' % outdir)
68 for relfile, metadata in infiles.iteritems():
69 infile = os.path.join(indir, relfile)
71 # Do the hashtable specific checks.
73 # Skip links when storing a hashtable.
75 outfile = os.path.join(outdir, metadata['h'])
76 if os.path.isfile(outfile):
77 # Just do a quick check that the file size matches. No need to stat()
78 # again the input file, grab the value from the dict.
79 if not 's' in metadata:
80 raise isolated_format.MappingError(
81 'Misconfigured item %s: %s' % (relfile, metadata))
82 if metadata['s'] == os.stat(outfile).st_size:
85 logging.warn('Overwritting %s' % metadata['h'])
88 outfile = os.path.join(outdir, relfile)
89 outsubdir = os.path.dirname(outfile)
90 if not os.path.isdir(outsubdir):
91 os.makedirs(outsubdir)
93 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
94 # if metadata.get('T') == True:
95 # open(outfile, 'ab').close()
97 pointed = metadata['l']
98 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
99 # symlink doesn't exist on Windows.
100 os.symlink(pointed, outfile) # pylint: disable=E1101
102 run_isolated.link_file(outfile, infile, action)
108 def _normalize_path_variable(cwd, relative_base_dir, key, value):
109 """Normalizes a path variable into a relative directory.
111 # Variables could contain / or \ on windows. Always normalize to
113 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
114 normalized = file_path.get_native_path_case(os.path.normpath(x))
115 if not os.path.isdir(normalized):
116 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
118 # All variables are relative to the .isolate file.
119 normalized = os.path.relpath(normalized, relative_base_dir)
121 'Translated variable %s from %s to %s', key, value, normalized)
125 def normalize_path_variables(cwd, path_variables, relative_base_dir):
126 """Processes path variables as a special case and returns a copy of the dict.
128 For each 'path' variable: first normalizes it based on |cwd|, verifies it
129 exists then sets it as relative to relative_base_dir.
132 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
134 assert isinstance(cwd, unicode), cwd
135 assert isinstance(relative_base_dir, unicode), relative_base_dir
136 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
138 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
139 for k, v in path_variables.iteritems())
142 ### Internal state files.
145 def isolatedfile_to_state(filename):
146 """For a '.isolate' file, returns the path to the saved '.state' file."""
147 return filename + '.state'
150 def chromium_save_isolated(isolated, data, path_variables, algo):
151 """Writes one or many .isolated files.
153 This slightly increases the cold cache cost but greatly reduce the warm cache
154 cost by splitting low-churn files off the master .isolated file. It also
155 reduces overall isolateserver memcache consumption.
159 def extract_into_included_isolated(prefix):
161 'algo': data['algo'],
163 'version': data['version'],
165 for f in data['files'].keys():
166 if f.startswith(prefix):
167 new_slave['files'][f] = data['files'].pop(f)
168 if new_slave['files']:
169 slaves.append(new_slave)
171 # Split test/data/ in its own .isolated file.
172 extract_into_included_isolated(os.path.join('test', 'data', ''))
174 # Split everything out of PRODUCT_DIR in its own .isolated file.
175 if path_variables.get('PRODUCT_DIR'):
176 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
179 for index, f in enumerate(slaves):
180 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
181 tools.write_json(slavepath, f, True)
182 data.setdefault('includes', []).append(
183 isolated_format.hash_file(slavepath, algo))
184 files.append(os.path.basename(slavepath))
186 files.extend(isolated_format.save_isolated(isolated, data))
190 class Flattenable(object):
191 """Represents data that can be represented as a json file."""
195 """Returns a json-serializable version of itself.
199 items = ((member, getattr(self, member)) for member in self.MEMBERS)
200 return dict((member, value) for member, value in items if value is not None)
203 def load(cls, data, *args, **kwargs):
204 """Loads a flattened version."""
206 out = cls(*args, **kwargs)
207 for member in out.MEMBERS:
209 # Access to a protected member XXX of a client class
210 # pylint: disable=W0212
211 out._load_member(member, data.pop(member))
214 'Found unexpected entry %s while constructing an object %s' %
215 (data, cls.__name__), data, cls.__name__)
218 def _load_member(self, member, value):
219 """Loads a member into self."""
220 setattr(self, member, value)
223 def load_file(cls, filename, *args, **kwargs):
224 """Loads the data from a file or return an empty instance."""
226 out = cls.load(tools.read_json(filename), *args, **kwargs)
227 logging.debug('Loaded %s(%s)', cls.__name__, filename)
228 except (IOError, ValueError) as e:
229 # On failure, loads the default instance.
230 out = cls(*args, **kwargs)
231 logging.warn('Failed to load %s: %s', filename, e)
235 class SavedState(Flattenable):
236 """Describes the content of a .state file.
238 This file caches the items calculated by this script and is used to increase
239 the performance of the script. This file is not loaded by run_isolated.py.
240 This file can always be safely removed.
242 It is important to note that the 'files' dict keys are using native OS path
243 separator instead of '/' used in .isolate file.
246 # Value of sys.platform so that the file is rejected if loaded from a
247 # different OS. While this should never happen in practice, users are ...
250 # Algorithm used to generate the hash. The only supported value is at the
251 # time of writting 'sha-1'.
253 # List of included .isolated files. Used to support/remember 'slave'
254 # .isolated files. Relative path to isolated_basedir.
255 'child_isolated_files',
256 # Cache of the processed command. This value is saved because .isolated
257 # files are never loaded by isolate.py so it's the only way to load the
260 # GYP variables that are used to generate conditions. The most frequent
263 # GYP variables that will be replaced in 'command' and paths but will not be
264 # considered a relative directory.
266 # Cache of the files found so the next run can skip hash calculation.
268 # Path of the original .isolate file. Relative path to isolated_basedir.
270 # GYP variables used to generate the .isolated files paths based on path
271 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
273 # If the generated directory tree should be read-only.
275 # Relative cwd to use to start the command.
277 # Root directory the files are mapped from.
279 # Version of the saved state file format. Any breaking change must update
284 # Bump this version whenever the saved state changes. It is also keyed on the
285 # .isolated file version so any change in the generator will invalidate .state
287 EXPECTED_VERSION = isolated_format.ISOLATED_FILE_VERSION + '.2'
289 def __init__(self, isolated_basedir):
290 """Creates an empty SavedState.
293 isolated_basedir: the directory where the .isolated and .isolated.state
296 super(SavedState, self).__init__()
297 assert os.path.isabs(isolated_basedir), isolated_basedir
298 assert os.path.isdir(isolated_basedir), isolated_basedir
299 self.isolated_basedir = isolated_basedir
301 # The default algorithm used.
302 self.OS = sys.platform
303 self.algo = isolated_format.SUPPORTED_ALGOS['sha-1']
304 self.child_isolated_files = []
306 self.config_variables = {}
307 self.extra_variables = {}
309 self.isolate_file = None
310 self.path_variables = {}
311 self.read_only = None
312 self.relative_cwd = None
314 self.version = self.EXPECTED_VERSION
316 def update_config(self, config_variables):
317 """Updates the saved state with only config variables."""
318 self.config_variables.update(config_variables)
320 def update(self, isolate_file, path_variables, extra_variables):
321 """Updates the saved state with new data to keep GYP variables and internal
322 reference to the original .isolate file.
324 assert os.path.isabs(isolate_file)
325 # Convert back to a relative path. On Windows, if the isolate and
326 # isolated files are on different drives, isolate_file will stay an absolute
328 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
330 # The same .isolate file should always be used to generate the .isolated and
332 assert isolate_file == self.isolate_file or not self.isolate_file, (
333 isolate_file, self.isolate_file)
334 self.extra_variables.update(extra_variables)
335 self.isolate_file = isolate_file
336 self.path_variables.update(path_variables)
338 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
339 """Updates the saved state with data necessary to generate a .isolated file.
341 The new files in |infiles| are added to self.files dict but their hash is
344 self.command = command
347 self.files.setdefault(f, {})
349 self.files.setdefault(f, {})['T'] = True
350 # Prune extraneous files that are not a dependency anymore.
351 for f in set(self.files).difference(set(infiles).union(touched)):
353 if read_only is not None:
354 self.read_only = read_only
355 self.relative_cwd = relative_cwd
357 def to_isolated(self):
358 """Creates a .isolated dictionary out of the saved state.
360 https://code.google.com/p/swarming/wiki/IsolatedDesign
363 """Returns a 'files' entry with only the whitelisted keys."""
364 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
367 'algo': isolated_format.SUPPORTED_ALGOS_REVERSE[self.algo],
369 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
370 # The version of the .state file is different than the one of the
372 'version': isolated_format.ISOLATED_FILE_VERSION,
375 out['command'] = self.command
376 if self.read_only is not None:
377 out['read_only'] = self.read_only
378 if self.relative_cwd:
379 out['relative_cwd'] = self.relative_cwd
383 def isolate_filepath(self):
384 """Returns the absolute path of self.isolate_file."""
385 return os.path.normpath(
386 os.path.join(self.isolated_basedir, self.isolate_file))
388 # Arguments number differs from overridden method
390 def load(cls, data, isolated_basedir): # pylint: disable=W0221
391 """Special case loading to disallow different OS.
393 It is not possible to load a .isolated.state files from a different OS, this
394 file is saved in OS-specific format.
396 out = super(SavedState, cls).load(data, isolated_basedir)
397 if data.get('OS') != sys.platform:
398 raise isolated_format.IsolatedError('Unexpected OS %s', data.get('OS'))
400 # Converts human readable form back into the proper class type.
401 algo = data.get('algo')
402 if not algo in isolated_format.SUPPORTED_ALGOS:
403 raise isolated_format.IsolatedError('Unknown algo \'%s\'' % out.algo)
404 out.algo = isolated_format.SUPPORTED_ALGOS[algo]
406 # Refuse the load non-exact version, even minor difference. This is unlike
407 # isolateserver.load_isolated(). This is because .isolated.state could have
408 # changed significantly even in minor version difference.
409 if out.version != cls.EXPECTED_VERSION:
410 raise isolated_format.IsolatedError(
411 'Unsupported version \'%s\'' % out.version)
413 # The .isolate file must be valid. If it is not present anymore, zap the
414 # value as if it was not noted, so .isolate_file can safely be overriden
416 if out.isolate_file and not os.path.isfile(out.isolate_filepath):
417 out.isolate_file = None
419 # It could be absolute on Windows if the drive containing the .isolate and
420 # the drive containing the .isolated files differ, .e.g .isolate is on
421 # C:\\ and .isolated is on D:\\ .
422 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
423 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
427 """Makes sure 'algo' is in human readable form."""
428 out = super(SavedState, self).flatten()
429 out['algo'] = isolated_format.SUPPORTED_ALGOS_REVERSE[out['algo']]
434 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
436 out = '%s(\n' % self.__class__.__name__
437 out += ' command: %s\n' % self.command
438 out += ' files: %d\n' % len(self.files)
439 out += ' isolate_file: %s\n' % self.isolate_file
440 out += ' read_only: %s\n' % self.read_only
441 out += ' relative_cwd: %s\n' % self.relative_cwd
442 out += ' child_isolated_files: %s\n' % self.child_isolated_files
443 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
444 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
445 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
449 class CompleteState(object):
450 """Contains all the state to run the task at hand."""
451 def __init__(self, isolated_filepath, saved_state):
452 super(CompleteState, self).__init__()
453 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
454 self.isolated_filepath = isolated_filepath
455 # Contains the data to ease developer's use-case but that is not strictly
457 self.saved_state = saved_state
460 def load_files(cls, isolated_filepath):
461 """Loads state from disk."""
462 assert os.path.isabs(isolated_filepath), isolated_filepath
463 isolated_basedir = os.path.dirname(isolated_filepath)
466 SavedState.load_file(
467 isolatedfile_to_state(isolated_filepath), isolated_basedir))
470 self, cwd, isolate_file, path_variables, config_variables,
471 extra_variables, ignore_broken_items):
472 """Updates self.isolated and self.saved_state with information loaded from a
475 Processes the loaded data, deduce root_dir, relative_cwd.
477 # Make sure to not depend on os.getcwd().
478 assert os.path.isabs(isolate_file), isolate_file
479 isolate_file = file_path.get_native_path_case(isolate_file)
481 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
482 cwd, isolate_file, path_variables, config_variables, extra_variables,
485 # Config variables are not affected by the paths and must be used to
486 # retrieve the paths, so update them first.
487 self.saved_state.update_config(config_variables)
489 with open(isolate_file, 'r') as f:
490 # At that point, variables are not replaced yet in command and infiles.
491 # infiles may contain directory entries and is in posix style.
492 command, infiles, touched, read_only, isolate_cmd_dir = (
493 isolate_format.load_isolate_for_config(
494 os.path.dirname(isolate_file), f.read(),
495 self.saved_state.config_variables))
497 # Processes the variables with the new found relative root. Note that 'cwd'
498 # is used when path variables are used.
499 path_variables = normalize_path_variables(
500 cwd, path_variables, isolate_cmd_dir)
501 # Update the rest of the saved state.
502 self.saved_state.update(isolate_file, path_variables, extra_variables)
504 total_variables = self.saved_state.path_variables.copy()
505 total_variables.update(self.saved_state.config_variables)
506 total_variables.update(self.saved_state.extra_variables)
508 isolate_format.eval_variables(i, total_variables) for i in command
511 total_variables = self.saved_state.path_variables.copy()
512 total_variables.update(self.saved_state.extra_variables)
514 isolate_format.eval_variables(f, total_variables) for f in infiles
517 isolate_format.eval_variables(f, total_variables) for f in touched
519 # root_dir is automatically determined by the deepest root accessed with the
520 # form '../../foo/bar'. Note that path variables must be taken in account
521 # too, add them as if they were input files.
522 self.saved_state.root_dir = isolate_format.determine_root_dir(
523 isolate_cmd_dir, infiles + touched +
524 self.saved_state.path_variables.values())
525 # The relative directory is automatically determined by the relative path
526 # between root_dir and the directory containing the .isolate file,
528 relative_cwd = os.path.relpath(isolate_cmd_dir, self.saved_state.root_dir)
529 # Now that we know where the root is, check that the path_variables point
531 for k, v in self.saved_state.path_variables.iteritems():
532 dest = os.path.join(isolate_cmd_dir, relative_cwd, v)
533 if not file_path.path_starts_with(self.saved_state.root_dir, dest):
534 raise isolated_format.MappingError(
535 'Path variable %s=%r points outside the inferred root directory '
537 % (k, v, self.saved_state.root_dir, dest))
538 # Normalize the files based to self.saved_state.root_dir. It is important to
539 # keep the trailing os.path.sep at that step.
542 file_path.normpath(os.path.join(isolate_cmd_dir, f)),
543 self.saved_state.root_dir)
548 file_path.normpath(os.path.join(isolate_cmd_dir, f)),
549 self.saved_state.root_dir)
552 follow_symlinks = sys.platform != 'win32'
553 # Expand the directories by listing each file inside. Up to now, trailing
554 # os.path.sep must be kept. Do not expand 'touched'.
555 infiles = isolated_format.expand_directories_and_symlinks(
556 self.saved_state.root_dir,
558 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
562 # If we ignore broken items then remove any missing touched items.
563 if ignore_broken_items:
564 original_touched_count = len(touched)
565 touched = [touch for touch in touched if os.path.exists(touch)]
567 if len(touched) != original_touched_count:
568 logging.info('Removed %d invalid touched entries',
569 len(touched) - original_touched_count)
571 # Finally, update the new data to be able to generate the foo.isolated file,
572 # the file that is used by run_isolated.py.
573 self.saved_state.update_isolated(
574 command, infiles, touched, read_only, relative_cwd)
577 def files_to_metadata(self, subdir):
578 """Updates self.saved_state.files with the files' mode and hash.
580 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
583 See isolated_format.file_to_metadata() for more information.
585 for infile in sorted(self.saved_state.files):
586 if subdir and not infile.startswith(subdir):
587 self.saved_state.files.pop(infile)
589 filepath = os.path.join(self.root_dir, infile)
590 self.saved_state.files[infile] = isolated_format.file_to_metadata(
592 self.saved_state.files[infile],
593 self.saved_state.read_only,
594 self.saved_state.algo)
596 def save_files(self):
597 """Saves self.saved_state and creates a .isolated file."""
598 logging.debug('Dumping to %s' % self.isolated_filepath)
599 self.saved_state.child_isolated_files = chromium_save_isolated(
600 self.isolated_filepath,
601 self.saved_state.to_isolated(),
602 self.saved_state.path_variables,
603 self.saved_state.algo)
605 i.get('s', 0) for i in self.saved_state.files.itervalues())
607 # TODO(maruel): Stats are missing the .isolated files.
608 logging.debug('Total size: %d bytes' % total_bytes)
609 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
610 logging.debug('Dumping to %s' % saved_state_file)
611 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
615 return self.saved_state.root_dir
618 def indent(data, indent_length):
620 spacing = ' ' * indent_length
621 return ''.join(spacing + l for l in str(data).splitlines(True))
623 out = '%s(\n' % self.__class__.__name__
624 out += ' root_dir: %s\n' % self.root_dir
625 out += ' saved_state: %s)' % indent(self.saved_state, 2)
629 def load_complete_state(options, cwd, subdir, skip_update):
630 """Loads a CompleteState.
632 This includes data from .isolate and .isolated.state files. Never reads the
636 options: Options instance generated with OptionParserIsolate. For either
637 options.isolate and options.isolated, if the value is set, it is an
639 cwd: base directory to be used when loading the .isolate file.
640 subdir: optional argument to only process file in the subdirectory, relative
641 to CompleteState.root_dir.
642 skip_update: Skip trying to load the .isolate file and processing the
643 dependencies. It is useful when not needed, like when tracing.
645 assert not options.isolate or os.path.isabs(options.isolate)
646 assert not options.isolated or os.path.isabs(options.isolated)
647 cwd = file_path.get_native_path_case(unicode(cwd))
649 # Load the previous state if it was present. Namely, "foo.isolated.state".
650 # Note: this call doesn't load the .isolate file.
651 complete_state = CompleteState.load_files(options.isolated)
653 # Constructs a dummy object that cannot be saved. Useful for temporary
654 # commands like 'run'. There is no directory containing a .isolated file so
655 # specify the current working directory as a valid directory.
656 complete_state = CompleteState(None, SavedState(os.getcwd()))
658 if not options.isolate:
659 if not complete_state.saved_state.isolate_file:
661 raise ExecutionError('A .isolate file is required.')
664 isolate = complete_state.saved_state.isolate_filepath
666 isolate = options.isolate
667 if complete_state.saved_state.isolate_file:
668 rel_isolate = file_path.safe_relpath(
669 options.isolate, complete_state.saved_state.isolated_basedir)
670 if rel_isolate != complete_state.saved_state.isolate_file:
671 # This happens if the .isolate file was moved for example. In this case,
672 # discard the saved state.
674 '--isolated %s != %s as saved in %s. Discarding saved state',
676 complete_state.saved_state.isolate_file,
677 isolatedfile_to_state(options.isolated))
678 complete_state = CompleteState(
680 SavedState(complete_state.saved_state.isolated_basedir))
683 # Then load the .isolate and expands directories.
684 complete_state.load_isolate(
685 cwd, isolate, options.path_variables, options.config_variables,
686 options.extra_variables, options.ignore_broken_items)
688 # Regenerate complete_state.saved_state.files.
690 subdir = unicode(subdir)
691 # This is tricky here. If it is a path, take it from the root_dir. If
692 # it is a variable, it must be keyed from the directory containing the
693 # .isolate file. So translate all variables first.
694 translated_path_variables = dict(
696 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
698 for k, v in complete_state.saved_state.path_variables.iteritems())
699 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
700 subdir = subdir.replace('/', os.path.sep)
703 complete_state.files_to_metadata(subdir)
704 return complete_state
707 def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
708 """Creates a isolated tree usable for test execution.
710 Returns the current working directory where the isolated command should be
713 # Forcibly copy when the tree has to be read only. Otherwise the inode is
714 # modified, and this cause real problems because the user's source tree
715 # becomes read only. On the other hand, the cost of doing file copy is huge.
716 if read_only not in (0, None):
717 action = run_isolated.COPY
719 action = run_isolated.HARDLINK_WITH_FALLBACK
727 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
728 if not os.path.isdir(cwd):
729 # It can happen when no files are mapped from the directory containing the
730 # .isolate file. But the directory must exist to be the current working
733 run_isolated.change_tree_read_only(outdir, read_only)
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()
746 infiles = complete_state.saved_state.files
747 # Add all the .isolated files.
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)
755 # Do not use isolated_format.hash_file() here because the file is
756 # likely smallish (under 500kb) and its file size is needed.
757 with open(item_path, 'rb') as f:
759 isolated_hash.append(
760 complete_state.saved_state.algo(content).hexdigest())
761 isolated_metadata = {
762 'h': isolated_hash[-1],
766 infiles[item_path] = isolated_metadata
767 return complete_state, infiles, isolated_hash
773 def CMDarchive(parser, args):
774 """Creates a .isolated file and uploads the tree to an isolate server.
776 All the files listed in the .isolated file are put in the isolate server
777 cache via isolateserver.py.
779 add_subdir_option(parser)
780 isolateserver.add_isolate_server_options(parser, False)
781 auth.add_auth_options(parser)
782 options, args = parser.parse_args(args)
783 auth.process_auth_options(parser, options)
784 isolateserver.process_isolate_server_options(parser, options)
786 parser.error('Unsupported argument: %s' % args)
787 if file_path.is_url(options.isolate_server):
788 auth.ensure_logged_in(options.isolate_server)
790 with tools.Profiler('GenerateHashtable'):
793 complete_state, infiles, isolated_hash = prepare_for_archival(
795 logging.info('Creating content addressed object store with %d item',
798 isolateserver.upload_tree(
799 base_url=options.isolate_server,
800 indir=complete_state.root_dir,
802 namespace=options.namespace)
804 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
806 # If the command failed, delete the .isolated file if it exists. This is
807 # important so no stale swarm job is executed.
808 if not success and os.path.isfile(options.isolated):
809 os.remove(options.isolated)
810 return int(not success)
813 def CMDcheck(parser, args):
814 """Checks that all the inputs are present and generates .isolated."""
815 add_subdir_option(parser)
816 options, args = parser.parse_args(args)
818 parser.error('Unsupported argument: %s' % args)
820 complete_state = load_complete_state(
821 options, os.getcwd(), options.subdir, False)
823 # Nothing is done specifically. Just store the result and state.
824 complete_state.save_files()
828 def CMDremap(parser, args):
829 """Creates a directory with all the dependencies mapped into it.
831 Useful to test manually why a test is failing. The target executable is not
834 parser.require_isolated = False
835 add_outdir_options(parser)
836 add_skip_refresh_option(parser)
837 options, args = parser.parse_args(args)
839 parser.error('Unsupported argument: %s' % args)
841 process_outdir_options(parser, options, cwd)
842 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
844 if not os.path.isdir(options.outdir):
845 os.makedirs(options.outdir)
846 print('Remapping into %s' % options.outdir)
847 if os.listdir(options.outdir):
848 raise ExecutionError('Can\'t remap in a non-empty directory')
851 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
852 complete_state.saved_state.relative_cwd,
853 complete_state.saved_state.read_only)
854 if complete_state.isolated_filepath:
855 complete_state.save_files()
859 def CMDrewrite(parser, args):
860 """Rewrites a .isolate file into the canonical format."""
861 parser.require_isolated = False
862 options, args = parser.parse_args(args)
864 parser.error('Unsupported argument: %s' % args)
867 # Load the previous state if it was present. Namely, "foo.isolated.state".
868 complete_state = CompleteState.load_files(options.isolated)
869 isolate = options.isolate or complete_state.saved_state.isolate_filepath
871 isolate = options.isolate
873 parser.error('--isolate is required.')
875 with open(isolate, 'r') as f:
877 config = isolate_format.load_isolate_as_config(
878 os.path.dirname(os.path.abspath(isolate)),
879 isolate_format.eval_content(content),
880 isolate_format.extract_comment(content))
881 data = config.make_isolate_file()
882 print('Updating %s' % isolate)
883 with open(isolate, 'wb') as f:
884 isolate_format.print_all(config.file_comment, data, f)
888 @subcommand.usage('-- [extra arguments]')
889 def CMDrun(parser, args):
890 """Runs the test executable in an isolated (temporary) directory.
892 All the dependencies are mapped into the temporary directory and the
893 directory is cleaned up after the target exits.
895 Argument processing stops at -- and these arguments are appended to the
896 command line of the target to run. For example, use:
897 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
899 parser.require_isolated = False
900 add_skip_refresh_option(parser)
901 options, args = parser.parse_args(args)
903 complete_state = load_complete_state(
904 options, os.getcwd(), None, options.skip_refresh)
905 cmd = complete_state.saved_state.command + args
907 raise ExecutionError('No command to run.')
908 cmd = tools.fix_python_path(cmd)
910 outdir = run_isolated.make_temp_dir(
911 'isolate-%s' % datetime.date.today(),
912 os.path.dirname(complete_state.root_dir))
914 # TODO(maruel): Use run_isolated.run_tha_test().
915 cwd = create_isolate_tree(
916 outdir, complete_state.root_dir, complete_state.saved_state.files,
917 complete_state.saved_state.relative_cwd,
918 complete_state.saved_state.read_only)
919 file_path.ensure_command_has_abs_path(cmd, cwd)
920 logging.info('Running %s, cwd=%s' % (cmd, cwd))
922 result = subprocess.call(cmd, cwd=cwd)
925 'Failed to executed the command; executable is missing, maybe you\n'
926 'forgot to map it in the .isolate file?\n %s\n in %s\n' %
927 (' '.join(cmd), cwd))
930 run_isolated.rmtree(outdir)
932 if complete_state.isolated_filepath:
933 complete_state.save_files()
937 def _process_variable_arg(option, opt, _value, parser):
938 """Called by OptionParser to process a --<foo>-variable argument."""
940 raise optparse.OptionValueError(
941 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
942 k = parser.rargs.pop(0)
943 variables = getattr(parser.values, option.dest)
945 k, v = k.split('=', 1)
948 raise optparse.OptionValueError(
949 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
950 v = parser.rargs.pop(0)
951 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
952 raise optparse.OptionValueError(
953 'Variable \'%s\' doesn\'t respect format \'%s\'' %
954 (k, isolate_format.VALID_VARIABLE))
955 variables.append((k, v.decode('utf-8')))
958 def add_variable_option(parser):
959 """Adds --isolated and --<foo>-variable to an OptionParser."""
963 help='.isolated file to generate or read')
964 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
968 help=optparse.SUPPRESS_HELP)
969 is_win = sys.platform in ('win32', 'cygwin')
970 # There is really 3 kind of variables:
971 # - path variables, like DEPTH or PRODUCT_DIR that should be
972 # replaced opportunistically when tracing tests.
973 # - extraneous things like EXECUTABE_SUFFIX.
974 # - configuration variables that are to be used in deducing the matrix to
976 # - unrelated variables that are used as command flags for example.
980 callback=_process_variable_arg,
982 dest='config_variables',
984 help='Config variables are used to determine which conditions should be '
985 'matched when loading a .isolate file, default: %default. '
986 'All 3 kinds of variables are persistent accross calls, they are '
987 'saved inside <.isolated>.state')
991 callback=_process_variable_arg,
993 dest='path_variables',
995 help='Path variables are used to replace file paths when loading a '
996 '.isolate file, default: %default')
1000 callback=_process_variable_arg,
1001 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1002 dest='extra_variables',
1004 help='Extraneous variables are replaced on the \'command\' entry and on '
1005 'paths in the .isolate file but are not considered relative paths.')
1008 def add_subdir_option(parser):
1011 help='Filters to a subdirectory. Its behavior changes depending if it '
1012 'is a relative path as a string or as a path variable. Path '
1013 'variables are always keyed from the directory containing the '
1014 '.isolate file. Anything else is keyed on the root directory.')
1017 def add_skip_refresh_option(parser):
1019 '--skip-refresh', action='store_true',
1020 help='Skip reading .isolate file and do not refresh the hash of '
1024 def add_outdir_options(parser):
1025 """Adds --outdir, which is orthogonal to --isolate-server.
1027 Note: On upload, separate commands are used between 'archive' and 'hashtable'.
1028 On 'download', the same command can download from either an isolate server or
1032 '-o', '--outdir', metavar='DIR',
1033 help='Directory used to recreate the tree.')
1036 def process_outdir_options(parser, options, cwd):
1037 if not options.outdir:
1038 parser.error('--outdir is required.')
1039 if file_path.is_url(options.outdir):
1040 parser.error('Can\'t use an URL for --outdir.')
1041 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1042 # outdir doesn't need native path case since tracing is never done from there.
1043 options.outdir = os.path.abspath(
1044 os.path.normpath(os.path.join(cwd, options.outdir)))
1045 # In theory, we'd create the directory outdir right away. Defer doing it in
1046 # case there's errors in the command line.
1049 def parse_isolated_option(parser, options, cwd, require_isolated):
1050 """Processes --isolated."""
1051 if options.isolated:
1052 options.isolated = os.path.normpath(
1053 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
1054 if require_isolated and not options.isolated:
1055 parser.error('--isolated is required.')
1056 if options.isolated and not options.isolated.endswith('.isolated'):
1057 parser.error('--isolated value must end with \'.isolated\'')
1060 def parse_variable_option(options):
1061 """Processes all the --<foo>-variable flags."""
1062 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
1063 # but it wouldn't be backward compatible.
1064 def try_make_int(s):
1065 """Converts a value to int if possible, converts to unicode otherwise."""
1069 return s.decode('utf-8')
1070 options.config_variables = dict(
1071 (k, try_make_int(v)) for k, v in options.config_variables)
1072 options.path_variables = dict(options.path_variables)
1073 options.extra_variables = dict(options.extra_variables)
1076 class OptionParserIsolate(tools.OptionParserWithLogging):
1077 """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
1079 # Set it to False if it is not required, e.g. it can be passed on but do not
1080 # fail if not given.
1081 require_isolated = True
1083 def __init__(self, **kwargs):
1084 tools.OptionParserWithLogging.__init__(
1086 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1088 group = optparse.OptionGroup(self, "Common options")
1092 help='.isolate file to load the dependency data from')
1093 add_variable_option(group)
1095 '--ignore_broken_items', action='store_true',
1096 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1097 help='Indicates that invalid entries in the isolated file to be '
1098 'only be logged and not stop processing. Defaults to True if '
1099 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
1100 self.add_option_group(group)
1102 def parse_args(self, *args, **kwargs):
1103 """Makes sure the paths make sense.
1105 On Windows, / and \ are often mixed together in a path.
1107 options, args = tools.OptionParserWithLogging.parse_args(
1108 self, *args, **kwargs)
1109 if not self.allow_interspersed_args and args:
1110 self.error('Unsupported argument: %s' % args)
1112 cwd = file_path.get_native_path_case(unicode(os.getcwd()))
1113 parse_isolated_option(self, options, cwd, self.require_isolated)
1114 parse_variable_option(options)
1117 # TODO(maruel): Work with non-ASCII.
1118 # The path must be in native path case for tracing purposes.
1119 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1120 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
1121 options.isolate = file_path.get_native_path_case(options.isolate)
1123 return options, args
1127 dispatcher = subcommand.CommandDispatcher(__name__)
1128 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
1131 if __name__ == '__main__':
1132 fix_encoding.fix_encoding()
1133 tools.disable_buffering()
1135 sys.exit(main(sys.argv[1:]))