blob: 519019e16fe86bfc78d4953f0b13894dd5d513db [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/python
maruel@chromium.orgba551772010-02-03 18:21:42 +00002# Copyright (c) 2010 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00005
6"""A wrapper script to manage a set of client modules in different SCM.
7
8This script is intended to be used to help basic management of client
msb@chromium.orgd6504212010-01-13 17:34:31 +00009program sources residing in one or more Subversion modules and Git
10repositories, along with other modules it depends on, also in Subversion or Git,
11but possibly on multiple respositories, making a wrapper system apparently
12necessary.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000013
14Files
15 .gclient : Current client configuration, written by 'config' command.
16 Format is a Python script defining 'solutions', a list whose
17 entries each are maps binding the strings "name" and "url"
18 to strings specifying the name and location of the client
19 module, as well as "custom_deps" to a map similar to the DEPS
20 file below.
21 .gclient_entries : A cache constructed by 'update' command. Format is a
22 Python script defining 'entries', a list of the names
23 of all modules in the client
24 <module>/DEPS : Python script defining var 'deps' as a map from each requisite
25 submodule name to a URL where it can be found (via one SCM)
26
27Hooks
28 .gclient and DEPS files may optionally contain a list named "hooks" to
29 allow custom actions to be performed based on files that have changed in the
evan@chromium.org67820ef2009-07-27 17:23:00 +000030 working copy as a result of a "sync"/"update" or "revert" operation. This
31 could be prevented by using --nohooks (hooks run by default). Hooks can also
maruel@chromium.org5df6a462009-08-28 18:52:26 +000032 be forced to run with the "runhooks" operation. If "sync" is run with
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000033 --force, all known hooks will run regardless of the state of the working
34 copy.
35
36 Each item in a "hooks" list is a dict, containing these two keys:
37 "pattern" The associated value is a string containing a regular
38 expression. When a file whose pathname matches the expression
39 is checked out, updated, or reverted, the hook's "action" will
40 run.
41 "action" A list describing a command to run along with its arguments, if
42 any. An action command will run at most one time per gclient
43 invocation, regardless of how many files matched the pattern.
44 The action is executed in the same directory as the .gclient
45 file. If the first item in the list is the string "python",
46 the current Python interpreter (sys.executable) will be used
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +000047 to run the command. If the list contains string "$matching_files"
48 it will be removed from the list and the list will be extended
49 by the list of matching files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050
51 Example:
52 hooks = [
53 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
54 "action": ["python", "image_indexer.py", "--all"]},
55 ]
56"""
57
58__author__ = "darinf@gmail.com (Darin Fisher)"
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000059__version__ = "0.3.4"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060
piman@chromium.org8b5086d2010-04-17 02:05:32 +000061import copy
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000062import errno
maruel@chromium.org754960e2009-09-21 12:31:05 +000063import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000064import optparse
65import os
msb@chromium.org2e38de72009-09-28 17:04:47 +000066import pprint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000067import re
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000068import sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000069import urlparse
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000070import urllib
71
maruel@chromium.orgada4c652009-12-03 15:32:01 +000072import breakpad
73
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000074import gclient_scm
75import gclient_utils
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +000076from third_party.repo.progress import Progress
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000077
78# default help text
79DEFAULT_USAGE_TEXT = (
msb@chromium.orgd6504212010-01-13 17:34:31 +000080"""usage: %prog <subcommand> [options] [--] [SCM options/args...]
81a wrapper for managing a set of svn client modules and/or git repositories.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000082Version """ + __version__ + """
83
84subcommands:
85 cleanup
86 config
87 diff
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +000088 export
kbr@google.comab318592009-09-04 00:54:55 +000089 pack
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000090 revert
91 status
92 sync
93 update
94 runhooks
95 revinfo
96
msb@chromium.orgd6504212010-01-13 17:34:31 +000097Options and extra arguments can be passed to invoked SCM commands by
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000098appending them to the command line. Note that if the first such
99appended option starts with a dash (-) then the options must be
100preceded by -- to distinguish them from gclient options.
101
102For additional help on a subcommand or examples of usage, try
103 %prog help <subcommand>
104 %prog help files
105""")
106
107GENERIC_UPDATE_USAGE_TEXT = (
108 """Perform a checkout/update of the modules specified by the gclient
109configuration; see 'help config'. Unless --revision is specified,
110then the latest revision of the root solutions is checked out, with
111dependent submodule versions updated according to DEPS files.
112If --revision is specified, then the given revision is used in place
113of the latest, either for a single solution or for all solutions.
114Unless the --force option is provided, solutions and modules whose
115local revision matches the one to update (i.e., they have not changed
evan@chromium.org67820ef2009-07-27 17:23:00 +0000116in the repository) are *not* modified. Unless --nohooks is provided,
117the hooks are run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000118This a synonym for 'gclient %(alias)s'
119
msb@chromium.orgd6504212010-01-13 17:34:31 +0000120usage: gclient %(cmd)s [options] [--] [SCM update options/args]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000121
122Valid options:
estade@chromium.org3b5cba42009-12-01 00:37:08 +0000123 --force : force update even for unchanged modules
124 --nohooks : don't run the hooks after the update is complete
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000125 --revision SOLUTION@REV : update given solution to specified revision
estade@chromium.org3b5cba42009-12-01 00:37:08 +0000126 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
127 --verbose : output additional diagnostics
128 --head : update to latest revision, instead of last good revision
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000129 --reset : resets any local changes before updating (git only)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000130
131Examples:
132 gclient %(cmd)s
msb@chromium.orgd6504212010-01-13 17:34:31 +0000133 update files from SCM according to current configuration,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000134 *for modules which have changed since last update or sync*
135 gclient %(cmd)s --force
msb@chromium.orgd6504212010-01-13 17:34:31 +0000136 update files from SCM according to current configuration, for
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000137 all modules (useful for recovering files deleted from local copy)
estade@chromium.org3b5cba42009-12-01 00:37:08 +0000138 gclient %(cmd)s --revision src@31000
139 update src directory to r31000
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000140""")
141
142COMMAND_USAGE_TEXT = {
143 "cleanup":
144 """Clean up all working copies, using 'svn cleanup' for each module.
145Additional options and args may be passed to 'svn cleanup'.
146
147usage: cleanup [options] [--] [svn cleanup args/options]
148
149Valid options:
150 --verbose : output additional diagnostics
151""",
152 "config": """Create a .gclient file in the current directory; this
153specifies the configuration for further commands. After update/sync,
154top-level DEPS files in each module are read to determine dependent
155modules to operate on as well. If optional [url] parameter is
156provided, then configuration is read from a specified Subversion server
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000157URL. Otherwise, a --spec option must be provided. A --name option overrides
158the default name for the solutions.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000159
160usage: config [option | url] [safesync url]
161
162Valid options:
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000163 --name path : alternate relative path for the solution
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000164 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
165 *Note that due to Cygwin/Python brokenness, it
166 probably can't contain any newlines.*
167
168Examples:
169 gclient config https://gclient.googlecode.com/svn/trunk/gclient
170 configure a new client to check out gclient.py tool sources
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000171 gclient config --name tools https://gclient.googlecode.com/svn/trunk/gclient
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000172 gclient config --spec='solutions=[{"name":"gclient","""
173 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
174 '"custom_deps":{}}]',
175 "diff": """Display the differences between two revisions of modules.
176(Does 'svn diff' for each checked out module and dependences.)
177Additional args and options to 'svn diff' can be passed after
178gclient options.
179
180usage: diff [options] [--] [svn args/options]
181
182Valid options:
183 --verbose : output additional diagnostics
184
185Examples:
186 gclient diff
187 simple 'svn diff' for configured client and dependences
188 gclient diff -- -x -b
189 use 'svn diff -x -b' to suppress whitespace-only differences
190 gclient diff -- -r HEAD -x -b
191 diff versus the latest version of each module
192""",
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000193 "export":
194 """Wrapper for svn export for all managed directories
195""",
kbr@google.comab318592009-09-04 00:54:55 +0000196 "pack":
197
198 """Generate a patch which can be applied at the root of the tree.
199Internally, runs 'svn diff' on each checked out module and
200dependencies, and performs minimal postprocessing of the output. The
201resulting patch is printed to stdout and can be applied to a freshly
202checked out tree via 'patch -p0 < patchfile'. Additional args and
203options to 'svn diff' can be passed after gclient options.
204
205usage: pack [options] [--] [svn args/options]
206
207Valid options:
208 --verbose : output additional diagnostics
209
210Examples:
211 gclient pack > patch.txt
212 generate simple patch for configured client and dependences
213 gclient pack -- -x -b > patch.txt
214 generate patch using 'svn diff -x -b' to suppress
215 whitespace-only differences
216 gclient pack -- -r HEAD -x -b > patch.txt
217 generate patch, diffing each file versus the latest version of
218 each module
219""",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000220 "revert":
221 """Revert every file in every managed directory in the client view.
222
223usage: revert
224""",
225 "status":
226 """Show the status of client and dependent modules, using 'svn diff'
227for each module. Additional options and args may be passed to 'svn diff'.
228
229usage: status [options] [--] [svn diff args/options]
230
231Valid options:
232 --verbose : output additional diagnostics
evan@chromium.org67820ef2009-07-27 17:23:00 +0000233 --nohooks : don't run the hooks after the update is complete
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000234""",
235 "sync": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "sync", "alias": "update"},
236 "update": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "update", "alias": "sync"},
237 "help": """Describe the usage of this program or its subcommands.
238
239usage: help [options] [subcommand]
240
241Valid options:
242 --verbose : output additional diagnostics
243""",
244 "runhooks":
245 """Runs hooks for files that have been modified in the local working copy,
maruel@chromium.org5df6a462009-08-28 18:52:26 +0000246according to 'svn status'. Implies --force.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000247
248usage: runhooks [options]
249
250Valid options:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000251 --verbose : output additional diagnostics
252""",
253 "revinfo":
254 """Outputs source path, server URL and revision information for every
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000255dependency in all solutions.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256
257usage: revinfo [options]
258""",
259}
260
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000261DEFAULT_CLIENT_FILE_TEXT = ("""\
262# An element of this array (a "solution") describes a repository directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000263# that will be checked out into your working copy. Each solution may
264# optionally define additional dependencies (via its DEPS file) to be
265# checked out alongside the solution's directory. A solution may also
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000266# specify custom dependencies (via the "custom_deps" property) that
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267# override or augment the dependencies specified by the DEPS file.
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000268# If a "safesync_url" is specified, it is assumed to reference the location of
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269# a text file which contains nothing but the last known good SCM revision to
270# sync against. It is fetched if specified and used unless --head is passed
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000271
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272solutions = [
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000273 { "name" : "%(solution_name)s",
274 "url" : "%(solution_url)s",
275 "custom_deps" : {
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276 # To use the trunk of a component instead of what's in DEPS:
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000277 #"component": "https://svnserver/component/trunk/",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000278 # To exclude a component from your working copy:
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000279 #"data/really_large_component": None,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000280 },
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000281 "safesync_url": "%(safesync_url)s"
282 },
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283]
284""")
285
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000286DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
287 { "name" : "%(solution_name)s",
288 "url" : "%(solution_url)s",
289 "custom_deps" : {
290 %(solution_deps)s,
291 },
292 "safesync_url": "%(safesync_url)s"
293 },
294""")
295
296DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
297# An element of this array (a "solution") describes a repository directory
298# that will be checked out into your working copy. Each solution may
299# optionally define additional dependencies (via its DEPS file) to be
300# checked out alongside the solution's directory. A solution may also
301# specify custom dependencies (via the "custom_deps" property) that
302# override or augment the dependencies specified by the DEPS file.
303# If a "safesync_url" is specified, it is assumed to reference the location of
304# a text file which contains nothing but the last known good SCM revision to
305# sync against. It is fetched if specified and used unless --head is passed
306
307solutions = [
308%(solution_list)s
309]
310""")
311
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000312
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000313## GClient implementation.
314
315
316class GClient(object):
317 """Object that represent a gclient checkout."""
318
319 supported_commands = [
kbr@google.comab318592009-09-04 00:54:55 +0000320 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
321 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322 ]
323
324 def __init__(self, root_dir, options):
325 self._root_dir = root_dir
326 self._options = options
327 self._config_content = None
328 self._config_dict = {}
329 self._deps_hooks = []
330
331 def SetConfig(self, content):
332 self._config_dict = {}
333 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000334 try:
335 exec(content, self._config_dict)
336 except SyntaxError, e:
337 try:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000338 __pychecker__ = 'no-objattrs'
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000339 # Try to construct a human readable error message
340 error_message = [
341 'There is a syntax error in your configuration file.',
342 'Line #%s, character %s:' % (e.lineno, e.offset),
343 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
344 except:
345 # Something went wrong, re-raise the original exception
346 raise e
347 else:
348 # Raise a new exception with the human readable message:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000349 raise gclient_utils.Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000350
351 def SaveConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000352 gclient_utils.FileWrite(os.path.join(self._root_dir,
353 self._options.config_filename),
354 self._config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000355
356 def _LoadConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000357 client_source = gclient_utils.FileRead(
358 os.path.join(self._root_dir, self._options.config_filename))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000359 self.SetConfig(client_source)
360
361 def ConfigContent(self):
362 return self._config_content
363
364 def GetVar(self, key, default=None):
365 return self._config_dict.get(key, default)
366
367 @staticmethod
368 def LoadCurrentConfig(options, from_dir=None):
369 """Searches for and loads a .gclient file relative to the current working
370 dir.
371
372 Returns:
373 A dict representing the contents of the .gclient file or an empty dict if
374 the .gclient file doesn't exist.
375 """
376 if not from_dir:
377 from_dir = os.curdir
378 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000379 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000380 split_path = os.path.split(path)
381 if not split_path[1]:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000382 return None
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000383 path = split_path[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000384 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000385 client._LoadConfig()
386 return client
387
388 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000389 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % {
390 'solution_name': solution_name,
391 'solution_url': solution_url,
392 'safesync_url' : safesync_url,
393 })
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000394
395 def _SaveEntries(self, entries):
396 """Creates a .gclient_entries file to record the list of unique checkouts.
397
398 The .gclient_entries file lives in the same directory as .gclient.
399
400 Args:
401 entries: A sequence of solution names.
402 """
maruel@chromium.orge41f6822010-04-08 16:37:06 +0000403 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
404 # makes testing a bit too fun.
405 result = pprint.pformat(entries, 2)
406 if result.startswith('{\''):
maruel@chromium.org1edec4d2010-04-08 17:17:06 +0000407 result = '{ \'' + result[2:]
maruel@chromium.orge41f6822010-04-08 16:37:06 +0000408 text = "entries = \\\n" + result + '\n'
msb@chromium.org2e38de72009-09-28 17:04:47 +0000409 file_path = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000410 gclient_utils.FileWrite(file_path, text)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000411
412 def _ReadEntries(self):
413 """Read the .gclient_entries file for the given client.
414
415 Args:
416 client: The client for which the entries file should be read.
417
418 Returns:
419 A sequence of solution names, which will be empty if there is the
420 entries file hasn't been created yet.
421 """
422 scope = {}
423 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000424 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000425 return []
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000426 exec(gclient_utils.FileRead(filename), scope)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000427 return scope["entries"]
428
429 class FromImpl:
430 """Used to implement the From syntax."""
431
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000432 def __init__(self, module_name, sub_target_name=None):
433 """module_name is the dep module we want to include from. It can also be
434 the name of a subdirectory to include from.
435
436 sub_target_name is an optional parameter if the module name in the other
437 DEPS file is different. E.g., you might want to map src/net to net."""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000438 self.module_name = module_name
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000439 self.sub_target_name = sub_target_name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000440
441 def __str__(self):
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000442 return 'From(%s, %s)' % (repr(self.module_name),
443 repr(self.sub_target_name))
444
445 def GetUrl(self, target_name, sub_deps_base_url, root_dir, sub_deps):
446 """Resolve the URL for this From entry."""
447 sub_deps_target_name = target_name
448 if self.sub_target_name:
449 sub_deps_target_name = self.sub_target_name
450 url = sub_deps[sub_deps_target_name]
451 if url.startswith('/'):
452 # If it's a relative URL, we need to resolve the URL relative to the
453 # sub deps base URL.
454 if not isinstance(sub_deps_base_url, basestring):
455 sub_deps_base_url = sub_deps_base_url.GetPath()
456 scm = gclient_scm.CreateSCM(sub_deps_base_url, root_dir,
457 None)
458 url = scm.FullUrlForRelativeUrl(url)
459 return url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000460
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000461 class FileImpl:
462 """Used to implement the File('') syntax which lets you sync a single file
463 from an SVN repo."""
464
465 def __init__(self, file_location):
466 self.file_location = file_location
467
468 def __str__(self):
469 return 'File("%s")' % self.file_location
470
471 def GetPath(self):
472 return os.path.split(self.file_location)[0]
473
474 def GetFilename(self):
475 rev_tokens = self.file_location.split('@')
476 return os.path.split(rev_tokens[0])[1]
477
478 def GetRevision(self):
479 rev_tokens = self.file_location.split('@')
480 if len(rev_tokens) > 1:
481 return rev_tokens[1]
482 return None
483
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000484 class _VarImpl:
485 def __init__(self, custom_vars, local_scope):
486 self._custom_vars = custom_vars
487 self._local_scope = local_scope
488
489 def Lookup(self, var_name):
490 """Implements the Var syntax."""
491 if var_name in self._custom_vars:
492 return self._custom_vars[var_name]
493 elif var_name in self._local_scope.get("vars", {}):
494 return self._local_scope["vars"][var_name]
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000495 raise gclient_utils.Error("Var is not defined: %s" % var_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000496
497 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000498 custom_vars, parse_hooks):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000499 """Parses the DEPS file for the specified solution.
500
501 Args:
502 solution_name: The name of the solution to query.
503 solution_deps_content: Content of the DEPS file for the solution
504 custom_vars: A dict of vars to override any vars defined in the DEPS file.
505
506 Returns:
507 A dict mapping module names (as relative paths) to URLs or an empty
508 dict if the solution does not have a DEPS file.
509 """
510 # Skip empty
511 if not solution_deps_content:
512 return {}
513 # Eval the content
514 local_scope = {}
515 var = self._VarImpl(custom_vars, local_scope)
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000516 global_scope = {
517 "File": self.FileImpl,
518 "From": self.FromImpl,
519 "Var": var.Lookup,
520 "deps_os": {},
521 }
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000522 exec(solution_deps_content, global_scope, local_scope)
523 deps = local_scope.get("deps", {})
524
525 # load os specific dependencies if defined. these dependencies may
526 # override or extend the values defined by the 'deps' member.
527 if "deps_os" in local_scope:
528 deps_os_choices = {
529 "win32": "win",
530 "win": "win",
531 "cygwin": "win",
532 "darwin": "mac",
533 "mac": "mac",
534 "unix": "unix",
535 "linux": "unix",
536 "linux2": "unix",
537 }
538
539 if self._options.deps_os is not None:
540 deps_to_include = self._options.deps_os.split(",")
541 if "all" in deps_to_include:
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000542 deps_to_include = list(set(deps_os_choices.itervalues()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000543 else:
544 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
545
546 deps_to_include = set(deps_to_include)
547 for deps_os_key in deps_to_include:
548 os_deps = local_scope["deps_os"].get(deps_os_key, {})
549 if len(deps_to_include) > 1:
550 # Ignore any overrides when including deps for more than one
551 # platform, so we collect the broadest set of dependencies available.
552 # We may end up with the wrong revision of something for our
553 # platform, but this is the best we can do.
554 deps.update([x for x in os_deps.items() if not x[0] in deps])
555 else:
556 deps.update(os_deps)
557
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000558 if 'hooks' in local_scope and parse_hooks:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000559 self._deps_hooks.extend(local_scope['hooks'])
560
561 # If use_relative_paths is set in the DEPS file, regenerate
562 # the dictionary using paths relative to the directory containing
563 # the DEPS file.
564 if local_scope.get('use_relative_paths'):
565 rel_deps = {}
566 for d, url in deps.items():
567 # normpath is required to allow DEPS to use .. in their
568 # dependency local path.
569 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
570 return rel_deps
571 else:
572 return deps
573
574 def _ParseAllDeps(self, solution_urls, solution_deps_content):
575 """Parse the complete list of dependencies for the client.
576
577 Args:
578 solution_urls: A dict mapping module names (as relative paths) to URLs
579 corresponding to the solutions specified by the client. This parameter
580 is passed as an optimization.
581 solution_deps_content: A dict mapping module names to the content
582 of their DEPS files
583
584 Returns:
585 A dict mapping module names (as relative paths) to URLs corresponding
586 to the entire set of dependencies to checkout for the given client.
587
588 Raises:
589 Error: If a dependency conflicts with another dependency or of a solution.
590 """
591 deps = {}
592 for solution in self.GetVar("solutions"):
593 custom_vars = solution.get("custom_vars", {})
594 solution_deps = self._ParseSolutionDeps(
595 solution["name"],
596 solution_deps_content[solution["name"]],
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000597 custom_vars,
598 True)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000599
600 # If a line is in custom_deps, but not in the solution, we want to append
601 # this line to the solution.
602 if "custom_deps" in solution:
603 for d in solution["custom_deps"]:
604 if d not in solution_deps:
605 solution_deps[d] = solution["custom_deps"][d]
606
607 for d in solution_deps:
608 if "custom_deps" in solution and d in solution["custom_deps"]:
609 # Dependency is overriden.
610 url = solution["custom_deps"][d]
611 if url is None:
612 continue
613 else:
614 url = solution_deps[d]
615 # if we have a From reference dependent on another solution, then
616 # just skip the From reference. When we pull deps for the solution,
617 # we will take care of this dependency.
618 #
619 # If multiple solutions all have the same From reference, then we
620 # should only add one to our list of dependencies.
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000621 if isinstance(url, self.FromImpl):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000622 if url.module_name in solution_urls:
623 # Already parsed.
624 continue
625 if d in deps and type(deps[d]) != str:
626 if url.module_name == deps[d].module_name:
627 continue
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000628 elif isinstance(url, str):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000629 parsed_url = urlparse.urlparse(url)
630 scheme = parsed_url[0]
631 if not scheme:
632 # A relative url. Fetch the real base.
633 path = parsed_url[2]
634 if path[0] != "/":
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000635 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000636 "relative DEPS entry \"%s\" must begin with a slash" % d)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000637 # Create a scm just to query the full url.
638 scm = gclient_scm.CreateSCM(solution["url"], self._root_dir,
639 None)
640 url = scm.FullUrlForRelativeUrl(url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000641 if d in deps and deps[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000642 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000643 "Solutions have conflicting versions of dependency \"%s\"" % d)
644 if d in solution_urls and solution_urls[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000645 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000646 "Dependency \"%s\" conflicts with specified solution" % d)
647 # Grab the dependency.
648 deps[d] = url
649 return deps
650
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000651 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000652 """Runs the action from a single hook.
653 """
654 command = hook_dict['action'][:]
655 if command[0] == 'python':
656 # If the hook specified "python" as the first item, the action is a
657 # Python script. Run it by starting a new copy of the same
658 # interpreter.
659 command[0] = sys.executable
660
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000661 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000662 splice_index = command.index('$matching_files')
663 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000664
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000665 # Use a discrete exit status code of 2 to indicate that a hook action
666 # failed. Users of this script may wish to treat hook action failures
667 # differently from VC failures.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000668 gclient_utils.SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000669
670 def _RunHooks(self, command, file_list, is_using_git):
671 """Evaluates all hooks, running actions as needed.
672 """
673 # Hooks only run for these command types.
674 if not command in ('update', 'revert', 'runhooks'):
675 return
676
evan@chromium.org67820ef2009-07-27 17:23:00 +0000677 # Hooks only run when --nohooks is not specified
678 if self._options.nohooks:
679 return
680
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681 # Get any hooks from the .gclient file.
682 hooks = self.GetVar("hooks", [])
683 # Add any hooks found in DEPS files.
684 hooks.extend(self._deps_hooks)
685
686 # If "--force" was specified, run all hooks regardless of what files have
687 # changed. If the user is using git, then we don't know what files have
688 # changed so we always run all hooks.
689 if self._options.force or is_using_git:
690 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000691 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000692 return
693
694 # Run hooks on the basis of whether the files from the gclient operation
695 # match each hook's pattern.
696 for hook_dict in hooks:
697 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000698 matching_file_list = [f for f in file_list if pattern.search(f)]
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000699 if matching_file_list:
700 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000701
piman@chromium.org8b5086d2010-04-17 02:05:32 +0000702 def GetSCMCommandClosure(self, path, url, revision, command, args, file_list):
703 """Gets a closure that runs a SCM command on a particular dependency."""
704 def _Closure():
705 logging.debug("Running %s in %s to %s %s" % (command, path, url,
706 revision))
707 options = copy.copy(self._options)
708 options.revision = revision
709 scm = gclient_scm.CreateSCM(url, self._root_dir, path)
710 scm.RunCommand(command, options, args, file_list)
711 return _Closure
712
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000713 def RunOnDeps(self, command, args):
714 """Runs a command on each dependency in a client and its dependencies.
715
716 The module's dependencies are specified in its top-level DEPS files.
717
718 Args:
719 command: The command to use (e.g., 'status' or 'diff')
720 args: list of str - extra arguments to add to the command line.
721
722 Raises:
723 Error: If the client has conflicting entries.
724 """
725 if not command in self.supported_commands:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000726 raise gclient_utils.Error("'%s' is an unsupported command" % command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727
728 # Check for revision overrides.
729 revision_overrides = {}
730 for revision in self._options.revisions:
731 if revision.find("@") == -1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000732 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000733 "Specify the full dependency when specifying a revision number.")
734 revision_elem = revision.split("@")
735 # Disallow conflicting revs
736 if revision_overrides.has_key(revision_elem[0]) and \
737 revision_overrides[revision_elem[0]] != revision_elem[1]:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000738 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000739 "Conflicting revision numbers specified.")
740 revision_overrides[revision_elem[0]] = revision_elem[1]
741
742 solutions = self.GetVar("solutions")
743 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000744 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000745
746 # When running runhooks --force, there's no need to consult the SCM.
747 # All known hooks are expected to run unconditionally regardless of working
748 # copy state, so skip the SCM status check.
749 run_scm = not (command == 'runhooks' and self._options.force)
750
751 entries = {}
752 entries_deps_content = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000753
piman@chromium.org8b5086d2010-04-17 02:05:32 +0000754 # To avoid threading issues, all file lists get constructed separately then
755 # gathered in a flattened list at the end.
756 file_list_list = []
757 file_list_dict = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000758
piman@chromium.org8b5086d2010-04-17 02:05:32 +0000759 thread_pool = gclient_utils.ThreadPool(self._options.jobs)
760 thread_pool.Start()
761
762 try:
763 # Run on the base solutions first.
764 for solution in solutions:
765 name = solution["name"]
766 deps_file = solution.get("deps_file", self._options.deps_file)
767 if '/' in deps_file or '\\' in deps_file:
768 raise gclient_utils.Error('deps_file name must not be a path, just a '
769 'filename.')
770 if name in entries:
771 raise gclient_utils.Error(
772 "solution %s specified more than once" % name)
773 url = solution["url"]
774 entries[name] = url
775 if run_scm and url:
776 revision = revision_overrides.get(name)
777 file_list = []
778 file_list_dict[name] = file_list
779 thread_pool.AddJob(self.GetSCMCommandClosure(
780 name, url, revision, command, args, file_list))
781
782 thread_pool.WaitJobs()
783
784 for solution in solutions:
785 name = solution["name"]
786 deps_file = solution.get("deps_file", self._options.deps_file)
787 try:
788 deps_content = gclient_utils.FileRead(
789 os.path.join(self._root_dir, name, deps_file))
790 except IOError, e:
791 if e.errno != errno.ENOENT:
792 raise
793 deps_content = ""
794 entries_deps_content[name] = deps_content
795 try:
796 file_list_list.append([os.path.join(name, f.strip())
797 for f in file_list_dict[name]])
798 except KeyError:
799 # We may not have added the file list to the dict, see tests above.
800 # Instead of duplicating the tests, it's less fragile to just ignore
801 # the exception.
802 pass
803
804 # Process the dependencies next (sort alphanumerically to ensure that
805 # containing directories get populated first and for readability)
806 # TODO(piman): when using multiple threads, the ordering is not ensured.
807 # In many cases (e.g. updates to an existing checkout where DEPS don't
808 # move between directories), it'll still be correct but for completeness
809 # this should be fixed.
810 deps = self._ParseAllDeps(entries, entries_deps_content)
811 deps_to_process = deps.keys()
812 deps_to_process.sort()
813
814 # First pass for direct dependencies.
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000815 if command == 'update' and not self._options.verbose:
piman@chromium.org8b5086d2010-04-17 02:05:32 +0000816 pm = Progress('Syncing projects', len(deps_to_process))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817
piman@chromium.org8b5086d2010-04-17 02:05:32 +0000818 for d in deps_to_process:
819 if command == 'update' and not self._options.verbose:
820 pm.update()
821 file_list = []
822 file_list_list.append(file_list)
823 if type(deps[d]) == str:
824 url = deps[d]
825 entries[d] = url
826 if run_scm:
827 revision = revision_overrides.get(d)
828 thread_pool.AddJob(self.GetSCMCommandClosure(d, url, revision,
829 command, args,
830 file_list))
831 elif isinstance(deps[d], self.FileImpl):
832 file = deps[d]
833 if run_scm:
834 revision = file.GetRevision()
835 thread_pool.AddJob(self.GetSCMCommandClosure(
836 d, url, revision, "updatesingle", args + [file.GetFilename()],
837 file_list))
838
839 thread_pool.WaitJobs()
840
841 if command == 'update' and not self._options.verbose:
842 pm.end()
843
844 # Second pass for inherited deps (via the From keyword)
845 for d in deps_to_process:
846 if isinstance(deps[d], self.FromImpl):
847 filename = os.path.join(self._root_dir,
848 deps[d].module_name,
849 self._options.deps_file)
850 content = gclient_utils.FileRead(filename)
851 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
852 False)
853 # Getting the URL from the sub_deps file can involve having to resolve
854 # a File() or having to resolve a relative URL. To resolve relative
855 # URLs, we need to pass in the orignal sub deps URL.
856 sub_deps_base_url = deps[deps[d].module_name]
857 url = deps[d].GetUrl(d, sub_deps_base_url, self._root_dir, sub_deps)
858 entries[d] = url
859 if run_scm:
860 revision = revision_overrides.get(d)
861 file_list = []
862 file_list_list.append(file_list)
863 thread_pool.AddJob(self.GetSCMCommandClosure(d, url, revision,
864 command, args,
865 file_list))
866
867 thread_pool.WaitJobs()
868 finally:
869 thread_pool.Stop()
870
871 file_list = sum(file_list_list, [])
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000872
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000873 # Convert all absolute paths to relative.
874 for i in range(len(file_list)):
875 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
876 # It depends on the command being executed (like runhooks vs sync).
877 if not os.path.isabs(file_list[i]):
878 continue
879
880 prefix = os.path.commonprefix([self._root_dir.lower(),
881 file_list[i].lower()])
882 file_list[i] = file_list[i][len(prefix):]
883
884 # Strip any leading path separators.
885 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
886 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000887
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000888 is_using_git = gclient_utils.IsUsingGit(self._root_dir, entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000889 self._RunHooks(command, file_list, is_using_git)
890
891 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000892 # Notify the user if there is an orphaned entry in their working copy.
893 # Only delete the directory if there are no changes in it, and
894 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000895 prev_entries = self._ReadEntries()
896 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000897 # Fix path separator on Windows.
898 entry_fixed = entry.replace('/', os.path.sep)
899 e_dir = os.path.join(self._root_dir, entry_fixed)
900 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000901 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000902 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000903 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000904 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000905 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000906 else:
907 file_list = []
908 scm = gclient_scm.CreateSCM(prev_entries[entry], self._root_dir,
909 entry_fixed)
910 scm.status(self._options, [], file_list)
911 modified_files = file_list != []
912 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000913 # There are modified files in this entry. Keep warning until
914 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000915 print(("\nWARNING: \"%s\" is no longer part of this client. "
916 "It is recommended that you manually remove it.\n") %
917 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000918 else:
919 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000920 print("\n________ deleting \'%s\' " +
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000921 "in \'%s\'") % (entry_fixed, self._root_dir)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000922 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000923 # record the current list of entries for next time
924 self._SaveEntries(entries)
925
926 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000927 """Output revision info mapping for the client and its dependencies.
928
929 This allows the capture of an overall "revision" for the source tree that
930 can be used to reproduce the same tree in the future. The actual output
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000931 contains enough information (source paths, svn server urls and revisions)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000932 that it can be used either to generate external svn/git commands (without
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000933 gclient) or as input to gclient's --rev option (with some massaging of
934 the data).
935
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000936 Since we care about the revision of the current source tree, for git
937 repositories this command uses the revision of the HEAD. For subversion we
938 use BASE.
939
940 The --snapshot option allows creating a .gclient file to reproduce the tree.
941
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000942 Raises:
943 Error: If the client has conflicting entries.
944 """
945 # Check for revision overrides.
946 revision_overrides = {}
947 for revision in self._options.revisions:
948 if revision.find("@") < 0:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000949 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000950 "Specify the full dependency when specifying a revision number.")
951 revision_elem = revision.split("@")
952 # Disallow conflicting revs
953 if revision_overrides.has_key(revision_elem[0]) and \
954 revision_overrides[revision_elem[0]] != revision_elem[1]:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000955 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000956 "Conflicting revision numbers specified.")
957 revision_overrides[revision_elem[0]] = revision_elem[1]
958
959 solutions = self.GetVar("solutions")
960 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000961 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000962
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000963 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000964 def GetURLAndRev(name, original_url):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000965 url, _ = gclient_utils.SplitUrlRevision(original_url)
966 scm = gclient_scm.CreateSCM(original_url, self._root_dir, name)
967 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000968
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000969 # text of the snapshot gclient file
970 new_gclient = ""
971 # Dictionary of { path : SCM url } to ensure no duplicate solutions
972 solution_names = {}
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000973 entries = {}
974 entries_deps_content = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000975 # Run on the base solutions first.
976 for solution in solutions:
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000977 # Dictionary of { path : SCM url } to describe the gclient checkout
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978 name = solution["name"]
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000979 if name in solution_names:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000980 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981 (url, rev) = GetURLAndRev(name, solution["url"])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000982 entries[name] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000983 solution_names[name] = "%s@%s" % (url, rev)
nasser@codeaurora.org952d7c72010-03-01 20:41:01 +0000984 deps_file = solution.get("deps_file", self._options.deps_file)
985 if '/' in deps_file or '\\' in deps_file:
986 raise gclient_utils.Error('deps_file name must not be a path, just a '
987 'filename.')
988 try:
989 deps_content = gclient_utils.FileRead(
990 os.path.join(self._root_dir, name, deps_file))
991 except IOError, e:
992 if e.errno != errno.ENOENT:
993 raise
994 deps_content = ""
995 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000996
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000997 # Process the dependencies next (sort alphanumerically to ensure that
998 # containing directories get populated first and for readability)
999 deps = self._ParseAllDeps(entries, entries_deps_content)
1000 deps_to_process = deps.keys()
1001 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001002
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +00001003 # First pass for direct dependencies.
1004 for d in deps_to_process:
1005 if type(deps[d]) == str:
1006 (url, rev) = GetURLAndRev(d, deps[d])
1007 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +00001009 # Second pass for inherited deps (via the From keyword)
1010 for d in deps_to_process:
tony@chromium.org4b5b1772010-04-08 01:52:56 +00001011 if isinstance(deps[d], self.FromImpl):
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +00001012 deps_parent_url = entries[deps[d].module_name]
1013 if deps_parent_url.find("@") < 0:
1014 raise gclient_utils.Error("From %s missing revisioned url" %
1015 deps[d].module_name)
1016 content = gclient_utils.FileRead(os.path.join(
1017 self._root_dir,
1018 deps[d].module_name,
1019 self._options.deps_file))
1020 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {})
1021 (url, rev) = GetURLAndRev(d, sub_deps[d])
1022 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +00001023
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +00001024 # Build the snapshot configuration string
1025 if self._options.snapshot:
1026 url = entries.pop(name)
1027 custom_deps = ",\n ".join(["\"%s\": \"%s\"" % (x, entries[x])
1028 for x in sorted(entries.keys())])
maruel@chromium.orge3da35f2010-03-09 21:40:45 +00001029
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +00001030 new_gclient += DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
1031 'solution_name': name,
1032 'solution_url': url,
1033 'safesync_url' : "",
1034 'solution_deps': custom_deps,
1035 }
1036 else:
1037 print(";\n".join(["%s: %s" % (x, entries[x])
1038 for x in sorted(entries.keys())]))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +00001039
1040 # Print the snapshot configuration file
1041 if self._options.snapshot:
1042 config = DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
1043 snapclient = GClient(self._root_dir, self._options)
1044 snapclient.SetConfig(config)
1045 print(snapclient._config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001046
1047
1048## gclient commands.
1049
1050
1051def DoCleanup(options, args):
1052 """Handle the cleanup subcommand.
1053
1054 Raises:
1055 Error: if client isn't configured properly.
1056 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001057 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001058 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001059 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001060 if options.verbose:
1061 # Print out the .gclient file. This is longer than if we just printed the
1062 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001063 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001064 return client.RunOnDeps('cleanup', args)
1065
1066
1067def DoConfig(options, args):
1068 """Handle the config subcommand.
1069
1070 Args:
1071 options: If options.spec set, a string providing contents of config file.
1072 args: The command line args. If spec is not set,
1073 then args[0] is a string URL to get for config file.
1074
1075 Raises:
1076 Error: on usage error
1077 """
1078 if len(args) < 1 and not options.spec:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001079 raise gclient_utils.Error("required argument missing; see 'gclient help "
1080 "config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +00001081 if os.path.exists(options.config_filename):
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001082 raise gclient_utils.Error("%s file already exists in the current directory"
1083 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001084 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001085 if options.spec:
1086 client.SetConfig(options.spec)
1087 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +00001088 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +00001089 if not options.name:
1090 name = base_url.split("/")[-1]
1091 else:
1092 # specify an alternate relpath for the given URL.
1093 name = options.name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001094 safesync_url = ""
1095 if len(args) > 1:
1096 safesync_url = args[1]
1097 client.SetDefaultConfig(name, base_url, safesync_url)
1098 client.SaveConfig()
1099
1100
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001101def DoExport(options, args):
1102 """Handle the export subcommand.
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001103
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001104 Raises:
1105 Error: on usage error
1106 """
1107 if len(args) != 1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001108 raise gclient_utils.Error("Need directory name")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001109 client = GClient.LoadCurrentConfig(options)
1110
1111 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001112 raise gclient_utils.Error("client not configured; see 'gclient config'")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001113
1114 if options.verbose:
1115 # Print out the .gclient file. This is longer than if we just printed the
1116 # client dict, but more legible, and it might contain helpful comments.
1117 print(client.ConfigContent())
1118 return client.RunOnDeps('export', args)
1119
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001120def DoHelp(options, args):
1121 """Handle the help subcommand giving help for another subcommand.
1122
1123 Raises:
1124 Error: if the command is unknown.
1125 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001126 __pychecker__ = 'unusednames=options'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001127 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001128 print(COMMAND_USAGE_TEXT[args[0]])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001129 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001130 raise gclient_utils.Error("unknown subcommand '%s'; see 'gclient help'" %
1131 args[0])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001132
1133
kbr@google.comab318592009-09-04 00:54:55 +00001134def DoPack(options, args):
1135 """Handle the pack subcommand.
1136
1137 Raises:
1138 Error: if client isn't configured properly.
1139 """
1140 client = GClient.LoadCurrentConfig(options)
1141 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001142 raise gclient_utils.Error("client not configured; see 'gclient config'")
kbr@google.comab318592009-09-04 00:54:55 +00001143 if options.verbose:
1144 # Print out the .gclient file. This is longer than if we just printed the
1145 # client dict, but more legible, and it might contain helpful comments.
1146 print(client.ConfigContent())
kbr@google.comab318592009-09-04 00:54:55 +00001147 return client.RunOnDeps('pack', args)
1148
1149
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001150def DoStatus(options, args):
1151 """Handle the status subcommand.
1152
1153 Raises:
1154 Error: if client isn't configured properly.
1155 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001156 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001157 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001158 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001159 if options.verbose:
1160 # Print out the .gclient file. This is longer than if we just printed the
1161 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001162 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001163 return client.RunOnDeps('status', args)
1164
1165
1166def DoUpdate(options, args):
1167 """Handle the update and sync subcommands.
1168
1169 Raises:
1170 Error: if client isn't configured properly.
1171 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001172 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001173
1174 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001175 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001176
1177 if not options.head:
1178 solutions = client.GetVar('solutions')
1179 if solutions:
1180 for s in solutions:
1181 if s.get('safesync_url', ''):
1182 # rip through revisions and make sure we're not over-riding
1183 # something that was explicitly passed
1184 has_key = False
1185 for r in options.revisions:
1186 if r.split('@')[0] == s['name']:
1187 has_key = True
1188 break
1189
1190 if not has_key:
1191 handle = urllib.urlopen(s['safesync_url'])
1192 rev = handle.read().strip()
1193 handle.close()
1194 if len(rev):
1195 options.revisions.append(s['name']+'@'+rev)
1196
1197 if options.verbose:
1198 # Print out the .gclient file. This is longer than if we just printed the
1199 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001200 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001201 return client.RunOnDeps('update', args)
1202
1203
1204def DoDiff(options, args):
1205 """Handle the diff subcommand.
1206
1207 Raises:
1208 Error: if client isn't configured properly.
1209 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001210 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001211 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001212 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001213 if options.verbose:
1214 # Print out the .gclient file. This is longer than if we just printed the
1215 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001216 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001217 return client.RunOnDeps('diff', args)
1218
1219
1220def DoRevert(options, args):
1221 """Handle the revert subcommand.
1222
1223 Raises:
1224 Error: if client isn't configured properly.
1225 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001226 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001227 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001228 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001229 return client.RunOnDeps('revert', args)
1230
1231
1232def DoRunHooks(options, args):
1233 """Handle the runhooks subcommand.
1234
1235 Raises:
1236 Error: if client isn't configured properly.
1237 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001238 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001239 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001240 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001241 if options.verbose:
1242 # Print out the .gclient file. This is longer than if we just printed the
1243 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001244 print(client.ConfigContent())
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001245 options.force = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001246 return client.RunOnDeps('runhooks', args)
1247
1248
1249def DoRevInfo(options, args):
1250 """Handle the revinfo subcommand.
1251
1252 Raises:
1253 Error: if client isn't configured properly.
1254 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001255 __pychecker__ = 'unusednames=args'
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001256 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001257 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001258 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001259 client.PrintRevInfo()
1260
1261
1262gclient_command_map = {
1263 "cleanup": DoCleanup,
1264 "config": DoConfig,
1265 "diff": DoDiff,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001266 "export": DoExport,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001267 "help": DoHelp,
kbr@google.comab318592009-09-04 00:54:55 +00001268 "pack": DoPack,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001269 "status": DoStatus,
1270 "sync": DoUpdate,
1271 "update": DoUpdate,
1272 "revert": DoRevert,
1273 "runhooks": DoRunHooks,
1274 "revinfo" : DoRevInfo,
1275}
1276
1277
1278def DispatchCommand(command, options, args, command_map=None):
1279 """Dispatches the appropriate subcommand based on command line arguments."""
1280 if command_map is None:
1281 command_map = gclient_command_map
1282
1283 if command in command_map:
1284 return command_map[command](options, args)
1285 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001286 raise gclient_utils.Error("unknown subcommand '%s'; see 'gclient help'" %
1287 command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001288
1289
1290def Main(argv):
1291 """Parse command line arguments and dispatch command."""
1292
1293 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1294 version=__version__)
1295 option_parser.disable_interspersed_args()
1296 option_parser.add_option("", "--force", action="store_true", default=False,
1297 help=("(update/sync only) force update even "
1298 "for modules which haven't changed"))
evan@chromium.org67820ef2009-07-27 17:23:00 +00001299 option_parser.add_option("", "--nohooks", action="store_true", default=False,
1300 help=("(update/sync/revert only) prevent the hooks from "
1301 "running"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001302 option_parser.add_option("", "--revision", action="append", dest="revisions",
1303 metavar="REV", default=[],
1304 help=("(update/sync only) sync to a specific "
1305 "revision, can be used multiple times for "
1306 "each solution, e.g. --revision=src@123, "
1307 "--revision=internal@32"))
1308 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1309 metavar="OS_LIST",
1310 help=("(update/sync only) sync deps for the "
1311 "specified (comma-separated) platform(s); "
1312 "'all' will sync all platforms"))
davemoore@chromium.org793796d2010-02-19 17:27:41 +00001313 option_parser.add_option("", "--reset", action="store_true", default=False,
1314 help=("(update/sync only) resets any local changes "
1315 "before updating (git only)"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001316 option_parser.add_option("", "--spec", default=None,
1317 help=("(config only) create a gclient file "
1318 "containing the provided string"))
maruel@chromium.orga6220d12010-01-06 21:04:17 +00001319 option_parser.add_option("-v", "--verbose", action="count", default=0,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001320 help="produce additional output for diagnostics")
maruel@chromium.org7753d242009-10-07 17:40:24 +00001321 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1322 default=False,
1323 help="Skip svn up whenever possible by requesting "
1324 "actual HEAD revision from the repository")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001325 option_parser.add_option("", "--head", action="store_true", default=False,
1326 help=("skips any safesync_urls specified in "
1327 "configured solutions"))
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001328 option_parser.add_option("", "--delete_unversioned_trees",
1329 action="store_true", default=False,
1330 help=("on update, delete any unexpected "
1331 "unversioned trees that are in the checkout"))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +00001332 option_parser.add_option("", "--snapshot", action="store_true", default=False,
1333 help=("(revinfo only), create a snapshot file "
1334 "of the current version of all repositories"))
iposva@chromium.org8cf7a392010-04-07 17:20:26 +00001335 option_parser.add_option("", "--name",
1336 help="specify alternate relative solution path")
maruel@chromium.orge3da35f2010-03-09 21:40:45 +00001337 option_parser.add_option("", "--gclientfile", default=None,
1338 metavar="FILENAME",
1339 help=("specify an alternate .gclient file"))
piman@chromium.org8b5086d2010-04-17 02:05:32 +00001340 option_parser.add_option("-j", "--jobs", default=1, type="int",
1341 help=("specify how many SCM commands can run in "
1342 "parallel"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001343
1344 if len(argv) < 2:
1345 # Users don't need to be told to use the 'help' command.
1346 option_parser.print_help()
1347 return 1
1348 # Add manual support for --version as first argument.
1349 if argv[1] == '--version':
1350 option_parser.print_version()
1351 return 0
1352
1353 # Add manual support for --help as first argument.
1354 if argv[1] == '--help':
1355 argv[1] = 'help'
1356
1357 command = argv[1]
1358 options, args = option_parser.parse_args(argv[2:])
1359
1360 if len(argv) < 3 and command == "help":
1361 option_parser.print_help()
1362 return 0
1363
maruel@chromium.orga6220d12010-01-06 21:04:17 +00001364 if options.verbose > 1:
maruel@chromium.org754960e2009-09-21 12:31:05 +00001365 logging.basicConfig(level=logging.DEBUG)
1366
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001367 # Files used for configuration and state saving.
1368 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
maruel@chromium.orge3da35f2010-03-09 21:40:45 +00001369 if options.gclientfile:
1370 options.config_filename = options.gclientfile
1371 options.entries_filename = options.config_filename + "_entries"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001372 options.deps_file = "DEPS"
1373
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001374 options.platform = sys.platform
1375 return DispatchCommand(command, options, args)
1376
1377
1378if "__main__" == __name__:
1379 try:
1380 result = Main(sys.argv)
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001381 except gclient_utils.Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001382 print >> sys.stderr, "Error: %s" % str(e)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001383 result = 1
1384 sys.exit(result)
1385
1386# vim: ts=2:sw=2:tw=80:et: