blob: 4d522cdbfeea5a199caace3a5eb2f968a85bd1be [file] [log] [blame]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00006"""Reads a .isolated, creates a tree of hardlinks and runs the test.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00007
8Keeps a local cache.
9"""
10
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000011import ctypes
12import hashlib
13import json
14import logging
15import optparse
16import os
17import Queue
18import re
19import shutil
20import stat
21import subprocess
22import sys
23import tempfile
24import threading
25import time
26import urllib
csharp@chromium.orga92403f2012-11-20 15:13:59 +000027import urllib2
28import zlib
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000029
30
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000031# Types of action accepted by link_file().
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000032HARDLINK, SYMLINK, COPY = range(1, 4)
33
34RE_IS_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$')
35
csharp@chromium.org8dc52542012-11-08 20:29:55 +000036# The file size to be used when we don't know the correct file size,
37# generally used for .isolated files.
38UNKNOWN_FILE_SIZE = None
39
csharp@chromium.orga92403f2012-11-20 15:13:59 +000040# The size of each chunk to read when downloading and unzipping files.
41ZIPPED_FILE_CHUNK = 16 * 1024
42
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000043
44class ConfigError(ValueError):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +000045 """Generic failure to load a .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000046 pass
47
48
49class MappingError(OSError):
50 """Failed to recreate the tree."""
51 pass
52
53
csharp@chromium.orga92403f2012-11-20 15:13:59 +000054class DownloadFileOpener(urllib.FancyURLopener):
55 """This class is needed to get urlretrive to raise an exception on
56 404 errors, instead of still writing to the file with the error code.
57 """
58 def http_error_default(self, url, fp, errcode, errmsg, headers):
59 raise urllib2.HTTPError(url, errcode, errmsg, headers, fp)
60
61
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000062def get_flavor():
63 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
64 flavors = {
65 'cygwin': 'win',
66 'win32': 'win',
67 'darwin': 'mac',
68 'sunos5': 'solaris',
69 'freebsd7': 'freebsd',
70 'freebsd8': 'freebsd',
71 }
72 return flavors.get(sys.platform, 'linux')
73
74
75def os_link(source, link_name):
76 """Add support for os.link() on Windows."""
77 if sys.platform == 'win32':
78 if not ctypes.windll.kernel32.CreateHardLinkW(
79 unicode(link_name), unicode(source), 0):
80 raise OSError()
81 else:
82 os.link(source, link_name)
83
84
85def readable_copy(outfile, infile):
86 """Makes a copy of the file that is readable by everyone."""
87 shutil.copy(infile, outfile)
88 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
89 stat.S_IRGRP | stat.S_IROTH)
90 os.chmod(outfile, read_enabled_mode)
91
92
93def link_file(outfile, infile, action):
94 """Links a file. The type of link depends on |action|."""
95 logging.debug('Mapping %s to %s' % (infile, outfile))
96 if action not in (HARDLINK, SYMLINK, COPY):
97 raise ValueError('Unknown mapping action %s' % action)
98 if not os.path.isfile(infile):
99 raise MappingError('%s is missing' % infile)
100 if os.path.isfile(outfile):
101 raise MappingError(
102 '%s already exist; insize:%d; outsize:%d' %
103 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
104
105 if action == COPY:
106 readable_copy(outfile, infile)
107 elif action == SYMLINK and sys.platform != 'win32':
108 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000109 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000110 else:
111 try:
112 os_link(infile, outfile)
113 except OSError:
114 # Probably a different file system.
115 logging.warn(
116 'Failed to hardlink, failing back to copy %s to %s' % (
117 infile, outfile))
118 readable_copy(outfile, infile)
119
120
121def _set_write_bit(path, read_only):
122 """Sets or resets the executable bit on a file or directory."""
123 mode = os.lstat(path).st_mode
124 if read_only:
125 mode = mode & 0500
126 else:
127 mode = mode | 0200
128 if hasattr(os, 'lchmod'):
129 os.lchmod(path, mode) # pylint: disable=E1101
130 else:
131 if stat.S_ISLNK(mode):
132 # Skip symlink without lchmod() support.
133 logging.debug('Can\'t change +w bit on symlink %s' % path)
134 return
135
136 # TODO(maruel): Implement proper DACL modification on Windows.
137 os.chmod(path, mode)
138
139
140def make_writable(root, read_only):
141 """Toggle the writable bit on a directory tree."""
142 root = os.path.abspath(root)
143 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
144 for filename in filenames:
145 _set_write_bit(os.path.join(dirpath, filename), read_only)
146
147 for dirname in dirnames:
148 _set_write_bit(os.path.join(dirpath, dirname), read_only)
149
150
151def rmtree(root):
152 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
153 make_writable(root, False)
154 if sys.platform == 'win32':
155 for i in range(3):
156 try:
157 shutil.rmtree(root)
158 break
159 except WindowsError: # pylint: disable=E0602
160 delay = (i+1)*2
161 print >> sys.stderr, (
162 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
163 time.sleep(delay)
164 else:
165 shutil.rmtree(root)
166
167
168def is_same_filesystem(path1, path2):
169 """Returns True if both paths are on the same filesystem.
170
171 This is required to enable the use of hardlinks.
172 """
173 assert os.path.isabs(path1), path1
174 assert os.path.isabs(path2), path2
175 if sys.platform == 'win32':
176 # If the drive letter mismatches, assume it's a separate partition.
177 # TODO(maruel): It should look at the underlying drive, a drive letter could
178 # be a mount point to a directory on another drive.
179 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
180 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
181 if path1[0].lower() != path2[0].lower():
182 return False
183 return os.stat(path1).st_dev == os.stat(path2).st_dev
184
185
186def get_free_space(path):
187 """Returns the number of free bytes."""
188 if sys.platform == 'win32':
189 free_bytes = ctypes.c_ulonglong(0)
190 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
191 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
192 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000193 # For OSes other than Windows.
194 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000195 return f.f_bfree * f.f_frsize
196
197
198def make_temp_dir(prefix, root_dir):
199 """Returns a temporary directory on the same file system as root_dir."""
200 base_temp_dir = None
201 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
202 base_temp_dir = os.path.dirname(root_dir)
203 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
204
205
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000206def load_isolated(content):
207 """Verifies the .isolated file is valid and loads this object with the json
208 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000209 """
210 try:
211 data = json.loads(content)
212 except ValueError:
213 raise ConfigError('Failed to parse: %s...' % content[:100])
214
215 if not isinstance(data, dict):
216 raise ConfigError('Expected dict, got %r' % data)
217
218 for key, value in data.iteritems():
219 if key == 'command':
220 if not isinstance(value, list):
221 raise ConfigError('Expected list, got %r' % value)
222 for subvalue in value:
223 if not isinstance(subvalue, basestring):
224 raise ConfigError('Expected string, got %r' % subvalue)
225
226 elif key == 'files':
227 if not isinstance(value, dict):
228 raise ConfigError('Expected dict, got %r' % value)
229 for subkey, subvalue in value.iteritems():
230 if not isinstance(subkey, basestring):
231 raise ConfigError('Expected string, got %r' % subkey)
232 if not isinstance(subvalue, dict):
233 raise ConfigError('Expected dict, got %r' % subvalue)
234 for subsubkey, subsubvalue in subvalue.iteritems():
235 if subsubkey == 'link':
236 if not isinstance(subsubvalue, basestring):
237 raise ConfigError('Expected string, got %r' % subsubvalue)
238 elif subsubkey == 'mode':
239 if not isinstance(subsubvalue, int):
240 raise ConfigError('Expected int, got %r' % subsubvalue)
241 elif subsubkey == 'sha-1':
242 if not RE_IS_SHA1.match(subsubvalue):
243 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
244 elif subsubkey == 'size':
245 if not isinstance(subsubvalue, int):
246 raise ConfigError('Expected int, got %r' % subsubvalue)
247 elif subsubkey == 'timestamp':
248 if not isinstance(subsubvalue, int):
249 raise ConfigError('Expected int, got %r' % subsubvalue)
250 elif subsubkey == 'touched_only':
251 if not isinstance(subsubvalue, bool):
252 raise ConfigError('Expected bool, got %r' % subsubvalue)
253 else:
254 raise ConfigError('Unknown subsubkey %s' % subsubkey)
255 if bool('sha-1' in subvalue) and bool('link' in subvalue):
256 raise ConfigError(
257 'Did not expect both \'sha-1\' and \'link\', got: %r' % subvalue)
258
259 elif key == 'includes':
260 if not isinstance(value, list):
261 raise ConfigError('Expected list, got %r' % value)
262 for subvalue in value:
263 if not RE_IS_SHA1.match(subvalue):
264 raise ConfigError('Expected sha-1, got %r' % subvalue)
265
266 elif key == 'read_only':
267 if not isinstance(value, bool):
268 raise ConfigError('Expected bool, got %r' % value)
269
270 elif key == 'relative_cwd':
271 if not isinstance(value, basestring):
272 raise ConfigError('Expected string, got %r' % value)
273
274 elif key == 'os':
275 if value != get_flavor():
276 raise ConfigError(
277 'Expected \'os\' to be \'%s\' but got \'%s\'' %
278 (get_flavor(), value))
279
280 else:
281 raise ConfigError('Unknown key %s' % key)
282
283 return data
284
285
286def fix_python_path(cmd):
287 """Returns the fixed command line to call the right python executable."""
288 out = cmd[:]
289 if out[0] == 'python':
290 out[0] = sys.executable
291 elif out[0].endswith('.py'):
292 out.insert(0, sys.executable)
293 return out
294
295
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000296class WorkerThread(threading.Thread):
297 """Keeps the results of each task in a thread-local outputs variable."""
298 def __init__(self, tasks, *args, **kwargs):
299 super(WorkerThread, self).__init__(*args, **kwargs)
300 self._tasks = tasks
301 self.outputs = []
302 self.exceptions = []
303
304 self.daemon = True
305 self.start()
306
307 def run(self):
308 """Runs until a None task is queued."""
309 while True:
310 task = self._tasks.get()
311 if task is None:
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000312 # We're done.
313 return
314 try:
315 func, args, kwargs = task
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000316 self.outputs.append(func(*args, **kwargs))
317 except Exception, e:
318 logging.error('Caught exception! %s' % e)
319 self.exceptions.append(sys.exc_info())
320 finally:
321 self._tasks.task_done()
322
323
324class ThreadPool(object):
325 """Implements a multithreaded worker pool oriented for mapping jobs with
326 thread-local result storage.
327 """
328 QUEUE_CLASS = Queue.Queue
329
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000330 def __init__(self, num_threads, queue_size=0):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000331 logging.debug('Creating ThreadPool')
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000332 self.tasks = self.QUEUE_CLASS(queue_size)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000333 self._workers = [
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000334 WorkerThread(self.tasks, name='worker-%d' % i)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000335 for i in range(num_threads)
336 ]
337
338 def add_task(self, func, *args, **kwargs):
339 """Adds a task, a function to be executed by a worker.
340
341 The function's return value will be stored in the the worker's thread local
342 outputs list.
343 """
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000344 self.tasks.put((func, args, kwargs))
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000345
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000346 def join(self):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000347 """Extracts all the results from each threads unordered."""
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000348 self.tasks.join()
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000349 out = []
350 # Look for exceptions.
351 for w in self._workers:
352 if w.exceptions:
353 raise w.exceptions[0][0], w.exceptions[0][1], w.exceptions[0][2]
354 out.extend(w.outputs)
355 w.outputs = []
356 return out
357
358 def close(self):
359 """Closes all the threads."""
360 for _ in range(len(self._workers)):
361 # Enqueueing None causes the worker to stop.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000362 self.tasks.put(None)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000363 for t in self._workers:
364 t.join()
365
366 def __enter__(self):
367 """Enables 'with' statement."""
368 return self
369
370 def __exit__(self, exc_type, exc_value, traceback):
371 """Enables 'with' statement."""
372 self.close()
373
374
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000375def valid_file(filepath, size):
376 """Determines if the given files appears valid (currently it just checks
377 the file's size)."""
378 return (size == UNKNOWN_FILE_SIZE or size == os.stat(filepath).st_size)
379
380
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000381class Profiler(object):
382 def __init__(self, name):
383 self.name = name
384 self.start_time = None
385
386 def __enter__(self):
387 self.start_time = time.time()
388 return self
389
390 def __exit__(self, _exc_type, _exec_value, _traceback):
391 time_taken = time.time() - self.start_time
392 logging.info('Profiling: Section %s took %3.3f seconds',
393 self.name, time_taken)
394
395
396class Remote(object):
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000397 """Priority based worker queue to fetch or upload files from a
398 content-address server. Any function may be given as the fetcher/upload,
399 as long as it takes two inputs (the item contents, and their relative
400 destination).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000401
402 Supports local file system, CIFS or http remotes.
403
404 When the priority of items is equals, works in strict FIFO mode.
405 """
406 # Initial and maximum number of worker threads.
407 INITIAL_WORKERS = 2
408 MAX_WORKERS = 16
409 # Priorities.
410 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
411 INTERNAL_PRIORITY_BITS = (1<<8) - 1
412 RETRIES = 5
413
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000414 def __init__(self, destination_root):
415 # Function to fetch a remote object or upload to a remote location..
416 self._do_item = self.get_file_handler(destination_root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000417 # Contains tuple(priority, index, obj, destination).
418 self._queue = Queue.PriorityQueue()
419 # Contains tuple(priority, index, obj).
420 self._done = Queue.PriorityQueue()
421
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000422 # Contains generated exceptions that haven't been handled yet.
423 self._exceptions = Queue.Queue()
424
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000425 # To keep FIFO ordering in self._queue. It is assumed xrange's iterator is
426 # thread-safe.
427 self._next_index = xrange(0, 1<<30).__iter__().next
428
429 # Control access to the following member.
430 self._ready_lock = threading.Lock()
431 # Number of threads in wait state.
432 self._ready = 0
433
434 # Control access to the following member.
435 self._workers_lock = threading.Lock()
436 self._workers = []
437 for _ in range(self.INITIAL_WORKERS):
438 self._add_worker()
439
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000440 def join(self):
441 """Blocks until the queue is empty."""
442 self._queue.join()
443
444 def next_exception(self):
445 """Returns the next unhandled exception, or None if there is
446 no exception."""
447 try:
448 return self._exceptions.get_nowait()
449 except Queue.Empty:
450 return None
451
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000452 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000453 """Retrieves an object from the remote data store.
454
455 The smaller |priority| gets fetched first.
456
457 Thread-safe.
458 """
459 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000460 self._add_to_queue(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000461
462 def get_result(self):
463 """Returns the next file that was successfully fetched."""
464 r = self._done.get()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000465 if r[0] == -1:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000466 # It's an exception.
467 raise r[2][0], r[2][1], r[2][2]
468 return r[2]
469
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000470 def _add_to_queue(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000471 with self._ready_lock:
472 start_new_worker = not self._ready
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000473 self._queue.put((priority, self._next_index(), obj, dest, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000474 if start_new_worker:
475 self._add_worker()
476
477 def _add_worker(self):
478 """Add one worker thread if there isn't too many. Thread-safe."""
479 with self._workers_lock:
480 if len(self._workers) >= self.MAX_WORKERS:
481 return False
482 worker = threading.Thread(target=self._run)
483 self._workers.append(worker)
484 worker.daemon = True
485 worker.start()
486
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000487 def _step_done(self, result):
488 """Worker helper function"""
489 self._done.put(result)
490 self._queue.task_done()
491 if result[0] == -1:
492 self._exceptions.put(sys.exc_info())
493
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000494 def _run(self):
495 """Worker thread loop."""
496 while True:
497 try:
498 with self._ready_lock:
499 self._ready += 1
500 item = self._queue.get()
501 finally:
502 with self._ready_lock:
503 self._ready -= 1
504 if not item:
505 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000506 priority, index, obj, dest, size = item
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000507 try:
508 self._do_item(obj, dest)
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000509 if size and not valid_file(dest, size):
510 download_size = os.stat(dest).st_size
511 os.remove(dest)
512 raise IOError('File incorrect size after download of %s. Got %s and '
513 'expected %s' % (dest, download_size, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000514 except IOError:
515 # Retry a few times, lowering the priority.
516 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000517 self._add_to_queue(priority + 1, obj, dest, size)
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000518 self._queue.task_done()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000519 continue
520 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000521 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000522 except:
523 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000524 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000525 else:
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000526 self._step_done((priority, index, obj))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000527
528 @staticmethod
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000529 def get_file_handler(file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000530 """Returns a object to retrieve objects from a remote."""
531 if re.match(r'^https?://.+$', file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000532 def download_file(item, dest):
533 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
534 # easy.
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000535
536 # TODO(csharp): This is a temporary workaround to generate the gzipped
537 # url, remove once the files are always zipped before being uploaded.
538 try:
539 zipped_source = file_or_url.rstrip('/') + '-gzip/' + item
540 logging.debug('download_file(%s)', zipped_source)
541 connection = urllib2.urlopen(zipped_source)
542 decompressor = zlib.decompressobj()
543 with open(dest, 'wb') as f:
544 while True:
545 chunk = connection.read(ZIPPED_FILE_CHUNK)
546 if not chunk:
547 break
548 f.write(decompressor.decompress(chunk))
549 # Ensure that all the data was properly decompressed.
550 uncompressed_data = decompressor.flush()
551 assert not uncompressed_data
552 except urllib2.URLError:
553 # Try the unzipped version
554 unzipped_source = file_or_url + item
555 logging.debug('Zipped version missing, try unzipped version')
556 logging.debug('download_file(%s, %s)', unzipped_source, dest)
557 DownloadFileOpener().retrieve(unzipped_source, dest)
558
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000559 return download_file
560
561 def copy_file(item, dest):
562 source = os.path.join(file_or_url, item)
563 logging.debug('copy_file(%s, %s)', source, dest)
564 shutil.copy(source, dest)
565 return copy_file
566
567
568class CachePolicies(object):
569 def __init__(self, max_cache_size, min_free_space, max_items):
570 """
571 Arguments:
572 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
573 cache is effectively a leak.
574 - min_free_space: Trim if disk free space becomes lower than this value. If
575 0, it unconditionally fill the disk.
576 - max_items: Maximum number of items to keep in the cache. If 0, do not
577 enforce a limit.
578 """
579 self.max_cache_size = max_cache_size
580 self.min_free_space = min_free_space
581 self.max_items = max_items
582
583
584class Cache(object):
585 """Stateful LRU cache.
586
587 Saves its state as json file.
588 """
589 STATE_FILE = 'state.json'
590
591 def __init__(self, cache_dir, remote, policies):
592 """
593 Arguments:
594 - cache_dir: Directory where to place the cache.
595 - remote: Remote where to fetch items from.
596 - policies: cache retention policies.
597 """
598 self.cache_dir = cache_dir
599 self.remote = remote
600 self.policies = policies
601 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
602 # The tuple(file, size) are kept as an array in a LRU style. E.g.
603 # self.state[0] is the oldest item.
604 self.state = []
605 # A lookup map to speed up searching.
606 self._lookup = {}
607 self._dirty = False
608
609 # Items currently being fetched. Keep it local to reduce lock contention.
610 self._pending_queue = set()
611
612 # Profiling values.
613 self._added = []
614 self._removed = []
615 self._free_disk = 0
616
617 if not os.path.isdir(self.cache_dir):
618 os.makedirs(self.cache_dir)
619 if os.path.isfile(self.state_file):
620 try:
621 self.state = json.load(open(self.state_file, 'r'))
622 except (IOError, ValueError), e:
623 # Too bad. The file will be overwritten and the cache cleared.
624 logging.error(
625 'Broken state file %s, ignoring.\n%s' % (self.STATE_FILE, e))
626 if (not isinstance(self.state, list) or
627 not all(
628 isinstance(i, (list, tuple)) and len(i) == 2 for i in self.state)):
629 # Discard.
630 self.state = []
631 self._dirty = True
632
633 # Ensure that all files listed in the state still exist and add new ones.
634 previous = set(filename for filename, _ in self.state)
635 if len(previous) != len(self.state):
636 logging.warn('Cache state is corrupted')
637 self._dirty = True
638 self.state = []
639 else:
640 added = 0
641 for filename in os.listdir(self.cache_dir):
642 if filename == self.STATE_FILE:
643 continue
644 if filename in previous:
645 previous.remove(filename)
646 continue
647 # An untracked file.
648 self._dirty = True
649 if not RE_IS_SHA1.match(filename):
650 logging.warn('Removing unknown file %s from cache', filename)
651 os.remove(self.path(filename))
652 else:
653 # Insert as the oldest file. It will be deleted eventually if not
654 # accessed.
655 self._add(filename, False)
656 added += 1
657 if added:
658 logging.warn('Added back %d unknown files', added)
659 self.state = [
660 (filename, size) for filename, size in self.state
661 if filename not in previous
662 ]
663 self._update_lookup()
664
665 with Profiler('SetupTrimming'):
666 self.trim()
667
668 def __enter__(self):
669 return self
670
671 def __exit__(self, _exc_type, _exec_value, _traceback):
672 with Profiler('CleanupTrimming'):
673 self.trim()
674
675 logging.info(
676 '%4d (%7dkb) added', len(self._added), sum(self._added) / 1024)
677 logging.info(
678 '%4d (%7dkb) current',
679 len(self.state),
680 sum(i[1] for i in self.state) / 1024)
681 logging.info(
682 '%4d (%7dkb) removed', len(self._removed), sum(self._removed) / 1024)
683 logging.info('%7dkb free', self._free_disk / 1024)
684
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000685 def remove_file_at_index(self, index):
686 """Removes the file at the given index."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000687 try:
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000688 filename, size = self.state.pop(index)
689 # TODO(csharp): _lookup should self-update.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000690 del self._lookup[filename]
691 self._removed.append(size)
692 os.remove(self.path(filename))
693 self._dirty = True
694 except OSError as e:
695 logging.error('Error attempting to delete a file\n%s' % e)
696
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000697 def remove_lru_file(self):
698 """Removes the last recently used file."""
699 self.remove_file_at_index(0)
700
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000701 def trim(self):
702 """Trims anything we don't know, make sure enough free space exists."""
703 # Ensure maximum cache size.
704 if self.policies.max_cache_size and self.state:
705 while sum(i[1] for i in self.state) > self.policies.max_cache_size:
706 self.remove_lru_file()
707
708 # Ensure maximum number of items in the cache.
709 if self.policies.max_items and self.state:
710 while len(self.state) > self.policies.max_items:
711 self.remove_lru_file()
712
713 # Ensure enough free space.
714 self._free_disk = get_free_space(self.cache_dir)
715 while (
716 self.policies.min_free_space and
717 self.state and
718 self._free_disk < self.policies.min_free_space):
719 self.remove_lru_file()
720 self._free_disk = get_free_space(self.cache_dir)
721
722 self.save()
723
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000724 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000725 """Retrieves a file from the remote, if not already cached, and adds it to
726 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000727
728 If the file is in the cache, verifiy that the file is valid (i.e. it is
729 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000730 """
731 assert not '/' in item
732 path = self.path(item)
733 index = self._lookup.get(item)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000734
735 if index is not None:
736 if not valid_file(self.path(item), size):
737 self.remove_file_at_index(index)
738 self._update_lookup()
739 index = None
740 else:
741 assert index < len(self.state)
742 # Was already in cache. Update it's LRU value by putting it at the end.
743 self.state.append(self.state.pop(index))
744 self._dirty = True
745 self._update_lookup()
746
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000747 if index is None:
748 if item in self._pending_queue:
749 # Already pending. The same object could be referenced multiple times.
750 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000751 self.remote.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000752 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000753
754 def add(self, filepath, obj):
755 """Forcibly adds a file to the cache."""
756 if not obj in self._lookup:
757 link_file(self.path(obj), filepath, HARDLINK)
758 self._add(obj, True)
759
760 def path(self, item):
761 """Returns the path to one item."""
762 return os.path.join(self.cache_dir, item)
763
764 def save(self):
765 """Saves the LRU ordering."""
766 json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':'))
767
768 def wait_for(self, items):
769 """Starts a loop that waits for at least one of |items| to be retrieved.
770
771 Returns the first item retrieved.
772 """
773 # Flush items already present.
774 for item in items:
775 if item in self._lookup:
776 return item
777
778 assert all(i in self._pending_queue for i in items), (
779 items, self._pending_queue)
780 # Note that:
781 # len(self._pending_queue) ==
782 # ( len(self.remote._workers) - self.remote._ready +
783 # len(self._remote._queue) + len(self._remote.done))
784 # There is no lock-free way to verify that.
785 while self._pending_queue:
786 item = self.remote.get_result()
787 self._pending_queue.remove(item)
788 self._add(item, True)
789 if item in items:
790 return item
791
792 def _add(self, item, at_end):
793 """Adds an item in the internal state.
794
795 If |at_end| is False, self._lookup becomes inconsistent and
796 self._update_lookup() must be called.
797 """
798 size = os.stat(self.path(item)).st_size
799 self._added.append(size)
800 if at_end:
801 self.state.append((item, size))
802 self._lookup[item] = len(self.state) - 1
803 else:
804 self.state.insert(0, (item, size))
805 self._dirty = True
806
807 def _update_lookup(self):
808 self._lookup = dict(
809 (filename, index) for index, (filename, _) in enumerate(self.state))
810
811
812
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000813class IsolatedFile(object):
814 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000815 def __init__(self, obj_hash):
816 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000817 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000818 self.obj_hash = obj_hash
819 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000820 # .isolate and all the .isolated files recursively included by it with
821 # 'includes' key. The order of each sha-1 in 'includes', each representing a
822 # .isolated file in the hash table, is important, as the later ones are not
823 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000824 self.can_fetch = False
825
826 # Raw data.
827 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000828 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000829 self.children = []
830
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000831 # Set once the .isolated file is loaded.
832 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000833 # Set once the files are fetched.
834 self.files_fetched = False
835
836 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000837 """Verifies the .isolated file is valid and loads this object with the json
838 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000839 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000840 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
841 assert not self._is_parsed
842 self.data = load_isolated(content)
843 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
844 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000845
846 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000847 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000848
849 Preemptively request files.
850
851 Note that |files| is modified by this function.
852 """
853 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000854 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000855 return
856 logging.debug('fetch_files(%s)' % self.obj_hash)
857 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000858 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000859 # overriden files must not be fetched.
860 if filepath not in files:
861 files[filepath] = properties
862 if 'sha-1' in properties:
863 # Preemptively request files.
864 logging.debug('fetching %s' % filepath)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000865 cache.retrieve(Remote.MED, properties['sha-1'], properties['size'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000866 self.files_fetched = True
867
868
869class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000870 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000871 def __init__(self):
872 self.command = []
873 self.files = {}
874 self.read_only = None
875 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000876 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000877 self.root = None
878 logging.debug('Settings')
879
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000880 def load(self, cache, root_isolated_hash):
881 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000882
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000883 It enables support for "included" .isolated files. They are processed in
884 strict order but fetched asynchronously from the cache. This is important so
885 that a file in an included .isolated file that is overridden by an embedding
886 .isolated file is not fetched neededlessly. The includes are fetched in one
887 pass and the files are fetched as soon as all the ones on the left-side
888 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000889
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000890 The prioritization is very important here for nested .isolated files.
891 'includes' have the highest priority and the algorithm is optimized for both
892 deep and wide trees. A deep one is a long link of .isolated files referenced
893 one at a time by one item in 'includes'. A wide one has a large number of
894 'includes' in a single .isolated file. 'left' is defined as an included
895 .isolated file earlier in the 'includes' list. So the order of the elements
896 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000897 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000898 self.root = IsolatedFile(root_isolated_hash)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000899 cache.retrieve(Remote.HIGH, root_isolated_hash, UNKNOWN_FILE_SIZE)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000900 pending = {root_isolated_hash: self.root}
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000901 # Keeps the list of retrieved items to refuse recursive includes.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000902 retrieved = [root_isolated_hash]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000903
904 def update_self(node):
905 node.fetch_files(cache, self.files)
906 # Grabs properties.
907 if not self.command and node.data.get('command'):
908 self.command = node.data['command']
909 if self.read_only is None and node.data.get('read_only') is not None:
910 self.read_only = node.data['read_only']
911 if (self.relative_cwd is None and
912 node.data.get('relative_cwd') is not None):
913 self.relative_cwd = node.data['relative_cwd']
914
915 def traverse_tree(node):
916 if node.can_fetch:
917 if not node.files_fetched:
918 update_self(node)
919 will_break = False
920 for i in node.children:
921 if not i.can_fetch:
922 if will_break:
923 break
924 # Automatically mark the first one as fetcheable.
925 i.can_fetch = True
926 will_break = True
927 traverse_tree(i)
928
929 while pending:
930 item_hash = cache.wait_for(pending)
931 item = pending.pop(item_hash)
932 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000933 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000934 # It's the root item.
935 item.can_fetch = True
936
937 for new_child in item.children:
938 h = new_child.obj_hash
939 if h in retrieved:
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000940 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000941 pending[h] = new_child
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000942 cache.retrieve(Remote.HIGH, h, UNKNOWN_FILE_SIZE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000943
944 # Traverse the whole tree to see if files can now be fetched.
945 traverse_tree(self.root)
946 def check(n):
947 return all(check(x) for x in n.children) and n.files_fetched
948 assert check(self.root)
949 self.relative_cwd = self.relative_cwd or ''
950 self.read_only = self.read_only or False
951
952
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000953def run_tha_test(isolated_hash, cache_dir, remote, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000954 """Downloads the dependencies in the cache, hardlinks them into a temporary
955 directory and runs the executable.
956 """
957 settings = Settings()
958 with Cache(cache_dir, Remote(remote), policies) as cache:
959 outdir = make_temp_dir('run_tha_test', cache_dir)
960 try:
961 # Initiate all the files download.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000962 with Profiler('GetIsolateds') as _prof:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000963 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000964 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000965 # Adds it in the cache. While not strictly necessary, this simplifies
966 # the rest.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000967 h = hashlib.sha1(open(isolated_hash, 'r').read()).hexdigest()
968 cache.add(isolated_hash, h)
969 isolated_hash = h
970 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000971
972 if not settings.command:
973 print >> sys.stderr, 'No command to run'
974 return 1
975
976 with Profiler('GetRest') as _prof:
977 logging.debug('Creating directories')
978 # Creates the tree of directories to create.
979 directories = set(os.path.dirname(f) for f in settings.files)
980 for item in list(directories):
981 while item:
982 directories.add(item)
983 item = os.path.dirname(item)
984 for d in sorted(directories):
985 if d:
986 os.mkdir(os.path.join(outdir, d))
987
988 # Creates the links if necessary.
989 for filepath, properties in settings.files.iteritems():
990 if 'link' not in properties:
991 continue
992 outfile = os.path.join(outdir, filepath)
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000993 # symlink doesn't exist on Windows. So the 'link' property should
994 # never be specified for windows .isolated file.
995 os.symlink(properties['link'], outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000996 if 'mode' in properties:
997 # It's not set on Windows.
maruel@chromium.org96768a42012-10-31 18:49:18 +0000998 lchmod = getattr(os, 'lchmod', None)
999 if lchmod:
1000 lchmod(outfile, properties['mode'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001001
1002 # Remaining files to be processed.
1003 # Note that files could still be not be downloaded yet here.
1004 remaining = dict()
1005 for filepath, props in settings.files.iteritems():
1006 if 'sha-1' in props:
1007 remaining.setdefault(props['sha-1'], []).append((filepath, props))
1008
1009 # Do bookkeeping while files are being downloaded in the background.
1010 cwd = os.path.join(outdir, settings.relative_cwd)
1011 if not os.path.isdir(cwd):
1012 os.makedirs(cwd)
1013 cmd = settings.command[:]
1014 # Ensure paths are correctly separated on windows.
1015 cmd[0] = cmd[0].replace('/', os.path.sep)
1016 cmd = fix_python_path(cmd)
1017
1018 # Now block on the remaining files to be downloaded and mapped.
1019 while remaining:
1020 obj = cache.wait_for(remaining)
1021 for filepath, properties in remaining.pop(obj):
1022 outfile = os.path.join(outdir, filepath)
1023 link_file(outfile, cache.path(obj), HARDLINK)
1024 if 'mode' in properties:
1025 # It's not set on Windows.
1026 os.chmod(outfile, properties['mode'])
1027
1028 if settings.read_only:
1029 make_writable(outdir, True)
1030 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1031 try:
1032 with Profiler('RunTest') as _prof:
1033 return subprocess.call(cmd, cwd=cwd)
1034 except OSError:
1035 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1036 raise
1037 finally:
1038 rmtree(outdir)
1039
1040
1041def main():
1042 parser = optparse.OptionParser(
1043 usage='%prog <options>', description=sys.modules[__name__].__doc__)
1044 parser.add_option(
1045 '-v', '--verbose', action='count', default=0, help='Use multiple times')
1046 parser.add_option('--no-run', action='store_true', help='Skip the run part')
1047
1048 group = optparse.OptionGroup(parser, 'Data source')
1049 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001050 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001051 metavar='FILE',
1052 help='File/url describing what to map or run')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001053 # TODO(maruel): Remove once not used anymore.
1054 group.add_option(
1055 '-m', '--manifest', dest='isolated', help=optparse.SUPPRESS_HELP)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001056 group.add_option(
1057 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001058 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001059 parser.add_option_group(group)
1060
1061 group.add_option(
1062 '-r', '--remote', metavar='URL', help='Remote where to get the items')
1063 group = optparse.OptionGroup(parser, 'Cache management')
1064 group.add_option(
1065 '--cache',
1066 default='cache',
1067 metavar='DIR',
1068 help='Cache directory, default=%default')
1069 group.add_option(
1070 '--max-cache-size',
1071 type='int',
1072 metavar='NNN',
1073 default=20*1024*1024*1024,
1074 help='Trim if the cache gets larger than this value, default=%default')
1075 group.add_option(
1076 '--min-free-space',
1077 type='int',
1078 metavar='NNN',
1079 default=1*1024*1024*1024,
1080 help='Trim if disk free space becomes lower than this value, '
1081 'default=%default')
1082 group.add_option(
1083 '--max-items',
1084 type='int',
1085 metavar='NNN',
1086 default=100000,
1087 help='Trim if more than this number of items are in the cache '
1088 'default=%default')
1089 parser.add_option_group(group)
1090
1091 options, args = parser.parse_args()
1092 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
1093 logging.basicConfig(
1094 level=level,
1095 format='%(levelname)5s %(module)15s(%(lineno)3d): %(message)s')
1096
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001097 if bool(options.isolated) == bool(options.hash):
1098 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001099 if not options.remote:
1100 parser.error('--remote is required.')
1101 if args:
1102 parser.error('Unsupported args %s' % ' '.join(args))
1103
1104 policies = CachePolicies(
1105 options.max_cache_size, options.min_free_space, options.max_items)
1106 try:
1107 return run_tha_test(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001108 options.isolated or options.hash,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001109 os.path.abspath(options.cache),
1110 options.remote,
1111 policies)
1112 except (ConfigError, MappingError), e:
1113 print >> sys.stderr, str(e)
1114 return 1
1115
1116
1117if __name__ == '__main__':
1118 sys.exit(main())