blob: 74b229852b9e318c8272991dc0ffa2a11fc60c33 [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
60 def init(self):
61 pass
62
Edward Lemur7a4ced22018-01-26 16:26:05 +010063 def run(self, cmd, return_stdout=False, **kwargs):
Raul Tambrec2f74c12019-03-19 05:55:53 +000064 print('Running: %s' % (' '.join(pipes.quote(x) for x in cmd)))
wtc@chromium.org38e94612014-02-12 22:19:41 +000065 if self.options.dry_run:
mmoss@chromium.org294c7832015-06-17 16:16:32 +000066 return ''
Edward Lemur7a4ced22018-01-26 16:26:05 +010067 if return_stdout:
Raul Tambre43271f92019-07-16 14:03:54 +000068 return subprocess.check_output(cmd, **kwargs).decode()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000069
70 try:
71 subprocess.check_call(cmd, **kwargs)
72 except subprocess.CalledProcessError as e:
73 # If the subprocess failed, it likely emitted its own distress message
74 # already - don't scroll that message off the screen with a stack trace
75 # from this program as well. Emit a terse message and bail out here;
76 # otherwise a later step will try doing more work and may hide the
77 # subprocess message.
78 print('Subprocess failed with return code %d.' % e.returncode)
79 sys.exit(e.returncode)
80 return ''
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000081
agable@chromium.orgcc023502013-04-03 20:24:21 +000082
83class GclientCheckout(Checkout):
84
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000085 def run_gclient(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000086 if not spawn.find_executable('gclient'):
87 cmd_prefix = (sys.executable, os.path.join(SCRIPT_PATH, 'gclient.py'))
88 else:
89 cmd_prefix = ('gclient',)
90 return self.run(cmd_prefix + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000091
mmoss@chromium.org5a447762015-06-10 20:01:39 +000092 def exists(self):
93 try:
Edward Lemur7a4ced22018-01-26 16:26:05 +010094 gclient_root = self.run_gclient('root', return_stdout=True).strip()
mmoss@chromium.org5a447762015-06-10 20:01:39 +000095 return (os.path.exists(os.path.join(gclient_root, '.gclient')) or
96 os.path.exists(os.path.join(os.getcwd(), self.root)))
97 except subprocess.CalledProcessError:
98 pass
99 return os.path.exists(os.path.join(os.getcwd(), self.root))
100
agable@chromium.orgcc023502013-04-03 20:24:21 +0000101
102class GitCheckout(Checkout):
103
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000104 def run_git(self, *cmd, **kwargs):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000105 print('Running: git %s' % (' '.join(pipes.quote(x) for x in cmd)))
Aaron Gablebd95f412017-11-29 11:20:26 -0800106 if self.options.dry_run:
107 return ''
Dan Jacques209a6812017-07-12 11:40:20 -0700108 return git_common.run(*cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000109
110
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000111class GclientGitCheckout(GclientCheckout, GitCheckout):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000112
digit@chromium.org3596d582013-12-13 17:07:33 +0000113 def __init__(self, options, spec, root):
114 super(GclientGitCheckout, self).__init__(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000115 assert 'solutions' in self.spec
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000116
117 def _format_spec(self):
118 def _format_literal(lit):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000119 if isinstance(lit, str) or (sys.version_info.major == 2 and
120 isinstance(lit, unicode)):
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000121 return '"%s"' % lit
122 if isinstance(lit, list):
123 return '[%s]' % ', '.join(_format_literal(i) for i in lit)
124 return '%r' % lit
125 soln_strings = []
126 for soln in self.spec['solutions']:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000127 soln_string = '\n'.join(' "%s": %s,' % (key, _format_literal(value))
128 for key, value in soln.items())
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000129 soln_strings.append(' {\n%s\n },' % soln_string)
130 gclient_spec = 'solutions = [\n%s\n]\n' % '\n'.join(soln_strings)
Dr Alex Gouaillarde1dd46f2016-11-28 15:00:04 +0800131 extra_keys = ['target_os', 'target_os_only', 'cache_dir']
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000132 gclient_spec += ''.join('%s = %s\n' % (key, _format_literal(self.spec[key]))
133 for key in extra_keys if key in self.spec)
134 return gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000135
agable@chromium.orgcc023502013-04-03 20:24:21 +0000136 def init(self):
137 # Configure and do the gclient checkout.
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000138 self.run_gclient('config', '--spec', self._format_spec())
jochen@chromium.org048da082014-05-06 08:32:40 +0000139 sync_cmd = ['sync']
agable@chromium.orgb98f3f22015-06-15 21:59:09 +0000140 if self.options.nohooks:
jochen@chromium.org048da082014-05-06 08:32:40 +0000141 sync_cmd.append('--nohooks')
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000142 if self.options.no_history:
143 sync_cmd.append('--no-history')
jochen@chromium.org048da082014-05-06 08:32:40 +0000144 if self.spec.get('with_branch_heads', False):
145 sync_cmd.append('--with_branch_heads')
146 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000147
148 # Configure git.
149 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000150 if self.options.dry_run:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000151 print('cd %s' % wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000152 self.run_git(
153 'submodule', 'foreach',
154 'git config -f $toplevel/.git/config submodule.$name.ignore all',
155 cwd=wd)
Torne (Richard Coles)08ca04b2018-02-08 15:23:08 -0500156 if not self.options.no_history:
157 self.run_git(
158 'config', '--add', 'remote.origin.fetch',
159 '+refs/tags/*:refs/tags/*', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000160 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
161
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000162
agable@chromium.orgcc023502013-04-03 20:24:21 +0000163CHECKOUT_TYPE_MAP = {
164 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000165 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000166 'git': GitCheckout,
167}
168
169
digit@chromium.org3596d582013-12-13 17:07:33 +0000170def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000171 """Factory to build Checkout class instances."""
172 class_ = CHECKOUT_TYPE_MAP.get(type_name)
173 if not class_:
174 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000175 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000176
177
178#################################################
179# Utility function and file entry point.
180#################################################
181def usage(msg=None):
182 """Print help and exit."""
183 if msg:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000184 print('Error:', msg)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000185
Raul Tambrec2f74c12019-03-19 05:55:53 +0000186 print(textwrap.dedent("""\
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000187 usage: %s [options] <config> [--property=value [--property2=value2 ...]]
digit@chromium.org3596d582013-12-13 17:07:33 +0000188
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000189 This script can be used to download the Chromium sources. See
190 http://www.chromium.org/developers/how-tos/get-the-code
191 for full usage instructions.
digit@chromium.org3596d582013-12-13 17:07:33 +0000192
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000193 Valid options:
194 -h, --help, help Print this message.
195 --nohooks Don't run hooks after checkout.
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000196 --force (dangerous) Don't look for existing .gclient file.
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000197 -n, --dry-run Don't run commands, only print them.
198 --no-history Perform shallow clones, don't fetch the full git history.
199
Raul Tambrec2f74c12019-03-19 05:55:53 +0000200 Valid fetch configs:""") % os.path.basename(sys.argv[0]))
thestig@chromium.org37103c92015-09-19 20:54:39 +0000201
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000202 configs_dir = os.path.join(SCRIPT_PATH, 'fetch_configs')
203 configs = [f[:-3] for f in os.listdir(configs_dir) if f.endswith('.py')]
204 configs.sort()
205 for fname in configs:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000206 print(' ' + fname)
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000207
agable@chromium.orgcc023502013-04-03 20:24:21 +0000208 sys.exit(bool(msg))
209
210
211def handle_args(argv):
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000212 """Gets the config name from the command line arguments."""
agable@chromium.orgcc023502013-04-03 20:24:21 +0000213 if len(argv) <= 1:
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000214 usage('Must specify a config.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000215 if argv[1] in ('-h', '--help', 'help'):
216 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000217
wtc@chromium.org38e94612014-02-12 22:19:41 +0000218 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000219 nohooks = False
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000220 no_history = False
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000221 force = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000222 while len(argv) >= 2:
223 arg = argv[1]
224 if not arg.startswith('-'):
225 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000226 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000227 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000228 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000229 elif arg == '--nohooks':
230 nohooks = True
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000231 elif arg == '--no-history':
232 no_history = True
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000233 elif arg == '--force':
234 force = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000235 else:
236 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000237
agable@chromium.orgcc023502013-04-03 20:24:21 +0000238 def looks_like_arg(arg):
239 return arg.startswith('--') and arg.count('=') == 1
240
241 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
242 if bad_parms:
243 usage('Got bad arguments %s' % bad_parms)
244
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000245 config = argv[1]
agable@chromium.orgcc023502013-04-03 20:24:21 +0000246 props = argv[2:]
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000247 return (
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000248 optparse.Values({
249 'dry_run': dry_run,
250 'nohooks': nohooks,
251 'no_history': no_history,
252 'force': force}),
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000253 config,
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000254 props)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000255
256
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000257def run_config_fetch(config, props, aliased=False):
258 """Invoke a config's fetch method with the passed-through args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000259 and return its json output as a python object."""
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000260 config_path = os.path.abspath(
261 os.path.join(SCRIPT_PATH, 'fetch_configs', config))
262 if not os.path.exists(config_path + '.py'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000263 print("Could not find a config for %s" % config)
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000264 sys.exit(1)
265
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000266 cmd = [sys.executable, config_path + '.py', 'fetch'] + props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000267 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000268
Milad Farazmande2686732020-04-17 17:52:50 +0000269 spec = json.loads(result.decode("utf-8"))
agable@chromium.orgcc023502013-04-03 20:24:21 +0000270 if 'alias' in spec:
271 assert not aliased
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000272 return run_config_fetch(
273 spec['alias']['config'], spec['alias']['props'] + props, aliased=True)
274 cmd = [sys.executable, config_path + '.py', 'root']
agable@chromium.orgcc023502013-04-03 20:24:21 +0000275 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
Milad Farazmande2686732020-04-17 17:52:50 +0000276 root = json.loads(result.decode("utf-8"))
agable@chromium.orgcc023502013-04-03 20:24:21 +0000277 return spec, root
278
279
digit@chromium.org3596d582013-12-13 17:07:33 +0000280def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000281 """Perform a checkout with the given type and configuration.
282
283 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000284 options: Options instance.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000285 spec: Checkout configuration returned by the the config's fetch_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000286 method (checkout type, repository url, etc.).
287 root: The directory into which the repo expects to be checkout out.
288 """
289 assert 'type' in spec
290 checkout_type = spec['type']
291 checkout_spec = spec['%s_spec' % checkout_type]
292 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000293 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000294 except KeyError:
295 return 1
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000296 if not options.force and checkout.exists():
Raul Tambrec2f74c12019-03-19 05:55:53 +0000297 print('Your current directory appears to already contain, or be part of, ')
298 print('a checkout. "fetch" is used only to get new checkouts. Use ')
299 print('"gclient sync" to update existing checkouts.')
300 print()
301 print('Fetch also does not yet deal with partial checkouts, so if fetch')
302 print('failed, delete the checkout and start over (crbug.com/230691).')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000303 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000304 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000305
306
307def main():
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000308 options, config, props = handle_args(sys.argv)
309 spec, root = run_config_fetch(config, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000310 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000311
312
313if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000314 try:
315 sys.exit(main())
316 except KeyboardInterrupt:
317 sys.stderr.write('interrupted\n')
318 sys.exit(1)