blob: 7a17e0e8d6a1ec920c1d1df6452cef9acdc89a48 [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/python
maruel@chromium.orgba551772010-02-03 18:21:42 +00002# Copyright (c) 2010 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.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00005
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00006"""Meta checkout manager supporting both Subversion and GIT.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00007
8Files
9 .gclient : Current client configuration, written by 'config' command.
10 Format is a Python script defining 'solutions', a list whose
11 entries each are maps binding the strings "name" and "url"
12 to strings specifying the name and location of the client
13 module, as well as "custom_deps" to a map similar to the DEPS
14 file below.
15 .gclient_entries : A cache constructed by 'update' command. Format is a
16 Python script defining 'entries', a list of the names
17 of all modules in the client
18 <module>/DEPS : Python script defining var 'deps' as a map from each requisite
19 submodule name to a URL where it can be found (via one SCM)
20
21Hooks
22 .gclient and DEPS files may optionally contain a list named "hooks" to
23 allow custom actions to be performed based on files that have changed in the
evan@chromium.org67820ef2009-07-27 17:23:00 +000024 working copy as a result of a "sync"/"update" or "revert" operation. This
maruel@chromium.org0b6a0842010-06-15 14:34:19 +000025 can be prevented by using --nohooks (hooks run by default). Hooks can also
maruel@chromium.org5df6a462009-08-28 18:52:26 +000026 be forced to run with the "runhooks" operation. If "sync" is run with
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027 --force, all known hooks will run regardless of the state of the working
28 copy.
29
30 Each item in a "hooks" list is a dict, containing these two keys:
31 "pattern" The associated value is a string containing a regular
32 expression. When a file whose pathname matches the expression
33 is checked out, updated, or reverted, the hook's "action" will
34 run.
35 "action" A list describing a command to run along with its arguments, if
36 any. An action command will run at most one time per gclient
37 invocation, regardless of how many files matched the pattern.
38 The action is executed in the same directory as the .gclient
39 file. If the first item in the list is the string "python",
40 the current Python interpreter (sys.executable) will be used
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +000041 to run the command. If the list contains string "$matching_files"
42 it will be removed from the list and the list will be extended
43 by the list of matching files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000044
45 Example:
46 hooks = [
47 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
48 "action": ["python", "image_indexer.py", "--all"]},
49 ]
50"""
51
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +000052__version__ = "0.5"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
maruel@chromium.org754960e2009-09-21 12:31:05 +000054import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055import optparse
56import os
msb@chromium.org2e38de72009-09-28 17:04:47 +000057import pprint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000058import re
piman@chromium.org4b90e3a2010-07-01 20:28:26 +000059import subprocess
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060import sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000061import urlparse
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000062import urllib
63
maruel@chromium.orgada4c652009-12-03 15:32:01 +000064import breakpad
65
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000066import gclient_scm
67import gclient_utils
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +000068from third_party.repo.progress import Progress
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000069
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000070
maruel@chromium.org1f7d1182010-05-17 18:17:38 +000071def attr(attr, data):
72 """Sets an attribute on a function."""
73 def hook(fn):
74 setattr(fn, attr, data)
75 return fn
76 return hook
maruel@chromium.orge3da35f2010-03-09 21:40:45 +000077
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000078
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000079## GClient implementation.
80
81
maruel@chromium.org116704f2010-06-11 17:34:38 +000082class GClientKeywords(object):
83 class FromImpl(object):
84 """Used to implement the From() syntax."""
85
86 def __init__(self, module_name, sub_target_name=None):
87 """module_name is the dep module we want to include from. It can also be
88 the name of a subdirectory to include from.
89
90 sub_target_name is an optional parameter if the module name in the other
91 DEPS file is different. E.g., you might want to map src/net to net."""
92 self.module_name = module_name
93 self.sub_target_name = sub_target_name
94
95 def __str__(self):
96 return 'From(%s, %s)' % (repr(self.module_name),
97 repr(self.sub_target_name))
98
maruel@chromium.org116704f2010-06-11 17:34:38 +000099 class FileImpl(object):
100 """Used to implement the File('') syntax which lets you sync a single file
maruel@chromium.orge3216c62010-07-08 03:31:43 +0000101 from a SVN repo."""
maruel@chromium.org116704f2010-06-11 17:34:38 +0000102
103 def __init__(self, file_location):
104 self.file_location = file_location
105
106 def __str__(self):
107 return 'File("%s")' % self.file_location
108
109 def GetPath(self):
110 return os.path.split(self.file_location)[0]
111
112 def GetFilename(self):
113 rev_tokens = self.file_location.split('@')
114 return os.path.split(rev_tokens[0])[1]
115
116 def GetRevision(self):
117 rev_tokens = self.file_location.split('@')
118 if len(rev_tokens) > 1:
119 return rev_tokens[1]
120 return None
121
122 class VarImpl(object):
123 def __init__(self, custom_vars, local_scope):
124 self._custom_vars = custom_vars
125 self._local_scope = local_scope
126
127 def Lookup(self, var_name):
128 """Implements the Var syntax."""
129 if var_name in self._custom_vars:
130 return self._custom_vars[var_name]
131 elif var_name in self._local_scope.get("vars", {}):
132 return self._local_scope["vars"][var_name]
133 raise gclient_utils.Error("Var is not defined: %s" % var_name)
134
135
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000136class Dependency(GClientKeywords):
137 """Object that represents a dependency checkout."""
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000138 DEPS_FILE = 'DEPS'
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000139
maruel@chromium.org0d812442010-08-10 12:41:08 +0000140 def __init__(self, parent, name, url, safesync_url, custom_deps,
141 custom_vars, deps_file):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000142 GClientKeywords.__init__(self)
143 self.parent = parent
144 self.name = name
145 self.url = url
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000146 self.parsed_url = None
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000147 # These 2 are only set in .gclient and not in DEPS files.
148 self.safesync_url = safesync_url
149 self.custom_vars = custom_vars or {}
150 self.custom_deps = custom_deps or {}
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000151 self.deps_hooks = []
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000152 self.dependencies = []
153 self.deps_file = deps_file or self.DEPS_FILE
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000154 # A cache of the files affected by the current operation, necessary for
155 # hooks.
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000156 self._file_list = []
maruel@chromium.org85c2a192010-07-22 21:14:43 +0000157 # If it is not set to True, the dependency wasn't processed for its child
158 # dependency, i.e. its DEPS wasn't read.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000159 self.deps_parsed = False
maruel@chromium.org85c2a192010-07-22 21:14:43 +0000160 # A direct reference is dependency that is referenced by a deps, deps_os or
161 # solution. A indirect one is one that was loaded with From() or that
162 # exceeded recursion limit.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000163 self.direct_reference = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000164
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000165 # Sanity checks
166 if not self.name and self.parent:
167 raise gclient_utils.Error('Dependency without name')
maruel@chromium.orgdefec8e2010-07-23 04:06:57 +0000168 # TODO(maruel): http://crbug.com/50015 Reenable this check once
169 # self.tree(False) is corrected.
170 # tree = dict((d.name, d) for d in self.tree(False))
171 #if self.name in tree:
172 # raise gclient_utils.Error(
173 # 'Dependency %s specified more than once:\n %s\nvs\n %s' %
174 # (self.name, tree[self.name].hierarchy(), self.hierarchy()))
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000175 if not isinstance(self.url,
176 (basestring, self.FromImpl, self.FileImpl, None.__class__)):
177 raise gclient_utils.Error('dependency url must be either a string, None, '
178 'File() or From() instead of %s' %
179 self.url.__class__.__name__)
180 if '/' in self.deps_file or '\\' in self.deps_file:
181 raise gclient_utils.Error('deps_file name must not be a path, just a '
182 'filename. %s' % self.deps_file)
183
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000184 def LateOverride(self, url):
maruel@chromium.orgda7a1f92010-08-10 17:19:02 +0000185 """Resolves the parsed url from url.
186
187 Manages From() keyword accordingly. Do not touch self.parsed_url nor
188 self.url because it may called with other urls due to From()."""
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000189 overriden_url = self.get_custom_deps(self.name, url)
190 if overriden_url != url:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000191 logging.debug('%s, %s was overriden to %s' % (self.name, url,
maruel@chromium.orgda7a1f92010-08-10 17:19:02 +0000192 overriden_url))
193 return overriden_url
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000194 elif isinstance(url, self.FromImpl):
195 ref = [dep for dep in self.tree(True) if url.module_name == dep.name]
196 if not len(ref) == 1:
197 raise Exception('Failed to find one reference to %s. %s' % (
198 url.module_name, ref))
199 ref = ref[0]
maruel@chromium.org98d05fa2010-07-22 21:58:01 +0000200 sub_target = url.sub_target_name or self.name
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000201 # Make sure the referenced dependency DEPS file is loaded and file the
202 # inner referenced dependency.
203 ref.ParseDepsFile(False)
204 found_dep = None
205 for d in ref.dependencies:
206 if d.name == sub_target:
207 found_dep = d
208 break
209 if not found_dep:
210 raise Exception('Couldn\'t find %s in %s, referenced by %s' % (
211 sub_target, ref.name, self.name))
212 # Call LateOverride() again.
maruel@chromium.orgda7a1f92010-08-10 17:19:02 +0000213 parsed_url = found_dep.LateOverride(found_dep.url)
214 logging.debug('%s, %s to %s' % (self.name, url, parsed_url))
215 return parsed_url
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000216 elif isinstance(url, basestring):
217 parsed_url = urlparse.urlparse(url)
218 if not parsed_url[0]:
219 # A relative url. Fetch the real base.
220 path = parsed_url[2]
221 if not path.startswith('/'):
222 raise gclient_utils.Error(
223 'relative DEPS entry \'%s\' must begin with a slash' % url)
224 # Create a scm just to query the full url.
225 parent_url = self.parent.parsed_url
226 if isinstance(parent_url, self.FileImpl):
227 parent_url = parent_url.file_location
228 scm = gclient_scm.CreateSCM(parent_url, self.root_dir(), None)
maruel@chromium.orgda7a1f92010-08-10 17:19:02 +0000229 parsed_url = scm.FullUrlForRelativeUrl(url)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000230 else:
maruel@chromium.orgda7a1f92010-08-10 17:19:02 +0000231 parsed_url = url
232 logging.debug('%s, %s -> %s' % (self.name, url, parsed_url))
233 return parsed_url
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000234 elif isinstance(url, self.FileImpl):
maruel@chromium.orgda7a1f92010-08-10 17:19:02 +0000235 parsed_url = url
236 logging.debug('%s, %s -> %s (File)' % (self.name, url, parsed_url))
237 return parsed_url
238 elif url is None:
239 return None
240 else:
241 raise gclient_utils.Error('Unkown url type')
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000242
maruel@chromium.org271375b2010-06-23 19:17:38 +0000243 def ParseDepsFile(self, direct_reference):
244 """Parses the DEPS file for this dependency."""
245 if direct_reference:
246 # Maybe it was referenced earlier by a From() keyword but it's now
247 # directly referenced.
248 self.direct_reference = direct_reference
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000249 if self.deps_parsed:
250 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000251 self.deps_parsed = True
252 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
253 if not os.path.isfile(filepath):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000254 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000255 deps_content = gclient_utils.FileRead(filepath)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000256
maruel@chromium.org271375b2010-06-23 19:17:38 +0000257 # Eval the content.
258 # One thing is unintuitive, vars= {} must happen before Var() use.
259 local_scope = {}
260 var = self.VarImpl(self.custom_vars, local_scope)
261 global_scope = {
262 'File': self.FileImpl,
263 'From': self.FromImpl,
264 'Var': var.Lookup,
265 'deps_os': {},
266 }
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000267 try:
268 exec(deps_content, global_scope, local_scope)
269 except SyntaxError, e:
270 gclient_utils.SyntaxErrorToError(filepath, e)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000271 deps = local_scope.get('deps', {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272 # load os specific dependencies if defined. these dependencies may
273 # override or extend the values defined by the 'deps' member.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000274 if 'deps_os' in local_scope:
275 for deps_os_key in self.enforced_os():
276 os_deps = local_scope['deps_os'].get(deps_os_key, {})
277 if len(self.enforced_os()) > 1:
278 # Ignore any conflict when including deps for more than one
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000279 # platform, so we collect the broadest set of dependencies available.
280 # We may end up with the wrong revision of something for our
281 # platform, but this is the best we can do.
282 deps.update([x for x in os_deps.items() if not x[0] in deps])
283 else:
284 deps.update(os_deps)
285
maruel@chromium.org271375b2010-06-23 19:17:38 +0000286 self.deps_hooks.extend(local_scope.get('hooks', []))
287
288 # If a line is in custom_deps, but not in the solution, we want to append
289 # this line to the solution.
290 for d in self.custom_deps:
291 if d not in deps:
292 deps[d] = self.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000293
294 # If use_relative_paths is set in the DEPS file, regenerate
295 # the dictionary using paths relative to the directory containing
296 # the DEPS file.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000297 use_relative_paths = local_scope.get('use_relative_paths', False)
298 if use_relative_paths:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000299 rel_deps = {}
300 for d, url in deps.items():
301 # normpath is required to allow DEPS to use .. in their
302 # dependency local path.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000303 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
304 deps = rel_deps
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000306 # Convert the deps into real Dependency.
307 for name, url in deps.iteritems():
308 if name in [s.name for s in self.dependencies]:
309 raise
maruel@chromium.org0d812442010-08-10 12:41:08 +0000310 self.dependencies.append(Dependency(self, name, url, None, None, None,
311 None))
maruel@chromium.orgda7a1f92010-08-10 17:19:02 +0000312 # Sorting by name would in theory make the whole thing coherent, since
313 # subdirectories will be sorted after the parent directory, but that doens't
314 # work with From() that fetch from a dependency with a name being sorted
315 # later. But if this would be removed right now, many projects wouldn't be
316 # able to sync anymore.
bradnelson@google.com5f8f2a82010-07-23 00:05:46 +0000317 self.dependencies.sort(key=lambda x: x.name)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000318 logging.info('Loaded: %s' % str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000319
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000320 def RunCommandRecursively(self, options, revision_overrides,
321 command, args, pm):
322 """Runs 'command' before parsing the DEPS in case it's a initial checkout
323 or a revert."""
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000324 assert self._file_list == []
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000325 # When running runhooks, there's no need to consult the SCM.
326 # All known hooks are expected to run unconditionally regardless of working
327 # copy state, so skip the SCM status check.
328 run_scm = command not in ('runhooks', None)
maruel@chromium.orgda7a1f92010-08-10 17:19:02 +0000329 self.parsed_url = self.LateOverride(self.url)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000330 if run_scm and self.parsed_url:
331 if isinstance(self.parsed_url, self.FileImpl):
332 # Special support for single-file checkout.
333 if not command in (None, 'cleanup', 'diff', 'pack', 'status'):
334 options.revision = self.parsed_url.GetRevision()
335 scm = gclient_scm.SVNWrapper(self.parsed_url.GetPath(),
336 self.root_dir(),
337 self.name)
338 scm.RunCommand('updatesingle', options,
339 args + [self.parsed_url.GetFilename()],
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000340 self._file_list)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000341 else:
342 options.revision = revision_overrides.get(self.name)
343 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name)
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000344 scm.RunCommand(command, options, args, self._file_list)
345 self._file_list = [os.path.join(self.name, f.strip())
346 for f in self._file_list]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000347 options.revision = None
348 if pm:
349 # The + 1 comes from the fact that .gclient is considered a step in
350 # itself, .i.e. this code is called one time for the .gclient. This is not
351 # conceptually correct but it simplifies code.
352 pm._total = len(self.tree(False)) + 1
353 pm.update()
354 if self.recursion_limit():
355 # Then we can parse the DEPS file.
356 self.ParseDepsFile(True)
357 if pm:
358 pm._total = len(self.tree(False)) + 1
359 pm.update(0)
360 # Parse the dependencies of this dependency.
361 for s in self.dependencies:
362 # TODO(maruel): All these can run concurrently! No need for threads,
363 # just buffer stdout&stderr on pipes and flush as they complete.
364 # Watch out for stdin.
365 s.RunCommandRecursively(options, revision_overrides, command, args, pm)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000366
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000367 def RunHooksRecursively(self, options):
368 """Evaluates all hooks, running actions as needed. RunCommandRecursively()
369 must have been called before to load the DEPS."""
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000370 # If "--force" was specified, run all hooks regardless of what files have
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000371 # changed.
372 if self.deps_hooks and self.direct_reference:
373 # TODO(maruel): If the user is using git or git-svn, then we don't know
374 # what files have changed so we always run all hooks. It'd be nice to fix
375 # that.
376 if (options.force or
377 isinstance(self.parsed_url, self.FileImpl) or
378 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or
379 os.path.isdir(os.path.join(self.root_dir(), self.name, '.git'))):
380 for hook_dict in self.deps_hooks:
381 self._RunHookAction(hook_dict, [])
382 else:
383 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
384 # Convert all absolute paths to relative.
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000385 file_list = self.file_list()
386 for i in range(len(file_list)):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000387 # It depends on the command being executed (like runhooks vs sync).
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000388 if not os.path.isabs(file_list[i]):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000389 continue
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000390
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000391 prefix = os.path.commonprefix([self.root_dir().lower(),
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000392 file_list[i].lower()])
393 file_list[i] = file_list[i][len(prefix):]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000394
395 # Strip any leading path separators.
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000396 while (file_list[i].startswith('\\') or
397 file_list[i].startswith('/')):
398 file_list[i] = file_list[i][1:]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000399
400 # Run hooks on the basis of whether the files from the gclient operation
401 # match each hook's pattern.
402 for hook_dict in self.deps_hooks:
403 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000404 matching_file_list = [f for f in file_list if pattern.search(f)]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000405 if matching_file_list:
406 self._RunHookAction(hook_dict, matching_file_list)
407 if self.recursion_limit():
408 for s in self.dependencies:
409 s.RunHooksRecursively(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000410
maruel@chromium.orgeaf61062010-07-07 18:42:39 +0000411 def _RunHookAction(self, hook_dict, matching_file_list):
412 """Runs the action from a single hook."""
413 logging.info(hook_dict)
414 logging.info(matching_file_list)
415 command = hook_dict['action'][:]
416 if command[0] == 'python':
417 # If the hook specified "python" as the first item, the action is a
418 # Python script. Run it by starting a new copy of the same
419 # interpreter.
420 command[0] = sys.executable
421
422 if '$matching_files' in command:
423 splice_index = command.index('$matching_files')
424 command[splice_index:splice_index + 1] = matching_file_list
425
426 # Use a discrete exit status code of 2 to indicate that a hook action
427 # failed. Users of this script may wish to treat hook action failures
428 # differently from VC failures.
429 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
430
maruel@chromium.org271375b2010-06-23 19:17:38 +0000431 def root_dir(self):
432 return self.parent.root_dir()
433
434 def enforced_os(self):
435 return self.parent.enforced_os()
436
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000437 def recursion_limit(self):
438 return self.parent.recursion_limit() - 1
439
maruel@chromium.org0d812442010-08-10 12:41:08 +0000440 def tree(self, include_all):
441 return self.parent.tree(include_all)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000442
maruel@chromium.org0d812442010-08-10 12:41:08 +0000443 def subtree(self, include_all):
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000444 result = []
445 # Add breadth-first.
maruel@chromium.org0d812442010-08-10 12:41:08 +0000446 if self.direct_reference or include_all:
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000447 for d in self.dependencies:
maruel@chromium.org044f4e32010-07-22 21:59:57 +0000448 result.append(d)
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000449 for d in self.dependencies:
maruel@chromium.org0d812442010-08-10 12:41:08 +0000450 result.extend(d.subtree(include_all))
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000451 return result
452
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000453 def get_custom_deps(self, name, url):
454 """Returns a custom deps if applicable."""
455 if self.parent:
456 url = self.parent.get_custom_deps(name, url)
457 # None is a valid return value to disable a dependency.
458 return self.custom_deps.get(name, url)
459
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000460 def file_list(self):
461 result = self._file_list[:]
462 for d in self.dependencies:
463 result.extend(d.file_list())
464 return result
465
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000466 def __str__(self):
467 out = []
468 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000469 'deps_hooks', '_file_list'):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000470 # 'deps_file'
471 if self.__dict__[i]:
472 out.append('%s: %s' % (i, self.__dict__[i]))
473
474 for d in self.dependencies:
475 out.extend([' ' + x for x in str(d).splitlines()])
476 out.append('')
477 return '\n'.join(out)
478
479 def __repr__(self):
480 return '%s: %s' % (self.name, self.url)
481
maruel@chromium.orgbffb9042010-07-22 20:59:36 +0000482 def hierarchy(self):
maruel@chromium.orgbc2d2f92010-07-22 21:26:48 +0000483 """Returns a human-readable hierarchical reference to a Dependency."""
maruel@chromium.orgbffb9042010-07-22 20:59:36 +0000484 out = '%s(%s)' % (self.name, self.url)
485 i = self.parent
486 while i and i.name:
487 out = '%s(%s) -> %s' % (i.name, i.url, out)
488 i = i.parent
489 return out
490
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000491
492class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000493 """Object that represent a gclient checkout. A tree of Dependency(), one per
494 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000495
496 DEPS_OS_CHOICES = {
497 "win32": "win",
498 "win": "win",
499 "cygwin": "win",
500 "darwin": "mac",
501 "mac": "mac",
502 "unix": "unix",
503 "linux": "unix",
504 "linux2": "unix",
505 }
506
507 DEFAULT_CLIENT_FILE_TEXT = ("""\
508solutions = [
509 { "name" : "%(solution_name)s",
510 "url" : "%(solution_url)s",
511 "custom_deps" : {
512 },
maruel@chromium.org73e21142010-07-05 13:32:01 +0000513 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000514 },
515]
516""")
517
518 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
519 { "name" : "%(solution_name)s",
520 "url" : "%(solution_url)s",
521 "custom_deps" : {
maruel@chromium.org73e21142010-07-05 13:32:01 +0000522%(solution_deps)s },
523 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000524 },
525""")
526
527 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
528# Snapshot generated with gclient revinfo --snapshot
529solutions = [
maruel@chromium.org73e21142010-07-05 13:32:01 +0000530%(solution_list)s]
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000531""")
532
533 def __init__(self, root_dir, options):
maruel@chromium.org0d812442010-08-10 12:41:08 +0000534 # Do not change previous behavior. Only solution level and immediate DEPS
535 # are processed.
536 self._recursion_limit = 2
537 Dependency.__init__(self, None, None, None, None, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000538 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000539 if options.deps_os:
540 enforced_os = options.deps_os.split(',')
541 else:
542 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
543 if 'all' in enforced_os:
544 enforced_os = self.DEPS_OS_CHOICES.itervalues()
545 self._enforced_os = list(set(enforced_os))
546 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000547 self.config_content = None
548
549 def SetConfig(self, content):
550 assert self.dependencies == []
551 config_dict = {}
552 self.config_content = content
553 try:
554 exec(content, config_dict)
555 except SyntaxError, e:
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000556 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000557 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000558 try:
559 self.dependencies.append(Dependency(
560 self, s['name'], s['url'],
561 s.get('safesync_url', None),
562 s.get('custom_deps', {}),
maruel@chromium.org0d812442010-08-10 12:41:08 +0000563 s.get('custom_vars', {}),
564 None))
maruel@chromium.org81843b82010-06-28 16:49:26 +0000565 except KeyError:
566 raise gclient_utils.Error('Invalid .gclient file. Solution is '
567 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000568 # .gclient can have hooks.
569 self.deps_hooks = config_dict.get('hooks', [])
570
571 def SaveConfig(self):
572 gclient_utils.FileWrite(os.path.join(self.root_dir(),
573 self._options.config_filename),
574 self.config_content)
575
576 @staticmethod
577 def LoadCurrentConfig(options):
578 """Searches for and loads a .gclient file relative to the current working
579 dir. Returns a GClient object."""
580 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
581 if not path:
582 return None
583 client = GClient(path, options)
584 client.SetConfig(gclient_utils.FileRead(
585 os.path.join(path, options.config_filename)))
586 return client
587
588 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
589 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
590 'solution_name': solution_name,
591 'solution_url': solution_url,
592 'safesync_url' : safesync_url,
593 })
594
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000595 def _SaveEntries(self):
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000596 """Creates a .gclient_entries file to record the list of unique checkouts.
597
598 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000599 """
600 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
601 # makes testing a bit too fun.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000602 result = 'entries = {\n'
603 for entry in self.tree(False):
604 # Skip over File() dependencies as we can't version them.
605 if not isinstance(entry.parsed_url, self.FileImpl):
606 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
607 pprint.pformat(entry.parsed_url))
608 result += '}\n'
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000609 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000610 logging.info(result)
611 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000612
613 def _ReadEntries(self):
614 """Read the .gclient_entries file for the given client.
615
616 Returns:
617 A sequence of solution names, which will be empty if there is the
618 entries file hasn't been created yet.
619 """
620 scope = {}
621 filename = os.path.join(self.root_dir(), self._options.entries_filename)
622 if not os.path.exists(filename):
maruel@chromium.org73e21142010-07-05 13:32:01 +0000623 return {}
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000624 try:
625 exec(gclient_utils.FileRead(filename), scope)
626 except SyntaxError, e:
627 gclient_utils.SyntaxErrorToError(filename, e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000628 return scope['entries']
629
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000630 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000631 """Checks for revision overrides."""
632 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000633 if self._options.head:
634 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000635 for s in self.dependencies:
636 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000637 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000638 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000639 rev = handle.read().strip()
640 handle.close()
641 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000642 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000643 if not self._options.revisions:
644 return revision_overrides
645 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000646 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000647 index = 0
648 for revision in self._options.revisions:
649 if not '@' in revision:
650 # Support for --revision 123
651 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000652 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000653 if not sol in solutions_names:
654 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
655 print >> sys.stderr, ('Please fix your script, having invalid '
656 '--revision flags will soon considered an error.')
657 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000658 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000659 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000660 return revision_overrides
661
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000662 def RunOnDeps(self, command, args):
663 """Runs a command on each dependency in a client and its dependencies.
664
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000665 Args:
666 command: The command to use (e.g., 'status' or 'diff')
667 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000668 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000669 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000670 raise gclient_utils.Error('No solution specified')
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000671 revision_overrides = self._EnforceRevisions()
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000672 pm = None
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000673 if command == 'update' and not self._options.verbose:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000674 pm = Progress('Syncing projects', len(self.tree(False)) + 1)
675 self.RunCommandRecursively(self._options, revision_overrides,
676 command, args, pm)
677 if pm:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000678 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000679
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000680 # Once all the dependencies have been processed, it's now safe to run the
681 # hooks.
682 if not self._options.nohooks:
683 self.RunHooksRecursively(self._options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684
685 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000686 # Notify the user if there is an orphaned entry in their working copy.
687 # Only delete the directory if there are no changes in it, and
688 # delete_unversioned_trees is set to true.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000689 entries = [i.name for i in self.tree(False)]
690 for entry, prev_url in self._ReadEntries().iteritems():
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000691 # Fix path separator on Windows.
692 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000693 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000694 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000695 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000696 file_list = []
697 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
698 scm.status(self._options, [], file_list)
699 modified_files = file_list != []
msb@chromium.org83017012009-09-28 18:52:12 +0000700 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000701 # There are modified files in this entry. Keep warning until
702 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000703 print(('\nWARNING: \'%s\' is no longer part of this client. '
704 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000705 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706 else:
707 # Delete the entry
maruel@chromium.org73e21142010-07-05 13:32:01 +0000708 print('\n________ deleting \'%s\' in \'%s\'' % (
709 entry_fixed, self.root_dir()))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000710 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000711 # record the current list of entries for next time
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000712 self._SaveEntries()
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000713 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714
715 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000716 """Output revision info mapping for the client and its dependencies.
717
718 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000719 can be used to reproduce the same tree in the future. It is only useful for
720 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
721 number or a git hash. A git branch name isn't "pinned" since the actual
722 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000723
724 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000726 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000727 raise gclient_utils.Error('No solution specified')
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000728 # Load all the settings.
729 self.RunCommandRecursively(self._options, {}, None, [], None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000730
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731 def GetURLAndRev(name, original_url):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000732 """Returns the revision-qualified SCM url."""
733 if original_url is None:
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000734 return None
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000735 if isinstance(original_url, self.FileImpl):
736 original_url = original_url.file_location
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000737 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000738 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000739 if not os.path.isdir(scm.checkout_path):
740 return None
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000741 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000743 if self._options.snapshot:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000744 new_gclient = ''
745 # First level at .gclient
746 for d in self.dependencies:
747 entries = {}
748 def GrabDeps(sol):
749 """Recursively grab dependencies."""
750 for i in sol.dependencies:
751 entries[i.name] = GetURLAndRev(i.name, i.parsed_url)
752 GrabDeps(i)
753 GrabDeps(d)
754 custom_deps = []
755 for k in sorted(entries.keys()):
756 if entries[k]:
757 # Quotes aren't escaped...
758 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
759 else:
760 custom_deps.append(' \"%s\": None,\n' % k)
761 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
762 'solution_name': d.name,
763 'solution_url': d.url,
764 'safesync_url' : d.safesync_url or '',
765 'solution_deps': ''.join(custom_deps),
766 }
767 # Print the snapshot configuration file
768 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000769 else:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000770 entries = sorted(self.tree(False), key=lambda i: i.name)
771 for entry in entries:
772 entry_url = GetURLAndRev(entry.name, entry.parsed_url)
773 line = '%s: %s' % (entry.name, entry_url)
774 if not entry is entries[-1]:
775 line += ';'
776 print line
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000777
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000778 def ParseDepsFile(self, direct_reference):
779 """No DEPS to parse for a .gclient file."""
maruel@chromium.org98d05fa2010-07-22 21:58:01 +0000780 self.direct_reference = True
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000781 self.deps_parsed = True
782
maruel@chromium.org75a59272010-06-11 22:34:03 +0000783 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000784 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000785 return self._root_dir
786
maruel@chromium.org271375b2010-06-23 19:17:38 +0000787 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000788 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000789 return self._enforced_os
790
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000791 def recursion_limit(self):
792 """How recursive can each dependencies in DEPS file can load DEPS file."""
793 return self._recursion_limit
794
maruel@chromium.org0d812442010-08-10 12:41:08 +0000795 def tree(self, include_all):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000796 """Returns a flat list of all the dependencies."""
maruel@chromium.org0d812442010-08-10 12:41:08 +0000797 return self.subtree(include_all)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000798
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000799
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000800#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000801
802
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000803def CMDcleanup(parser, args):
804 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000805
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000806Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000807"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000808 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
809 help='override deps for the specified (comma-separated) '
810 'platform(s); \'all\' will process all deps_os '
811 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000812 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000813 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000814 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000815 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000816 if options.verbose:
817 # Print out the .gclient file. This is longer than if we just printed the
818 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000819 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820 return client.RunOnDeps('cleanup', args)
821
822
piman@chromium.org4b90e3a2010-07-01 20:28:26 +0000823@attr('usage', '[command] [args ...]')
824def CMDrecurse(parser, args):
825 """Operates on all the entries.
826
827 Runs a shell command on all entries.
828 """
829 # Stop parsing at the first non-arg so that these go through to the command
830 parser.disable_interspersed_args()
831 parser.add_option('-s', '--scm', action='append', default=[],
832 help='choose scm types to operate upon')
833 options, args = parser.parse_args(args)
834 root, entries = gclient_utils.GetGClientRootAndEntries()
835 scm_set = set()
836 for scm in options.scm:
837 scm_set.update(scm.split(','))
838
839 # Pass in the SCM type as an env variable
840 env = os.environ.copy()
841
842 for path, url in entries.iteritems():
843 scm = gclient_scm.GetScmName(url)
844 if scm_set and scm not in scm_set:
845 continue
846 dir = os.path.normpath(os.path.join(root, path))
847 env['GCLIENT_SCM'] = scm
848 env['GCLIENT_URL'] = url
849 subprocess.Popen(args, cwd=dir, env=env).communicate()
850
851
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000852@attr('usage', '[url] [safesync url]')
853def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000854 """Create a .gclient file in the current directory.
855
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000856This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000857top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000858modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000859provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000860URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000861"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000862 parser.add_option('--spec',
863 help='create a gclient file containing the provided '
864 'string. Due to Cygwin/Python brokenness, it '
865 'probably can\'t contain any newlines.')
866 parser.add_option('--name',
867 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000868 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000869 if ((options.spec and args) or len(args) > 2 or
870 (not options.spec and not args)):
871 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
872
maruel@chromium.org0329e672009-05-13 18:41:04 +0000873 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000874 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000875 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000876 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000877 if options.spec:
878 client.SetConfig(options.spec)
879 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000880 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000881 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000882 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000883 else:
884 # specify an alternate relpath for the given URL.
885 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000886 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000887 if len(args) > 1:
888 safesync_url = args[1]
889 client.SetDefaultConfig(name, base_url, safesync_url)
890 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000891 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000892
893
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000894def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000895 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000896 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
897 help='override deps for the specified (comma-separated) '
898 'platform(s); \'all\' will process all deps_os '
899 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000900 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000901 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000902 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000903 client = GClient.LoadCurrentConfig(options)
904
905 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000906 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000907
908 if options.verbose:
909 # Print out the .gclient file. This is longer than if we just printed the
910 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000911 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000912 return client.RunOnDeps('export', args)
913
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000914
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000915@attr('epilog', """Example:
916 gclient pack > patch.txt
917 generate simple patch for configured client and dependences
918""")
919def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000920 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000921
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000922Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000923dependencies, and performs minimal postprocessing of the output. The
924resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000925checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000926"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000927 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
928 help='override deps for the specified (comma-separated) '
929 'platform(s); \'all\' will process all deps_os '
930 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000931 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000932 client = GClient.LoadCurrentConfig(options)
933 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000934 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000935 if options.verbose:
936 # Print out the .gclient file. This is longer than if we just printed the
937 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000938 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000939 return client.RunOnDeps('pack', args)
940
941
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000942def CMDstatus(parser, args):
943 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000944 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
945 help='override deps for the specified (comma-separated) '
946 'platform(s); \'all\' will process all deps_os '
947 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000948 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000949 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000950 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000951 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000952 if options.verbose:
953 # Print out the .gclient file. This is longer than if we just printed the
954 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000955 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000956 return client.RunOnDeps('status', args)
957
958
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000959@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000960 gclient sync
961 update files from SCM according to current configuration,
962 *for modules which have changed since last update or sync*
963 gclient sync --force
964 update files from SCM according to current configuration, for
965 all modules (useful for recovering files deleted from local copy)
966 gclient sync --revision src@31000
967 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000968""")
969def CMDsync(parser, args):
970 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000971 parser.add_option('-f', '--force', action='store_true',
972 help='force update even for unchanged modules')
973 parser.add_option('-n', '--nohooks', action='store_true',
974 help='don\'t run hooks after the update is complete')
975 parser.add_option('-r', '--revision', action='append',
976 dest='revisions', metavar='REV', default=[],
977 help='Enforces revision/hash for the solutions with the '
978 'format src@rev. The src@ part is optional and can be '
979 'skipped. -r can be used multiple times when .gclient '
980 'has multiple solutions configured and will work even '
981 'if the src@ part is skipped.')
982 parser.add_option('-H', '--head', action='store_true',
983 help='skips any safesync_urls specified in '
984 'configured solutions and sync to head instead')
985 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
986 help='delete any unexpected unversioned trees '
987 'that are in the checkout')
988 parser.add_option('-R', '--reset', action='store_true',
989 help='resets any local changes before updating (git only)')
990 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
991 help='override deps for the specified (comma-separated) '
992 'platform(s); \'all\' will process all deps_os '
993 'references')
994 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
995 help='Skip svn up whenever possible by requesting '
996 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000997 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000998 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000999
1000 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001001 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001002
maruel@chromium.org307d1792010-05-31 20:03:13 +00001003 if options.revisions and options.head:
1004 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001005 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006
1007 if options.verbose:
1008 # Print out the .gclient file. This is longer than if we just printed the
1009 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001010 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001011 return client.RunOnDeps('update', args)
1012
1013
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001014def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001015 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001016 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001017
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001018def CMDdiff(parser, args):
1019 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001020 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1021 help='override deps for the specified (comma-separated) '
1022 'platform(s); \'all\' will process all deps_os '
1023 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001024 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001025 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001026 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001027 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001028 if options.verbose:
1029 # Print out the .gclient file. This is longer than if we just printed the
1030 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001031 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001032 return client.RunOnDeps('diff', args)
1033
1034
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001035def CMDrevert(parser, args):
1036 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001037 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1038 help='override deps for the specified (comma-separated) '
1039 'platform(s); \'all\' will process all deps_os '
1040 'references')
1041 parser.add_option('-n', '--nohooks', action='store_true',
1042 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001043 (options, args) = parser.parse_args(args)
1044 # --force is implied.
1045 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001046 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001048 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001049 return client.RunOnDeps('revert', args)
1050
1051
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001052def CMDrunhooks(parser, args):
1053 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001054 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1055 help='override deps for the specified (comma-separated) '
1056 'platform(s); \'all\' will process all deps_os '
1057 'references')
1058 parser.add_option('-f', '--force', action='store_true', default=True,
1059 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001060 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001061 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001062 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001063 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001064 if options.verbose:
1065 # Print out the .gclient file. This is longer than if we just printed the
1066 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001067 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001068 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001069 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001070 return client.RunOnDeps('runhooks', args)
1071
1072
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001073def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001074 """Output revision info mapping for the client and its dependencies.
1075
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001076 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001077 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001078 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1079 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001080 commit can change.
1081 """
1082 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1083 help='override deps for the specified (comma-separated) '
1084 'platform(s); \'all\' will process all deps_os '
1085 'references')
1086 parser.add_option('-s', '--snapshot', action='store_true',
1087 help='creates a snapshot .gclient file of the current '
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +00001088 'version of all repositories to reproduce the tree')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001089 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001090 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001091 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001092 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001093 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001094 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001095
1096
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001097def Command(name):
1098 return getattr(sys.modules[__name__], 'CMD' + name, None)
1099
1100
1101def CMDhelp(parser, args):
1102 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001103 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001104 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001105 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001106 parser.print_help()
1107 return 0
1108
1109
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001110def GenUsage(parser, command):
1111 """Modify an OptParse object with the function's documentation."""
1112 obj = Command(command)
1113 if command == 'help':
1114 command = '<command>'
1115 # OptParser.description prefer nicely non-formatted strings.
1116 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1117 usage = getattr(obj, 'usage', '')
1118 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1119 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001120
1121
1122def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001123 """Doesn't parse the arguments here, just find the right subcommand to
1124 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001125 try:
1126 # Do it late so all commands are listed.
1127 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1128 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1129 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1130 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001131 parser.add_option('-v', '--verbose', action='count', default=0,
1132 help='Produces additional output for diagnostics. Can be '
1133 'used up to three times for more logging info.')
1134 parser.add_option('--gclientfile', dest='config_filename',
1135 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1136 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001137 # Integrate standard options processing.
1138 old_parser = parser.parse_args
1139 def Parse(args):
1140 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001141 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001142 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001143 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001144 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001145 level = logging.DEBUG
1146 logging.basicConfig(level=level,
1147 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1148 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001149
1150 # These hacks need to die.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001151 if not hasattr(options, 'revisions'):
1152 # GClient.RunOnDeps expects it even if not applicable.
1153 options.revisions = []
1154 if not hasattr(options, 'head'):
1155 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001156 if not hasattr(options, 'nohooks'):
1157 options.nohooks = True
1158 if not hasattr(options, 'deps_os'):
1159 options.deps_os = None
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001160 if not hasattr(options, 'manually_grab_svn_rev'):
1161 options.manually_grab_svn_rev = None
1162 if not hasattr(options, 'force'):
1163 options.force = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001164 return (options, args)
1165 parser.parse_args = Parse
1166 # We don't want wordwrapping in epilog (usually examples)
1167 parser.format_epilog = lambda _: parser.epilog or ''
1168 if argv:
1169 command = Command(argv[0])
1170 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001171 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001172 GenUsage(parser, argv[0])
1173 return command(parser, argv[1:])
1174 # Not a known command. Default to help.
1175 GenUsage(parser, 'help')
1176 return CMDhelp(parser, argv)
1177 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001178 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001179 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001180
1181
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001182if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001183 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001184
1185# vim: ts=2:sw=2:tw=80:et: