blob: dfd038641b7d18cf17b0c5aa55eefc6b35502482 [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)
127 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
128
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000129
130class GclientGitSvnCheckout(GclientGitCheckout, SvnCheckout):
131
digit@chromium.org3596d582013-12-13 17:07:33 +0000132 def __init__(self, options, spec, root):
133 super(GclientGitSvnCheckout, self).__init__(options, spec, root)
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000134 assert 'svn_url' in self.spec
135 assert 'svn_branch' in self.spec
136 assert 'svn_ref' in self.spec
137
138 def init(self):
139 # Ensure we are authenticated with subversion for all submodules.
140 git_svn_dirs = json.loads(self.spec.get('submodule_git_svn_spec', '{}'))
141 git_svn_dirs.update({self.root: self.spec})
142 for _, svn_spec in git_svn_dirs.iteritems():
143 try:
144 self.run_svn('ls', '--non-interactive', svn_spec['svn_url'])
145 except subprocess.CalledProcessError:
146 print 'Please run `svn ls %s`' % svn_spec['svn_url']
147 return 1
148
149 super(GclientGitSvnCheckout, self).init()
150
agable@chromium.orgcc023502013-04-03 20:24:21 +0000151 # Configure git-svn.
agable@chromium.org2560ea72013-04-04 01:22:38 +0000152 for path, svn_spec in git_svn_dirs.iteritems():
153 real_path = os.path.join(*path.split('/'))
154 if real_path != self.root:
155 real_path = os.path.join(self.root, real_path)
156 wd = os.path.join(self.base, real_path)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000157 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000158 print 'cd %s' % wd
agable@chromium.orgcc023502013-04-03 20:24:21 +0000159 self.run_git('svn', 'init', '--prefix=origin/', '-T',
agable@chromium.org2560ea72013-04-04 01:22:38 +0000160 svn_spec['svn_branch'], svn_spec['svn_url'], cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000161 self.run_git('config', '--replace', 'svn-remote.svn.fetch',
agable@chromium.org2560ea72013-04-04 01:22:38 +0000162 svn_spec['svn_branch'] + ':refs/remotes/origin/' +
163 svn_spec['svn_ref'], cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000164 self.run_git('svn', 'fetch', cwd=wd)
165
166
agable@chromium.org2560ea72013-04-04 01:22:38 +0000167
agable@chromium.orgcc023502013-04-03 20:24:21 +0000168CHECKOUT_TYPE_MAP = {
169 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000170 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000171 'gclient_git_svn': GclientGitSvnCheckout,
172 'git': GitCheckout,
173}
174
175
digit@chromium.org3596d582013-12-13 17:07:33 +0000176def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000177 """Factory to build Checkout class instances."""
178 class_ = CHECKOUT_TYPE_MAP.get(type_name)
179 if not class_:
180 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000181 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000182
183
184#################################################
185# Utility function and file entry point.
186#################################################
187def usage(msg=None):
188 """Print help and exit."""
189 if msg:
190 print 'Error:', msg
191
192 print (
193"""
digit@chromium.org3596d582013-12-13 17:07:33 +0000194usage: %s [options] <recipe> [--property=value [--property2=value2 ...]]
195
196This script can be used to download the Chromium sources. See
197http://www.chromium.org/developers/how-tos/get-the-code
198for full usage instructions.
199
200Valid options:
201 -h, --help, help Print this message.
202 --nohooks Don't run hooks after checkout.
wtc@chromium.org38e94612014-02-12 22:19:41 +0000203 -n, --dry-run Don't run commands, only print them.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000204""" % os.path.basename(sys.argv[0]))
205 sys.exit(bool(msg))
206
207
208def handle_args(argv):
209 """Gets the recipe name from the command line arguments."""
210 if len(argv) <= 1:
211 usage('Must specify a recipe.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000212 if argv[1] in ('-h', '--help', 'help'):
213 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000214
wtc@chromium.org38e94612014-02-12 22:19:41 +0000215 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000216 nohooks = False
217 while len(argv) >= 2:
218 arg = argv[1]
219 if not arg.startswith('-'):
220 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000221 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000222 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000223 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000224 elif arg == '--nohooks':
225 nohooks = True
226 else:
227 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000228
agable@chromium.orgcc023502013-04-03 20:24:21 +0000229 def looks_like_arg(arg):
230 return arg.startswith('--') and arg.count('=') == 1
231
232 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
233 if bad_parms:
234 usage('Got bad arguments %s' % bad_parms)
235
236 recipe = argv[1]
237 props = argv[2:]
wtc@chromium.org38e94612014-02-12 22:19:41 +0000238 return optparse.Values({'dry_run':dry_run, 'nohooks':nohooks }), recipe, props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000239
240
241def run_recipe_fetch(recipe, props, aliased=False):
242 """Invoke a recipe's fetch method with the passed-through args
243 and return its json output as a python object."""
244 recipe_path = os.path.abspath(os.path.join(SCRIPT_PATH, 'recipes', recipe))
dpranke@chromium.orga992edb2013-04-03 21:22:20 +0000245 if not os.path.exists(recipe_path + '.py'):
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000246 print "Could not find a recipe for %s" % recipe
247 sys.exit(1)
248
agable@chromium.orgcc023502013-04-03 20:24:21 +0000249 cmd = [sys.executable, recipe_path + '.py', 'fetch'] + props
250 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000251
agable@chromium.orgcc023502013-04-03 20:24:21 +0000252 spec = json.loads(result)
253 if 'alias' in spec:
254 assert not aliased
255 return run_recipe_fetch(
256 spec['alias']['recipe'], spec['alias']['props'] + props, aliased=True)
257 cmd = [sys.executable, recipe_path + '.py', 'root']
258 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
259 root = json.loads(result)
260 return spec, root
261
262
digit@chromium.org3596d582013-12-13 17:07:33 +0000263def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000264 """Perform a checkout with the given type and configuration.
265
266 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000267 options: Options instance.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000268 spec: Checkout configuration returned by the the recipe's fetch_spec
269 method (checkout type, repository url, etc.).
270 root: The directory into which the repo expects to be checkout out.
271 """
272 assert 'type' in spec
273 checkout_type = spec['type']
274 checkout_spec = spec['%s_spec' % checkout_type]
275 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000276 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000277 except KeyError:
278 return 1
279 if checkout.exists():
dpranke@chromium.orgfd79e0d2013-04-12 21:34:32 +0000280 print 'You appear to already have a checkout. "fetch" is used only'
281 print 'to get new checkouts. Use "gclient sync" to update the checkout.'
282 print
283 print 'Fetch also does not yet deal with partial checkouts, so if fetch'
284 print 'failed, delete the checkout and start over (crbug.com/230691).'
agable@chromium.orgcc023502013-04-03 20:24:21 +0000285 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000286 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000287
288
289def main():
digit@chromium.org3596d582013-12-13 17:07:33 +0000290 options, recipe, props = handle_args(sys.argv)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000291 spec, root = run_recipe_fetch(recipe, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000292 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000293
294
295if __name__ == '__main__':
296 sys.exit(main())