blob: 897356f299c5aaaeff22b71f44c9259b3b24f867 [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')
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000117 if self.options.no_history:
118 sync_cmd.append('--no-history')
jochen@chromium.org048da082014-05-06 08:32:40 +0000119 if self.spec.get('with_branch_heads', False):
120 sync_cmd.append('--with_branch_heads')
121 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000122
123 # Configure git.
124 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000125 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000126 print 'cd %s' % wd
agable@chromium.orgcc023502013-04-03 20:24:21 +0000127 self.run_git(
128 'submodule', 'foreach',
129 'git config -f $toplevel/.git/config submodule.$name.ignore all',
130 cwd=wd)
iannucci@chromium.orgf2fb5e72014-04-03 02:36:44 +0000131 self.run_git(
132 'config', '--add', 'remote.origin.fetch',
133 '+refs/tags/*:refs/tags/*', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000134 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
135
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000136
137class GclientGitSvnCheckout(GclientGitCheckout, SvnCheckout):
138
digit@chromium.org3596d582013-12-13 17:07:33 +0000139 def __init__(self, options, spec, root):
140 super(GclientGitSvnCheckout, self).__init__(options, spec, root)
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000141 assert 'svn_url' in self.spec
142 assert 'svn_branch' in self.spec
143 assert 'svn_ref' in self.spec
144
145 def init(self):
146 # Ensure we are authenticated with subversion for all submodules.
147 git_svn_dirs = json.loads(self.spec.get('submodule_git_svn_spec', '{}'))
148 git_svn_dirs.update({self.root: self.spec})
149 for _, svn_spec in git_svn_dirs.iteritems():
150 try:
151 self.run_svn('ls', '--non-interactive', svn_spec['svn_url'])
152 except subprocess.CalledProcessError:
153 print 'Please run `svn ls %s`' % svn_spec['svn_url']
154 return 1
155
156 super(GclientGitSvnCheckout, self).init()
157
agable@chromium.orgcc023502013-04-03 20:24:21 +0000158 # Configure git-svn.
agable@chromium.org2560ea72013-04-04 01:22:38 +0000159 for path, svn_spec in git_svn_dirs.iteritems():
160 real_path = os.path.join(*path.split('/'))
161 if real_path != self.root:
162 real_path = os.path.join(self.root, real_path)
163 wd = os.path.join(self.base, real_path)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000164 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000165 print 'cd %s' % wd
jochen@chromium.org048da082014-05-06 08:32:40 +0000166 prefix = svn_spec.get('svn_prefix', 'origin/')
167 self.run_git('svn', 'init', '--prefix=' + prefix, '-T',
agable@chromium.org2560ea72013-04-04 01:22:38 +0000168 svn_spec['svn_branch'], svn_spec['svn_url'], cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000169 self.run_git('config', '--replace', 'svn-remote.svn.fetch',
jochen@chromium.org048da082014-05-06 08:32:40 +0000170 svn_spec['svn_branch'] + ':refs/remotes/' + prefix +
agable@chromium.org2560ea72013-04-04 01:22:38 +0000171 svn_spec['svn_ref'], cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000172 self.run_git('svn', 'fetch', cwd=wd)
173
174
agable@chromium.org2560ea72013-04-04 01:22:38 +0000175
agable@chromium.orgcc023502013-04-03 20:24:21 +0000176CHECKOUT_TYPE_MAP = {
177 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000178 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000179 'gclient_git_svn': GclientGitSvnCheckout,
180 'git': GitCheckout,
181}
182
183
digit@chromium.org3596d582013-12-13 17:07:33 +0000184def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000185 """Factory to build Checkout class instances."""
186 class_ = CHECKOUT_TYPE_MAP.get(type_name)
187 if not class_:
188 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000189 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000190
191
192#################################################
193# Utility function and file entry point.
194#################################################
195def usage(msg=None):
196 """Print help and exit."""
197 if msg:
198 print 'Error:', msg
199
200 print (
201"""
digit@chromium.org3596d582013-12-13 17:07:33 +0000202usage: %s [options] <recipe> [--property=value [--property2=value2 ...]]
203
204This script can be used to download the Chromium sources. See
205http://www.chromium.org/developers/how-tos/get-the-code
206for full usage instructions.
207
208Valid options:
209 -h, --help, help Print this message.
210 --nohooks Don't run hooks after checkout.
wtc@chromium.org38e94612014-02-12 22:19:41 +0000211 -n, --dry-run Don't run commands, only print them.
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000212 --no-history Perform shallow clones, don't fetch the full git history.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000213""" % os.path.basename(sys.argv[0]))
214 sys.exit(bool(msg))
215
216
217def handle_args(argv):
218 """Gets the recipe name from the command line arguments."""
219 if len(argv) <= 1:
220 usage('Must specify a recipe.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000221 if argv[1] in ('-h', '--help', 'help'):
222 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000223
wtc@chromium.org38e94612014-02-12 22:19:41 +0000224 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000225 nohooks = False
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000226 no_history = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000227 while len(argv) >= 2:
228 arg = argv[1]
229 if not arg.startswith('-'):
230 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000231 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000232 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000233 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000234 elif arg == '--nohooks':
235 nohooks = True
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000236 elif arg == '--no-history':
237 no_history = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000238 else:
239 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000240
agable@chromium.orgcc023502013-04-03 20:24:21 +0000241 def looks_like_arg(arg):
242 return arg.startswith('--') and arg.count('=') == 1
243
244 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
245 if bad_parms:
246 usage('Got bad arguments %s' % bad_parms)
247
248 recipe = argv[1]
249 props = argv[2:]
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000250 return (
251 optparse.Values(
252 {'dry_run':dry_run, 'nohooks':nohooks, 'no_history': no_history }),
253 recipe,
254 props)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000255
256
257def run_recipe_fetch(recipe, props, aliased=False):
258 """Invoke a recipe's fetch method with the passed-through args
259 and return its json output as a python object."""
260 recipe_path = os.path.abspath(os.path.join(SCRIPT_PATH, 'recipes', recipe))
dpranke@chromium.orga992edb2013-04-03 21:22:20 +0000261 if not os.path.exists(recipe_path + '.py'):
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000262 print "Could not find a recipe for %s" % recipe
263 sys.exit(1)
264
agable@chromium.orgcc023502013-04-03 20:24:21 +0000265 cmd = [sys.executable, recipe_path + '.py', 'fetch'] + props
266 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000267
agable@chromium.orgcc023502013-04-03 20:24:21 +0000268 spec = json.loads(result)
269 if 'alias' in spec:
270 assert not aliased
271 return run_recipe_fetch(
272 spec['alias']['recipe'], spec['alias']['props'] + props, aliased=True)
273 cmd = [sys.executable, recipe_path + '.py', 'root']
274 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
275 root = json.loads(result)
276 return spec, root
277
278
digit@chromium.org3596d582013-12-13 17:07:33 +0000279def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000280 """Perform a checkout with the given type and configuration.
281
282 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000283 options: Options instance.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000284 spec: Checkout configuration returned by the the recipe's fetch_spec
285 method (checkout type, repository url, etc.).
286 root: The directory into which the repo expects to be checkout out.
287 """
288 assert 'type' in spec
289 checkout_type = spec['type']
290 checkout_spec = spec['%s_spec' % checkout_type]
291 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000292 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000293 except KeyError:
294 return 1
295 if checkout.exists():
dpranke@chromium.orgfd79e0d2013-04-12 21:34:32 +0000296 print 'You appear to already have a checkout. "fetch" is used only'
297 print 'to get new checkouts. Use "gclient sync" to update the checkout.'
298 print
299 print 'Fetch also does not yet deal with partial checkouts, so if fetch'
300 print 'failed, delete the checkout and start over (crbug.com/230691).'
agable@chromium.orgcc023502013-04-03 20:24:21 +0000301 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000302 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000303
304
305def main():
digit@chromium.org3596d582013-12-13 17:07:33 +0000306 options, recipe, props = handle_args(sys.argv)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000307 spec, root = run_recipe_fetch(recipe, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000308 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000309
310
311if __name__ == '__main__':
312 sys.exit(main())