blob: 80f8927d0f1b650f3b2f42fd5ec18607c2d0d534 [file] [log] [blame]
Josip Sokcevic4de5dea2022-03-23 21:15:14 +00001#!/usr/bin/env vpython3
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
Aravind Vasudevan075cd762022-03-23 21:13:13 +000024import argparse
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
agable@chromium.orgcc023502013-04-03 20:24:21 +000029
Dan Jacques209a6812017-07-12 11:40:20 -070030import git_common
31
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000032from distutils import spawn
33
agable@chromium.orgcc023502013-04-03 20:24:21 +000034
35SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
36
agable@chromium.orgcc023502013-04-03 20:24:21 +000037#################################################
38# Checkout class definitions.
39#################################################
40class Checkout(object):
41 """Base class for implementing different types of checkouts.
42
43 Attributes:
44 |base|: the absolute path of the directory in which this script is run.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000045 |spec|: the spec for this checkout as returned by the config. Different
agable@chromium.orgcc023502013-04-03 20:24:21 +000046 subclasses will expect different keys in this dictionary.
47 |root|: the directory into which the checkout will be performed, as returned
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000048 by the config. This is a relative path from |base|.
agable@chromium.orgcc023502013-04-03 20:24:21 +000049 """
digit@chromium.org3596d582013-12-13 17:07:33 +000050 def __init__(self, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +000051 self.base = os.getcwd()
digit@chromium.org3596d582013-12-13 17:07:33 +000052 self.options = options
agable@chromium.orgcc023502013-04-03 20:24:21 +000053 self.spec = spec
54 self.root = root
55
56 def exists(self):
Josip Sokcevic06c8bce2020-03-13 18:42:32 +000057 """Check does this checkout already exist on desired location"""
agable@chromium.orgcc023502013-04-03 20:24:21 +000058
59 def init(self):
60 pass
61
Edward Lemur7a4ced22018-01-26 16:26:05 +010062 def run(self, cmd, return_stdout=False, **kwargs):
Raul Tambrec2f74c12019-03-19 05:55:53 +000063 print('Running: %s' % (' '.join(pipes.quote(x) for x in cmd)))
wtc@chromium.org38e94612014-02-12 22:19:41 +000064 if self.options.dry_run:
mmoss@chromium.org294c7832015-06-17 16:16:32 +000065 return ''
Edward Lemur7a4ced22018-01-26 16:26:05 +010066 if return_stdout:
Raul Tambre43271f92019-07-16 14:03:54 +000067 return subprocess.check_output(cmd, **kwargs).decode()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000068
69 try:
70 subprocess.check_call(cmd, **kwargs)
71 except subprocess.CalledProcessError as e:
72 # If the subprocess failed, it likely emitted its own distress message
73 # already - don't scroll that message off the screen with a stack trace
74 # from this program as well. Emit a terse message and bail out here;
75 # otherwise a later step will try doing more work and may hide the
76 # subprocess message.
77 print('Subprocess failed with return code %d.' % e.returncode)
78 sys.exit(e.returncode)
79 return ''
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000080
agable@chromium.orgcc023502013-04-03 20:24:21 +000081
82class GclientCheckout(Checkout):
83
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000084 def run_gclient(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000085 if not spawn.find_executable('gclient'):
86 cmd_prefix = (sys.executable, os.path.join(SCRIPT_PATH, 'gclient.py'))
87 else:
88 cmd_prefix = ('gclient',)
89 return self.run(cmd_prefix + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000090
mmoss@chromium.org5a447762015-06-10 20:01:39 +000091 def exists(self):
92 try:
Edward Lemur7a4ced22018-01-26 16:26:05 +010093 gclient_root = self.run_gclient('root', return_stdout=True).strip()
mmoss@chromium.org5a447762015-06-10 20:01:39 +000094 return (os.path.exists(os.path.join(gclient_root, '.gclient')) or
Jamie Madill9e3b7a92022-02-15 20:18:55 +000095 os.path.exists(os.path.join(os.getcwd(), self.root, '.git')))
mmoss@chromium.org5a447762015-06-10 20:01:39 +000096 except subprocess.CalledProcessError:
97 pass
98 return os.path.exists(os.path.join(os.getcwd(), self.root))
99
agable@chromium.orgcc023502013-04-03 20:24:21 +0000100
101class GitCheckout(Checkout):
102
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000103 def run_git(self, *cmd, **kwargs):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000104 print('Running: git %s' % (' '.join(pipes.quote(x) for x in cmd)))
Aaron Gablebd95f412017-11-29 11:20:26 -0800105 if self.options.dry_run:
106 return ''
Dan Jacques209a6812017-07-12 11:40:20 -0700107 return git_common.run(*cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000108
109
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000110class GclientGitCheckout(GclientCheckout, GitCheckout):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000111
digit@chromium.org3596d582013-12-13 17:07:33 +0000112 def __init__(self, options, spec, root):
113 super(GclientGitCheckout, self).__init__(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000114 assert 'solutions' in self.spec
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000115
116 def _format_spec(self):
117 def _format_literal(lit):
Gavin Mak512f3cb2023-09-05 18:02:24 +0000118 if isinstance(lit, str):
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000119 return '"%s"' % lit
120 if isinstance(lit, list):
121 return '[%s]' % ', '.join(_format_literal(i) for i in lit)
122 return '%r' % lit
123 soln_strings = []
124 for soln in self.spec['solutions']:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000125 soln_string = '\n'.join(' "%s": %s,' % (key, _format_literal(value))
126 for key, value in soln.items())
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000127 soln_strings.append(' {\n%s\n },' % soln_string)
128 gclient_spec = 'solutions = [\n%s\n]\n' % '\n'.join(soln_strings)
Dr Alex Gouaillarde1dd46f2016-11-28 15:00:04 +0800129 extra_keys = ['target_os', 'target_os_only', 'cache_dir']
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000130 gclient_spec += ''.join('%s = %s\n' % (key, _format_literal(self.spec[key]))
131 for key in extra_keys if key in self.spec)
132 return gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000133
agable@chromium.orgcc023502013-04-03 20:24:21 +0000134 def init(self):
135 # Configure and do the gclient checkout.
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000136 self.run_gclient('config', '--spec', self._format_spec())
jochen@chromium.org048da082014-05-06 08:32:40 +0000137 sync_cmd = ['sync']
agable@chromium.orgb98f3f22015-06-15 21:59:09 +0000138 if self.options.nohooks:
jochen@chromium.org048da082014-05-06 08:32:40 +0000139 sync_cmd.append('--nohooks')
Thiago Perrotta512dfd62022-09-20 16:50:02 +0000140 if self.options.nohistory:
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000141 sync_cmd.append('--no-history')
jochen@chromium.org048da082014-05-06 08:32:40 +0000142 if self.spec.get('with_branch_heads', False):
143 sync_cmd.append('--with_branch_heads')
144 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000145
146 # Configure git.
147 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000148 if self.options.dry_run:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000149 print('cd %s' % wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000150 self.run_git(
151 'submodule', 'foreach',
152 'git config -f $toplevel/.git/config submodule.$name.ignore all',
153 cwd=wd)
Thiago Perrotta512dfd62022-09-20 16:50:02 +0000154 if not self.options.nohistory:
Torne (Richard Coles)08ca04b2018-02-08 15:23:08 -0500155 self.run_git(
156 'config', '--add', 'remote.origin.fetch',
157 '+refs/tags/*:refs/tags/*', cwd=wd)
Josip Sokcevic2c3875e2023-08-22 17:29:47 +0000158 self.run_git('config', 'diff.ignoreSubmodules', 'dirty', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000159
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000160
agable@chromium.orgcc023502013-04-03 20:24:21 +0000161CHECKOUT_TYPE_MAP = {
162 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000163 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000164 'git': GitCheckout,
165}
166
167
digit@chromium.org3596d582013-12-13 17:07:33 +0000168def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000169 """Factory to build Checkout class instances."""
170 class_ = CHECKOUT_TYPE_MAP.get(type_name)
171 if not class_:
172 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000173 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000174
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000175def handle_args(argv):
176 """Gets the config name from the command line arguments."""
thestig@chromium.org37103c92015-09-19 20:54:39 +0000177
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000178 configs_dir = os.path.join(SCRIPT_PATH, 'fetch_configs')
179 configs = [f[:-3] for f in os.listdir(configs_dir) if f.endswith('.py')]
180 configs.sort()
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000181
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000182 parser = argparse.ArgumentParser(
183 formatter_class=argparse.RawDescriptionHelpFormatter,
184 description='''
185 This script can be used to download the Chromium sources. See
186 http://www.chromium.org/developers/how-tos/get-the-code
187 for full usage instructions.''',
188 epilog='Valid fetch configs:\n' + \
189 '\n'.join(map(lambda s: ' ' + s, configs))
190 )
agable@chromium.orgcc023502013-04-03 20:24:21 +0000191
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000192 parser.add_argument('-n', '--dry-run', action='store_true', default=False,
193 help='Don\'t run commands, only print them.')
Thiago Perrotta512dfd62022-09-20 16:50:02 +0000194 parser.add_argument('--nohooks',
195 '--no-hooks',
196 action='store_true',
197 default=False,
198 help='Don\'t run hooks after checkout.')
199 parser.add_argument(
200 '--nohistory',
201 '--no-history',
202 action='store_true',
203 default=False,
204 help='Perform shallow clones, don\'t fetch the full git history.')
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000205 parser.add_argument('--force', action='store_true', default=False,
206 help='(dangerous) Don\'t look for existing .gclient file.')
Aravind Vasudevanfb8cf9c2022-05-03 18:33:38 +0000207 parser.add_argument(
208 '-p',
209 '--protocol-override',
210 type=str,
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000211 default=None,
Aravind Vasudevanfb8cf9c2022-05-03 18:33:38 +0000212 help='Protocol to use to fetch dependencies, defaults to https.')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000213
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000214 parser.add_argument('config', type=str,
215 help="Project to fetch, e.g. chromium.")
216 parser.add_argument('props', metavar='props', type=str,
217 nargs=argparse.REMAINDER, default=[])
agable@chromium.orgcc023502013-04-03 20:24:21 +0000218
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000219 args = parser.parse_args(argv[1:])
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000220
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000221 # props passed to config must be of the format --<name>=<value>
222 looks_like_arg = lambda arg: arg.startswith('--') and arg.count('=') == 1
223 bad_param = [x for x in args.props if not looks_like_arg(x)]
224 if bad_param:
225 print('Error: Got bad arguments %s' % bad_param)
226 parser.print_help()
227 sys.exit(1)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000228
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000229 return args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000230
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000231def run_config_fetch(config, props, aliased=False):
232 """Invoke a config's fetch method with the passed-through args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000233 and return its json output as a python object."""
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000234 config_path = os.path.abspath(
235 os.path.join(SCRIPT_PATH, 'fetch_configs', config))
236 if not os.path.exists(config_path + '.py'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000237 print("Could not find a config for %s" % config)
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000238 sys.exit(1)
239
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000240 cmd = [sys.executable, config_path + '.py', 'fetch'] + props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000241 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000242
Milad Farazmande2686732020-04-17 17:52:50 +0000243 spec = json.loads(result.decode("utf-8"))
agable@chromium.orgcc023502013-04-03 20:24:21 +0000244 if 'alias' in spec:
245 assert not aliased
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000246 return run_config_fetch(
247 spec['alias']['config'], spec['alias']['props'] + props, aliased=True)
248 cmd = [sys.executable, config_path + '.py', 'root']
agable@chromium.orgcc023502013-04-03 20:24:21 +0000249 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
Milad Farazmande2686732020-04-17 17:52:50 +0000250 root = json.loads(result.decode("utf-8"))
agable@chromium.orgcc023502013-04-03 20:24:21 +0000251 return spec, root
252
253
digit@chromium.org3596d582013-12-13 17:07:33 +0000254def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000255 """Perform a checkout with the given type and configuration.
256
257 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000258 options: Options instance.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000259 spec: Checkout configuration returned by the the config's fetch_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000260 method (checkout type, repository url, etc.).
261 root: The directory into which the repo expects to be checkout out.
262 """
263 assert 'type' in spec
264 checkout_type = spec['type']
265 checkout_spec = spec['%s_spec' % checkout_type]
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000266
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000267 # Use sso:// by default if the env is cog
268 if not options.protocol_override and \
Joanna Wangb4155442022-09-01 20:41:20 +0000269 (any(os.getcwd().startswith(x) for x in [
270 '/google/src/cloud', '/google/cog/cloud'])):
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000271 options.protocol_override = 'sso'
272
Aravind Vasudevan810598d2022-06-13 21:23:47 +0000273 # Update solutions with protocol_override field
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000274 if options.protocol_override is not None:
275 for solution in checkout_spec['solutions']:
Aravind Vasudevan810598d2022-06-13 21:23:47 +0000276 solution['protocol_override'] = options.protocol_override
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000277
agable@chromium.orgcc023502013-04-03 20:24:21 +0000278 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():
Raul Tambrec2f74c12019-03-19 05:55:53 +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.')
286 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():
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000294 args = handle_args(sys.argv)
295 spec, root = run_config_fetch(args.config, args.props)
296 return run(args, 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)