blob: da4741dd0ecf2113ab828fc774ee82e8ae60fdac [file] [log] [blame]
agable@chromium.orgcc023502013-04-03 20:24:21 +00001#!/usr/bin/env python
2# 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
digit@chromium.org3596d582013-12-13 17:07:33 +000024import optparse
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):
58 pass
59
60 def init(self):
61 pass
62
63 def sync(self):
64 pass
65
Edward Lemur7a4ced22018-01-26 16:26:05 +010066 def run(self, cmd, return_stdout=False, **kwargs):
Raul Tambrec2f74c12019-03-19 05:55:53 +000067 print('Running: %s' % (' '.join(pipes.quote(x) for x in cmd)))
wtc@chromium.org38e94612014-02-12 22:19:41 +000068 if self.options.dry_run:
mmoss@chromium.org294c7832015-06-17 16:16:32 +000069 return ''
Edward Lemur7a4ced22018-01-26 16:26:05 +010070 if return_stdout:
71 return subprocess.check_output(cmd, **kwargs)
72 else:
73 subprocess.check_call(cmd, **kwargs)
74 return ''
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000075
agable@chromium.orgcc023502013-04-03 20:24:21 +000076
77class GclientCheckout(Checkout):
78
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000079 def run_gclient(self, *cmd, **kwargs):
dpranke@chromium.org6cc97a12013-04-12 06:15:58 +000080 if not spawn.find_executable('gclient'):
81 cmd_prefix = (sys.executable, os.path.join(SCRIPT_PATH, 'gclient.py'))
82 else:
83 cmd_prefix = ('gclient',)
84 return self.run(cmd_prefix + cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +000085
mmoss@chromium.org5a447762015-06-10 20:01:39 +000086 def exists(self):
87 try:
Edward Lemur7a4ced22018-01-26 16:26:05 +010088 gclient_root = self.run_gclient('root', return_stdout=True).strip()
mmoss@chromium.org5a447762015-06-10 20:01:39 +000089 return (os.path.exists(os.path.join(gclient_root, '.gclient')) or
90 os.path.exists(os.path.join(os.getcwd(), self.root)))
91 except subprocess.CalledProcessError:
92 pass
93 return os.path.exists(os.path.join(os.getcwd(), self.root))
94
agable@chromium.orgcc023502013-04-03 20:24:21 +000095
96class GitCheckout(Checkout):
97
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +000098 def run_git(self, *cmd, **kwargs):
Raul Tambrec2f74c12019-03-19 05:55:53 +000099 print('Running: git %s' % (' '.join(pipes.quote(x) for x in cmd)))
Aaron Gablebd95f412017-11-29 11:20:26 -0800100 if self.options.dry_run:
101 return ''
Dan Jacques209a6812017-07-12 11:40:20 -0700102 return git_common.run(*cmd, **kwargs)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000103
104
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000105class GclientGitCheckout(GclientCheckout, GitCheckout):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000106
digit@chromium.org3596d582013-12-13 17:07:33 +0000107 def __init__(self, options, spec, root):
108 super(GclientGitCheckout, self).__init__(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000109 assert 'solutions' in self.spec
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000110
111 def _format_spec(self):
112 def _format_literal(lit):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000113 if isinstance(lit, str) or (sys.version_info.major == 2 and
114 isinstance(lit, unicode)):
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000115 return '"%s"' % lit
116 if isinstance(lit, list):
117 return '[%s]' % ', '.join(_format_literal(i) for i in lit)
118 return '%r' % lit
119 soln_strings = []
120 for soln in self.spec['solutions']:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000121 soln_string = '\n'.join(' "%s": %s,' % (key, _format_literal(value))
122 for key, value in soln.items())
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000123 soln_strings.append(' {\n%s\n },' % soln_string)
124 gclient_spec = 'solutions = [\n%s\n]\n' % '\n'.join(soln_strings)
Dr Alex Gouaillarde1dd46f2016-11-28 15:00:04 +0800125 extra_keys = ['target_os', 'target_os_only', 'cache_dir']
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000126 gclient_spec += ''.join('%s = %s\n' % (key, _format_literal(self.spec[key]))
127 for key in extra_keys if key in self.spec)
128 return gclient_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000129
agable@chromium.orgcc023502013-04-03 20:24:21 +0000130 def init(self):
131 # Configure and do the gclient checkout.
agable@chromium.org5bde64e2014-11-25 22:15:26 +0000132 self.run_gclient('config', '--spec', self._format_spec())
jochen@chromium.org048da082014-05-06 08:32:40 +0000133 sync_cmd = ['sync']
agable@chromium.orgb98f3f22015-06-15 21:59:09 +0000134 if self.options.nohooks:
jochen@chromium.org048da082014-05-06 08:32:40 +0000135 sync_cmd.append('--nohooks')
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000136 if self.options.no_history:
137 sync_cmd.append('--no-history')
jochen@chromium.org048da082014-05-06 08:32:40 +0000138 if self.spec.get('with_branch_heads', False):
139 sync_cmd.append('--with_branch_heads')
140 self.run_gclient(*sync_cmd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000141
142 # Configure git.
143 wd = os.path.join(self.base, self.root)
wtc@chromium.org38e94612014-02-12 22:19:41 +0000144 if self.options.dry_run:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000145 print('cd %s' % wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000146 self.run_git(
147 'submodule', 'foreach',
148 'git config -f $toplevel/.git/config submodule.$name.ignore all',
149 cwd=wd)
Torne (Richard Coles)08ca04b2018-02-08 15:23:08 -0500150 if not self.options.no_history:
151 self.run_git(
152 'config', '--add', 'remote.origin.fetch',
153 '+refs/tags/*:refs/tags/*', cwd=wd)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000154 self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
155
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000156
agable@chromium.orgcc023502013-04-03 20:24:21 +0000157CHECKOUT_TYPE_MAP = {
158 'gclient': GclientCheckout,
jochen@chromium.orgd993e782013-04-11 20:03:13 +0000159 'gclient_git': GclientGitCheckout,
agable@chromium.orgcc023502013-04-03 20:24:21 +0000160 'git': GitCheckout,
161}
162
163
digit@chromium.org3596d582013-12-13 17:07:33 +0000164def CheckoutFactory(type_name, options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000165 """Factory to build Checkout class instances."""
166 class_ = CHECKOUT_TYPE_MAP.get(type_name)
167 if not class_:
168 raise KeyError('unrecognized checkout type: %s' % type_name)
digit@chromium.org3596d582013-12-13 17:07:33 +0000169 return class_(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000170
171
172#################################################
173# Utility function and file entry point.
174#################################################
175def usage(msg=None):
176 """Print help and exit."""
177 if msg:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000178 print('Error:', msg)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000179
Raul Tambrec2f74c12019-03-19 05:55:53 +0000180 print(textwrap.dedent("""\
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000181 usage: %s [options] <config> [--property=value [--property2=value2 ...]]
digit@chromium.org3596d582013-12-13 17:07:33 +0000182
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +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.
digit@chromium.org3596d582013-12-13 17:07:33 +0000186
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000187 Valid options:
188 -h, --help, help Print this message.
189 --nohooks Don't run hooks after checkout.
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000190 --force (dangerous) Don't look for existing .gclient file.
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000191 -n, --dry-run Don't run commands, only print them.
192 --no-history Perform shallow clones, don't fetch the full git history.
193
Raul Tambrec2f74c12019-03-19 05:55:53 +0000194 Valid fetch configs:""") % os.path.basename(sys.argv[0]))
thestig@chromium.org37103c92015-09-19 20:54:39 +0000195
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000196 configs_dir = os.path.join(SCRIPT_PATH, 'fetch_configs')
197 configs = [f[:-3] for f in os.listdir(configs_dir) if f.endswith('.py')]
198 configs.sort()
199 for fname in configs:
Raul Tambrec2f74c12019-03-19 05:55:53 +0000200 print(' ' + fname)
iannucci@chromium.orgcc2d3e32014-08-06 19:47:54 +0000201
agable@chromium.orgcc023502013-04-03 20:24:21 +0000202 sys.exit(bool(msg))
203
204
205def handle_args(argv):
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000206 """Gets the config name from the command line arguments."""
agable@chromium.orgcc023502013-04-03 20:24:21 +0000207 if len(argv) <= 1:
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000208 usage('Must specify a config.')
dpranke@chromium.orge3d147d2013-04-03 20:31:27 +0000209 if argv[1] in ('-h', '--help', 'help'):
210 usage()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000211
wtc@chromium.org38e94612014-02-12 22:19:41 +0000212 dry_run = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000213 nohooks = False
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000214 no_history = False
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000215 force = False
digit@chromium.org3596d582013-12-13 17:07:33 +0000216 while len(argv) >= 2:
217 arg = argv[1]
218 if not arg.startswith('-'):
219 break
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000220 argv.pop(1)
digit@chromium.org3596d582013-12-13 17:07:33 +0000221 if arg in ('-n', '--dry-run'):
wtc@chromium.org38e94612014-02-12 22:19:41 +0000222 dry_run = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000223 elif arg == '--nohooks':
224 nohooks = True
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000225 elif arg == '--no-history':
226 no_history = True
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000227 elif arg == '--force':
228 force = True
digit@chromium.org3596d582013-12-13 17:07:33 +0000229 else:
230 usage('Invalid option %s.' % arg)
dpranke@chromium.orgd88d7f52013-04-03 21:09:07 +0000231
agable@chromium.orgcc023502013-04-03 20:24:21 +0000232 def looks_like_arg(arg):
233 return arg.startswith('--') and arg.count('=') == 1
234
235 bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
236 if bad_parms:
237 usage('Got bad arguments %s' % bad_parms)
238
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000239 config = argv[1]
agable@chromium.orgcc023502013-04-03 20:24:21 +0000240 props = argv[2:]
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000241 return (
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000242 optparse.Values({
243 'dry_run': dry_run,
244 'nohooks': nohooks,
245 'no_history': no_history,
246 'force': force}),
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000247 config,
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000248 props)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000249
250
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000251def run_config_fetch(config, props, aliased=False):
252 """Invoke a config's fetch method with the passed-through args
agable@chromium.orgcc023502013-04-03 20:24:21 +0000253 and return its json output as a python object."""
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000254 config_path = os.path.abspath(
255 os.path.join(SCRIPT_PATH, 'fetch_configs', config))
256 if not os.path.exists(config_path + '.py'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000257 print("Could not find a config for %s" % config)
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000258 sys.exit(1)
259
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000260 cmd = [sys.executable, config_path + '.py', 'fetch'] + props
agable@chromium.orgcc023502013-04-03 20:24:21 +0000261 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
dpranke@chromium.org2bf328a2013-04-03 21:14:41 +0000262
agable@chromium.orgcc023502013-04-03 20:24:21 +0000263 spec = json.loads(result)
264 if 'alias' in spec:
265 assert not aliased
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000266 return run_config_fetch(
267 spec['alias']['config'], spec['alias']['props'] + props, aliased=True)
268 cmd = [sys.executable, config_path + '.py', 'root']
agable@chromium.orgcc023502013-04-03 20:24:21 +0000269 result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
270 root = json.loads(result)
271 return spec, root
272
273
digit@chromium.org3596d582013-12-13 17:07:33 +0000274def run(options, spec, root):
agable@chromium.orgcc023502013-04-03 20:24:21 +0000275 """Perform a checkout with the given type and configuration.
276
277 Args:
digit@chromium.org3596d582013-12-13 17:07:33 +0000278 options: Options instance.
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000279 spec: Checkout configuration returned by the the config's fetch_spec
agable@chromium.orgcc023502013-04-03 20:24:21 +0000280 method (checkout type, repository url, etc.).
281 root: The directory into which the repo expects to be checkout out.
282 """
283 assert 'type' in spec
284 checkout_type = spec['type']
285 checkout_spec = spec['%s_spec' % checkout_type]
286 try:
digit@chromium.org3596d582013-12-13 17:07:33 +0000287 checkout = CheckoutFactory(checkout_type, options, checkout_spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000288 except KeyError:
289 return 1
iannucci@chromium.org78faf8b2016-05-09 23:26:37 +0000290 if not options.force and checkout.exists():
Raul Tambrec2f74c12019-03-19 05:55:53 +0000291 print('Your current directory appears to already contain, or be part of, ')
292 print('a checkout. "fetch" is used only to get new checkouts. Use ')
293 print('"gclient sync" to update existing checkouts.')
294 print()
295 print('Fetch also does not yet deal with partial checkouts, so if fetch')
296 print('failed, delete the checkout and start over (crbug.com/230691).')
agable@chromium.orgcc023502013-04-03 20:24:21 +0000297 return 1
agable@chromium.org2560ea72013-04-04 01:22:38 +0000298 return checkout.init()
agable@chromium.orgcc023502013-04-03 20:24:21 +0000299
300
301def main():
luqui@chromium.orgb371a1c2015-12-04 01:42:48 +0000302 options, config, props = handle_args(sys.argv)
303 spec, root = run_config_fetch(config, props)
digit@chromium.org3596d582013-12-13 17:07:33 +0000304 return run(options, spec, root)
agable@chromium.orgcc023502013-04-03 20:24:21 +0000305
306
307if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000308 try:
309 sys.exit(main())
310 except KeyboardInterrupt:
311 sys.stderr.write('interrupted\n')
312 sys.exit(1)