blob: 0d1e2f93f2f950066e8969fc208cba195a067b1a [file] [log] [blame]
[email protected]fb2b8eb2009-04-23 21:03:421#!/usr/bin/python
[email protected]ba551772010-02-03 18:21:422# 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.
[email protected]fb2b8eb2009-04-23 21:03:425
[email protected]0b6a0842010-06-15 14:34:196"""Meta checkout manager supporting both Subversion and GIT.
[email protected]fb2b8eb2009-04-23 21:03:427
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
[email protected]67820ef2009-07-27 17:23:0024 working copy as a result of a "sync"/"update" or "revert" operation. This
[email protected]0b6a0842010-06-15 14:34:1925 can be prevented by using --nohooks (hooks run by default). Hooks can also
[email protected]5df6a462009-08-28 18:52:2626 be forced to run with the "runhooks" operation. If "sync" is run with
[email protected]fb2b8eb2009-04-23 21:03:4227 --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
[email protected]71b40682009-07-31 23:40:0941 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.
[email protected]fb2b8eb2009-04-23 21:03:4244
45 Example:
46 hooks = [
47 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
48 "action": ["python", "image_indexer.py", "--all"]},
49 ]
50"""
51
[email protected]46304292010-10-28 11:42:0052__version__ = "0.6.1"
[email protected]fb2b8eb2009-04-23 21:03:4253
[email protected]9e5317a2010-08-13 20:35:1154import copy
[email protected]754960e2009-09-21 12:31:0555import logging
[email protected]fb2b8eb2009-04-23 21:03:4256import optparse
57import os
[email protected]621939b2010-08-10 20:12:0058import posixpath
[email protected]2e38de72009-09-28 17:04:4759import pprint
[email protected]fb2b8eb2009-04-23 21:03:4260import re
[email protected]4b90e3a2010-07-01 20:28:2661import subprocess
[email protected]fb2b8eb2009-04-23 21:03:4262import sys
[email protected]fb2b8eb2009-04-23 21:03:4263import urlparse
[email protected]fb2b8eb2009-04-23 21:03:4264import urllib
65
[email protected]ada4c652009-12-03 15:32:0166import breakpad
67
[email protected]5f3eee32009-09-17 00:34:3068import gclient_scm
69import gclient_utils
[email protected]1f7a3d12010-02-04 15:11:5070from third_party.repo.progress import Progress
[email protected]fb2b8eb2009-04-23 21:03:4271
[email protected]fb2b8eb2009-04-23 21:03:4272
[email protected]1f7d1182010-05-17 18:17:3873def attr(attr, data):
74 """Sets an attribute on a function."""
75 def hook(fn):
76 setattr(fn, attr, data)
77 return fn
78 return hook
[email protected]e3da35f2010-03-09 21:40:4579
[email protected]fb2b8eb2009-04-23 21:03:4280
[email protected]fb2b8eb2009-04-23 21:03:4281## GClient implementation.
82
83
[email protected]116704f2010-06-11 17:34:3884class GClientKeywords(object):
85 class FromImpl(object):
86 """Used to implement the From() syntax."""
87
88 def __init__(self, module_name, sub_target_name=None):
89 """module_name is the dep module we want to include from. It can also be
90 the name of a subdirectory to include from.
91
92 sub_target_name is an optional parameter if the module name in the other
93 DEPS file is different. E.g., you might want to map src/net to net."""
94 self.module_name = module_name
95 self.sub_target_name = sub_target_name
96
97 def __str__(self):
98 return 'From(%s, %s)' % (repr(self.module_name),
99 repr(self.sub_target_name))
100
[email protected]116704f2010-06-11 17:34:38101 class FileImpl(object):
102 """Used to implement the File('') syntax which lets you sync a single file
[email protected]e3216c62010-07-08 03:31:43103 from a SVN repo."""
[email protected]116704f2010-06-11 17:34:38104
105 def __init__(self, file_location):
106 self.file_location = file_location
107
108 def __str__(self):
109 return 'File("%s")' % self.file_location
110
111 def GetPath(self):
112 return os.path.split(self.file_location)[0]
113
114 def GetFilename(self):
115 rev_tokens = self.file_location.split('@')
116 return os.path.split(rev_tokens[0])[1]
117
118 def GetRevision(self):
119 rev_tokens = self.file_location.split('@')
120 if len(rev_tokens) > 1:
121 return rev_tokens[1]
122 return None
123
124 class VarImpl(object):
125 def __init__(self, custom_vars, local_scope):
126 self._custom_vars = custom_vars
127 self._local_scope = local_scope
128
129 def Lookup(self, var_name):
130 """Implements the Var syntax."""
131 if var_name in self._custom_vars:
132 return self._custom_vars[var_name]
133 elif var_name in self._local_scope.get("vars", {}):
134 return self._local_scope["vars"][var_name]
135 raise gclient_utils.Error("Var is not defined: %s" % var_name)
136
137
[email protected]80cbe8b2010-08-13 13:53:07138class Dependency(GClientKeywords, gclient_utils.WorkItem):
[email protected]54a07a22010-06-14 19:07:39139 """Object that represents a dependency checkout."""
[email protected]9eda4112010-06-11 18:56:10140 DEPS_FILE = 'DEPS'
[email protected]0b6a0842010-06-15 14:34:19141
[email protected]0d812442010-08-10 12:41:08142 def __init__(self, parent, name, url, safesync_url, custom_deps,
[email protected]f50907b2010-08-12 17:05:48143 custom_vars, deps_file, should_process):
[email protected]54a07a22010-06-14 19:07:39144 GClientKeywords.__init__(self)
[email protected]6985efc2010-09-08 13:26:12145 gclient_utils.WorkItem.__init__(self)
[email protected]54a07a22010-06-14 19:07:39146 self.parent = parent
147 self.name = name
148 self.url = url
[email protected]df2b3152010-07-21 17:35:24149 self.parsed_url = None
[email protected]54a07a22010-06-14 19:07:39150 # These 2 are only set in .gclient and not in DEPS files.
151 self.safesync_url = safesync_url
152 self.custom_vars = custom_vars or {}
153 self.custom_deps = custom_deps or {}
[email protected]0b6a0842010-06-15 14:34:19154 self.deps_hooks = []
[email protected]54a07a22010-06-14 19:07:39155 self.dependencies = []
156 self.deps_file = deps_file or self.DEPS_FILE
[email protected]df2b3152010-07-21 17:35:24157 # A cache of the files affected by the current operation, necessary for
158 # hooks.
[email protected]861fd0f2010-07-23 03:05:05159 self._file_list = []
[email protected]85c2a192010-07-22 21:14:43160 # If it is not set to True, the dependency wasn't processed for its child
161 # dependency, i.e. its DEPS wasn't read.
[email protected]271375b2010-06-23 19:17:38162 self.deps_parsed = False
[email protected]f50907b2010-08-12 17:05:48163 # This dependency should be processed, i.e. checked out
164 self.should_process = should_process
[email protected]f3abb802010-08-10 17:19:56165 # This dependency has been processed, i.e. checked out
166 self.processed = False
167 # This dependency had its hook run
168 self.hooks_ran = False
[email protected]621939b2010-08-10 20:12:00169 # Required dependencies to run before running this one:
170 self.requirements = []
171 if self.parent and self.parent.name:
172 self.requirements.append(self.parent.name)
173 if isinstance(self.url, self.FromImpl):
174 self.requirements.append(self.url.module_name)
[email protected]fb2b8eb2009-04-23 21:03:42175
[email protected]54a07a22010-06-14 19:07:39176 # Sanity checks
177 if not self.name and self.parent:
178 raise gclient_utils.Error('Dependency without name')
179 if not isinstance(self.url,
180 (basestring, self.FromImpl, self.FileImpl, None.__class__)):
181 raise gclient_utils.Error('dependency url must be either a string, None, '
182 'File() or From() instead of %s' %
183 self.url.__class__.__name__)
184 if '/' in self.deps_file or '\\' in self.deps_file:
185 raise gclient_utils.Error('deps_file name must not be a path, just a '
186 'filename. %s' % self.deps_file)
187
[email protected]df2b3152010-07-21 17:35:24188 def LateOverride(self, url):
[email protected]da7a1f92010-08-10 17:19:02189 """Resolves the parsed url from url.
190
191 Manages From() keyword accordingly. Do not touch self.parsed_url nor
192 self.url because it may called with other urls due to From()."""
[email protected]f50907b2010-08-12 17:05:48193 assert self.parsed_url == None or not self.should_process, self.parsed_url
[email protected]df2b3152010-07-21 17:35:24194 overriden_url = self.get_custom_deps(self.name, url)
195 if overriden_url != url:
[email protected]dde32ee2010-08-10 17:44:05196 logging.info('%s, %s was overriden to %s' % (self.name, url,
[email protected]da7a1f92010-08-10 17:19:02197 overriden_url))
198 return overriden_url
[email protected]df2b3152010-07-21 17:35:24199 elif isinstance(url, self.FromImpl):
200 ref = [dep for dep in self.tree(True) if url.module_name == dep.name]
[email protected]f3abb802010-08-10 17:19:56201 if not ref:
202 raise gclient_utils.Error('Failed to find one reference to %s. %s' % (
203 url.module_name, ref))
204 # It may happen that len(ref) > 1 but it's no big deal.
[email protected]df2b3152010-07-21 17:35:24205 ref = ref[0]
[email protected]98d05fa2010-07-22 21:58:01206 sub_target = url.sub_target_name or self.name
[email protected]df2b3152010-07-21 17:35:24207 # Make sure the referenced dependency DEPS file is loaded and file the
208 # inner referenced dependency.
[email protected]f50907b2010-08-12 17:05:48209 ref.ParseDepsFile()
[email protected]df2b3152010-07-21 17:35:24210 found_dep = None
211 for d in ref.dependencies:
212 if d.name == sub_target:
213 found_dep = d
214 break
215 if not found_dep:
[email protected]f3abb802010-08-10 17:19:56216 raise gclient_utils.Error(
[email protected]dde32ee2010-08-10 17:44:05217 'Couldn\'t find %s in %s, referenced by %s\n%s' % (
218 sub_target, ref.name, self.name, str(self.root_parent())))
[email protected]df2b3152010-07-21 17:35:24219 # Call LateOverride() again.
[email protected]da7a1f92010-08-10 17:19:02220 parsed_url = found_dep.LateOverride(found_dep.url)
[email protected]dde32ee2010-08-10 17:44:05221 logging.info('%s, %s to %s' % (self.name, url, parsed_url))
[email protected]da7a1f92010-08-10 17:19:02222 return parsed_url
[email protected]df2b3152010-07-21 17:35:24223 elif isinstance(url, basestring):
224 parsed_url = urlparse.urlparse(url)
225 if not parsed_url[0]:
226 # A relative url. Fetch the real base.
227 path = parsed_url[2]
228 if not path.startswith('/'):
229 raise gclient_utils.Error(
230 'relative DEPS entry \'%s\' must begin with a slash' % url)
231 # Create a scm just to query the full url.
232 parent_url = self.parent.parsed_url
233 if isinstance(parent_url, self.FileImpl):
234 parent_url = parent_url.file_location
235 scm = gclient_scm.CreateSCM(parent_url, self.root_dir(), None)
[email protected]da7a1f92010-08-10 17:19:02236 parsed_url = scm.FullUrlForRelativeUrl(url)
[email protected]df2b3152010-07-21 17:35:24237 else:
[email protected]da7a1f92010-08-10 17:19:02238 parsed_url = url
[email protected]dde32ee2010-08-10 17:44:05239 logging.info('%s, %s -> %s' % (self.name, url, parsed_url))
[email protected]da7a1f92010-08-10 17:19:02240 return parsed_url
[email protected]df2b3152010-07-21 17:35:24241 elif isinstance(url, self.FileImpl):
[email protected]da7a1f92010-08-10 17:19:02242 parsed_url = url
[email protected]dde32ee2010-08-10 17:44:05243 logging.info('%s, %s -> %s (File)' % (self.name, url, parsed_url))
[email protected]da7a1f92010-08-10 17:19:02244 return parsed_url
245 elif url is None:
246 return None
247 else:
248 raise gclient_utils.Error('Unkown url type')
[email protected]df2b3152010-07-21 17:35:24249
[email protected]f50907b2010-08-12 17:05:48250 def ParseDepsFile(self):
[email protected]271375b2010-06-23 19:17:38251 """Parses the DEPS file for this dependency."""
[email protected]f50907b2010-08-12 17:05:48252 assert self.processed == True
[email protected]df2b3152010-07-21 17:35:24253 if self.deps_parsed:
[email protected]dde32ee2010-08-10 17:44:05254 logging.debug('%s was already parsed' % self.name)
[email protected]df2b3152010-07-21 17:35:24255 return
[email protected]271375b2010-06-23 19:17:38256 self.deps_parsed = True
[email protected]271375b2010-06-23 19:17:38257 # One thing is unintuitive, vars= {} must happen before Var() use.
258 local_scope = {}
259 var = self.VarImpl(self.custom_vars, local_scope)
260 global_scope = {
261 'File': self.FileImpl,
262 'From': self.FromImpl,
263 'Var': var.Lookup,
264 'deps_os': {},
265 }
[email protected]46304292010-10-28 11:42:00266 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
267 if not os.path.isfile(filepath):
268 logging.info('%s: No DEPS file found at %s' % (self.name, filepath))
269 else:
270 deps_content = gclient_utils.FileRead(filepath)
271 logging.debug(deps_content)
272 # Eval the content.
273 try:
274 exec(deps_content, global_scope, local_scope)
275 except SyntaxError, e:
276 gclient_utils.SyntaxErrorToError(filepath, e)
[email protected]271375b2010-06-23 19:17:38277 deps = local_scope.get('deps', {})
[email protected]fb2b8eb2009-04-23 21:03:42278 # load os specific dependencies if defined. these dependencies may
279 # override or extend the values defined by the 'deps' member.
[email protected]271375b2010-06-23 19:17:38280 if 'deps_os' in local_scope:
281 for deps_os_key in self.enforced_os():
282 os_deps = local_scope['deps_os'].get(deps_os_key, {})
283 if len(self.enforced_os()) > 1:
284 # Ignore any conflict when including deps for more than one
[email protected]46304292010-10-28 11:42:00285 # platform, so we collect the broadest set of dependencies
286 # available. We may end up with the wrong revision of something for
287 # our platform, but this is the best we can do.
[email protected]fb2b8eb2009-04-23 21:03:42288 deps.update([x for x in os_deps.items() if not x[0] in deps])
289 else:
290 deps.update(os_deps)
291
[email protected]271375b2010-06-23 19:17:38292 self.deps_hooks.extend(local_scope.get('hooks', []))
293
294 # If a line is in custom_deps, but not in the solution, we want to append
295 # this line to the solution.
296 for d in self.custom_deps:
297 if d not in deps:
298 deps[d] = self.custom_deps[d]
[email protected]fb2b8eb2009-04-23 21:03:42299
300 # If use_relative_paths is set in the DEPS file, regenerate
301 # the dictionary using paths relative to the directory containing
302 # the DEPS file.
[email protected]271375b2010-06-23 19:17:38303 use_relative_paths = local_scope.get('use_relative_paths', False)
304 if use_relative_paths:
[email protected]fb2b8eb2009-04-23 21:03:42305 rel_deps = {}
306 for d, url in deps.items():
307 # normpath is required to allow DEPS to use .. in their
308 # dependency local path.
[email protected]271375b2010-06-23 19:17:38309 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
310 deps = rel_deps
[email protected]fb2b8eb2009-04-23 21:03:42311
[email protected]df2b3152010-07-21 17:35:24312 # Convert the deps into real Dependency.
313 for name, url in deps.iteritems():
314 if name in [s.name for s in self.dependencies]:
[email protected]dde32ee2010-08-10 17:44:05315 raise gclient_utils.Error(
316 'The same name "%s" appears multiple times in the deps section' %
317 name)
[email protected]f50907b2010-08-12 17:05:48318 should_process = self.recursion_limit() > 0 and self.should_process
319 if should_process:
320 tree = dict((d.name, d) for d in self.tree(False))
321 if name in tree:
322 if url == tree[name].url:
323 logging.info('Won\'t process duplicate dependency %s' % tree[name])
324 # In theory we could keep it as a shadow of the other one. In
325 # practice, simply ignore it.
326 #should_process = False
327 continue
328 else:
329 raise gclient_utils.Error(
330 'Dependency %s specified more than once:\n %s\nvs\n %s' %
331 (name, tree[name].hierarchy(), self.hierarchy()))
[email protected]0d812442010-08-10 12:41:08332 self.dependencies.append(Dependency(self, name, url, None, None, None,
[email protected]f50907b2010-08-12 17:05:48333 None, should_process))
[email protected]dde32ee2010-08-10 17:44:05334 logging.debug('Loaded: %s' % str(self))
[email protected]fb2b8eb2009-04-23 21:03:42335
[email protected]3742c842010-09-09 19:27:14336 def run(self, revision_overrides, command, args, work_queue, options):
[email protected]df2b3152010-07-21 17:35:24337 """Runs 'command' before parsing the DEPS in case it's a initial checkout
338 or a revert."""
[email protected]861fd0f2010-07-23 03:05:05339 assert self._file_list == []
[email protected]f50907b2010-08-12 17:05:48340 if not self.should_process:
341 return
[email protected]df2b3152010-07-21 17:35:24342 # When running runhooks, there's no need to consult the SCM.
343 # All known hooks are expected to run unconditionally regardless of working
344 # copy state, so skip the SCM status check.
345 run_scm = command not in ('runhooks', None)
[email protected]da7a1f92010-08-10 17:19:02346 self.parsed_url = self.LateOverride(self.url)
[email protected]df2b3152010-07-21 17:35:24347 if run_scm and self.parsed_url:
348 if isinstance(self.parsed_url, self.FileImpl):
349 # Special support for single-file checkout.
350 if not command in (None, 'cleanup', 'diff', 'pack', 'status'):
351 options.revision = self.parsed_url.GetRevision()
352 scm = gclient_scm.SVNWrapper(self.parsed_url.GetPath(),
353 self.root_dir(),
354 self.name)
355 scm.RunCommand('updatesingle', options,
356 args + [self.parsed_url.GetFilename()],
[email protected]861fd0f2010-07-23 03:05:05357 self._file_list)
[email protected]df2b3152010-07-21 17:35:24358 else:
[email protected]9e5317a2010-08-13 20:35:11359 # Create a shallow copy to mutate revision.
360 options = copy.copy(options)
[email protected]df2b3152010-07-21 17:35:24361 options.revision = revision_overrides.get(self.name)
362 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name)
[email protected]861fd0f2010-07-23 03:05:05363 scm.RunCommand(command, options, args, self._file_list)
364 self._file_list = [os.path.join(self.name, f.strip())
365 for f in self._file_list]
[email protected]f3abb802010-08-10 17:19:56366 self.processed = True
[email protected]f50907b2010-08-12 17:05:48367 if self.recursion_limit() > 0:
[email protected]df2b3152010-07-21 17:35:24368 # Then we can parse the DEPS file.
[email protected]f50907b2010-08-12 17:05:48369 self.ParseDepsFile()
[email protected]621939b2010-08-10 20:12:00370 # Adjust the implicit dependency requirement; e.g. if a DEPS file contains
371 # both src/foo and src/foo/bar, src/foo/bar is implicitly dependent of
[email protected]049bced2010-08-12 13:37:20372 # src/foo. Yes, it's O(n^2)... It's important to do that before
373 # enqueueing them.
[email protected]621939b2010-08-10 20:12:00374 for s in self.dependencies:
375 for s2 in self.dependencies:
376 if s is s2:
377 continue
378 if s.name.startswith(posixpath.join(s2.name, '')):
379 s.requirements.append(s2.name)
380
[email protected]df2b3152010-07-21 17:35:24381 # Parse the dependencies of this dependency.
382 for s in self.dependencies:
[email protected]049bced2010-08-12 13:37:20383 work_queue.enqueue(s)
[email protected]fb2b8eb2009-04-23 21:03:42384
[email protected]df2b3152010-07-21 17:35:24385 def RunHooksRecursively(self, options):
[email protected]049bced2010-08-12 13:37:20386 """Evaluates all hooks, running actions as needed. run()
[email protected]df2b3152010-07-21 17:35:24387 must have been called before to load the DEPS."""
[email protected]f50907b2010-08-12 17:05:48388 assert self.hooks_ran == False
389 if not self.should_process or self.recursion_limit() <= 0:
390 # Don't run the hook when it is above recursion_limit.
391 return
[email protected]dc7445d2010-07-09 21:05:29392 # If "--force" was specified, run all hooks regardless of what files have
[email protected]df2b3152010-07-21 17:35:24393 # changed.
[email protected]f50907b2010-08-12 17:05:48394 if self.deps_hooks:
[email protected]df2b3152010-07-21 17:35:24395 # TODO(maruel): If the user is using git or git-svn, then we don't know
396 # what files have changed so we always run all hooks. It'd be nice to fix
397 # that.
398 if (options.force or
399 isinstance(self.parsed_url, self.FileImpl) or
400 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or
401 os.path.isdir(os.path.join(self.root_dir(), self.name, '.git'))):
402 for hook_dict in self.deps_hooks:
403 self._RunHookAction(hook_dict, [])
404 else:
405 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
406 # Convert all absolute paths to relative.
[email protected]861fd0f2010-07-23 03:05:05407 file_list = self.file_list()
408 for i in range(len(file_list)):
[email protected]df2b3152010-07-21 17:35:24409 # It depends on the command being executed (like runhooks vs sync).
[email protected]861fd0f2010-07-23 03:05:05410 if not os.path.isabs(file_list[i]):
[email protected]df2b3152010-07-21 17:35:24411 continue
[email protected]dc7445d2010-07-09 21:05:29412
[email protected]df2b3152010-07-21 17:35:24413 prefix = os.path.commonprefix([self.root_dir().lower(),
[email protected]861fd0f2010-07-23 03:05:05414 file_list[i].lower()])
415 file_list[i] = file_list[i][len(prefix):]
[email protected]df2b3152010-07-21 17:35:24416
417 # Strip any leading path separators.
[email protected]861fd0f2010-07-23 03:05:05418 while (file_list[i].startswith('\\') or
419 file_list[i].startswith('/')):
420 file_list[i] = file_list[i][1:]
[email protected]df2b3152010-07-21 17:35:24421
422 # Run hooks on the basis of whether the files from the gclient operation
423 # match each hook's pattern.
424 for hook_dict in self.deps_hooks:
425 pattern = re.compile(hook_dict['pattern'])
[email protected]861fd0f2010-07-23 03:05:05426 matching_file_list = [f for f in file_list if pattern.search(f)]
[email protected]df2b3152010-07-21 17:35:24427 if matching_file_list:
428 self._RunHookAction(hook_dict, matching_file_list)
[email protected]f50907b2010-08-12 17:05:48429 for s in self.dependencies:
430 s.RunHooksRecursively(options)
[email protected]fb2b8eb2009-04-23 21:03:42431
[email protected]eaf61062010-07-07 18:42:39432 def _RunHookAction(self, hook_dict, matching_file_list):
433 """Runs the action from a single hook."""
[email protected]dde32ee2010-08-10 17:44:05434 # A single DEPS file can specify multiple hooks so this function can be
435 # called multiple times on a single Dependency.
436 #assert self.hooks_ran == False
[email protected]f3abb802010-08-10 17:19:56437 self.hooks_ran = True
[email protected]dde32ee2010-08-10 17:44:05438 logging.debug(hook_dict)
439 logging.debug(matching_file_list)
[email protected]eaf61062010-07-07 18:42:39440 command = hook_dict['action'][:]
441 if command[0] == 'python':
442 # If the hook specified "python" as the first item, the action is a
443 # Python script. Run it by starting a new copy of the same
444 # interpreter.
445 command[0] = sys.executable
446
447 if '$matching_files' in command:
448 splice_index = command.index('$matching_files')
449 command[splice_index:splice_index + 1] = matching_file_list
450
[email protected]17d01792010-09-01 18:07:10451 try:
452 gclient_utils.CheckCallAndFilterAndHeader(
453 command, cwd=self.root_dir(), always=True)
454 except gclient_utils.Error, e:
455 # Use a discrete exit status code of 2 to indicate that a hook action
456 # failed. Users of this script may wish to treat hook action failures
457 # differently from VC failures.
458 print >> sys.stderr, 'Error: %s' % str(e)
459 sys.exit(2)
[email protected]eaf61062010-07-07 18:42:39460
[email protected]271375b2010-06-23 19:17:38461 def root_dir(self):
462 return self.parent.root_dir()
463
464 def enforced_os(self):
465 return self.parent.enforced_os()
466
[email protected]d36fba82010-06-28 16:50:40467 def recursion_limit(self):
468 return self.parent.recursion_limit() - 1
469
[email protected]0d812442010-08-10 12:41:08470 def tree(self, include_all):
471 return self.parent.tree(include_all)
[email protected]d36fba82010-06-28 16:50:40472
[email protected]0d812442010-08-10 12:41:08473 def subtree(self, include_all):
[email protected]f50907b2010-08-12 17:05:48474 """Breadth first"""
[email protected]c57e4f22010-07-22 21:37:46475 result = []
[email protected]f50907b2010-08-12 17:05:48476 for d in self.dependencies:
477 if d.should_process or include_all:
[email protected]044f4e32010-07-22 21:59:57478 result.append(d)
[email protected]f50907b2010-08-12 17:05:48479 for d in self.dependencies:
480 result.extend(d.subtree(include_all))
[email protected]c57e4f22010-07-22 21:37:46481 return result
482
[email protected]d36fba82010-06-28 16:50:40483 def get_custom_deps(self, name, url):
484 """Returns a custom deps if applicable."""
485 if self.parent:
486 url = self.parent.get_custom_deps(name, url)
487 # None is a valid return value to disable a dependency.
488 return self.custom_deps.get(name, url)
489
[email protected]861fd0f2010-07-23 03:05:05490 def file_list(self):
491 result = self._file_list[:]
492 for d in self.dependencies:
493 result.extend(d.file_list())
494 return result
495
[email protected]d36fba82010-06-28 16:50:40496 def __str__(self):
497 out = []
[email protected]dde32ee2010-08-10 17:44:05498 for i in ('name', 'url', 'parsed_url', 'safesync_url', 'custom_deps',
[email protected]f50907b2010-08-12 17:05:48499 'custom_vars', 'deps_hooks', '_file_list', 'should_process',
500 'processed', 'hooks_ran', 'deps_parsed', 'requirements'):
[email protected]d36fba82010-06-28 16:50:40501 # 'deps_file'
502 if self.__dict__[i]:
503 out.append('%s: %s' % (i, self.__dict__[i]))
504
505 for d in self.dependencies:
506 out.extend([' ' + x for x in str(d).splitlines()])
507 out.append('')
508 return '\n'.join(out)
509
510 def __repr__(self):
511 return '%s: %s' % (self.name, self.url)
512
[email protected]bffb9042010-07-22 20:59:36513 def hierarchy(self):
[email protected]bc2d2f92010-07-22 21:26:48514 """Returns a human-readable hierarchical reference to a Dependency."""
[email protected]bffb9042010-07-22 20:59:36515 out = '%s(%s)' % (self.name, self.url)
516 i = self.parent
517 while i and i.name:
518 out = '%s(%s) -> %s' % (i.name, i.url, out)
519 i = i.parent
520 return out
521
[email protected]dde32ee2010-08-10 17:44:05522 def root_parent(self):
523 """Returns the root object, normally a GClient object."""
524 d = self
525 while d.parent:
526 d = d.parent
527 return d
528
[email protected]9a66ddf2010-06-16 16:54:16529
530class GClient(Dependency):
[email protected]d36fba82010-06-28 16:50:40531 """Object that represent a gclient checkout. A tree of Dependency(), one per
532 solution or DEPS entry."""
[email protected]9a66ddf2010-06-16 16:54:16533
534 DEPS_OS_CHOICES = {
535 "win32": "win",
536 "win": "win",
537 "cygwin": "win",
538 "darwin": "mac",
539 "mac": "mac",
540 "unix": "unix",
541 "linux": "unix",
542 "linux2": "unix",
543 }
544
545 DEFAULT_CLIENT_FILE_TEXT = ("""\
546solutions = [
547 { "name" : "%(solution_name)s",
548 "url" : "%(solution_url)s",
549 "custom_deps" : {
550 },
[email protected]73e21142010-07-05 13:32:01551 "safesync_url": "%(safesync_url)s",
[email protected]9a66ddf2010-06-16 16:54:16552 },
553]
554""")
555
556 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
557 { "name" : "%(solution_name)s",
558 "url" : "%(solution_url)s",
559 "custom_deps" : {
[email protected]73e21142010-07-05 13:32:01560%(solution_deps)s },
561 "safesync_url": "%(safesync_url)s",
[email protected]9a66ddf2010-06-16 16:54:16562 },
563""")
564
565 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
566# Snapshot generated with gclient revinfo --snapshot
567solutions = [
[email protected]73e21142010-07-05 13:32:01568%(solution_list)s]
[email protected]9a66ddf2010-06-16 16:54:16569""")
570
571 def __init__(self, root_dir, options):
[email protected]0d812442010-08-10 12:41:08572 # Do not change previous behavior. Only solution level and immediate DEPS
573 # are processed.
574 self._recursion_limit = 2
[email protected]f50907b2010-08-12 17:05:48575 Dependency.__init__(self, None, None, None, None, None, None, None, True)
[email protected]0d425922010-06-21 19:22:24576 self._options = options
[email protected]271375b2010-06-23 19:17:38577 if options.deps_os:
578 enforced_os = options.deps_os.split(',')
579 else:
580 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
581 if 'all' in enforced_os:
582 enforced_os = self.DEPS_OS_CHOICES.itervalues()
583 self._enforced_os = list(set(enforced_os))
584 self._root_dir = root_dir
[email protected]9a66ddf2010-06-16 16:54:16585 self.config_content = None
586
587 def SetConfig(self, content):
588 assert self.dependencies == []
589 config_dict = {}
590 self.config_content = content
591 try:
592 exec(content, config_dict)
593 except SyntaxError, e:
[email protected]5990f9d2010-07-07 18:02:58594 gclient_utils.SyntaxErrorToError('.gclient', e)
[email protected]9a66ddf2010-06-16 16:54:16595 for s in config_dict.get('solutions', []):
[email protected]81843b82010-06-28 16:49:26596 try:
[email protected]f50907b2010-08-12 17:05:48597 tree = dict((d.name, d) for d in self.tree(False))
598 if s['name'] in tree:
599 raise gclient_utils.Error(
600 'Dependency %s specified more than once in .gclient' % s['name'])
[email protected]81843b82010-06-28 16:49:26601 self.dependencies.append(Dependency(
602 self, s['name'], s['url'],
603 s.get('safesync_url', None),
604 s.get('custom_deps', {}),
[email protected]0d812442010-08-10 12:41:08605 s.get('custom_vars', {}),
[email protected]f50907b2010-08-12 17:05:48606 None,
607 True))
[email protected]81843b82010-06-28 16:49:26608 except KeyError:
609 raise gclient_utils.Error('Invalid .gclient file. Solution is '
610 'incomplete: %s' % s)
[email protected]9a66ddf2010-06-16 16:54:16611 # .gclient can have hooks.
612 self.deps_hooks = config_dict.get('hooks', [])
[email protected]049bced2010-08-12 13:37:20613 self.deps_parsed = True
[email protected]9a66ddf2010-06-16 16:54:16614
615 def SaveConfig(self):
616 gclient_utils.FileWrite(os.path.join(self.root_dir(),
617 self._options.config_filename),
618 self.config_content)
619
620 @staticmethod
621 def LoadCurrentConfig(options):
622 """Searches for and loads a .gclient file relative to the current working
623 dir. Returns a GClient object."""
[email protected]15804092010-09-02 17:07:37624 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
[email protected]9a66ddf2010-06-16 16:54:16625 if not path:
626 return None
627 client = GClient(path, options)
628 client.SetConfig(gclient_utils.FileRead(
629 os.path.join(path, options.config_filename)))
[email protected]15804092010-09-02 17:07:37630 return client
[email protected]9a66ddf2010-06-16 16:54:16631
632 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
633 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
634 'solution_name': solution_name,
635 'solution_url': solution_url,
636 'safesync_url' : safesync_url,
637 })
638
[email protected]df2b3152010-07-21 17:35:24639 def _SaveEntries(self):
[email protected]9a66ddf2010-06-16 16:54:16640 """Creates a .gclient_entries file to record the list of unique checkouts.
641
642 The .gclient_entries file lives in the same directory as .gclient.
[email protected]9a66ddf2010-06-16 16:54:16643 """
644 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
645 # makes testing a bit too fun.
[email protected]df2b3152010-07-21 17:35:24646 result = 'entries = {\n'
647 for entry in self.tree(False):
648 # Skip over File() dependencies as we can't version them.
649 if not isinstance(entry.parsed_url, self.FileImpl):
650 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
651 pprint.pformat(entry.parsed_url))
652 result += '}\n'
[email protected]9a66ddf2010-06-16 16:54:16653 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
[email protected]df2b3152010-07-21 17:35:24654 logging.info(result)
655 gclient_utils.FileWrite(file_path, result)
[email protected]9a66ddf2010-06-16 16:54:16656
657 def _ReadEntries(self):
658 """Read the .gclient_entries file for the given client.
659
660 Returns:
661 A sequence of solution names, which will be empty if there is the
662 entries file hasn't been created yet.
663 """
664 scope = {}
665 filename = os.path.join(self.root_dir(), self._options.entries_filename)
666 if not os.path.exists(filename):
[email protected]73e21142010-07-05 13:32:01667 return {}
[email protected]5990f9d2010-07-07 18:02:58668 try:
669 exec(gclient_utils.FileRead(filename), scope)
670 except SyntaxError, e:
671 gclient_utils.SyntaxErrorToError(filename, e)
[email protected]9a66ddf2010-06-16 16:54:16672 return scope['entries']
673
[email protected]54a07a22010-06-14 19:07:39674 def _EnforceRevisions(self):
[email protected]918a9ae2010-05-28 15:50:30675 """Checks for revision overrides."""
676 revision_overrides = {}
[email protected]307d1792010-05-31 20:03:13677 if self._options.head:
678 return revision_overrides
[email protected]54a07a22010-06-14 19:07:39679 for s in self.dependencies:
680 if not s.safesync_url:
[email protected]307d1792010-05-31 20:03:13681 continue
[email protected]54a07a22010-06-14 19:07:39682 handle = urllib.urlopen(s.safesync_url)
[email protected]307d1792010-05-31 20:03:13683 rev = handle.read().strip()
684 handle.close()
685 if len(rev):
[email protected]54a07a22010-06-14 19:07:39686 self._options.revisions.append('%s@%s' % (s.name, rev))
[email protected]307d1792010-05-31 20:03:13687 if not self._options.revisions:
688 return revision_overrides
689 # --revision will take over safesync_url.
[email protected]54a07a22010-06-14 19:07:39690 solutions_names = [s.name for s in self.dependencies]
[email protected]307d1792010-05-31 20:03:13691 index = 0
692 for revision in self._options.revisions:
693 if not '@' in revision:
694 # Support for --revision 123
695 revision = '%s@%s' % (solutions_names[index], revision)
[email protected]54a07a22010-06-14 19:07:39696 sol, rev = revision.split('@', 1)
[email protected]307d1792010-05-31 20:03:13697 if not sol in solutions_names:
698 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
699 print >> sys.stderr, ('Please fix your script, having invalid '
700 '--revision flags will soon considered an error.')
701 else:
[email protected]918a9ae2010-05-28 15:50:30702 revision_overrides[sol] = rev
[email protected]307d1792010-05-31 20:03:13703 index += 1
[email protected]918a9ae2010-05-28 15:50:30704 return revision_overrides
705
[email protected]fb2b8eb2009-04-23 21:03:42706 def RunOnDeps(self, command, args):
707 """Runs a command on each dependency in a client and its dependencies.
708
[email protected]fb2b8eb2009-04-23 21:03:42709 Args:
710 command: The command to use (e.g., 'status' or 'diff')
711 args: list of str - extra arguments to add to the command line.
[email protected]fb2b8eb2009-04-23 21:03:42712 """
[email protected]54a07a22010-06-14 19:07:39713 if not self.dependencies:
[email protected]73e21142010-07-05 13:32:01714 raise gclient_utils.Error('No solution specified')
[email protected]54a07a22010-06-14 19:07:39715 revision_overrides = self._EnforceRevisions()
[email protected]df2b3152010-07-21 17:35:24716 pm = None
[email protected]5b3f8852010-09-10 16:49:54717 # Disable progress for non-tty stdout.
[email protected]a116e7d2010-10-05 19:58:02718 if (command in ('update', 'revert') and sys.stdout.isatty() and not
719 self._options.verbose):
[email protected]049bced2010-08-12 13:37:20720 pm = Progress('Syncing projects', 1)
[email protected]9e5317a2010-08-13 20:35:11721 work_queue = gclient_utils.ExecutionQueue(self._options.jobs, pm)
[email protected]049bced2010-08-12 13:37:20722 for s in self.dependencies:
723 work_queue.enqueue(s)
[email protected]3742c842010-09-09 19:27:14724 work_queue.flush(revision_overrides, command, args, options=self._options)
[email protected]6f363722010-04-27 00:41:09725
[email protected]df2b3152010-07-21 17:35:24726 # Once all the dependencies have been processed, it's now safe to run the
727 # hooks.
728 if not self._options.nohooks:
729 self.RunHooksRecursively(self._options)
[email protected]fb2b8eb2009-04-23 21:03:42730
731 if command == 'update':
[email protected]cdcee802009-06-23 15:30:42732 # Notify the user if there is an orphaned entry in their working copy.
733 # Only delete the directory if there are no changes in it, and
734 # delete_unversioned_trees is set to true.
[email protected]df2b3152010-07-21 17:35:24735 entries = [i.name for i in self.tree(False)]
736 for entry, prev_url in self._ReadEntries().iteritems():
[email protected]04dd7de2010-10-14 13:25:49737 if not prev_url:
738 # entry must have been overridden via .gclient custom_deps
739 continue
[email protected]c5e9aec2009-08-03 18:25:56740 # Fix path separator on Windows.
741 entry_fixed = entry.replace('/', os.path.sep)
[email protected]75a59272010-06-11 22:34:03742 e_dir = os.path.join(self.root_dir(), entry_fixed)
[email protected]c5e9aec2009-08-03 18:25:56743 # Use entry and not entry_fixed there.
[email protected]0329e672009-05-13 18:41:04744 if entry not in entries and os.path.exists(e_dir):
[email protected]df2b3152010-07-21 17:35:24745 file_list = []
746 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
747 scm.status(self._options, [], file_list)
748 modified_files = file_list != []
[email protected]83017012009-09-28 18:52:12749 if not self._options.delete_unversioned_trees or modified_files:
[email protected]c5e9aec2009-08-03 18:25:56750 # There are modified files in this entry. Keep warning until
751 # removed.
[email protected]d36fba82010-06-28 16:50:40752 print(('\nWARNING: \'%s\' is no longer part of this client. '
753 'It is recommended that you manually remove it.\n') %
[email protected]c5e9aec2009-08-03 18:25:56754 entry_fixed)
[email protected]fb2b8eb2009-04-23 21:03:42755 else:
756 # Delete the entry
[email protected]73e21142010-07-05 13:32:01757 print('\n________ deleting \'%s\' in \'%s\'' % (
758 entry_fixed, self.root_dir()))
[email protected]5f3eee32009-09-17 00:34:30759 gclient_utils.RemoveDirectory(e_dir)
[email protected]fb2b8eb2009-04-23 21:03:42760 # record the current list of entries for next time
[email protected]df2b3152010-07-21 17:35:24761 self._SaveEntries()
[email protected]17cdf762010-05-28 17:30:52762 return 0
[email protected]fb2b8eb2009-04-23 21:03:42763
764 def PrintRevInfo(self):
[email protected]54a07a22010-06-14 19:07:39765 if not self.dependencies:
[email protected]73e21142010-07-05 13:32:01766 raise gclient_utils.Error('No solution specified')
[email protected]df2b3152010-07-21 17:35:24767 # Load all the settings.
[email protected]9e5317a2010-08-13 20:35:11768 work_queue = gclient_utils.ExecutionQueue(self._options.jobs, None)
[email protected]049bced2010-08-12 13:37:20769 for s in self.dependencies:
770 work_queue.enqueue(s)
[email protected]3742c842010-09-09 19:27:14771 work_queue.flush({}, None, [], options=self._options)
[email protected]fb2b8eb2009-04-23 21:03:42772
[email protected]6da25d02010-08-11 17:32:55773 def GetURLAndRev(dep):
774 """Returns the revision-qualified SCM url for a Dependency."""
775 if dep.parsed_url is None:
[email protected]baa578e2010-07-12 17:36:59776 return None
[email protected]6da25d02010-08-11 17:32:55777 if isinstance(dep.parsed_url, self.FileImpl):
778 original_url = dep.parsed_url.file_location
779 else:
780 original_url = dep.parsed_url
[email protected]5d63eb82010-03-24 23:22:09781 url, _ = gclient_utils.SplitUrlRevision(original_url)
[email protected]6da25d02010-08-11 17:32:55782 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), dep.name)
[email protected]df2b3152010-07-21 17:35:24783 if not os.path.isdir(scm.checkout_path):
784 return None
[email protected]baa578e2010-07-12 17:36:59785 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
[email protected]fb2b8eb2009-04-23 21:03:42786
[email protected]baa578e2010-07-12 17:36:59787 if self._options.snapshot:
[email protected]df2b3152010-07-21 17:35:24788 new_gclient = ''
789 # First level at .gclient
790 for d in self.dependencies:
791 entries = {}
[email protected]6da25d02010-08-11 17:32:55792 def GrabDeps(dep):
[email protected]df2b3152010-07-21 17:35:24793 """Recursively grab dependencies."""
[email protected]6da25d02010-08-11 17:32:55794 for d in dep.dependencies:
795 entries[d.name] = GetURLAndRev(d)
796 GrabDeps(d)
[email protected]df2b3152010-07-21 17:35:24797 GrabDeps(d)
798 custom_deps = []
799 for k in sorted(entries.keys()):
800 if entries[k]:
801 # Quotes aren't escaped...
802 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
803 else:
804 custom_deps.append(' \"%s\": None,\n' % k)
805 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
806 'solution_name': d.name,
807 'solution_url': d.url,
808 'safesync_url' : d.safesync_url or '',
809 'solution_deps': ''.join(custom_deps),
810 }
811 # Print the snapshot configuration file
812 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
[email protected]de8f3522010-03-11 23:47:44813 else:
[email protected]b1e315f2010-08-11 18:44:50814 entries = {}
815 for d in self.tree(False):
816 if self._options.actual:
817 entries[d.name] = GetURLAndRev(d)
818 else:
819 entries[d.name] = d.parsed_url
820 keys = sorted(entries.keys())
821 for x in keys:
[email protected]ce464892010-08-12 17:12:18822 print('%s: %s' % (x, entries[x]))
[email protected]dde32ee2010-08-10 17:44:05823 logging.info(str(self))
[email protected]fb2b8eb2009-04-23 21:03:42824
[email protected]f50907b2010-08-12 17:05:48825 def ParseDepsFile(self):
[email protected]d36fba82010-06-28 16:50:40826 """No DEPS to parse for a .gclient file."""
[email protected]049bced2010-08-12 13:37:20827 raise gclient_utils.Error('Internal error')
[email protected]d36fba82010-06-28 16:50:40828
[email protected]75a59272010-06-11 22:34:03829 def root_dir(self):
[email protected]d36fba82010-06-28 16:50:40830 """Root directory of gclient checkout."""
[email protected]75a59272010-06-11 22:34:03831 return self._root_dir
832
[email protected]271375b2010-06-23 19:17:38833 def enforced_os(self):
[email protected]d36fba82010-06-28 16:50:40834 """What deps_os entries that are to be parsed."""
[email protected]271375b2010-06-23 19:17:38835 return self._enforced_os
836
[email protected]d36fba82010-06-28 16:50:40837 def recursion_limit(self):
838 """How recursive can each dependencies in DEPS file can load DEPS file."""
839 return self._recursion_limit
840
[email protected]0d812442010-08-10 12:41:08841 def tree(self, include_all):
[email protected]d36fba82010-06-28 16:50:40842 """Returns a flat list of all the dependencies."""
[email protected]0d812442010-08-10 12:41:08843 return self.subtree(include_all)
[email protected]d36fba82010-06-28 16:50:40844
[email protected]fb2b8eb2009-04-23 21:03:42845
[email protected]5ca27692010-05-26 19:32:41846#### gclient commands.
[email protected]fb2b8eb2009-04-23 21:03:42847
848
[email protected]5ca27692010-05-26 19:32:41849def CMDcleanup(parser, args):
850 """Cleans up all working copies.
[email protected]ddff62d2010-05-17 21:02:36851
[email protected]5ca27692010-05-26 19:32:41852Mostly svn-specific. Simply runs 'svn cleanup' for each module.
[email protected]79692d62010-05-14 18:57:13853"""
[email protected]0b6a0842010-06-15 14:34:19854 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
855 help='override deps for the specified (comma-separated) '
856 'platform(s); \'all\' will process all deps_os '
857 'references')
[email protected]5ca27692010-05-26 19:32:41858 (options, args) = parser.parse_args(args)
[email protected]2806acc2009-05-15 12:33:34859 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:42860 if not client:
[email protected]0b6a0842010-06-15 14:34:19861 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:42862 if options.verbose:
863 # Print out the .gclient file. This is longer than if we just printed the
864 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:38865 print(client.config_content)
[email protected]fb2b8eb2009-04-23 21:03:42866 return client.RunOnDeps('cleanup', args)
867
868
[email protected]4b90e3a2010-07-01 20:28:26869@attr('usage', '[command] [args ...]')
870def CMDrecurse(parser, args):
871 """Operates on all the entries.
872
873 Runs a shell command on all entries.
874 """
875 # Stop parsing at the first non-arg so that these go through to the command
876 parser.disable_interspersed_args()
877 parser.add_option('-s', '--scm', action='append', default=[],
878 help='choose scm types to operate upon')
879 options, args = parser.parse_args(args)
[email protected]45e9f2d2010-10-18 13:33:46880 if not args:
881 print >> sys.stderr, 'Need to supply a command!'
882 return 1
[email protected]78cba522010-10-18 13:32:05883 root_and_entries = gclient_utils.GetGClientRootAndEntries()
884 if not root_and_entries:
885 print >> sys.stderr, (
886 'You need to run gclient sync at least once to use \'recurse\'.\n'
887 'This is because .gclient_entries needs to exist and be up to date.')
888 return 1
889 root, entries = root_and_entries
[email protected]4b90e3a2010-07-01 20:28:26890 scm_set = set()
891 for scm in options.scm:
892 scm_set.update(scm.split(','))
893
894 # Pass in the SCM type as an env variable
895 env = os.environ.copy()
896
897 for path, url in entries.iteritems():
898 scm = gclient_scm.GetScmName(url)
899 if scm_set and scm not in scm_set:
900 continue
[email protected]2b9aa8e2010-08-25 20:01:42901 cwd = os.path.normpath(os.path.join(root, path))
[email protected]ac610232010-10-13 14:01:31902 if scm:
903 env['GCLIENT_SCM'] = scm
904 if url:
905 env['GCLIENT_URL'] = url
906 gclient_utils.Popen(args, cwd=cwd, env=env).communicate()
907 return 0
[email protected]4b90e3a2010-07-01 20:28:26908
909
[email protected]5ca27692010-05-26 19:32:41910@attr('usage', '[url] [safesync url]')
911def CMDconfig(parser, args):
[email protected]ddff62d2010-05-17 21:02:36912 """Create a .gclient file in the current directory.
913
[email protected]5ca27692010-05-26 19:32:41914This specifies the configuration for further commands. After update/sync,
[email protected]79692d62010-05-14 18:57:13915top-level DEPS files in each module are read to determine dependent
[email protected]5ca27692010-05-26 19:32:41916modules to operate on as well. If optional [url] parameter is
[email protected]79692d62010-05-14 18:57:13917provided, then configuration is read from a specified Subversion server
[email protected]5ca27692010-05-26 19:32:41918URL.
[email protected]79692d62010-05-14 18:57:13919"""
[email protected]0b6a0842010-06-15 14:34:19920 parser.add_option('--spec',
921 help='create a gclient file containing the provided '
922 'string. Due to Cygwin/Python brokenness, it '
923 'probably can\'t contain any newlines.')
924 parser.add_option('--name',
925 help='overrides the default name for the solution')
[email protected]5ca27692010-05-26 19:32:41926 (options, args) = parser.parse_args(args)
[email protected]5fc2a332010-05-26 19:37:15927 if ((options.spec and args) or len(args) > 2 or
928 (not options.spec and not args)):
929 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
930
[email protected]0329e672009-05-13 18:41:04931 if os.path.exists(options.config_filename):
[email protected]0b6a0842010-06-15 14:34:19932 raise gclient_utils.Error('%s file already exists in the current directory'
[email protected]e3608df2009-11-10 20:22:57933 % options.config_filename)
[email protected]2806acc2009-05-15 12:33:34934 client = GClient('.', options)
[email protected]fb2b8eb2009-04-23 21:03:42935 if options.spec:
936 client.SetConfig(options.spec)
937 else:
[email protected]1ab7ffc2009-06-03 17:21:37938 base_url = args[0].rstrip('/')
[email protected]8cf7a392010-04-07 17:20:26939 if not options.name:
[email protected]0b6a0842010-06-15 14:34:19940 name = base_url.split('/')[-1]
[email protected]8cf7a392010-04-07 17:20:26941 else:
942 # specify an alternate relpath for the given URL.
943 name = options.name
[email protected]0b6a0842010-06-15 14:34:19944 safesync_url = ''
[email protected]fb2b8eb2009-04-23 21:03:42945 if len(args) > 1:
946 safesync_url = args[1]
947 client.SetDefaultConfig(name, base_url, safesync_url)
948 client.SaveConfig()
[email protected]79692d62010-05-14 18:57:13949 return 0
[email protected]fb2b8eb2009-04-23 21:03:42950
951
[email protected]5ca27692010-05-26 19:32:41952def CMDexport(parser, args):
[email protected]ddff62d2010-05-17 21:02:36953 """Wrapper for svn export for all managed directories."""
[email protected]0b6a0842010-06-15 14:34:19954 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
955 help='override deps for the specified (comma-separated) '
956 'platform(s); \'all\' will process all deps_os '
957 'references')
[email protected]5ca27692010-05-26 19:32:41958 (options, args) = parser.parse_args(args)
[email protected]644aa0c2009-07-17 20:20:41959 if len(args) != 1:
[email protected]0b6a0842010-06-15 14:34:19960 raise gclient_utils.Error('Need directory name')
[email protected]644aa0c2009-07-17 20:20:41961 client = GClient.LoadCurrentConfig(options)
962
963 if not client:
[email protected]0b6a0842010-06-15 14:34:19964 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]644aa0c2009-07-17 20:20:41965
966 if options.verbose:
967 # Print out the .gclient file. This is longer than if we just printed the
968 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:38969 print(client.config_content)
[email protected]644aa0c2009-07-17 20:20:41970 return client.RunOnDeps('export', args)
971
[email protected]fb2b8eb2009-04-23 21:03:42972
[email protected]5ca27692010-05-26 19:32:41973@attr('epilog', """Example:
974 gclient pack > patch.txt
975 generate simple patch for configured client and dependences
976""")
977def CMDpack(parser, args):
[email protected]79692d62010-05-14 18:57:13978 """Generate a patch which can be applied at the root of the tree.
[email protected]ddff62d2010-05-17 21:02:36979
[email protected]5ca27692010-05-26 19:32:41980Internally, runs 'svn diff'/'git diff' on each checked out module and
[email protected]79692d62010-05-14 18:57:13981dependencies, and performs minimal postprocessing of the output. The
982resulting patch is printed to stdout and can be applied to a freshly
[email protected]5ca27692010-05-26 19:32:41983checked out tree via 'patch -p0 < patchfile'.
[email protected]79692d62010-05-14 18:57:13984"""
[email protected]0b6a0842010-06-15 14:34:19985 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
986 help='override deps for the specified (comma-separated) '
987 'platform(s); \'all\' will process all deps_os '
988 'references')
[email protected]5ca27692010-05-26 19:32:41989 (options, args) = parser.parse_args(args)
[email protected]ab318592009-09-04 00:54:55990 client = GClient.LoadCurrentConfig(options)
991 if not client:
[email protected]0b6a0842010-06-15 14:34:19992 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]ab318592009-09-04 00:54:55993 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.
[email protected]116704f2010-06-11 17:34:38996 print(client.config_content)
[email protected]ab318592009-09-04 00:54:55997 return client.RunOnDeps('pack', args)
998
999
[email protected]5ca27692010-05-26 19:32:411000def CMDstatus(parser, args):
1001 """Show modification status for every dependencies."""
[email protected]0b6a0842010-06-15 14:34:191002 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1003 help='override deps for the specified (comma-separated) '
1004 'platform(s); \'all\' will process all deps_os '
1005 'references')
[email protected]5ca27692010-05-26 19:32:411006 (options, args) = parser.parse_args(args)
[email protected]2806acc2009-05-15 12:33:341007 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421008 if not client:
[email protected]0b6a0842010-06-15 14:34:191009 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:421010 if options.verbose:
1011 # Print out the .gclient file. This is longer than if we just printed the
1012 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:381013 print(client.config_content)
[email protected]fb2b8eb2009-04-23 21:03:421014 return client.RunOnDeps('status', args)
1015
1016
[email protected]5ca27692010-05-26 19:32:411017@attr('epilog', """Examples:
[email protected]79692d62010-05-14 18:57:131018 gclient sync
1019 update files from SCM according to current configuration,
1020 *for modules which have changed since last update or sync*
1021 gclient sync --force
1022 update files from SCM according to current configuration, for
1023 all modules (useful for recovering files deleted from local copy)
1024 gclient sync --revision src@31000
1025 update src directory to r31000
[email protected]5ca27692010-05-26 19:32:411026""")
1027def CMDsync(parser, args):
1028 """Checkout/update all modules."""
[email protected]0b6a0842010-06-15 14:34:191029 parser.add_option('-f', '--force', action='store_true',
1030 help='force update even for unchanged modules')
1031 parser.add_option('-n', '--nohooks', action='store_true',
1032 help='don\'t run hooks after the update is complete')
1033 parser.add_option('-r', '--revision', action='append',
1034 dest='revisions', metavar='REV', default=[],
1035 help='Enforces revision/hash for the solutions with the '
1036 'format src@rev. The src@ part is optional and can be '
1037 'skipped. -r can be used multiple times when .gclient '
1038 'has multiple solutions configured and will work even '
1039 'if the src@ part is skipped.')
1040 parser.add_option('-H', '--head', action='store_true',
1041 help='skips any safesync_urls specified in '
1042 'configured solutions and sync to head instead')
1043 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
1044 help='delete any unexpected unversioned trees '
1045 'that are in the checkout')
1046 parser.add_option('-R', '--reset', action='store_true',
1047 help='resets any local changes before updating (git only)')
1048 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1049 help='override deps for the specified (comma-separated) '
1050 'platform(s); \'all\' will process all deps_os '
1051 'references')
1052 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
1053 help='Skip svn up whenever possible by requesting '
1054 'actual HEAD revision from the repository')
[email protected]5ca27692010-05-26 19:32:411055 (options, args) = parser.parse_args(args)
[email protected]2806acc2009-05-15 12:33:341056 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421057
1058 if not client:
[email protected]0b6a0842010-06-15 14:34:191059 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:421060
[email protected]307d1792010-05-31 20:03:131061 if options.revisions and options.head:
1062 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
[email protected]0b6a0842010-06-15 14:34:191063 print('Warning: you cannot use both --head and --revision')
[email protected]fb2b8eb2009-04-23 21:03:421064
1065 if options.verbose:
1066 # Print out the .gclient file. This is longer than if we just printed the
1067 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:381068 print(client.config_content)
[email protected]fb2b8eb2009-04-23 21:03:421069 return client.RunOnDeps('update', args)
1070
1071
[email protected]5ca27692010-05-26 19:32:411072def CMDupdate(parser, args):
[email protected]ddff62d2010-05-17 21:02:361073 """Alias for the sync command. Deprecated."""
[email protected]5ca27692010-05-26 19:32:411074 return CMDsync(parser, args)
[email protected]fb2b8eb2009-04-23 21:03:421075
[email protected]5ca27692010-05-26 19:32:411076def CMDdiff(parser, args):
1077 """Displays local diff for every dependencies."""
[email protected]0b6a0842010-06-15 14:34:191078 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1079 help='override deps for the specified (comma-separated) '
1080 'platform(s); \'all\' will process all deps_os '
1081 'references')
[email protected]5ca27692010-05-26 19:32:411082 (options, args) = parser.parse_args(args)
[email protected]2806acc2009-05-15 12:33:341083 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421084 if not client:
[email protected]0b6a0842010-06-15 14:34:191085 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:421086 if options.verbose:
1087 # Print out the .gclient file. This is longer than if we just printed the
1088 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:381089 print(client.config_content)
[email protected]fb2b8eb2009-04-23 21:03:421090 return client.RunOnDeps('diff', args)
1091
1092
[email protected]5ca27692010-05-26 19:32:411093def CMDrevert(parser, args):
1094 """Revert all modifications in every dependencies."""
[email protected]0b6a0842010-06-15 14:34:191095 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1096 help='override deps for the specified (comma-separated) '
1097 'platform(s); \'all\' will process all deps_os '
1098 'references')
1099 parser.add_option('-n', '--nohooks', action='store_true',
1100 help='don\'t run hooks after the revert is complete')
[email protected]5ca27692010-05-26 19:32:411101 (options, args) = parser.parse_args(args)
1102 # --force is implied.
1103 options.force = True
[email protected]2806acc2009-05-15 12:33:341104 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421105 if not client:
[email protected]0b6a0842010-06-15 14:34:191106 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:421107 return client.RunOnDeps('revert', args)
1108
1109
[email protected]5ca27692010-05-26 19:32:411110def CMDrunhooks(parser, args):
1111 """Runs hooks for files that have been modified in the local working copy."""
[email protected]0b6a0842010-06-15 14:34:191112 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1113 help='override deps for the specified (comma-separated) '
1114 'platform(s); \'all\' will process all deps_os '
1115 'references')
1116 parser.add_option('-f', '--force', action='store_true', default=True,
1117 help='Deprecated. No effect.')
[email protected]5ca27692010-05-26 19:32:411118 (options, args) = parser.parse_args(args)
[email protected]2806acc2009-05-15 12:33:341119 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421120 if not client:
[email protected]0b6a0842010-06-15 14:34:191121 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:421122 if options.verbose:
1123 # Print out the .gclient file. This is longer than if we just printed the
1124 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:381125 print(client.config_content)
[email protected]5df6a462009-08-28 18:52:261126 options.force = True
[email protected]5ca27692010-05-26 19:32:411127 options.nohooks = False
[email protected]fb2b8eb2009-04-23 21:03:421128 return client.RunOnDeps('runhooks', args)
1129
1130
[email protected]5ca27692010-05-26 19:32:411131def CMDrevinfo(parser, args):
[email protected]9eda4112010-06-11 18:56:101132 """Output revision info mapping for the client and its dependencies.
1133
[email protected]0b6a0842010-06-15 14:34:191134 This allows the capture of an overall 'revision' for the source tree that
[email protected]9eda4112010-06-11 18:56:101135 can be used to reproduce the same tree in the future. It is only useful for
[email protected]0b6a0842010-06-15 14:34:191136 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1137 number or a git hash. A git branch name isn't 'pinned' since the actual
[email protected]9eda4112010-06-11 18:56:101138 commit can change.
1139 """
1140 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1141 help='override deps for the specified (comma-separated) '
1142 'platform(s); \'all\' will process all deps_os '
1143 'references')
[email protected]b1e315f2010-08-11 18:44:501144 parser.add_option('-a', '--actual', action='store_true',
1145 help='gets the actual checked out revisions instead of the '
1146 'ones specified in the DEPS and .gclient files')
[email protected]9eda4112010-06-11 18:56:101147 parser.add_option('-s', '--snapshot', action='store_true',
1148 help='creates a snapshot .gclient file of the current '
[email protected]b1e315f2010-08-11 18:44:501149 'version of all repositories to reproduce the tree, '
1150 'implies -a')
[email protected]5ca27692010-05-26 19:32:411151 (options, args) = parser.parse_args(args)
[email protected]2806acc2009-05-15 12:33:341152 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421153 if not client:
[email protected]0b6a0842010-06-15 14:34:191154 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:421155 client.PrintRevInfo()
[email protected]79692d62010-05-14 18:57:131156 return 0
[email protected]fb2b8eb2009-04-23 21:03:421157
1158
[email protected]5ca27692010-05-26 19:32:411159def Command(name):
1160 return getattr(sys.modules[__name__], 'CMD' + name, None)
1161
1162
1163def CMDhelp(parser, args):
1164 """Prints list of commands or help for a specific command."""
[email protected]6e29d572010-06-04 17:32:201165 (_, args) = parser.parse_args(args)
[email protected]ddff62d2010-05-17 21:02:361166 if len(args) == 1:
[email protected]5ca27692010-05-26 19:32:411167 return Main(args + ['--help'])
[email protected]ddff62d2010-05-17 21:02:361168 parser.print_help()
1169 return 0
1170
1171
[email protected]5ca27692010-05-26 19:32:411172def GenUsage(parser, command):
1173 """Modify an OptParse object with the function's documentation."""
1174 obj = Command(command)
1175 if command == 'help':
1176 command = '<command>'
1177 # OptParser.description prefer nicely non-formatted strings.
1178 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1179 usage = getattr(obj, 'usage', '')
1180 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1181 parser.epilog = getattr(obj, 'epilog', None)
[email protected]fb2b8eb2009-04-23 21:03:421182
1183
1184def Main(argv):
[email protected]5ca27692010-05-26 19:32:411185 """Doesn't parse the arguments here, just find the right subcommand to
1186 execute."""
[email protected]6e29d572010-06-04 17:32:201187 try:
[email protected]e0de9cb2010-09-17 15:07:141188 # Make stdout auto-flush so buildbot doesn't kill us during lengthy
1189 # operations. Python as a strong tendency to buffer sys.stdout.
1190 sys.stdout = gclient_utils.MakeFileAutoFlush(sys.stdout)
[email protected]4ed34182010-09-17 15:57:471191 # Make stdout annotated with the thread ids.
1192 sys.stdout = gclient_utils.MakeFileAnnotated(sys.stdout)
[email protected]6e29d572010-06-04 17:32:201193 # Do it late so all commands are listed.
1194 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1195 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1196 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1197 parser = optparse.OptionParser(version='%prog ' + __version__)
[email protected]fac9d3d2010-09-10 20:38:491198 parser.add_option('-j', '--jobs', default=1, type='int',
[email protected]3b84d4c2010-09-10 18:02:431199 help='Specify how many SCM commands can run in parallel; '
1200 'default=%default')
[email protected]f0fc9912010-06-11 17:57:331201 parser.add_option('-v', '--verbose', action='count', default=0,
1202 help='Produces additional output for diagnostics. Can be '
1203 'used up to three times for more logging info.')
1204 parser.add_option('--gclientfile', dest='config_filename',
1205 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1206 help='Specify an alternate %default file')
[email protected]6e29d572010-06-04 17:32:201207 # Integrate standard options processing.
1208 old_parser = parser.parse_args
1209 def Parse(args):
1210 (options, args) = old_parser(args)
[email protected]f0fc9912010-06-11 17:57:331211 level = None
[email protected]6e29d572010-06-04 17:32:201212 if options.verbose == 2:
[email protected]f0fc9912010-06-11 17:57:331213 level = logging.INFO
[email protected]6e29d572010-06-04 17:32:201214 elif options.verbose > 2:
[email protected]f0fc9912010-06-11 17:57:331215 level = logging.DEBUG
1216 logging.basicConfig(level=level,
1217 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1218 options.entries_filename = options.config_filename + '_entries'
[email protected]9e5317a2010-08-13 20:35:111219 if options.jobs < 1:
1220 parser.error('--jobs must be 1 or higher')
[email protected]e3216c62010-07-08 03:31:431221
1222 # These hacks need to die.
[email protected]6e29d572010-06-04 17:32:201223 if not hasattr(options, 'revisions'):
1224 # GClient.RunOnDeps expects it even if not applicable.
1225 options.revisions = []
1226 if not hasattr(options, 'head'):
1227 options.head = None
[email protected]f0fc9912010-06-11 17:57:331228 if not hasattr(options, 'nohooks'):
1229 options.nohooks = True
1230 if not hasattr(options, 'deps_os'):
1231 options.deps_os = None
[email protected]e3216c62010-07-08 03:31:431232 if not hasattr(options, 'manually_grab_svn_rev'):
1233 options.manually_grab_svn_rev = None
1234 if not hasattr(options, 'force'):
1235 options.force = None
[email protected]6e29d572010-06-04 17:32:201236 return (options, args)
1237 parser.parse_args = Parse
1238 # We don't want wordwrapping in epilog (usually examples)
1239 parser.format_epilog = lambda _: parser.epilog or ''
1240 if argv:
1241 command = Command(argv[0])
1242 if command:
[email protected]f0fc9912010-06-11 17:57:331243 # 'fix' the usage and the description now that we know the subcommand.
[email protected]6e29d572010-06-04 17:32:201244 GenUsage(parser, argv[0])
1245 return command(parser, argv[1:])
1246 # Not a known command. Default to help.
1247 GenUsage(parser, 'help')
1248 return CMDhelp(parser, argv)
1249 except gclient_utils.Error, e:
[email protected]f0fc9912010-06-11 17:57:331250 print >> sys.stderr, 'Error: %s' % str(e)
[email protected]6e29d572010-06-04 17:32:201251 return 1
[email protected]fb2b8eb2009-04-23 21:03:421252
1253
[email protected]f0fc9912010-06-11 17:57:331254if '__main__' == __name__:
[email protected]6e29d572010-06-04 17:32:201255 sys.exit(Main(sys.argv[1:]))
[email protected]fb2b8eb2009-04-23 21:03:421256
1257# vim: ts=2:sw=2:tw=80:et: