blob: 5d595f45aac6711407c7fc4250f303d8f0917dd7 [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'])
digit@chromium.org3596d582013-12-13 17:07:33 +0000114 if self.options.nohooks:
115 self.run_gclient('sync', '--nohooks')
116 else:
117 self.run_gclient('sync')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000118
119 # Configure git.
120 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000121 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000122 print 'cd %s' % wd
agable@chromium.orgcc023502013-04-03 20:24:21 +0000123 self.run_git(
124 'submodule', 'foreach',
125 'git config -f $toplevel/.git/config submodule.$name.ignore all',
126 cwd=wd)
iannucci@chromium.orgf2fb5e72014-04-03 02:36:44 +0000127 self.run_git(
128 'config', '--add', 'remote.origin.fetch',
129 '+refs/tags/*:refs/tags/*', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000130 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
131
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000132
133class GclientGitSvnCheckout(GclientGitCheckout, SvnCheckout):
134
digit@chromium.org3596d582013-12-13 17:07:33 +0000135 def __init__(self, options, spec, root):
136 super(GclientGitSvnCheckout, self).__init__(options, spec, root)
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000137 assert 'svn_url' in self.spec
138 assert 'svn_branch' in self.spec
139 assert 'svn_ref' in self.spec
140
141 def init(self):
142 # Ensure we are authenticated with subversion for all submodules.
143 git_svn_dirs = json.loads(self.spec.get('submodule_git_svn_spec', '{}'))
144 git_svn_dirs.update({self.root: self.spec})
145 for _, svn_spec in git_svn_dirs.iteritems():
146 try:
147 self.run_svn('ls', '--non-interactive', svn_spec['svn_url'])
148 except subprocess.CalledProcessError:
149 print 'Please run `svn ls %s`' % svn_spec['svn_url']
150 return 1
151
152 super(GclientGitSvnCheckout, self).init()
153
agable@chromium.orgcc023502013-04-03 20:24:21 +0000154 # Configure git-svn.
agable@chromium.org2560ea72013-04-04 01:22:38 +0000155 for path, svn_spec in git_svn_dirs.iteritems():
156 real_path = os.path.join(*path.split('/'))
157 if real_path != self.root:
158 real_path = os.path.join(self.root, real_path)
159 wd = os.path.join(self.base, real_path)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000160 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000161 print 'cd %s' % wd
agable@chromium.orgcc023502013-04-03 20:24:21 +0000162 self.run_git('svn', 'init', '--prefix=origin/', '-T',
agable@chromium.org2560ea72013-04-04 01:22:38 +0000163 svn_spec['svn_branch'], svn_spec['svn_url'], cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000164 self.run_git('config', '--replace', 'svn-remote.svn.fetch',
agable@chromium.org2560ea72013-04-04 01:22:38 +0000165 svn_spec['svn_branch'] + ':refs/remotes/origin/' +
166 svn_spec['svn_ref'], cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000167 self.run_git('svn', 'fetch', cwd=wd)
168
169
agable@chromium.org2560ea72013-04-04 01:22:38 +0000170
agable@chromium.orgcc023502013-04-03 20:24:21 +0000171CHECKOUT_TYPE_MAP = {
172 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000173 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000174 'gclient_git_svn': GclientGitSvnCheckout,
175 'git': GitCheckout,
176}
177
178
digit@chromium.org3596d582013-12-13 17:07:33 +0000179def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000180 """Factory to build Checkout class instances."""
181 class_ = CHECKOUT_TYPE_MAP.get(type_name)
182 if not class_:
183 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000184 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000185
186
187#################################################
188# Utility function and file entry point.
189#################################################
190def usage(msg=None):
191 """Print help and exit."""
192 if msg:
193 print 'Error:', msg
194
195 print (
196"""
digit@chromium.org3596d582013-12-13 17:07:33 +0000197usage: %s [options] <recipe> [--property=value [--property2=value2 ...]]
198
199This script can be used to download the Chromium sources. See
200http://www.chromium.org/developers/how-tos/get-the-code
201for full usage instructions.
202
203Valid options:
204 -h, --help, help Print this message.
205 --nohooks Don't run hooks after checkout.
wtc@chromium.org38e94612014-02-12 22:19:41 +0000206 -n, --dry-run Don't run commands, only print them.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000207""" % os.path.basename(sys.argv[0]))
208 sys.exit(bool(msg))
209
210
211def handle_args(argv):
212 """Gets the recipe name from the command line arguments."""
213 if len(argv) <= 1:
214 usage('Must specify a recipe.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000215 if argv[1] in ('-h', '--help', 'help'):
216 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000217
wtc@chromium.org38e94612014-02-12 22:19:41 +0000218 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000219 nohooks = False
220 while len(argv) >= 2:
221 arg = argv[1]
222 if not arg.startswith('-'):
223 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000224 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000225 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000226 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000227 elif arg == '--nohooks':
228 nohooks = True
229 else:
230 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000231
agable@chromium.orgcc023502013-04-03 20:24:21 +0000232 def looks_like_arg(arg):
233 return arg.startswith('--') and arg.count('=') == 1
234
235 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
236 if bad_parms:
237 usage('Got bad arguments %s' % bad_parms)
238
239 recipe = argv[1]
240 props = argv[2:]
wtc@chromium.org38e94612014-02-12 22:19:41 +0000241 return optparse.Values({'dry_run':dry_run, 'nohooks':nohooks }), recipe, props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000242
243
244def run_recipe_fetch(recipe, props, aliased=False):
245 """Invoke a recipe's fetch method with the passed-through args
246 and return its json output as a python object."""
247 recipe_path = os.path.abspath(os.path.join(SCRIPT_PATH, 'recipes', recipe))
dpranke@chromium.orga992edb2013-04-03 21:22:20 +0000248 if not os.path.exists(recipe_path + '.py'):
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000249 print "Could not find a recipe for %s" % recipe
250 sys.exit(1)
251
agable@chromium.orgcc023502013-04-03 20:24:21 +0000252 cmd = [sys.executable, recipe_path + '.py', 'fetch'] + props
253 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000254
agable@chromium.orgcc023502013-04-03 20:24:21 +0000255 spec = json.loads(result)
256 if 'alias' in spec:
257 assert not aliased
258 return run_recipe_fetch(
259 spec['alias']['recipe'], spec['alias']['props'] + props, aliased=True)
260 cmd = [sys.executable, recipe_path + '.py', 'root']
261 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
262 root = json.loads(result)
263 return spec, root
264
265
digit@chromium.org3596d582013-12-13 17:07:33 +0000266def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000267 """Perform a checkout with the given type and configuration.
268
269 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000270 options: Options instance.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000271 spec: Checkout configuration returned by the the recipe's fetch_spec
272 method (checkout type, repository url, etc.).
273 root: The directory into which the repo expects to be checkout out.
274 """
275 assert 'type' in spec
276 checkout_type = spec['type']
277 checkout_spec = spec['%s_spec' % checkout_type]
278 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000279 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000280 except KeyError:
281 return 1
282 if checkout.exists():
dpranke@chromium.orgfd79e0d2013-04-12 21:34:32 +0000283 print 'You appear to already have a checkout. "fetch" is used only'
284 print 'to get new checkouts. Use "gclient sync" to update the checkout.'
285 print
286 print 'Fetch also does not yet deal with partial checkouts, so if fetch'
287 print 'failed, delete the checkout and start over (crbug.com/230691).'
agable@chromium.orgcc023502013-04-03 20:24:21 +0000288 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000289 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000290
291
292def main():
digit@chromium.org3596d582013-12-13 17:07:33 +0000293 options, recipe, props = handle_args(sys.argv)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000294 spec, root = run_recipe_fetch(recipe, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000295 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000296
297
298if __name__ == '__main__':
299 sys.exit(main())