blob: d3bcacd793195438a658cee1e03c7e83fbc70f93 [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
24import subprocess
25import sys
26import pipes
27
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000028from distutils import spawn
29
agable@chromium.orgcc023502013-04-03 20:24:21 +000030
31SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
32
agable@chromium.orgcc023502013-04-03 20:24:21 +000033#################################################
34# Checkout class definitions.
35#################################################
36class Checkout(object):
37 """Base class for implementing different types of checkouts.
38
39 Attributes:
40 |base|: the absolute path of the directory in which this script is run.
41 |spec|: the spec for this checkout as returned by the recipe. Different
42 subclasses will expect different keys in this dictionary.
43 |root|: the directory into which the checkout will be performed, as returned
44 by the recipe. This is a relative path from |base|.
45 """
digit@chromium.org3596d582013-12-13 17:07:33 +000046 def __init__(self, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +000047 self.base = os.getcwd()
digit@chromium.org3596d582013-12-13 17:07:33 +000048 self.options = options
agable@chromium.orgcc023502013-04-03 20:24:21 +000049 self.spec = spec
50 self.root = root
51
52 def exists(self):
53 pass
54
55 def init(self):
56 pass
57
58 def sync(self):
59 pass
60
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000061 def run(self, cmd, **kwargs):
62 print 'Running: %s' % (' '.join(pipes.quote(x) for x in cmd))
wtc@chromium.org38e94612014-02-12 22:19:41 +000063 if self.options.dry_run:
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000064 return 0
65 return subprocess.check_call(cmd, **kwargs)
66
agable@chromium.orgcc023502013-04-03 20:24:21 +000067
68class GclientCheckout(Checkout):
69
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000070 def run_gclient(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000071 if not spawn.find_executable('gclient'):
72 cmd_prefix = (sys.executable, os.path.join(SCRIPT_PATH, 'gclient.py'))
73 else:
74 cmd_prefix = ('gclient',)
75 return self.run(cmd_prefix + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000076
77
78class GitCheckout(Checkout):
79
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000080 def run_git(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000081 if sys.platform == 'win32' and not spawn.find_executable('git'):
mmoss@chromium.orgcc2b6a12014-02-20 17:42:59 +000082 git_path = os.path.join(SCRIPT_PATH, 'git.bat')
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000083 else:
84 git_path = 'git'
85 return self.run((git_path,) + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000086
87
agable@chromium.org2560ea72013-04-04 01:22:38 +000088class SvnCheckout(Checkout):
89
90 def run_svn(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000091 if sys.platform == 'win32' and not spawn.find_executable('svn'):
92 svn_path = os.path.join(SCRIPT_PATH, 'svn_bin', 'svn.exe')
93 else:
94 svn_path = 'svn'
95 return self.run((svn_path,) + cmd, **kwargs)
agable@chromium.org2560ea72013-04-04 01:22:38 +000096
97
jochen@chromium.orgd993e782013-04-11 20:03:13 +000098class GclientGitCheckout(GclientCheckout, GitCheckout):
agable@chromium.orgcc023502013-04-03 20:24:21 +000099
digit@chromium.org3596d582013-12-13 17:07:33 +0000100 def __init__(self, options, spec, root):
101 super(GclientGitCheckout, self).__init__(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000102 assert 'solutions' in self.spec
103 keys = ['solutions', 'target_os', 'target_os_only']
104 gclient_spec = '\n'.join('%s = %s' % (key, self.spec[key])
janx@chromium.orgc4209082013-04-18 18:33:52 +0000105 for key in keys if key in self.spec)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000106 self.spec['gclient_spec'] = gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000107
108 def exists(self):
109 return os.path.exists(os.path.join(os.getcwd(), self.root))
110
111 def init(self):
112 # Configure and do the gclient checkout.
113 self.run_gclient('config', '--spec', self.spec['gclient_spec'])
jochen@chromium.org048da082014-05-06 08:32:40 +0000114 sync_cmd = ['sync']
digit@chromium.org3596d582013-12-13 17:07:33 +0000115 if self.options.nohooks:
jochen@chromium.org048da082014-05-06 08:32:40 +0000116 sync_cmd.append('--nohooks')
117 if self.spec.get('with_branch_heads', False):
118 sync_cmd.append('--with_branch_heads')
119 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000120
121 # Configure git.
122 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000123 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000124 print 'cd %s' % wd
agable@chromium.orgcc023502013-04-03 20:24:21 +0000125 self.run_git(
126 'submodule', 'foreach',
127 'git config -f $toplevel/.git/config submodule.$name.ignore all',
128 cwd=wd)
iannucci@chromium.orgf2fb5e72014-04-03 02:36:44 +0000129 self.run_git(
130 'config', '--add', 'remote.origin.fetch',
131 '+refs/tags/*:refs/tags/*', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000132 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
133
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000134
135class GclientGitSvnCheckout(GclientGitCheckout, SvnCheckout):
136
digit@chromium.org3596d582013-12-13 17:07:33 +0000137 def __init__(self, options, spec, root):
138 super(GclientGitSvnCheckout, self).__init__(options, spec, root)
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000139 assert 'svn_url' in self.spec
140 assert 'svn_branch' in self.spec
141 assert 'svn_ref' in self.spec
142
143 def init(self):
144 # Ensure we are authenticated with subversion for all submodules.
145 git_svn_dirs = json.loads(self.spec.get('submodule_git_svn_spec', '{}'))
146 git_svn_dirs.update({self.root: self.spec})
147 for _, svn_spec in git_svn_dirs.iteritems():
148 try:
149 self.run_svn('ls', '--non-interactive', svn_spec['svn_url'])
150 except subprocess.CalledProcessError:
151 print 'Please run `svn ls %s`' % svn_spec['svn_url']
152 return 1
153
154 super(GclientGitSvnCheckout, self).init()
155
agable@chromium.orgcc023502013-04-03 20:24:21 +0000156 # Configure git-svn.
agable@chromium.org2560ea72013-04-04 01:22:38 +0000157 for path, svn_spec in git_svn_dirs.iteritems():
158 real_path = os.path.join(*path.split('/'))
159 if real_path != self.root:
160 real_path = os.path.join(self.root, real_path)
161 wd = os.path.join(self.base, real_path)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000162 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000163 print 'cd %s' % wd
jochen@chromium.org048da082014-05-06 08:32:40 +0000164 prefix = svn_spec.get('svn_prefix', 'origin/')
165 self.run_git('svn', 'init', '--prefix=' + prefix, '-T',
agable@chromium.org2560ea72013-04-04 01:22:38 +0000166 svn_spec['svn_branch'], svn_spec['svn_url'], cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000167 self.run_git('config', '--replace', 'svn-remote.svn.fetch',
jochen@chromium.org048da082014-05-06 08:32:40 +0000168 svn_spec['svn_branch'] + ':refs/remotes/' + prefix +
agable@chromium.org2560ea72013-04-04 01:22:38 +0000169 svn_spec['svn_ref'], cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000170 self.run_git('svn', 'fetch', cwd=wd)
171
172
agable@chromium.org2560ea72013-04-04 01:22:38 +0000173
agable@chromium.orgcc023502013-04-03 20:24:21 +0000174CHECKOUT_TYPE_MAP = {
175 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000176 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000177 'gclient_git_svn': GclientGitSvnCheckout,
178 'git': GitCheckout,
179}
180
181
digit@chromium.org3596d582013-12-13 17:07:33 +0000182def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000183 """Factory to build Checkout class instances."""
184 class_ = CHECKOUT_TYPE_MAP.get(type_name)
185 if not class_:
186 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000187 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000188
189
190#################################################
191# Utility function and file entry point.
192#################################################
193def usage(msg=None):
194 """Print help and exit."""
195 if msg:
196 print 'Error:', msg
197
198 print (
199"""
digit@chromium.org3596d582013-12-13 17:07:33 +0000200usage: %s [options] <recipe> [--property=value [--property2=value2 ...]]
201
202This script can be used to download the Chromium sources. See
203http://www.chromium.org/developers/how-tos/get-the-code
204for full usage instructions.
205
206Valid options:
207 -h, --help, help Print this message.
208 --nohooks Don't run hooks after checkout.
wtc@chromium.org38e94612014-02-12 22:19:41 +0000209 -n, --dry-run Don't run commands, only print them.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000210""" % os.path.basename(sys.argv[0]))
211 sys.exit(bool(msg))
212
213
214def handle_args(argv):
215 """Gets the recipe name from the command line arguments."""
216 if len(argv) <= 1:
217 usage('Must specify a recipe.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000218 if argv[1] in ('-h', '--help', 'help'):
219 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000220
wtc@chromium.org38e94612014-02-12 22:19:41 +0000221 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000222 nohooks = False
223 while len(argv) >= 2:
224 arg = argv[1]
225 if not arg.startswith('-'):
226 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000227 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000228 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000229 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000230 elif arg == '--nohooks':
231 nohooks = True
232 else:
233 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000234
agable@chromium.orgcc023502013-04-03 20:24:21 +0000235 def looks_like_arg(arg):
236 return arg.startswith('--') and arg.count('=') == 1
237
238 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
239 if bad_parms:
240 usage('Got bad arguments %s' % bad_parms)
241
242 recipe = argv[1]
243 props = argv[2:]
primiano@chromium.org69177ea2014-08-05 23:19:29 +0000244 return optparse.Values({'dry_run':dry_run, 'nohooks':nohooks }), recipe, props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000245
246
247def run_recipe_fetch(recipe, props, aliased=False):
248 """Invoke a recipe's fetch method with the passed-through args
249 and return its json output as a python object."""
250 recipe_path = os.path.abspath(os.path.join(SCRIPT_PATH, 'recipes', recipe))
dpranke@chromium.orga992edb2013-04-03 21:22:20 +0000251 if not os.path.exists(recipe_path + '.py'):
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000252 print "Could not find a recipe for %s" % recipe
253 sys.exit(1)
254
agable@chromium.orgcc023502013-04-03 20:24:21 +0000255 cmd = [sys.executable, recipe_path + '.py', 'fetch'] + props
256 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000257
agable@chromium.orgcc023502013-04-03 20:24:21 +0000258 spec = json.loads(result)
259 if 'alias' in spec:
260 assert not aliased
261 return run_recipe_fetch(
262 spec['alias']['recipe'], spec['alias']['props'] + props, aliased=True)
263 cmd = [sys.executable, recipe_path + '.py', 'root']
264 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
265 root = json.loads(result)
266 return spec, root
267
268
digit@chromium.org3596d582013-12-13 17:07:33 +0000269def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000270 """Perform a checkout with the given type and configuration.
271
272 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000273 options: Options instance.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000274 spec: Checkout configuration returned by the the recipe's fetch_spec
275 method (checkout type, repository url, etc.).
276 root: The directory into which the repo expects to be checkout out.
277 """
278 assert 'type' in spec
279 checkout_type = spec['type']
280 checkout_spec = spec['%s_spec' % checkout_type]
281 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000282 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000283 except KeyError:
284 return 1
285 if checkout.exists():
dpranke@chromium.orgfd79e0d2013-04-12 21:34:32 +0000286 print 'You appear to already have a checkout. "fetch" is used only'
287 print 'to get new checkouts. Use "gclient sync" to update the checkout.'
288 print
289 print 'Fetch also does not yet deal with partial checkouts, so if fetch'
290 print 'failed, delete the checkout and start over (crbug.com/230691).'
agable@chromium.orgcc023502013-04-03 20:24:21 +0000291 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000292 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000293
294
295def main():
digit@chromium.org3596d582013-12-13 17:07:33 +0000296 options, recipe, props = handle_args(sys.argv)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000297 spec, root = run_recipe_fetch(recipe, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000298 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000299
300
301if __name__ == '__main__':
302 sys.exit(main())