blob: 68795587480cf508bf9c91f78a9058c2cd68d72d [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])
105 for key in self.spec if key in keys)
106 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():
262 print 'You appear to already have this checkout.'
263 print 'Aborting to avoid clobbering your work.'
264 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000265 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000266
267
268def main():
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000269 dryrun, recipe, props = handle_args(sys.argv)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000270 spec, root = run_recipe_fetch(recipe, props)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000271 return run(dryrun, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000272
273
274if __name__ == '__main__':
275 sys.exit(main())