blob: fcfdf781f35363059d8676108126df9974ba5f94 [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.
agable@chromium.orgcc023502013-04-03 20:24:21 +00005"""
6Tool to perform checkouts in one easy command line!
7
8Usage:
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +00009 fetch <config> [--property=value [--property2=value2 ...]]
agable@chromium.orgcc023502013-04-03 20:24:21 +000010
11This script is a wrapper around various version control and repository
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000012checkout commands. It requires a |config| name, fetches data from that
13config in depot_tools/fetch_configs, and then performs all necessary inits,
agable@chromium.orgcc023502013-04-03 20:24:21 +000014checkouts, pulls, fetches, etc.
15
16Optional arguments may be passed on the command line in key-value pairs.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000017These parameters will be passed through to the config's main method.
agable@chromium.orgcc023502013-04-03 20:24:21 +000018"""
19
Raul Tambrec2f74c12019-03-19 05:55:53 +000020from __future__ import print_function
21
agable@chromium.orgcc023502013-04-03 20:24:21 +000022import json
Aravind Vasudevan075cd762022-03-23 21:13:13 +000023import argparse
agable@chromium.orgcc023502013-04-03 20:24:21 +000024import os
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +000025import pipes
agable@chromium.orgcc023502013-04-03 20:24:21 +000026import subprocess
27import sys
agable@chromium.orgcc023502013-04-03 20:24:21 +000028
Dan Jacques209a6812017-07-12 11:40:20 -070029import git_common
30
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000031from distutils import spawn
32
agable@chromium.orgcc023502013-04-03 20:24:21 +000033SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
34
Mike Frysinger124bb8e2023-09-06 05:48:55 +000035
agable@chromium.orgcc023502013-04-03 20:24:21 +000036#################################################
37# Checkout class definitions.
38#################################################
39class Checkout(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000040 """Base class for implementing different types of checkouts.
agable@chromium.orgcc023502013-04-03 20:24:21 +000041
42 Attributes:
43 |base|: the absolute path of the directory in which this script is run.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000044 |spec|: the spec for this checkout as returned by the config. Different
agable@chromium.orgcc023502013-04-03 20:24:21 +000045 subclasses will expect different keys in this dictionary.
46 |root|: the directory into which the checkout will be performed, as returned
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000047 by the config. This is a relative path from |base|.
agable@chromium.orgcc023502013-04-03 20:24:21 +000048 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +000049 def __init__(self, options, spec, root):
50 self.base = os.getcwd()
51 self.options = options
52 self.spec = spec
53 self.root = root
agable@chromium.orgcc023502013-04-03 20:24:21 +000054
Mike Frysinger124bb8e2023-09-06 05:48:55 +000055 def exists(self):
56 """Check does this checkout already exist on desired location"""
agable@chromium.orgcc023502013-04-03 20:24:21 +000057
Mike Frysinger124bb8e2023-09-06 05:48:55 +000058 def init(self):
59 pass
agable@chromium.orgcc023502013-04-03 20:24:21 +000060
Mike Frysinger124bb8e2023-09-06 05:48:55 +000061 def run(self, cmd, return_stdout=False, **kwargs):
62 print('Running: %s' % (' '.join(pipes.quote(x) for x in cmd)))
63 if self.options.dry_run:
64 return ''
65 if return_stdout:
66 return subprocess.check_output(cmd, **kwargs).decode()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000067
Mike Frysinger124bb8e2023-09-06 05:48:55 +000068 try:
69 subprocess.check_call(cmd, **kwargs)
70 except subprocess.CalledProcessError as e:
71 # If the subprocess failed, it likely emitted its own distress
72 # message already - don't scroll that message off the screen with a
73 # stack trace from this program as well. Emit a terse message and
74 # bail out here; otherwise a later step will try doing more work and
75 # may hide the subprocess message.
76 print('Subprocess failed with return code %d.' % e.returncode)
77 sys.exit(e.returncode)
78 return ''
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000079
agable@chromium.orgcc023502013-04-03 20:24:21 +000080
81class GclientCheckout(Checkout):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000082 def run_gclient(self, *cmd, **kwargs):
83 if not spawn.find_executable('gclient'):
84 cmd_prefix = (sys.executable, os.path.join(SCRIPT_PATH,
85 'gclient.py'))
86 else:
87 cmd_prefix = ('gclient', )
88 return self.run(cmd_prefix + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000089
Mike Frysinger124bb8e2023-09-06 05:48:55 +000090 def exists(self):
91 try:
92 gclient_root = self.run_gclient('root', return_stdout=True).strip()
93 return (os.path.exists(os.path.join(gclient_root, '.gclient'))
94 or os.path.exists(
95 os.path.join(os.getcwd(), self.root, '.git')))
96 except subprocess.CalledProcessError:
97 pass
98 return os.path.exists(os.path.join(os.getcwd(), self.root))
mmoss@chromium.org5a447762015-06-10 20:01:39 +000099
agable@chromium.orgcc023502013-04-03 20:24:21 +0000100
101class GitCheckout(Checkout):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000102 def run_git(self, *cmd, **kwargs):
103 print('Running: git %s' % (' '.join(pipes.quote(x) for x in cmd)))
104 if self.options.dry_run:
105 return ''
106 return git_common.run(*cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000107
108
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000109class GclientGitCheckout(GclientCheckout, GitCheckout):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000110 def __init__(self, options, spec, root):
111 super(GclientGitCheckout, self).__init__(options, spec, root)
112 assert 'solutions' in self.spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000113
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000114 def _format_spec(self):
115 def _format_literal(lit):
116 if isinstance(lit, str):
117 return '"%s"' % lit
118 if isinstance(lit, list):
119 return '[%s]' % ', '.join(_format_literal(i) for i in lit)
120 return '%r' % lit
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000121
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000122 soln_strings = []
123 for soln in self.spec['solutions']:
124 soln_string = '\n'.join(' "%s": %s,' %
125 (key, _format_literal(value))
126 for key, value in soln.items())
127 soln_strings.append(' {\n%s\n },' % soln_string)
128 gclient_spec = 'solutions = [\n%s\n]\n' % '\n'.join(soln_strings)
129 extra_keys = ['target_os', 'target_os_only', 'cache_dir']
130 gclient_spec += ''.join('%s = %s\n' %
131 (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
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000135 def init(self):
136 # Configure and do the gclient checkout.
137 self.run_gclient('config', '--spec', self._format_spec())
138 sync_cmd = ['sync']
139 if self.options.nohooks:
140 sync_cmd.append('--nohooks')
141 if self.options.nohistory:
142 sync_cmd.append('--no-history')
143 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
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000147 # Configure git.
148 wd = os.path.join(self.base, self.root)
149 if self.options.dry_run:
150 print('cd %s' % wd)
151 self.run_git(
152 'submodule',
153 'foreach',
154 'git config -f $toplevel/.git/config submodule.$name.ignore all',
155 cwd=wd)
156 if not self.options.nohistory:
157 self.run_git('config',
158 '--add',
159 'remote.origin.fetch',
160 '+refs/tags/*:refs/tags/*',
161 cwd=wd)
162 self.run_git('config', 'diff.ignoreSubmodules', 'dirty', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000163
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000164
agable@chromium.orgcc023502013-04-03 20:24:21 +0000165CHECKOUT_TYPE_MAP = {
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000166 'gclient': GclientCheckout,
167 'gclient_git': GclientGitCheckout,
168 'git': GitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000169}
170
171
digit@chromium.org3596d582013-12-13 17:07:33 +0000172def CheckoutFactory(type_name, options, spec, root):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000173 """Factory to build Checkout class instances."""
174 class_ = CHECKOUT_TYPE_MAP.get(type_name)
175 if not class_:
176 raise KeyError('unrecognized checkout type: %s' % type_name)
177 return class_(options, spec, root)
178
agable@chromium.orgcc023502013-04-03 20:24:21 +0000179
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000180def handle_args(argv):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000181 """Gets the config name from the command line arguments."""
thestig@chromium.org37103c92015-09-19 20:54:39 +0000182
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000183 configs_dir = os.path.join(SCRIPT_PATH, 'fetch_configs')
184 configs = [f[:-3] for f in os.listdir(configs_dir) if f.endswith('.py')]
185 configs.sort()
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000186
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000187 parser = argparse.ArgumentParser(
188 formatter_class=argparse.RawDescriptionHelpFormatter,
189 description='''
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000190 This script can be used to download the Chromium sources. See
191 http://www.chromium.org/developers/how-tos/get-the-code
192 for full usage instructions.''',
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000193 epilog='Valid fetch configs:\n' + \
194 '\n'.join(map(lambda s: ' ' + s, configs))
195 )
agable@chromium.orgcc023502013-04-03 20:24:21 +0000196
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000197 parser.add_argument('-n',
198 '--dry-run',
199 action='store_true',
200 default=False,
201 help='Don\'t run commands, only print them.')
202 parser.add_argument('--nohooks',
203 '--no-hooks',
204 action='store_true',
205 default=False,
206 help='Don\'t run hooks after checkout.')
207 parser.add_argument(
208 '--nohistory',
209 '--no-history',
210 action='store_true',
211 default=False,
212 help='Perform shallow clones, don\'t fetch the full git history.')
213 parser.add_argument(
214 '--force',
215 action='store_true',
216 default=False,
217 help='(dangerous) Don\'t look for existing .gclient file.')
218 parser.add_argument(
219 '-p',
220 '--protocol-override',
221 type=str,
222 default=None,
223 help='Protocol to use to fetch dependencies, defaults to https.')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000224
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000225 parser.add_argument('config',
226 type=str,
227 help="Project to fetch, e.g. chromium.")
228 parser.add_argument('props',
229 metavar='props',
230 type=str,
231 nargs=argparse.REMAINDER,
232 default=[])
agable@chromium.orgcc023502013-04-03 20:24:21 +0000233
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000234 args = parser.parse_args(argv[1:])
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000235
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000236 # props passed to config must be of the format --<name>=<value>
237 looks_like_arg = lambda arg: arg.startswith('--') and arg.count('=') == 1
238 bad_param = [x for x in args.props if not looks_like_arg(x)]
239 if bad_param:
240 print('Error: Got bad arguments %s' % bad_param)
241 parser.print_help()
242 sys.exit(1)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000243
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000244 return args
245
agable@chromium.orgcc023502013-04-03 20:24:21 +0000246
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000247def run_config_fetch(config, props, aliased=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000248 """Invoke a config's fetch method with the passed-through args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000249 and return its json output as a python object."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000250 config_path = os.path.abspath(
251 os.path.join(SCRIPT_PATH, 'fetch_configs', config))
252 if not os.path.exists(config_path + '.py'):
253 print("Could not find a config for %s" % config)
254 sys.exit(1)
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000255
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000256 cmd = [sys.executable, config_path + '.py', 'fetch'] + props
257 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000258
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000259 spec = json.loads(result.decode("utf-8"))
260 if 'alias' in spec:
261 assert not aliased
262 return run_config_fetch(spec['alias']['config'],
263 spec['alias']['props'] + props,
264 aliased=True)
265 cmd = [sys.executable, config_path + '.py', 'root']
266 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
267 root = json.loads(result.decode("utf-8"))
268 return spec, root
agable@chromium.orgcc023502013-04-03 20:24:21 +0000269
270
digit@chromium.org3596d582013-12-13 17:07:33 +0000271def run(options, spec, root):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000272 """Perform a checkout with the given type and configuration.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000273
274 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000275 options: Options instance.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000276 spec: Checkout configuration returned by the the config's fetch_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000277 method (checkout type, repository url, etc.).
278 root: The directory into which the repo expects to be checkout out.
279 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000280 assert 'type' in spec
281 checkout_type = spec['type']
282 checkout_spec = spec['%s_spec' % checkout_type]
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000283
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000284 # Use sso:// by default if the env is cog
285 if not options.protocol_override and \
286 (any(os.getcwd().startswith(x) for x in [
287 '/google/src/cloud', '/google/cog/cloud'])):
288 options.protocol_override = 'sso'
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000289
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000290 # Update solutions with protocol_override field
291 if options.protocol_override is not None:
292 for solution in checkout_spec['solutions']:
293 solution['protocol_override'] = options.protocol_override
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000294
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000295 try:
296 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
297 except KeyError:
298 return 1
299 if not options.force and checkout.exists():
300 print(
301 'Your current directory appears to already contain, or be part of, '
302 )
303 print('a checkout. "fetch" is used only to get new checkouts. Use ')
304 print('"gclient sync" to update existing checkouts.')
305 print()
306 print(
307 'Fetch also does not yet deal with partial checkouts, so if fetch')
308 print('failed, delete the checkout and start over (crbug.com/230691).')
309 return 1
310 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000311
312
313def main():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000314 args = handle_args(sys.argv)
315 spec, root = run_config_fetch(args.config, args.props)
316 return run(args, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000317
318
319if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000320 try:
321 sys.exit(main())
322 except KeyboardInterrupt:
323 sys.stderr.write('interrupted\n')
324 sys.exit(1)