blob: 04be95fa48ea0d095692a321c0e6b14ec5fedb57 [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:
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000065 return 0
66 return subprocess.check_call(cmd, **kwargs)
67
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
78
79class GitCheckout(Checkout):
80
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000081 def run_git(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000082 if sys.platform == 'win32' and not spawn.find_executable('git'):
mmoss@chromium.orgcc2b6a12014-02-20 17:42:59 +000083 git_path = os.path.join(SCRIPT_PATH, 'git.bat')
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000084 else:
85 git_path = 'git'
86 return self.run((git_path,) + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000087
88
agable@chromium.org2560ea72013-04-04 01:22:38 +000089class SvnCheckout(Checkout):
90
91 def run_svn(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000092 if sys.platform == 'win32' and not spawn.find_executable('svn'):
93 svn_path = os.path.join(SCRIPT_PATH, 'svn_bin', 'svn.exe')
94 else:
95 svn_path = 'svn'
96 return self.run((svn_path,) + cmd, **kwargs)
agable@chromium.org2560ea72013-04-04 01:22:38 +000097
98
jochen@chromium.orgd993e782013-04-11 20:03:13 +000099class GclientGitCheckout(GclientCheckout, GitCheckout):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000100
digit@chromium.org3596d582013-12-13 17:07:33 +0000101 def __init__(self, options, spec, root):
102 super(GclientGitCheckout, self).__init__(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000103 assert 'solutions' in self.spec
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000104
105 def _format_spec(self):
106 def _format_literal(lit):
107 if isinstance(lit, basestring):
108 return '"%s"' % lit
109 if isinstance(lit, list):
110 return '[%s]' % ', '.join(_format_literal(i) for i in lit)
111 return '%r' % lit
112 soln_strings = []
113 for soln in self.spec['solutions']:
114 soln_string= '\n'.join(' "%s": %s,' % (key, _format_literal(value))
115 for key, value in soln.iteritems())
116 soln_strings.append(' {\n%s\n },' % soln_string)
117 gclient_spec = 'solutions = [\n%s\n]\n' % '\n'.join(soln_strings)
118 extra_keys = ['target_os', 'target_os_only']
119 gclient_spec += ''.join('%s = %s\n' % (key, _format_literal(self.spec[key]))
120 for key in extra_keys if key in self.spec)
121 return gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000122
123 def exists(self):
124 return os.path.exists(os.path.join(os.getcwd(), self.root))
125
126 def init(self):
127 # Configure and do the gclient checkout.
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000128 self.run_gclient('config', '--spec', self._format_spec())
jochen@chromium.org048da082014-05-06 08:32:40 +0000129 sync_cmd = ['sync']
digit@chromium.org3596d582013-12-13 17:07:33 +0000130 if self.options.nohooks:
jochen@chromium.org048da082014-05-06 08:32:40 +0000131 sync_cmd.append('--nohooks')
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000132 if self.options.no_history:
133 sync_cmd.append('--no-history')
jochen@chromium.org048da082014-05-06 08:32:40 +0000134 if self.spec.get('with_branch_heads', False):
135 sync_cmd.append('--with_branch_heads')
136 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000137
138 # Configure git.
139 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000140 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000141 print 'cd %s' % wd
agable@chromium.orgcc023502013-04-03 20:24:21 +0000142 self.run_git(
143 'submodule', 'foreach',
144 'git config -f $toplevel/.git/config submodule.$name.ignore all',
145 cwd=wd)
iannucci@chromium.orgf2fb5e72014-04-03 02:36:44 +0000146 self.run_git(
147 'config', '--add', 'remote.origin.fetch',
148 '+refs/tags/*:refs/tags/*', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000149 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
150
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000151
152class GclientGitSvnCheckout(GclientGitCheckout, SvnCheckout):
153
digit@chromium.org3596d582013-12-13 17:07:33 +0000154 def __init__(self, options, spec, root):
155 super(GclientGitSvnCheckout, self).__init__(options, spec, root)
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000156
157 def init(self):
158 # Ensure we are authenticated with subversion for all submodules.
159 git_svn_dirs = json.loads(self.spec.get('submodule_git_svn_spec', '{}'))
160 git_svn_dirs.update({self.root: self.spec})
161 for _, svn_spec in git_svn_dirs.iteritems():
agable@chromium.org14f633b2014-10-22 10:35:33 +0000162 if svn_spec.get('svn_url'):
163 try:
164 self.run_svn('ls', '--non-interactive', svn_spec['svn_url'])
165 except subprocess.CalledProcessError:
166 print 'Please run `svn ls %s`' % svn_spec['svn_url']
167 return 1
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000168
169 super(GclientGitSvnCheckout, self).init()
170
agable@chromium.orgcc023502013-04-03 20:24:21 +0000171 # Configure git-svn.
agable@chromium.org2560ea72013-04-04 01:22:38 +0000172 for path, svn_spec in git_svn_dirs.iteritems():
173 real_path = os.path.join(*path.split('/'))
174 if real_path != self.root:
175 real_path = os.path.join(self.root, real_path)
176 wd = os.path.join(self.base, real_path)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000177 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000178 print 'cd %s' % wd
agable@chromium.org14f633b2014-10-22 10:35:33 +0000179 if svn_spec.get('auto'):
180 self.run_git('auto-svn', cwd=wd)
181 continue
182 self.run_git('svn', 'init', svn_spec['svn_url'], cwd=wd)
183 self.run_git('config', '--unset-all', 'svn-remote.svn.fetch', cwd=wd)
184 for svn_branch, git_ref in svn_spec.get('git_svn_fetch', {}).items():
185 self.run_git('config', '--add', 'svn-remote.svn.fetch',
186 '%s:%s' % (svn_branch, git_ref), cwd=wd)
187 for svn_branch, git_ref in svn_spec.get('git_svn_branches', {}).items():
188 self.run_git('config', '--add', 'svn-remote.svn.branches',
189 '%s:%s' % (svn_branch, git_ref), cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000190 self.run_git('svn', 'fetch', cwd=wd)
191
192
agable@chromium.org2560ea72013-04-04 01:22:38 +0000193
agable@chromium.orgcc023502013-04-03 20:24:21 +0000194CHECKOUT_TYPE_MAP = {
195 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000196 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000197 'gclient_git_svn': GclientGitSvnCheckout,
198 'git': GitCheckout,
199}
200
201
digit@chromium.org3596d582013-12-13 17:07:33 +0000202def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000203 """Factory to build Checkout class instances."""
204 class_ = CHECKOUT_TYPE_MAP.get(type_name)
205 if not class_:
206 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000207 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000208
209
210#################################################
211# Utility function and file entry point.
212#################################################
213def usage(msg=None):
214 """Print help and exit."""
215 if msg:
216 print 'Error:', msg
217
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000218 print textwrap.dedent("""\
219 usage: %s [options] <recipe> [--property=value [--property2=value2 ...]]
digit@chromium.org3596d582013-12-13 17:07:33 +0000220
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000221 This script can be used to download the Chromium sources. See
222 http://www.chromium.org/developers/how-tos/get-the-code
223 for full usage instructions.
digit@chromium.org3596d582013-12-13 17:07:33 +0000224
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000225 Valid options:
226 -h, --help, help Print this message.
227 --nohooks Don't run hooks after checkout.
228 -n, --dry-run Don't run commands, only print them.
229 --no-history Perform shallow clones, don't fetch the full git history.
230
231 Valid fetch recipes:""") % os.path.basename(sys.argv[0])
232 for fname in os.listdir(os.path.join(SCRIPT_PATH, 'recipes')):
233 if fname.endswith('.py'):
234 print ' ' + fname[:-3]
235
agable@chromium.orgcc023502013-04-03 20:24:21 +0000236 sys.exit(bool(msg))
237
238
239def handle_args(argv):
240 """Gets the recipe name from the command line arguments."""
241 if len(argv) <= 1:
242 usage('Must specify a recipe.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000243 if argv[1] in ('-h', '--help', 'help'):
244 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000245
wtc@chromium.org38e94612014-02-12 22:19:41 +0000246 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000247 nohooks = False
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000248 no_history = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000249 while len(argv) >= 2:
250 arg = argv[1]
251 if not arg.startswith('-'):
252 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000253 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000254 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000255 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000256 elif arg == '--nohooks':
257 nohooks = True
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000258 elif arg == '--no-history':
259 no_history = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000260 else:
261 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000262
agable@chromium.orgcc023502013-04-03 20:24:21 +0000263 def looks_like_arg(arg):
264 return arg.startswith('--') and arg.count('=') == 1
265
266 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
267 if bad_parms:
268 usage('Got bad arguments %s' % bad_parms)
269
270 recipe = argv[1]
271 props = argv[2:]
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000272 return (
273 optparse.Values(
274 {'dry_run':dry_run, 'nohooks':nohooks, 'no_history': no_history }),
275 recipe,
276 props)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000277
278
279def run_recipe_fetch(recipe, props, aliased=False):
280 """Invoke a recipe's fetch method with the passed-through args
281 and return its json output as a python object."""
282 recipe_path = os.path.abspath(os.path.join(SCRIPT_PATH, 'recipes', recipe))
dpranke@chromium.orga992edb2013-04-03 21:22:20 +0000283 if not os.path.exists(recipe_path + '.py'):
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000284 print "Could not find a recipe for %s" % recipe
285 sys.exit(1)
286
agable@chromium.orgcc023502013-04-03 20:24:21 +0000287 cmd = [sys.executable, recipe_path + '.py', 'fetch'] + props
288 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000289
agable@chromium.orgcc023502013-04-03 20:24:21 +0000290 spec = json.loads(result)
291 if 'alias' in spec:
292 assert not aliased
293 return run_recipe_fetch(
294 spec['alias']['recipe'], spec['alias']['props'] + props, aliased=True)
295 cmd = [sys.executable, recipe_path + '.py', 'root']
296 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
297 root = json.loads(result)
298 return spec, root
299
300
digit@chromium.org3596d582013-12-13 17:07:33 +0000301def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000302 """Perform a checkout with the given type and configuration.
303
304 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000305 options: Options instance.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000306 spec: Checkout configuration returned by the the recipe's fetch_spec
307 method (checkout type, repository url, etc.).
308 root: The directory into which the repo expects to be checkout out.
309 """
310 assert 'type' in spec
311 checkout_type = spec['type']
312 checkout_spec = spec['%s_spec' % checkout_type]
313 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000314 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000315 except KeyError:
316 return 1
317 if checkout.exists():
dpranke@chromium.orgfd79e0d2013-04-12 21:34:32 +0000318 print 'You appear to already have a checkout. "fetch" is used only'
319 print 'to get new checkouts. Use "gclient sync" to update the checkout.'
320 print
321 print 'Fetch also does not yet deal with partial checkouts, so if fetch'
322 print 'failed, delete the checkout and start over (crbug.com/230691).'
agable@chromium.orgcc023502013-04-03 20:24:21 +0000323 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000324 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000325
326
327def main():
digit@chromium.org3596d582013-12-13 17:07:33 +0000328 options, recipe, props = handle_args(sys.argv)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000329 spec, root = run_recipe_fetch(recipe, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000330 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000331
332
333if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000334 try:
335 sys.exit(main())
336 except KeyboardInterrupt:
337 sys.stderr.write('interrupted\n')
338 sys.exit(1)