blob: 9a654c752d74801c83d3c1fb6127977ebfb4b59d [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):
185 overriden_url = self.get_custom_deps(self.name, url)
186 if overriden_url != url:
187 self.parsed_url = overriden_url
188 logging.debug('%s, %s was overriden to %s' % (self.name, url,
189 self.parsed_url))
190 elif isinstance(url, self.FromImpl):
191 ref = [dep for dep in self.tree(True) if url.module_name == dep.name]
192 if not len(ref) == 1:
193 raise Exception('Failed to find one reference to %s. %s' % (
194 url.module_name, ref))
195 ref = ref[0]
maruel@chromium.org98d05fa2010-07-22 21:58:01 +0000196 sub_target = url.sub_target_name or self.name
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000197 # Make sure the referenced dependency DEPS file is loaded and file the
198 # inner referenced dependency.
199 ref.ParseDepsFile(False)
200 found_dep = None
201 for d in ref.dependencies:
202 if d.name == sub_target:
203 found_dep = d
204 break
205 if not found_dep:
206 raise Exception('Couldn\'t find %s in %s, referenced by %s' % (
207 sub_target, ref.name, self.name))
208 # Call LateOverride() again.
209 self.parsed_url = found_dep.LateOverride(found_dep.url)
210 logging.debug('%s, %s to %s' % (self.name, url, self.parsed_url))
211 elif isinstance(url, basestring):
212 parsed_url = urlparse.urlparse(url)
213 if not parsed_url[0]:
214 # A relative url. Fetch the real base.
215 path = parsed_url[2]
216 if not path.startswith('/'):
217 raise gclient_utils.Error(
218 'relative DEPS entry \'%s\' must begin with a slash' % url)
219 # Create a scm just to query the full url.
220 parent_url = self.parent.parsed_url
221 if isinstance(parent_url, self.FileImpl):
222 parent_url = parent_url.file_location
223 scm = gclient_scm.CreateSCM(parent_url, self.root_dir(), None)
224 self.parsed_url = scm.FullUrlForRelativeUrl(url)
225 else:
226 self.parsed_url = url
227 logging.debug('%s, %s -> %s' % (self.name, url, self.parsed_url))
228 elif isinstance(url, self.FileImpl):
229 self.parsed_url = url
230 logging.debug('%s, %s -> %s (File)' % (self.name, url, self.parsed_url))
231 return self.parsed_url
232
maruel@chromium.org271375b2010-06-23 19:17:38 +0000233 def ParseDepsFile(self, direct_reference):
234 """Parses the DEPS file for this dependency."""
235 if direct_reference:
236 # Maybe it was referenced earlier by a From() keyword but it's now
237 # directly referenced.
238 self.direct_reference = direct_reference
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000239 if self.deps_parsed:
240 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000241 self.deps_parsed = True
242 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
243 if not os.path.isfile(filepath):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000244 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000245 deps_content = gclient_utils.FileRead(filepath)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000246
maruel@chromium.org271375b2010-06-23 19:17:38 +0000247 # Eval the content.
248 # One thing is unintuitive, vars= {} must happen before Var() use.
249 local_scope = {}
250 var = self.VarImpl(self.custom_vars, local_scope)
251 global_scope = {
252 'File': self.FileImpl,
253 'From': self.FromImpl,
254 'Var': var.Lookup,
255 'deps_os': {},
256 }
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000257 try:
258 exec(deps_content, global_scope, local_scope)
259 except SyntaxError, e:
260 gclient_utils.SyntaxErrorToError(filepath, e)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000261 deps = local_scope.get('deps', {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000262 # load os specific dependencies if defined. these dependencies may
263 # override or extend the values defined by the 'deps' member.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000264 if 'deps_os' in local_scope:
265 for deps_os_key in self.enforced_os():
266 os_deps = local_scope['deps_os'].get(deps_os_key, {})
267 if len(self.enforced_os()) > 1:
268 # Ignore any conflict when including deps for more than one
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269 # platform, so we collect the broadest set of dependencies available.
270 # We may end up with the wrong revision of something for our
271 # platform, but this is the best we can do.
272 deps.update([x for x in os_deps.items() if not x[0] in deps])
273 else:
274 deps.update(os_deps)
275
maruel@chromium.org271375b2010-06-23 19:17:38 +0000276 self.deps_hooks.extend(local_scope.get('hooks', []))
277
278 # If a line is in custom_deps, but not in the solution, we want to append
279 # this line to the solution.
280 for d in self.custom_deps:
281 if d not in deps:
282 deps[d] = self.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283
284 # If use_relative_paths is set in the DEPS file, regenerate
285 # the dictionary using paths relative to the directory containing
286 # the DEPS file.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000287 use_relative_paths = local_scope.get('use_relative_paths', False)
288 if use_relative_paths:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289 rel_deps = {}
290 for d, url in deps.items():
291 # normpath is required to allow DEPS to use .. in their
292 # dependency local path.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000293 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
294 deps = rel_deps
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000295
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000296 # Convert the deps into real Dependency.
297 for name, url in deps.iteritems():
298 if name in [s.name for s in self.dependencies]:
299 raise
maruel@chromium.org0d812442010-08-10 12:41:08 +0000300 self.dependencies.append(Dependency(self, name, url, None, None, None,
301 None))
bradnelson@google.com5f8f2a82010-07-23 00:05:46 +0000302 # Sort by name.
303 self.dependencies.sort(key=lambda x: x.name)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000304 logging.info('Loaded: %s' % str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000306 def RunCommandRecursively(self, options, revision_overrides,
307 command, args, pm):
308 """Runs 'command' before parsing the DEPS in case it's a initial checkout
309 or a revert."""
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000310 assert self._file_list == []
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000311 # When running runhooks, there's no need to consult the SCM.
312 # All known hooks are expected to run unconditionally regardless of working
313 # copy state, so skip the SCM status check.
314 run_scm = command not in ('runhooks', None)
315 self.LateOverride(self.url)
316 if run_scm and self.parsed_url:
317 if isinstance(self.parsed_url, self.FileImpl):
318 # Special support for single-file checkout.
319 if not command in (None, 'cleanup', 'diff', 'pack', 'status'):
320 options.revision = self.parsed_url.GetRevision()
321 scm = gclient_scm.SVNWrapper(self.parsed_url.GetPath(),
322 self.root_dir(),
323 self.name)
324 scm.RunCommand('updatesingle', options,
325 args + [self.parsed_url.GetFilename()],
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000326 self._file_list)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000327 else:
328 options.revision = revision_overrides.get(self.name)
329 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name)
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000330 scm.RunCommand(command, options, args, self._file_list)
331 self._file_list = [os.path.join(self.name, f.strip())
332 for f in self._file_list]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000333 options.revision = None
334 if pm:
335 # The + 1 comes from the fact that .gclient is considered a step in
336 # itself, .i.e. this code is called one time for the .gclient. This is not
337 # conceptually correct but it simplifies code.
338 pm._total = len(self.tree(False)) + 1
339 pm.update()
340 if self.recursion_limit():
341 # Then we can parse the DEPS file.
342 self.ParseDepsFile(True)
343 if pm:
344 pm._total = len(self.tree(False)) + 1
345 pm.update(0)
346 # Parse the dependencies of this dependency.
347 for s in self.dependencies:
348 # TODO(maruel): All these can run concurrently! No need for threads,
349 # just buffer stdout&stderr on pipes and flush as they complete.
350 # Watch out for stdin.
351 s.RunCommandRecursively(options, revision_overrides, command, args, pm)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000352
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000353 def RunHooksRecursively(self, options):
354 """Evaluates all hooks, running actions as needed. RunCommandRecursively()
355 must have been called before to load the DEPS."""
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000356 # If "--force" was specified, run all hooks regardless of what files have
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000357 # changed.
358 if self.deps_hooks and self.direct_reference:
359 # TODO(maruel): If the user is using git or git-svn, then we don't know
360 # what files have changed so we always run all hooks. It'd be nice to fix
361 # that.
362 if (options.force or
363 isinstance(self.parsed_url, self.FileImpl) or
364 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or
365 os.path.isdir(os.path.join(self.root_dir(), self.name, '.git'))):
366 for hook_dict in self.deps_hooks:
367 self._RunHookAction(hook_dict, [])
368 else:
369 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
370 # Convert all absolute paths to relative.
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000371 file_list = self.file_list()
372 for i in range(len(file_list)):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000373 # It depends on the command being executed (like runhooks vs sync).
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000374 if not os.path.isabs(file_list[i]):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000375 continue
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000376
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000377 prefix = os.path.commonprefix([self.root_dir().lower(),
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000378 file_list[i].lower()])
379 file_list[i] = file_list[i][len(prefix):]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000380
381 # Strip any leading path separators.
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000382 while (file_list[i].startswith('\\') or
383 file_list[i].startswith('/')):
384 file_list[i] = file_list[i][1:]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000385
386 # Run hooks on the basis of whether the files from the gclient operation
387 # match each hook's pattern.
388 for hook_dict in self.deps_hooks:
389 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000390 matching_file_list = [f for f in file_list if pattern.search(f)]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000391 if matching_file_list:
392 self._RunHookAction(hook_dict, matching_file_list)
393 if self.recursion_limit():
394 for s in self.dependencies:
395 s.RunHooksRecursively(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000396
maruel@chromium.orgeaf61062010-07-07 18:42:39 +0000397 def _RunHookAction(self, hook_dict, matching_file_list):
398 """Runs the action from a single hook."""
399 logging.info(hook_dict)
400 logging.info(matching_file_list)
401 command = hook_dict['action'][:]
402 if command[0] == 'python':
403 # If the hook specified "python" as the first item, the action is a
404 # Python script. Run it by starting a new copy of the same
405 # interpreter.
406 command[0] = sys.executable
407
408 if '$matching_files' in command:
409 splice_index = command.index('$matching_files')
410 command[splice_index:splice_index + 1] = matching_file_list
411
412 # Use a discrete exit status code of 2 to indicate that a hook action
413 # failed. Users of this script may wish to treat hook action failures
414 # differently from VC failures.
415 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
416
maruel@chromium.org271375b2010-06-23 19:17:38 +0000417 def root_dir(self):
418 return self.parent.root_dir()
419
420 def enforced_os(self):
421 return self.parent.enforced_os()
422
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000423 def recursion_limit(self):
424 return self.parent.recursion_limit() - 1
425
maruel@chromium.org0d812442010-08-10 12:41:08 +0000426 def tree(self, include_all):
427 return self.parent.tree(include_all)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000428
maruel@chromium.org0d812442010-08-10 12:41:08 +0000429 def subtree(self, include_all):
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000430 result = []
431 # Add breadth-first.
maruel@chromium.org0d812442010-08-10 12:41:08 +0000432 if self.direct_reference or include_all:
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000433 for d in self.dependencies:
maruel@chromium.org044f4e32010-07-22 21:59:57 +0000434 result.append(d)
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000435 for d in self.dependencies:
maruel@chromium.org0d812442010-08-10 12:41:08 +0000436 result.extend(d.subtree(include_all))
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000437 return result
438
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000439 def get_custom_deps(self, name, url):
440 """Returns a custom deps if applicable."""
441 if self.parent:
442 url = self.parent.get_custom_deps(name, url)
443 # None is a valid return value to disable a dependency.
444 return self.custom_deps.get(name, url)
445
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000446 def file_list(self):
447 result = self._file_list[:]
448 for d in self.dependencies:
449 result.extend(d.file_list())
450 return result
451
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000452 def __str__(self):
453 out = []
454 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000455 'deps_hooks', '_file_list'):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000456 # 'deps_file'
457 if self.__dict__[i]:
458 out.append('%s: %s' % (i, self.__dict__[i]))
459
460 for d in self.dependencies:
461 out.extend([' ' + x for x in str(d).splitlines()])
462 out.append('')
463 return '\n'.join(out)
464
465 def __repr__(self):
466 return '%s: %s' % (self.name, self.url)
467
maruel@chromium.orgbffb9042010-07-22 20:59:36 +0000468 def hierarchy(self):
maruel@chromium.orgbc2d2f92010-07-22 21:26:48 +0000469 """Returns a human-readable hierarchical reference to a Dependency."""
maruel@chromium.orgbffb9042010-07-22 20:59:36 +0000470 out = '%s(%s)' % (self.name, self.url)
471 i = self.parent
472 while i and i.name:
473 out = '%s(%s) -> %s' % (i.name, i.url, out)
474 i = i.parent
475 return out
476
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000477
478class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000479 """Object that represent a gclient checkout. A tree of Dependency(), one per
480 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000481
482 DEPS_OS_CHOICES = {
483 "win32": "win",
484 "win": "win",
485 "cygwin": "win",
486 "darwin": "mac",
487 "mac": "mac",
488 "unix": "unix",
489 "linux": "unix",
490 "linux2": "unix",
491 }
492
493 DEFAULT_CLIENT_FILE_TEXT = ("""\
494solutions = [
495 { "name" : "%(solution_name)s",
496 "url" : "%(solution_url)s",
497 "custom_deps" : {
498 },
maruel@chromium.org73e21142010-07-05 13:32:01 +0000499 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000500 },
501]
502""")
503
504 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
505 { "name" : "%(solution_name)s",
506 "url" : "%(solution_url)s",
507 "custom_deps" : {
maruel@chromium.org73e21142010-07-05 13:32:01 +0000508%(solution_deps)s },
509 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000510 },
511""")
512
513 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
514# Snapshot generated with gclient revinfo --snapshot
515solutions = [
maruel@chromium.org73e21142010-07-05 13:32:01 +0000516%(solution_list)s]
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000517""")
518
519 def __init__(self, root_dir, options):
maruel@chromium.org0d812442010-08-10 12:41:08 +0000520 # Do not change previous behavior. Only solution level and immediate DEPS
521 # are processed.
522 self._recursion_limit = 2
523 Dependency.__init__(self, None, None, None, None, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000524 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000525 if options.deps_os:
526 enforced_os = options.deps_os.split(',')
527 else:
528 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
529 if 'all' in enforced_os:
530 enforced_os = self.DEPS_OS_CHOICES.itervalues()
531 self._enforced_os = list(set(enforced_os))
532 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000533 self.config_content = None
534
535 def SetConfig(self, content):
536 assert self.dependencies == []
537 config_dict = {}
538 self.config_content = content
539 try:
540 exec(content, config_dict)
541 except SyntaxError, e:
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000542 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000543 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000544 try:
545 self.dependencies.append(Dependency(
546 self, s['name'], s['url'],
547 s.get('safesync_url', None),
548 s.get('custom_deps', {}),
maruel@chromium.org0d812442010-08-10 12:41:08 +0000549 s.get('custom_vars', {}),
550 None))
maruel@chromium.org81843b82010-06-28 16:49:26 +0000551 except KeyError:
552 raise gclient_utils.Error('Invalid .gclient file. Solution is '
553 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000554 # .gclient can have hooks.
555 self.deps_hooks = config_dict.get('hooks', [])
556
557 def SaveConfig(self):
558 gclient_utils.FileWrite(os.path.join(self.root_dir(),
559 self._options.config_filename),
560 self.config_content)
561
562 @staticmethod
563 def LoadCurrentConfig(options):
564 """Searches for and loads a .gclient file relative to the current working
565 dir. Returns a GClient object."""
566 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
567 if not path:
568 return None
569 client = GClient(path, options)
570 client.SetConfig(gclient_utils.FileRead(
571 os.path.join(path, options.config_filename)))
572 return client
573
574 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
575 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
576 'solution_name': solution_name,
577 'solution_url': solution_url,
578 'safesync_url' : safesync_url,
579 })
580
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000581 def _SaveEntries(self):
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000582 """Creates a .gclient_entries file to record the list of unique checkouts.
583
584 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000585 """
586 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
587 # makes testing a bit too fun.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000588 result = 'entries = {\n'
589 for entry in self.tree(False):
590 # Skip over File() dependencies as we can't version them.
591 if not isinstance(entry.parsed_url, self.FileImpl):
592 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
593 pprint.pformat(entry.parsed_url))
594 result += '}\n'
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000595 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000596 logging.info(result)
597 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000598
599 def _ReadEntries(self):
600 """Read the .gclient_entries file for the given client.
601
602 Returns:
603 A sequence of solution names, which will be empty if there is the
604 entries file hasn't been created yet.
605 """
606 scope = {}
607 filename = os.path.join(self.root_dir(), self._options.entries_filename)
608 if not os.path.exists(filename):
maruel@chromium.org73e21142010-07-05 13:32:01 +0000609 return {}
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000610 try:
611 exec(gclient_utils.FileRead(filename), scope)
612 except SyntaxError, e:
613 gclient_utils.SyntaxErrorToError(filename, e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000614 return scope['entries']
615
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000616 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000617 """Checks for revision overrides."""
618 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000619 if self._options.head:
620 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000621 for s in self.dependencies:
622 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000623 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000624 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000625 rev = handle.read().strip()
626 handle.close()
627 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000628 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000629 if not self._options.revisions:
630 return revision_overrides
631 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000632 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000633 index = 0
634 for revision in self._options.revisions:
635 if not '@' in revision:
636 # Support for --revision 123
637 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000638 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000639 if not sol in solutions_names:
640 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
641 print >> sys.stderr, ('Please fix your script, having invalid '
642 '--revision flags will soon considered an error.')
643 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000644 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000645 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000646 return revision_overrides
647
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000648 def RunOnDeps(self, command, args):
649 """Runs a command on each dependency in a client and its dependencies.
650
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000651 Args:
652 command: The command to use (e.g., 'status' or 'diff')
653 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000654 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000655 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000656 raise gclient_utils.Error('No solution specified')
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000657 revision_overrides = self._EnforceRevisions()
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000658 pm = None
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000659 if command == 'update' and not self._options.verbose:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000660 pm = Progress('Syncing projects', len(self.tree(False)) + 1)
661 self.RunCommandRecursively(self._options, revision_overrides,
662 command, args, pm)
663 if pm:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000664 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000665
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000666 # Once all the dependencies have been processed, it's now safe to run the
667 # hooks.
668 if not self._options.nohooks:
669 self.RunHooksRecursively(self._options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000670
671 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000672 # Notify the user if there is an orphaned entry in their working copy.
673 # Only delete the directory if there are no changes in it, and
674 # delete_unversioned_trees is set to true.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000675 entries = [i.name for i in self.tree(False)]
676 for entry, prev_url in self._ReadEntries().iteritems():
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000677 # Fix path separator on Windows.
678 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000679 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000680 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000681 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000682 file_list = []
683 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
684 scm.status(self._options, [], file_list)
685 modified_files = file_list != []
msb@chromium.org83017012009-09-28 18:52:12 +0000686 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000687 # There are modified files in this entry. Keep warning until
688 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000689 print(('\nWARNING: \'%s\' is no longer part of this client. '
690 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000691 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000692 else:
693 # Delete the entry
maruel@chromium.org73e21142010-07-05 13:32:01 +0000694 print('\n________ deleting \'%s\' in \'%s\'' % (
695 entry_fixed, self.root_dir()))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000696 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697 # record the current list of entries for next time
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000698 self._SaveEntries()
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000699 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700
701 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000702 """Output revision info mapping for the client and its dependencies.
703
704 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000705 can be used to reproduce the same tree in the future. It is only useful for
706 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
707 number or a git hash. A git branch name isn't "pinned" since the actual
708 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000709
710 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000711 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000712 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000713 raise gclient_utils.Error('No solution specified')
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000714 # Load all the settings.
715 self.RunCommandRecursively(self._options, {}, None, [], None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000716
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717 def GetURLAndRev(name, original_url):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000718 """Returns the revision-qualified SCM url."""
719 if original_url is None:
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000720 return None
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000721 if isinstance(original_url, self.FileImpl):
722 original_url = original_url.file_location
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000723 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000724 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000725 if not os.path.isdir(scm.checkout_path):
726 return None
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000727 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000728
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000729 if self._options.snapshot:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000730 new_gclient = ''
731 # First level at .gclient
732 for d in self.dependencies:
733 entries = {}
734 def GrabDeps(sol):
735 """Recursively grab dependencies."""
736 for i in sol.dependencies:
737 entries[i.name] = GetURLAndRev(i.name, i.parsed_url)
738 GrabDeps(i)
739 GrabDeps(d)
740 custom_deps = []
741 for k in sorted(entries.keys()):
742 if entries[k]:
743 # Quotes aren't escaped...
744 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
745 else:
746 custom_deps.append(' \"%s\": None,\n' % k)
747 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
748 'solution_name': d.name,
749 'solution_url': d.url,
750 'safesync_url' : d.safesync_url or '',
751 'solution_deps': ''.join(custom_deps),
752 }
753 # Print the snapshot configuration file
754 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000755 else:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000756 entries = sorted(self.tree(False), key=lambda i: i.name)
757 for entry in entries:
758 entry_url = GetURLAndRev(entry.name, entry.parsed_url)
759 line = '%s: %s' % (entry.name, entry_url)
760 if not entry is entries[-1]:
761 line += ';'
762 print line
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000763
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000764 def ParseDepsFile(self, direct_reference):
765 """No DEPS to parse for a .gclient file."""
maruel@chromium.org98d05fa2010-07-22 21:58:01 +0000766 self.direct_reference = True
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000767 self.deps_parsed = True
768
maruel@chromium.org75a59272010-06-11 22:34:03 +0000769 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000770 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000771 return self._root_dir
772
maruel@chromium.org271375b2010-06-23 19:17:38 +0000773 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000774 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000775 return self._enforced_os
776
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000777 def recursion_limit(self):
778 """How recursive can each dependencies in DEPS file can load DEPS file."""
779 return self._recursion_limit
780
maruel@chromium.org0d812442010-08-10 12:41:08 +0000781 def tree(self, include_all):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000782 """Returns a flat list of all the dependencies."""
maruel@chromium.org0d812442010-08-10 12:41:08 +0000783 return self.subtree(include_all)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000784
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000785
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000786#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000787
788
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000789def CMDcleanup(parser, args):
790 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000791
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000792Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000793"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000794 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
795 help='override deps for the specified (comma-separated) '
796 'platform(s); \'all\' will process all deps_os '
797 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000798 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000799 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000800 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000801 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000802 if options.verbose:
803 # Print out the .gclient file. This is longer than if we just printed the
804 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000805 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000806 return client.RunOnDeps('cleanup', args)
807
808
piman@chromium.org4b90e3a2010-07-01 20:28:26 +0000809@attr('usage', '[command] [args ...]')
810def CMDrecurse(parser, args):
811 """Operates on all the entries.
812
813 Runs a shell command on all entries.
814 """
815 # Stop parsing at the first non-arg so that these go through to the command
816 parser.disable_interspersed_args()
817 parser.add_option('-s', '--scm', action='append', default=[],
818 help='choose scm types to operate upon')
819 options, args = parser.parse_args(args)
820 root, entries = gclient_utils.GetGClientRootAndEntries()
821 scm_set = set()
822 for scm in options.scm:
823 scm_set.update(scm.split(','))
824
825 # Pass in the SCM type as an env variable
826 env = os.environ.copy()
827
828 for path, url in entries.iteritems():
829 scm = gclient_scm.GetScmName(url)
830 if scm_set and scm not in scm_set:
831 continue
832 dir = os.path.normpath(os.path.join(root, path))
833 env['GCLIENT_SCM'] = scm
834 env['GCLIENT_URL'] = url
835 subprocess.Popen(args, cwd=dir, env=env).communicate()
836
837
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000838@attr('usage', '[url] [safesync url]')
839def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000840 """Create a .gclient file in the current directory.
841
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000842This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000843top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000844modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000845provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000846URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000847"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000848 parser.add_option('--spec',
849 help='create a gclient file containing the provided '
850 'string. Due to Cygwin/Python brokenness, it '
851 'probably can\'t contain any newlines.')
852 parser.add_option('--name',
853 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000854 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000855 if ((options.spec and args) or len(args) > 2 or
856 (not options.spec and not args)):
857 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
858
maruel@chromium.org0329e672009-05-13 18:41:04 +0000859 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000860 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000861 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000862 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000863 if options.spec:
864 client.SetConfig(options.spec)
865 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000866 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000867 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000868 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000869 else:
870 # specify an alternate relpath for the given URL.
871 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000872 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000873 if len(args) > 1:
874 safesync_url = args[1]
875 client.SetDefaultConfig(name, base_url, safesync_url)
876 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000877 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000878
879
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000880def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000881 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000882 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
883 help='override deps for the specified (comma-separated) '
884 'platform(s); \'all\' will process all deps_os '
885 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000886 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000887 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000888 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000889 client = GClient.LoadCurrentConfig(options)
890
891 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000892 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000893
894 if options.verbose:
895 # Print out the .gclient file. This is longer than if we just printed the
896 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000897 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000898 return client.RunOnDeps('export', args)
899
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000900
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000901@attr('epilog', """Example:
902 gclient pack > patch.txt
903 generate simple patch for configured client and dependences
904""")
905def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000906 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000907
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000908Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000909dependencies, and performs minimal postprocessing of the output. The
910resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000911checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000912"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000913 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
914 help='override deps for the specified (comma-separated) '
915 'platform(s); \'all\' will process all deps_os '
916 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000917 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000918 client = GClient.LoadCurrentConfig(options)
919 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000920 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000921 if options.verbose:
922 # Print out the .gclient file. This is longer than if we just printed the
923 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000924 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000925 return client.RunOnDeps('pack', args)
926
927
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000928def CMDstatus(parser, args):
929 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000930 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
931 help='override deps for the specified (comma-separated) '
932 'platform(s); \'all\' will process all deps_os '
933 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000934 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000935 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000936 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000937 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000938 if options.verbose:
939 # Print out the .gclient file. This is longer than if we just printed the
940 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000941 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000942 return client.RunOnDeps('status', args)
943
944
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000945@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000946 gclient sync
947 update files from SCM according to current configuration,
948 *for modules which have changed since last update or sync*
949 gclient sync --force
950 update files from SCM according to current configuration, for
951 all modules (useful for recovering files deleted from local copy)
952 gclient sync --revision src@31000
953 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000954""")
955def CMDsync(parser, args):
956 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000957 parser.add_option('-f', '--force', action='store_true',
958 help='force update even for unchanged modules')
959 parser.add_option('-n', '--nohooks', action='store_true',
960 help='don\'t run hooks after the update is complete')
961 parser.add_option('-r', '--revision', action='append',
962 dest='revisions', metavar='REV', default=[],
963 help='Enforces revision/hash for the solutions with the '
964 'format src@rev. The src@ part is optional and can be '
965 'skipped. -r can be used multiple times when .gclient '
966 'has multiple solutions configured and will work even '
967 'if the src@ part is skipped.')
968 parser.add_option('-H', '--head', action='store_true',
969 help='skips any safesync_urls specified in '
970 'configured solutions and sync to head instead')
971 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
972 help='delete any unexpected unversioned trees '
973 'that are in the checkout')
974 parser.add_option('-R', '--reset', action='store_true',
975 help='resets any local changes before updating (git only)')
976 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
977 help='override deps for the specified (comma-separated) '
978 'platform(s); \'all\' will process all deps_os '
979 'references')
980 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
981 help='Skip svn up whenever possible by requesting '
982 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000983 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000984 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000985
986 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000987 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000988
maruel@chromium.org307d1792010-05-31 20:03:13 +0000989 if options.revisions and options.head:
990 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000991 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992
993 if options.verbose:
994 # Print out the .gclient file. This is longer than if we just printed the
995 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000996 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000997 return client.RunOnDeps('update', args)
998
999
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001000def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001001 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001002 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001003
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001004def CMDdiff(parser, args):
1005 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001006 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1007 help='override deps for the specified (comma-separated) '
1008 'platform(s); \'all\' will process all deps_os '
1009 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001010 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001011 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001012 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001013 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001014 if options.verbose:
1015 # Print out the .gclient file. This is longer than if we just printed the
1016 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001017 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001018 return client.RunOnDeps('diff', args)
1019
1020
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001021def CMDrevert(parser, args):
1022 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001023 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1024 help='override deps for the specified (comma-separated) '
1025 'platform(s); \'all\' will process all deps_os '
1026 'references')
1027 parser.add_option('-n', '--nohooks', action='store_true',
1028 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001029 (options, args) = parser.parse_args(args)
1030 # --force is implied.
1031 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001032 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001034 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035 return client.RunOnDeps('revert', args)
1036
1037
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001038def CMDrunhooks(parser, args):
1039 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001040 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1041 help='override deps for the specified (comma-separated) '
1042 'platform(s); \'all\' will process all deps_os '
1043 'references')
1044 parser.add_option('-f', '--force', action='store_true', default=True,
1045 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001046 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001047 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001048 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001049 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001050 if options.verbose:
1051 # Print out the .gclient file. This is longer than if we just printed the
1052 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001053 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001054 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001055 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001056 return client.RunOnDeps('runhooks', args)
1057
1058
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001059def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001060 """Output revision info mapping for the client and its dependencies.
1061
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001062 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001063 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001064 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1065 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001066 commit can change.
1067 """
1068 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1069 help='override deps for the specified (comma-separated) '
1070 'platform(s); \'all\' will process all deps_os '
1071 'references')
1072 parser.add_option('-s', '--snapshot', action='store_true',
1073 help='creates a snapshot .gclient file of the current '
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +00001074 'version of all repositories to reproduce the tree')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001075 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001076 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001077 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001078 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001079 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001080 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001081
1082
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001083def Command(name):
1084 return getattr(sys.modules[__name__], 'CMD' + name, None)
1085
1086
1087def CMDhelp(parser, args):
1088 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001089 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001090 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001091 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001092 parser.print_help()
1093 return 0
1094
1095
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001096def GenUsage(parser, command):
1097 """Modify an OptParse object with the function's documentation."""
1098 obj = Command(command)
1099 if command == 'help':
1100 command = '<command>'
1101 # OptParser.description prefer nicely non-formatted strings.
1102 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1103 usage = getattr(obj, 'usage', '')
1104 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1105 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001106
1107
1108def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001109 """Doesn't parse the arguments here, just find the right subcommand to
1110 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001111 try:
1112 # Do it late so all commands are listed.
1113 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1114 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1115 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1116 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001117 parser.add_option('-v', '--verbose', action='count', default=0,
1118 help='Produces additional output for diagnostics. Can be '
1119 'used up to three times for more logging info.')
1120 parser.add_option('--gclientfile', dest='config_filename',
1121 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1122 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001123 # Integrate standard options processing.
1124 old_parser = parser.parse_args
1125 def Parse(args):
1126 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001127 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001128 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001129 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001130 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001131 level = logging.DEBUG
1132 logging.basicConfig(level=level,
1133 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1134 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001135
1136 # These hacks need to die.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001137 if not hasattr(options, 'revisions'):
1138 # GClient.RunOnDeps expects it even if not applicable.
1139 options.revisions = []
1140 if not hasattr(options, 'head'):
1141 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001142 if not hasattr(options, 'nohooks'):
1143 options.nohooks = True
1144 if not hasattr(options, 'deps_os'):
1145 options.deps_os = None
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001146 if not hasattr(options, 'manually_grab_svn_rev'):
1147 options.manually_grab_svn_rev = None
1148 if not hasattr(options, 'force'):
1149 options.force = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001150 return (options, args)
1151 parser.parse_args = Parse
1152 # We don't want wordwrapping in epilog (usually examples)
1153 parser.format_epilog = lambda _: parser.epilog or ''
1154 if argv:
1155 command = Command(argv[0])
1156 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001157 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001158 GenUsage(parser, argv[0])
1159 return command(parser, argv[1:])
1160 # Not a known command. Default to help.
1161 GenUsage(parser, 'help')
1162 return CMDhelp(parser, argv)
1163 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001164 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001165 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001166
1167
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001168if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001169 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001170
1171# vim: ts=2:sw=2:tw=80:et: