blob: f5640cc95a2ea0a70df6788732842f8fa76b328b [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
104 keys = ['solutions', 'target_os', 'target_os_only']
105 gclient_spec = '\n'.join('%s = %s' % (key, self.spec[key])
janx@chromium.orgc4209082013-04-18 18:33:52 +0000106 for key in keys if key in self.spec)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000107 self.spec['gclient_spec'] = gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000108
109 def exists(self):
110 return os.path.exists(os.path.join(os.getcwd(), self.root))
111
112 def init(self):
113 # Configure and do the gclient checkout.
114 self.run_gclient('config', '--spec', self.spec['gclient_spec'])
jochen@chromium.org048da082014-05-06 08:32:40 +0000115 sync_cmd = ['sync']
digit@chromium.org3596d582013-12-13 17:07:33 +0000116 if self.options.nohooks:
jochen@chromium.org048da082014-05-06 08:32:40 +0000117 sync_cmd.append('--nohooks')
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000118 if self.options.no_history:
119 sync_cmd.append('--no-history')
jochen@chromium.org048da082014-05-06 08:32:40 +0000120 if self.spec.get('with_branch_heads', False):
121 sync_cmd.append('--with_branch_heads')
122 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000123
124 # Configure git.
125 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000126 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000127 print 'cd %s' % wd
agable@chromium.orgcc023502013-04-03 20:24:21 +0000128 self.run_git(
129 'submodule', 'foreach',
130 'git config -f $toplevel/.git/config submodule.$name.ignore all',
131 cwd=wd)
iannucci@chromium.orgf2fb5e72014-04-03 02:36:44 +0000132 self.run_git(
133 'config', '--add', 'remote.origin.fetch',
134 '+refs/tags/*:refs/tags/*', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000135 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
136
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000137
138class GclientGitSvnCheckout(GclientGitCheckout, SvnCheckout):
139
digit@chromium.org3596d582013-12-13 17:07:33 +0000140 def __init__(self, options, spec, root):
141 super(GclientGitSvnCheckout, self).__init__(options, spec, root)
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000142 assert 'svn_url' in self.spec
143 assert 'svn_branch' in self.spec
144 assert 'svn_ref' in self.spec
145
146 def init(self):
147 # Ensure we are authenticated with subversion for all submodules.
148 git_svn_dirs = json.loads(self.spec.get('submodule_git_svn_spec', '{}'))
149 git_svn_dirs.update({self.root: self.spec})
150 for _, svn_spec in git_svn_dirs.iteritems():
151 try:
152 self.run_svn('ls', '--non-interactive', svn_spec['svn_url'])
153 except subprocess.CalledProcessError:
154 print 'Please run `svn ls %s`' % svn_spec['svn_url']
155 return 1
156
157 super(GclientGitSvnCheckout, self).init()
158
agable@chromium.orgcc023502013-04-03 20:24:21 +0000159 # Configure git-svn.
agable@chromium.org2560ea72013-04-04 01:22:38 +0000160 for path, svn_spec in git_svn_dirs.iteritems():
161 real_path = os.path.join(*path.split('/'))
162 if real_path != self.root:
163 real_path = os.path.join(self.root, real_path)
164 wd = os.path.join(self.base, real_path)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000165 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000166 print 'cd %s' % wd
jochen@chromium.org048da082014-05-06 08:32:40 +0000167 prefix = svn_spec.get('svn_prefix', 'origin/')
168 self.run_git('svn', 'init', '--prefix=' + prefix, '-T',
agable@chromium.org2560ea72013-04-04 01:22:38 +0000169 svn_spec['svn_branch'], svn_spec['svn_url'], cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000170 self.run_git('config', '--replace', 'svn-remote.svn.fetch',
jochen@chromium.org048da082014-05-06 08:32:40 +0000171 svn_spec['svn_branch'] + ':refs/remotes/' + prefix +
agable@chromium.org2560ea72013-04-04 01:22:38 +0000172 svn_spec['svn_ref'], cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000173 self.run_git('svn', 'fetch', cwd=wd)
174
175
agable@chromium.org2560ea72013-04-04 01:22:38 +0000176
agable@chromium.orgcc023502013-04-03 20:24:21 +0000177CHECKOUT_TYPE_MAP = {
178 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000179 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000180 'gclient_git_svn': GclientGitSvnCheckout,
181 'git': GitCheckout,
182}
183
184
digit@chromium.org3596d582013-12-13 17:07:33 +0000185def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000186 """Factory to build Checkout class instances."""
187 class_ = CHECKOUT_TYPE_MAP.get(type_name)
188 if not class_:
189 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000190 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000191
192
193#################################################
194# Utility function and file entry point.
195#################################################
196def usage(msg=None):
197 """Print help and exit."""
198 if msg:
199 print 'Error:', msg
200
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000201 print textwrap.dedent("""\
202 usage: %s [options] <recipe> [--property=value [--property2=value2 ...]]
digit@chromium.org3596d582013-12-13 17:07:33 +0000203
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000204 This script can be used to download the Chromium sources. See
205 http://www.chromium.org/developers/how-tos/get-the-code
206 for full usage instructions.
digit@chromium.org3596d582013-12-13 17:07:33 +0000207
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000208 Valid options:
209 -h, --help, help Print this message.
210 --nohooks Don't run hooks after checkout.
211 -n, --dry-run Don't run commands, only print them.
212 --no-history Perform shallow clones, don't fetch the full git history.
213
214 Valid fetch recipes:""") % os.path.basename(sys.argv[0])
215 for fname in os.listdir(os.path.join(SCRIPT_PATH, 'recipes')):
216 if fname.endswith('.py'):
217 print ' ' + fname[:-3]
218
agable@chromium.orgcc023502013-04-03 20:24:21 +0000219 sys.exit(bool(msg))
220
221
222def handle_args(argv):
223 """Gets the recipe name from the command line arguments."""
224 if len(argv) <= 1:
225 usage('Must specify a recipe.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000226 if argv[1] in ('-h', '--help', 'help'):
227 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000228
wtc@chromium.org38e94612014-02-12 22:19:41 +0000229 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000230 nohooks = False
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000231 no_history = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000232 while len(argv) >= 2:
233 arg = argv[1]
234 if not arg.startswith('-'):
235 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000236 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000237 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000238 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000239 elif arg == '--nohooks':
240 nohooks = True
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000241 elif arg == '--no-history':
242 no_history = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000243 else:
244 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000245
agable@chromium.orgcc023502013-04-03 20:24:21 +0000246 def looks_like_arg(arg):
247 return arg.startswith('--') and arg.count('=') == 1
248
249 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
250 if bad_parms:
251 usage('Got bad arguments %s' % bad_parms)
252
253 recipe = argv[1]
254 props = argv[2:]
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000255 return (
256 optparse.Values(
257 {'dry_run':dry_run, 'nohooks':nohooks, 'no_history': no_history }),
258 recipe,
259 props)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000260
261
262def run_recipe_fetch(recipe, props, aliased=False):
263 """Invoke a recipe's fetch method with the passed-through args
264 and return its json output as a python object."""
265 recipe_path = os.path.abspath(os.path.join(SCRIPT_PATH, 'recipes', recipe))
dpranke@chromium.orga992edb2013-04-03 21:22:20 +0000266 if not os.path.exists(recipe_path + '.py'):
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000267 print "Could not find a recipe for %s" % recipe
268 sys.exit(1)
269
agable@chromium.orgcc023502013-04-03 20:24:21 +0000270 cmd = [sys.executable, recipe_path + '.py', 'fetch'] + props
271 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000272
agable@chromium.orgcc023502013-04-03 20:24:21 +0000273 spec = json.loads(result)
274 if 'alias' in spec:
275 assert not aliased
276 return run_recipe_fetch(
277 spec['alias']['recipe'], spec['alias']['props'] + props, aliased=True)
278 cmd = [sys.executable, recipe_path + '.py', 'root']
279 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
280 root = json.loads(result)
281 return spec, root
282
283
digit@chromium.org3596d582013-12-13 17:07:33 +0000284def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000285 """Perform a checkout with the given type and configuration.
286
287 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000288 options: Options instance.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000289 spec: Checkout configuration returned by the the recipe's fetch_spec
290 method (checkout type, repository url, etc.).
291 root: The directory into which the repo expects to be checkout out.
292 """
293 assert 'type' in spec
294 checkout_type = spec['type']
295 checkout_spec = spec['%s_spec' % checkout_type]
296 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000297 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000298 except KeyError:
299 return 1
300 if checkout.exists():
dpranke@chromium.orgfd79e0d2013-04-12 21:34:32 +0000301 print 'You appear to already have a checkout. "fetch" is used only'
302 print 'to get new checkouts. Use "gclient sync" to update the checkout.'
303 print
304 print 'Fetch also does not yet deal with partial checkouts, so if fetch'
305 print 'failed, delete the checkout and start over (crbug.com/230691).'
agable@chromium.orgcc023502013-04-03 20:24:21 +0000306 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000307 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000308
309
310def main():
digit@chromium.org3596d582013-12-13 17:07:33 +0000311 options, recipe, props = handle_args(sys.argv)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000312 spec, root = run_recipe_fetch(recipe, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000313 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000314
315
316if __name__ == '__main__':
317 sys.exit(main())