blob: 5944140c1d113502a3468ad47896d98105189ed8 [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:
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000010 fetch <config> [--property=value [--property2=value2 ...]]
agable@chromium.orgcc023502013-04-03 20:24:21 +000011
12This script is a wrapper around various version control and repository
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000013checkout commands. It requires a |config| name, fetches data from that
14config in depot_tools/fetch_configs, and then performs all necessary inits,
agable@chromium.orgcc023502013-04-03 20:24:21 +000015checkouts, pulls, fetches, etc.
16
17Optional arguments may be passed on the command line in key-value pairs.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000018These parameters will be passed through to the config's main method.
agable@chromium.orgcc023502013-04-03 20:24:21 +000019"""
20
21import json
digit@chromium.org3596d582013-12-13 17:07:33 +000022import optparse
agable@chromium.orgcc023502013-04-03 20:24:21 +000023import os
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +000024import pipes
agable@chromium.orgcc023502013-04-03 20:24:21 +000025import subprocess
26import sys
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +000027import textwrap
agable@chromium.orgcc023502013-04-03 20:24:21 +000028
Dan Jacques209a6812017-07-12 11:40:20 -070029import git_common
30
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000031from distutils import spawn
32
agable@chromium.orgcc023502013-04-03 20:24:21 +000033
34SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
35
agable@chromium.orgcc023502013-04-03 20:24:21 +000036#################################################
37# Checkout class definitions.
38#################################################
39class Checkout(object):
40 """Base class for implementing different types of checkouts.
41
42 Attributes:
43 |base|: the absolute path of the directory in which this script is run.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000044 |spec|: the spec for this checkout as returned by the config. Different
agable@chromium.orgcc023502013-04-03 20:24:21 +000045 subclasses will expect different keys in this dictionary.
46 |root|: the directory into which the checkout will be performed, as returned
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000047 by the config. This is a relative path from |base|.
agable@chromium.orgcc023502013-04-03 20:24:21 +000048 """
digit@chromium.org3596d582013-12-13 17:07:33 +000049 def __init__(self, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +000050 self.base = os.getcwd()
digit@chromium.org3596d582013-12-13 17:07:33 +000051 self.options = options
agable@chromium.orgcc023502013-04-03 20:24:21 +000052 self.spec = spec
53 self.root = root
54
55 def exists(self):
56 pass
57
58 def init(self):
59 pass
60
61 def sync(self):
62 pass
63
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000064 def run(self, cmd, **kwargs):
65 print 'Running: %s' % (' '.join(pipes.quote(x) for x in cmd))
wtc@chromium.org38e94612014-02-12 22:19:41 +000066 if self.options.dry_run:
mmoss@chromium.org294c7832015-06-17 16:16:32 +000067 return ''
mmoss@chromium.org5a447762015-06-10 20:01:39 +000068 return subprocess.check_output(cmd, **kwargs)
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000069
agable@chromium.orgcc023502013-04-03 20:24:21 +000070
71class GclientCheckout(Checkout):
72
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000073 def run_gclient(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000074 if not spawn.find_executable('gclient'):
75 cmd_prefix = (sys.executable, os.path.join(SCRIPT_PATH, 'gclient.py'))
76 else:
77 cmd_prefix = ('gclient',)
78 return self.run(cmd_prefix + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000079
mmoss@chromium.org5a447762015-06-10 20:01:39 +000080 def exists(self):
81 try:
82 gclient_root = self.run_gclient('root').strip()
83 return (os.path.exists(os.path.join(gclient_root, '.gclient')) or
84 os.path.exists(os.path.join(os.getcwd(), self.root)))
85 except subprocess.CalledProcessError:
86 pass
87 return os.path.exists(os.path.join(os.getcwd(), self.root))
88
agable@chromium.orgcc023502013-04-03 20:24:21 +000089
90class GitCheckout(Checkout):
91
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000092 def run_git(self, *cmd, **kwargs):
Dan Jacques209a6812017-07-12 11:40:20 -070093 print 'Running: git %s' % (' '.join(pipes.quote(x) for x in cmd))
Aaron Gablebd95f412017-11-29 11:20:26 -080094 if self.options.dry_run:
95 return ''
Dan Jacques209a6812017-07-12 11:40:20 -070096 return git_common.run(*cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000097
98
jochen@chromium.orgd993e782013-04-11 20:03:13 +000099class GclientGitCheckout(GclientCheckout, GitCheckout):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000100
digit@chromium.org3596d582013-12-13 17:07:33 +0000101 def __init__(self, options, spec, root):
102 super(GclientGitCheckout, self).__init__(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000103 assert 'solutions' in self.spec
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000104
105 def _format_spec(self):
106 def _format_literal(lit):
107 if isinstance(lit, basestring):
108 return '"%s"' % lit
109 if isinstance(lit, list):
110 return '[%s]' % ', '.join(_format_literal(i) for i in lit)
111 return '%r' % lit
112 soln_strings = []
113 for soln in self.spec['solutions']:
114 soln_string= '\n'.join(' "%s": %s,' % (key, _format_literal(value))
115 for key, value in soln.iteritems())
116 soln_strings.append(' {\n%s\n },' % soln_string)
117 gclient_spec = 'solutions = [\n%s\n]\n' % '\n'.join(soln_strings)
Dr Alex Gouaillarde1dd46f2016-11-28 15:00:04 +0800118 extra_keys = ['target_os', 'target_os_only', 'cache_dir']
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000119 gclient_spec += ''.join('%s = %s\n' % (key, _format_literal(self.spec[key]))
120 for key in extra_keys if key in self.spec)
121 return gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000122
agable@chromium.orgcc023502013-04-03 20:24:21 +0000123 def init(self):
124 # Configure and do the gclient checkout.
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000125 self.run_gclient('config', '--spec', self._format_spec())
jochen@chromium.org048da082014-05-06 08:32:40 +0000126 sync_cmd = ['sync']
agable@chromium.orgb98f3f22015-06-15 21:59:09 +0000127 if self.options.nohooks:
jochen@chromium.org048da082014-05-06 08:32:40 +0000128 sync_cmd.append('--nohooks')
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000129 if self.options.no_history:
130 sync_cmd.append('--no-history')
jochen@chromium.org048da082014-05-06 08:32:40 +0000131 if self.spec.get('with_branch_heads', False):
132 sync_cmd.append('--with_branch_heads')
133 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000134
135 # Configure git.
136 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000137 if self.options.dry_run:
agable@chromium.org2560ea72013-04-04 01:22:38 +0000138 print 'cd %s' % wd
agable@chromium.orgcc023502013-04-03 20:24:21 +0000139 self.run_git(
140 'submodule', 'foreach',
141 'git config -f $toplevel/.git/config submodule.$name.ignore all',
142 cwd=wd)
iannucci@chromium.orgf2fb5e72014-04-03 02:36:44 +0000143 self.run_git(
144 'config', '--add', 'remote.origin.fetch',
145 '+refs/tags/*:refs/tags/*', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000146 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
147
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000148
agable@chromium.orgcc023502013-04-03 20:24:21 +0000149CHECKOUT_TYPE_MAP = {
150 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000151 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000152 'git': GitCheckout,
153}
154
155
digit@chromium.org3596d582013-12-13 17:07:33 +0000156def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000157 """Factory to build Checkout class instances."""
158 class_ = CHECKOUT_TYPE_MAP.get(type_name)
159 if not class_:
160 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000161 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000162
163
164#################################################
165# Utility function and file entry point.
166#################################################
167def usage(msg=None):
168 """Print help and exit."""
169 if msg:
170 print 'Error:', msg
171
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000172 print textwrap.dedent("""\
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000173 usage: %s [options] <config> [--property=value [--property2=value2 ...]]
digit@chromium.org3596d582013-12-13 17:07:33 +0000174
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000175 This script can be used to download the Chromium sources. See
176 http://www.chromium.org/developers/how-tos/get-the-code
177 for full usage instructions.
digit@chromium.org3596d582013-12-13 17:07:33 +0000178
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000179 Valid options:
180 -h, --help, help Print this message.
181 --nohooks Don't run hooks after checkout.
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000182 --force (dangerous) Don't look for existing .gclient file.
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000183 -n, --dry-run Don't run commands, only print them.
184 --no-history Perform shallow clones, don't fetch the full git history.
185
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000186 Valid fetch configs:""") % os.path.basename(sys.argv[0])
thestig@chromium.org37103c92015-09-19 20:54:39 +0000187
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000188 configs_dir = os.path.join(SCRIPT_PATH, 'fetch_configs')
189 configs = [f[:-3] for f in os.listdir(configs_dir) if f.endswith('.py')]
190 configs.sort()
191 for fname in configs:
thestig@chromium.org37103c92015-09-19 20:54:39 +0000192 print ' ' + fname
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000193
agable@chromium.orgcc023502013-04-03 20:24:21 +0000194 sys.exit(bool(msg))
195
196
197def handle_args(argv):
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000198 """Gets the config name from the command line arguments."""
agable@chromium.orgcc023502013-04-03 20:24:21 +0000199 if len(argv) <= 1:
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000200 usage('Must specify a config.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000201 if argv[1] in ('-h', '--help', 'help'):
202 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000203
wtc@chromium.org38e94612014-02-12 22:19:41 +0000204 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000205 nohooks = False
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000206 no_history = False
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000207 force = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000208 while len(argv) >= 2:
209 arg = argv[1]
210 if not arg.startswith('-'):
211 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000212 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000213 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000214 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000215 elif arg == '--nohooks':
216 nohooks = True
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000217 elif arg == '--no-history':
218 no_history = True
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000219 elif arg == '--force':
220 force = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000221 else:
222 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000223
agable@chromium.orgcc023502013-04-03 20:24:21 +0000224 def looks_like_arg(arg):
225 return arg.startswith('--') and arg.count('=') == 1
226
227 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
228 if bad_parms:
229 usage('Got bad arguments %s' % bad_parms)
230
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000231 config = argv[1]
agable@chromium.orgcc023502013-04-03 20:24:21 +0000232 props = argv[2:]
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000233 return (
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000234 optparse.Values({
235 'dry_run': dry_run,
236 'nohooks': nohooks,
237 'no_history': no_history,
238 'force': force}),
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000239 config,
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000240 props)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000241
242
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000243def run_config_fetch(config, props, aliased=False):
244 """Invoke a config's fetch method with the passed-through args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000245 and return its json output as a python object."""
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000246 config_path = os.path.abspath(
247 os.path.join(SCRIPT_PATH, 'fetch_configs', config))
248 if not os.path.exists(config_path + '.py'):
249 print "Could not find a config for %s" % config
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000250 sys.exit(1)
251
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000252 cmd = [sys.executable, config_path + '.py', 'fetch'] + props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000253 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
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000258 return run_config_fetch(
259 spec['alias']['config'], spec['alias']['props'] + props, aliased=True)
260 cmd = [sys.executable, config_path + '.py', 'root']
agable@chromium.orgcc023502013-04-03 20:24:21 +0000261 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.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000271 spec: Checkout configuration returned by the the config's fetch_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000272 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
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000282 if not options.force and checkout.exists():
mmoss@chromium.org5a447762015-06-10 20:01:39 +0000283 print 'Your current directory appears to already contain, or be part of, '
284 print 'a checkout. "fetch" is used only to get new checkouts. Use '
285 print '"gclient sync" to update existing checkouts.'
dpranke@chromium.orgfd79e0d2013-04-12 21:34:32 +0000286 print
287 print 'Fetch also does not yet deal with partial checkouts, so if fetch'
288 print 'failed, delete the checkout and start over (crbug.com/230691).'
agable@chromium.orgcc023502013-04-03 20:24:21 +0000289 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000290 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000291
292
293def main():
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000294 options, config, props = handle_args(sys.argv)
295 spec, root = run_config_fetch(config, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000296 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000297
298
299if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000300 try:
301 sys.exit(main())
302 except KeyboardInterrupt:
303 sys.stderr.write('interrupted\n')
304 sys.exit(1)