blob: 1e5d688f0b177f1d3dcf19c0ba8d4260472d84fa [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
27
28SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
29
30
31#################################################
32# Checkout class definitions.
33#################################################
34class Checkout(object):
35 """Base class for implementing different types of checkouts.
36
37 Attributes:
38 |base|: the absolute path of the directory in which this script is run.
39 |spec|: the spec for this checkout as returned by the recipe. Different
40 subclasses will expect different keys in this dictionary.
41 |root|: the directory into which the checkout will be performed, as returned
42 by the recipe. This is a relative path from |base|.
43 """
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000044 def __init__(self, dryrun, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +000045 self.base = os.getcwd()
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000046 self.dryrun = dryrun
agable@chromium.orgcc023502013-04-03 20:24:21 +000047 self.spec = spec
48 self.root = root
49
50 def exists(self):
51 pass
52
53 def init(self):
54 pass
55
56 def sync(self):
57 pass
58
59
60class GclientCheckout(Checkout):
61
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000062 def run_gclient(self, *cmd, **kwargs):
agable@chromium.orgcc023502013-04-03 20:24:21 +000063 print 'Running: gclient %s' % ' '.join(pipes.quote(x) for x in cmd)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000064 if not self.dryrun:
dpranke@chromium.org6b8c91a2013-04-03 22:05:06 +000065 return subprocess.check_call(
66 (sys.executable, os.path.join(SCRIPT_PATH, 'gclient.py')) + cmd,
67 **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000068
69
70class GitCheckout(Checkout):
71
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000072 def run_git(self, *cmd, **kwargs):
agable@chromium.orgcc023502013-04-03 20:24:21 +000073 print 'Running: git %s' % ' '.join(pipes.quote(x) for x in cmd)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000074 if not self.dryrun:
75 return subprocess.check_call(('git',) + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000076
77
agable@chromium.org2560ea72013-04-04 01:22:38 +000078class SvnCheckout(Checkout):
79
80 def run_svn(self, *cmd, **kwargs):
81 print 'Running: svn %s' % ' '.join(pipes.quote(x) for x in cmd)
82 if not self.dryrun:
83 return subprocess.check_call(('svn',) + cmd, **kwargs)
84
85
jochen@chromium.orgd993e782013-04-11 20:03:13 +000086class GclientGitCheckout(GclientCheckout, GitCheckout):
agable@chromium.orgcc023502013-04-03 20:24:21 +000087
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000088 def __init__(self, dryrun, spec, root):
jochen@chromium.orgd993e782013-04-11 20:03:13 +000089 super(GclientGitCheckout, self).__init__(dryrun, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +000090 assert 'solutions' in self.spec
91 keys = ['solutions', 'target_os', 'target_os_only']
92 gclient_spec = '\n'.join('%s = %s' % (key, self.spec[key])
93 for key in self.spec if key in keys)
94 self.spec['gclient_spec'] = gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +000095
96 def exists(self):
97 return os.path.exists(os.path.join(os.getcwd(), self.root))
98
99 def init(self):
dpranke@chromium.org8623da32013-04-04 17:36:01 +0000100 # TODO(dpranke): Work around issues w/ delta compression on big repos.
101 self.run_git('config', '--global', 'core.deltaBaseCacheLimit', '1G')
102
agable@chromium.orgcc023502013-04-03 20:24:21 +0000103 # Configure and do the gclient checkout.
104 self.run_gclient('config', '--spec', self.spec['gclient_spec'])
105 self.run_gclient('sync')
106
107 # Configure git.
108 wd = os.path.join(self.base, self.root)
dpranke@chromium.org7ca51b32013-04-03 21:29:50 +0000109 if self.dryrun:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000110 print 'cd %s' % wd
agable@chromium.orgcc023502013-04-03 20:24:21 +0000111 self.run_git(
112 'submodule', 'foreach',
113 'git config -f $toplevel/.git/config submodule.$name.ignore all',
114 cwd=wd)
115 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
116
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000117
118class GclientGitSvnCheckout(GclientGitCheckout, SvnCheckout):
119
120 def __init__(self, dryrun, spec, root):
121 super(GclientGitSvnCheckout, self).__init__(dryrun, spec, root)
122 assert 'svn_url' in self.spec
123 assert 'svn_branch' in self.spec
124 assert 'svn_ref' in self.spec
125
126 def init(self):
127 # Ensure we are authenticated with subversion for all submodules.
128 git_svn_dirs = json.loads(self.spec.get('submodule_git_svn_spec', '{}'))
129 git_svn_dirs.update({self.root: self.spec})
130 for _, svn_spec in git_svn_dirs.iteritems():
131 try:
132 self.run_svn('ls', '--non-interactive', svn_spec['svn_url'])
133 except subprocess.CalledProcessError:
134 print 'Please run `svn ls %s`' % svn_spec['svn_url']
135 return 1
136
137 super(GclientGitSvnCheckout, self).init()
138
agable@chromium.orgcc023502013-04-03 20:24:21 +0000139 # Configure git-svn.
agable@chromium.org2560ea72013-04-04 01:22:38 +0000140 for path, svn_spec in git_svn_dirs.iteritems():
141 real_path = os.path.join(*path.split('/'))
142 if real_path != self.root:
143 real_path = os.path.join(self.root, real_path)
144 wd = os.path.join(self.base, real_path)
dpranke@chromium.org7ca51b32013-04-03 21:29:50 +0000145 if self.dryrun:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000146 print 'cd %s' % wd
agable@chromium.orgcc023502013-04-03 20:24:21 +0000147 self.run_git('svn', 'init', '--prefix=origin/', '-T',
agable@chromium.org2560ea72013-04-04 01:22:38 +0000148 svn_spec['svn_branch'], svn_spec['svn_url'], cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000149 self.run_git('config', '--replace', 'svn-remote.svn.fetch',
agable@chromium.org2560ea72013-04-04 01:22:38 +0000150 svn_spec['svn_branch'] + ':refs/remotes/origin/' +
151 svn_spec['svn_ref'], cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000152 self.run_git('svn', 'fetch', cwd=wd)
153
154
agable@chromium.org2560ea72013-04-04 01:22:38 +0000155
agable@chromium.orgcc023502013-04-03 20:24:21 +0000156CHECKOUT_TYPE_MAP = {
157 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000158 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000159 'gclient_git_svn': GclientGitSvnCheckout,
160 'git': GitCheckout,
161}
162
163
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000164def CheckoutFactory(type_name, dryrun, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000165 """Factory to build Checkout class instances."""
166 class_ = CHECKOUT_TYPE_MAP.get(type_name)
167 if not class_:
168 raise KeyError('unrecognized checkout type: %s' % type_name)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000169 return class_(dryrun, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000170
171
172#################################################
173# Utility function and file entry point.
174#################################################
175def usage(msg=None):
176 """Print help and exit."""
177 if msg:
178 print 'Error:', msg
179
180 print (
181"""
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000182usage: %s [-n|--dry-run] <recipe> [--property=value [--property2=value2 ...]]
agable@chromium.orgcc023502013-04-03 20:24:21 +0000183""" % os.path.basename(sys.argv[0]))
184 sys.exit(bool(msg))
185
186
187def handle_args(argv):
188 """Gets the recipe name from the command line arguments."""
189 if len(argv) <= 1:
190 usage('Must specify a recipe.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000191 if argv[1] in ('-h', '--help', 'help'):
192 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000193
dpranke@chromium.orga992edb2013-04-03 21:22:20 +0000194 dryrun = False
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000195 if argv[1] in ('-n', '--dry-run'):
196 dryrun = True
197 argv.pop(1)
198
agable@chromium.orgcc023502013-04-03 20:24:21 +0000199 def looks_like_arg(arg):
200 return arg.startswith('--') and arg.count('=') == 1
201
202 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
203 if bad_parms:
204 usage('Got bad arguments %s' % bad_parms)
205
206 recipe = argv[1]
207 props = argv[2:]
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000208 return dryrun, recipe, props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000209
210
211def run_recipe_fetch(recipe, props, aliased=False):
212 """Invoke a recipe's fetch method with the passed-through args
213 and return its json output as a python object."""
214 recipe_path = os.path.abspath(os.path.join(SCRIPT_PATH, 'recipes', recipe))
dpranke@chromium.orga992edb2013-04-03 21:22:20 +0000215 if not os.path.exists(recipe_path + '.py'):
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000216 print "Could not find a recipe for %s" % recipe
217 sys.exit(1)
218
agable@chromium.orgcc023502013-04-03 20:24:21 +0000219 cmd = [sys.executable, recipe_path + '.py', 'fetch'] + props
220 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000221
agable@chromium.orgcc023502013-04-03 20:24:21 +0000222 spec = json.loads(result)
223 if 'alias' in spec:
224 assert not aliased
225 return run_recipe_fetch(
226 spec['alias']['recipe'], spec['alias']['props'] + props, aliased=True)
227 cmd = [sys.executable, recipe_path + '.py', 'root']
228 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
229 root = json.loads(result)
230 return spec, root
231
232
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000233def run(dryrun, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000234 """Perform a checkout with the given type and configuration.
235
236 Args:
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000237 dryrun: if True, don't actually execute the commands
agable@chromium.orgcc023502013-04-03 20:24:21 +0000238 spec: Checkout configuration returned by the the recipe's fetch_spec
239 method (checkout type, repository url, etc.).
240 root: The directory into which the repo expects to be checkout out.
241 """
242 assert 'type' in spec
243 checkout_type = spec['type']
244 checkout_spec = spec['%s_spec' % checkout_type]
245 try:
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000246 checkout = CheckoutFactory(checkout_type, dryrun, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000247 except KeyError:
248 return 1
249 if checkout.exists():
250 print 'You appear to already have this checkout.'
251 print 'Aborting to avoid clobbering your work.'
252 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000253 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000254
255
256def main():
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000257 dryrun, recipe, props = handle_args(sys.argv)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000258 spec, root = run_recipe_fetch(recipe, props)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000259 return run(dryrun, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000260
261
262if __name__ == '__main__':
263 sys.exit(main())