blob: c4b605c6652bb9a9fc602e84387334961e849956 [file] [log] [blame]
Edward Lemur5c869192019-10-11 20:08:17 +00001#!/usr/bin/env vpython
agable@chromium.orgcc023502013-04-03 20:24:21 +00002# 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):
Josip Sokcevic06c8bce2020-03-13 18:42:32 +000058 """Check does this checkout already exist on desired location"""
agable@chromium.orgcc023502013-04-03 20:24:21 +000059 pass
60
61 def init(self):
62 pass
63
Edward Lemur7a4ced22018-01-26 16:26:05 +010064 def run(self, cmd, return_stdout=False, **kwargs):
Raul Tambrec2f74c12019-03-19 05:55:53 +000065 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 ''
Edward Lemur7a4ced22018-01-26 16:26:05 +010068 if return_stdout:
Raul Tambre43271f92019-07-16 14:03:54 +000069 return subprocess.check_output(cmd, **kwargs).decode()
Edward Lemur7a4ced22018-01-26 16:26:05 +010070 else:
Elly Fong-Jones7b294392019-04-18 18:32:10 +000071 try:
72 subprocess.check_call(cmd, **kwargs)
73 except subprocess.CalledProcessError as e:
74 # If the subprocess failed, it likely emitted its own distress message
75 # already - don't scroll that message off the screen with a stack trace
76 # from this program as well. Emit a terse message and bail out here;
77 # otherwise a later step will try doing more work and may hide the
78 # subprocess message.
79 print('Subprocess failed with return code %d.' % e.returncode)
80 sys.exit(e.returncode)
Edward Lemur7a4ced22018-01-26 16:26:05 +010081 return ''
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000082
agable@chromium.orgcc023502013-04-03 20:24:21 +000083
84class GclientCheckout(Checkout):
85
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000086 def run_gclient(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000087 if not spawn.find_executable('gclient'):
88 cmd_prefix = (sys.executable, os.path.join(SCRIPT_PATH, 'gclient.py'))
89 else:
90 cmd_prefix = ('gclient',)
91 return self.run(cmd_prefix + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000092
mmoss@chromium.org5a447762015-06-10 20:01:39 +000093 def exists(self):
94 try:
Edward Lemur7a4ced22018-01-26 16:26:05 +010095 gclient_root = self.run_gclient('root', return_stdout=True).strip()
mmoss@chromium.org5a447762015-06-10 20:01:39 +000096 return (os.path.exists(os.path.join(gclient_root, '.gclient')) or
97 os.path.exists(os.path.join(os.getcwd(), self.root)))
98 except subprocess.CalledProcessError:
99 pass
100 return os.path.exists(os.path.join(os.getcwd(), self.root))
101
agable@chromium.orgcc023502013-04-03 20:24:21 +0000102
103class GitCheckout(Checkout):
104
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000105 def run_git(self, *cmd, **kwargs):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000106 print('Running: git %s' % (' '.join(pipes.quote(x) for x in cmd)))
Aaron Gablebd95f412017-11-29 11:20:26 -0800107 if self.options.dry_run:
108 return ''
Dan Jacques209a6812017-07-12 11:40:20 -0700109 return git_common.run(*cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000110
111
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000112class GclientGitCheckout(GclientCheckout, GitCheckout):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000113
digit@chromium.org3596d582013-12-13 17:07:33 +0000114 def __init__(self, options, spec, root):
115 super(GclientGitCheckout, self).__init__(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000116 assert 'solutions' in self.spec
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000117
118 def _format_spec(self):
119 def _format_literal(lit):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000120 if isinstance(lit, str) or (sys.version_info.major == 2 and
121 isinstance(lit, unicode)):
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000122 return '"%s"' % lit
123 if isinstance(lit, list):
124 return '[%s]' % ', '.join(_format_literal(i) for i in lit)
125 return '%r' % lit
126 soln_strings = []
127 for soln in self.spec['solutions']:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000128 soln_string = '\n'.join(' "%s": %s,' % (key, _format_literal(value))
129 for key, value in soln.items())
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000130 soln_strings.append(' {\n%s\n },' % soln_string)
131 gclient_spec = 'solutions = [\n%s\n]\n' % '\n'.join(soln_strings)
Dr Alex Gouaillarde1dd46f2016-11-28 15:00:04 +0800132 extra_keys = ['target_os', 'target_os_only', 'cache_dir']
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000133 gclient_spec += ''.join('%s = %s\n' % (key, _format_literal(self.spec[key]))
134 for key in extra_keys if key in self.spec)
135 return gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000136
agable@chromium.orgcc023502013-04-03 20:24:21 +0000137 def init(self):
138 # Configure and do the gclient checkout.
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000139 self.run_gclient('config', '--spec', self._format_spec())
jochen@chromium.org048da082014-05-06 08:32:40 +0000140 sync_cmd = ['sync']
agable@chromium.orgb98f3f22015-06-15 21:59:09 +0000141 if self.options.nohooks:
jochen@chromium.org048da082014-05-06 08:32:40 +0000142 sync_cmd.append('--nohooks')
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000143 if self.options.no_history:
144 sync_cmd.append('--no-history')
jochen@chromium.org048da082014-05-06 08:32:40 +0000145 if self.spec.get('with_branch_heads', False):
146 sync_cmd.append('--with_branch_heads')
147 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000148
149 # Configure git.
150 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000151 if self.options.dry_run:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000152 print('cd %s' % wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000153 self.run_git(
154 'submodule', 'foreach',
155 'git config -f $toplevel/.git/config submodule.$name.ignore all',
156 cwd=wd)
Torne (Richard Coles)08ca04b2018-02-08 15:23:08 -0500157 if not self.options.no_history:
158 self.run_git(
159 'config', '--add', 'remote.origin.fetch',
160 '+refs/tags/*:refs/tags/*', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000161 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
162
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000163
agable@chromium.orgcc023502013-04-03 20:24:21 +0000164CHECKOUT_TYPE_MAP = {
165 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000166 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000167 'git': GitCheckout,
168}
169
170
digit@chromium.org3596d582013-12-13 17:07:33 +0000171def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000172 """Factory to build Checkout class instances."""
173 class_ = CHECKOUT_TYPE_MAP.get(type_name)
174 if not class_:
175 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000176 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000177
178
179#################################################
180# Utility function and file entry point.
181#################################################
182def usage(msg=None):
183 """Print help and exit."""
184 if msg:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000185 print('Error:', msg)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000186
Raul Tambrec2f74c12019-03-19 05:55:53 +0000187 print(textwrap.dedent("""\
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000188 usage: %s [options] <config> [--property=value [--property2=value2 ...]]
digit@chromium.org3596d582013-12-13 17:07:33 +0000189
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000190 This script can be used to download the Chromium sources. See
191 http://www.chromium.org/developers/how-tos/get-the-code
192 for full usage instructions.
digit@chromium.org3596d582013-12-13 17:07:33 +0000193
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000194 Valid options:
195 -h, --help, help Print this message.
196 --nohooks Don't run hooks after checkout.
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000197 --force (dangerous) Don't look for existing .gclient file.
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000198 -n, --dry-run Don't run commands, only print them.
199 --no-history Perform shallow clones, don't fetch the full git history.
200
Raul Tambrec2f74c12019-03-19 05:55:53 +0000201 Valid fetch configs:""") % os.path.basename(sys.argv[0]))
thestig@chromium.org37103c92015-09-19 20:54:39 +0000202
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000203 configs_dir = os.path.join(SCRIPT_PATH, 'fetch_configs')
204 configs = [f[:-3] for f in os.listdir(configs_dir) if f.endswith('.py')]
205 configs.sort()
206 for fname in configs:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000207 print(' ' + fname)
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000208
agable@chromium.orgcc023502013-04-03 20:24:21 +0000209 sys.exit(bool(msg))
210
211
212def handle_args(argv):
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000213 """Gets the config name from the command line arguments."""
agable@chromium.orgcc023502013-04-03 20:24:21 +0000214 if len(argv) <= 1:
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000215 usage('Must specify a config.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000216 if argv[1] in ('-h', '--help', 'help'):
217 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000218
wtc@chromium.org38e94612014-02-12 22:19:41 +0000219 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000220 nohooks = False
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000221 no_history = False
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000222 force = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000223 while len(argv) >= 2:
224 arg = argv[1]
225 if not arg.startswith('-'):
226 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000227 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000228 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000229 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000230 elif arg == '--nohooks':
231 nohooks = True
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000232 elif arg == '--no-history':
233 no_history = True
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000234 elif arg == '--force':
235 force = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000236 else:
237 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000238
agable@chromium.orgcc023502013-04-03 20:24:21 +0000239 def looks_like_arg(arg):
240 return arg.startswith('--') and arg.count('=') == 1
241
242 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
243 if bad_parms:
244 usage('Got bad arguments %s' % bad_parms)
245
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000246 config = argv[1]
agable@chromium.orgcc023502013-04-03 20:24:21 +0000247 props = argv[2:]
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000248 return (
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000249 optparse.Values({
250 'dry_run': dry_run,
251 'nohooks': nohooks,
252 'no_history': no_history,
253 'force': force}),
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000254 config,
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000255 props)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000256
257
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000258def run_config_fetch(config, props, aliased=False):
259 """Invoke a config's fetch method with the passed-through args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000260 and return its json output as a python object."""
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000261 config_path = os.path.abspath(
262 os.path.join(SCRIPT_PATH, 'fetch_configs', config))
263 if not os.path.exists(config_path + '.py'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000264 print("Could not find a config for %s" % config)
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000265 sys.exit(1)
266
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000267 cmd = [sys.executable, config_path + '.py', 'fetch'] + props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000268 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000269
Milad Farazmande2686732020-04-17 17:52:50 +0000270 spec = json.loads(result.decode("utf-8"))
agable@chromium.orgcc023502013-04-03 20:24:21 +0000271 if 'alias' in spec:
272 assert not aliased
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000273 return run_config_fetch(
274 spec['alias']['config'], spec['alias']['props'] + props, aliased=True)
275 cmd = [sys.executable, config_path + '.py', 'root']
agable@chromium.orgcc023502013-04-03 20:24:21 +0000276 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
Milad Farazmande2686732020-04-17 17:52:50 +0000277 root = json.loads(result.decode("utf-8"))
agable@chromium.orgcc023502013-04-03 20:24:21 +0000278 return spec, root
279
280
digit@chromium.org3596d582013-12-13 17:07:33 +0000281def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000282 """Perform a checkout with the given type and configuration.
283
284 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000285 options: Options instance.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000286 spec: Checkout configuration returned by the the config's fetch_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000287 method (checkout type, repository url, etc.).
288 root: The directory into which the repo expects to be checkout out.
289 """
290 assert 'type' in spec
291 checkout_type = spec['type']
292 checkout_spec = spec['%s_spec' % checkout_type]
293 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000294 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000295 except KeyError:
296 return 1
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000297 if not options.force and checkout.exists():
Raul Tambrec2f74c12019-03-19 05:55:53 +0000298 print('Your current directory appears to already contain, or be part of, ')
299 print('a checkout. "fetch" is used only to get new checkouts. Use ')
300 print('"gclient sync" to update existing checkouts.')
301 print()
302 print('Fetch also does not yet deal with partial checkouts, so if fetch')
303 print('failed, delete the checkout and start over (crbug.com/230691).')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000304 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000305 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000306
307
308def main():
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000309 options, config, props = handle_args(sys.argv)
310 spec, root = run_config_fetch(config, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000311 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000312
313
314if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000315 try:
316 sys.exit(main())
317 except KeyboardInterrupt:
318 sys.stderr.write('interrupted\n')
319 sys.exit(1)