blob: 9ade6746363eb9455327ec97483fa7581ab950d6 [file] [log] [blame]
agable@chromium.org5a306a22014-02-24 22:13:59 +00001#!/usr/bin/env python
2# Copyright 2014 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
6"""A git command for managing a local cache of git repositories."""
7
szager@chromium.org848fd492014-04-09 19:06:44 +00008from __future__ import print_function
agable@chromium.org5a306a22014-02-24 22:13:59 +00009import errno
10import logging
11import optparse
12import os
szager@chromium.org174766f2014-05-13 21:27:46 +000013import re
agable@chromium.org5a306a22014-02-24 22:13:59 +000014import tempfile
pgervais@chromium.orgf3726102014-04-17 17:24:15 +000015import time
agable@chromium.org5a306a22014-02-24 22:13:59 +000016import subprocess
17import sys
18import urlparse
hinoka@google.com776a2c32014-04-25 07:54:25 +000019import zipfile
agable@chromium.org5a306a22014-02-24 22:13:59 +000020
hinoka@google.com563559c2014-04-02 00:36:24 +000021from download_from_google_storage import Gsutil
agable@chromium.org5a306a22014-02-24 22:13:59 +000022import gclient_utils
23import subcommand
24
szager@chromium.org848fd492014-04-09 19:06:44 +000025try:
26 # pylint: disable=E0602
27 WinErr = WindowsError
28except NameError:
29 class WinErr(Exception):
30 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000031
32class LockError(Exception):
33 pass
34
35
36class Lockfile(object):
37 """Class to represent a cross-platform process-specific lockfile."""
38
39 def __init__(self, path):
40 self.path = os.path.abspath(path)
41 self.lockfile = self.path + ".lock"
42 self.pid = os.getpid()
43
44 def _read_pid(self):
45 """Read the pid stored in the lockfile.
46
47 Note: This method is potentially racy. By the time it returns the lockfile
48 may have been unlocked, removed, or stolen by some other process.
49 """
50 try:
51 with open(self.lockfile, 'r') as f:
52 pid = int(f.readline().strip())
53 except (IOError, ValueError):
54 pid = None
55 return pid
56
57 def _make_lockfile(self):
58 """Safely creates a lockfile containing the current pid."""
59 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
60 fd = os.open(self.lockfile, open_flags, 0o644)
61 f = os.fdopen(fd, 'w')
szager@chromium.org848fd492014-04-09 19:06:44 +000062 print(self.pid, file=f)
agable@chromium.org5a306a22014-02-24 22:13:59 +000063 f.close()
64
65 def _remove_lockfile(self):
pgervais@chromium.orgf3726102014-04-17 17:24:15 +000066 """Delete the lockfile. Complains (implicitly) if it doesn't exist.
67
68 See gclient_utils.py:rmtree docstring for more explanation on the
69 windows case.
70 """
71 if sys.platform == 'win32':
72 lockfile = os.path.normcase(self.lockfile)
73 for _ in xrange(3):
74 exitcode = subprocess.call(['cmd.exe', '/c',
75 'del', '/f', '/q', lockfile])
76 if exitcode == 0:
77 return
78 time.sleep(3)
79 raise LockError('Failed to remove lock: %s' % lockfile)
80 else:
81 os.remove(self.lockfile)
agable@chromium.org5a306a22014-02-24 22:13:59 +000082
83 def lock(self):
84 """Acquire the lock.
85
86 Note: This is a NON-BLOCKING FAIL-FAST operation.
87 Do. Or do not. There is no try.
88 """
89 try:
90 self._make_lockfile()
91 except OSError as e:
92 if e.errno == errno.EEXIST:
93 raise LockError("%s is already locked" % self.path)
94 else:
95 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
96
97 def unlock(self):
98 """Release the lock."""
99 if not self.is_locked():
100 raise LockError("%s is not locked" % self.path)
101 if not self.i_am_locking():
102 raise LockError("%s is locked, but not by me" % self.path)
103 self._remove_lockfile()
104
105 def break_lock(self):
106 """Remove the lock, even if it was created by someone else."""
107 try:
108 self._remove_lockfile()
109 return True
110 except OSError as exc:
111 if exc.errno == errno.ENOENT:
112 return False
113 else:
114 raise
115
116 def is_locked(self):
117 """Test if the file is locked by anyone.
118
119 Note: This method is potentially racy. By the time it returns the lockfile
120 may have been unlocked, removed, or stolen by some other process.
121 """
122 return os.path.exists(self.lockfile)
123
124 def i_am_locking(self):
125 """Test if the file is locked by this process."""
126 return self.is_locked() and self.pid == self._read_pid()
127
128 def __enter__(self):
129 self.lock()
130 return self
131
132 def __exit__(self, *_exc):
szager@chromium.org848fd492014-04-09 19:06:44 +0000133 # Windows is unreliable when it comes to file locking. YMMV.
134 try:
135 self.unlock()
136 except WinErr:
137 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +0000138
139
szager@chromium.org848fd492014-04-09 19:06:44 +0000140class Mirror(object):
141
142 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
143 gsutil_exe = os.path.join(
144 os.path.dirname(os.path.abspath(__file__)),
145 'third_party', 'gsutil', 'gsutil')
szager@chromium.org848fd492014-04-09 19:06:44 +0000146
147 def __init__(self, url, refs=None, print_func=None):
148 self.url = url
149 self.refs = refs or []
150 self.basedir = self.UrlToCacheDir(url)
151 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
152 self.print = print_func or print
153
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000154 @property
155 def bootstrap_bucket(self):
156 if 'chrome-internal' in self.url:
157 return 'chrome-git-cache'
158 else:
159 return 'chromium-git-cache'
160
szager@chromium.org174766f2014-05-13 21:27:46 +0000161 @classmethod
162 def FromPath(cls, path):
163 return cls(cls.CacheDirToUrl(path))
164
szager@chromium.org848fd492014-04-09 19:06:44 +0000165 @staticmethod
166 def UrlToCacheDir(url):
167 """Convert a git url to a normalized form for the cache dir path."""
168 parsed = urlparse.urlparse(url)
169 norm_url = parsed.netloc + parsed.path
170 if norm_url.endswith('.git'):
171 norm_url = norm_url[:-len('.git')]
172 return norm_url.replace('-', '--').replace('/', '-').lower()
173
174 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000175 def CacheDirToUrl(path):
176 """Convert a cache dir path to its corresponding url."""
177 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
178 return 'https://%s' % netpath
179
180 @staticmethod
szager@chromium.org848fd492014-04-09 19:06:44 +0000181 def FindExecutable(executable):
182 """This mimics the "which" utility."""
183 path_folders = os.environ.get('PATH').split(os.pathsep)
184
185 for path_folder in path_folders:
186 target = os.path.join(path_folder, executable)
187 # Just incase we have some ~/blah paths.
188 target = os.path.abspath(os.path.expanduser(target))
189 if os.path.isfile(target) and os.access(target, os.X_OK):
190 return target
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000191 if sys.platform.startswith('win'):
192 for suffix in ('.bat', '.cmd', '.exe'):
193 alt_target = target + suffix
szager@chromium.org4039b312014-04-09 21:56:46 +0000194 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000195 return alt_target
szager@chromium.org848fd492014-04-09 19:06:44 +0000196 return None
197
198 @classmethod
199 def SetCachePath(cls, cachepath):
200 setattr(cls, 'cachepath', cachepath)
201
202 @classmethod
203 def GetCachePath(cls):
204 if not hasattr(cls, 'cachepath'):
205 try:
206 cachepath = subprocess.check_output(
207 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
208 except subprocess.CalledProcessError:
209 cachepath = None
210 if not cachepath:
211 raise RuntimeError('No global cache.cachepath git configuration found.')
212 setattr(cls, 'cachepath', cachepath)
213 return getattr(cls, 'cachepath')
214
215 def RunGit(self, cmd, **kwargs):
216 """Run git in a subprocess."""
217 cwd = kwargs.setdefault('cwd', self.mirror_path)
218 kwargs.setdefault('print_stdout', False)
219 kwargs.setdefault('filter_fn', self.print)
220 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
221 env.setdefault('GIT_ASKPASS', 'true')
222 env.setdefault('SSH_ASKPASS', 'true')
223 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
224 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
225
226 def config(self, cwd=None):
227 if cwd is None:
228 cwd = self.mirror_path
229 self.RunGit(['config', 'core.deltaBaseCacheLimit',
230 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
231 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
232 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
233 '+refs/heads/*:refs/heads/*'], cwd=cwd)
234 for ref in self.refs:
235 ref = ref.lstrip('+').rstrip('/')
236 if ref.startswith('refs/'):
237 refspec = '+%s:%s' % (ref, ref)
238 else:
239 refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
240 self.RunGit(['config', '--add', 'remote.origin.fetch', refspec], cwd=cwd)
241
242 def bootstrap_repo(self, directory):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000243 """Bootstrap the repo from Google Stroage if possible."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000244
hinoka@google.com776a2c32014-04-25 07:54:25 +0000245 python_fallback = False
246 if sys.platform.startswith('win') and not self.FindExecutable('7z'):
247 python_fallback = True
248 elif sys.platform.startswith('darwin'):
249 # The OSX version of unzip doesn't support zip64.
250 python_fallback = True
251 elif not self.FindExecutable('unzip'):
252 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000253
254 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.orgc2bc22d2014-06-05 21:19:38 +0000255 gsutil = Gsutil(self.gsutil_exe, boto_path=None, bypass_prodaccess=True)
szager@chromium.org848fd492014-04-09 19:06:44 +0000256 # Get the most recent version of the zipfile.
257 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
258 ls_out_sorted = sorted(ls_out.splitlines())
259 if not ls_out_sorted:
260 # This repo is not on Google Storage.
261 return False
262 latest_checkout = ls_out_sorted[-1]
263
264 # Download zip file to a temporary directory.
265 try:
266 tempdir = tempfile.mkdtemp()
267 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000268 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000269 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000270 return False
271 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
272
hinoka@google.com776a2c32014-04-25 07:54:25 +0000273 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
274 if not python_fallback:
275 if sys.platform.startswith('win'):
276 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
277 else:
278 cmd = ['unzip', filename, '-d', directory]
279 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000280 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000281 try:
282 with zipfile.ZipFile(filename, 'r') as f:
283 f.printdir()
284 f.extractall(directory)
285 except Exception as e:
286 self.print('Encountered error: %s' % str(e), file=sys.stderr)
287 retcode = 1
288 else:
289 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000290 finally:
291 # Clean up the downloaded zipfile.
292 gclient_utils.rmtree(tempdir)
293
294 if retcode:
295 self.print(
296 'Extracting bootstrap zipfile %s failed.\n'
297 'Resuming normal operations.' % filename)
298 return False
299 return True
300
301 def exists(self):
302 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
303
304 def populate(self, depth=None, shallow=False, bootstrap=False,
305 verbose=False):
306 if shallow and not depth:
307 depth = 10000
308 gclient_utils.safe_makedirs(self.GetCachePath())
309
310 v = []
311 if verbose:
312 v = ['-v', '--progress']
313
314 d = []
315 if depth:
316 d = ['--depth', str(depth)]
317
318
319 with Lockfile(self.mirror_path):
320 # Setup from scratch if the repo is new or is in a bad state.
321 tempdir = None
322 if not os.path.exists(os.path.join(self.mirror_path, 'config')):
323 gclient_utils.rmtree(self.mirror_path)
324 tempdir = tempfile.mkdtemp(
325 suffix=self.basedir, dir=self.GetCachePath())
326 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
327 if not bootstrapped:
328 self.RunGit(['init', '--bare'], cwd=tempdir)
329 else:
330 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
331 logging.warn(
332 'Shallow fetch requested, but repo cache already exists.')
333 d = []
334
335 rundir = tempdir or self.mirror_path
336 self.config(rundir)
337 fetch_cmd = ['fetch'] + v + d + ['origin']
338 fetch_specs = subprocess.check_output(
339 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
340 cwd=rundir).strip().splitlines()
341 for spec in fetch_specs:
342 try:
343 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
344 except subprocess.CalledProcessError:
345 logging.warn('Fetch of %s failed' % spec)
346 if tempdir:
347 os.rename(tempdir, self.mirror_path)
348
349 def update_bootstrap(self):
350 # The files are named <git number>.zip
351 gen_number = subprocess.check_output(
352 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
353 self.RunGit(['gc']) # Run Garbage Collect to compress packfile.
354 # Creating a temp file and then deleting it ensures we can use this name.
355 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
356 os.remove(tmp_zipfile)
357 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
358 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
359 dest_name = 'gs://%s/%s/%s.zip' % (
360 self.bootstrap_bucket, self.basedir, gen_number)
361 gsutil.call('cp', tmp_zipfile, dest_name)
362 os.remove(tmp_zipfile)
363
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000364 @staticmethod
365 def DeleteTmpPackFiles(path):
366 pack_dir = os.path.join(path, 'objects', 'pack')
367 pack_files = [f for f in os.listdir(pack_dir) if
368 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
369 for f in pack_files:
370 f = os.path.join(pack_dir, f)
371 try:
372 os.remove(f)
373 logging.warn('Deleted stale temporary pack file %s' % f)
374 except OSError:
375 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000376
377 @staticmethod
378 def BreakLocks(path):
379 did_unlock = False
380 lf = Lockfile(path)
381 if lf.break_lock():
382 did_unlock = True
383 # Look for lock files that might have been left behind by an interrupted
384 # git process.
385 lf = os.path.join(path, 'config.lock')
386 if os.path.exists(lf):
387 os.remove(lf)
388 did_unlock = True
389 return did_unlock
390
szager@chromium.org848fd492014-04-09 19:06:44 +0000391 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000392 return self.BreakLocks(self.mirror_path)
393
394 @classmethod
395 def UnlockAll(cls):
396 cachepath = cls.GetCachePath()
397 dirlist = os.listdir(cachepath)
398 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
399 if os.path.isdir(os.path.join(cachepath, path))])
400 for dirent in dirlist:
401 if (dirent.endswith('.lock') and
402 os.path.isfile(os.path.join(cachepath, dirent))):
403 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
404
405 unlocked_repos = []
406 for repo_dir in repo_dirs:
407 if cls.BreakLocks(repo_dir):
408 unlocked_repos.append(repo_dir)
409
410 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000411
agable@chromium.org5a306a22014-02-24 22:13:59 +0000412@subcommand.usage('[url of repo to check for caching]')
413def CMDexists(parser, args):
414 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000415 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000416 if not len(args) == 1:
417 parser.error('git cache exists only takes exactly one repo url.')
418 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000419 mirror = Mirror(url)
420 if mirror.exists():
421 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000422 return 0
423 return 1
424
425
hinoka@google.com563559c2014-04-02 00:36:24 +0000426@subcommand.usage('[url of repo to create a bootstrap zip file]')
427def CMDupdate_bootstrap(parser, args):
428 """Create and uploads a bootstrap tarball."""
429 # Lets just assert we can't do this on Windows.
430 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000431 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000432 return 1
433
434 # First, we need to ensure the cache is populated.
435 populate_args = args[:]
436 populate_args.append('--no_bootstrap')
437 CMDpopulate(parser, populate_args)
438
439 # Get the repo directory.
szager@chromium.org848fd492014-04-09 19:06:44 +0000440 _, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000441 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000442 mirror = Mirror(url)
443 mirror.update_bootstrap()
444 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000445
446
agable@chromium.org5a306a22014-02-24 22:13:59 +0000447@subcommand.usage('[url of repo to add to or update in cache]')
448def CMDpopulate(parser, args):
449 """Ensure that the cache has all up-to-date objects for the given repo."""
450 parser.add_option('--depth', type='int',
451 help='Only cache DEPTH commits of history')
452 parser.add_option('--shallow', '-s', action='store_true',
453 help='Only cache 10000 commits of history')
454 parser.add_option('--ref', action='append',
455 help='Specify additional refs to be fetched')
hinoka@google.com563559c2014-04-02 00:36:24 +0000456 parser.add_option('--no_bootstrap', action='store_true',
457 help='Don\'t bootstrap from Google Storage')
458
agable@chromium.org5a306a22014-02-24 22:13:59 +0000459 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000460 if not len(args) == 1:
461 parser.error('git cache populate only takes exactly one repo url.')
462 url = args[0]
463
szager@chromium.org848fd492014-04-09 19:06:44 +0000464 mirror = Mirror(url, refs=options.ref)
465 kwargs = {
466 'verbose': options.verbose,
467 'shallow': options.shallow,
468 'bootstrap': not options.no_bootstrap,
469 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000470 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000471 kwargs['depth'] = options.depth
472 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000473
474
475@subcommand.usage('[url of repo to unlock, or -a|--all]')
476def CMDunlock(parser, args):
477 """Unlock one or all repos if their lock files are still around."""
478 parser.add_option('--force', '-f', action='store_true',
479 help='Actually perform the action')
480 parser.add_option('--all', '-a', action='store_true',
481 help='Unlock all repository caches')
482 options, args = parser.parse_args(args)
483 if len(args) > 1 or (len(args) == 0 and not options.all):
484 parser.error('git cache unlock takes exactly one repo url, or --all')
485
agable@chromium.org5a306a22014-02-24 22:13:59 +0000486 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000487 cachepath = Mirror.GetCachePath()
488 lockfiles = [os.path.join(cachepath, path)
489 for path in os.listdir(cachepath)
490 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000491 parser.error('git cache unlock requires -f|--force to do anything. '
492 'Refusing to unlock the following repo caches: '
493 ', '.join(lockfiles))
494
szager@chromium.org848fd492014-04-09 19:06:44 +0000495 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000496 if options.all:
497 unlocked_repos.extend(Mirror.UnlockAll())
498 else:
499 m = Mirror(args[0])
500 if m.unlock():
501 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000502
szager@chromium.org848fd492014-04-09 19:06:44 +0000503 if unlocked_repos:
504 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
505 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000506
507
508class OptionParser(optparse.OptionParser):
509 """Wrapper class for OptionParser to handle global options."""
510
511 def __init__(self, *args, **kwargs):
512 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
513 self.add_option('-c', '--cache-dir',
514 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000515 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000516 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000517 self.add_option('-q', '--quiet', action='store_true',
518 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000519
520 def parse_args(self, args=None, values=None):
521 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000522 if options.quiet:
523 options.verbose = 0
524
525 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
526 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000527
528 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000529 global_cache_dir = Mirror.GetCachePath()
530 except RuntimeError:
531 global_cache_dir = None
532 if options.cache_dir:
533 if global_cache_dir and (
534 os.path.abspath(options.cache_dir) !=
535 os.path.abspath(global_cache_dir)):
536 logging.warn('Overriding globally-configured cache directory.')
537 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000538
agable@chromium.org5a306a22014-02-24 22:13:59 +0000539 return options, args
540
541
542def main(argv):
543 dispatcher = subcommand.CommandDispatcher(__name__)
544 return dispatcher.execute(OptionParser(), argv)
545
546
547if __name__ == '__main__':
548 sys.exit(main(sys.argv[1:]))