blob: 4170f5fc62e716bfa8a024f5abfb227acf8a615c [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]049bced2010-08-12 13:37:2052__version__ = "0.5.1"
[email protected]fb2b8eb2009-04-23 21:03:4253
[email protected]754960e2009-09-21 12:31:0554import logging
[email protected]fb2b8eb2009-04-23 21:03:4255import optparse
56import os
[email protected]621939b2010-08-10 20:12:0057import posixpath
[email protected]2e38de72009-09-28 17:04:4758import pprint
[email protected]fb2b8eb2009-04-23 21:03:4259import re
[email protected]4b90e3a2010-07-01 20:28:2660import subprocess
[email protected]fb2b8eb2009-04-23 21:03:4261import sys
[email protected]049bced2010-08-12 13:37:2062import threading
[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
[email protected]049bced2010-08-12 13:37:2083class WorkItem(object):
84 """One work item."""
85 requirements = []
86 name = None
87
88 def run(self):
89 pass
90
91
92class ExecutionQueue(object):
93 """Dependencies sometime needs to be run out of order due to From() keyword.
94
95 This class manages that all the required dependencies are run before running
96 each one.
97
98 Methods of this class are multithread safe.
99 """
100 def __init__(self, progress):
101 self.lock = threading.Lock()
102 # List of Dependency.
103 self.queued = []
104 # List of strings representing each Dependency.name that was run.
105 self.ran = []
106 # List of items currently running.
107 self.running = []
108 self.progress = progress
109 if self.progress:
110 self.progress.update()
111
112 def enqueue(self, d):
113 """Enqueue one Dependency to be executed later once its requirements are
114 satisfied.
115 """
116 assert isinstance(d, WorkItem)
117 try:
118 self.lock.acquire()
119 self.queued.append(d)
120 total = len(self.queued) + len(self.ran) + len(self.running)
121 finally:
122 self.lock.release()
123 if self.progress:
124 self.progress._total = total + 1
125 self.progress.update(0)
126
127 def flush(self, *args, **kwargs):
128 """Runs all enqueued items until all are executed."""
129 while self._run_one_item(*args, **kwargs):
130 pass
131 queued = []
132 running = []
133 try:
134 self.lock.acquire()
135 if self.queued:
136 queued = self.queued
137 self.queued = []
138 if self.running:
139 running = self.running
140 self.running = []
141 finally:
142 self.lock.release()
143 if self.progress:
144 self.progress.end()
145 if queued:
146 raise gclient_utils.Error('Entries still queued: %s' % str(queued))
147 if running:
148 raise gclient_utils.Error('Entries still queued: %s' % str(running))
149
150 def _run_one_item(self, *args, **kwargs):
151 """Removes one item from the queue that has all its requirements completed
152 and execute it.
153
154 Returns False if no item could be run.
155 """
156 i = 0
157 d = None
158 try:
159 self.lock.acquire()
160 while i != len(self.queued) and not d:
161 d = self.queued.pop(i)
162 for r in d.requirements:
163 if not r in self.ran:
164 self.queued.insert(i, d)
165 d = None
166 break
167 i += 1
168 if not d:
169 return False
170 self.running.append(d)
171 finally:
172 self.lock.release()
173 d.run(*args, **kwargs)
174 try:
175 self.lock.acquire()
176 # TODO(maruel): http://crbug.com/51711
177 #assert not d.name in self.ran
178 if not d.name in self.ran:
179 self.ran.append(d.name)
180 self.running.remove(d)
181 if self.progress:
182 self.progress.update(1)
183 finally:
184 self.lock.release()
185 return True
186
[email protected]fb2b8eb2009-04-23 21:03:42187
[email protected]116704f2010-06-11 17:34:38188class GClientKeywords(object):
189 class FromImpl(object):
190 """Used to implement the From() syntax."""
191
192 def __init__(self, module_name, sub_target_name=None):
193 """module_name is the dep module we want to include from. It can also be
194 the name of a subdirectory to include from.
195
196 sub_target_name is an optional parameter if the module name in the other
197 DEPS file is different. E.g., you might want to map src/net to net."""
198 self.module_name = module_name
199 self.sub_target_name = sub_target_name
200
201 def __str__(self):
202 return 'From(%s, %s)' % (repr(self.module_name),
203 repr(self.sub_target_name))
204
[email protected]116704f2010-06-11 17:34:38205 class FileImpl(object):
206 """Used to implement the File('') syntax which lets you sync a single file
[email protected]e3216c62010-07-08 03:31:43207 from a SVN repo."""
[email protected]116704f2010-06-11 17:34:38208
209 def __init__(self, file_location):
210 self.file_location = file_location
211
212 def __str__(self):
213 return 'File("%s")' % self.file_location
214
215 def GetPath(self):
216 return os.path.split(self.file_location)[0]
217
218 def GetFilename(self):
219 rev_tokens = self.file_location.split('@')
220 return os.path.split(rev_tokens[0])[1]
221
222 def GetRevision(self):
223 rev_tokens = self.file_location.split('@')
224 if len(rev_tokens) > 1:
225 return rev_tokens[1]
226 return None
227
228 class VarImpl(object):
229 def __init__(self, custom_vars, local_scope):
230 self._custom_vars = custom_vars
231 self._local_scope = local_scope
232
233 def Lookup(self, var_name):
234 """Implements the Var syntax."""
235 if var_name in self._custom_vars:
236 return self._custom_vars[var_name]
237 elif var_name in self._local_scope.get("vars", {}):
238 return self._local_scope["vars"][var_name]
239 raise gclient_utils.Error("Var is not defined: %s" % var_name)
240
241
[email protected]049bced2010-08-12 13:37:20242class Dependency(GClientKeywords, WorkItem):
[email protected]54a07a22010-06-14 19:07:39243 """Object that represents a dependency checkout."""
[email protected]9eda4112010-06-11 18:56:10244 DEPS_FILE = 'DEPS'
[email protected]0b6a0842010-06-15 14:34:19245
[email protected]0d812442010-08-10 12:41:08246 def __init__(self, parent, name, url, safesync_url, custom_deps,
247 custom_vars, deps_file):
[email protected]54a07a22010-06-14 19:07:39248 GClientKeywords.__init__(self)
249 self.parent = parent
250 self.name = name
251 self.url = url
[email protected]df2b3152010-07-21 17:35:24252 self.parsed_url = None
[email protected]54a07a22010-06-14 19:07:39253 # These 2 are only set in .gclient and not in DEPS files.
254 self.safesync_url = safesync_url
255 self.custom_vars = custom_vars or {}
256 self.custom_deps = custom_deps or {}
[email protected]0b6a0842010-06-15 14:34:19257 self.deps_hooks = []
[email protected]54a07a22010-06-14 19:07:39258 self.dependencies = []
259 self.deps_file = deps_file or self.DEPS_FILE
[email protected]df2b3152010-07-21 17:35:24260 # A cache of the files affected by the current operation, necessary for
261 # hooks.
[email protected]861fd0f2010-07-23 03:05:05262 self._file_list = []
[email protected]85c2a192010-07-22 21:14:43263 # If it is not set to True, the dependency wasn't processed for its child
264 # dependency, i.e. its DEPS wasn't read.
[email protected]271375b2010-06-23 19:17:38265 self.deps_parsed = False
[email protected]85c2a192010-07-22 21:14:43266 # A direct reference is dependency that is referenced by a deps, deps_os or
267 # solution. A indirect one is one that was loaded with From() or that
268 # exceeded recursion limit.
[email protected]271375b2010-06-23 19:17:38269 self.direct_reference = False
[email protected]f3abb802010-08-10 17:19:56270 # This dependency has been processed, i.e. checked out
271 self.processed = False
272 # This dependency had its hook run
273 self.hooks_ran = False
[email protected]621939b2010-08-10 20:12:00274 # Required dependencies to run before running this one:
275 self.requirements = []
276 if self.parent and self.parent.name:
277 self.requirements.append(self.parent.name)
278 if isinstance(self.url, self.FromImpl):
279 self.requirements.append(self.url.module_name)
[email protected]fb2b8eb2009-04-23 21:03:42280
[email protected]54a07a22010-06-14 19:07:39281 # Sanity checks
282 if not self.name and self.parent:
283 raise gclient_utils.Error('Dependency without name')
284 if not isinstance(self.url,
285 (basestring, self.FromImpl, self.FileImpl, None.__class__)):
286 raise gclient_utils.Error('dependency url must be either a string, None, '
287 'File() or From() instead of %s' %
288 self.url.__class__.__name__)
289 if '/' in self.deps_file or '\\' in self.deps_file:
290 raise gclient_utils.Error('deps_file name must not be a path, just a '
291 'filename. %s' % self.deps_file)
292
[email protected]df2b3152010-07-21 17:35:24293 def LateOverride(self, url):
[email protected]da7a1f92010-08-10 17:19:02294 """Resolves the parsed url from url.
295
296 Manages From() keyword accordingly. Do not touch self.parsed_url nor
297 self.url because it may called with other urls due to From()."""
[email protected]df2b3152010-07-21 17:35:24298 overriden_url = self.get_custom_deps(self.name, url)
299 if overriden_url != url:
[email protected]dde32ee2010-08-10 17:44:05300 logging.info('%s, %s was overriden to %s' % (self.name, url,
[email protected]da7a1f92010-08-10 17:19:02301 overriden_url))
302 return overriden_url
[email protected]df2b3152010-07-21 17:35:24303 elif isinstance(url, self.FromImpl):
304 ref = [dep for dep in self.tree(True) if url.module_name == dep.name]
[email protected]f3abb802010-08-10 17:19:56305 if not ref:
306 raise gclient_utils.Error('Failed to find one reference to %s. %s' % (
307 url.module_name, ref))
308 # It may happen that len(ref) > 1 but it's no big deal.
[email protected]df2b3152010-07-21 17:35:24309 ref = ref[0]
[email protected]98d05fa2010-07-22 21:58:01310 sub_target = url.sub_target_name or self.name
[email protected]df2b3152010-07-21 17:35:24311 # Make sure the referenced dependency DEPS file is loaded and file the
312 # inner referenced dependency.
313 ref.ParseDepsFile(False)
314 found_dep = None
315 for d in ref.dependencies:
316 if d.name == sub_target:
317 found_dep = d
318 break
319 if not found_dep:
[email protected]f3abb802010-08-10 17:19:56320 raise gclient_utils.Error(
[email protected]dde32ee2010-08-10 17:44:05321 'Couldn\'t find %s in %s, referenced by %s\n%s' % (
322 sub_target, ref.name, self.name, str(self.root_parent())))
[email protected]df2b3152010-07-21 17:35:24323 # Call LateOverride() again.
[email protected]da7a1f92010-08-10 17:19:02324 parsed_url = found_dep.LateOverride(found_dep.url)
[email protected]dde32ee2010-08-10 17:44:05325 logging.info('%s, %s to %s' % (self.name, url, parsed_url))
[email protected]da7a1f92010-08-10 17:19:02326 return parsed_url
[email protected]df2b3152010-07-21 17:35:24327 elif isinstance(url, basestring):
328 parsed_url = urlparse.urlparse(url)
329 if not parsed_url[0]:
330 # A relative url. Fetch the real base.
331 path = parsed_url[2]
332 if not path.startswith('/'):
333 raise gclient_utils.Error(
334 'relative DEPS entry \'%s\' must begin with a slash' % url)
335 # Create a scm just to query the full url.
336 parent_url = self.parent.parsed_url
337 if isinstance(parent_url, self.FileImpl):
338 parent_url = parent_url.file_location
339 scm = gclient_scm.CreateSCM(parent_url, self.root_dir(), None)
[email protected]da7a1f92010-08-10 17:19:02340 parsed_url = scm.FullUrlForRelativeUrl(url)
[email protected]df2b3152010-07-21 17:35:24341 else:
[email protected]da7a1f92010-08-10 17:19:02342 parsed_url = url
[email protected]dde32ee2010-08-10 17:44:05343 logging.info('%s, %s -> %s' % (self.name, url, parsed_url))
[email protected]da7a1f92010-08-10 17:19:02344 return parsed_url
[email protected]df2b3152010-07-21 17:35:24345 elif isinstance(url, self.FileImpl):
[email protected]da7a1f92010-08-10 17:19:02346 parsed_url = url
[email protected]dde32ee2010-08-10 17:44:05347 logging.info('%s, %s -> %s (File)' % (self.name, url, parsed_url))
[email protected]da7a1f92010-08-10 17:19:02348 return parsed_url
349 elif url is None:
350 return None
351 else:
352 raise gclient_utils.Error('Unkown url type')
[email protected]df2b3152010-07-21 17:35:24353
[email protected]271375b2010-06-23 19:17:38354 def ParseDepsFile(self, direct_reference):
355 """Parses the DEPS file for this dependency."""
356 if direct_reference:
357 # Maybe it was referenced earlier by a From() keyword but it's now
358 # directly referenced.
359 self.direct_reference = direct_reference
[email protected]df2b3152010-07-21 17:35:24360 if self.deps_parsed:
[email protected]dde32ee2010-08-10 17:44:05361 logging.debug('%s was already parsed' % self.name)
[email protected]df2b3152010-07-21 17:35:24362 return
[email protected]271375b2010-06-23 19:17:38363 self.deps_parsed = True
364 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
365 if not os.path.isfile(filepath):
[email protected]dde32ee2010-08-10 17:44:05366 logging.info('%s: No DEPS file found at %s' % (self.name, filepath))
[email protected]df2b3152010-07-21 17:35:24367 return
[email protected]271375b2010-06-23 19:17:38368 deps_content = gclient_utils.FileRead(filepath)
[email protected]dde32ee2010-08-10 17:44:05369 logging.debug(deps_content)
[email protected]0d425922010-06-21 19:22:24370
[email protected]271375b2010-06-23 19:17:38371 # Eval the content.
372 # One thing is unintuitive, vars= {} must happen before Var() use.
373 local_scope = {}
374 var = self.VarImpl(self.custom_vars, local_scope)
375 global_scope = {
376 'File': self.FileImpl,
377 'From': self.FromImpl,
378 'Var': var.Lookup,
379 'deps_os': {},
380 }
[email protected]5990f9d2010-07-07 18:02:58381 try:
382 exec(deps_content, global_scope, local_scope)
383 except SyntaxError, e:
384 gclient_utils.SyntaxErrorToError(filepath, e)
[email protected]271375b2010-06-23 19:17:38385 deps = local_scope.get('deps', {})
[email protected]fb2b8eb2009-04-23 21:03:42386 # load os specific dependencies if defined. these dependencies may
387 # override or extend the values defined by the 'deps' member.
[email protected]271375b2010-06-23 19:17:38388 if 'deps_os' in local_scope:
389 for deps_os_key in self.enforced_os():
390 os_deps = local_scope['deps_os'].get(deps_os_key, {})
391 if len(self.enforced_os()) > 1:
392 # Ignore any conflict when including deps for more than one
[email protected]fb2b8eb2009-04-23 21:03:42393 # platform, so we collect the broadest set of dependencies available.
394 # We may end up with the wrong revision of something for our
395 # platform, but this is the best we can do.
396 deps.update([x for x in os_deps.items() if not x[0] in deps])
397 else:
398 deps.update(os_deps)
399
[email protected]271375b2010-06-23 19:17:38400 self.deps_hooks.extend(local_scope.get('hooks', []))
401
402 # If a line is in custom_deps, but not in the solution, we want to append
403 # this line to the solution.
404 for d in self.custom_deps:
405 if d not in deps:
406 deps[d] = self.custom_deps[d]
[email protected]fb2b8eb2009-04-23 21:03:42407
408 # If use_relative_paths is set in the DEPS file, regenerate
409 # the dictionary using paths relative to the directory containing
410 # the DEPS file.
[email protected]271375b2010-06-23 19:17:38411 use_relative_paths = local_scope.get('use_relative_paths', False)
412 if use_relative_paths:
[email protected]fb2b8eb2009-04-23 21:03:42413 rel_deps = {}
414 for d, url in deps.items():
415 # normpath is required to allow DEPS to use .. in their
416 # dependency local path.
[email protected]271375b2010-06-23 19:17:38417 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
418 deps = rel_deps
[email protected]fb2b8eb2009-04-23 21:03:42419
[email protected]df2b3152010-07-21 17:35:24420 # Convert the deps into real Dependency.
421 for name, url in deps.iteritems():
422 if name in [s.name for s in self.dependencies]:
[email protected]dde32ee2010-08-10 17:44:05423 raise gclient_utils.Error(
424 'The same name "%s" appears multiple times in the deps section' %
425 name)
[email protected]0d812442010-08-10 12:41:08426 self.dependencies.append(Dependency(self, name, url, None, None, None,
427 None))
[email protected]dde32ee2010-08-10 17:44:05428 logging.debug('Loaded: %s' % str(self))
[email protected]fb2b8eb2009-04-23 21:03:42429
[email protected]049bced2010-08-12 13:37:20430 def run(self, options, revision_overrides, command, args, work_queue):
[email protected]df2b3152010-07-21 17:35:24431 """Runs 'command' before parsing the DEPS in case it's a initial checkout
432 or a revert."""
[email protected]861fd0f2010-07-23 03:05:05433 assert self._file_list == []
[email protected]df2b3152010-07-21 17:35:24434 # When running runhooks, there's no need to consult the SCM.
435 # All known hooks are expected to run unconditionally regardless of working
436 # copy state, so skip the SCM status check.
437 run_scm = command not in ('runhooks', None)
[email protected]da7a1f92010-08-10 17:19:02438 self.parsed_url = self.LateOverride(self.url)
[email protected]df2b3152010-07-21 17:35:24439 if run_scm and self.parsed_url:
440 if isinstance(self.parsed_url, self.FileImpl):
441 # Special support for single-file checkout.
442 if not command in (None, 'cleanup', 'diff', 'pack', 'status'):
443 options.revision = self.parsed_url.GetRevision()
444 scm = gclient_scm.SVNWrapper(self.parsed_url.GetPath(),
445 self.root_dir(),
446 self.name)
447 scm.RunCommand('updatesingle', options,
448 args + [self.parsed_url.GetFilename()],
[email protected]861fd0f2010-07-23 03:05:05449 self._file_list)
[email protected]df2b3152010-07-21 17:35:24450 else:
451 options.revision = revision_overrides.get(self.name)
452 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name)
[email protected]861fd0f2010-07-23 03:05:05453 scm.RunCommand(command, options, args, self._file_list)
454 self._file_list = [os.path.join(self.name, f.strip())
455 for f in self._file_list]
[email protected]df2b3152010-07-21 17:35:24456 options.revision = None
[email protected]f3abb802010-08-10 17:19:56457 self.processed = True
[email protected]df2b3152010-07-21 17:35:24458 if self.recursion_limit():
459 # Then we can parse the DEPS file.
460 self.ParseDepsFile(True)
[email protected]621939b2010-08-10 20:12:00461 # Adjust the implicit dependency requirement; e.g. if a DEPS file contains
462 # both src/foo and src/foo/bar, src/foo/bar is implicitly dependent of
[email protected]049bced2010-08-12 13:37:20463 # src/foo. Yes, it's O(n^2)... It's important to do that before
464 # enqueueing them.
[email protected]621939b2010-08-10 20:12:00465 for s in self.dependencies:
466 for s2 in self.dependencies:
467 if s is s2:
468 continue
469 if s.name.startswith(posixpath.join(s2.name, '')):
470 s.requirements.append(s2.name)
471
[email protected]df2b3152010-07-21 17:35:24472 # Parse the dependencies of this dependency.
473 for s in self.dependencies:
[email protected]049bced2010-08-12 13:37:20474 work_queue.enqueue(s)
[email protected]fb2b8eb2009-04-23 21:03:42475
[email protected]df2b3152010-07-21 17:35:24476 def RunHooksRecursively(self, options):
[email protected]049bced2010-08-12 13:37:20477 """Evaluates all hooks, running actions as needed. run()
[email protected]df2b3152010-07-21 17:35:24478 must have been called before to load the DEPS."""
[email protected]dc7445d2010-07-09 21:05:29479 # If "--force" was specified, run all hooks regardless of what files have
[email protected]df2b3152010-07-21 17:35:24480 # changed.
481 if self.deps_hooks and self.direct_reference:
482 # TODO(maruel): If the user is using git or git-svn, then we don't know
483 # what files have changed so we always run all hooks. It'd be nice to fix
484 # that.
485 if (options.force or
486 isinstance(self.parsed_url, self.FileImpl) or
487 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or
488 os.path.isdir(os.path.join(self.root_dir(), self.name, '.git'))):
489 for hook_dict in self.deps_hooks:
490 self._RunHookAction(hook_dict, [])
491 else:
492 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
493 # Convert all absolute paths to relative.
[email protected]861fd0f2010-07-23 03:05:05494 file_list = self.file_list()
495 for i in range(len(file_list)):
[email protected]df2b3152010-07-21 17:35:24496 # It depends on the command being executed (like runhooks vs sync).
[email protected]861fd0f2010-07-23 03:05:05497 if not os.path.isabs(file_list[i]):
[email protected]df2b3152010-07-21 17:35:24498 continue
[email protected]dc7445d2010-07-09 21:05:29499
[email protected]df2b3152010-07-21 17:35:24500 prefix = os.path.commonprefix([self.root_dir().lower(),
[email protected]861fd0f2010-07-23 03:05:05501 file_list[i].lower()])
502 file_list[i] = file_list[i][len(prefix):]
[email protected]df2b3152010-07-21 17:35:24503
504 # Strip any leading path separators.
[email protected]861fd0f2010-07-23 03:05:05505 while (file_list[i].startswith('\\') or
506 file_list[i].startswith('/')):
507 file_list[i] = file_list[i][1:]
[email protected]df2b3152010-07-21 17:35:24508
509 # Run hooks on the basis of whether the files from the gclient operation
510 # match each hook's pattern.
511 for hook_dict in self.deps_hooks:
512 pattern = re.compile(hook_dict['pattern'])
[email protected]861fd0f2010-07-23 03:05:05513 matching_file_list = [f for f in file_list if pattern.search(f)]
[email protected]df2b3152010-07-21 17:35:24514 if matching_file_list:
515 self._RunHookAction(hook_dict, matching_file_list)
516 if self.recursion_limit():
517 for s in self.dependencies:
518 s.RunHooksRecursively(options)
[email protected]fb2b8eb2009-04-23 21:03:42519
[email protected]eaf61062010-07-07 18:42:39520 def _RunHookAction(self, hook_dict, matching_file_list):
521 """Runs the action from a single hook."""
[email protected]dde32ee2010-08-10 17:44:05522 # A single DEPS file can specify multiple hooks so this function can be
523 # called multiple times on a single Dependency.
524 #assert self.hooks_ran == False
[email protected]f3abb802010-08-10 17:19:56525 self.hooks_ran = True
[email protected]dde32ee2010-08-10 17:44:05526 logging.debug(hook_dict)
527 logging.debug(matching_file_list)
[email protected]eaf61062010-07-07 18:42:39528 command = hook_dict['action'][:]
529 if command[0] == 'python':
530 # If the hook specified "python" as the first item, the action is a
531 # Python script. Run it by starting a new copy of the same
532 # interpreter.
533 command[0] = sys.executable
534
535 if '$matching_files' in command:
536 splice_index = command.index('$matching_files')
537 command[splice_index:splice_index + 1] = matching_file_list
538
539 # Use a discrete exit status code of 2 to indicate that a hook action
540 # failed. Users of this script may wish to treat hook action failures
541 # differently from VC failures.
542 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
543
[email protected]271375b2010-06-23 19:17:38544 def root_dir(self):
545 return self.parent.root_dir()
546
547 def enforced_os(self):
548 return self.parent.enforced_os()
549
[email protected]d36fba82010-06-28 16:50:40550 def recursion_limit(self):
551 return self.parent.recursion_limit() - 1
552
[email protected]0d812442010-08-10 12:41:08553 def tree(self, include_all):
554 return self.parent.tree(include_all)
[email protected]d36fba82010-06-28 16:50:40555
[email protected]0d812442010-08-10 12:41:08556 def subtree(self, include_all):
[email protected]c57e4f22010-07-22 21:37:46557 result = []
558 # Add breadth-first.
[email protected]0d812442010-08-10 12:41:08559 if self.direct_reference or include_all:
[email protected]c57e4f22010-07-22 21:37:46560 for d in self.dependencies:
[email protected]044f4e32010-07-22 21:59:57561 result.append(d)
[email protected]c57e4f22010-07-22 21:37:46562 for d in self.dependencies:
[email protected]0d812442010-08-10 12:41:08563 result.extend(d.subtree(include_all))
[email protected]c57e4f22010-07-22 21:37:46564 return result
565
[email protected]d36fba82010-06-28 16:50:40566 def get_custom_deps(self, name, url):
567 """Returns a custom deps if applicable."""
568 if self.parent:
569 url = self.parent.get_custom_deps(name, url)
570 # None is a valid return value to disable a dependency.
571 return self.custom_deps.get(name, url)
572
[email protected]861fd0f2010-07-23 03:05:05573 def file_list(self):
574 result = self._file_list[:]
575 for d in self.dependencies:
576 result.extend(d.file_list())
577 return result
578
[email protected]d36fba82010-06-28 16:50:40579 def __str__(self):
580 out = []
[email protected]dde32ee2010-08-10 17:44:05581 for i in ('name', 'url', 'parsed_url', 'safesync_url', 'custom_deps',
582 'custom_vars', 'deps_hooks', '_file_list', 'processed',
[email protected]049bced2010-08-12 13:37:20583 'hooks_ran', 'deps_parsed', 'requirements', 'direct_reference'):
[email protected]d36fba82010-06-28 16:50:40584 # 'deps_file'
585 if self.__dict__[i]:
586 out.append('%s: %s' % (i, self.__dict__[i]))
587
588 for d in self.dependencies:
589 out.extend([' ' + x for x in str(d).splitlines()])
590 out.append('')
591 return '\n'.join(out)
592
593 def __repr__(self):
594 return '%s: %s' % (self.name, self.url)
595
[email protected]bffb9042010-07-22 20:59:36596 def hierarchy(self):
[email protected]bc2d2f92010-07-22 21:26:48597 """Returns a human-readable hierarchical reference to a Dependency."""
[email protected]bffb9042010-07-22 20:59:36598 out = '%s(%s)' % (self.name, self.url)
599 i = self.parent
600 while i and i.name:
601 out = '%s(%s) -> %s' % (i.name, i.url, out)
602 i = i.parent
603 return out
604
[email protected]dde32ee2010-08-10 17:44:05605 def root_parent(self):
606 """Returns the root object, normally a GClient object."""
607 d = self
608 while d.parent:
609 d = d.parent
610 return d
611
[email protected]9a66ddf2010-06-16 16:54:16612
613class GClient(Dependency):
[email protected]d36fba82010-06-28 16:50:40614 """Object that represent a gclient checkout. A tree of Dependency(), one per
615 solution or DEPS entry."""
[email protected]9a66ddf2010-06-16 16:54:16616
617 DEPS_OS_CHOICES = {
618 "win32": "win",
619 "win": "win",
620 "cygwin": "win",
621 "darwin": "mac",
622 "mac": "mac",
623 "unix": "unix",
624 "linux": "unix",
625 "linux2": "unix",
626 }
627
628 DEFAULT_CLIENT_FILE_TEXT = ("""\
629solutions = [
630 { "name" : "%(solution_name)s",
631 "url" : "%(solution_url)s",
632 "custom_deps" : {
633 },
[email protected]73e21142010-07-05 13:32:01634 "safesync_url": "%(safesync_url)s",
[email protected]9a66ddf2010-06-16 16:54:16635 },
636]
637""")
638
639 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
640 { "name" : "%(solution_name)s",
641 "url" : "%(solution_url)s",
642 "custom_deps" : {
[email protected]73e21142010-07-05 13:32:01643%(solution_deps)s },
644 "safesync_url": "%(safesync_url)s",
[email protected]9a66ddf2010-06-16 16:54:16645 },
646""")
647
648 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
649# Snapshot generated with gclient revinfo --snapshot
650solutions = [
[email protected]73e21142010-07-05 13:32:01651%(solution_list)s]
[email protected]9a66ddf2010-06-16 16:54:16652""")
653
654 def __init__(self, root_dir, options):
[email protected]0d812442010-08-10 12:41:08655 # Do not change previous behavior. Only solution level and immediate DEPS
656 # are processed.
657 self._recursion_limit = 2
658 Dependency.__init__(self, None, None, None, None, None, None, None)
[email protected]0d425922010-06-21 19:22:24659 self._options = options
[email protected]271375b2010-06-23 19:17:38660 if options.deps_os:
661 enforced_os = options.deps_os.split(',')
662 else:
663 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
664 if 'all' in enforced_os:
665 enforced_os = self.DEPS_OS_CHOICES.itervalues()
666 self._enforced_os = list(set(enforced_os))
667 self._root_dir = root_dir
[email protected]9a66ddf2010-06-16 16:54:16668 self.config_content = None
669
670 def SetConfig(self, content):
671 assert self.dependencies == []
672 config_dict = {}
673 self.config_content = content
674 try:
675 exec(content, config_dict)
676 except SyntaxError, e:
[email protected]5990f9d2010-07-07 18:02:58677 gclient_utils.SyntaxErrorToError('.gclient', e)
[email protected]9a66ddf2010-06-16 16:54:16678 for s in config_dict.get('solutions', []):
[email protected]81843b82010-06-28 16:49:26679 try:
680 self.dependencies.append(Dependency(
681 self, s['name'], s['url'],
682 s.get('safesync_url', None),
683 s.get('custom_deps', {}),
[email protected]0d812442010-08-10 12:41:08684 s.get('custom_vars', {}),
685 None))
[email protected]81843b82010-06-28 16:49:26686 except KeyError:
687 raise gclient_utils.Error('Invalid .gclient file. Solution is '
688 'incomplete: %s' % s)
[email protected]9a66ddf2010-06-16 16:54:16689 # .gclient can have hooks.
690 self.deps_hooks = config_dict.get('hooks', [])
[email protected]049bced2010-08-12 13:37:20691 self.direct_reference = True
692 self.deps_parsed = True
[email protected]9a66ddf2010-06-16 16:54:16693
694 def SaveConfig(self):
695 gclient_utils.FileWrite(os.path.join(self.root_dir(),
696 self._options.config_filename),
697 self.config_content)
698
699 @staticmethod
700 def LoadCurrentConfig(options):
701 """Searches for and loads a .gclient file relative to the current working
702 dir. Returns a GClient object."""
703 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
704 if not path:
705 return None
706 client = GClient(path, options)
707 client.SetConfig(gclient_utils.FileRead(
708 os.path.join(path, options.config_filename)))
709 return client
710
711 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
712 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
713 'solution_name': solution_name,
714 'solution_url': solution_url,
715 'safesync_url' : safesync_url,
716 })
717
[email protected]df2b3152010-07-21 17:35:24718 def _SaveEntries(self):
[email protected]9a66ddf2010-06-16 16:54:16719 """Creates a .gclient_entries file to record the list of unique checkouts.
720
721 The .gclient_entries file lives in the same directory as .gclient.
[email protected]9a66ddf2010-06-16 16:54:16722 """
723 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
724 # makes testing a bit too fun.
[email protected]df2b3152010-07-21 17:35:24725 result = 'entries = {\n'
726 for entry in self.tree(False):
727 # Skip over File() dependencies as we can't version them.
728 if not isinstance(entry.parsed_url, self.FileImpl):
729 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
730 pprint.pformat(entry.parsed_url))
731 result += '}\n'
[email protected]9a66ddf2010-06-16 16:54:16732 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
[email protected]df2b3152010-07-21 17:35:24733 logging.info(result)
734 gclient_utils.FileWrite(file_path, result)
[email protected]9a66ddf2010-06-16 16:54:16735
736 def _ReadEntries(self):
737 """Read the .gclient_entries file for the given client.
738
739 Returns:
740 A sequence of solution names, which will be empty if there is the
741 entries file hasn't been created yet.
742 """
743 scope = {}
744 filename = os.path.join(self.root_dir(), self._options.entries_filename)
745 if not os.path.exists(filename):
[email protected]73e21142010-07-05 13:32:01746 return {}
[email protected]5990f9d2010-07-07 18:02:58747 try:
748 exec(gclient_utils.FileRead(filename), scope)
749 except SyntaxError, e:
750 gclient_utils.SyntaxErrorToError(filename, e)
[email protected]9a66ddf2010-06-16 16:54:16751 return scope['entries']
752
[email protected]54a07a22010-06-14 19:07:39753 def _EnforceRevisions(self):
[email protected]918a9ae2010-05-28 15:50:30754 """Checks for revision overrides."""
755 revision_overrides = {}
[email protected]307d1792010-05-31 20:03:13756 if self._options.head:
757 return revision_overrides
[email protected]54a07a22010-06-14 19:07:39758 for s in self.dependencies:
759 if not s.safesync_url:
[email protected]307d1792010-05-31 20:03:13760 continue
[email protected]54a07a22010-06-14 19:07:39761 handle = urllib.urlopen(s.safesync_url)
[email protected]307d1792010-05-31 20:03:13762 rev = handle.read().strip()
763 handle.close()
764 if len(rev):
[email protected]54a07a22010-06-14 19:07:39765 self._options.revisions.append('%s@%s' % (s.name, rev))
[email protected]307d1792010-05-31 20:03:13766 if not self._options.revisions:
767 return revision_overrides
768 # --revision will take over safesync_url.
[email protected]54a07a22010-06-14 19:07:39769 solutions_names = [s.name for s in self.dependencies]
[email protected]307d1792010-05-31 20:03:13770 index = 0
771 for revision in self._options.revisions:
772 if not '@' in revision:
773 # Support for --revision 123
774 revision = '%s@%s' % (solutions_names[index], revision)
[email protected]54a07a22010-06-14 19:07:39775 sol, rev = revision.split('@', 1)
[email protected]307d1792010-05-31 20:03:13776 if not sol in solutions_names:
777 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
778 print >> sys.stderr, ('Please fix your script, having invalid '
779 '--revision flags will soon considered an error.')
780 else:
[email protected]918a9ae2010-05-28 15:50:30781 revision_overrides[sol] = rev
[email protected]307d1792010-05-31 20:03:13782 index += 1
[email protected]918a9ae2010-05-28 15:50:30783 return revision_overrides
784
[email protected]fb2b8eb2009-04-23 21:03:42785 def RunOnDeps(self, command, args):
786 """Runs a command on each dependency in a client and its dependencies.
787
[email protected]fb2b8eb2009-04-23 21:03:42788 Args:
789 command: The command to use (e.g., 'status' or 'diff')
790 args: list of str - extra arguments to add to the command line.
[email protected]fb2b8eb2009-04-23 21:03:42791 """
[email protected]54a07a22010-06-14 19:07:39792 if not self.dependencies:
[email protected]73e21142010-07-05 13:32:01793 raise gclient_utils.Error('No solution specified')
[email protected]54a07a22010-06-14 19:07:39794 revision_overrides = self._EnforceRevisions()
[email protected]df2b3152010-07-21 17:35:24795 pm = None
[email protected]d2e92562010-04-27 01:55:18796 if command == 'update' and not self._options.verbose:
[email protected]049bced2010-08-12 13:37:20797 pm = Progress('Syncing projects', 1)
798 work_queue = ExecutionQueue(pm)
799 for s in self.dependencies:
800 work_queue.enqueue(s)
801 work_queue.flush(self._options, revision_overrides, command, args,
802 work_queue)
[email protected]6f363722010-04-27 00:41:09803
[email protected]df2b3152010-07-21 17:35:24804 # Once all the dependencies have been processed, it's now safe to run the
805 # hooks.
806 if not self._options.nohooks:
807 self.RunHooksRecursively(self._options)
[email protected]fb2b8eb2009-04-23 21:03:42808
809 if command == 'update':
[email protected]cdcee802009-06-23 15:30:42810 # Notify the user if there is an orphaned entry in their working copy.
811 # Only delete the directory if there are no changes in it, and
812 # delete_unversioned_trees is set to true.
[email protected]df2b3152010-07-21 17:35:24813 entries = [i.name for i in self.tree(False)]
814 for entry, prev_url in self._ReadEntries().iteritems():
[email protected]c5e9aec2009-08-03 18:25:56815 # Fix path separator on Windows.
816 entry_fixed = entry.replace('/', os.path.sep)
[email protected]75a59272010-06-11 22:34:03817 e_dir = os.path.join(self.root_dir(), entry_fixed)
[email protected]c5e9aec2009-08-03 18:25:56818 # Use entry and not entry_fixed there.
[email protected]0329e672009-05-13 18:41:04819 if entry not in entries and os.path.exists(e_dir):
[email protected]df2b3152010-07-21 17:35:24820 file_list = []
821 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
822 scm.status(self._options, [], file_list)
823 modified_files = file_list != []
[email protected]83017012009-09-28 18:52:12824 if not self._options.delete_unversioned_trees or modified_files:
[email protected]c5e9aec2009-08-03 18:25:56825 # There are modified files in this entry. Keep warning until
826 # removed.
[email protected]d36fba82010-06-28 16:50:40827 print(('\nWARNING: \'%s\' is no longer part of this client. '
828 'It is recommended that you manually remove it.\n') %
[email protected]c5e9aec2009-08-03 18:25:56829 entry_fixed)
[email protected]fb2b8eb2009-04-23 21:03:42830 else:
831 # Delete the entry
[email protected]73e21142010-07-05 13:32:01832 print('\n________ deleting \'%s\' in \'%s\'' % (
833 entry_fixed, self.root_dir()))
[email protected]5f3eee32009-09-17 00:34:30834 gclient_utils.RemoveDirectory(e_dir)
[email protected]fb2b8eb2009-04-23 21:03:42835 # record the current list of entries for next time
[email protected]df2b3152010-07-21 17:35:24836 self._SaveEntries()
[email protected]17cdf762010-05-28 17:30:52837 return 0
[email protected]fb2b8eb2009-04-23 21:03:42838
839 def PrintRevInfo(self):
[email protected]54a07a22010-06-14 19:07:39840 if not self.dependencies:
[email protected]73e21142010-07-05 13:32:01841 raise gclient_utils.Error('No solution specified')
[email protected]df2b3152010-07-21 17:35:24842 # Load all the settings.
[email protected]049bced2010-08-12 13:37:20843 work_queue = ExecutionQueue(None)
844 for s in self.dependencies:
845 work_queue.enqueue(s)
846 work_queue.flush(self._options, {}, None, [], work_queue)
[email protected]fb2b8eb2009-04-23 21:03:42847
[email protected]6da25d02010-08-11 17:32:55848 def GetURLAndRev(dep):
849 """Returns the revision-qualified SCM url for a Dependency."""
850 if dep.parsed_url is None:
[email protected]baa578e2010-07-12 17:36:59851 return None
[email protected]6da25d02010-08-11 17:32:55852 if isinstance(dep.parsed_url, self.FileImpl):
853 original_url = dep.parsed_url.file_location
854 else:
855 original_url = dep.parsed_url
[email protected]5d63eb82010-03-24 23:22:09856 url, _ = gclient_utils.SplitUrlRevision(original_url)
[email protected]6da25d02010-08-11 17:32:55857 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), dep.name)
[email protected]df2b3152010-07-21 17:35:24858 if not os.path.isdir(scm.checkout_path):
859 return None
[email protected]baa578e2010-07-12 17:36:59860 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
[email protected]fb2b8eb2009-04-23 21:03:42861
[email protected]baa578e2010-07-12 17:36:59862 if self._options.snapshot:
[email protected]df2b3152010-07-21 17:35:24863 new_gclient = ''
864 # First level at .gclient
865 for d in self.dependencies:
866 entries = {}
[email protected]6da25d02010-08-11 17:32:55867 def GrabDeps(dep):
[email protected]df2b3152010-07-21 17:35:24868 """Recursively grab dependencies."""
[email protected]6da25d02010-08-11 17:32:55869 for d in dep.dependencies:
870 entries[d.name] = GetURLAndRev(d)
871 GrabDeps(d)
[email protected]df2b3152010-07-21 17:35:24872 GrabDeps(d)
873 custom_deps = []
874 for k in sorted(entries.keys()):
875 if entries[k]:
876 # Quotes aren't escaped...
877 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
878 else:
879 custom_deps.append(' \"%s\": None,\n' % k)
880 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
881 'solution_name': d.name,
882 'solution_url': d.url,
883 'safesync_url' : d.safesync_url or '',
884 'solution_deps': ''.join(custom_deps),
885 }
886 # Print the snapshot configuration file
887 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
[email protected]de8f3522010-03-11 23:47:44888 else:
[email protected]b1e315f2010-08-11 18:44:50889 entries = {}
890 for d in self.tree(False):
891 if self._options.actual:
892 entries[d.name] = GetURLAndRev(d)
893 else:
894 entries[d.name] = d.parsed_url
895 keys = sorted(entries.keys())
896 for x in keys:
897 line = '%s: %s' % (x, entries[x])
898 if x is not keys[-1]:
[email protected]df2b3152010-07-21 17:35:24899 line += ';'
900 print line
[email protected]dde32ee2010-08-10 17:44:05901 logging.info(str(self))
[email protected]fb2b8eb2009-04-23 21:03:42902
[email protected]d36fba82010-06-28 16:50:40903 def ParseDepsFile(self, direct_reference):
904 """No DEPS to parse for a .gclient file."""
[email protected]049bced2010-08-12 13:37:20905 raise gclient_utils.Error('Internal error')
[email protected]d36fba82010-06-28 16:50:40906
[email protected]75a59272010-06-11 22:34:03907 def root_dir(self):
[email protected]d36fba82010-06-28 16:50:40908 """Root directory of gclient checkout."""
[email protected]75a59272010-06-11 22:34:03909 return self._root_dir
910
[email protected]271375b2010-06-23 19:17:38911 def enforced_os(self):
[email protected]d36fba82010-06-28 16:50:40912 """What deps_os entries that are to be parsed."""
[email protected]271375b2010-06-23 19:17:38913 return self._enforced_os
914
[email protected]d36fba82010-06-28 16:50:40915 def recursion_limit(self):
916 """How recursive can each dependencies in DEPS file can load DEPS file."""
917 return self._recursion_limit
918
[email protected]0d812442010-08-10 12:41:08919 def tree(self, include_all):
[email protected]d36fba82010-06-28 16:50:40920 """Returns a flat list of all the dependencies."""
[email protected]0d812442010-08-10 12:41:08921 return self.subtree(include_all)
[email protected]d36fba82010-06-28 16:50:40922
[email protected]fb2b8eb2009-04-23 21:03:42923
[email protected]5ca27692010-05-26 19:32:41924#### gclient commands.
[email protected]fb2b8eb2009-04-23 21:03:42925
926
[email protected]5ca27692010-05-26 19:32:41927def CMDcleanup(parser, args):
928 """Cleans up all working copies.
[email protected]ddff62d2010-05-17 21:02:36929
[email protected]5ca27692010-05-26 19:32:41930Mostly svn-specific. Simply runs 'svn cleanup' for each module.
[email protected]79692d62010-05-14 18:57:13931"""
[email protected]0b6a0842010-06-15 14:34:19932 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
933 help='override deps for the specified (comma-separated) '
934 'platform(s); \'all\' will process all deps_os '
935 'references')
[email protected]5ca27692010-05-26 19:32:41936 (options, args) = parser.parse_args(args)
[email protected]2806acc2009-05-15 12:33:34937 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:42938 if not client:
[email protected]0b6a0842010-06-15 14:34:19939 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:42940 if options.verbose:
941 # Print out the .gclient file. This is longer than if we just printed the
942 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:38943 print(client.config_content)
[email protected]fb2b8eb2009-04-23 21:03:42944 return client.RunOnDeps('cleanup', args)
945
946
[email protected]4b90e3a2010-07-01 20:28:26947@attr('usage', '[command] [args ...]')
948def CMDrecurse(parser, args):
949 """Operates on all the entries.
950
951 Runs a shell command on all entries.
952 """
953 # Stop parsing at the first non-arg so that these go through to the command
954 parser.disable_interspersed_args()
955 parser.add_option('-s', '--scm', action='append', default=[],
956 help='choose scm types to operate upon')
957 options, args = parser.parse_args(args)
958 root, entries = gclient_utils.GetGClientRootAndEntries()
959 scm_set = set()
960 for scm in options.scm:
961 scm_set.update(scm.split(','))
962
963 # Pass in the SCM type as an env variable
964 env = os.environ.copy()
965
966 for path, url in entries.iteritems():
967 scm = gclient_scm.GetScmName(url)
968 if scm_set and scm not in scm_set:
969 continue
970 dir = os.path.normpath(os.path.join(root, path))
971 env['GCLIENT_SCM'] = scm
972 env['GCLIENT_URL'] = url
973 subprocess.Popen(args, cwd=dir, env=env).communicate()
974
975
[email protected]5ca27692010-05-26 19:32:41976@attr('usage', '[url] [safesync url]')
977def CMDconfig(parser, args):
[email protected]ddff62d2010-05-17 21:02:36978 """Create a .gclient file in the current directory.
979
[email protected]5ca27692010-05-26 19:32:41980This specifies the configuration for further commands. After update/sync,
[email protected]79692d62010-05-14 18:57:13981top-level DEPS files in each module are read to determine dependent
[email protected]5ca27692010-05-26 19:32:41982modules to operate on as well. If optional [url] parameter is
[email protected]79692d62010-05-14 18:57:13983provided, then configuration is read from a specified Subversion server
[email protected]5ca27692010-05-26 19:32:41984URL.
[email protected]79692d62010-05-14 18:57:13985"""
[email protected]0b6a0842010-06-15 14:34:19986 parser.add_option('--spec',
987 help='create a gclient file containing the provided '
988 'string. Due to Cygwin/Python brokenness, it '
989 'probably can\'t contain any newlines.')
990 parser.add_option('--name',
991 help='overrides the default name for the solution')
[email protected]5ca27692010-05-26 19:32:41992 (options, args) = parser.parse_args(args)
[email protected]5fc2a332010-05-26 19:37:15993 if ((options.spec and args) or len(args) > 2 or
994 (not options.spec and not args)):
995 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
996
[email protected]0329e672009-05-13 18:41:04997 if os.path.exists(options.config_filename):
[email protected]0b6a0842010-06-15 14:34:19998 raise gclient_utils.Error('%s file already exists in the current directory'
[email protected]e3608df2009-11-10 20:22:57999 % options.config_filename)
[email protected]2806acc2009-05-15 12:33:341000 client = GClient('.', options)
[email protected]fb2b8eb2009-04-23 21:03:421001 if options.spec:
1002 client.SetConfig(options.spec)
1003 else:
[email protected]1ab7ffc2009-06-03 17:21:371004 base_url = args[0].rstrip('/')
[email protected]8cf7a392010-04-07 17:20:261005 if not options.name:
[email protected]0b6a0842010-06-15 14:34:191006 name = base_url.split('/')[-1]
[email protected]8cf7a392010-04-07 17:20:261007 else:
1008 # specify an alternate relpath for the given URL.
1009 name = options.name
[email protected]0b6a0842010-06-15 14:34:191010 safesync_url = ''
[email protected]fb2b8eb2009-04-23 21:03:421011 if len(args) > 1:
1012 safesync_url = args[1]
1013 client.SetDefaultConfig(name, base_url, safesync_url)
1014 client.SaveConfig()
[email protected]79692d62010-05-14 18:57:131015 return 0
[email protected]fb2b8eb2009-04-23 21:03:421016
1017
[email protected]5ca27692010-05-26 19:32:411018def CMDexport(parser, args):
[email protected]ddff62d2010-05-17 21:02:361019 """Wrapper for svn export for all managed directories."""
[email protected]0b6a0842010-06-15 14:34:191020 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1021 help='override deps for the specified (comma-separated) '
1022 'platform(s); \'all\' will process all deps_os '
1023 'references')
[email protected]5ca27692010-05-26 19:32:411024 (options, args) = parser.parse_args(args)
[email protected]644aa0c2009-07-17 20:20:411025 if len(args) != 1:
[email protected]0b6a0842010-06-15 14:34:191026 raise gclient_utils.Error('Need directory name')
[email protected]644aa0c2009-07-17 20:20:411027 client = GClient.LoadCurrentConfig(options)
1028
1029 if not client:
[email protected]0b6a0842010-06-15 14:34:191030 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]644aa0c2009-07-17 20:20:411031
1032 if options.verbose:
1033 # Print out the .gclient file. This is longer than if we just printed the
1034 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:381035 print(client.config_content)
[email protected]644aa0c2009-07-17 20:20:411036 return client.RunOnDeps('export', args)
1037
[email protected]fb2b8eb2009-04-23 21:03:421038
[email protected]5ca27692010-05-26 19:32:411039@attr('epilog', """Example:
1040 gclient pack > patch.txt
1041 generate simple patch for configured client and dependences
1042""")
1043def CMDpack(parser, args):
[email protected]79692d62010-05-14 18:57:131044 """Generate a patch which can be applied at the root of the tree.
[email protected]ddff62d2010-05-17 21:02:361045
[email protected]5ca27692010-05-26 19:32:411046Internally, runs 'svn diff'/'git diff' on each checked out module and
[email protected]79692d62010-05-14 18:57:131047dependencies, and performs minimal postprocessing of the output. The
1048resulting patch is printed to stdout and can be applied to a freshly
[email protected]5ca27692010-05-26 19:32:411049checked out tree via 'patch -p0 < patchfile'.
[email protected]79692d62010-05-14 18:57:131050"""
[email protected]0b6a0842010-06-15 14:34:191051 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1052 help='override deps for the specified (comma-separated) '
1053 'platform(s); \'all\' will process all deps_os '
1054 'references')
[email protected]5ca27692010-05-26 19:32:411055 (options, args) = parser.parse_args(args)
[email protected]ab318592009-09-04 00:54:551056 client = GClient.LoadCurrentConfig(options)
1057 if not client:
[email protected]0b6a0842010-06-15 14:34:191058 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]ab318592009-09-04 00:54:551059 if options.verbose:
1060 # Print out the .gclient file. This is longer than if we just printed the
1061 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:381062 print(client.config_content)
[email protected]ab318592009-09-04 00:54:551063 return client.RunOnDeps('pack', args)
1064
1065
[email protected]5ca27692010-05-26 19:32:411066def CMDstatus(parser, args):
1067 """Show modification status for every dependencies."""
[email protected]0b6a0842010-06-15 14:34:191068 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')
[email protected]5ca27692010-05-26 19:32:411072 (options, args) = parser.parse_args(args)
[email protected]2806acc2009-05-15 12:33:341073 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421074 if not client:
[email protected]0b6a0842010-06-15 14:34:191075 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:421076 if options.verbose:
1077 # Print out the .gclient file. This is longer than if we just printed the
1078 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:381079 print(client.config_content)
[email protected]fb2b8eb2009-04-23 21:03:421080 return client.RunOnDeps('status', args)
1081
1082
[email protected]5ca27692010-05-26 19:32:411083@attr('epilog', """Examples:
[email protected]79692d62010-05-14 18:57:131084 gclient sync
1085 update files from SCM according to current configuration,
1086 *for modules which have changed since last update or sync*
1087 gclient sync --force
1088 update files from SCM according to current configuration, for
1089 all modules (useful for recovering files deleted from local copy)
1090 gclient sync --revision src@31000
1091 update src directory to r31000
[email protected]5ca27692010-05-26 19:32:411092""")
1093def CMDsync(parser, args):
1094 """Checkout/update all modules."""
[email protected]0b6a0842010-06-15 14:34:191095 parser.add_option('-f', '--force', action='store_true',
1096 help='force update even for unchanged modules')
1097 parser.add_option('-n', '--nohooks', action='store_true',
1098 help='don\'t run hooks after the update is complete')
1099 parser.add_option('-r', '--revision', action='append',
1100 dest='revisions', metavar='REV', default=[],
1101 help='Enforces revision/hash for the solutions with the '
1102 'format src@rev. The src@ part is optional and can be '
1103 'skipped. -r can be used multiple times when .gclient '
1104 'has multiple solutions configured and will work even '
1105 'if the src@ part is skipped.')
1106 parser.add_option('-H', '--head', action='store_true',
1107 help='skips any safesync_urls specified in '
1108 'configured solutions and sync to head instead')
1109 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
1110 help='delete any unexpected unversioned trees '
1111 'that are in the checkout')
1112 parser.add_option('-R', '--reset', action='store_true',
1113 help='resets any local changes before updating (git only)')
1114 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1115 help='override deps for the specified (comma-separated) '
1116 'platform(s); \'all\' will process all deps_os '
1117 'references')
1118 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
1119 help='Skip svn up whenever possible by requesting '
1120 'actual HEAD revision from the repository')
[email protected]5ca27692010-05-26 19:32:411121 (options, args) = parser.parse_args(args)
[email protected]2806acc2009-05-15 12:33:341122 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421123
1124 if not client:
[email protected]0b6a0842010-06-15 14:34:191125 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:421126
[email protected]307d1792010-05-31 20:03:131127 if options.revisions and options.head:
1128 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
[email protected]0b6a0842010-06-15 14:34:191129 print('Warning: you cannot use both --head and --revision')
[email protected]fb2b8eb2009-04-23 21:03:421130
1131 if options.verbose:
1132 # Print out the .gclient file. This is longer than if we just printed the
1133 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:381134 print(client.config_content)
[email protected]fb2b8eb2009-04-23 21:03:421135 return client.RunOnDeps('update', args)
1136
1137
[email protected]5ca27692010-05-26 19:32:411138def CMDupdate(parser, args):
[email protected]ddff62d2010-05-17 21:02:361139 """Alias for the sync command. Deprecated."""
[email protected]5ca27692010-05-26 19:32:411140 return CMDsync(parser, args)
[email protected]fb2b8eb2009-04-23 21:03:421141
[email protected]5ca27692010-05-26 19:32:411142def CMDdiff(parser, args):
1143 """Displays local diff for every dependencies."""
[email protected]0b6a0842010-06-15 14:34:191144 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1145 help='override deps for the specified (comma-separated) '
1146 'platform(s); \'all\' will process all deps_os '
1147 'references')
[email protected]5ca27692010-05-26 19:32:411148 (options, args) = parser.parse_args(args)
[email protected]2806acc2009-05-15 12:33:341149 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421150 if not client:
[email protected]0b6a0842010-06-15 14:34:191151 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:421152 if options.verbose:
1153 # Print out the .gclient file. This is longer than if we just printed the
1154 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:381155 print(client.config_content)
[email protected]fb2b8eb2009-04-23 21:03:421156 return client.RunOnDeps('diff', args)
1157
1158
[email protected]5ca27692010-05-26 19:32:411159def CMDrevert(parser, args):
1160 """Revert all modifications in every dependencies."""
[email protected]0b6a0842010-06-15 14:34:191161 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1162 help='override deps for the specified (comma-separated) '
1163 'platform(s); \'all\' will process all deps_os '
1164 'references')
1165 parser.add_option('-n', '--nohooks', action='store_true',
1166 help='don\'t run hooks after the revert is complete')
[email protected]5ca27692010-05-26 19:32:411167 (options, args) = parser.parse_args(args)
1168 # --force is implied.
1169 options.force = True
[email protected]2806acc2009-05-15 12:33:341170 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421171 if not client:
[email protected]0b6a0842010-06-15 14:34:191172 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:421173 return client.RunOnDeps('revert', args)
1174
1175
[email protected]5ca27692010-05-26 19:32:411176def CMDrunhooks(parser, args):
1177 """Runs hooks for files that have been modified in the local working copy."""
[email protected]0b6a0842010-06-15 14:34:191178 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1179 help='override deps for the specified (comma-separated) '
1180 'platform(s); \'all\' will process all deps_os '
1181 'references')
1182 parser.add_option('-f', '--force', action='store_true', default=True,
1183 help='Deprecated. No effect.')
[email protected]5ca27692010-05-26 19:32:411184 (options, args) = parser.parse_args(args)
[email protected]2806acc2009-05-15 12:33:341185 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421186 if not client:
[email protected]0b6a0842010-06-15 14:34:191187 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:421188 if options.verbose:
1189 # Print out the .gclient file. This is longer than if we just printed the
1190 # client dict, but more legible, and it might contain helpful comments.
[email protected]116704f2010-06-11 17:34:381191 print(client.config_content)
[email protected]5df6a462009-08-28 18:52:261192 options.force = True
[email protected]5ca27692010-05-26 19:32:411193 options.nohooks = False
[email protected]fb2b8eb2009-04-23 21:03:421194 return client.RunOnDeps('runhooks', args)
1195
1196
[email protected]5ca27692010-05-26 19:32:411197def CMDrevinfo(parser, args):
[email protected]9eda4112010-06-11 18:56:101198 """Output revision info mapping for the client and its dependencies.
1199
[email protected]0b6a0842010-06-15 14:34:191200 This allows the capture of an overall 'revision' for the source tree that
[email protected]9eda4112010-06-11 18:56:101201 can be used to reproduce the same tree in the future. It is only useful for
[email protected]0b6a0842010-06-15 14:34:191202 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1203 number or a git hash. A git branch name isn't 'pinned' since the actual
[email protected]9eda4112010-06-11 18:56:101204 commit can change.
1205 """
1206 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1207 help='override deps for the specified (comma-separated) '
1208 'platform(s); \'all\' will process all deps_os '
1209 'references')
[email protected]b1e315f2010-08-11 18:44:501210 parser.add_option('-a', '--actual', action='store_true',
1211 help='gets the actual checked out revisions instead of the '
1212 'ones specified in the DEPS and .gclient files')
[email protected]9eda4112010-06-11 18:56:101213 parser.add_option('-s', '--snapshot', action='store_true',
1214 help='creates a snapshot .gclient file of the current '
[email protected]b1e315f2010-08-11 18:44:501215 'version of all repositories to reproduce the tree, '
1216 'implies -a')
[email protected]5ca27692010-05-26 19:32:411217 (options, args) = parser.parse_args(args)
[email protected]2806acc2009-05-15 12:33:341218 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421219 if not client:
[email protected]0b6a0842010-06-15 14:34:191220 raise gclient_utils.Error('client not configured; see \'gclient config\'')
[email protected]fb2b8eb2009-04-23 21:03:421221 client.PrintRevInfo()
[email protected]79692d62010-05-14 18:57:131222 return 0
[email protected]fb2b8eb2009-04-23 21:03:421223
1224
[email protected]5ca27692010-05-26 19:32:411225def Command(name):
1226 return getattr(sys.modules[__name__], 'CMD' + name, None)
1227
1228
1229def CMDhelp(parser, args):
1230 """Prints list of commands or help for a specific command."""
[email protected]6e29d572010-06-04 17:32:201231 (_, args) = parser.parse_args(args)
[email protected]ddff62d2010-05-17 21:02:361232 if len(args) == 1:
[email protected]5ca27692010-05-26 19:32:411233 return Main(args + ['--help'])
[email protected]ddff62d2010-05-17 21:02:361234 parser.print_help()
1235 return 0
1236
1237
[email protected]5ca27692010-05-26 19:32:411238def GenUsage(parser, command):
1239 """Modify an OptParse object with the function's documentation."""
1240 obj = Command(command)
1241 if command == 'help':
1242 command = '<command>'
1243 # OptParser.description prefer nicely non-formatted strings.
1244 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1245 usage = getattr(obj, 'usage', '')
1246 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1247 parser.epilog = getattr(obj, 'epilog', None)
[email protected]fb2b8eb2009-04-23 21:03:421248
1249
1250def Main(argv):
[email protected]5ca27692010-05-26 19:32:411251 """Doesn't parse the arguments here, just find the right subcommand to
1252 execute."""
[email protected]6e29d572010-06-04 17:32:201253 try:
1254 # Do it late so all commands are listed.
1255 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1256 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1257 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1258 parser = optparse.OptionParser(version='%prog ' + __version__)
[email protected]f0fc9912010-06-11 17:57:331259 parser.add_option('-v', '--verbose', action='count', default=0,
1260 help='Produces additional output for diagnostics. Can be '
1261 'used up to three times for more logging info.')
1262 parser.add_option('--gclientfile', dest='config_filename',
1263 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1264 help='Specify an alternate %default file')
[email protected]6e29d572010-06-04 17:32:201265 # Integrate standard options processing.
1266 old_parser = parser.parse_args
1267 def Parse(args):
1268 (options, args) = old_parser(args)
[email protected]f0fc9912010-06-11 17:57:331269 level = None
[email protected]6e29d572010-06-04 17:32:201270 if options.verbose == 2:
[email protected]f0fc9912010-06-11 17:57:331271 level = logging.INFO
[email protected]6e29d572010-06-04 17:32:201272 elif options.verbose > 2:
[email protected]f0fc9912010-06-11 17:57:331273 level = logging.DEBUG
1274 logging.basicConfig(level=level,
1275 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1276 options.entries_filename = options.config_filename + '_entries'
[email protected]e3216c62010-07-08 03:31:431277
1278 # These hacks need to die.
[email protected]6e29d572010-06-04 17:32:201279 if not hasattr(options, 'revisions'):
1280 # GClient.RunOnDeps expects it even if not applicable.
1281 options.revisions = []
1282 if not hasattr(options, 'head'):
1283 options.head = None
[email protected]f0fc9912010-06-11 17:57:331284 if not hasattr(options, 'nohooks'):
1285 options.nohooks = True
1286 if not hasattr(options, 'deps_os'):
1287 options.deps_os = None
[email protected]e3216c62010-07-08 03:31:431288 if not hasattr(options, 'manually_grab_svn_rev'):
1289 options.manually_grab_svn_rev = None
1290 if not hasattr(options, 'force'):
1291 options.force = None
[email protected]6e29d572010-06-04 17:32:201292 return (options, args)
1293 parser.parse_args = Parse
1294 # We don't want wordwrapping in epilog (usually examples)
1295 parser.format_epilog = lambda _: parser.epilog or ''
1296 if argv:
1297 command = Command(argv[0])
1298 if command:
[email protected]f0fc9912010-06-11 17:57:331299 # 'fix' the usage and the description now that we know the subcommand.
[email protected]6e29d572010-06-04 17:32:201300 GenUsage(parser, argv[0])
1301 return command(parser, argv[1:])
1302 # Not a known command. Default to help.
1303 GenUsage(parser, 'help')
1304 return CMDhelp(parser, argv)
1305 except gclient_utils.Error, e:
[email protected]f0fc9912010-06-11 17:57:331306 print >> sys.stderr, 'Error: %s' % str(e)
[email protected]6e29d572010-06-04 17:32:201307 return 1
[email protected]fb2b8eb2009-04-23 21:03:421308
1309
[email protected]f0fc9912010-06-11 17:57:331310if '__main__' == __name__:
[email protected]6e29d572010-06-04 17:32:201311 sys.exit(Main(sys.argv[1:]))
[email protected]fb2b8eb2009-04-23 21:03:421312
1313# vim: ts=2:sw=2:tw=80:et: