blob: f20b1e8f80ae269d12c460724aeb03364380d330 [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
20import json
Aravind Vasudevan075cd762022-03-23 21:13:13 +000021import argparse
agable@chromium.orgcc023502013-04-03 20:24:21 +000022import os
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +000023import pipes
agable@chromium.orgcc023502013-04-03 20:24:21 +000024import subprocess
25import sys
agable@chromium.orgcc023502013-04-03 20:24:21 +000026
Dan Jacques209a6812017-07-12 11:40:20 -070027import git_common
28
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000029from distutils import spawn
30
agable@chromium.orgcc023502013-04-03 20:24:21 +000031SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
32
Mike Frysinger124bb8e2023-09-06 05:48:55 +000033
agable@chromium.orgcc023502013-04-03 20:24:21 +000034#################################################
35# Checkout class definitions.
36#################################################
37class Checkout(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000038 """Base class for implementing different types of checkouts.
agable@chromium.orgcc023502013-04-03 20:24:21 +000039
40 Attributes:
41 |base|: the absolute path of the directory in which this script is run.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000042 |spec|: the spec for this checkout as returned by the config. Different
agable@chromium.orgcc023502013-04-03 20:24:21 +000043 subclasses will expect different keys in this dictionary.
44 |root|: the directory into which the checkout will be performed, as returned
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +000045 by the config. This is a relative path from |base|.
agable@chromium.orgcc023502013-04-03 20:24:21 +000046 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +000047 def __init__(self, options, spec, root):
48 self.base = os.getcwd()
49 self.options = options
50 self.spec = spec
51 self.root = root
agable@chromium.orgcc023502013-04-03 20:24:21 +000052
Mike Frysinger124bb8e2023-09-06 05:48:55 +000053 def exists(self):
54 """Check does this checkout already exist on desired location"""
agable@chromium.orgcc023502013-04-03 20:24:21 +000055
Mike Frysinger124bb8e2023-09-06 05:48:55 +000056 def init(self):
57 pass
agable@chromium.orgcc023502013-04-03 20:24:21 +000058
Mike Frysinger124bb8e2023-09-06 05:48:55 +000059 def run(self, cmd, return_stdout=False, **kwargs):
60 print('Running: %s' % (' '.join(pipes.quote(x) for x in cmd)))
61 if self.options.dry_run:
62 return ''
63 if return_stdout:
64 return subprocess.check_output(cmd, **kwargs).decode()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000065
Mike Frysinger124bb8e2023-09-06 05:48:55 +000066 try:
67 subprocess.check_call(cmd, **kwargs)
68 except subprocess.CalledProcessError as e:
69 # If the subprocess failed, it likely emitted its own distress
70 # message already - don't scroll that message off the screen with a
71 # stack trace from this program as well. Emit a terse message and
72 # bail out here; otherwise a later step will try doing more work and
73 # may hide the subprocess message.
74 print('Subprocess failed with return code %d.' % e.returncode)
75 sys.exit(e.returncode)
76 return ''
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000077
agable@chromium.orgcc023502013-04-03 20:24:21 +000078
79class GclientCheckout(Checkout):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000080 def run_gclient(self, *cmd, **kwargs):
81 if not spawn.find_executable('gclient'):
82 cmd_prefix = (sys.executable, os.path.join(SCRIPT_PATH,
83 'gclient.py'))
84 else:
85 cmd_prefix = ('gclient', )
86 return self.run(cmd_prefix + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000087
Mike Frysinger124bb8e2023-09-06 05:48:55 +000088 def exists(self):
89 try:
90 gclient_root = self.run_gclient('root', return_stdout=True).strip()
91 return (os.path.exists(os.path.join(gclient_root, '.gclient'))
92 or os.path.exists(
93 os.path.join(os.getcwd(), self.root, '.git')))
94 except subprocess.CalledProcessError:
95 pass
96 return os.path.exists(os.path.join(os.getcwd(), self.root))
mmoss@chromium.org5a447762015-06-10 20:01:39 +000097
agable@chromium.orgcc023502013-04-03 20:24:21 +000098
99class GitCheckout(Checkout):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000100 def run_git(self, *cmd, **kwargs):
101 print('Running: git %s' % (' '.join(pipes.quote(x) for x in cmd)))
102 if self.options.dry_run:
103 return ''
104 return git_common.run(*cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000105
106
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000107class GclientGitCheckout(GclientCheckout, GitCheckout):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000108 def __init__(self, options, spec, root):
109 super(GclientGitCheckout, self).__init__(options, spec, root)
110 assert 'solutions' in self.spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000111
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000112 def _format_spec(self):
113 def _format_literal(lit):
114 if isinstance(lit, str):
115 return '"%s"' % lit
116 if isinstance(lit, list):
117 return '[%s]' % ', '.join(_format_literal(i) for i in lit)
118 return '%r' % lit
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000119
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000120 soln_strings = []
121 for soln in self.spec['solutions']:
122 soln_string = '\n'.join(' "%s": %s,' %
123 (key, _format_literal(value))
124 for key, value in soln.items())
125 soln_strings.append(' {\n%s\n },' % soln_string)
126 gclient_spec = 'solutions = [\n%s\n]\n' % '\n'.join(soln_strings)
127 extra_keys = ['target_os', 'target_os_only', 'cache_dir']
128 gclient_spec += ''.join('%s = %s\n' %
129 (key, _format_literal(self.spec[key]))
130 for key in extra_keys if key in self.spec)
131 return gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000132
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000133 def init(self):
134 # Configure and do the gclient checkout.
135 self.run_gclient('config', '--spec', self._format_spec())
136 sync_cmd = ['sync']
137 if self.options.nohooks:
138 sync_cmd.append('--nohooks')
139 if self.options.nohistory:
140 sync_cmd.append('--no-history')
141 if self.spec.get('with_branch_heads', False):
142 sync_cmd.append('--with_branch_heads')
143 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000144
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000145 # Configure git.
146 wd = os.path.join(self.base, self.root)
147 if self.options.dry_run:
148 print('cd %s' % wd)
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000149 if not self.options.nohistory:
150 self.run_git('config',
151 '--add',
152 'remote.origin.fetch',
153 '+refs/tags/*:refs/tags/*',
154 cwd=wd)
155 self.run_git('config', 'diff.ignoreSubmodules', 'dirty', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000156
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000157
agable@chromium.orgcc023502013-04-03 20:24:21 +0000158CHECKOUT_TYPE_MAP = {
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000159 'gclient': GclientCheckout,
160 'gclient_git': GclientGitCheckout,
161 'git': GitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000162}
163
164
digit@chromium.org3596d582013-12-13 17:07:33 +0000165def CheckoutFactory(type_name, options, spec, root):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000166 """Factory to build Checkout class instances."""
167 class_ = CHECKOUT_TYPE_MAP.get(type_name)
168 if not class_:
169 raise KeyError('unrecognized checkout type: %s' % type_name)
170 return class_(options, spec, root)
171
agable@chromium.orgcc023502013-04-03 20:24:21 +0000172
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000173def handle_args(argv):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000174 """Gets the config name from the command line arguments."""
thestig@chromium.org37103c92015-09-19 20:54:39 +0000175
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000176 configs_dir = os.path.join(SCRIPT_PATH, 'fetch_configs')
177 configs = [f[:-3] for f in os.listdir(configs_dir) if f.endswith('.py')]
178 configs.sort()
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000179
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000180 parser = argparse.ArgumentParser(
181 formatter_class=argparse.RawDescriptionHelpFormatter,
182 description='''
Aravind Vasudevan075cd762022-03-23 21:13:13 +0000183 This script can be used to download the Chromium sources. See
184 http://www.chromium.org/developers/how-tos/get-the-code
185 for full usage instructions.''',
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000186 epilog='Valid fetch configs:\n' + \
187 '\n'.join(map(lambda s: ' ' + s, configs))
188 )
agable@chromium.orgcc023502013-04-03 20:24:21 +0000189
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000190 parser.add_argument('-n',
191 '--dry-run',
192 action='store_true',
193 default=False,
194 help='Don\'t run commands, only print them.')
195 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.')
206 parser.add_argument(
207 '--force',
208 action='store_true',
209 default=False,
210 help='(dangerous) Don\'t look for existing .gclient file.')
211 parser.add_argument(
212 '-p',
213 '--protocol-override',
214 type=str,
215 default=None,
216 help='Protocol to use to fetch dependencies, defaults to https.')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000217
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000218 parser.add_argument('config',
219 type=str,
220 help="Project to fetch, e.g. chromium.")
221 parser.add_argument('props',
222 metavar='props',
223 type=str,
224 nargs=argparse.REMAINDER,
225 default=[])
agable@chromium.orgcc023502013-04-03 20:24:21 +0000226
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000227 args = parser.parse_args(argv[1:])
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000228
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000229 # props passed to config must be of the format --<name>=<value>
230 looks_like_arg = lambda arg: arg.startswith('--') and arg.count('=') == 1
231 bad_param = [x for x in args.props if not looks_like_arg(x)]
232 if bad_param:
233 print('Error: Got bad arguments %s' % bad_param)
234 parser.print_help()
235 sys.exit(1)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000236
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000237 return args
238
agable@chromium.orgcc023502013-04-03 20:24:21 +0000239
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000240def run_config_fetch(config, props, aliased=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000241 """Invoke a config's fetch method with the passed-through args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000242 and return its json output as a python object."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000243 config_path = os.path.abspath(
244 os.path.join(SCRIPT_PATH, 'fetch_configs', config))
245 if not os.path.exists(config_path + '.py'):
246 print("Could not find a config for %s" % config)
247 sys.exit(1)
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000248
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000249 cmd = [sys.executable, config_path + '.py', 'fetch'] + props
250 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000251
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000252 spec = json.loads(result.decode("utf-8"))
253 if 'alias' in spec:
254 assert not aliased
255 return run_config_fetch(spec['alias']['config'],
256 spec['alias']['props'] + props,
257 aliased=True)
258 cmd = [sys.executable, config_path + '.py', 'root']
259 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
260 root = json.loads(result.decode("utf-8"))
261 return spec, root
agable@chromium.orgcc023502013-04-03 20:24:21 +0000262
263
digit@chromium.org3596d582013-12-13 17:07:33 +0000264def run(options, spec, root):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000265 """Perform a checkout with the given type and configuration.
agable@chromium.orgcc023502013-04-03 20:24:21 +0000266
267 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000268 options: Options instance.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000269 spec: Checkout configuration returned by the the config's fetch_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000270 method (checkout type, repository url, etc.).
271 root: The directory into which the repo expects to be checkout out.
272 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000273 assert 'type' in spec
274 checkout_type = spec['type']
275 checkout_spec = spec['%s_spec' % checkout_type]
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000276
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000277 # Update solutions with protocol_override field
278 if options.protocol_override is not None:
279 for solution in checkout_spec['solutions']:
280 solution['protocol_override'] = options.protocol_override
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000281
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000282 try:
283 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
284 except KeyError:
285 return 1
286 if not options.force and checkout.exists():
287 print(
288 'Your current directory appears to already contain, or be part of, '
289 )
290 print('a checkout. "fetch" is used only to get new checkouts. Use ')
291 print('"gclient sync" to update existing checkouts.')
292 print()
293 print(
294 'Fetch also does not yet deal with partial checkouts, so if fetch')
295 print('failed, delete the checkout and start over (crbug.com/230691).')
296 return 1
297 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000298
299
300def main():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000301 args = handle_args(sys.argv)
302 spec, root = run_config_fetch(args.config, args.props)
303 return run(args, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000304
305
306if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000307 try:
308 sys.exit(main())
309 except KeyboardInterrupt:
310 sys.stderr.write('interrupted\n')
311 sys.exit(1)