blob: 39268f358076919ed490e361f644b2e811c45f34 [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):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000118 if isinstance(lit, str) or (sys.version_info.major == 2 and
119 isinstance(lit, unicode)):
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000120 return '"%s"' % lit
121 if isinstance(lit, list):
122 return '[%s]' % ', '.join(_format_literal(i) for i in lit)
123 return '%r' % lit
124 soln_strings = []
125 for soln in self.spec['solutions']:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000126 soln_string = '\n'.join(' "%s": %s,' % (key, _format_literal(value))
127 for key, value in soln.items())
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000128 soln_strings.append(' {\n%s\n },' % soln_string)
129 gclient_spec = 'solutions = [\n%s\n]\n' % '\n'.join(soln_strings)
Dr Alex Gouaillarde1dd46f2016-11-28 15:00:04 +0800130 extra_keys = ['target_os', 'target_os_only', 'cache_dir']
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000131 gclient_spec += ''.join('%s = %s\n' % (key, _format_literal(self.spec[key]))
132 for key in extra_keys if key in self.spec)
133 return gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000134
agable@chromium.orgcc023502013-04-03 20:24:21 +0000135 def init(self):
136 # Configure and do the gclient checkout.
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000137 self.run_gclient('config', '--spec', self._format_spec())
jochen@chromium.org048da082014-05-06 08:32:40 +0000138 sync_cmd = ['sync']
agable@chromium.orgb98f3f22015-06-15 21:59:09 +0000139 if self.options.nohooks:
jochen@chromium.org048da082014-05-06 08:32:40 +0000140 sync_cmd.append('--nohooks')
Thiago Perrotta512dfd62022-09-20 16:50:02 +0000141 if self.options.nohistory:
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000142 sync_cmd.append('--no-history')
jochen@chromium.org048da082014-05-06 08:32:40 +0000143 if self.spec.get('with_branch_heads', False):
144 sync_cmd.append('--with_branch_heads')
145 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000146
147 # Configure git.
148 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000149 if self.options.dry_run:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000150 print('cd %s' % wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000151 self.run_git(
152 'submodule', 'foreach',
153 'git config -f $toplevel/.git/config submodule.$name.ignore all',
154 cwd=wd)
Thiago Perrotta512dfd62022-09-20 16:50:02 +0000155 if not self.options.nohistory:
Torne (Richard Coles)08ca04b2018-02-08 15:23:08 -0500156 self.run_git(
157 'config', '--add', 'remote.origin.fetch',
158 '+refs/tags/*:refs/tags/*', cwd=wd)
Josip Sokcevic2c3875e2023-08-22 17:29:47 +0000159 self.run_git('config', 'diff.ignoreSubmodules', 'dirty', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000160
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000161
agable@chromium.orgcc023502013-04-03 20:24:21 +0000162CHECKOUT_TYPE_MAP = {
163 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000164 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000165 'git': GitCheckout,
166}
167
168
digit@chromium.org3596d582013-12-13 17:07:33 +0000169def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000170 """Factory to build Checkout class instances."""
171 class_ = CHECKOUT_TYPE_MAP.get(type_name)
172 if not class_:
173 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000174 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000175
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000176def handle_args(argv):
177 """Gets the config name from the command line arguments."""
thestig@chromium.org37103c92015-09-19 20:54:39 +0000178
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000179 configs_dir = os.path.join(SCRIPT_PATH, 'fetch_configs')
180 configs = [f[:-3] for f in os.listdir(configs_dir) if f.endswith('.py')]
181 configs.sort()
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000182
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000183 parser = argparse.ArgumentParser(
184 formatter_class=argparse.RawDescriptionHelpFormatter,
185 description='''
186 This script can be used to download the Chromium sources. See
187 http://www.chromium.org/developers/how-tos/get-the-code
188 for full usage instructions.''',
189 epilog='Valid fetch configs:\n' + \
190 '\n'.join(map(lambda s: ' ' + s, configs))
191 )
agable@chromium.orgcc023502013-04-03 20:24:21 +0000192
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000193 parser.add_argument('-n', '--dry-run', action='store_true', default=False,
194 help='Don\'t run commands, only print them.')
Thiago Perrotta512dfd62022-09-20 16:50:02 +0000195 parser.add_argument('--nohooks',
196 '--no-hooks',
197 action='store_true',
198 default=False,
199 help='Don\'t run hooks after checkout.')
200 parser.add_argument(
201 '--nohistory',
202 '--no-history',
203 action='store_true',
204 default=False,
205 help='Perform shallow clones, don\'t fetch the full git history.')
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000206 parser.add_argument('--force', action='store_true', default=False,
207 help='(dangerous) Don\'t look for existing .gclient file.')
Aravind Vasudevanfb8cf9c2022-05-03 18:33:38 +0000208 parser.add_argument(
209 '-p',
210 '--protocol-override',
211 type=str,
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000212 default=None,
Aravind Vasudevanfb8cf9c2022-05-03 18:33:38 +0000213 help='Protocol to use to fetch dependencies, defaults to https.')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000214
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000215 parser.add_argument('config', type=str,
216 help="Project to fetch, e.g. chromium.")
217 parser.add_argument('props', metavar='props', type=str,
218 nargs=argparse.REMAINDER, default=[])
agable@chromium.orgcc023502013-04-03 20:24:21 +0000219
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000220 args = parser.parse_args(argv[1:])
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000221
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000222 # props passed to config must be of the format --<name>=<value>
223 looks_like_arg = lambda arg: arg.startswith('--') and arg.count('=') == 1
224 bad_param = [x for x in args.props if not looks_like_arg(x)]
225 if bad_param:
226 print('Error: Got bad arguments %s' % bad_param)
227 parser.print_help()
228 sys.exit(1)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000229
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000230 return args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000231
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000232def run_config_fetch(config, props, aliased=False):
233 """Invoke a config's fetch method with the passed-through args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000234 and return its json output as a python object."""
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000235 config_path = os.path.abspath(
236 os.path.join(SCRIPT_PATH, 'fetch_configs', config))
237 if not os.path.exists(config_path + '.py'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000238 print("Could not find a config for %s" % config)
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000239 sys.exit(1)
240
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000241 cmd = [sys.executable, config_path + '.py', 'fetch'] + props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000242 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000243
Milad Farazmande2686732020-04-17 17:52:50 +0000244 spec = json.loads(result.decode("utf-8"))
agable@chromium.orgcc023502013-04-03 20:24:21 +0000245 if 'alias' in spec:
246 assert not aliased
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000247 return run_config_fetch(
248 spec['alias']['config'], spec['alias']['props'] + props, aliased=True)
249 cmd = [sys.executable, config_path + '.py', 'root']
agable@chromium.orgcc023502013-04-03 20:24:21 +0000250 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
Milad Farazmande2686732020-04-17 17:52:50 +0000251 root = json.loads(result.decode("utf-8"))
agable@chromium.orgcc023502013-04-03 20:24:21 +0000252 return spec, root
253
254
digit@chromium.org3596d582013-12-13 17:07:33 +0000255def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000256 """Perform a checkout with the given type and configuration.
257
258 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000259 options: Options instance.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000260 spec: Checkout configuration returned by the the config's fetch_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000261 method (checkout type, repository url, etc.).
262 root: The directory into which the repo expects to be checkout out.
263 """
264 assert 'type' in spec
265 checkout_type = spec['type']
266 checkout_spec = spec['%s_spec' % checkout_type]
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000267
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000268 # Use sso:// by default if the env is cog
269 if not options.protocol_override and \
Joanna Wangb4155442022-09-01 20:41:20 +0000270 (any(os.getcwd().startswith(x) for x in [
271 '/google/src/cloud', '/google/cog/cloud'])):
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000272 options.protocol_override = 'sso'
273
Aravind Vasudevan810598d2022-06-13 21:23:47 +0000274 # Update solutions with protocol_override field
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000275 if options.protocol_override is not None:
276 for solution in checkout_spec['solutions']:
Aravind Vasudevan810598d2022-06-13 21:23:47 +0000277 solution['protocol_override'] = options.protocol_override
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000278
agable@chromium.orgcc023502013-04-03 20:24:21 +0000279 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000280 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000281 except KeyError:
282 return 1
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000283 if not options.force and checkout.exists():
Raul Tambrec2f74c12019-03-19 05:55:53 +0000284 print('Your current directory appears to already contain, or be part of, ')
285 print('a checkout. "fetch" is used only to get new checkouts. Use ')
286 print('"gclient sync" to update existing checkouts.')
287 print()
288 print('Fetch also does not yet deal with partial checkouts, so if fetch')
289 print('failed, delete the checkout and start over (crbug.com/230691).')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000290 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000291 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000292
293
294def main():
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000295 args = handle_args(sys.argv)
296 spec, root = run_config_fetch(args.config, args.props)
297 return run(args, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000298
299
300if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000301 try:
302 sys.exit(main())
303 except KeyboardInterrupt:
304 sys.stderr.write('interrupted\n')
305 sys.exit(1)