blob: 56abfb9a6d07c5d60295762eaf024875176c3bcb [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
Raul Tambrec2f74c12019-03-19 05:55:53 +000021from __future__ import print_function
22
agable@chromium.orgcc023502013-04-03 20:24:21 +000023import json
digit@chromium.org3596d582013-12-13 17:07:33 +000024import optparse
agable@chromium.orgcc023502013-04-03 20:24:21 +000025import os
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +000026import pipes
agable@chromium.orgcc023502013-04-03 20:24:21 +000027import subprocess
28import sys
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +000029import textwrap
agable@chromium.orgcc023502013-04-03 20:24:21 +000030
Dan Jacques209a6812017-07-12 11:40:20 -070031import git_common
32
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000033from distutils import spawn
34
agable@chromium.orgcc023502013-04-03 20:24:21 +000035
36SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
37
agable@chromium.orgcc023502013-04-03 20:24:21 +000038#################################################
39# Checkout class definitions.
40#################################################
41class Checkout(object):
42 """Base class for implementing different types of checkouts.
43
44 Attributes:
45 |base|: the absolute path of the directory in which this script is run.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000046 |spec|: the spec for this checkout as returned by the config. Different
agable@chromium.orgcc023502013-04-03 20:24:21 +000047 subclasses will expect different keys in this dictionary.
48 |root|: the directory into which the checkout will be performed, as returned
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000049 by the config. This is a relative path from |base|.
agable@chromium.orgcc023502013-04-03 20:24:21 +000050 """
digit@chromium.org3596d582013-12-13 17:07:33 +000051 def __init__(self, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +000052 self.base = os.getcwd()
digit@chromium.org3596d582013-12-13 17:07:33 +000053 self.options = options
agable@chromium.orgcc023502013-04-03 20:24:21 +000054 self.spec = spec
55 self.root = root
56
57 def exists(self):
58 pass
59
60 def init(self):
61 pass
62
63 def sync(self):
64 pass
65
Edward Lemur7a4ced22018-01-26 16:26:05 +010066 def run(self, cmd, return_stdout=False, **kwargs):
Raul Tambrec2f74c12019-03-19 05:55:53 +000067 print('Running: %s' % (' '.join(pipes.quote(x) for x in cmd)))
wtc@chromium.org38e94612014-02-12 22:19:41 +000068 if self.options.dry_run:
mmoss@chromium.org294c7832015-06-17 16:16:32 +000069 return ''
Edward Lemur7a4ced22018-01-26 16:26:05 +010070 if return_stdout:
Raul Tambre43271f92019-07-16 14:03:54 +000071 return subprocess.check_output(cmd, **kwargs).decode()
Edward Lemur7a4ced22018-01-26 16:26:05 +010072 else:
Elly Fong-Jones7b294392019-04-18 18:32:10 +000073 try:
74 subprocess.check_call(cmd, **kwargs)
75 except subprocess.CalledProcessError as e:
76 # If the subprocess failed, it likely emitted its own distress message
77 # already - don't scroll that message off the screen with a stack trace
78 # from this program as well. Emit a terse message and bail out here;
79 # otherwise a later step will try doing more work and may hide the
80 # subprocess message.
81 print('Subprocess failed with return code %d.' % e.returncode)
82 sys.exit(e.returncode)
Edward Lemur7a4ced22018-01-26 16:26:05 +010083 return ''
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000084
agable@chromium.orgcc023502013-04-03 20:24:21 +000085
86class GclientCheckout(Checkout):
87
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000088 def run_gclient(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000089 if not spawn.find_executable('gclient'):
90 cmd_prefix = (sys.executable, os.path.join(SCRIPT_PATH, 'gclient.py'))
91 else:
92 cmd_prefix = ('gclient',)
93 return self.run(cmd_prefix + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000094
mmoss@chromium.org5a447762015-06-10 20:01:39 +000095 def exists(self):
96 try:
Edward Lemur7a4ced22018-01-26 16:26:05 +010097 gclient_root = self.run_gclient('root', return_stdout=True).strip()
mmoss@chromium.org5a447762015-06-10 20:01:39 +000098 return (os.path.exists(os.path.join(gclient_root, '.gclient')) or
99 os.path.exists(os.path.join(os.getcwd(), self.root)))
100 except subprocess.CalledProcessError:
101 pass
102 return os.path.exists(os.path.join(os.getcwd(), self.root))
103
agable@chromium.orgcc023502013-04-03 20:24:21 +0000104
105class GitCheckout(Checkout):
106
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000107 def run_git(self, *cmd, **kwargs):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000108 print('Running: git %s' % (' '.join(pipes.quote(x) for x in cmd)))
Aaron Gablebd95f412017-11-29 11:20:26 -0800109 if self.options.dry_run:
110 return ''
Dan Jacques209a6812017-07-12 11:40:20 -0700111 return git_common.run(*cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000112
113
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000114class GclientGitCheckout(GclientCheckout, GitCheckout):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000115
digit@chromium.org3596d582013-12-13 17:07:33 +0000116 def __init__(self, options, spec, root):
117 super(GclientGitCheckout, self).__init__(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000118 assert 'solutions' in self.spec
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000119
120 def _format_spec(self):
121 def _format_literal(lit):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000122 if isinstance(lit, str) or (sys.version_info.major == 2 and
123 isinstance(lit, unicode)):
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000124 return '"%s"' % lit
125 if isinstance(lit, list):
126 return '[%s]' % ', '.join(_format_literal(i) for i in lit)
127 return '%r' % lit
128 soln_strings = []
129 for soln in self.spec['solutions']:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000130 soln_string = '\n'.join(' "%s": %s,' % (key, _format_literal(value))
131 for key, value in soln.items())
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000132 soln_strings.append(' {\n%s\n },' % soln_string)
133 gclient_spec = 'solutions = [\n%s\n]\n' % '\n'.join(soln_strings)
Dr Alex Gouaillarde1dd46f2016-11-28 15:00:04 +0800134 extra_keys = ['target_os', 'target_os_only', 'cache_dir']
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000135 gclient_spec += ''.join('%s = %s\n' % (key, _format_literal(self.spec[key]))
136 for key in extra_keys if key in self.spec)
137 return gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000138
agable@chromium.orgcc023502013-04-03 20:24:21 +0000139 def init(self):
140 # Configure and do the gclient checkout.
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000141 self.run_gclient('config', '--spec', self._format_spec())
jochen@chromium.org048da082014-05-06 08:32:40 +0000142 sync_cmd = ['sync']
agable@chromium.orgb98f3f22015-06-15 21:59:09 +0000143 if self.options.nohooks:
jochen@chromium.org048da082014-05-06 08:32:40 +0000144 sync_cmd.append('--nohooks')
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000145 if self.options.no_history:
146 sync_cmd.append('--no-history')
jochen@chromium.org048da082014-05-06 08:32:40 +0000147 if self.spec.get('with_branch_heads', False):
148 sync_cmd.append('--with_branch_heads')
149 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000150
151 # Configure git.
152 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000153 if self.options.dry_run:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000154 print('cd %s' % wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000155 self.run_git(
156 'submodule', 'foreach',
157 'git config -f $toplevel/.git/config submodule.$name.ignore all',
158 cwd=wd)
Torne (Richard Coles)08ca04b2018-02-08 15:23:08 -0500159 if not self.options.no_history:
160 self.run_git(
161 'config', '--add', 'remote.origin.fetch',
162 '+refs/tags/*:refs/tags/*', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000163 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
164
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000165
agable@chromium.orgcc023502013-04-03 20:24:21 +0000166CHECKOUT_TYPE_MAP = {
167 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000168 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000169 'git': GitCheckout,
170}
171
172
digit@chromium.org3596d582013-12-13 17:07:33 +0000173def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000174 """Factory to build Checkout class instances."""
175 class_ = CHECKOUT_TYPE_MAP.get(type_name)
176 if not class_:
177 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000178 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000179
180
181#################################################
182# Utility function and file entry point.
183#################################################
184def usage(msg=None):
185 """Print help and exit."""
186 if msg:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000187 print('Error:', msg)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000188
Raul Tambrec2f74c12019-03-19 05:55:53 +0000189 print(textwrap.dedent("""\
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000190 usage: %s [options] <config> [--property=value [--property2=value2 ...]]
digit@chromium.org3596d582013-12-13 17:07:33 +0000191
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000192 This script can be used to download the Chromium sources. See
193 http://www.chromium.org/developers/how-tos/get-the-code
194 for full usage instructions.
digit@chromium.org3596d582013-12-13 17:07:33 +0000195
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000196 Valid options:
197 -h, --help, help Print this message.
198 --nohooks Don't run hooks after checkout.
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000199 --force (dangerous) Don't look for existing .gclient file.
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000200 -n, --dry-run Don't run commands, only print them.
201 --no-history Perform shallow clones, don't fetch the full git history.
202
Raul Tambrec2f74c12019-03-19 05:55:53 +0000203 Valid fetch configs:""") % os.path.basename(sys.argv[0]))
thestig@chromium.org37103c92015-09-19 20:54:39 +0000204
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000205 configs_dir = os.path.join(SCRIPT_PATH, 'fetch_configs')
206 configs = [f[:-3] for f in os.listdir(configs_dir) if f.endswith('.py')]
207 configs.sort()
208 for fname in configs:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000209 print(' ' + fname)
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000210
agable@chromium.orgcc023502013-04-03 20:24:21 +0000211 sys.exit(bool(msg))
212
213
214def handle_args(argv):
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000215 """Gets the config name from the command line arguments."""
agable@chromium.orgcc023502013-04-03 20:24:21 +0000216 if len(argv) <= 1:
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000217 usage('Must specify a config.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000218 if argv[1] in ('-h', '--help', 'help'):
219 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000220
wtc@chromium.org38e94612014-02-12 22:19:41 +0000221 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000222 nohooks = False
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000223 no_history = False
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000224 force = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000225 while len(argv) >= 2:
226 arg = argv[1]
227 if not arg.startswith('-'):
228 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000229 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000230 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000231 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000232 elif arg == '--nohooks':
233 nohooks = True
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000234 elif arg == '--no-history':
235 no_history = True
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000236 elif arg == '--force':
237 force = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000238 else:
239 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000240
agable@chromium.orgcc023502013-04-03 20:24:21 +0000241 def looks_like_arg(arg):
242 return arg.startswith('--') and arg.count('=') == 1
243
244 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
245 if bad_parms:
246 usage('Got bad arguments %s' % bad_parms)
247
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000248 config = argv[1]
agable@chromium.orgcc023502013-04-03 20:24:21 +0000249 props = argv[2:]
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000250 return (
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000251 optparse.Values({
252 'dry_run': dry_run,
253 'nohooks': nohooks,
254 'no_history': no_history,
255 'force': force}),
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000256 config,
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000257 props)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000258
259
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000260def run_config_fetch(config, props, aliased=False):
261 """Invoke a config's fetch method with the passed-through args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000262 and return its json output as a python object."""
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000263 config_path = os.path.abspath(
264 os.path.join(SCRIPT_PATH, 'fetch_configs', config))
265 if not os.path.exists(config_path + '.py'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000266 print("Could not find a config for %s" % config)
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000267 sys.exit(1)
268
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000269 cmd = [sys.executable, config_path + '.py', 'fetch'] + props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000270 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000271
agable@chromium.orgcc023502013-04-03 20:24:21 +0000272 spec = json.loads(result)
273 if 'alias' in spec:
274 assert not aliased
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000275 return run_config_fetch(
276 spec['alias']['config'], spec['alias']['props'] + props, aliased=True)
277 cmd = [sys.executable, config_path + '.py', 'root']
agable@chromium.orgcc023502013-04-03 20:24:21 +0000278 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
279 root = json.loads(result)
280 return spec, root
281
282
digit@chromium.org3596d582013-12-13 17:07:33 +0000283def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000284 """Perform a checkout with the given type and configuration.
285
286 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000287 options: Options instance.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000288 spec: Checkout configuration returned by the the config's fetch_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000289 method (checkout type, repository url, etc.).
290 root: The directory into which the repo expects to be checkout out.
291 """
292 assert 'type' in spec
293 checkout_type = spec['type']
294 checkout_spec = spec['%s_spec' % checkout_type]
295 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000296 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000297 except KeyError:
298 return 1
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000299 if not options.force and checkout.exists():
Raul Tambrec2f74c12019-03-19 05:55:53 +0000300 print('Your current directory appears to already contain, or be part of, ')
301 print('a checkout. "fetch" is used only to get new checkouts. Use ')
302 print('"gclient sync" to update existing checkouts.')
303 print()
304 print('Fetch also does not yet deal with partial checkouts, so if fetch')
305 print('failed, delete the checkout and start over (crbug.com/230691).')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000306 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000307 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000308
309
310def main():
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000311 options, config, props = handle_args(sys.argv)
312 spec, root = run_config_fetch(config, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000313 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000314
315
316if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000317 try:
318 sys.exit(main())
319 except KeyboardInterrupt:
320 sys.stderr.write('interrupted\n')
321 sys.exit(1)