blob: c78dfe722b9ca060075b647d8c1624c364375c66 [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
22import os
23import subprocess
24import sys
25import pipes
26
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000027from distutils import spawn
28
agable@chromium.orgcc023502013-04-03 20:24:21 +000029
30SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
31
32
33#################################################
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 """
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000046 def __init__(self, dryrun, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +000047 self.base = os.getcwd()
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000048 self.dryrun = dryrun
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))
63 if self.dryrun:
64 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'):
82 git_path = os.path.join(SCRIPT_PATH, 'git-1.8.0_bin', 'bin', 'git.exe')
83 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
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000100 def __init__(self, dryrun, spec, root):
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000101 super(GclientGitCheckout, self).__init__(dryrun, 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):
dpranke@chromium.org8623da32013-04-04 17:36:01 +0000112 # TODO(dpranke): Work around issues w/ delta compression on big repos.
113 self.run_git('config', '--global', 'core.deltaBaseCacheLimit', '1G')
114
agable@chromium.orgcc023502013-04-03 20:24:21 +0000115 # Configure and do the gclient checkout.
116 self.run_gclient('config', '--spec', self.spec['gclient_spec'])
117 self.run_gclient('sync')
118
119 # Configure git.
120 wd = os.path.join(self.base, self.root)
dpranke@chromium.org7ca51b32013-04-03 21:29:50 +0000121 if self.dryrun:
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
132 def __init__(self, dryrun, spec, root):
133 super(GclientGitSvnCheckout, self).__init__(dryrun, spec, root)
134 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)
dpranke@chromium.org7ca51b32013-04-03 21:29:50 +0000157 if self.dryrun:
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
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000176def CheckoutFactory(type_name, dryrun, 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)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000181 return class_(dryrun, 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"""
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000194usage: %s [-n|--dry-run] <recipe> [--property=value [--property2=value2 ...]]
agable@chromium.orgcc023502013-04-03 20:24:21 +0000195""" % os.path.basename(sys.argv[0]))
196 sys.exit(bool(msg))
197
198
199def handle_args(argv):
200 """Gets the recipe name from the command line arguments."""
201 if len(argv) <= 1:
202 usage('Must specify a recipe.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000203 if argv[1] in ('-h', '--help', 'help'):
204 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000205
dpranke@chromium.orga992edb2013-04-03 21:22:20 +0000206 dryrun = False
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000207 if argv[1] in ('-n', '--dry-run'):
208 dryrun = True
209 argv.pop(1)
210
agable@chromium.orgcc023502013-04-03 20:24:21 +0000211 def looks_like_arg(arg):
212 return arg.startswith('--') and arg.count('=') == 1
213
214 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
215 if bad_parms:
216 usage('Got bad arguments %s' % bad_parms)
217
218 recipe = argv[1]
219 props = argv[2:]
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000220 return dryrun, recipe, props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000221
222
223def run_recipe_fetch(recipe, props, aliased=False):
224 """Invoke a recipe's fetch method with the passed-through args
225 and return its json output as a python object."""
226 recipe_path = os.path.abspath(os.path.join(SCRIPT_PATH, 'recipes', recipe))
dpranke@chromium.orga992edb2013-04-03 21:22:20 +0000227 if not os.path.exists(recipe_path + '.py'):
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000228 print "Could not find a recipe for %s" % recipe
229 sys.exit(1)
230
agable@chromium.orgcc023502013-04-03 20:24:21 +0000231 cmd = [sys.executable, recipe_path + '.py', 'fetch'] + props
232 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000233
agable@chromium.orgcc023502013-04-03 20:24:21 +0000234 spec = json.loads(result)
235 if 'alias' in spec:
236 assert not aliased
237 return run_recipe_fetch(
238 spec['alias']['recipe'], spec['alias']['props'] + props, aliased=True)
239 cmd = [sys.executable, recipe_path + '.py', 'root']
240 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
241 root = json.loads(result)
242 return spec, root
243
244
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000245def run(dryrun, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000246 """Perform a checkout with the given type and configuration.
247
248 Args:
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000249 dryrun: if True, don't actually execute the commands
agable@chromium.orgcc023502013-04-03 20:24:21 +0000250 spec: Checkout configuration returned by the the recipe's fetch_spec
251 method (checkout type, repository url, etc.).
252 root: The directory into which the repo expects to be checkout out.
253 """
254 assert 'type' in spec
255 checkout_type = spec['type']
256 checkout_spec = spec['%s_spec' % checkout_type]
257 try:
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000258 checkout = CheckoutFactory(checkout_type, dryrun, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000259 except KeyError:
260 return 1
261 if checkout.exists():
dpranke@chromium.orgfd79e0d2013-04-12 21:34:32 +0000262 print 'You appear to already have a checkout. "fetch" is used only'
263 print 'to get new checkouts. Use "gclient sync" to update the checkout.'
264 print
265 print 'Fetch also does not yet deal with partial checkouts, so if fetch'
266 print 'failed, delete the checkout and start over (crbug.com/230691).'
agable@chromium.orgcc023502013-04-03 20:24:21 +0000267 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000268 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000269
270
271def main():
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000272 dryrun, recipe, props = handle_args(sys.argv)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000273 spec, root = run_recipe_fetch(recipe, props)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000274 return run(dryrun, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000275
276
277if __name__ == '__main__':
278 sys.exit(main())