blob: e579f916ed4c32411bcc8eefd9e1d96aeaff3166 [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
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():
agable@chromium.org14f633b2014-10-22 10:35:33 +0000148 if svn_spec.get('svn_url'):
149 try:
150 self.run_svn('ls', '--non-interactive', svn_spec['svn_url'])
151 except subprocess.CalledProcessError:
152 print 'Please run `svn ls %s`' % svn_spec['svn_url']
153 return 1
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000154
155 super(GclientGitSvnCheckout, self).init()
156
agable@chromium.orgcc023502013-04-03 20:24:21 +0000157 # Configure git-svn.
agable@chromium.org2560ea72013-04-04 01:22:38 +0000158 for path, svn_spec in git_svn_dirs.iteritems():
159 real_path = os.path.join(*path.split('/'))
160 if real_path != self.root:
161 real_path = os.path.join(self.root, real_path)
162 wd = os.path.join(self.base, real_path)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000163 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000164 print 'cd %s' % wd
agable@chromium.org14f633b2014-10-22 10:35:33 +0000165 if svn_spec.get('auto'):
166 self.run_git('auto-svn', cwd=wd)
167 continue
168 self.run_git('svn', 'init', svn_spec['svn_url'], cwd=wd)
169 self.run_git('config', '--unset-all', 'svn-remote.svn.fetch', cwd=wd)
170 for svn_branch, git_ref in svn_spec.get('git_svn_fetch', {}).items():
171 self.run_git('config', '--add', 'svn-remote.svn.fetch',
172 '%s:%s' % (svn_branch, git_ref), cwd=wd)
173 for svn_branch, git_ref in svn_spec.get('git_svn_branches', {}).items():
174 self.run_git('config', '--add', 'svn-remote.svn.branches',
175 '%s:%s' % (svn_branch, git_ref), cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000176 self.run_git('svn', 'fetch', cwd=wd)
177
178
agable@chromium.org2560ea72013-04-04 01:22:38 +0000179
agable@chromium.orgcc023502013-04-03 20:24:21 +0000180CHECKOUT_TYPE_MAP = {
181 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000182 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000183 'gclient_git_svn': GclientGitSvnCheckout,
184 'git': GitCheckout,
185}
186
187
digit@chromium.org3596d582013-12-13 17:07:33 +0000188def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000189 """Factory to build Checkout class instances."""
190 class_ = CHECKOUT_TYPE_MAP.get(type_name)
191 if not class_:
192 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000193 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000194
195
196#################################################
197# Utility function and file entry point.
198#################################################
199def usage(msg=None):
200 """Print help and exit."""
201 if msg:
202 print 'Error:', msg
203
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000204 print textwrap.dedent("""\
205 usage: %s [options] <recipe> [--property=value [--property2=value2 ...]]
digit@chromium.org3596d582013-12-13 17:07:33 +0000206
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000207 This script can be used to download the Chromium sources. See
208 http://www.chromium.org/developers/how-tos/get-the-code
209 for full usage instructions.
digit@chromium.org3596d582013-12-13 17:07:33 +0000210
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000211 Valid options:
212 -h, --help, help Print this message.
213 --nohooks Don't run hooks after checkout.
214 -n, --dry-run Don't run commands, only print them.
215 --no-history Perform shallow clones, don't fetch the full git history.
216
217 Valid fetch recipes:""") % os.path.basename(sys.argv[0])
218 for fname in os.listdir(os.path.join(SCRIPT_PATH, 'recipes')):
219 if fname.endswith('.py'):
220 print ' ' + fname[:-3]
221
agable@chromium.orgcc023502013-04-03 20:24:21 +0000222 sys.exit(bool(msg))
223
224
225def handle_args(argv):
226 """Gets the recipe name from the command line arguments."""
227 if len(argv) <= 1:
228 usage('Must specify a recipe.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000229 if argv[1] in ('-h', '--help', 'help'):
230 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000231
wtc@chromium.org38e94612014-02-12 22:19:41 +0000232 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000233 nohooks = False
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000234 no_history = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000235 while len(argv) >= 2:
236 arg = argv[1]
237 if not arg.startswith('-'):
238 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000239 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000240 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000241 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000242 elif arg == '--nohooks':
243 nohooks = True
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000244 elif arg == '--no-history':
245 no_history = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000246 else:
247 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000248
agable@chromium.orgcc023502013-04-03 20:24:21 +0000249 def looks_like_arg(arg):
250 return arg.startswith('--') and arg.count('=') == 1
251
252 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
253 if bad_parms:
254 usage('Got bad arguments %s' % bad_parms)
255
256 recipe = argv[1]
257 props = argv[2:]
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000258 return (
259 optparse.Values(
260 {'dry_run':dry_run, 'nohooks':nohooks, 'no_history': no_history }),
261 recipe,
262 props)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000263
264
265def run_recipe_fetch(recipe, props, aliased=False):
266 """Invoke a recipe's fetch method with the passed-through args
267 and return its json output as a python object."""
268 recipe_path = os.path.abspath(os.path.join(SCRIPT_PATH, 'recipes', recipe))
dpranke@chromium.orga992edb2013-04-03 21:22:20 +0000269 if not os.path.exists(recipe_path + '.py'):
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000270 print "Could not find a recipe for %s" % recipe
271 sys.exit(1)
272
agable@chromium.orgcc023502013-04-03 20:24:21 +0000273 cmd = [sys.executable, recipe_path + '.py', 'fetch'] + props
274 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000275
agable@chromium.orgcc023502013-04-03 20:24:21 +0000276 spec = json.loads(result)
277 if 'alias' in spec:
278 assert not aliased
279 return run_recipe_fetch(
280 spec['alias']['recipe'], spec['alias']['props'] + props, aliased=True)
281 cmd = [sys.executable, recipe_path + '.py', 'root']
282 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
283 root = json.loads(result)
284 return spec, root
285
286
digit@chromium.org3596d582013-12-13 17:07:33 +0000287def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000288 """Perform a checkout with the given type and configuration.
289
290 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000291 options: Options instance.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000292 spec: Checkout configuration returned by the the recipe's fetch_spec
293 method (checkout type, repository url, etc.).
294 root: The directory into which the repo expects to be checkout out.
295 """
296 assert 'type' in spec
297 checkout_type = spec['type']
298 checkout_spec = spec['%s_spec' % checkout_type]
299 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000300 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000301 except KeyError:
302 return 1
303 if checkout.exists():
dpranke@chromium.orgfd79e0d2013-04-12 21:34:32 +0000304 print 'You appear to already have a checkout. "fetch" is used only'
305 print 'to get new checkouts. Use "gclient sync" to update the checkout.'
306 print
307 print 'Fetch also does not yet deal with partial checkouts, so if fetch'
308 print 'failed, delete the checkout and start over (crbug.com/230691).'
agable@chromium.orgcc023502013-04-03 20:24:21 +0000309 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000310 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000311
312
313def main():
digit@chromium.org3596d582013-12-13 17:07:33 +0000314 options, recipe, props = handle_args(sys.argv)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000315 spec, root = run_recipe_fetch(recipe, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000316 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000317
318
319if __name__ == '__main__':
320 sys.exit(main())