Upstream version 10.39.225.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 __version__ = '0.4'
17
18 import datetime
19 import logging
20 import optparse
21 import os
22 import re
23 import subprocess
24 import sys
25
26 import auth
27 import isolate_format
28 import isolated_format
29 import isolateserver
30 import run_isolated
31
32 from third_party import colorama
33 from third_party.depot_tools import fix_encoding
34 from third_party.depot_tools import subcommand
35
36 from utils import file_path
37 from utils import tools
38
39
40 class ExecutionError(Exception):
41   """A generic error occurred."""
42   def __str__(self):
43     return self.args[0]
44
45
46 ### Path handling code.
47
48
49 def recreate_tree(outdir, indir, infiles, action, as_hash):
50   """Creates a new tree with only the input files in it.
51
52   Arguments:
53     outdir:    Output directory to create the files in.
54     indir:     Root directory the infiles are based in.
55     infiles:   dict of files to map from |indir| to |outdir|.
56     action:    One of accepted action of run_isolated.link_file().
57     as_hash:   Output filename is the hash instead of relfile.
58   """
59   logging.info(
60       'recreate_tree(outdir=%s, indir=%s, files=%d, action=%s, as_hash=%s)' %
61       (outdir, indir, len(infiles), action, as_hash))
62
63   assert os.path.isabs(outdir) and outdir == os.path.normpath(outdir), outdir
64   if not os.path.isdir(outdir):
65     logging.info('Creating %s' % outdir)
66     os.makedirs(outdir)
67
68   for relfile, metadata in infiles.iteritems():
69     infile = os.path.join(indir, relfile)
70     if as_hash:
71       # Do the hashtable specific checks.
72       if 'l' in metadata:
73         # Skip links when storing a hashtable.
74         continue
75       outfile = os.path.join(outdir, metadata['h'])
76       if os.path.isfile(outfile):
77         # Just do a quick check that the file size matches. No need to stat()
78         # again the input file, grab the value from the dict.
79         if not 's' in metadata:
80           raise isolated_format.MappingError(
81               'Misconfigured item %s: %s' % (relfile, metadata))
82         if metadata['s'] == os.stat(outfile).st_size:
83           continue
84         else:
85           logging.warn('Overwritting %s' % metadata['h'])
86           os.remove(outfile)
87     else:
88       outfile = os.path.join(outdir, relfile)
89       outsubdir = os.path.dirname(outfile)
90       if not os.path.isdir(outsubdir):
91         os.makedirs(outsubdir)
92
93     # TODO(csharp): Fix crbug.com/150823 and enable the touched logic again.
94     # if metadata.get('T') == True:
95     #   open(outfile, 'ab').close()
96     if 'l' in metadata:
97       pointed = metadata['l']
98       logging.debug('Symlink: %s -> %s' % (outfile, pointed))
99       # symlink doesn't exist on Windows.
100       os.symlink(pointed, outfile)  # pylint: disable=E1101
101     else:
102       run_isolated.link_file(outfile, infile, action)
103
104
105 ### Variable stuff.
106
107
108 def _normalize_path_variable(cwd, relative_base_dir, key, value):
109   """Normalizes a path variable into a relative directory.
110   """
111   # Variables could contain / or \ on windows. Always normalize to
112   # os.path.sep.
113   x = os.path.join(cwd, value.strip().replace('/', os.path.sep))
114   normalized = file_path.get_native_path_case(os.path.normpath(x))
115   if not os.path.isdir(normalized):
116     raise ExecutionError('%s=%s is not a directory' % (key, normalized))
117
118   # All variables are relative to the .isolate file.
119   normalized = os.path.relpath(normalized, relative_base_dir)
120   logging.debug(
121       'Translated variable %s from %s to %s', key, value, normalized)
122   return normalized
123
124
125 def normalize_path_variables(cwd, path_variables, relative_base_dir):
126   """Processes path variables as a special case and returns a copy of the dict.
127
128   For each 'path' variable: first normalizes it based on |cwd|, verifies it
129   exists then sets it as relative to relative_base_dir.
130   """
131   logging.info(
132       'normalize_path_variables(%s, %s, %s)', cwd, path_variables,
133       relative_base_dir)
134   assert isinstance(cwd, unicode), cwd
135   assert isinstance(relative_base_dir, unicode), relative_base_dir
136   relative_base_dir = file_path.get_native_path_case(relative_base_dir)
137   return dict(
138       (k, _normalize_path_variable(cwd, relative_base_dir, k, v))
139       for k, v in path_variables.iteritems())
140
141
142 ### Internal state files.
143
144
145 def isolatedfile_to_state(filename):
146   """For a '.isolate' file, returns the path to the saved '.state' file."""
147   return filename + '.state'
148
149
150 def chromium_save_isolated(isolated, data, path_variables, algo):
151   """Writes one or many .isolated files.
152
153   This slightly increases the cold cache cost but greatly reduce the warm cache
154   cost by splitting low-churn files off the master .isolated file. It also
155   reduces overall isolateserver memcache consumption.
156   """
157   slaves = []
158
159   def extract_into_included_isolated(prefix):
160     new_slave = {
161       'algo': data['algo'],
162       'files': {},
163       'version': data['version'],
164     }
165     for f in data['files'].keys():
166       if f.startswith(prefix):
167         new_slave['files'][f] = data['files'].pop(f)
168     if new_slave['files']:
169       slaves.append(new_slave)
170
171   # Split test/data/ in its own .isolated file.
172   extract_into_included_isolated(os.path.join('test', 'data', ''))
173
174   # Split everything out of PRODUCT_DIR in its own .isolated file.
175   if path_variables.get('PRODUCT_DIR'):
176     extract_into_included_isolated(path_variables['PRODUCT_DIR'])
177
178   files = []
179   for index, f in enumerate(slaves):
180     slavepath = isolated[:-len('.isolated')] + '.%d.isolated' % index
181     tools.write_json(slavepath, f, True)
182     data.setdefault('includes', []).append(
183         isolated_format.hash_file(slavepath, algo))
184     files.append(os.path.basename(slavepath))
185
186   files.extend(isolated_format.save_isolated(isolated, data))
187   return files
188
189
190 class Flattenable(object):
191   """Represents data that can be represented as a json file."""
192   MEMBERS = ()
193
194   def flatten(self):
195     """Returns a json-serializable version of itself.
196
197     Skips None entries.
198     """
199     items = ((member, getattr(self, member)) for member in self.MEMBERS)
200     return dict((member, value) for member, value in items if value is not None)
201
202   @classmethod
203   def load(cls, data, *args, **kwargs):
204     """Loads a flattened version."""
205     data = data.copy()
206     out = cls(*args, **kwargs)
207     for member in out.MEMBERS:
208       if member in data:
209         # Access to a protected member XXX of a client class
210         # pylint: disable=W0212
211         out._load_member(member, data.pop(member))
212     if data:
213       raise ValueError(
214           'Found unexpected entry %s while constructing an object %s' %
215             (data, cls.__name__), data, cls.__name__)
216     return out
217
218   def _load_member(self, member, value):
219     """Loads a member into self."""
220     setattr(self, member, value)
221
222   @classmethod
223   def load_file(cls, filename, *args, **kwargs):
224     """Loads the data from a file or return an empty instance."""
225     try:
226       out = cls.load(tools.read_json(filename), *args, **kwargs)
227       logging.debug('Loaded %s(%s)', cls.__name__, filename)
228     except (IOError, ValueError) as e:
229       # On failure, loads the default instance.
230       out = cls(*args, **kwargs)
231       logging.warn('Failed to load %s: %s', filename, e)
232     return out
233
234
235 class SavedState(Flattenable):
236   """Describes the content of a .state file.
237
238   This file caches the items calculated by this script and is used to increase
239   the performance of the script. This file is not loaded by run_isolated.py.
240   This file can always be safely removed.
241
242   It is important to note that the 'files' dict keys are using native OS path
243   separator instead of '/' used in .isolate file.
244   """
245   MEMBERS = (
246     # Value of sys.platform so that the file is rejected if loaded from a
247     # different OS. While this should never happen in practice, users are ...
248     # "creative".
249     'OS',
250     # Algorithm used to generate the hash. The only supported value is at the
251     # time of writting 'sha-1'.
252     'algo',
253     # List of included .isolated files. Used to support/remember 'slave'
254     # .isolated files. Relative path to isolated_basedir.
255     'child_isolated_files',
256     # Cache of the processed command. This value is saved because .isolated
257     # files are never loaded by isolate.py so it's the only way to load the
258     # command safely.
259     'command',
260     # GYP variables that are used to generate conditions. The most frequent
261     # example is 'OS'.
262     'config_variables',
263     # GYP variables that will be replaced in 'command' and paths but will not be
264     # considered a relative directory.
265     'extra_variables',
266     # Cache of the files found so the next run can skip hash calculation.
267     'files',
268     # Path of the original .isolate file. Relative path to isolated_basedir.
269     'isolate_file',
270     # GYP variables used to generate the .isolated files paths based on path
271     # variables. Frequent examples are DEPTH and PRODUCT_DIR.
272     'path_variables',
273     # If the generated directory tree should be read-only.
274     'read_only',
275     # Relative cwd to use to start the command.
276     'relative_cwd',
277     # Root directory the files are mapped from.
278     'root_dir',
279     # Version of the saved state file format. Any breaking change must update
280     # the value.
281     'version',
282   )
283
284   # Bump this version whenever the saved state changes. It is also keyed on the
285   # .isolated file version so any change in the generator will invalidate .state
286   # files.
287   EXPECTED_VERSION = isolated_format.ISOLATED_FILE_VERSION + '.2'
288
289   def __init__(self, isolated_basedir):
290     """Creates an empty SavedState.
291
292     Arguments:
293       isolated_basedir: the directory where the .isolated and .isolated.state
294           files are saved.
295     """
296     super(SavedState, self).__init__()
297     assert os.path.isabs(isolated_basedir), isolated_basedir
298     assert os.path.isdir(isolated_basedir), isolated_basedir
299     self.isolated_basedir = isolated_basedir
300
301     # The default algorithm used.
302     self.OS = sys.platform
303     self.algo = isolated_format.SUPPORTED_ALGOS['sha-1']
304     self.child_isolated_files = []
305     self.command = []
306     self.config_variables = {}
307     self.extra_variables = {}
308     self.files = {}
309     self.isolate_file = None
310     self.path_variables = {}
311     self.read_only = None
312     self.relative_cwd = None
313     self.root_dir = None
314     self.version = self.EXPECTED_VERSION
315
316   def update_config(self, config_variables):
317     """Updates the saved state with only config variables."""
318     self.config_variables.update(config_variables)
319
320   def update(self, isolate_file, path_variables, extra_variables):
321     """Updates the saved state with new data to keep GYP variables and internal
322     reference to the original .isolate file.
323     """
324     assert os.path.isabs(isolate_file)
325     # Convert back to a relative path. On Windows, if the isolate and
326     # isolated files are on different drives, isolate_file will stay an absolute
327     # path.
328     isolate_file = file_path.safe_relpath(isolate_file, self.isolated_basedir)
329
330     # The same .isolate file should always be used to generate the .isolated and
331     # .isolated.state.
332     assert isolate_file == self.isolate_file or not self.isolate_file, (
333         isolate_file, self.isolate_file)
334     self.extra_variables.update(extra_variables)
335     self.isolate_file = isolate_file
336     self.path_variables.update(path_variables)
337
338   def update_isolated(self, command, infiles, touched, read_only, relative_cwd):
339     """Updates the saved state with data necessary to generate a .isolated file.
340
341     The new files in |infiles| are added to self.files dict but their hash is
342     not calculated here.
343     """
344     self.command = command
345     # Add new files.
346     for f in infiles:
347       self.files.setdefault(f, {})
348     for f in touched:
349       self.files.setdefault(f, {})['T'] = True
350     # Prune extraneous files that are not a dependency anymore.
351     for f in set(self.files).difference(set(infiles).union(touched)):
352       del self.files[f]
353     if read_only is not None:
354       self.read_only = read_only
355     self.relative_cwd = relative_cwd
356
357   def to_isolated(self):
358     """Creates a .isolated dictionary out of the saved state.
359
360     https://code.google.com/p/swarming/wiki/IsolatedDesign
361     """
362     def strip(data):
363       """Returns a 'files' entry with only the whitelisted keys."""
364       return dict((k, data[k]) for k in ('h', 'l', 'm', 's') if k in data)
365
366     out = {
367       'algo': isolated_format.SUPPORTED_ALGOS_REVERSE[self.algo],
368       'files': dict(
369           (filepath, strip(data)) for filepath, data in self.files.iteritems()),
370       # The version of the .state file is different than the one of the
371       # .isolated file.
372       'version': isolated_format.ISOLATED_FILE_VERSION,
373     }
374     if self.command:
375       out['command'] = self.command
376     if self.read_only is not None:
377       out['read_only'] = self.read_only
378     if self.relative_cwd:
379       out['relative_cwd'] = self.relative_cwd
380     return out
381
382   @property
383   def isolate_filepath(self):
384     """Returns the absolute path of self.isolate_file."""
385     return os.path.normpath(
386         os.path.join(self.isolated_basedir, self.isolate_file))
387
388   # Arguments number differs from overridden method
389   @classmethod
390   def load(cls, data, isolated_basedir):  # pylint: disable=W0221
391     """Special case loading to disallow different OS.
392
393     It is not possible to load a .isolated.state files from a different OS, this
394     file is saved in OS-specific format.
395     """
396     out = super(SavedState, cls).load(data, isolated_basedir)
397     if data.get('OS') != sys.platform:
398       raise isolated_format.IsolatedError('Unexpected OS %s', data.get('OS'))
399
400     # Converts human readable form back into the proper class type.
401     algo = data.get('algo')
402     if not algo in isolated_format.SUPPORTED_ALGOS:
403       raise isolated_format.IsolatedError('Unknown algo \'%s\'' % out.algo)
404     out.algo = isolated_format.SUPPORTED_ALGOS[algo]
405
406     # Refuse the load non-exact version, even minor difference. This is unlike
407     # isolateserver.load_isolated(). This is because .isolated.state could have
408     # changed significantly even in minor version difference.
409     if out.version != cls.EXPECTED_VERSION:
410       raise isolated_format.IsolatedError(
411           'Unsupported version \'%s\'' % out.version)
412
413     # The .isolate file must be valid. If it is not present anymore, zap the
414     # value as if it was not noted, so .isolate_file can safely be overriden
415     # later.
416     if out.isolate_file and not os.path.isfile(out.isolate_filepath):
417       out.isolate_file = None
418     if out.isolate_file:
419       # It could be absolute on Windows if the drive containing the .isolate and
420       # the drive containing the .isolated files differ, .e.g .isolate is on
421       # C:\\ and .isolated is on D:\\   .
422       assert not os.path.isabs(out.isolate_file) or sys.platform == 'win32'
423       assert os.path.isfile(out.isolate_filepath), out.isolate_filepath
424     return out
425
426   def flatten(self):
427     """Makes sure 'algo' is in human readable form."""
428     out = super(SavedState, self).flatten()
429     out['algo'] = isolated_format.SUPPORTED_ALGOS_REVERSE[out['algo']]
430     return out
431
432   def __str__(self):
433     def dict_to_str(d):
434       return ''.join('\n    %s=%s' % (k, d[k]) for k in sorted(d))
435
436     out = '%s(\n' % self.__class__.__name__
437     out += '  command: %s\n' % self.command
438     out += '  files: %d\n' % len(self.files)
439     out += '  isolate_file: %s\n' % self.isolate_file
440     out += '  read_only: %s\n' % self.read_only
441     out += '  relative_cwd: %s\n' % self.relative_cwd
442     out += '  child_isolated_files: %s\n' % self.child_isolated_files
443     out += '  path_variables: %s\n' % dict_to_str(self.path_variables)
444     out += '  config_variables: %s\n' % dict_to_str(self.config_variables)
445     out += '  extra_variables: %s\n' % dict_to_str(self.extra_variables)
446     return out
447
448
449 class CompleteState(object):
450   """Contains all the state to run the task at hand."""
451   def __init__(self, isolated_filepath, saved_state):
452     super(CompleteState, self).__init__()
453     assert isolated_filepath is None or os.path.isabs(isolated_filepath)
454     self.isolated_filepath = isolated_filepath
455     # Contains the data to ease developer's use-case but that is not strictly
456     # necessary.
457     self.saved_state = saved_state
458
459   @classmethod
460   def load_files(cls, isolated_filepath):
461     """Loads state from disk."""
462     assert os.path.isabs(isolated_filepath), isolated_filepath
463     isolated_basedir = os.path.dirname(isolated_filepath)
464     return cls(
465         isolated_filepath,
466         SavedState.load_file(
467             isolatedfile_to_state(isolated_filepath), isolated_basedir))
468
469   def load_isolate(
470       self, cwd, isolate_file, path_variables, config_variables,
471       extra_variables, ignore_broken_items):
472     """Updates self.isolated and self.saved_state with information loaded from a
473     .isolate file.
474
475     Processes the loaded data, deduce root_dir, relative_cwd.
476     """
477     # Make sure to not depend on os.getcwd().
478     assert os.path.isabs(isolate_file), isolate_file
479     isolate_file = file_path.get_native_path_case(isolate_file)
480     logging.info(
481         'CompleteState.load_isolate(%s, %s, %s, %s, %s, %s)',
482         cwd, isolate_file, path_variables, config_variables, extra_variables,
483         ignore_broken_items)
484
485     # Config variables are not affected by the paths and must be used to
486     # retrieve the paths, so update them first.
487     self.saved_state.update_config(config_variables)
488
489     with open(isolate_file, 'r') as f:
490       # At that point, variables are not replaced yet in command and infiles.
491       # infiles may contain directory entries and is in posix style.
492       command, infiles, touched, read_only, isolate_cmd_dir = (
493           isolate_format.load_isolate_for_config(
494               os.path.dirname(isolate_file), f.read(),
495               self.saved_state.config_variables))
496
497     # Processes the variables with the new found relative root. Note that 'cwd'
498     # is used when path variables are used.
499     path_variables = normalize_path_variables(
500         cwd, path_variables, isolate_cmd_dir)
501     # Update the rest of the saved state.
502     self.saved_state.update(isolate_file, path_variables, extra_variables)
503
504     total_variables = self.saved_state.path_variables.copy()
505     total_variables.update(self.saved_state.config_variables)
506     total_variables.update(self.saved_state.extra_variables)
507     command = [
508         isolate_format.eval_variables(i, total_variables) for i in command
509     ]
510
511     total_variables = self.saved_state.path_variables.copy()
512     total_variables.update(self.saved_state.extra_variables)
513     infiles = [
514         isolate_format.eval_variables(f, total_variables) for f in infiles
515     ]
516     touched = [
517         isolate_format.eval_variables(f, total_variables) for f in touched
518     ]
519     # root_dir is automatically determined by the deepest root accessed with the
520     # form '../../foo/bar'. Note that path variables must be taken in account
521     # too, add them as if they were input files.
522     self.saved_state.root_dir = isolate_format.determine_root_dir(
523         isolate_cmd_dir, infiles + touched +
524         self.saved_state.path_variables.values())
525     # The relative directory is automatically determined by the relative path
526     # between root_dir and the directory containing the .isolate file,
527     # isolate_base_dir.
528     relative_cwd = os.path.relpath(isolate_cmd_dir, self.saved_state.root_dir)
529     # Now that we know where the root is, check that the path_variables point
530     # inside it.
531     for k, v in self.saved_state.path_variables.iteritems():
532       dest = os.path.join(isolate_cmd_dir, relative_cwd, v)
533       if not file_path.path_starts_with(self.saved_state.root_dir, dest):
534         raise isolated_format.MappingError(
535             'Path variable %s=%r points outside the inferred root directory '
536             '%s; %s'
537             % (k, v, self.saved_state.root_dir, dest))
538     # Normalize the files based to self.saved_state.root_dir. It is important to
539     # keep the trailing os.path.sep at that step.
540     infiles = [
541       file_path.relpath(
542           file_path.normpath(os.path.join(isolate_cmd_dir, f)),
543           self.saved_state.root_dir)
544       for f in infiles
545     ]
546     touched = [
547       file_path.relpath(
548           file_path.normpath(os.path.join(isolate_cmd_dir, f)),
549           self.saved_state.root_dir)
550       for f in touched
551     ]
552     follow_symlinks = sys.platform != 'win32'
553     # Expand the directories by listing each file inside. Up to now, trailing
554     # os.path.sep must be kept. Do not expand 'touched'.
555     infiles = isolated_format.expand_directories_and_symlinks(
556         self.saved_state.root_dir,
557         infiles,
558         lambda x: re.match(r'.*\.(git|svn|pyc)$', x),
559         follow_symlinks,
560         ignore_broken_items)
561
562     # If we ignore broken items then remove any missing touched items.
563     if ignore_broken_items:
564       original_touched_count = len(touched)
565       touched = [touch for touch in touched if os.path.exists(touch)]
566
567       if len(touched) != original_touched_count:
568         logging.info('Removed %d invalid touched entries',
569                      len(touched) - original_touched_count)
570
571     # Finally, update the new data to be able to generate the foo.isolated file,
572     # the file that is used by run_isolated.py.
573     self.saved_state.update_isolated(
574         command, infiles, touched, read_only, relative_cwd)
575     logging.debug(self)
576
577   def files_to_metadata(self, subdir):
578     """Updates self.saved_state.files with the files' mode and hash.
579
580     If |subdir| is specified, filters to a subdirectory. The resulting .isolated
581     file is tainted.
582
583     See isolated_format.file_to_metadata() for more information.
584     """
585     for infile in sorted(self.saved_state.files):
586       if subdir and not infile.startswith(subdir):
587         self.saved_state.files.pop(infile)
588       else:
589         filepath = os.path.join(self.root_dir, infile)
590         self.saved_state.files[infile] = isolated_format.file_to_metadata(
591             filepath,
592             self.saved_state.files[infile],
593             self.saved_state.read_only,
594             self.saved_state.algo)
595
596   def save_files(self):
597     """Saves self.saved_state and creates a .isolated file."""
598     logging.debug('Dumping to %s' % self.isolated_filepath)
599     self.saved_state.child_isolated_files = chromium_save_isolated(
600         self.isolated_filepath,
601         self.saved_state.to_isolated(),
602         self.saved_state.path_variables,
603         self.saved_state.algo)
604     total_bytes = sum(
605         i.get('s', 0) for i in self.saved_state.files.itervalues())
606     if total_bytes:
607       # TODO(maruel): Stats are missing the .isolated files.
608       logging.debug('Total size: %d bytes' % total_bytes)
609     saved_state_file = isolatedfile_to_state(self.isolated_filepath)
610     logging.debug('Dumping to %s' % saved_state_file)
611     tools.write_json(saved_state_file, self.saved_state.flatten(), True)
612
613   @property
614   def root_dir(self):
615     return self.saved_state.root_dir
616
617   def __str__(self):
618     def indent(data, indent_length):
619       """Indents text."""
620       spacing = ' ' * indent_length
621       return ''.join(spacing + l for l in str(data).splitlines(True))
622
623     out = '%s(\n' % self.__class__.__name__
624     out += '  root_dir: %s\n' % self.root_dir
625     out += '  saved_state: %s)' % indent(self.saved_state, 2)
626     return out
627
628
629 def load_complete_state(options, cwd, subdir, skip_update):
630   """Loads a CompleteState.
631
632   This includes data from .isolate and .isolated.state files. Never reads the
633   .isolated file.
634
635   Arguments:
636     options: Options instance generated with OptionParserIsolate. For either
637              options.isolate and options.isolated, if the value is set, it is an
638              absolute path.
639     cwd: base directory to be used when loading the .isolate file.
640     subdir: optional argument to only process file in the subdirectory, relative
641             to CompleteState.root_dir.
642     skip_update: Skip trying to load the .isolate file and processing the
643                  dependencies. It is useful when not needed, like when tracing.
644   """
645   assert not options.isolate or os.path.isabs(options.isolate)
646   assert not options.isolated or os.path.isabs(options.isolated)
647   cwd = file_path.get_native_path_case(unicode(cwd))
648   if options.isolated:
649     # Load the previous state if it was present. Namely, "foo.isolated.state".
650     # Note: this call doesn't load the .isolate file.
651     complete_state = CompleteState.load_files(options.isolated)
652   else:
653     # Constructs a dummy object that cannot be saved. Useful for temporary
654     # commands like 'run'. There is no directory containing a .isolated file so
655     # specify the current working directory as a valid directory.
656     complete_state = CompleteState(None, SavedState(os.getcwd()))
657
658   if not options.isolate:
659     if not complete_state.saved_state.isolate_file:
660       if not skip_update:
661         raise ExecutionError('A .isolate file is required.')
662       isolate = None
663     else:
664       isolate = complete_state.saved_state.isolate_filepath
665   else:
666     isolate = options.isolate
667     if complete_state.saved_state.isolate_file:
668       rel_isolate = file_path.safe_relpath(
669           options.isolate, complete_state.saved_state.isolated_basedir)
670       if rel_isolate != complete_state.saved_state.isolate_file:
671         # This happens if the .isolate file was moved for example. In this case,
672         # discard the saved state.
673         logging.warning(
674             '--isolated %s != %s as saved in %s. Discarding saved state',
675             rel_isolate,
676             complete_state.saved_state.isolate_file,
677             isolatedfile_to_state(options.isolated))
678         complete_state = CompleteState(
679             options.isolated,
680             SavedState(complete_state.saved_state.isolated_basedir))
681
682   if not skip_update:
683     # Then load the .isolate and expands directories.
684     complete_state.load_isolate(
685         cwd, isolate, options.path_variables, options.config_variables,
686         options.extra_variables, options.ignore_broken_items)
687
688   # Regenerate complete_state.saved_state.files.
689   if subdir:
690     subdir = unicode(subdir)
691     # This is tricky here. If it is a path, take it from the root_dir. If
692     # it is a variable, it must be keyed from the directory containing the
693     # .isolate file. So translate all variables first.
694     translated_path_variables = dict(
695         (k,
696           os.path.normpath(os.path.join(complete_state.saved_state.relative_cwd,
697             v)))
698         for k, v in complete_state.saved_state.path_variables.iteritems())
699     subdir = isolate_format.eval_variables(subdir, translated_path_variables)
700     subdir = subdir.replace('/', os.path.sep)
701
702   if not skip_update:
703     complete_state.files_to_metadata(subdir)
704   return complete_state
705
706
707 def create_isolate_tree(outdir, root_dir, files, relative_cwd, read_only):
708   """Creates a isolated tree usable for test execution.
709
710   Returns the current working directory where the isolated command should be
711   started in.
712   """
713   # Forcibly copy when the tree has to be read only. Otherwise the inode is
714   # modified, and this cause real problems because the user's source tree
715   # becomes read only. On the other hand, the cost of doing file copy is huge.
716   if read_only not in (0, None):
717     action = run_isolated.COPY
718   else:
719     action = run_isolated.HARDLINK_WITH_FALLBACK
720
721   recreate_tree(
722       outdir=outdir,
723       indir=root_dir,
724       infiles=files,
725       action=action,
726       as_hash=False)
727   cwd = os.path.normpath(os.path.join(outdir, relative_cwd))
728   if not os.path.isdir(cwd):
729     # It can happen when no files are mapped from the directory containing the
730     # .isolate file. But the directory must exist to be the current working
731     # directory.
732     os.makedirs(cwd)
733   run_isolated.change_tree_read_only(outdir, read_only)
734   return cwd
735
736
737 def prepare_for_archival(options, cwd):
738   """Loads the isolated file and create 'infiles' for archival."""
739   complete_state = load_complete_state(
740       options, cwd, options.subdir, False)
741   # Make sure that complete_state isn't modified until save_files() is
742   # called, because any changes made to it here will propagate to the files
743   # created (which is probably not intended).
744   complete_state.save_files()
745
746   infiles = complete_state.saved_state.files
747   # Add all the .isolated files.
748   isolated_hash = []
749   isolated_files = [
750     options.isolated,
751   ] + complete_state.saved_state.child_isolated_files
752   for item in isolated_files:
753     item_path = os.path.join(
754         os.path.dirname(complete_state.isolated_filepath), item)
755     # Do not use isolated_format.hash_file() here because the file is
756     # likely smallish (under 500kb) and its file size is needed.
757     with open(item_path, 'rb') as f:
758       content = f.read()
759     isolated_hash.append(
760         complete_state.saved_state.algo(content).hexdigest())
761     isolated_metadata = {
762       'h': isolated_hash[-1],
763       's': len(content),
764       'priority': '0'
765     }
766     infiles[item_path] = isolated_metadata
767   return complete_state, infiles, isolated_hash
768
769
770 ### Commands.
771
772
773 def CMDarchive(parser, args):
774   """Creates a .isolated file and uploads the tree to an isolate server.
775
776   All the files listed in the .isolated file are put in the isolate server
777   cache via isolateserver.py.
778   """
779   add_subdir_option(parser)
780   isolateserver.add_isolate_server_options(parser, False)
781   auth.add_auth_options(parser)
782   options, args = parser.parse_args(args)
783   auth.process_auth_options(parser, options)
784   isolateserver.process_isolate_server_options(parser, options)
785   if args:
786     parser.error('Unsupported argument: %s' % args)
787   if file_path.is_url(options.isolate_server):
788     auth.ensure_logged_in(options.isolate_server)
789   cwd = os.getcwd()
790   with tools.Profiler('GenerateHashtable'):
791     success = False
792     try:
793       complete_state, infiles, isolated_hash = prepare_for_archival(
794           options, cwd)
795       logging.info('Creating content addressed object store with %d item',
796                    len(infiles))
797
798       isolateserver.upload_tree(
799           base_url=options.isolate_server,
800           indir=complete_state.root_dir,
801           infiles=infiles,
802           namespace=options.namespace)
803       success = True
804       print('%s  %s' % (isolated_hash[0], os.path.basename(options.isolated)))
805     finally:
806       # If the command failed, delete the .isolated file if it exists. This is
807       # important so no stale swarm job is executed.
808       if not success and os.path.isfile(options.isolated):
809         os.remove(options.isolated)
810   return int(not success)
811
812
813 def CMDcheck(parser, args):
814   """Checks that all the inputs are present and generates .isolated."""
815   add_subdir_option(parser)
816   options, args = parser.parse_args(args)
817   if args:
818     parser.error('Unsupported argument: %s' % args)
819
820   complete_state = load_complete_state(
821       options, os.getcwd(), options.subdir, False)
822
823   # Nothing is done specifically. Just store the result and state.
824   complete_state.save_files()
825   return 0
826
827
828 def CMDremap(parser, args):
829   """Creates a directory with all the dependencies mapped into it.
830
831   Useful to test manually why a test is failing. The target executable is not
832   run.
833   """
834   parser.require_isolated = False
835   add_outdir_options(parser)
836   add_skip_refresh_option(parser)
837   options, args = parser.parse_args(args)
838   if args:
839     parser.error('Unsupported argument: %s' % args)
840   cwd = os.getcwd()
841   process_outdir_options(parser, options, cwd)
842   complete_state = load_complete_state(options, cwd, None, options.skip_refresh)
843
844   if not os.path.isdir(options.outdir):
845     os.makedirs(options.outdir)
846   print('Remapping into %s' % options.outdir)
847   if os.listdir(options.outdir):
848     raise ExecutionError('Can\'t remap in a non-empty directory')
849
850   create_isolate_tree(
851       options.outdir, complete_state.root_dir, complete_state.saved_state.files,
852       complete_state.saved_state.relative_cwd,
853       complete_state.saved_state.read_only)
854   if complete_state.isolated_filepath:
855     complete_state.save_files()
856   return 0
857
858
859 def CMDrewrite(parser, args):
860   """Rewrites a .isolate file into the canonical format."""
861   parser.require_isolated = False
862   options, args = parser.parse_args(args)
863   if args:
864     parser.error('Unsupported argument: %s' % args)
865
866   if options.isolated:
867     # Load the previous state if it was present. Namely, "foo.isolated.state".
868     complete_state = CompleteState.load_files(options.isolated)
869     isolate = options.isolate or complete_state.saved_state.isolate_filepath
870   else:
871     isolate = options.isolate
872   if not isolate:
873     parser.error('--isolate is required.')
874
875   with open(isolate, 'r') as f:
876     content = f.read()
877   config = isolate_format.load_isolate_as_config(
878       os.path.dirname(os.path.abspath(isolate)),
879       isolate_format.eval_content(content),
880       isolate_format.extract_comment(content))
881   data = config.make_isolate_file()
882   print('Updating %s' % isolate)
883   with open(isolate, 'wb') as f:
884     isolate_format.print_all(config.file_comment, data, f)
885   return 0
886
887
888 @subcommand.usage('-- [extra arguments]')
889 def CMDrun(parser, args):
890   """Runs the test executable in an isolated (temporary) directory.
891
892   All the dependencies are mapped into the temporary directory and the
893   directory is cleaned up after the target exits.
894
895   Argument processing stops at -- and these arguments are appended to the
896   command line of the target to run. For example, use:
897     isolate.py run --isolated foo.isolated -- --gtest_filter=Foo.Bar
898   """
899   parser.require_isolated = False
900   add_skip_refresh_option(parser)
901   options, args = parser.parse_args(args)
902
903   complete_state = load_complete_state(
904       options, os.getcwd(), None, options.skip_refresh)
905   cmd = complete_state.saved_state.command + args
906   if not cmd:
907     raise ExecutionError('No command to run.')
908   cmd = tools.fix_python_path(cmd)
909
910   outdir = run_isolated.make_temp_dir(
911       'isolate-%s' % datetime.date.today(),
912       os.path.dirname(complete_state.root_dir))
913   try:
914     # TODO(maruel): Use run_isolated.run_tha_test().
915     cwd = create_isolate_tree(
916         outdir, complete_state.root_dir, complete_state.saved_state.files,
917         complete_state.saved_state.relative_cwd,
918         complete_state.saved_state.read_only)
919     file_path.ensure_command_has_abs_path(cmd, cwd)
920     logging.info('Running %s, cwd=%s' % (cmd, cwd))
921     try:
922       result = subprocess.call(cmd, cwd=cwd)
923     except OSError:
924       sys.stderr.write(
925           'Failed to executed the command; executable is missing, maybe you\n'
926           'forgot to map it in the .isolate file?\n  %s\n  in %s\n' %
927           (' '.join(cmd), cwd))
928       result = 1
929   finally:
930     run_isolated.rmtree(outdir)
931
932   if complete_state.isolated_filepath:
933     complete_state.save_files()
934   return result
935
936
937 def _process_variable_arg(option, opt, _value, parser):
938   """Called by OptionParser to process a --<foo>-variable argument."""
939   if not parser.rargs:
940     raise optparse.OptionValueError(
941         'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
942   k = parser.rargs.pop(0)
943   variables = getattr(parser.values, option.dest)
944   if '=' in k:
945     k, v = k.split('=', 1)
946   else:
947     if not parser.rargs:
948       raise optparse.OptionValueError(
949           'Please use %s FOO=BAR or %s FOO BAR' % (opt, opt))
950     v = parser.rargs.pop(0)
951   if not re.match('^' + isolate_format.VALID_VARIABLE + '$', k):
952     raise optparse.OptionValueError(
953         'Variable \'%s\' doesn\'t respect format \'%s\'' %
954         (k, isolate_format.VALID_VARIABLE))
955   variables.append((k, v.decode('utf-8')))
956
957
958 def add_variable_option(parser):
959   """Adds --isolated and --<foo>-variable to an OptionParser."""
960   parser.add_option(
961       '-s', '--isolated',
962       metavar='FILE',
963       help='.isolated file to generate or read')
964   # Keep for compatibility. TODO(maruel): Remove once not used anymore.
965   parser.add_option(
966       '-r', '--result',
967       dest='isolated',
968       help=optparse.SUPPRESS_HELP)
969   is_win = sys.platform in ('win32', 'cygwin')
970   # There is really 3 kind of variables:
971   # - path variables, like DEPTH or PRODUCT_DIR that should be
972   #   replaced opportunistically when tracing tests.
973   # - extraneous things like EXECUTABE_SUFFIX.
974   # - configuration variables that are to be used in deducing the matrix to
975   #   reduce.
976   # - unrelated variables that are used as command flags for example.
977   parser.add_option(
978       '--config-variable',
979       action='callback',
980       callback=_process_variable_arg,
981       default=[],
982       dest='config_variables',
983       metavar='FOO BAR',
984       help='Config variables are used to determine which conditions should be '
985            'matched when loading a .isolate file, default: %default. '
986             'All 3 kinds of variables are persistent accross calls, they are '
987             'saved inside <.isolated>.state')
988   parser.add_option(
989       '--path-variable',
990       action='callback',
991       callback=_process_variable_arg,
992       default=[],
993       dest='path_variables',
994       metavar='FOO BAR',
995       help='Path variables are used to replace file paths when loading a '
996            '.isolate file, default: %default')
997   parser.add_option(
998       '--extra-variable',
999       action='callback',
1000       callback=_process_variable_arg,
1001       default=[('EXECUTABLE_SUFFIX', '.exe' if is_win else '')],
1002       dest='extra_variables',
1003       metavar='FOO BAR',
1004       help='Extraneous variables are replaced on the \'command\' entry and on '
1005            'paths in the .isolate file but are not considered relative paths.')
1006
1007
1008 def add_subdir_option(parser):
1009   parser.add_option(
1010       '--subdir',
1011       help='Filters to a subdirectory. Its behavior changes depending if it '
1012            'is a relative path as a string or as a path variable. Path '
1013            'variables are always keyed from the directory containing the '
1014            '.isolate file. Anything else is keyed on the root directory.')
1015
1016
1017 def add_skip_refresh_option(parser):
1018   parser.add_option(
1019       '--skip-refresh', action='store_true',
1020       help='Skip reading .isolate file and do not refresh the hash of '
1021            'dependencies')
1022
1023
1024 def add_outdir_options(parser):
1025   """Adds --outdir, which is orthogonal to --isolate-server.
1026
1027   Note: On upload, separate commands are used between 'archive' and 'hashtable'.
1028   On 'download', the same command can download from either an isolate server or
1029   a file system.
1030   """
1031   parser.add_option(
1032       '-o', '--outdir', metavar='DIR',
1033       help='Directory used to recreate the tree.')
1034
1035
1036 def process_outdir_options(parser, options, cwd):
1037   if not options.outdir:
1038     parser.error('--outdir is required.')
1039   if file_path.is_url(options.outdir):
1040     parser.error('Can\'t use an URL for --outdir.')
1041   options.outdir = unicode(options.outdir).replace('/', os.path.sep)
1042   # outdir doesn't need native path case since tracing is never done from there.
1043   options.outdir = os.path.abspath(
1044       os.path.normpath(os.path.join(cwd, options.outdir)))
1045   # In theory, we'd create the directory outdir right away. Defer doing it in
1046   # case there's errors in the command line.
1047
1048
1049 def parse_isolated_option(parser, options, cwd, require_isolated):
1050   """Processes --isolated."""
1051   if options.isolated:
1052     options.isolated = os.path.normpath(
1053         os.path.join(cwd, options.isolated.replace('/', os.path.sep)))
1054   if require_isolated and not options.isolated:
1055     parser.error('--isolated is required.')
1056   if options.isolated and not options.isolated.endswith('.isolated'):
1057     parser.error('--isolated value must end with \'.isolated\'')
1058
1059
1060 def parse_variable_option(options):
1061   """Processes all the --<foo>-variable flags."""
1062   # TODO(benrg): Maybe we should use a copy of gyp's NameValueListToDict here,
1063   # but it wouldn't be backward compatible.
1064   def try_make_int(s):
1065     """Converts a value to int if possible, converts to unicode otherwise."""
1066     try:
1067       return int(s)
1068     except ValueError:
1069       return s.decode('utf-8')
1070   options.config_variables = dict(
1071       (k, try_make_int(v)) for k, v in options.config_variables)
1072   options.path_variables = dict(options.path_variables)
1073   options.extra_variables = dict(options.extra_variables)
1074
1075
1076 class OptionParserIsolate(tools.OptionParserWithLogging):
1077   """Adds automatic --isolate, --isolated, --out and --<foo>-variable handling.
1078   """
1079   # Set it to False if it is not required, e.g. it can be passed on but do not
1080   # fail if not given.
1081   require_isolated = True
1082
1083   def __init__(self, **kwargs):
1084     tools.OptionParserWithLogging.__init__(
1085         self,
1086         verbose=int(os.environ.get('ISOLATE_DEBUG', 0)),
1087         **kwargs)
1088     group = optparse.OptionGroup(self, "Common options")
1089     group.add_option(
1090         '-i', '--isolate',
1091         metavar='FILE',
1092         help='.isolate file to load the dependency data from')
1093     add_variable_option(group)
1094     group.add_option(
1095         '--ignore_broken_items', action='store_true',
1096         default=bool(os.environ.get('ISOLATE_IGNORE_BROKEN_ITEMS')),
1097         help='Indicates that invalid entries in the isolated file to be '
1098              'only be logged and not stop processing. Defaults to True if '
1099              'env var ISOLATE_IGNORE_BROKEN_ITEMS is set')
1100     self.add_option_group(group)
1101
1102   def parse_args(self, *args, **kwargs):
1103     """Makes sure the paths make sense.
1104
1105     On Windows, / and \ are often mixed together in a path.
1106     """
1107     options, args = tools.OptionParserWithLogging.parse_args(
1108         self, *args, **kwargs)
1109     if not self.allow_interspersed_args and args:
1110       self.error('Unsupported argument: %s' % args)
1111
1112     cwd = file_path.get_native_path_case(unicode(os.getcwd()))
1113     parse_isolated_option(self, options, cwd, self.require_isolated)
1114     parse_variable_option(options)
1115
1116     if options.isolate:
1117       # TODO(maruel): Work with non-ASCII.
1118       # The path must be in native path case for tracing purposes.
1119       options.isolate = unicode(options.isolate).replace('/', os.path.sep)
1120       options.isolate = os.path.normpath(os.path.join(cwd, options.isolate))
1121       options.isolate = file_path.get_native_path_case(options.isolate)
1122
1123     return options, args
1124
1125
1126 def main(argv):
1127   dispatcher = subcommand.CommandDispatcher(__name__)
1128   return dispatcher.execute(OptionParserIsolate(version=__version__), argv)
1129
1130
1131 if __name__ == '__main__':
1132   fix_encoding.fix_encoding()
1133   tools.disable_buffering()
1134   colorama.init()
1135   sys.exit(main(sys.argv[1:]))