blob: 522639fed22170c1a71f9ad987003f25332699f2 [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
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +000027import re
agable@chromium.orgcc023502013-04-03 20:24:21 +000028import subprocess
29import sys
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +000030import textwrap
agable@chromium.orgcc023502013-04-03 20:24:21 +000031
Dan Jacques209a6812017-07-12 11:40:20 -070032import git_common
33
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000034from distutils import spawn
35
agable@chromium.orgcc023502013-04-03 20:24:21 +000036
37SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
38
agable@chromium.orgcc023502013-04-03 20:24:21 +000039#################################################
40# Checkout class definitions.
41#################################################
42class Checkout(object):
43 """Base class for implementing different types of checkouts.
44
45 Attributes:
46 |base|: the absolute path of the directory in which this script is run.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000047 |spec|: the spec for this checkout as returned by the config. Different
agable@chromium.orgcc023502013-04-03 20:24:21 +000048 subclasses will expect different keys in this dictionary.
49 |root|: the directory into which the checkout will be performed, as returned
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000050 by the config. This is a relative path from |base|.
agable@chromium.orgcc023502013-04-03 20:24:21 +000051 """
digit@chromium.org3596d582013-12-13 17:07:33 +000052 def __init__(self, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +000053 self.base = os.getcwd()
digit@chromium.org3596d582013-12-13 17:07:33 +000054 self.options = options
agable@chromium.orgcc023502013-04-03 20:24:21 +000055 self.spec = spec
56 self.root = root
57
58 def exists(self):
Josip Sokcevic06c8bce2020-03-13 18:42:32 +000059 """Check does this checkout already exist on desired location"""
agable@chromium.orgcc023502013-04-03 20:24:21 +000060
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()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000070
71 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)
81 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
Jamie Madill9e3b7a92022-02-15 20:18:55 +000097 os.path.exists(os.path.join(os.getcwd(), self.root, '.git')))
mmoss@chromium.org5a447762015-06-10 20:01:39 +000098 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
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000178def handle_args(argv):
179 """Gets the config name from the command line arguments."""
thestig@chromium.org37103c92015-09-19 20:54:39 +0000180
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000181 configs_dir = os.path.join(SCRIPT_PATH, 'fetch_configs')
182 configs = [f[:-3] for f in os.listdir(configs_dir) if f.endswith('.py')]
183 configs.sort()
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000184
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000185 parser = argparse.ArgumentParser(
186 formatter_class=argparse.RawDescriptionHelpFormatter,
187 description='''
188 This script can be used to download the Chromium sources. See
189 http://www.chromium.org/developers/how-tos/get-the-code
190 for full usage instructions.''',
191 epilog='Valid fetch configs:\n' + \
192 '\n'.join(map(lambda s: ' ' + s, configs))
193 )
agable@chromium.orgcc023502013-04-03 20:24:21 +0000194
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000195 parser.add_argument('-n', '--dry-run', action='store_true', default=False,
196 help='Don\'t run commands, only print them.')
197 parser.add_argument('--nohooks', action='store_true', default=False,
198 help='Don\'t run hooks after checkout.')
199 parser.add_argument('--no-history', action='store_true', default=False,
200 help='Perform shallow clones, don\'t fetch the full git history.')
201 parser.add_argument('--force', action='store_true', default=False,
202 help='(dangerous) Don\'t look for existing .gclient file.')
Aravind Vasudevanfb8cf9c2022-05-03 18:33:38 +0000203 parser.add_argument(
204 '-p',
205 '--protocol-override',
206 type=str,
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000207 default=None,
Aravind Vasudevanfb8cf9c2022-05-03 18:33:38 +0000208 help='Protocol to use to fetch dependencies, defaults to https.')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000209
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000210 parser.add_argument('config', type=str,
211 help="Project to fetch, e.g. chromium.")
212 parser.add_argument('props', metavar='props', type=str,
213 nargs=argparse.REMAINDER, default=[])
agable@chromium.orgcc023502013-04-03 20:24:21 +0000214
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000215 args = parser.parse_args(argv[1:])
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000216
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000217 # props passed to config must be of the format --<name>=<value>
218 looks_like_arg = lambda arg: arg.startswith('--') and arg.count('=') == 1
219 bad_param = [x for x in args.props if not looks_like_arg(x)]
220 if bad_param:
221 print('Error: Got bad arguments %s' % bad_param)
222 parser.print_help()
223 sys.exit(1)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000224
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000225 return args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000226
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000227def run_config_fetch(config, props, aliased=False):
228 """Invoke a config's fetch method with the passed-through args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000229 and return its json output as a python object."""
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000230 config_path = os.path.abspath(
231 os.path.join(SCRIPT_PATH, 'fetch_configs', config))
232 if not os.path.exists(config_path + '.py'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000233 print("Could not find a config for %s" % config)
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000234 sys.exit(1)
235
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000236 cmd = [sys.executable, config_path + '.py', 'fetch'] + props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000237 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000238
Milad Farazmande2686732020-04-17 17:52:50 +0000239 spec = json.loads(result.decode("utf-8"))
agable@chromium.orgcc023502013-04-03 20:24:21 +0000240 if 'alias' in spec:
241 assert not aliased
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000242 return run_config_fetch(
243 spec['alias']['config'], spec['alias']['props'] + props, aliased=True)
244 cmd = [sys.executable, config_path + '.py', 'root']
agable@chromium.orgcc023502013-04-03 20:24:21 +0000245 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
Milad Farazmande2686732020-04-17 17:52:50 +0000246 root = json.loads(result.decode("utf-8"))
agable@chromium.orgcc023502013-04-03 20:24:21 +0000247 return spec, root
248
249
digit@chromium.org3596d582013-12-13 17:07:33 +0000250def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000251 """Perform a checkout with the given type and configuration.
252
253 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000254 options: Options instance.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000255 spec: Checkout configuration returned by the the config's fetch_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000256 method (checkout type, repository url, etc.).
257 root: The directory into which the repo expects to be checkout out.
258 """
259 assert 'type' in spec
260 checkout_type = spec['type']
261 checkout_spec = spec['%s_spec' % checkout_type]
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000262
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000263 # Use sso:// by default if the env is cog
264 if not options.protocol_override and \
265 os.getcwd().startswith('/google/src/cloud'):
266 options.protocol_override = 'sso'
267
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000268 # Replace https using the protocol specified in --protocol-override
269 if options.protocol_override is not None:
270 for solution in checkout_spec['solutions']:
271 solution['url'] = re.sub(
272 '^([a-z]+):', options.protocol_override + ':', solution['url'])
273
agable@chromium.orgcc023502013-04-03 20:24:21 +0000274 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000275 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000276 except KeyError:
277 return 1
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000278 if not options.force and checkout.exists():
Raul Tambrec2f74c12019-03-19 05:55:53 +0000279 print('Your current directory appears to already contain, or be part of, ')
280 print('a checkout. "fetch" is used only to get new checkouts. Use ')
281 print('"gclient sync" to update existing checkouts.')
282 print()
283 print('Fetch also does not yet deal with partial checkouts, so if fetch')
284 print('failed, delete the checkout and start over (crbug.com/230691).')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000285 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000286 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000287
288
289def main():
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000290 args = handle_args(sys.argv)
291 spec, root = run_config_fetch(args.config, args.props)
292 return run(args, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000293
294
295if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000296 try:
297 sys.exit(main())
298 except KeyboardInterrupt:
299 sys.stderr.write('interrupted\n')
300 sys.exit(1)