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.
31 from third_party import colorama
32 from third_party.depot_tools import fix_encoding
33 from third_party.depot_tools import subcommand
35 from utils import file_path
36 from utils import tools
42 class ExecutionError(Exception):
43 """A generic error occurred."""
48 ### Path handling code.
51 def expand_directories_and_symlinks(indir, infiles, blacklist,
52 follow_symlinks, ignore_broken_items):
53 """Expands the directories and the symlinks, applies the blacklist and
56 Files are specified in os native path separator.
59 for relfile in infiles:
62 isolateserver.expand_directory_and_symlink(
63 indir, relfile, blacklist, follow_symlinks))
64 except isolateserver.MappingError as e:
65 if ignore_broken_items:
66 logging.info('warning: %s', e)
72 def recreate_tree(outdir, indir, infiles, action, as_hash):
73 """Creates a new tree with only the input files in it.
76 outdir: Output directory to create the files in.
77 indir: Root directory the infiles are based in.
78 infiles: dict of files to map from |indir| to |outdir|.
79 action: One of accepted action of run_isolated.link_file().
80 as_hash: Output filename is the hash instead of relfile.
83 'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
84 (outdir, indir, len(infiles), action, as_hash))
86 assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
87 if not os.path.isdir(outdir):
88 logging.info('Creating %s' % outdir)
91 for relfile, metadata in infiles.iteritems():
92 infile = os.path.join(indir, relfile)
94 # Do the hashtable specific checks.
96 # Skip links when storing a hashtable.
98 outfile = os.path.join(outdir, metadata['h'])
99 if os.path.isfile(outfile):
100 # Just do a quick check that the file size matches. No need to stat()
101 # again the input file, grab the value from the dict.
102 if not 's' in metadata:
103 raise isolateserver.MappingError(
104 'Misconfigured item %s: %s' % (relfile, metadata))
105 if metadata['s'] == os.stat(outfile).st_size:
108 logging.warn('Overwritting %s' % metadata['h'])
111 outfile = os.path.join(outdir, relfile)
112 outsubdir = os.path.dirname(outfile)
113 if not os.path.isdir(outsubdir):
114 os.makedirs(outsubdir)
116 # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
117 # if metadata.get('T') == True:
118 # open(outfile, 'ab').close()
120 pointed = metadata['l']
121 logging.debug('Symlink: %s -> %s' % (outfile, pointed))
122 # symlink doesn't exist on Windows.
123 os.symlink(pointed, outfile) # pylint: disable=E1101
125 run_isolated.link_file(outfile, infile, action)
131 def _normalize_path_variable(cwd, relative_base_dir, key, value):
132 """Normalizes a path variable into a relative directory.
134 # Variables could contain / or \ on windows. Always normalize to
136 x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
137 normalized = file_path.get_native_path_case(os.path.normpath(x))
138 if not os.path.isdir(normalized):
139 raise ExecutionError('%s=%s is not a directory' % (key, normalized))
141 # All variables are relative to the .isolate file.
142 normalized = os.path.relpath(normalized, relative_base_dir)
144 'Translated variable %s from %s to %s', key, value, normalized)
148 def normalize_path_variables(cwd, path_variables, relative_base_dir):
149 """Processes path variables as a special case and returns a copy of the dict.
151 For each 'path' variable: first normalizes it based on |cwd|, verifies it
152 exists then sets it as relative to relative_base_dir.
155 'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
157 assert isinstance(cwd, unicode), cwd
158 assert isinstance(relative_base_dir, unicode), relative_base_dir
159 relative_base_dir = file_path.get_native_path_case(relative_base_dir)
161 (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
162 for k, v in path_variables.iteritems())
165 ### Internal state files.
168 def isolatedfile_to_state(filename):
169 """For a '.isolate' file, returns the path to the saved '.state' file."""
170 return filename + '.state'
173 def classify_files(root_dir, tracked, untracked):
174 """Converts the list of files into a .isolate 'variables' dictionary.
177 - tracked: list of files names to generate a dictionary out of that should
179 - untracked: list of files names that must not be tracked.
182 new_untracked = list(untracked)
184 def should_be_tracked(filepath):
185 """Returns True if it is a file without whitespace in a non-optional
186 directory that has no symlink in its path.
188 if filepath.endswith('/'):
192 # Look if any element in the path is a symlink.
193 split = filepath.split('/')
194 for i in range(len(split)):
195 if os.path.islink(os.path.join(root_dir, '/'.join(split[:i+1]))):
199 for filepath in sorted(tracked):
200 if should_be_tracked(filepath):
201 new_tracked.append(filepath)
204 new_untracked.append(filepath)
208 variables[isolate_format.KEY_TRACKED] = sorted(new_tracked)
210 variables[isolate_format.KEY_UNTRACKED] = sorted(new_untracked)
214 def chromium_fix(f, variables):
215 """Fixes an isolate dependency with Chromium-specific fixes."""
216 # Blacklist logs and other unimportant files.
217 # - 'First Run' is not created by the compile but by the test itself.
218 # - Skip log in PRODUCT_DIR. Note that these are applied on '/' style path
219 # separator at this point.
220 if (re.match(r'^\<\(PRODUCT_DIR\)\/[^\/]+\.log$', f) or
221 f == '<(PRODUCT_DIR)/First Run'):
222 logging.debug('Ignoring %s', f)
225 EXECUTABLE = re.compile(
226 r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
227 re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
229 match = EXECUTABLE.match(f)
231 return match.group(1) + '<(EXECUTABLE_SUFFIX)'
233 if sys.platform == 'darwin':
234 # On OSX, the name of the output is dependent on gyp define, it can be
235 # 'Google Chrome.app' or 'Chromium.app', same for 'XXX
236 # Framework.framework'. Furthermore, they are versioned with a gyp
237 # variable. To lower the complexity of the .isolate file, remove all the
238 # individual entries that show up under any of the 4 entries and replace
239 # them with the directory itself. Overall, this results in a bit more
240 # files than strictly necessary.
242 '<(PRODUCT_DIR)/Chromium Framework.framework/',
243 '<(PRODUCT_DIR)/Chromium.app/',
244 '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
245 '<(PRODUCT_DIR)/Google Chrome.app/',
247 for prefix in OSX_BUNDLES:
248 if f.startswith(prefix):
249 # Note this result in duplicate values, so the a set() must be used to
255 def generate_simplified(
256 tracked, untracked, touched, root_dir, path_variables, extra_variables,
257 relative_cwd, trace_blacklist):
258 """Generates a clean and complete .isolate 'variables' dictionary.
260 Cleans up and extracts only files from within root_dir then processes
261 variables and relative_cwd.
263 root_dir = os.path.realpath(root_dir)
265 'generate_simplified(%d files, %s, %s, %s, %s)' %
266 (len(tracked) + len(untracked) + len(touched),
267 root_dir, path_variables, extra_variables, relative_cwd))
270 relative_cwd = file_path.cleanup_path(relative_cwd)
271 assert not os.path.isabs(relative_cwd), relative_cwd
273 # Normalizes to posix path. .isolate files are using posix paths on all OSes
275 path_variables = dict(
276 (k, v.replace(os.path.sep, '/')) for k, v in path_variables.iteritems())
277 # Contains normalized path_variables plus extra_variables.
278 total_variables = path_variables.copy()
279 total_variables.update(extra_variables)
281 # Actual work: Process the files.
282 # TODO(maruel): if all the files in a directory are in part tracked and in
283 # part untracked, the directory will not be extracted. Tracked files should be
284 # 'promoted' to be untracked as needed.
285 tracked = trace_inputs.extract_directories(
286 root_dir, tracked, trace_blacklist)
287 untracked = trace_inputs.extract_directories(
288 root_dir, untracked, trace_blacklist)
289 # touched is not compressed, otherwise it would result in files to be archived
290 # that we don't need.
292 root_dir_posix = root_dir.replace(os.path.sep, '/')
294 """Bases the file on the most restrictive variable."""
295 # Important, GYP stores the files with / and not \.
296 f = f.replace(os.path.sep, '/')
297 logging.debug('fix(%s)' % f)
298 # If it's not already a variable.
299 if not f.startswith('<'):
300 # relative_cwd is usually the directory containing the gyp file. It may be
301 # empty if the whole directory containing the gyp file is needed.
302 # Use absolute paths in case cwd_dir is outside of root_dir.
303 # Convert the whole thing to / since it's isolate's speak.
304 f = file_path.posix_relpath(
305 posixpath.join(root_dir_posix, f),
306 posixpath.join(root_dir_posix, relative_cwd)) or './'
308 # Use the longest value first.
309 for key, value in sorted(
310 path_variables.iteritems(), key=lambda x: -len(x[1])):
311 if f.startswith(value):
312 f = '<(%s)%s' % (key, f[len(value):])
313 logging.debug('Converted to %s' % f)
318 """Reduces the items to convert variables, removes unneeded items, apply
319 chromium-specific fixes and only return unique items.
321 variables_converted = (fix(f.path) for f in items)
323 chromium_fix(f, total_variables) for f in variables_converted)
324 return set(f for f in chromium_fixed if f)
326 tracked = fix_all(tracked)
327 untracked = fix_all(untracked)
328 touched = fix_all(touched)
329 out = classify_files(root_dir, tracked, untracked)
331 out[isolate_format.KEY_TOUCHED] = sorted(touched)
335 def generate_isolate(
336 tracked, untracked, touched, root_dir, path_variables, config_variables,
337 extra_variables, relative_cwd, trace_blacklist):
338 """Generates a clean and complete .isolate file."""
339 dependencies = generate_simplified(
340 tracked, untracked, touched, root_dir, path_variables, extra_variables,
341 relative_cwd, trace_blacklist)
342 config_variable_names, config_values = zip(
343 *sorted(config_variables.iteritems()))
344 out = isolate_format.Configs(None, config_variable_names)
345 out.set_config(config_values, isolate_format.ConfigSettings(dependencies))
346 return out.make_isolate_file()
349 def chromium_save_isolated(isolated, data, path_variables, algo):
350 """Writes one or many .isolated files.
352 This slightly increases the cold cache cost but greatly reduce the warm cache
353 cost by splitting low-churn files off the master .isolated file. It also
354 reduces overall isolateserver memcache consumption.
358 def extract_into_included_isolated(prefix):
360 'algo': data['algo'],
362 'version': data['version'],
364 for f in data['files'].keys():
365 if f.startswith(prefix):
366 new_slave['files'][f] = data['files'].pop(f)
367 if new_slave['files']:
368 slaves.append(new_slave)
370 # Split test/data/ in its own .isolated file.
371 extract_into_included_isolated(os.path.join('test', 'data', ''))
373 # Split everything out of PRODUCT_DIR in its own .isolated file.
374 if path_variables.get('PRODUCT_DIR'):
375 extract_into_included_isolated(path_variables['PRODUCT_DIR'])
378 for index, f in enumerate(slaves):
379 slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
380 tools.write_json(slavepath, f, True)
381 data.setdefault('includes', []).append(
382 isolateserver.hash_file(slavepath, algo))
383 files.append(os.path.basename(slavepath))
385 files.extend(isolateserver.save_isolated(isolated, data))
389 class Flattenable(object):
390 """Represents data that can be represented as a json file."""
394 """Returns a json-serializable version of itself.
398 items = ((member, getattr(self, member)) for member in self.MEMBERS)
399 return dict((member, value) for member, value in items if value is not None)
402 def load(cls, data, *args, **kwargs):
403 """Loads a flattened version."""
405 out = cls(*args, **kwargs)
406 for member in out.MEMBERS:
408 # Access to a protected member XXX of a client class
409 # pylint: disable=W0212
410 out._load_member(member, data.pop(member))
413 'Found unexpected entry %s while constructing an object %s' %
414 (data, cls.__name__), data, cls.__name__)
417 def _load_member(self, member, value):
418 """Loads a member into self."""
419 setattr(self, member, value)
422 def load_file(cls, filename, *args, **kwargs):
423 """Loads the data from a file or return an empty instance."""
425 out = cls.load(tools.read_json(filename), *args, **kwargs)
426 logging.debug('Loaded %s(%s)', cls.__name__, filename)
427 except (IOError, ValueError) as e:
428 # On failure, loads the default instance.
429 out = cls(*args, **kwargs)
430 logging.warn('Failed to load %s: %s', filename, e)
434 class SavedState(Flattenable):
435 """Describes the content of a .state file.
437 This file caches the items calculated by this script and is used to increase
438 the performance of the script. This file is not loaded by run_isolated.py.
439 This file can always be safely removed.
441 It is important to note that the 'files' dict keys are using native OS path
442 separator instead of '/' used in .isolate file.
445 # Value of sys.platform so that the file is rejected if loaded from a
446 # different OS. While this should never happen in practice, users are ...
449 # Algorithm used to generate the hash. The only supported value is at the
450 # time of writting 'sha-1'.
452 # Cache of the processed command. This value is saved because .isolated
453 # files are never loaded by isolate.py so it's the only way to load the
456 # GYP variables that are used to generate conditions. The most frequent
459 # GYP variables that will be replaced in 'command' and paths but will not be
460 # considered a relative directory.
462 # Cache of the files found so the next run can skip hash calculation.
464 # Path of the original .isolate file. Relative path to isolated_basedir.
466 # List of included .isolated files. Used to support/remember 'slave'
467 # .isolated files. Relative path to isolated_basedir.
468 'child_isolated_files',
469 # If the generated directory tree should be read-only.
471 # Relative cwd to use to start the command.
473 # GYP variables used to generate the .isolated files paths based on path
474 # variables. Frequent examples are DEPTH and PRODUCT_DIR.
476 # Version of the file format in format 'major.minor'. Any non-breaking
477 # change must update minor. Any breaking change must update major.
481 def __init__(self, isolated_basedir):
482 """Creates an empty SavedState.
485 isolated_basedir: the directory where the .isolated and .isolated.state
488 super(SavedState, self).__init__()
489 assert os.path.isabs(isolated_basedir), isolated_basedir
490 assert os.path.isdir(isolated_basedir), isolated_basedir
491 self.isolated_basedir = isolated_basedir
493 # The default algorithm used.
494 self.OS = sys.platform
495 self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
496 self.child_isolated_files = []
498 self.config_variables = {}
499 self.extra_variables = {}
501 self.isolate_file = None
502 self.path_variables = {}
503 self.read_only = None
504 self.relative_cwd = None
505 self.version = isolateserver.ISOLATED_FILE_VERSION
508 self, isolate_file, path_variables, config_variables, extra_variables):
509 """Updates the saved state with new data to keep GYP variables and internal
510 reference to the original .isolate file.
512 assert os.path.isabs(isolate_file)
513 # Convert back to a relative path. On Windows, if the isolate and
514 # isolated files are on different drives, isolate_file will stay an absolute
516 isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
518 # The same .isolate file should always be used to generate the .isolated and
520 assert isolate_file == self.isolate_file or not self.isolate_file, (
521 isolate_file, self.isolate_file)
522 self.config_variables.update(config_variables)
523 self.extra_variables.update(extra_variables)
524 self.isolate_file = isolate_file
525 self.path_variables.update(path_variables)
527 def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
528 """Updates the saved state with data necessary to generate a .isolated file.
530 The new files in |infiles| are added to self.files dict but their hash is
533 self.command = command
536 self.files.setdefault(f, {})
538 self.files.setdefault(f, {})['T'] = True
539 # Prune extraneous files that are not a dependency anymore.
540 for f in set(self.files).difference(set(infiles).union(touched)):
542 if read_only is not None:
543 self.read_only = read_only
544 self.relative_cwd = relative_cwd
546 def to_isolated(self):
547 """Creates a .isolated dictionary out of the saved state.
549 https://code.google.com/p/swarming/wiki/IsolatedDesign
552 """Returns a 'files' entry with only the whitelisted keys."""
553 return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
556 'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
558 (filepath, strip(data)) for filepath, data in self.files.iteritems()),
559 'version': self.version,
562 out['command'] = self.command
563 if self.read_only is not None:
564 out['read_only'] = self.read_only
565 if self.relative_cwd:
566 out['relative_cwd'] = self.relative_cwd
570 def isolate_filepath(self):
571 """Returns the absolute path of self.isolate_file."""
572 return os.path.normpath(
573 os.path.join(self.isolated_basedir, self.isolate_file))
575 # Arguments number differs from overridden method
577 def load(cls, data, isolated_basedir): # pylint: disable=W0221
578 """Special case loading to disallow different OS.
580 It is not possible to load a .isolated.state files from a different OS, this
581 file is saved in OS-specific format.
583 out = super(SavedState, cls).load(data, isolated_basedir)
584 if data.get('OS') != sys.platform:
585 raise isolateserver.ConfigError('Unexpected OS %s', data.get('OS'))
587 # Converts human readable form back into the proper class type.
588 algo = data.get('algo')
589 if not algo in isolateserver.SUPPORTED_ALGOS:
590 raise isolateserver.ConfigError('Unknown algo \'%s\'' % out.algo)
591 out.algo = isolateserver.SUPPORTED_ALGOS[algo]
593 # Refuse the load non-exact version, even minor difference. This is unlike
594 # isolateserver.load_isolated(). This is because .isolated.state could have
595 # changed significantly even in minor version difference.
596 if not re.match(r'^(\d+)\.(\d+)$', out.version):
597 raise isolateserver.ConfigError('Unknown version \'%s\'' % out.version)
598 if out.version != isolateserver.ISOLATED_FILE_VERSION:
599 raise isolateserver.ConfigError(
600 'Unsupported version \'%s\'' % out.version)
602 # The .isolate file must be valid. If it is not present anymore, zap the
603 # value as if it was not noted, so .isolate_file can safely be overriden
605 if out.isolate_file and not os.path.isfile(out.isolate_filepath):
606 out.isolate_file = None
608 # It could be absolute on Windows if the drive containing the .isolate and
609 # the drive containing the .isolated files differ, .e.g .isolate is on
610 # C:\\ and .isolated is on D:\\ .
611 assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
612 assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
616 """Makes sure 'algo' is in human readable form."""
617 out = super(SavedState, self).flatten()
618 out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
623 return ''.join('\n %s=%s' % (k, d[k]) for k in sorted(d))
625 out = '%s(\n' % self.__class__.__name__
626 out += ' command: %s\n' % self.command
627 out += ' files: %d\n' % len(self.files)
628 out += ' isolate_file: %s\n' % self.isolate_file
629 out += ' read_only: %s\n' % self.read_only
630 out += ' relative_cwd: %s\n' % self.relative_cwd
631 out += ' child_isolated_files: %s\n' % self.child_isolated_files
632 out += ' path_variables: %s\n' % dict_to_str(self.path_variables)
633 out += ' config_variables: %s\n' % dict_to_str(self.config_variables)
634 out += ' extra_variables: %s\n' % dict_to_str(self.extra_variables)
638 class CompleteState(object):
639 """Contains all the state to run the task at hand."""
640 def __init__(self, isolated_filepath, saved_state):
641 super(CompleteState, self).__init__()
642 assert isolated_filepath is None or os.path.isabs(isolated_filepath)
643 self.isolated_filepath = isolated_filepath
644 # Contains the data to ease developer's use-case but that is not strictly
646 self.saved_state = saved_state
649 def load_files(cls, isolated_filepath):
650 """Loads state from disk."""
651 assert os.path.isabs(isolated_filepath), isolated_filepath
652 isolated_basedir = os.path.dirname(isolated_filepath)
655 SavedState.load_file(
656 isolatedfile_to_state(isolated_filepath), isolated_basedir))
659 self, cwd, isolate_file, path_variables, config_variables,
660 extra_variables, ignore_broken_items):
661 """Updates self.isolated and self.saved_state with information loaded from a
664 Processes the loaded data, deduce root_dir, relative_cwd.
666 # Make sure to not depend on os.getcwd().
667 assert os.path.isabs(isolate_file), isolate_file
668 isolate_file = file_path.get_native_path_case(isolate_file)
670 'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
671 cwd, isolate_file, path_variables, config_variables, extra_variables,
673 relative_base_dir = os.path.dirname(isolate_file)
675 # Processes the variables.
676 path_variables = normalize_path_variables(
677 cwd, path_variables, relative_base_dir)
678 # Update the saved state.
679 self.saved_state.update(
680 isolate_file, path_variables, config_variables, extra_variables)
681 path_variables = self.saved_state.path_variables
683 with open(isolate_file, 'r') as f:
684 # At that point, variables are not replaced yet in command and infiles.
685 # infiles may contain directory entries and is in posix style.
686 command, infiles, touched, read_only = (
687 isolate_format.load_isolate_for_config(
688 os.path.dirname(isolate_file), f.read(),
689 self.saved_state.config_variables))
691 total_variables = self.saved_state.path_variables.copy()
692 total_variables.update(self.saved_state.config_variables)
693 total_variables.update(self.saved_state.extra_variables)
695 isolate_format.eval_variables(i, total_variables) for i in command
698 total_variables = self.saved_state.path_variables.copy()
699 total_variables.update(self.saved_state.extra_variables)
701 isolate_format.eval_variables(f, total_variables) for f in infiles
704 isolate_format.eval_variables(f, total_variables) for f in touched
706 # root_dir is automatically determined by the deepest root accessed with the
707 # form '../../foo/bar'. Note that path variables must be taken in account
708 # too, add them as if they were input files.
709 root_dir = isolate_format.determine_root_dir(
710 relative_base_dir, infiles + touched +
711 self.saved_state.path_variables.values())
712 # The relative directory is automatically determined by the relative path
713 # between root_dir and the directory containing the .isolate file,
715 relative_cwd = os.path.relpath(relative_base_dir, root_dir)
716 # Now that we know where the root is, check that the path_variables point
718 for k, v in self.saved_state.path_variables.iteritems():
719 if not file_path.path_starts_with(
720 root_dir, os.path.join(relative_base_dir, v)):
721 raise isolateserver.MappingError(
722 'Path variable %s=%r points outside the inferred root directory %s'
724 # Normalize the files based to root_dir. It is important to keep the
725 # trailing os.path.sep at that step.
728 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
733 file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
736 follow_symlinks = sys.platform != 'win32'
737 # Expand the directories by listing each file inside. Up to now, trailing
738 # os.path.sep must be kept. Do not expand 'touched'.
739 infiles = expand_directories_and_symlinks(
742 lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
746 # If we ignore broken items then remove any missing touched items.
747 if ignore_broken_items:
748 original_touched_count = len(touched)
749 touched = [touch for touch in touched if os.path.exists(touch)]
751 if len(touched) != original_touched_count:
752 logging.info('Removed %d invalid touched entries',
753 len(touched) - original_touched_count)
755 # Finally, update the new data to be able to generate the foo.isolated file,
756 # the file that is used by run_isolated.py.
757 self.saved_state.update_isolated(
758 command, infiles, touched, read_only, relative_cwd)
761 def process_inputs(self, subdir):
762 """Updates self.saved_state.files with the files' mode and hash.
764 If |subdir| is specified, filters to a subdirectory. The resulting .isolated
767 See isolateserver.process_input() for more information.
769 for infile in sorted(self.saved_state.files):
770 if subdir and not infile.startswith(subdir):
771 self.saved_state.files.pop(infile)
773 filepath = os.path.join(self.root_dir, infile)
774 self.saved_state.files[infile] = isolateserver.process_input(
776 self.saved_state.files[infile],
777 self.saved_state.read_only,
778 self.saved_state.algo)
780 def save_files(self):
781 """Saves self.saved_state and creates a .isolated file."""
782 logging.debug('Dumping to %s' % self.isolated_filepath)
783 self.saved_state.child_isolated_files = chromium_save_isolated(
784 self.isolated_filepath,
785 self.saved_state.to_isolated(),
786 self.saved_state.path_variables,
787 self.saved_state.algo)
789 i.get('s', 0) for i in self.saved_state.files.itervalues())
791 # TODO(maruel): Stats are missing the .isolated files.
792 logging.debug('Total size: %d bytes' % total_bytes)
793 saved_state_file = isolatedfile_to_state(self.isolated_filepath)
794 logging.debug('Dumping to %s' % saved_state_file)
795 tools.write_json(saved_state_file, self.saved_state.flatten(), True)
799 """Returns the absolute path of the root_dir to reference the .isolate file
802 So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
805 if not self.saved_state.isolate_file:
806 raise ExecutionError('Please specify --isolate')
807 isolate_dir = os.path.dirname(self.saved_state.isolate_filepath)
809 if self.saved_state.relative_cwd == '.':
810 root_dir = isolate_dir
812 if not isolate_dir.endswith(self.saved_state.relative_cwd):
813 raise ExecutionError(
814 ('Make sure the .isolate file is in the directory that will be '
815 'used as the relative directory. It is currently in %s and should '
816 'be in %s') % (isolate_dir, self.saved_state.relative_cwd))
817 # Walk back back to the root directory.
818 root_dir = isolate_dir[:-(len(self.saved_state.relative_cwd) + 1)]
819 return file_path.get_native_path_case(root_dir)
823 """Returns the absolute path containing the .isolated file.
825 It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
828 return os.path.dirname(self.isolated_filepath)
831 def indent(data, indent_length):
833 spacing = ' ' * indent_length
834 return ''.join(spacing + l for l in str(data).splitlines(True))
836 out = '%s(\n' % self.__class__.__name__
837 out += ' root_dir: %s\n' % self.root_dir
838 out += ' saved_state: %s)' % indent(self.saved_state, 2)
842 def load_complete_state(options, cwd, subdir, skip_update):
843 """Loads a CompleteState.
845 This includes data from .isolate and .isolated.state files. Never reads the
849 options: Options instance generated with OptionParserIsolate. For either
850 options.isolate and options.isolated, if the value is set, it is an
852 cwd: base directory to be used when loading the .isolate file.
853 subdir: optional argument to only process file in the subdirectory, relative
854 to CompleteState.root_dir.
855 skip_update: Skip trying to load the .isolate file and processing the
856 dependencies. It is useful when not needed, like when tracing.
858 assert not options.isolate or os.path.isabs(options.isolate)
859 assert not options.isolated or os.path.isabs(options.isolated)
860 cwd = file_path.get_native_path_case(unicode(cwd))
862 # Load the previous state if it was present. Namely, "foo.isolated.state".
863 # Note: this call doesn't load the .isolate file.
864 complete_state = CompleteState.load_files(options.isolated)
866 # Constructs a dummy object that cannot be saved. Useful for temporary
867 # commands like 'run'. There is no directory containing a .isolated file so
868 # specify the current working directory as a valid directory.
869 complete_state = CompleteState(None, SavedState(os.getcwd()))
871 if not options.isolate:
872 if not complete_state.saved_state.isolate_file:
874 raise ExecutionError('A .isolate file is required.')
877 isolate = complete_state.saved_state.isolate_filepath
879 isolate = options.isolate
880 if complete_state.saved_state.isolate_file:
881 rel_isolate = file_path.safe_relpath(
882 options.isolate, complete_state.saved_state.isolated_basedir)
883 if rel_isolate != complete_state.saved_state.isolate_file:
884 # This happens if the .isolate file was moved for example. In this case,
885 # discard the saved state.
887 '--isolated %s != %s as saved in %s. Discarding saved state',
889 complete_state.saved_state.isolate_file,
890 isolatedfile_to_state(options.isolated))
891 complete_state = CompleteState(
893 SavedState(complete_state.saved_state.isolated_basedir))
896 # Then load the .isolate and expands directories.
897 complete_state.load_isolate(
898 cwd, isolate, options.path_variables, options.config_variables,
899 options.extra_variables, options.ignore_broken_items)
901 # Regenerate complete_state.saved_state.files.
903 subdir = unicode(subdir)
904 # This is tricky here. If it is a path, take it from the root_dir. If
905 # it is a variable, it must be keyed from the directory containing the
906 # .isolate file. So translate all variables first.
907 translated_path_variables = dict(
909 os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
911 for k, v in complete_state.saved_state.path_variables.iteritems())
912 subdir = isolate_format.eval_variables(subdir, translated_path_variables)
913 subdir = subdir.replace('/', os.path.sep)
916 complete_state.process_inputs(subdir)
917 return complete_state
920 def read_trace_as_isolate_dict(complete_state, trace_blacklist):
921 """Reads a trace and returns the .isolate dictionary.
923 Returns exceptions during the log parsing so it can be re-raised.
925 api = trace_inputs.get_api()
926 logfile = complete_state.isolated_filepath + '.log'
927 if not os.path.isfile(logfile):
928 raise ExecutionError(
929 'No log file \'%s\' to read, did you forget to \'trace\'?' % logfile)
931 data = api.parse_log(logfile, trace_blacklist, None)
932 exceptions = [i['exception'] for i in data if 'exception' in i]
933 results = (i['results'] for i in data if 'results' in i)
934 results_stripped = (i.strip_root(complete_state.root_dir) for i in results)
935 files = set(sum((result.existent for result in results_stripped), []))
936 tracked, touched = isolate_format.split_touched(files)
937 value = generate_isolate(
941 complete_state.root_dir,
942 complete_state.saved_state.path_variables,
943 complete_state.saved_state.config_variables,
944 complete_state.saved_state.extra_variables,
945 complete_state.saved_state.relative_cwd,
947 return value, exceptions
948 except trace_inputs.TracingFailure, e:
949 raise ExecutionError(
950 'Reading traces failed for: %s\n%s' %
951 (' '.join(complete_state.saved_state.command), str(e)))
954 def merge(complete_state, trace_blacklist):
955 """Reads a trace and merges it back into the source .isolate file."""
956 value, exceptions = read_trace_as_isolate_dict(
957 complete_state, trace_blacklist)
959 # Now take that data and union it into the original .isolate file.
960 with open(complete_state.saved_state.isolate_filepath, 'r') as f:
961 prev_content = f.read()
962 isolate_dir = os.path.dirname(complete_state.saved_state.isolate_filepath)
963 prev_config = isolate_format.load_isolate_as_config(
965 isolate_format.eval_content(prev_content),
966 isolate_format.extract_comment(prev_content))
967 new_config = isolate_format.load_isolate_as_config(isolate_dir, value, '')
968 config = isolate_format.union(prev_config, new_config)
969 data = config.make_isolate_file()
970 print('Updating %s' % complete_state.saved_state.isolate_file)
971 with open(complete_state.saved_state.isolate_filepath, 'wb') as f:
972 isolate_format.print_all(config.file_comment, data, f)
974 # It got an exception, raise the first one.
981 def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
982 """Creates a isolated tree usable for test execution.
984 Returns the current working directory where the isolated command should be
987 # Forcibly copy when the tree has to be read only. Otherwise the inode is
988 # modified, and this cause real problems because the user's source tree
989 # becomes read only. On the other hand, the cost of doing file copy is huge.
990 if read_only not in (0, None):
991 action = run_isolated.COPY
993 action = run_isolated.HARDLINK_WITH_FALLBACK
1001 cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
1002 if not os.path.isdir(cwd):
1003 # It can happen when no files are mapped from the directory containing the
1004 # .isolate file. But the directory must exist to be the current working
1007 run_isolated.change_tree_read_only(outdir, read_only)
1011 def prepare_for_archival(options, cwd):
1012 """Loads the isolated file and create 'infiles' for archival."""
1013 complete_state = load_complete_state(
1014 options, cwd, options.subdir, False)
1015 # Make sure that complete_state isn't modified until save_files() is
1016 # called, because any changes made to it here will propagate to the files
1017 # created (which is probably not intended).
1018 complete_state.save_files()
1020 infiles = complete_state.saved_state.files
1021 # Add all the .isolated files.
1025 ] + complete_state.saved_state.child_isolated_files
1026 for item in isolated_files:
1027 item_path = os.path.join(
1028 os.path.dirname(complete_state.isolated_filepath), item)
1029 # Do not use isolateserver.hash_file() here because the file is
1030 # likely smallish (under 500kb) and its file size is needed.
1031 with open(item_path, 'rb') as f:
1033 isolated_hash.append(
1034 complete_state.saved_state.algo(content).hexdigest())
1035 isolated_metadata = {
1036 'h': isolated_hash[-1],
1040 infiles[item_path] = isolated_metadata
1041 return complete_state, infiles, isolated_hash
1047 def CMDarchive(parser, args):
1048 """Creates a .isolated file and uploads the tree to an isolate server.
1050 All the files listed in the .isolated file are put in the isolate server
1051 cache via isolateserver.py.
1053 add_subdir_option(parser)
1054 isolateserver.add_isolate_server_options(parser, False)
1055 auth.add_auth_options(parser)
1056 options, args = parser.parse_args(args)
1057 auth.process_auth_options(parser, options)
1058 isolateserver.process_isolate_server_options(parser, options)
1060 parser.error('Unsupported argument: %s' % args)
1062 with tools.Profiler('GenerateHashtable'):
1065 complete_state, infiles, isolated_hash = prepare_for_archival(
1067 logging.info('Creating content addressed object store with %d item',
1070 isolateserver.upload_tree(
1071 base_url=options.isolate_server,
1072 indir=complete_state.root_dir,
1074 namespace=options.namespace)
1076 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
1078 # If the command failed, delete the .isolated file if it exists. This is
1079 # important so no stale swarm job is executed.
1080 if not success and os.path.isfile(options.isolated):
1081 os.remove(options.isolated)
1082 return int(not success)
1085 def CMDcheck(parser, args):
1086 """Checks that all the inputs are present and generates .isolated."""
1087 add_subdir_option(parser)
1088 options, args = parser.parse_args(args)
1090 parser.error('Unsupported argument: %s' % args)
1092 complete_state = load_complete_state(
1093 options, os.getcwd(), options.subdir, False)
1095 # Nothing is done specifically. Just store the result and state.
1096 complete_state.save_files()
1100 def CMDhashtable(parser, args):
1101 """Creates a .isolated file and stores the contains in a directory.
1103 All the files listed in the .isolated file are put in the directory with their
1104 sha-1 as their file name. When using an NFS/CIFS server, the files can then be
1105 shared accross slaves without an isolate server.
1107 add_subdir_option(parser)
1108 isolateserver.add_outdir_options(parser)
1109 add_skip_refresh_option(parser)
1110 options, args = parser.parse_args(args)
1112 parser.error('Unsupported argument: %s' % args)
1114 isolateserver.process_outdir_options(parser, options, cwd)
1118 complete_state, infiles, isolated_hash = prepare_for_archival(options, cwd)
1119 logging.info('Creating content addressed object store with %d item',
1121 if not os.path.isdir(options.outdir):
1122 os.makedirs(options.outdir)
1124 # TODO(maruel): Make the files read-only?
1126 outdir=options.outdir,
1127 indir=complete_state.root_dir,
1129 action=run_isolated.HARDLINK_WITH_FALLBACK,
1132 print('%s %s' % (isolated_hash[0], os.path.basename(options.isolated)))
1134 # If the command failed, delete the .isolated file if it exists. This is
1135 # important so no stale swarm job is executed.
1136 if not success and os.path.isfile(options.isolated):
1137 os.remove(options.isolated)
1138 return int(not success)
1141 def CMDmerge(parser, args):
1142 """Reads and merges the data from the trace back into the original .isolate.
1144 parser.require_isolated = False
1145 add_trace_option(parser)
1146 options, args = parser.parse_args(args)
1148 parser.error('Unsupported argument: %s' % args)
1150 complete_state = load_complete_state(options, os.getcwd(), None, False)
1151 blacklist = tools.gen_blacklist(options.trace_blacklist)
1152 merge(complete_state, blacklist)
1156 def CMDread(parser, args):
1157 """Reads the trace file generated with command 'trace'."""
1158 parser.require_isolated = False
1159 add_trace_option(parser)
1160 add_skip_refresh_option(parser)
1162 '-m', '--merge', action='store_true',
1163 help='merge the results back in the .isolate file instead of printing')
1164 options, args = parser.parse_args(args)
1166 parser.error('Unsupported argument: %s' % args)
1168 complete_state = load_complete_state(
1169 options, os.getcwd(), None, options.skip_refresh)
1170 blacklist = tools.gen_blacklist(options.trace_blacklist)
1171 value, exceptions = read_trace_as_isolate_dict(complete_state, blacklist)
1173 merge(complete_state, blacklist)
1175 isolate_format.pretty_print(value, sys.stdout)
1178 # It got an exception, raise the first one.
1186 def CMDremap(parser, args):
1187 """Creates a directory with all the dependencies mapped into it.
1189 Useful to test manually why a test is failing. The target executable is not
1192 parser.require_isolated = False
1193 isolateserver.add_outdir_options(parser)
1194 add_skip_refresh_option(parser)
1195 options, args = parser.parse_args(args)
1197 parser.error('Unsupported argument: %s' % args)
1199 isolateserver.process_outdir_options(parser, options, cwd)
1200 complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
1202 if not os.path.isdir(options.outdir):
1203 os.makedirs(options.outdir)
1204 print('Remapping into %s' % options.outdir)
1205 if os.listdir(options.outdir):
1206 raise ExecutionError('Can\'t remap in a non-empty directory')
1208 create_isolate_tree(
1209 options.outdir, complete_state.root_dir, complete_state.saved_state.files,
1210 complete_state.saved_state.relative_cwd,
1211 complete_state.saved_state.read_only)
1212 if complete_state.isolated_filepath:
1213 complete_state.save_files()
1217 def CMDrewrite(parser, args):
1218 """Rewrites a .isolate file into the canonical format."""
1219 parser.require_isolated = False
1220 options, args = parser.parse_args(args)
1222 parser.error('Unsupported argument: %s' % args)
1224 if options.isolated:
1225 # Load the previous state if it was present. Namely, "foo.isolated.state".
1226 complete_state = CompleteState.load_files(options.isolated)
1227 isolate = options.isolate or complete_state.saved_state.isolate_filepath
1229 isolate = options.isolate
1231 parser.error('--isolate is required.')
1233 with open(isolate, 'r') as f:
1235 config = isolate_format.load_isolate_as_config(
1236 os.path.dirname(os.path.abspath(isolate)),
1237 isolate_format.eval_content(content),
1238 isolate_format.extract_comment(content))
1239 data = config.make_isolate_file()
1240 print('Updating %s' % isolate)
1241 with open(isolate, 'wb') as f:
1242 isolate_format.print_all(config.file_comment, data, f)
1246 @subcommand.usage('-- [extra arguments]')
1247 def CMDrun(parser, args):
1248 """Runs the test executable in an isolated (temporary) directory.
1250 All the dependencies are mapped into the temporary directory and the
1251 directory is cleaned up after the target exits.
1253 Argument processing stops at -- and these arguments are appended to the
1254 command line of the target to run. For example, use:
1255 isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
1257 parser.require_isolated = False
1258 add_skip_refresh_option(parser)
1259 options, args = parser.parse_args(args)
1261 complete_state = load_complete_state(
1262 options, os.getcwd(), None, options.skip_refresh)
1263 cmd = complete_state.saved_state.command + args
1265 raise ExecutionError('No command to run.')
1266 cmd = tools.fix_python_path(cmd)
1268 outdir = run_isolated.make_temp_dir(
1269 'isolate-%s' % datetime.date.today(),
1270 os.path.dirname(complete_state.root_dir))
1272 # TODO(maruel): Use run_isolated.run_tha_test().
1273 cwd = create_isolate_tree(
1274 outdir, complete_state.root_dir, complete_state.saved_state.files,
1275 complete_state.saved_state.relative_cwd,
1276 complete_state.saved_state.read_only)
1277 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1278 result = subprocess.call(cmd, cwd=cwd)
1280 run_isolated.rmtree(outdir)
1282 if complete_state.isolated_filepath:
1283 complete_state.save_files()
1287 @subcommand.usage('-- [extra arguments]')
1288 def CMDtrace(parser, args):
1289 """Traces the target using trace_inputs.py.
1291 It runs the executable without remapping it, and traces all the files it and
1292 its child processes access. Then the 'merge' command can be used to generate
1293 an updated .isolate file out of it or the 'read' command to print it out to
1296 Argument processing stops at -- and these arguments are appended to the
1297 command line of the target to run. For example, use:
1298 isolate.py trace --isolated foo.isolated -- --gtest_filter=Foo.Bar
1300 add_trace_option(parser)
1302 '-m', '--merge', action='store_true',
1303 help='After tracing, merge the results back in the .isolate file')
1304 add_skip_refresh_option(parser)
1305 options, args = parser.parse_args(args)
1307 complete_state = load_complete_state(
1308 options, os.getcwd(), None, options.skip_refresh)
1309 cmd = complete_state.saved_state.command + args
1311 raise ExecutionError('No command to run.')
1312 cmd = tools.fix_python_path(cmd)
1313 cwd = os.path.normpath(os.path.join(
1314 unicode(complete_state.root_dir),
1315 complete_state.saved_state.relative_cwd))
1316 cmd[0] = os.path.normpath(os.path.join(cwd, cmd[0]))
1317 if not os.path.isfile(cmd[0]):
1318 raise ExecutionError(
1319 'Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd))
1320 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1321 api = trace_inputs.get_api()
1322 logfile = complete_state.isolated_filepath + '.log'
1323 api.clean_trace(logfile)
1326 with api.get_tracer(logfile) as tracer:
1327 result, out = tracer.trace(
1332 except trace_inputs.TracingFailure, e:
1333 raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1337 'Tracer exited with %d, which means the tests probably failed so the '
1338 'trace is probably incomplete.', result)
1341 complete_state.save_files()
1344 blacklist = tools.gen_blacklist(options.trace_blacklist)
1345 merge(complete_state, blacklist)
1350 def _process_variable_arg(option, opt, _value, parser):
1351 """Called by OptionParser to process a --<foo>-variable argument."""
1352 if not parser.rargs:
1353 raise optparse.OptionValueError(
1354 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
1355 k = parser.rargs.pop(0)
1356 variables = getattr(parser.values, option.dest)
1358 k, v = k.split('=', 1)
1360 if not parser.rargs:
1361 raise optparse.OptionValueError(
1362 'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
1363 v = parser.rargs.pop(0)
1364 if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
1365 raise optparse.OptionValueError(
1366 'Variable \'%s\' doesn\'t respect format \'%s\'' %
1367 (k, isolate_format.VALID_VARIABLE))
1368 variables.append((k, v.decode('utf-8')))
1371 def add_variable_option(parser):
1372 """Adds --isolated and --<foo>-variable to an OptionParser."""
1376 help='.isolated file to generate or read')
1377 # Keep for compatibility. TODO(maruel): Remove once not used anymore.
1381 help=optparse.SUPPRESS_HELP)
1382 is_win = sys.platform in ('win32', 'cygwin')
1383 # There is really 3 kind of variables:
1384 # - path variables, like DEPTH or PRODUCT_DIR that should be
1385 # replaced opportunistically when tracing tests.
1386 # - extraneous things like EXECUTABE_SUFFIX.
1387 # - configuration variables that are to be used in deducing the matrix to
1389 # - unrelated variables that are used as command flags for example.
1391 '--config-variable',
1393 callback=_process_variable_arg,
1395 dest='config_variables',
1397 help='Config variables are used to determine which conditions should be '
1398 'matched when loading a .isolate file, default: %default. '
1399 'All 3 kinds of variables are persistent accross calls, they are '
1400 'saved inside <.isolated>.state')
1404 callback=_process_variable_arg,
1406 dest='path_variables',
1408 help='Path variables are used to replace file paths when loading a '
1409 '.isolate file, default: %default')
1413 callback=_process_variable_arg,
1414 default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1415 dest='extra_variables',
1417 help='Extraneous variables are replaced on the \'command\' entry and on '
1418 'paths in the .isolate file but are not considered relative paths.')
1421 def add_subdir_option(parser):
1424 help='Filters to a subdirectory. Its behavior changes depending if it '
1425 'is a relative path as a string or as a path variable. Path '
1426 'variables are always keyed from the directory containing the '
1427 '.isolate file. Anything else is keyed on the root directory.')
1430 def add_trace_option(parser):
1431 """Adds --trace-blacklist to the parser."""
1433 '--trace-blacklist',
1434 action='append', default=list(isolateserver.DEFAULT_BLACKLIST),
1435 help='List of regexp to use as blacklist filter for files to consider '
1436 'important, not to be confused with --blacklist which blacklists '
1440 def add_skip_refresh_option(parser):
1442 '--skip-refresh', action='store_true',
1443 help='Skip reading .isolate file and do not refresh the hash of '
1447 def parse_isolated_option(parser, options, cwd, require_isolated):
1448 """Processes --isolated."""
1449 if options.isolated:
1450 options.isolated = os.path.normpath(
1451 os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
1452 if require_isolated and not options.isolated:
1453 parser.error('--isolated is required.')
1454 if options.isolated and not options.isolated.endswith('.isolated'):
1455 parser.error('--isolated value must end with \'.isolated\'')
1458 def parse_variable_option(options):
1459 """Processes all the --<foo>-variable flags."""
1460 # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
1461 # but it wouldn't be backward compatible.
1462 def try_make_int(s):
1463 """Converts a value to int if possible, converts to unicode otherwise."""
1467 return s.decode('utf-8')
1468 options.config_variables = dict(
1469 (k, try_make_int(v)) for k, v in options.config_variables)
1470 options.path_variables = dict(options.path_variables)
1471 options.extra_variables = dict(options.extra_variables)
1474 class OptionParserIsolate(tools.OptionParserWithLogging):
1475 """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
1477 # Set it to False if it is not required, e.g. it can be passed on but do not
1478 # fail if not given.
1479 require_isolated = True
1481 def __init__(self, **kwargs):
1482 tools.OptionParserWithLogging.__init__(
1484 verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1486 group = optparse.OptionGroup(self, "Common options")
1490 help='.isolate file to load the dependency data from')
1491 add_variable_option(group)
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 return options, args
1525 dispatcher = subcommand.CommandDispatcher(__name__)
1527 return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
1528 except Exception as e:
1529 tools.report_error(e)
1533 if __name__ == '__main__':
1534 fix_encoding.fix_encoding()
1535 tools.disable_buffering()
1537 sys.exit(main(sys.argv[1:]))