blob: 03ba98671c81ba56a0271710980e09c67258ab17 [file] [log] [blame]
agable@chromium.orgcc023502013-04-03 20:24:21 +00001#!/usr/bin/env python
2# Copyright (c) 2013 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.
5
6"""
7Tool to perform checkouts in one easy command line!
8
9Usage:
10 fetch <recipe> [--property=value [--property2=value2 ...]]
11
12This script is a wrapper around various version control and repository
13checkout commands. It requires a |recipe| name, fetches data from that
14recipe in depot_tools/recipes, and then performs all necessary inits,
15checkouts, pulls, fetches, etc.
16
17Optional arguments may be passed on the command line in key-value pairs.
18These parameters will be passed through to the recipe's main method.
19"""
20
21import json
digit@chromium.org3596d582013-12-13 17:07:33 +000022import optparse
agable@chromium.orgcc023502013-04-03 20:24:21 +000023import os
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +000024import pipes
agable@chromium.orgcc023502013-04-03 20:24:21 +000025import subprocess
26import sys
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +000027import textwrap
agable@chromium.orgcc023502013-04-03 20:24:21 +000028
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000029from distutils import spawn
30
agable@chromium.orgcc023502013-04-03 20:24:21 +000031
32SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
33
agable@chromium.orgcc023502013-04-03 20:24:21 +000034#################################################
35# Checkout class definitions.
36#################################################
37class Checkout(object):
38 """Base class for implementing different types of checkouts.
39
40 Attributes:
41 |base|: the absolute path of the directory in which this script is run.
42 |spec|: the spec for this checkout as returned by the recipe. Different
43 subclasses will expect different keys in this dictionary.
44 |root|: the directory into which the checkout will be performed, as returned
45 by the recipe. This is a relative path from |base|.
46 """
digit@chromium.org3596d582013-12-13 17:07:33 +000047 def __init__(self, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +000048 self.base = os.getcwd()
digit@chromium.org3596d582013-12-13 17:07:33 +000049 self.options = options
agable@chromium.orgcc023502013-04-03 20:24:21 +000050 self.spec = spec
51 self.root = root
52
53 def exists(self):
54 pass
55
56 def init(self):
57 pass
58
59 def sync(self):
60 pass
61
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000062 def run(self, cmd, **kwargs):
63 print 'Running: %s' % (' '.join(pipes.quote(x) for x in cmd))
wtc@chromium.org38e94612014-02-12 22:19:41 +000064 if self.options.dry_run:
agable@chromium.org78624b62015-06-15 19:15:53 +000065 return ''
mmoss@chromium.org5a447762015-06-10 20:01:39 +000066 return subprocess.check_output(cmd, **kwargs)
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000067
agable@chromium.orgcc023502013-04-03 20:24:21 +000068
69class GclientCheckout(Checkout):
70
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000071 def run_gclient(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000072 if not spawn.find_executable('gclient'):
73 cmd_prefix = (sys.executable, os.path.join(SCRIPT_PATH, 'gclient.py'))
74 else:
75 cmd_prefix = ('gclient',)
76 return self.run(cmd_prefix + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000077
mmoss@chromium.org5a447762015-06-10 20:01:39 +000078 def exists(self):
79 try:
80 gclient_root = self.run_gclient('root').strip()
81 return (os.path.exists(os.path.join(gclient_root, '.gclient')) or
82 os.path.exists(os.path.join(os.getcwd(), self.root)))
83 except subprocess.CalledProcessError:
84 pass
85 return os.path.exists(os.path.join(os.getcwd(), self.root))
86
agable@chromium.orgcc023502013-04-03 20:24:21 +000087
88class GitCheckout(Checkout):
89
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000090 def run_git(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000091 if sys.platform == 'win32' and not spawn.find_executable('git'):
mmoss@chromium.orgcc2b6a12014-02-20 17:42:59 +000092 git_path = os.path.join(SCRIPT_PATH, 'git.bat')
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000093 else:
94 git_path = 'git'
95 return self.run((git_path,) + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000096
97
agable@chromium.org2560ea72013-04-04 01:22:38 +000098class SvnCheckout(Checkout):
99
100 def run_svn(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +0000101 if sys.platform == 'win32' and not spawn.find_executable('svn'):
102 svn_path = os.path.join(SCRIPT_PATH, 'svn_bin', 'svn.exe')
103 else:
104 svn_path = 'svn'
105 return self.run((svn_path,) + cmd, **kwargs)
agable@chromium.org2560ea72013-04-04 01:22:38 +0000106
107
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000108class GclientGitCheckout(GclientCheckout, GitCheckout):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000109
digit@chromium.org3596d582013-12-13 17:07:33 +0000110 def __init__(self, options, spec, root):
111 super(GclientGitCheckout, self).__init__(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000112 assert 'solutions' in self.spec
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000113
114 def _format_spec(self):
115 def _format_literal(lit):
116 if isinstance(lit, basestring):
117 return '"%s"' % lit
118 if isinstance(lit, list):
119 return '[%s]' % ', '.join(_format_literal(i) for i in lit)
120 return '%r' % lit
121 soln_strings = []
122 for soln in self.spec['solutions']:
123 soln_string= '\n'.join(' "%s": %s,' % (key, _format_literal(value))
124 for key, value in soln.iteritems())
125 soln_strings.append(' {\n%s\n },' % soln_string)
126 gclient_spec = 'solutions = [\n%s\n]\n' % '\n'.join(soln_strings)
127 extra_keys = ['target_os', 'target_os_only']
128 gclient_spec += ''.join('%s = %s\n' % (key, _format_literal(self.spec[key]))
129 for key in extra_keys if key in self.spec)
130 return gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000131
agable@chromium.orgcc023502013-04-03 20:24:21 +0000132 def init(self):
133 # Configure and do the gclient checkout.
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000134 self.run_gclient('config', '--spec', self._format_spec())
jochen@chromium.org048da082014-05-06 08:32:40 +0000135 sync_cmd = ['sync']
agable@chromium.org78624b62015-06-15 19:15:53 +0000136 if self.options.nohooks or self.spec.get('fetch_hooks'):
jochen@chromium.org048da082014-05-06 08:32:40 +0000137 sync_cmd.append('--nohooks')
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000138 if self.options.no_history:
139 sync_cmd.append('--no-history')
jochen@chromium.org048da082014-05-06 08:32:40 +0000140 if self.spec.get('with_branch_heads', False):
141 sync_cmd.append('--with_branch_heads')
142 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000143
agable@chromium.org78624b62015-06-15 19:15:53 +0000144 for cmd in self.spec.get('fetch_hooks', []):
145 self.run(cmd)
146 if self.spec.get('fetch_hooks') and not self.options.nohooks:
147 self.run_gclient('runhooks')
148
agable@chromium.orgcc023502013-04-03 20:24:21 +0000149 # Configure git.
150 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000151 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000152 print 'cd %s' % wd
agable@chromium.orgcc023502013-04-03 20:24:21 +0000153 self.run_git(
154 'submodule', 'foreach',
155 'git config -f $toplevel/.git/config submodule.$name.ignore all',
156 cwd=wd)
iannucci@chromium.orgf2fb5e72014-04-03 02:36:44 +0000157 self.run_git(
158 'config', '--add', 'remote.origin.fetch',
159 '+refs/tags/*:refs/tags/*', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000160 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
161
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000162
163class GclientGitSvnCheckout(GclientGitCheckout, SvnCheckout):
164
digit@chromium.org3596d582013-12-13 17:07:33 +0000165 def __init__(self, options, spec, root):
166 super(GclientGitSvnCheckout, self).__init__(options, spec, root)
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000167
168 def init(self):
169 # Ensure we are authenticated with subversion for all submodules.
170 git_svn_dirs = json.loads(self.spec.get('submodule_git_svn_spec', '{}'))
171 git_svn_dirs.update({self.root: self.spec})
172 for _, svn_spec in git_svn_dirs.iteritems():
agable@chromium.org14f633b2014-10-22 10:35:33 +0000173 if svn_spec.get('svn_url'):
174 try:
175 self.run_svn('ls', '--non-interactive', svn_spec['svn_url'])
176 except subprocess.CalledProcessError:
177 print 'Please run `svn ls %s`' % svn_spec['svn_url']
178 return 1
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000179
180 super(GclientGitSvnCheckout, self).init()
181
agable@chromium.orgcc023502013-04-03 20:24:21 +0000182 # Configure git-svn.
agable@chromium.org2560ea72013-04-04 01:22:38 +0000183 for path, svn_spec in git_svn_dirs.iteritems():
184 real_path = os.path.join(*path.split('/'))
185 if real_path != self.root:
186 real_path = os.path.join(self.root, real_path)
187 wd = os.path.join(self.base, real_path)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000188 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000189 print 'cd %s' % wd
agable@chromium.org14f633b2014-10-22 10:35:33 +0000190 if svn_spec.get('auto'):
191 self.run_git('auto-svn', cwd=wd)
192 continue
193 self.run_git('svn', 'init', svn_spec['svn_url'], cwd=wd)
194 self.run_git('config', '--unset-all', 'svn-remote.svn.fetch', cwd=wd)
195 for svn_branch, git_ref in svn_spec.get('git_svn_fetch', {}).items():
196 self.run_git('config', '--add', 'svn-remote.svn.fetch',
197 '%s:%s' % (svn_branch, git_ref), cwd=wd)
198 for svn_branch, git_ref in svn_spec.get('git_svn_branches', {}).items():
199 self.run_git('config', '--add', 'svn-remote.svn.branches',
200 '%s:%s' % (svn_branch, git_ref), cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000201 self.run_git('svn', 'fetch', cwd=wd)
202
203
agable@chromium.org2560ea72013-04-04 01:22:38 +0000204
agable@chromium.orgcc023502013-04-03 20:24:21 +0000205CHECKOUT_TYPE_MAP = {
206 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000207 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000208 'gclient_git_svn': GclientGitSvnCheckout,
209 'git': GitCheckout,
210}
211
212
digit@chromium.org3596d582013-12-13 17:07:33 +0000213def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000214 """Factory to build Checkout class instances."""
215 class_ = CHECKOUT_TYPE_MAP.get(type_name)
216 if not class_:
217 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000218 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000219
220
221#################################################
222# Utility function and file entry point.
223#################################################
224def usage(msg=None):
225 """Print help and exit."""
226 if msg:
227 print 'Error:', msg
228
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000229 print textwrap.dedent("""\
230 usage: %s [options] <recipe> [--property=value [--property2=value2 ...]]
digit@chromium.org3596d582013-12-13 17:07:33 +0000231
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000232 This script can be used to download the Chromium sources. See
233 http://www.chromium.org/developers/how-tos/get-the-code
234 for full usage instructions.
digit@chromium.org3596d582013-12-13 17:07:33 +0000235
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000236 Valid options:
237 -h, --help, help Print this message.
238 --nohooks Don't run hooks after checkout.
239 -n, --dry-run Don't run commands, only print them.
240 --no-history Perform shallow clones, don't fetch the full git history.
241
242 Valid fetch recipes:""") % os.path.basename(sys.argv[0])
243 for fname in os.listdir(os.path.join(SCRIPT_PATH, 'recipes')):
244 if fname.endswith('.py'):
245 print ' ' + fname[:-3]
246
agable@chromium.orgcc023502013-04-03 20:24:21 +0000247 sys.exit(bool(msg))
248
249
250def handle_args(argv):
251 """Gets the recipe name from the command line arguments."""
252 if len(argv) <= 1:
253 usage('Must specify a recipe.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000254 if argv[1] in ('-h', '--help', 'help'):
255 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000256
wtc@chromium.org38e94612014-02-12 22:19:41 +0000257 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000258 nohooks = False
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000259 no_history = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000260 while len(argv) >= 2:
261 arg = argv[1]
262 if not arg.startswith('-'):
263 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000264 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000265 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000266 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000267 elif arg == '--nohooks':
268 nohooks = True
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000269 elif arg == '--no-history':
270 no_history = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000271 else:
272 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000273
agable@chromium.orgcc023502013-04-03 20:24:21 +0000274 def looks_like_arg(arg):
275 return arg.startswith('--') and arg.count('=') == 1
276
277 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
278 if bad_parms:
279 usage('Got bad arguments %s' % bad_parms)
280
281 recipe = argv[1]
282 props = argv[2:]
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000283 return (
284 optparse.Values(
285 {'dry_run':dry_run, 'nohooks':nohooks, 'no_history': no_history }),
286 recipe,
287 props)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000288
289
290def run_recipe_fetch(recipe, props, aliased=False):
291 """Invoke a recipe's fetch method with the passed-through args
292 and return its json output as a python object."""
293 recipe_path = os.path.abspath(os.path.join(SCRIPT_PATH, 'recipes', recipe))
dpranke@chromium.orga992edb2013-04-03 21:22:20 +0000294 if not os.path.exists(recipe_path + '.py'):
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000295 print "Could not find a recipe for %s" % recipe
296 sys.exit(1)
297
agable@chromium.orgcc023502013-04-03 20:24:21 +0000298 cmd = [sys.executable, recipe_path + '.py', 'fetch'] + props
299 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000300
agable@chromium.orgcc023502013-04-03 20:24:21 +0000301 spec = json.loads(result)
302 if 'alias' in spec:
303 assert not aliased
304 return run_recipe_fetch(
305 spec['alias']['recipe'], spec['alias']['props'] + props, aliased=True)
306 cmd = [sys.executable, recipe_path + '.py', 'root']
307 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
308 root = json.loads(result)
309 return spec, root
310
311
digit@chromium.org3596d582013-12-13 17:07:33 +0000312def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000313 """Perform a checkout with the given type and configuration.
314
315 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000316 options: Options instance.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000317 spec: Checkout configuration returned by the the recipe's fetch_spec
318 method (checkout type, repository url, etc.).
319 root: The directory into which the repo expects to be checkout out.
320 """
321 assert 'type' in spec
322 checkout_type = spec['type']
323 checkout_spec = spec['%s_spec' % checkout_type]
324 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000325 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000326 except KeyError:
327 return 1
328 if checkout.exists():
mmoss@chromium.org5a447762015-06-10 20:01:39 +0000329 print 'Your current directory appears to already contain, or be part of, '
330 print 'a checkout. "fetch" is used only to get new checkouts. Use '
331 print '"gclient sync" to update existing checkouts.'
dpranke@chromium.orgfd79e0d2013-04-12 21:34:32 +0000332 print
333 print 'Fetch also does not yet deal with partial checkouts, so if fetch'
334 print 'failed, delete the checkout and start over (crbug.com/230691).'
agable@chromium.orgcc023502013-04-03 20:24:21 +0000335 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000336 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000337
338
339def main():
digit@chromium.org3596d582013-12-13 17:07:33 +0000340 options, recipe, props = handle_args(sys.argv)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000341 spec, root = run_recipe_fetch(recipe, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000342 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000343
344
345if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000346 try:
347 sys.exit(main())
348 except KeyboardInterrupt:
349 sys.stderr.write('interrupted\n')
350 sys.exit(1)