blob: f19f103bd73d3f0c57313293c2ef8bae175a3819 [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
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
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
Jamie Madill9e3b7a92022-02-15 20:18:55 +000096 os.path.exists(os.path.join(os.getcwd(), self.root, '.git')))
mmoss@chromium.org5a447762015-06-10 20:01:39 +000097 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
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000177def handle_args(argv):
178 """Gets the config name from the command line arguments."""
thestig@chromium.org37103c92015-09-19 20:54:39 +0000179
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000180 configs_dir = os.path.join(SCRIPT_PATH, 'fetch_configs')
181 configs = [f[:-3] for f in os.listdir(configs_dir) if f.endswith('.py')]
182 configs.sort()
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000183
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000184 parser = argparse.ArgumentParser(
185 formatter_class=argparse.RawDescriptionHelpFormatter,
186 description='''
187 This script can be used to download the Chromium sources. See
188 http://www.chromium.org/developers/how-tos/get-the-code
189 for full usage instructions.''',
190 epilog='Valid fetch configs:\n' + \
191 '\n'.join(map(lambda s: ' ' + s, configs))
192 )
agable@chromium.orgcc023502013-04-03 20:24:21 +0000193
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000194 parser.add_argument('-n', '--dry-run', action='store_true', default=False,
195 help='Don\'t run commands, only print them.')
196 parser.add_argument('--nohooks', action='store_true', default=False,
197 help='Don\'t run hooks after checkout.')
198 parser.add_argument('--no-history', action='store_true', default=False,
199 help='Perform shallow clones, don\'t fetch the full git history.')
200 parser.add_argument('--force', action='store_true', default=False,
201 help='(dangerous) Don\'t look for existing .gclient file.')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000202
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000203 parser.add_argument('config', type=str,
204 help="Project to fetch, e.g. chromium.")
205 parser.add_argument('props', metavar='props', type=str,
206 nargs=argparse.REMAINDER, default=[])
agable@chromium.orgcc023502013-04-03 20:24:21 +0000207
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000208 args = parser.parse_args(argv[1:])
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000209
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000210 # props passed to config must be of the format --<name>=<value>
211 looks_like_arg = lambda arg: arg.startswith('--') and arg.count('=') == 1
212 bad_param = [x for x in args.props if not looks_like_arg(x)]
213 if bad_param:
214 print('Error: Got bad arguments %s' % bad_param)
215 parser.print_help()
216 sys.exit(1)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000217
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000218 return args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000219
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000220def run_config_fetch(config, props, aliased=False):
221 """Invoke a config's fetch method with the passed-through args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000222 and return its json output as a python object."""
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000223 config_path = os.path.abspath(
224 os.path.join(SCRIPT_PATH, 'fetch_configs', config))
225 if not os.path.exists(config_path + '.py'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000226 print("Could not find a config for %s" % config)
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000227 sys.exit(1)
228
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000229 cmd = [sys.executable, config_path + '.py', 'fetch'] + props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000230 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000231
Milad Farazmande2686732020-04-17 17:52:50 +0000232 spec = json.loads(result.decode("utf-8"))
agable@chromium.orgcc023502013-04-03 20:24:21 +0000233 if 'alias' in spec:
234 assert not aliased
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000235 return run_config_fetch(
236 spec['alias']['config'], spec['alias']['props'] + props, aliased=True)
237 cmd = [sys.executable, config_path + '.py', 'root']
agable@chromium.orgcc023502013-04-03 20:24:21 +0000238 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
Milad Farazmande2686732020-04-17 17:52:50 +0000239 root = json.loads(result.decode("utf-8"))
agable@chromium.orgcc023502013-04-03 20:24:21 +0000240 return spec, root
241
242
digit@chromium.org3596d582013-12-13 17:07:33 +0000243def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000244 """Perform a checkout with the given type and configuration.
245
246 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000247 options: Options instance.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000248 spec: Checkout configuration returned by the the config's fetch_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000249 method (checkout type, repository url, etc.).
250 root: The directory into which the repo expects to be checkout out.
251 """
252 assert 'type' in spec
253 checkout_type = spec['type']
254 checkout_spec = spec['%s_spec' % checkout_type]
255 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000256 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000257 except KeyError:
258 return 1
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000259 if not options.force and checkout.exists():
Raul Tambrec2f74c12019-03-19 05:55:53 +0000260 print('Your current directory appears to already contain, or be part of, ')
261 print('a checkout. "fetch" is used only to get new checkouts. Use ')
262 print('"gclient sync" to update existing checkouts.')
263 print()
264 print('Fetch also does not yet deal with partial checkouts, so if fetch')
265 print('failed, delete the checkout and start over (crbug.com/230691).')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000266 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000267 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000268
269
270def main():
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000271 args = handle_args(sys.argv)
272 spec, root = run_config_fetch(args.config, args.props)
273 return run(args, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000274
275
276if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000277 try:
278 sys.exit(main())
279 except KeyboardInterrupt:
280 sys.stderr.write('interrupted\n')
281 sys.exit(1)