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.
30 # Import here directly so isolate is easier to use as a library.
31 from run_isolated import get_flavor
33 from third_party import colorama
34 from third_party.depot_tools import fix_encoding
35 from third_party.depot_tools import subcommand
37 from utils import file_path
38 from utils import tools
44 class ExecutionError(Exception):
45 """A generic error occurred."""
50 ### Path handling code.
53 def expand_directories_and_symlinks(indir, infiles, blacklist,
54 follow_symlinks, ignore_broken_items):
55 """Expands the directories and the symlinks, applies the blacklist and
58 Files are specified in os native path separator.
61 for relfile in infiles:
64 isolateserver.expand_directory_and_symlink(
65 indir, relfile, blacklist, follow_symlinks))
66 except isolateserver.MappingError as e:
67 if ignore_broken_items:
68 logging.info('warning: %s', e)
74 def recreate_tree(outdir, indir, infiles, action, as_hash):
75 """Creates a new tree with only the input files in it.
78 outdir: Output directory to create the files in.
79 indir: Root directory the infiles are based in.
80 infiles: dict of files to map from |indir| to |outdir|.
81 action: One of accepted action of run_isolated.link_file().
82 as_hash: Output filename is the hash instead of relfile.
85 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
86 (outdir, indir, len(infiles), action, as_hash))
88 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
89 if not os.path.isdir(outdir):
90 logging.info('Creating %s' % outdir)
93 for relfile, metadata in infiles.iteritems():
94 infile = os.path.join(indir, relfile)
96 # Do the hashtable specific checks.
98 # Skip links when storing a hashtable.
100 outfile = os.path.join(outdir, metadata['h'])
101 if os.path.isfile(outfile):
102 # Just do a quick check that the file size matches. No need to stat()
103 # again the input file, grab the value from the dict.
104 if not 's' in metadata:
105 raise isolateserver.MappingError(
106 'Misconfigured item %s: %s' % (relfile, metadata))
107 if metadata['s'] == os.stat(outfile).st_size:
110 logging.warn('Overwritting %s' % metadata['h'])
113 outfile = os.path.join(outdir, relfile)
114 outsubdir = os.path.dirname(outfile)
115 if not os.path.isdir(outsubdir):
116 os.makedirs(outsubdir)
118 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
119 # if metadata.get('T') == True:
120 # open(outfile, 'ab').close()
122 pointed = metadata['l']
123 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
124 # symlink doesn't exist on Windows.
125 os.symlink(pointed, outfile) # pylint: disable=E1101
127 run_isolated.link_file(outfile, infile, action)
133 def _normalize_path_variable(cwd, relative_base_dir, key, value):
134 """Normalizes a path variable into a relative directory.
136 # Variables could contain / or \ on windows. Always normalize to
138 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
139 normalized = file_path.get_native_path_case(os.path.normpath(x))
140 if not os.path.isdir(normalized):
141 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
143 # All variables are relative to the .isolate file.
144 normalized = os.path.relpath(normalized, relative_base_dir)
146 'Translated variable %s from %s to %s', key, value, normalized)
150 def normalize_path_variables(cwd, path_variables, relative_base_dir):
151 """Processes path variables as a special case and returns a copy of the dict.
153 For each 'path' variable: first normalizes it based on |cwd|, verifies it
154 exists then sets it as relative to relative_base_dir.
157 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
159 assert isinstance(cwd, unicode), cwd
160 assert isinstance(relative_base_dir, unicode), relative_base_dir
161 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
163 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
164 for k, v in path_variables.iteritems())
167 ### Internal state files.
170 def isolatedfile_to_state(filename):
171 """For a '.isolate' file, returns the path to the saved '.state' file."""
172 return filename + '.state'
175 def classify_files(root_dir, tracked, untracked):
176 """Converts the list of files into a .isolate 'variables' dictionary.
179 - tracked: list of files names to generate a dictionary out of that should
181 - untracked: list of files names that must not be tracked.
183 # These directories are not guaranteed to be always present on every builder.
184 CHROMIUM_OPTIONAL_DIRECTORIES = (
186 'third_party/WebKit/LayoutTests',
190 new_untracked = list(untracked)
192 def should_be_tracked(filepath):
193 """Returns True if it is a file without whitespace in a non-optional
194 directory that has no symlink in its path.
196 if filepath.endswith('/'):
200 if any(i in filepath for i in CHROMIUM_OPTIONAL_DIRECTORIES):
202 # Look if any element in the path is a symlink.
203 split = filepath.split('/')
204 for i in range(len(split)):
205 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
209 for filepath in sorted(tracked):
210 if should_be_tracked(filepath):
211 new_tracked.append(filepath)
214 new_untracked.append(filepath)
218 variables[isolate_format.KEY_TRACKED] = sorted(new_tracked)
220 variables[isolate_format.KEY_UNTRACKED] = sorted(new_untracked)
224 def chromium_fix(f, variables):
225 """Fixes an isolate dependency with Chromium-specific fixes."""
226 # Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
228 LOG_FILE = re.compile(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$')
231 # http://crbug.com/160539, on Windows, it's in chrome/.
233 'chrome/Media Cache/',
234 # 'First Run' is not created by the compile, but by the test itself.
235 '<(PRODUCT_DIR)/First Run')
237 # Blacklist logs and other unimportant files.
238 if LOG_FILE.match(f) or f in IGNORED_ITEMS:
239 logging.debug('Ignoring %s', f)
242 EXECUTABLE = re.compile(
243 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
244 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
246 match = EXECUTABLE.match(f)
248 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
250 if sys.platform == 'darwin':
251 # On OSX, the name of the output is dependent on gyp define, it can be
252 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
253 # Framework.framework'. Furthermore, they are versioned with a gyp
254 # variable. To lower the complexity of the .isolate file, remove all the
255 # individual entries that show up under any of the 4 entries and replace
256 # them with the directory itself. Overall, this results in a bit more
257 # files than strictly necessary.
259 '<(PRODUCT_DIR)/Chromium Framework.framework/',
260 '<(PRODUCT_DIR)/Chromium.app/',
261 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
262 '<(PRODUCT_DIR)/Google Chrome.app/',
264 for prefix in OSX_BUNDLES:
265 if f.startswith(prefix):
266 # Note this result in duplicate values, so the a set() must be used to
272 def generate_simplified(
273 tracked, untracked, touched, root_dir, path_variables, extra_variables,
274 relative_cwd, trace_blacklist):
275 """Generates a clean and complete .isolate 'variables' dictionary.
277 Cleans up and extracts only files from within root_dir then processes
278 variables and relative_cwd.
280 root_dir = os.path.realpath(root_dir)
282 'generate_simplified(%d files, %s, %s, %s, %s)' %
283 (len(tracked) + len(untracked) + len(touched),
284 root_dir, path_variables, extra_variables, relative_cwd))
287 relative_cwd = file_path.cleanup_path(relative_cwd)
288 assert not os.path.isabs(relative_cwd), relative_cwd
290 # Normalizes to posix path. .isolate files are using posix paths on all OSes
292 path_variables = dict(
293 (k, v.replace(os.path.sep, '/')) for k, v in path_variables.iteritems())
294 # Contains normalized path_variables plus extra_variables.
295 total_variables = path_variables.copy()
296 total_variables.update(extra_variables)
298 # Actual work: Process the files.
299 # TODO(maruel): if all the files in a directory are in part tracked and in
300 # part untracked, the directory will not be extracted. Tracked files should be
301 # 'promoted' to be untracked as needed.
302 tracked = trace_inputs.extract_directories(
303 root_dir, tracked, trace_blacklist)
304 untracked = trace_inputs.extract_directories(
305 root_dir, untracked, trace_blacklist)
306 # touched is not compressed, otherwise it would result in files to be archived
307 # that we don't need.
309 root_dir_posix = root_dir.replace(os.path.sep, '/')
311 """Bases the file on the most restrictive variable."""
312 # Important, GYP stores the files with / and not \.
313 f = f.replace(os.path.sep, '/')
314 logging.debug('fix(%s)' % f)
315 # If it's not already a variable.
316 if not f.startswith('<'):
317 # relative_cwd is usually the directory containing the gyp file. It may be
318 # empty if the whole directory containing the gyp file is needed.
319 # Use absolute paths in case cwd_dir is outside of root_dir.
320 # Convert the whole thing to / since it's isolate's speak.
321 f = file_path.posix_relpath(
322 posixpath.join(root_dir_posix, f),
323 posixpath.join(root_dir_posix, relative_cwd)) or './'
325 # Use the longest value first.
326 for key, value in sorted(
327 path_variables.iteritems(), key=lambda x: -len(x[1])):
328 if f.startswith(value):
329 f = '<(%s)%s' % (key, f[len(value):])
330 logging.debug('Converted to %s' % f)
335 """Reduces the items to convert variables, removes unneeded items, apply
336 chromium-specific fixes and only return unique items.
338 variables_converted = (fix(f.path) for f in items)
340 chromium_fix(f, total_variables) for f in variables_converted)
341 return set(f for f in chromium_fixed if f)
343 tracked = fix_all(tracked)
344 untracked = fix_all(untracked)
345 touched = fix_all(touched)
346 out = classify_files(root_dir, tracked, untracked)
348 out[isolate_format.KEY_TOUCHED] = sorted(touched)
352 def generate_isolate(
353 tracked, untracked, touched, root_dir, path_variables, config_variables,
354 extra_variables, relative_cwd, trace_blacklist):
355 """Generates a clean and complete .isolate file."""
356 dependencies = generate_simplified(
357 tracked, untracked, touched, root_dir, path_variables, extra_variables,
358 relative_cwd, trace_blacklist)
359 config_variable_names, config_values = zip(
360 *sorted(config_variables.iteritems()))
361 out = isolate_format.Configs(None, config_variable_names)
362 # TODO(maruel): Create a public interface in Configs to add a ConfigSettings.
363 # pylint: disable=W0212
364 out._by_config[config_values] = isolate_format.ConfigSettings(dependencies)
365 return out.make_isolate_file()
368 def chromium_save_isolated(isolated, data, path_variables, algo):
369 """Writes one or many .isolated files.
371 This slightly increases the cold cache cost but greatly reduce the warm cache
372 cost by splitting low-churn files off the master .isolated file. It also
373 reduces overall isolateserver memcache consumption.
377 def extract_into_included_isolated(prefix):
379 'algo': data['algo'],
382 'version': data['version'],
384 for f in data['files'].keys():
385 if f.startswith(prefix):
386 new_slave['files'][f] = data['files'].pop(f)
387 if new_slave['files']:
388 slaves.append(new_slave)
390 # Split test/data/ in its own .isolated file.
391 extract_into_included_isolated(os.path.join('test', 'data', ''))
393 # Split everything out of PRODUCT_DIR in its own .isolated file.
394 if path_variables.get('PRODUCT_DIR'):
395 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
398 for index, f in enumerate(slaves):
399 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
400 tools.write_json(slavepath, f, True)
401 data.setdefault('includes', []).append(
402 isolateserver.hash_file(slavepath, algo))
403 files.append(os.path.basename(slavepath))
405 files.extend(isolateserver.save_isolated(isolated, data))
409 class Flattenable(object):
410 """Represents data that can be represented as a json file."""
414 """Returns a json-serializable version of itself.
418 items = ((member, getattr(self, member)) for member in self.MEMBERS)
419 return dict((member, value) for member, value in items if value is not None)
422 def load(cls, data, *args, **kwargs):
423 """Loads a flattened version."""
425 out = cls(*args, **kwargs)
426 for member in out.MEMBERS:
428 # Access to a protected member XXX of a client class
429 # pylint: disable=W0212
430 out._load_member(member, data.pop(member))
433 'Found unexpected entry %s while constructing an object %s' %
434 (data, cls.__name__), data, cls.__name__)
437 def _load_member(self, member, value):
438 """Loads a member into self."""
439 setattr(self, member, value)
442 def load_file(cls, filename, *args, **kwargs):
443 """Loads the data from a file or return an empty instance."""
445 out = cls.load(tools.read_json(filename), *args, **kwargs)
446 logging.debug('Loaded %s(%s)', cls.__name__, filename)
447 except (IOError, ValueError) as e:
448 # On failure, loads the default instance.
449 out = cls(*args, **kwargs)
450 logging.warn('Failed to load %s: %s', filename, e)
454 class SavedState(Flattenable):
455 """Describes the content of a .state file.
457 This file caches the items calculated by this script and is used to increase
458 the performance of the script. This file is not loaded by run_isolated.py.
459 This file can always be safely removed.
461 It is important to note that the 'files' dict keys are using native OS path
462 separator instead of '/' used in .isolate file.
465 # Algorithm used to generate the hash. The only supported value is at the
466 # time of writting 'sha-1'.
468 # Cache of the processed command. This value is saved because .isolated
469 # files are never loaded by isolate.py so it's the only way to load the
472 # GYP variables that are used to generate conditions. The most frequent
475 # GYP variables that will be replaced in 'command' and paths but will not be
476 # considered a relative directory.
478 # Cache of the files found so the next run can skip hash calculation.
480 # Path of the original .isolate file. Relative path to isolated_basedir.
482 # List of included .isolated files. Used to support/remember 'slave'
483 # .isolated files. Relative path to isolated_basedir.
484 'child_isolated_files',
485 # If the generated directory tree should be read-only.
487 # Relative cwd to use to start the command.
489 # GYP variables used to generate the .isolated files paths based on path
490 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
492 # Version of the file format in format 'major.minor'. Any non-breaking
493 # change must update minor. Any breaking change must update major.
497 def __init__(self, isolated_basedir):
498 """Creates an empty SavedState.
500 |isolated_basedir| is the directory where the .isolated and .isolated.state
503 super(SavedState, self).__init__()
504 assert os.path.isabs(isolated_basedir), isolated_basedir
505 assert os.path.isdir(isolated_basedir), isolated_basedir
506 self.isolated_basedir = isolated_basedir
508 # The default algorithm used.
509 self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
510 self.child_isolated_files = []
512 self.config_variables = {}
513 self.extra_variables = {}
515 self.isolate_file = None
516 self.path_variables = {}
517 self.read_only = None
518 self.relative_cwd = None
519 self.version = isolateserver.ISOLATED_FILE_VERSION
522 self, isolate_file, path_variables, config_variables, extra_variables):
523 """Updates the saved state with new data to keep GYP variables and internal
524 reference to the original .isolate file.
526 assert os.path.isabs(isolate_file)
527 # Convert back to a relative path. On Windows, if the isolate and
528 # isolated files are on different drives, isolate_file will stay an absolute
530 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
532 # The same .isolate file should always be used to generate the .isolated and
534 assert isolate_file == self.isolate_file or not self.isolate_file, (
535 isolate_file, self.isolate_file)
536 self.config_variables.update(config_variables)
537 self.extra_variables.update(extra_variables)
538 self.isolate_file = isolate_file
539 self.path_variables.update(path_variables)
541 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
542 """Updates the saved state with data necessary to generate a .isolated file.
544 The new files in |infiles| are added to self.files dict but their hash is
547 self.command = command
550 self.files.setdefault(f, {})
552 self.files.setdefault(f, {})['T'] = True
553 # Prune extraneous files that are not a dependency anymore.
554 for f in set(self.files).difference(set(infiles).union(touched)):
556 if read_only is not None:
557 self.read_only = read_only
558 self.relative_cwd = relative_cwd
560 def to_isolated(self):
561 """Creates a .isolated dictionary out of the saved state.
563 https://code.google.com/p/swarming/wiki/IsolatedDesign
566 """Returns a 'files' entry with only the whitelisted keys."""
567 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
570 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
572 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
573 'version': self.version,
575 if self.config_variables.get('OS'):
576 out['os'] = self.config_variables['OS']
578 out['command'] = self.command
579 if self.read_only is not None:
580 out['read_only'] = self.read_only
581 if self.relative_cwd:
582 out['relative_cwd'] = self.relative_cwd
586 def isolate_filepath(self):
587 """Returns the absolute path of self.isolate_file."""
588 return os.path.normpath(
589 os.path.join(self.isolated_basedir, self.isolate_file))
591 # Arguments number differs from overridden method
593 def load(cls, data, isolated_basedir): # pylint: disable=W0221
594 """Special case loading to disallow different OS.
596 It is not possible to load a .isolated.state files from a different OS, this
597 file is saved in OS-specific format.
599 out = super(SavedState, cls).load(data, isolated_basedir)
601 out.config_variables['OS'] = data['os']
603 # Converts human readable form back into the proper class type.
604 algo = data.get('algo', 'sha-1')
605 if not algo in isolateserver.SUPPORTED_ALGOS:
606 raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
607 out.algo = isolateserver.SUPPORTED_ALGOS[algo]
609 # Refuse the load non-exact version, even minor difference. This is unlike
610 # isolateserver.load_isolated(). This is because .isolated.state could have
611 # changed significantly even in minor version difference.
612 if not re.match(r'^(\d+)\.(\d+)$', out.version):
613 raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
614 if out.version != isolateserver.ISOLATED_FILE_VERSION:
615 raise isolateserver.ConfigError(
616 'Unsupported version \'%s\'' % out.version)
618 # The .isolate file must be valid. It could be absolute on Windows if the
619 # drive containing the .isolate and the drive containing the .isolated files
621 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
622 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
626 """Makes sure 'algo' is in human readable form."""
627 out = super(SavedState, self).flatten()
628 out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
633 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
635 out = '%s(\n' % self.__class__.__name__
636 out += ' command: %s\n' % self.command
637 out += ' files: %d\n' % len(self.files)
638 out += ' isolate_file: %s\n' % self.isolate_file
639 out += ' read_only: %s\n' % self.read_only
640 out += ' relative_cwd: %s\n' % self.relative_cwd
641 out += ' child_isolated_files: %s\n' % self.child_isolated_files
642 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
643 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
644 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
648 class CompleteState(object):
649 """Contains all the state to run the task at hand."""
650 def __init__(self, isolated_filepath, saved_state):
651 super(CompleteState, self).__init__()
652 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
653 self.isolated_filepath = isolated_filepath
654 # Contains the data to ease developer's use-case but that is not strictly
656 self.saved_state = saved_state
659 def load_files(cls, isolated_filepath):
660 """Loads state from disk."""
661 assert os.path.isabs(isolated_filepath), isolated_filepath
662 isolated_basedir = os.path.dirname(isolated_filepath)
665 SavedState.load_file(
666 isolatedfile_to_state(isolated_filepath), isolated_basedir))
669 self, cwd, isolate_file, path_variables, config_variables,
670 extra_variables, ignore_broken_items):
671 """Updates self.isolated and self.saved_state with information loaded from a
674 Processes the loaded data, deduce root_dir, relative_cwd.
676 # Make sure to not depend on os.getcwd().
677 assert os.path.isabs(isolate_file), isolate_file
678 isolate_file = file_path.get_native_path_case(isolate_file)
680 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
681 cwd, isolate_file, path_variables, config_variables, extra_variables,
683 relative_base_dir = os.path.dirname(isolate_file)
685 # Processes the variables.
686 path_variables = normalize_path_variables(
687 cwd, path_variables, relative_base_dir)
688 # Update the saved state.
689 self.saved_state.update(
690 isolate_file, path_variables, config_variables, extra_variables)
691 path_variables = self.saved_state.path_variables
693 with open(isolate_file, 'r') as f:
694 # At that point, variables are not replaced yet in command and infiles.
695 # infiles may contain directory entries and is in posix style.
696 command, infiles, touched, read_only = (
697 isolate_format.load_isolate_for_config(
698 os.path.dirname(isolate_file), f.read(),
699 self.saved_state.config_variables))
701 total_variables = self.saved_state.path_variables.copy()
702 total_variables.update(self.saved_state.config_variables)
703 total_variables.update(self.saved_state.extra_variables)
705 isolate_format.eval_variables(i, total_variables) for i in command
708 total_variables = self.saved_state.path_variables.copy()
709 total_variables.update(self.saved_state.extra_variables)
711 isolate_format.eval_variables(f, total_variables) for f in infiles
714 isolate_format.eval_variables(f, total_variables) for f in touched
716 # root_dir is automatically determined by the deepest root accessed with the
717 # form '../../foo/bar'. Note that path variables must be taken in account
718 # too, add them as if they were input files.
719 root_dir = isolate_format.determine_root_dir(
720 relative_base_dir, infiles + touched +
721 self.saved_state.path_variables.values())
722 # The relative directory is automatically determined by the relative path
723 # between root_dir and the directory containing the .isolate file,
725 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
726 # Now that we know where the root is, check that the path_variables point
728 for k, v in self.saved_state.path_variables.iteritems():
729 if not file_path.path_starts_with(
730 root_dir, os.path.join(relative_base_dir, v)):
731 raise isolateserver.MappingError(
732 'Path variable %s=%r points outside the inferred root directory %s'
734 # Normalize the files based to root_dir. It is important to keep the
735 # trailing os.path.sep at that step.
738 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
743 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
746 follow_symlinks = config_variables['OS'] != 'win'
747 # Expand the directories by listing each file inside. Up to now, trailing
748 # os.path.sep must be kept. Do not expand 'touched'.
749 infiles = expand_directories_and_symlinks(
752 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
756 # If we ignore broken items then remove any missing touched items.
757 if ignore_broken_items:
758 original_touched_count = len(touched)
759 touched = [touch for touch in touched if os.path.exists(touch)]
761 if len(touched) != original_touched_count:
762 logging.info('Removed %d invalid touched entries',
763 len(touched) - original_touched_count)
765 # Finally, update the new data to be able to generate the foo.isolated file,
766 # the file that is used by run_isolated.py.
767 self.saved_state.update_isolated(
768 command, infiles, touched, read_only, relative_cwd)
771 def process_inputs(self, subdir):
772 """Updates self.saved_state.files with the files' mode and hash.
774 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
777 See isolateserver.process_input() for more information.
779 for infile in sorted(self.saved_state.files):
780 if subdir and not infile.startswith(subdir):
781 self.saved_state.files.pop(infile)
783 filepath = os.path.join(self.root_dir, infile)
784 self.saved_state.files[infile] = isolateserver.process_input(
786 self.saved_state.files[infile],
787 self.saved_state.read_only,
788 self.saved_state.config_variables['OS'],
789 self.saved_state.algo)
791 def save_files(self):
792 """Saves self.saved_state and creates a .isolated file."""
793 logging.debug('Dumping to %s' % self.isolated_filepath)
794 self.saved_state.child_isolated_files = chromium_save_isolated(
795 self.isolated_filepath,
796 self.saved_state.to_isolated(),
797 self.saved_state.path_variables,
798 self.saved_state.algo)
800 i.get('s', 0) for i in self.saved_state.files.itervalues())
802 # TODO(maruel): Stats are missing the .isolated files.
803 logging.debug('Total size: %d bytes' % total_bytes)
804 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
805 logging.debug('Dumping to %s' % saved_state_file)
806 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
810 """Returns the absolute path of the root_dir to reference the .isolate file
813 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
816 if not self.saved_state.isolate_file:
817 raise ExecutionError('Please specify --isolate')
818 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
820 if self.saved_state.relative_cwd == '.':
821 root_dir = isolate_dir
823 if not isolate_dir.endswith(self.saved_state.relative_cwd):
824 raise ExecutionError(
825 ('Make sure the .isolate file is in the directory that will be '
826 'used as the relative directory. It is currently in %s and should '
827 'be in %s') % (isolate_dir, self.saved_state.relative_cwd))
828 # Walk back back to the root directory.
829 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
830 return file_path.get_native_path_case(root_dir)
834 """Returns the absolute path containing the .isolated file.
836 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
839 return os.path.dirname(self.isolated_filepath)
842 def indent(data, indent_length):
844 spacing = ' ' * indent_length
845 return ''.join(spacing + l for l in str(data).splitlines(True))
847 out = '%s(\n' % self.__class__.__name__
848 out += ' root_dir: %s\n' % self.root_dir
849 out += ' saved_state: %s)' % indent(self.saved_state, 2)
853 def load_complete_state(options, cwd, subdir, skip_update):
854 """Loads a CompleteState.
856 This includes data from .isolate and .isolated.state files. Never reads the
860 options: Options instance generated with OptionParserIsolate. For either
861 options.isolate and options.isolated, if the value is set, it is an
863 cwd: base directory to be used when loading the .isolate file.
864 subdir: optional argument to only process file in the subdirectory, relative
865 to CompleteState.root_dir.
866 skip_update: Skip trying to load the .isolate file and processing the
867 dependencies. It is useful when not needed, like when tracing.
869 assert not options.isolate or os.path.isabs(options.isolate)
870 assert not options.isolated or os.path.isabs(options.isolated)
871 cwd = file_path.get_native_path_case(unicode(cwd))
873 # Load the previous state if it was present. Namely, "foo.isolated.state".
874 # Note: this call doesn't load the .isolate file.
875 complete_state = CompleteState.load_files(options.isolated)
877 # Constructs a dummy object that cannot be saved. Useful for temporary
878 # commands like 'run'.
879 complete_state = CompleteState(None, SavedState())
881 if not options.isolate:
882 if not complete_state.saved_state.isolate_file:
884 raise ExecutionError('A .isolate file is required.')
887 isolate = complete_state.saved_state.isolate_filepath
889 isolate = options.isolate
890 if complete_state.saved_state.isolate_file:
891 rel_isolate = file_path.safe_relpath(
892 options.isolate, complete_state.saved_state.isolated_basedir)
893 if rel_isolate != complete_state.saved_state.isolate_file:
894 raise ExecutionError(
895 '%s and %s do not match.' % (
896 options.isolate, complete_state.saved_state.isolate_file))
899 # Then load the .isolate and expands directories.
900 complete_state.load_isolate(
901 cwd, isolate, options.path_variables, options.config_variables,
902 options.extra_variables, options.ignore_broken_items)
904 # Regenerate complete_state.saved_state.files.
906 subdir = unicode(subdir)
907 # This is tricky here. If it is a path, take it from the root_dir. If
908 # it is a variable, it must be keyed from the directory containing the
909 # .isolate file. So translate all variables first.
910 translated_path_variables = dict(
912 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
914 for k, v in complete_state.saved_state.path_variables.iteritems())
915 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
916 subdir = subdir.replace('/', os.path.sep)
919 complete_state.process_inputs(subdir)
920 return complete_state
923 def read_trace_as_isolate_dict(complete_state, trace_blacklist):
924 """Reads a trace and returns the .isolate dictionary.
926 Returns exceptions during the log parsing so it can be re-raised.
928 api = trace_inputs.get_api()
929 logfile = complete_state.isolated_filepath + '.log'
930 if not os.path.isfile(logfile):
931 raise ExecutionError(
932 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
934 data = api.parse_log(logfile, trace_blacklist, None)
935 exceptions = [i['exception'] for i in data if 'exception' in i]
936 results = (i['results'] for i in data if 'results' in i)
937 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
938 files = set(sum((result.existent for result in results_stripped), []))
939 tracked, touched = isolate_format.split_touched(files)
940 value = generate_isolate(
944 complete_state.root_dir,
945 complete_state.saved_state.path_variables,
946 complete_state.saved_state.config_variables,
947 complete_state.saved_state.extra_variables,
948 complete_state.saved_state.relative_cwd,
950 return value, exceptions
951 except trace_inputs.TracingFailure, e:
952 raise ExecutionError(
953 'Reading traces failed for: %s\n%s' %
954 (' '.join(complete_state.saved_state.command), str(e)))
957 def merge(complete_state, trace_blacklist):
958 """Reads a trace and merges it back into the source .isolate file."""
959 value, exceptions = read_trace_as_isolate_dict(
960 complete_state, trace_blacklist)
962 # Now take that data and union it into the original .isolate file.
963 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
964 prev_content = f.read()
965 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
966 prev_config = isolate_format.load_isolate_as_config(
968 isolate_format.eval_content(prev_content),
969 isolate_format.extract_comment(prev_content))
970 new_config = isolate_format.load_isolate_as_config(isolate_dir, value, '')
971 config = isolate_format.union(prev_config, new_config)
972 data = config.make_isolate_file()
973 print('Updating %s' % complete_state.saved_state.isolate_file)
974 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
975 isolate_format.print_all(config.file_comment, data, f)
977 # It got an exception, raise the first one.
984 def get_remap_dir(root_dir, isolated, outdir):
985 """If necessary, creates a directory aside the root directory."""
987 if not os.path.isdir(outdir):
991 if not os.path.isabs(root_dir):
992 root_dir = os.path.join(os.path.dirname(isolated), root_dir)
993 return run_isolated.make_temp_dir(
994 'isolate-%s' % datetime.date.today(), root_dir)
997 def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
998 """Creates a isolated tree usable for test execution.
1000 Returns the current working directory where the isolated command should be
1003 # Forcibly copy when the tree has to be read only. Otherwise the inode is
1004 # modified, and this cause real problems because the user's source tree
1005 # becomes read only. On the other hand, the cost of doing file copy is huge.
1006 if read_only not in (0, None):
1007 action = run_isolated.COPY
1009 action = run_isolated.HARDLINK_WITH_FALLBACK
1017 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
1018 if not os.path.isdir(cwd):
1019 # It can happen when no files are mapped from the directory containing the
1020 # .isolate file. But the directory must exist to be the current working
1023 run_isolated.change_tree_read_only(outdir, read_only)
1030 def add_subdir_flag(parser):
1033 help='Filters to a subdirectory. Its behavior changes depending if it '
1034 'is a relative path as a string or as a path variable. Path '
1035 'variables are always keyed from the directory containing the '
1036 '.isolate file. Anything else is keyed on the root directory.')
1039 def CMDarchive(parser, args):
1040 """Creates a .isolated file and uploads the tree to an isolate server.
1042 All the files listed in the .isolated file are put in the isolate server
1043 cache via isolateserver.py.
1045 add_subdir_flag(parser)
1046 options, args = parser.parse_args(args)
1048 parser.error('Unsupported argument: %s' % args)
1050 with tools.Profiler('GenerateHashtable'):
1053 complete_state = load_complete_state(
1054 options, os.getcwd(), options.subdir, False)
1055 if not options.outdir:
1056 options.outdir = os.path.join(
1057 os.path.dirname(complete_state.isolated_filepath), 'hashtable')
1058 # Make sure that complete_state isn't modified until save_files() is
1059 # called, because any changes made to it here will propagate to the files
1060 # created (which is probably not intended).
1061 complete_state.save_files()
1063 infiles = complete_state.saved_state.files
1064 # Add all the .isolated files.
1068 ] + complete_state.saved_state.child_isolated_files
1069 for item in isolated_files:
1070 item_path = os.path.join(
1071 os.path.dirname(complete_state.isolated_filepath), item)
1072 # Do not use isolateserver.hash_file() here because the file is
1073 # likely smallish (under 500kb) and its file size is needed.
1074 with open(item_path, 'rb') as f:
1076 isolated_hash.append(
1077 complete_state.saved_state.algo(content).hexdigest())
1078 isolated_metadata = {
1079 'h': isolated_hash[-1],
1083 infiles[item_path] = isolated_metadata
1085 logging.info('Creating content addressed object store with %d item',
1088 if file_path.is_url(options.outdir):
1089 isolateserver.upload_tree(
1090 base_url=options.outdir,
1091 indir=complete_state.root_dir,
1093 namespace='default-gzip')
1096 outdir=options.outdir,
1097 indir=complete_state.root_dir,
1099 action=run_isolated.HARDLINK_WITH_FALLBACK,
1101 # TODO(maruel): Make the files read-only?
1103 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
1105 # If the command failed, delete the .isolated file if it exists. This is
1106 # important so no stale swarm job is executed.
1107 if not success and os.path.isfile(options.isolated):
1108 os.remove(options.isolated)
1112 def CMDcheck(parser, args):
1113 """Checks that all the inputs are present and generates .isolated."""
1114 add_subdir_flag(parser)
1115 options, args = parser.parse_args(args)
1117 parser.error('Unsupported argument: %s' % args)
1119 complete_state = load_complete_state(
1120 options, os.getcwd(), options.subdir, False)
1122 # Nothing is done specifically. Just store the result and state.
1123 complete_state.save_files()
1127 CMDhashtable = CMDarchive
1130 def CMDmerge(parser, args):
1131 """Reads and merges the data from the trace back into the original .isolate.
1135 parser.require_isolated = False
1136 add_trace_option(parser)
1137 options, args = parser.parse_args(args)
1139 parser.error('Unsupported argument: %s' % args)
1141 complete_state = load_complete_state(options, os.getcwd(), None, False)
1142 blacklist = tools.gen_blacklist(options.trace_blacklist)
1143 merge(complete_state, blacklist)
1147 def CMDread(parser, args):
1148 """Reads the trace file generated with command 'trace'.
1152 parser.require_isolated = False
1153 add_trace_option(parser)
1155 '--skip-refresh', action='store_true',
1156 help='Skip reading .isolate file and do not refresh the hash of '
1159 '-m', '--merge', action='store_true',
1160 help='merge the results back in the .isolate file instead of printing')
1161 options, args = parser.parse_args(args)
1163 parser.error('Unsupported argument: %s' % args)
1165 complete_state = load_complete_state(
1166 options, os.getcwd(), None, options.skip_refresh)
1167 blacklist = tools.gen_blacklist(options.trace_blacklist)
1168 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
1170 merge(complete_state, blacklist)
1172 isolate_format.pretty_print(value, sys.stdout)
1175 # It got an exception, raise the first one.
1183 def CMDremap(parser, args):
1184 """Creates a directory with all the dependencies mapped into it.
1186 Useful to test manually why a test is failing. The target executable is not
1189 parser.require_isolated = False
1191 '--skip-refresh', action='store_true',
1192 help='Skip reading .isolate file and do not refresh the hash of '
1194 options, args = parser.parse_args(args)
1196 parser.error('Unsupported argument: %s' % args)
1197 if options.outdir and file_path.is_url(options.outdir):
1198 parser.error('Can\'t use url for --outdir with mode remap.')
1200 complete_state = load_complete_state(
1201 options, os.getcwd(), None, options.skip_refresh)
1203 outdir = get_remap_dir(
1204 complete_state.root_dir, options.isolated, options.outdir)
1206 print('Remapping into %s' % outdir)
1207 if len(os.listdir(outdir)):
1208 raise ExecutionError('Can\'t remap in a non-empty directory')
1210 create_isolate_tree(
1211 outdir, complete_state.root_dir, complete_state.saved_state.files,
1212 complete_state.saved_state.relative_cwd,
1213 complete_state.saved_state.read_only)
1214 if complete_state.isolated_filepath:
1215 complete_state.save_files()
1219 def CMDrewrite(parser, args):
1220 """Rewrites a .isolate file into the canonical format."""
1221 parser.require_isolated = False
1222 options, args = parser.parse_args(args)
1224 parser.error('Unsupported argument: %s' % args)
1226 if options.isolated:
1227 # Load the previous state if it was present. Namely, "foo.isolated.state".
1228 complete_state = CompleteState.load_files(options.isolated)
1229 isolate = options.isolate or complete_state.saved_state.isolate_filepath
1231 isolate = options.isolate
1233 parser.error('--isolate is required.')
1235 with open(isolate, 'r') as f:
1237 config = isolate_format.load_isolate_as_config(
1238 os.path.dirname(os.path.abspath(isolate)),
1239 isolate_format.eval_content(content),
1240 isolate_format.extract_comment(content))
1241 data = config.make_isolate_file()
1242 print('Updating %s' % isolate)
1243 with open(isolate, 'wb') as f:
1244 isolate_format.print_all(config.file_comment, data, f)
1248 @subcommand.usage('-- [extra arguments]')
1249 def CMDrun(parser, args):
1250 """Runs the test executable in an isolated (temporary) directory.
1252 All the dependencies are mapped into the temporary directory and the
1253 directory is cleaned up after the target exits. Warning: if --outdir is
1254 specified, it is deleted upon exit.
1256 Argument processing stops at -- and these arguments are appended to the
1257 command line of the target to run. For example, use:
1258 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
1260 parser.require_isolated = False
1262 '--skip-refresh', action='store_true',
1263 help='Skip reading .isolate file and do not refresh the hash of '
1265 options, args = parser.parse_args(args)
1266 if options.outdir and file_path.is_url(options.outdir):
1267 parser.error('Can\'t use url for --outdir with mode run.')
1269 complete_state = load_complete_state(
1270 options, os.getcwd(), None, options.skip_refresh)
1271 cmd = complete_state.saved_state.command + args
1273 raise ExecutionError('No command to run.')
1274 cmd = tools.fix_python_path(cmd)
1277 outdir = get_remap_dir(
1278 complete_state.root_dir, options.isolated, options.outdir)
1279 # TODO(maruel): Use run_isolated.run_tha_test().
1280 cwd = create_isolate_tree(
1281 outdir, complete_state.root_dir, complete_state.saved_state.files,
1282 complete_state.saved_state.relative_cwd,
1283 complete_state.saved_state.read_only)
1284 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1285 result = subprocess.call(cmd, cwd=cwd)
1288 run_isolated.rmtree(options.outdir)
1290 if complete_state.isolated_filepath:
1291 complete_state.save_files()
1295 @subcommand.usage('-- [extra arguments]')
1296 def CMDtrace(parser, args):
1297 """Traces the target using trace_inputs.py.
1299 It runs the executable without remapping it, and traces all the files it and
1300 its child processes access. Then the 'merge' command can be used to generate
1301 an updated .isolate file out of it or the 'read' command to print it out to
1304 Argument processing stops at -- and these arguments are appended to the
1305 command line of the target to run. For example, use:
1306 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
1308 add_trace_option(parser)
1310 '-m', '--merge', action='store_true',
1311 help='After tracing, merge the results back in the .isolate file')
1313 '--skip-refresh', action='store_true',
1314 help='Skip reading .isolate file and do not refresh the hash of '
1316 options, args = parser.parse_args(args)
1318 complete_state = load_complete_state(
1319 options, os.getcwd(), None, options.skip_refresh)
1320 cmd = complete_state.saved_state.command + args
1322 raise ExecutionError('No command to run.')
1323 cmd = tools.fix_python_path(cmd)
1324 cwd = os.path.normpath(os.path.join(
1325 unicode(complete_state.root_dir),
1326 complete_state.saved_state.relative_cwd))
1327 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1328 if not os.path.isfile(cmd[0]):
1329 raise ExecutionError(
1330 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
1331 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1332 api = trace_inputs.get_api()
1333 logfile = complete_state.isolated_filepath + '.log'
1334 api.clean_trace(logfile)
1337 with api.get_tracer(logfile) as tracer:
1338 result, out = tracer.trace(
1343 except trace_inputs.TracingFailure, e:
1344 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1348 'Tracer exited with %d, which means the tests probably failed so the '
1349 'trace is probably incomplete.', result)
1352 complete_state.save_files()
1355 blacklist = tools.gen_blacklist(options.trace_blacklist)
1356 merge(complete_state, blacklist)
1361 def _process_variable_arg(option, opt, _value, parser):
1362 """Called by OptionParser to process a --<foo>-variable argument."""
1363 if not parser.rargs:
1364 raise optparse.OptionValueError(
1365 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
1366 k = parser.rargs.pop(0)
1367 variables = getattr(parser.values, option.dest)
1369 k, v = k.split('=', 1)
1371 if not parser.rargs:
1372 raise optparse.OptionValueError(
1373 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
1374 v = parser.rargs.pop(0)
1375 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
1376 raise optparse.OptionValueError(
1377 'Variable \'%s\' doesn\'t respect format \'%s\'' %
1378 (k, isolate_format.VALID_VARIABLE))
1379 variables.append((k, v.decode('utf-8')))
1382 def add_variable_option(parser):
1383 """Adds --isolated and --<foo>-variable to an OptionParser."""
1387 help='.isolated file to generate or read')
1388 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
1392 help=optparse.SUPPRESS_HELP)
1393 is_win = sys.platform in ('win32', 'cygwin')
1394 # There is really 3 kind of variables:
1395 # - path variables, like DEPTH or PRODUCT_DIR that should be
1396 # replaced opportunistically when tracing tests.
1397 # - extraneous things like EXECUTABE_SUFFIX.
1398 # - configuration variables that are to be used in deducing the matrix to
1400 # - unrelated variables that are used as command flags for example.
1402 '--config-variable',
1404 callback=_process_variable_arg,
1405 default=[('OS', get_flavor())],
1406 dest='config_variables',
1408 help='Config variables are used to determine which conditions should be '
1409 'matched when loading a .isolate file, default: %default. '
1410 'All 3 kinds of variables are persistent accross calls, they are '
1411 'saved inside <.isolated>.state')
1415 callback=_process_variable_arg,
1417 dest='path_variables',
1419 help='Path variables are used to replace file paths when loading a '
1420 '.isolate file, default: %default')
1424 callback=_process_variable_arg,
1425 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1426 dest='extra_variables',
1428 help='Extraneous variables are replaced on the \'command\' entry and on '
1429 'paths in the .isolate file but are not considered relative paths.')
1432 def add_trace_option(parser):
1433 """Adds --trace-blacklist to the parser."""
1435 '--trace-blacklist',
1436 action='append', default=list(isolateserver.DEFAULT_BLACKLIST),
1437 help='List of regexp to use as blacklist filter for files to consider '
1438 'important, not to be confused with --blacklist which blacklists '
1442 def parse_isolated_option(parser, options, cwd, require_isolated):
1443 """Processes --isolated."""
1444 if options.isolated:
1445 options.isolated = os.path.normpath(
1446 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
1447 if require_isolated and not options.isolated:
1448 parser.error('--isolated is required.')
1449 if options.isolated and not options.isolated.endswith('.isolated'):
1450 parser.error('--isolated value must end with \'.isolated\'')
1453 def parse_variable_option(options):
1454 """Processes all the --<foo>-variable flags."""
1455 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
1456 # but it wouldn't be backward compatible.
1457 def try_make_int(s):
1458 """Converts a value to int if possible, converts to unicode otherwise."""
1462 return s.decode('utf-8')
1463 options.config_variables = dict(
1464 (k, try_make_int(v)) for k, v in options.config_variables)
1465 options.path_variables = dict(options.path_variables)
1466 options.extra_variables = dict(options.extra_variables)
1469 class OptionParserIsolate(tools.OptionParserWithLogging):
1470 """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
1472 # Set it to False if it is not required, e.g. it can be passed on but do not
1473 # fail if not given.
1474 require_isolated = True
1476 def __init__(self, **kwargs):
1477 tools.OptionParserWithLogging.__init__(
1479 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1481 group = optparse.OptionGroup(self, "Common options")
1485 help='.isolate file to load the dependency data from')
1486 add_variable_option(group)
1488 '-o', '--outdir', metavar='DIR',
1489 help='Directory used to recreate the tree or store the hash table. '
1490 'Defaults: run|remap: a /tmp subdirectory, others: '
1491 'defaults to the directory containing --isolated')
1493 '--ignore_broken_items', action='store_true',
1494 default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1495 help='Indicates that invalid entries in the isolated file to be '
1496 'only be logged and not stop processing. Defaults to True if '
1497 'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
1498 self.add_option_group(group)
1500 def parse_args(self, *args, **kwargs):
1501 """Makes sure the paths make sense.
1503 On Windows, / and \ are often mixed together in a path.
1505 options, args = tools.OptionParserWithLogging.parse_args(
1506 self, *args, **kwargs)
1507 if not self.allow_interspersed_args and args:
1508 self.error('Unsupported argument: %s' % args)
1510 cwd = file_path.get_native_path_case(unicode(os.getcwd()))
1511 parse_isolated_option(self, options, cwd, self.require_isolated)
1512 parse_variable_option(options)
1515 # TODO(maruel): Work with non-ASCII.
1516 # The path must be in native path case for tracing purposes.
1517 options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1518 options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
1519 options.isolate = file_path.get_native_path_case(options.isolate)
1521 if options.outdir and not file_path.is_url(options.outdir):
1522 options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1523 # outdir doesn't need native path case since tracing is never done from
1525 options.outdir = os.path.normpath(os.path.join(cwd, options.outdir))
1527 return options, args
1531 dispatcher = subcommand.CommandDispatcher(__name__)
1533 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
1534 except Exception as e:
1535 tools.report_error(e)
1539 if __name__ == '__main__':
1540 fix_encoding.fix_encoding()
1541 tools.disable_buffering()
1543 sys.exit(main(sys.argv[1:]))