Upstream version 6.35.121.0
[platform/framework/web/crosswalk.git] / src / tools / swarming_client / isolate.py
1 #!/usr/bin/env python
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.
5
6 """Front end tool to operate on .isolate files.
7
8 This includes creating, merging or compiling them to generate a .isolated file.
9
10 See more information at
11   https://code.google.com/p/swarming/wiki/IsolateDesign
12   https://code.google.com/p/swarming/wiki/IsolateUserGuide
13 """
14 # Run ./isolate.py --help for more detailed information.
15
16 import datetime
17 import logging
18 import optparse
19 import os
20 import posixpath
21 import re
22 import subprocess
23 import sys
24
25 import auth
26 import isolate_format
27 import isolateserver
28 import run_isolated
29 import trace_inputs
30
31 from third_party import colorama
32 from third_party.depot_tools import fix_encoding
33 from third_party.depot_tools import subcommand
34
35 from utils import file_path
36 from utils import tools
37
38
39 __version__ = '0.3.1'
40
41
42 class ExecutionError(Exception):
43   """A generic error occurred."""
44   def __str__(self):
45     return self.args[0]
46
47
48 ### Path handling code.
49
50
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
54   verifies files exist.
55
56   Files are specified in os native path separator.
57   """
58   outfiles = []
59   for relfile in infiles:
60     try:
61       outfiles.extend(
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)
67       else:
68         raise
69   return outfiles
70
71
72 def recreate_tree(outdir, indir, infiles, action, as_hash):
73   """Creates a new tree with only the input files in it.
74
75   Arguments:
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.
81   """
82   logging.info(
83       'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
84       (outdir, indir, len(infiles), action, as_hash))
85
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)
89     os.makedirs(outdir)
90
91   for relfile, metadata in infiles.iteritems():
92     infile = os.path.join(indir, relfile)
93     if as_hash:
94       # Do the hashtable specific checks.
95       if 'l' in metadata:
96         # Skip links when storing a hashtable.
97         continue
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:
106           continue
107         else:
108           logging.warn('Overwritting %s' % metadata['h'])
109           os.remove(outfile)
110     else:
111       outfile = os.path.join(outdir, relfile)
112       outsubdir = os.path.dirname(outfile)
113       if not os.path.isdir(outsubdir):
114         os.makedirs(outsubdir)
115
116     # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
117     # if metadata.get('T') == True:
118     #   open(outfile, 'ab').close()
119     if 'l' in metadata:
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
124     else:
125       run_isolated.link_file(outfile, infile, action)
126
127
128 ### Variable stuff.
129
130
131 def _normalize_path_variable(cwd, relative_base_dir, key, value):
132   """Normalizes a path variable into a relative directory.
133   """
134   # Variables could contain / or \ on windows. Always normalize to
135   # os.path.sep.
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))
140
141   # All variables are relative to the .isolate file.
142   normalized = os.path.relpath(normalized, relative_base_dir)
143   logging.debug(
144       'Translated variable %s from %s to %s', key, value, normalized)
145   return normalized
146
147
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.
150
151   For each 'path' variable: first normalizes it based on |cwd|, verifies it
152   exists then sets it as relative to relative_base_dir.
153   """
154   logging.info(
155       'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
156       relative_base_dir)
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)
160   return dict(
161       (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
162       for k, v in path_variables.iteritems())
163
164
165 ### Internal state files.
166
167
168 def isolatedfile_to_state(filename):
169   """For a '.isolate' file, returns the path to the saved '.state' file."""
170   return filename + '.state'
171
172
173 def classify_files(root_dir, tracked, untracked):
174   """Converts the list of files into a .isolate 'variables' dictionary.
175
176   Arguments:
177   - tracked: list of files names to generate a dictionary out of that should
178              probably be tracked.
179   - untracked: list of files names that must not be tracked.
180   """
181   new_tracked = []
182   new_untracked = list(untracked)
183
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.
187     """
188     if filepath.endswith('/'):
189       return False
190     if ' ' in filepath:
191       return False
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]))):
196         return False
197     return True
198
199   for filepath in sorted(tracked):
200     if should_be_tracked(filepath):
201       new_tracked.append(filepath)
202     else:
203       # Anything else.
204       new_untracked.append(filepath)
205
206   variables = {}
207   if new_tracked:
208     variables[isolate_format.KEY_TRACKED] = sorted(new_tracked)
209   if new_untracked:
210     variables[isolate_format.KEY_UNTRACKED] = sorted(new_untracked)
211   return variables
212
213
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)
223     return None
224
225   EXECUTABLE = re.compile(
226       r'^(\<\(PRODUCT_DIR\)\/[^\/\.]+)' +
227       re.escape(variables.get('EXECUTABLE_SUFFIX', '')) +
228       r'$')
229   match = EXECUTABLE.match(f)
230   if match:
231     return match.group(1) + '<(EXECUTABLE_SUFFIX)'
232
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.
241     OSX_BUNDLES = (
242       '<(PRODUCT_DIR)/Chromium Framework.framework/',
243       '<(PRODUCT_DIR)/Chromium.app/',
244       '<(PRODUCT_DIR)/Google Chrome Framework.framework/',
245       '<(PRODUCT_DIR)/Google Chrome.app/',
246     )
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
250         # remove duplicates.
251         return prefix
252   return f
253
254
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.
259
260   Cleans up and extracts only files from within root_dir then processes
261   variables and relative_cwd.
262   """
263   root_dir = os.path.realpath(root_dir)
264   logging.info(
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))
268
269   # Preparation work.
270   relative_cwd = file_path.cleanup_path(relative_cwd)
271   assert not os.path.isabs(relative_cwd), relative_cwd
272
273   # Normalizes to posix path. .isolate files are using posix paths on all OSes
274   # for coherency.
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)
280
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.
291
292   root_dir_posix = root_dir.replace(os.path.sep, '/')
293   def fix(f):
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 './'
307
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)
314           break
315     return f
316
317   def fix_all(items):
318     """Reduces the items to convert variables, removes unneeded items, apply
319     chromium-specific fixes and only return unique items.
320     """
321     variables_converted = (fix(f.path) for f in items)
322     chromium_fixed = (
323         chromium_fix(f, total_variables) for f in variables_converted)
324     return set(f for f in chromium_fixed if f)
325
326   tracked = fix_all(tracked)
327   untracked = fix_all(untracked)
328   touched = fix_all(touched)
329   out = classify_files(root_dir, tracked, untracked)
330   if touched:
331     out[isolate_format.KEY_TOUCHED] = sorted(touched)
332   return out
333
334
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()
347
348
349 def chromium_save_isolated(isolated, data, path_variables, algo):
350   """Writes one or many .isolated files.
351
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.
355   """
356   slaves = []
357
358   def extract_into_included_isolated(prefix):
359     new_slave = {
360       'algo': data['algo'],
361       'files': {},
362       'version': data['version'],
363     }
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)
369
370   # Split test/data/ in its own .isolated file.
371   extract_into_included_isolated(os.path.join('test', 'data', ''))
372
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'])
376
377   files = []
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))
384
385   files.extend(isolateserver.save_isolated(isolated, data))
386   return files
387
388
389 class Flattenable(object):
390   """Represents data that can be represented as a json file."""
391   MEMBERS = ()
392
393   def flatten(self):
394     """Returns a json-serializable version of itself.
395
396     Skips None entries.
397     """
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)
400
401   @classmethod
402   def load(cls, data, *args, **kwargs):
403     """Loads a flattened version."""
404     data = data.copy()
405     out = cls(*args, **kwargs)
406     for member in out.MEMBERS:
407       if member in data:
408         # Access to a protected member XXX of a client class
409         # pylint: disable=W0212
410         out._load_member(member, data.pop(member))
411     if data:
412       raise ValueError(
413           'Found unexpected entry %s while constructing an object %s' %
414             (data, cls.__name__), data, cls.__name__)
415     return out
416
417   def _load_member(self, member, value):
418     """Loads a member into self."""
419     setattr(self, member, value)
420
421   @classmethod
422   def load_file(cls, filename, *args, **kwargs):
423     """Loads the data from a file or return an empty instance."""
424     try:
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)
431     return out
432
433
434 class SavedState(Flattenable):
435   """Describes the content of a .state file.
436
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.
440
441   It is important to note that the 'files' dict keys are using native OS path
442   separator instead of '/' used in .isolate file.
443   """
444   MEMBERS = (
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 ...
447     # "creative".
448     'OS',
449     # Algorithm used to generate the hash. The only supported value is at the
450     # time of writting 'sha-1'.
451     'algo',
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
454     # command safely.
455     'command',
456     # GYP variables that are used to generate conditions. The most frequent
457     # example is 'OS'.
458     'config_variables',
459     # GYP variables that will be replaced in 'command' and paths but will not be
460     # considered a relative directory.
461     'extra_variables',
462     # Cache of the files found so the next run can skip hash calculation.
463     'files',
464     # Path of the original .isolate file. Relative path to isolated_basedir.
465     'isolate_file',
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.
470     'read_only',
471     # Relative cwd to use to start the command.
472     'relative_cwd',
473     # GYP variables used to generate the .isolated files paths based on path
474     # variables. Frequent examples are DEPTH and PRODUCT_DIR.
475     'path_variables',
476     # Version of the file format in format 'major.minor'. Any non-breaking
477     # change must update minor. Any breaking change must update major.
478     'version',
479   )
480
481   def __init__(self, isolated_basedir):
482     """Creates an empty SavedState.
483
484     Arguments:
485       isolated_basedir: the directory where the .isolated and .isolated.state
486           files are saved.
487     """
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
492
493     # The default algorithm used.
494     self.OS = sys.platform
495     self.algo = isolateserver.SUPPORTED_ALGOS['sha-1']
496     self.child_isolated_files = []
497     self.command = []
498     self.config_variables = {}
499     self.extra_variables = {}
500     self.files = {}
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
506
507   def update(
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.
511     """
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
515     # path.
516     isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
517
518     # The same .isolate file should always be used to generate the .isolated and
519     # .isolated.state.
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)
526
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.
529
530     The new files in |infiles| are added to self.files dict but their hash is
531     not calculated here.
532     """
533     self.command = command
534     # Add new files.
535     for f in infiles:
536       self.files.setdefault(f, {})
537     for f in touched:
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)):
541       del self.files[f]
542     if read_only is not None:
543       self.read_only = read_only
544     self.relative_cwd = relative_cwd
545
546   def to_isolated(self):
547     """Creates a .isolated dictionary out of the saved state.
548
549     https://code.google.com/p/swarming/wiki/IsolatedDesign
550     """
551     def strip(data):
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)
554
555     out = {
556       'algo': isolateserver.SUPPORTED_ALGOS_REVERSE[self.algo],
557       'files': dict(
558           (filepath, strip(data)) for filepath, data in self.files.iteritems()),
559       'version': self.version,
560     }
561     if self.command:
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
567     return out
568
569   @property
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))
574
575   # Arguments number differs from overridden method
576   @classmethod
577   def load(cls, data, isolated_basedir):  # pylint: disable=W0221
578     """Special case loading to disallow different OS.
579
580     It is not possible to load a .isolated.state files from a different OS, this
581     file is saved in OS-specific format.
582     """
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'))
586
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]
592
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)
601
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
604     # later.
605     if out.isolate_file and not os.path.isfile(out.isolate_filepath):
606       out.isolate_file = None
607     if out.isolate_file:
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
613     return out
614
615   def flatten(self):
616     """Makes sure 'algo' is in human readable form."""
617     out = super(SavedState, self).flatten()
618     out['algo'] = isolateserver.SUPPORTED_ALGOS_REVERSE[out['algo']]
619     return out
620
621   def __str__(self):
622     def dict_to_str(d):
623       return ''.join('\n    %s=%s' % (k, d[k]) for k in sorted(d))
624
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)
635     return out
636
637
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
645     # necessary.
646     self.saved_state = saved_state
647
648   @classmethod
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)
653     return cls(
654         isolated_filepath,
655         SavedState.load_file(
656             isolatedfile_to_state(isolated_filepath), isolated_basedir))
657
658   def load_isolate(
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
662     .isolate file.
663
664     Processes the loaded data, deduce root_dir, relative_cwd.
665     """
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)
669     logging.info(
670         'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
671         cwd, isolate_file, path_variables, config_variables, extra_variables,
672         ignore_broken_items)
673     relative_base_dir = os.path.dirname(isolate_file)
674
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
682
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))
690
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)
694     command = [
695         isolate_format.eval_variables(i, total_variables) for i in command
696     ]
697
698     total_variables = self.saved_state.path_variables.copy()
699     total_variables.update(self.saved_state.extra_variables)
700     infiles = [
701         isolate_format.eval_variables(f, total_variables) for f in infiles
702     ]
703     touched = [
704         isolate_format.eval_variables(f, total_variables) for f in touched
705     ]
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,
714     # isolate_base_dir.
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
717     # inside it.
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'
723             % (k, v, root_dir))
724     # Normalize the files based to root_dir. It is important to keep the
725     # trailing os.path.sep at that step.
726     infiles = [
727       file_path.relpath(
728           file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
729       for f in infiles
730     ]
731     touched = [
732       file_path.relpath(
733           file_path.normpath(os.path.join(relative_base_dir, f)), root_dir)
734       for f in touched
735     ]
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(
740         root_dir,
741         infiles,
742         lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
743         follow_symlinks,
744         ignore_broken_items)
745
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)]
750
751       if len(touched) != original_touched_count:
752         logging.info('Removed %d invalid touched entries',
753                      len(touched) - original_touched_count)
754
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)
759     logging.debug(self)
760
761   def process_inputs(self, subdir):
762     """Updates self.saved_state.files with the files' mode and hash.
763
764     If |subdir| is specified, filters to a subdirectory. The resulting .isolated
765     file is tainted.
766
767     See isolateserver.process_input() for more information.
768     """
769     for infile in sorted(self.saved_state.files):
770       if subdir and not infile.startswith(subdir):
771         self.saved_state.files.pop(infile)
772       else:
773         filepath = os.path.join(self.root_dir, infile)
774         self.saved_state.files[infile] = isolateserver.process_input(
775             filepath,
776             self.saved_state.files[infile],
777             self.saved_state.read_only,
778             self.saved_state.algo)
779
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)
788     total_bytes = sum(
789         i.get('s', 0) for i in self.saved_state.files.itervalues())
790     if total_bytes:
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)
796
797   @property
798   def root_dir(self):
799     """Returns the absolute path of the root_dir to reference the .isolate file
800     via relative_cwd.
801
802     So that join(root_dir, relative_cwd, basename(isolate_file)) is equivalent
803     to isolate_filepath.
804     """
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)
808     # Special case '.'.
809     if self.saved_state.relative_cwd == '.':
810       root_dir = isolate_dir
811     else:
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)
820
821   @property
822   def resultdir(self):
823     """Returns the absolute path containing the .isolated file.
824
825     It is usually equivalent to the variable PRODUCT_DIR. Uses the .isolated
826     path as the value.
827     """
828     return os.path.dirname(self.isolated_filepath)
829
830   def __str__(self):
831     def indent(data, indent_length):
832       """Indents text."""
833       spacing = ' ' * indent_length
834       return ''.join(spacing + l for l in str(data).splitlines(True))
835
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)
839     return out
840
841
842 def load_complete_state(options, cwd, subdir, skip_update):
843   """Loads a CompleteState.
844
845   This includes data from .isolate and .isolated.state files. Never reads the
846   .isolated file.
847
848   Arguments:
849     options: Options instance generated with OptionParserIsolate. For either
850              options.isolate and options.isolated, if the value is set, it is an
851              absolute path.
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.
857   """
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))
861   if options.isolated:
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)
865   else:
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()))
870
871   if not options.isolate:
872     if not complete_state.saved_state.isolate_file:
873       if not skip_update:
874         raise ExecutionError('A .isolate file is required.')
875       isolate = None
876     else:
877       isolate = complete_state.saved_state.isolate_filepath
878   else:
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.
886         logging.warning(
887             '--isolated %s != %s as saved in %s. Discarding saved state',
888             rel_isolate,
889             complete_state.saved_state.isolate_file,
890             isolatedfile_to_state(options.isolated))
891         complete_state = CompleteState(
892             options.isolated,
893             SavedState(complete_state.saved_state.isolated_basedir))
894
895   if not skip_update:
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)
900
901   # Regenerate complete_state.saved_state.files.
902   if subdir:
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(
908         (k,
909           os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
910             v)))
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)
914
915   if not skip_update:
916     complete_state.process_inputs(subdir)
917   return complete_state
918
919
920 def read_trace_as_isolate_dict(complete_state, trace_blacklist):
921   """Reads a trace and returns the .isolate dictionary.
922
923   Returns exceptions during the log parsing so it can be re-raised.
924   """
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)
930   try:
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(
938         tracked,
939         [],
940         touched,
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,
946         trace_blacklist)
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)))
952
953
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)
958
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(
964       isolate_dir,
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)
973   if exceptions:
974     # It got an exception, raise the first one.
975     raise \
976         exceptions[0][0], \
977         exceptions[0][1], \
978         exceptions[0][2]
979
980
981 def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
982   """Creates a isolated tree usable for test execution.
983
984   Returns the current working directory where the isolated command should be
985   started in.
986   """
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
992   else:
993     action = run_isolated.HARDLINK_WITH_FALLBACK
994
995   recreate_tree(
996       outdir=outdir,
997       indir=root_dir,
998       infiles=files,
999       action=action,
1000       as_hash=False)
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
1005     # directory.
1006     os.makedirs(cwd)
1007   run_isolated.change_tree_read_only(outdir, read_only)
1008   return cwd
1009
1010
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()
1019
1020   infiles = complete_state.saved_state.files
1021   # Add all the .isolated files.
1022   isolated_hash = []
1023   isolated_files = [
1024     options.isolated,
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:
1032       content = f.read()
1033     isolated_hash.append(
1034         complete_state.saved_state.algo(content).hexdigest())
1035     isolated_metadata = {
1036       'h': isolated_hash[-1],
1037       's': len(content),
1038       'priority': '0'
1039     }
1040     infiles[item_path] = isolated_metadata
1041   return complete_state, infiles, isolated_hash
1042
1043
1044 ### Commands.
1045
1046
1047 def CMDarchive(parser, args):
1048   """Creates a .isolated file and uploads the tree to an isolate server.
1049
1050   All the files listed in the .isolated file are put in the isolate server
1051   cache via isolateserver.py.
1052   """
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)
1059   if args:
1060     parser.error('Unsupported argument: %s' % args)
1061   cwd = os.getcwd()
1062   with tools.Profiler('GenerateHashtable'):
1063     success = False
1064     try:
1065       complete_state, infiles, isolated_hash = prepare_for_archival(
1066           options, cwd)
1067       logging.info('Creating content addressed object store with %d item',
1068                    len(infiles))
1069
1070       isolateserver.upload_tree(
1071           base_url=options.isolate_server,
1072           indir=complete_state.root_dir,
1073           infiles=infiles,
1074           namespace=options.namespace)
1075       success = True
1076       print('%s  %s' % (isolated_hash[0], os.path.basename(options.isolated)))
1077     finally:
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)
1083
1084
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)
1089   if args:
1090     parser.error('Unsupported argument: %s' % args)
1091
1092   complete_state = load_complete_state(
1093       options, os.getcwd(), options.subdir, False)
1094
1095   # Nothing is done specifically. Just store the result and state.
1096   complete_state.save_files()
1097   return 0
1098
1099
1100 def CMDhashtable(parser, args):
1101   """Creates a .isolated file and stores the contains in a directory.
1102
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.
1106   """
1107   add_subdir_option(parser)
1108   isolateserver.add_outdir_options(parser)
1109   add_skip_refresh_option(parser)
1110   options, args = parser.parse_args(args)
1111   if args:
1112     parser.error('Unsupported argument: %s' % args)
1113   cwd = os.getcwd()
1114   isolateserver.process_outdir_options(parser, options, cwd)
1115
1116   success = False
1117   try:
1118     complete_state, infiles, isolated_hash = prepare_for_archival(options, cwd)
1119     logging.info('Creating content addressed object store with %d item',
1120                   len(infiles))
1121     if not os.path.isdir(options.outdir):
1122       os.makedirs(options.outdir)
1123
1124     # TODO(maruel): Make the files read-only?
1125     recreate_tree(
1126         outdir=options.outdir,
1127         indir=complete_state.root_dir,
1128         infiles=infiles,
1129         action=run_isolated.HARDLINK_WITH_FALLBACK,
1130         as_hash=True)
1131     success = True
1132     print('%s  %s' % (isolated_hash[0], os.path.basename(options.isolated)))
1133   finally:
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)
1139
1140
1141 def CMDmerge(parser, args):
1142   """Reads and merges the data from the trace back into the original .isolate.
1143   """
1144   parser.require_isolated = False
1145   add_trace_option(parser)
1146   options, args = parser.parse_args(args)
1147   if args:
1148     parser.error('Unsupported argument: %s' % args)
1149
1150   complete_state = load_complete_state(options, os.getcwd(), None, False)
1151   blacklist = tools.gen_blacklist(options.trace_blacklist)
1152   merge(complete_state, blacklist)
1153   return 0
1154
1155
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)
1161   parser.add_option(
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)
1165   if args:
1166     parser.error('Unsupported argument: %s' % args)
1167
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)
1172   if options.merge:
1173     merge(complete_state, blacklist)
1174   else:
1175     isolate_format.pretty_print(value, sys.stdout)
1176
1177   if exceptions:
1178     # It got an exception, raise the first one.
1179     raise \
1180         exceptions[0][0], \
1181         exceptions[0][1], \
1182         exceptions[0][2]
1183   return 0
1184
1185
1186 def CMDremap(parser, args):
1187   """Creates a directory with all the dependencies mapped into it.
1188
1189   Useful to test manually why a test is failing. The target executable is not
1190   run.
1191   """
1192   parser.require_isolated = False
1193   isolateserver.add_outdir_options(parser)
1194   add_skip_refresh_option(parser)
1195   options, args = parser.parse_args(args)
1196   if args:
1197     parser.error('Unsupported argument: %s' % args)
1198   cwd = os.getcwd()
1199   isolateserver.process_outdir_options(parser, options, cwd)
1200   complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
1201
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')
1207
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()
1214   return 0
1215
1216
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)
1221   if args:
1222     parser.error('Unsupported argument: %s' % args)
1223
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
1228   else:
1229     isolate = options.isolate
1230   if not isolate:
1231     parser.error('--isolate is required.')
1232
1233   with open(isolate, 'r') as f:
1234     content = f.read()
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)
1243   return 0
1244
1245
1246 @subcommand.usage('-- [extra arguments]')
1247 def CMDrun(parser, args):
1248   """Runs the test executable in an isolated (temporary) directory.
1249
1250   All the dependencies are mapped into the temporary directory and the
1251   directory is cleaned up after the target exits.
1252
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
1256   """
1257   parser.require_isolated = False
1258   add_skip_refresh_option(parser)
1259   options, args = parser.parse_args(args)
1260
1261   complete_state = load_complete_state(
1262       options, os.getcwd(), None, options.skip_refresh)
1263   cmd = complete_state.saved_state.command + args
1264   if not cmd:
1265     raise ExecutionError('No command to run.')
1266   cmd = tools.fix_python_path(cmd)
1267
1268   outdir = run_isolated.make_temp_dir(
1269       'isolate-%s' % datetime.date.today(),
1270       os.path.dirname(complete_state.root_dir))
1271   try:
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)
1279   finally:
1280     run_isolated.rmtree(outdir)
1281
1282   if complete_state.isolated_filepath:
1283     complete_state.save_files()
1284   return result
1285
1286
1287 @subcommand.usage('-- [extra arguments]')
1288 def CMDtrace(parser, args):
1289   """Traces the target using trace_inputs.py.
1290
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
1294   stdout.
1295
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
1299   """
1300   add_trace_option(parser)
1301   parser.add_option(
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)
1306
1307   complete_state = load_complete_state(
1308       options, os.getcwd(), None, options.skip_refresh)
1309   cmd = complete_state.saved_state.command + args
1310   if not cmd:
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)
1324   out = None
1325   try:
1326     with api.get_tracer(logfile) as tracer:
1327       result, out = tracer.trace(
1328           cmd,
1329           cwd,
1330           'default',
1331           True)
1332   except trace_inputs.TracingFailure, e:
1333     raise ExecutionError('Tracing failed for: %s\n%s' % (' '.join(cmd), str(e)))
1334
1335   if result:
1336     logging.error(
1337         'Tracer exited with %d, which means the tests probably failed so the '
1338         'trace is probably incomplete.', result)
1339     logging.info(out)
1340
1341   complete_state.save_files()
1342
1343   if options.merge:
1344     blacklist = tools.gen_blacklist(options.trace_blacklist)
1345     merge(complete_state, blacklist)
1346
1347   return result
1348
1349
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)
1357   if '=' in k:
1358     k, v = k.split('=', 1)
1359   else:
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')))
1369
1370
1371 def add_variable_option(parser):
1372   """Adds --isolated and --<foo>-variable to an OptionParser."""
1373   parser.add_option(
1374       '-s', '--isolated',
1375       metavar='FILE',
1376       help='.isolated file to generate or read')
1377   # Keep for compatibility. TODO(maruel): Remove once not used anymore.
1378   parser.add_option(
1379       '-r', '--result',
1380       dest='isolated',
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
1388   #   reduce.
1389   # - unrelated variables that are used as command flags for example.
1390   parser.add_option(
1391       '--config-variable',
1392       action='callback',
1393       callback=_process_variable_arg,
1394       default=[],
1395       dest='config_variables',
1396       metavar='FOO BAR',
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')
1401   parser.add_option(
1402       '--path-variable',
1403       action='callback',
1404       callback=_process_variable_arg,
1405       default=[],
1406       dest='path_variables',
1407       metavar='FOO BAR',
1408       help='Path variables are used to replace file paths when loading a '
1409            '.isolate file, default: %default')
1410   parser.add_option(
1411       '--extra-variable',
1412       action='callback',
1413       callback=_process_variable_arg,
1414       default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1415       dest='extra_variables',
1416       metavar='FOO BAR',
1417       help='Extraneous variables are replaced on the \'command\' entry and on '
1418            'paths in the .isolate file but are not considered relative paths.')
1419
1420
1421 def add_subdir_option(parser):
1422   parser.add_option(
1423       '--subdir',
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.')
1428
1429
1430 def add_trace_option(parser):
1431   """Adds --trace-blacklist to the parser."""
1432   parser.add_option(
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 '
1437            'test case.')
1438
1439
1440 def add_skip_refresh_option(parser):
1441   parser.add_option(
1442       '--skip-refresh', action='store_true',
1443       help='Skip reading .isolate file and do not refresh the hash of '
1444            'dependencies')
1445
1446
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\'')
1456
1457
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."""
1464     try:
1465       return int(s)
1466     except ValueError:
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)
1472
1473
1474 class OptionParserIsolate(tools.OptionParserWithLogging):
1475   """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
1476   """
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
1480
1481   def __init__(self, **kwargs):
1482     tools.OptionParserWithLogging.__init__(
1483         self,
1484         verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1485         **kwargs)
1486     group = optparse.OptionGroup(self, "Common options")
1487     group.add_option(
1488         '-i', '--isolate',
1489         metavar='FILE',
1490         help='.isolate file to load the dependency data from')
1491     add_variable_option(group)
1492     group.add_option(
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)
1499
1500   def parse_args(self, *args, **kwargs):
1501     """Makes sure the paths make sense.
1502
1503     On Windows, / and \ are often mixed together in a path.
1504     """
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)
1509
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)
1513
1514     if options.isolate:
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)
1520
1521     return options, args
1522
1523
1524 def main(argv):
1525   dispatcher = subcommand.CommandDispatcher(__name__)
1526   try:
1527     return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
1528   except Exception as e:
1529     tools.report_error(e)
1530     return 1
1531
1532
1533 if __name__ == '__main__':
1534   fix_encoding.fix_encoding()
1535   tools.disable_buffering()
1536   colorama.init()
1537   sys.exit(main(sys.argv[1:]))