blob: 6a47709a9fb1877f90d03111d771d36fa3cf9dbc [file] [log] [blame]
Josip Sokceviced6aa2b2022-01-26 18:14:05 +00001#!/usr/bin/env python3
thakis@chromium.org4f474b62012-01-18 01:31:29 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.orgba551772010-02-03 18:21:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
agabled437d762016-10-17 09:35:11 -07005"""Meta checkout dependency manager for Git."""
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006# Files
7# .gclient : Current client configuration, written by 'config' command.
8# Format is a Python script defining 'solutions', a list whose
9# entries each are maps binding the strings "name" and "url"
10# to strings specifying the name and location of the client
11# module, as well as "custom_deps" to a map similar to the
12# deps section of the DEPS file below, as well as
13# "custom_hooks" to a list similar to the hooks sections of
14# the DEPS file below.
15# .gclient_entries : A cache constructed by 'update' command. Format is a
16# Python script defining 'entries', a list of the names
17# of all modules in the client
18# <module>/DEPS : Python script defining var 'deps' as a map from each
19# requisite submodule name to a URL where it can be found (via
20# one SCM)
21#
22# Hooks
23# .gclient and DEPS files may optionally contain a list named "hooks" to
24# allow custom actions to be performed based on files that have changed in the
25# working copy as a result of a "sync"/"update" or "revert" operation. This
26# can be prevented by using --nohooks (hooks run by default). Hooks can also
27# be forced to run with the "runhooks" operation. If "sync" is run with
28# --force, all known but not suppressed hooks will run regardless of the state
29# of the working copy.
30#
31# Each item in a "hooks" list is a dict, containing these two keys:
32# "pattern" The associated value is a string containing a regular
33# expression. When a file whose pathname matches the expression
34# is checked out, updated, or reverted, the hook's "action" will
35# run.
36# "action" A list describing a command to run along with its arguments, if
37# any. An action command will run at most one time per gclient
38# invocation, regardless of how many files matched the pattern.
39# The action is executed in the same directory as the .gclient
40# file. If the first item in the list is the string "python",
41# the current Python interpreter (sys.executable) will be used
42# to run the command. If the list contains string
43# "$matching_files" it will be removed from the list and the list
44# will be extended by the list of matching files.
45# "name" An optional string specifying the group to which a hook belongs
46# for overriding and organizing.
47#
48# Example:
49# hooks = [
50# { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
51# "action": ["python", "image_indexer.py", "--all"]},
52# { "pattern": ".",
53# "name": "gyp",
54# "action": ["python", "src/build/gyp_chromium"]},
55# ]
56#
borenet@google.com2d1ee9e2013-10-15 08:13:16 +000057# Pre-DEPS Hooks
58# DEPS files may optionally contain a list named "pre_deps_hooks". These are
59# the same as normal hooks, except that they run before the DEPS are
60# processed. Pre-DEPS run with "sync" and "revert" unless the --noprehooks
61# flag is used.
rdsmith@chromium.orgd9591f02014-02-05 19:28:20 +000062#
maruel@chromium.org39c0b222013-08-17 16:57:01 +000063# Specifying a target OS
64# An optional key named "target_os" may be added to a gclient file to specify
65# one or more additional operating systems that should be considered when
Scott Grahamc4826742017-05-11 16:59:23 -070066# processing the deps_os/hooks_os dict of a DEPS file.
maruel@chromium.org39c0b222013-08-17 16:57:01 +000067#
68# Example:
69# target_os = [ "android" ]
70#
71# If the "target_os_only" key is also present and true, then *only* the
72# operating systems listed in "target_os" will be used.
73#
74# Example:
75# target_os = [ "ios" ]
76# target_os_only = True
Tom Andersonc31ae0b2018-02-06 14:48:56 -080077#
78# Specifying a target CPU
79# To specify a target CPU, the variables target_cpu and target_cpu_only
Quinten Yearsley925cedb2020-04-13 17:49:39 +000080# are available and are analogous to target_os and target_os_only.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000081
maruel@chromium.org39c0b222013-08-17 16:57:01 +000082__version__ = '0.7'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000083
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000084import copy
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +000085import json
maruel@chromium.org754960e2009-09-21 12:31:05 +000086import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000087import optparse
88import os
bradnelson@google.com4949dab2012-04-19 16:41:07 +000089import platform
maruel@chromium.org621939b2010-08-10 20:12:00 +000090import posixpath
msb@chromium.org2e38de72009-09-28 17:04:47 +000091import pprint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000092import re
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000093import sys
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000094import time
Gavin Mak65c49b12023-08-24 18:06:42 +000095import urllib.parse
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000096
Yiwei Zhang52353702023-09-18 15:53:52 +000097from collections.abc import Collection, Mapping, Sequence
98
Tom Andersonc31ae0b2018-02-06 14:48:56 -080099import detect_host_arch
maruel@chromium.org35625c72011-03-23 17:34:02 +0000100import fix_encoding
Aravind Vasudevanb8164182023-08-25 21:49:12 +0000101import git_common
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200102import gclient_eval
Nico Weber09e0b382019-03-11 16:54:07 +0000103import gclient_paths
Gavin Mak65c49b12023-08-24 18:06:42 +0000104import gclient_scm
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000105import gclient_utils
szager@chromium.org848fd492014-04-09 19:06:44 +0000106import git_cache
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000107import metrics
Edward Lemur40764b02018-07-20 18:50:29 +0000108import metrics_utils
Joanna Wange36c6bb2023-08-30 22:09:59 +0000109import scm as scm_git
Gavin Mak65c49b12023-08-24 18:06:42 +0000110import setup_color
maruel@chromium.org39c0b222013-08-17 16:57:01 +0000111import subcommand
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000112import subprocess2
Gavin Mak65c49b12023-08-24 18:06:42 +0000113from third_party.repo.progress import Progress
Aaron Gableac9b0f32019-04-18 17:38:37 +0000114
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000115# TODO: Should fix these warnings.
116# pylint: disable=line-too-long
Aaron Gableac9b0f32019-04-18 17:38:37 +0000117
Henrique Ferreiro4ef32212019-04-29 23:32:31 +0000118DEPOT_TOOLS_DIR = os.path.dirname(os.path.abspath(os.path.realpath(__file__)))
119
Robert Iannuccia19649b2018-06-29 16:31:45 +0000120# Singleton object to represent an unset cache_dir (as opposed to a disabled
121# one, e.g. if a spec explicitly says `cache_dir = None`.)
122UNSET_CACHE_DIR = object()
123
Joanna Wang01870792022-08-01 19:02:57 +0000124PREVIOUS_CUSTOM_VARS_FILE = '.gclient_previous_custom_vars'
125PREVIOUS_SYNC_COMMITS_FILE = '.gclient_previous_sync_commits'
Robert Iannuccia19649b2018-06-29 16:31:45 +0000126
Joanna Wangf3edc502022-07-20 00:12:10 +0000127PREVIOUS_SYNC_COMMITS = 'GCLIENT_PREVIOUS_SYNC_COMMITS'
Joanna Wang66286612022-06-30 19:59:13 +0000128
Joanna Wanga84a16b2022-07-27 18:52:17 +0000129NO_SYNC_EXPERIMENT = 'no-sync'
130
Gavin Mak50b27a52023-09-19 22:44:59 +0000131PRECOMMIT_HOOK_VAR = 'GCLIENT_PRECOMMIT'
132
Joanna Wang66286612022-06-30 19:59:13 +0000133
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200134class GNException(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000135 pass
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200136
137
Aravind Vasudevaned935cf2023-08-24 23:52:20 +0000138def ToGNString(value):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000139 """Returns a stringified GN equivalent of the Python value."""
140 if isinstance(value, str):
141 if value.find('\n') >= 0:
142 raise GNException("Trying to print a string with a newline in it.")
143 return '"' + \
144 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
145 '"'
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200146
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000147 if isinstance(value, bool):
148 if value:
149 return "true"
150 return "false"
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200151
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000152 # NOTE: some type handling removed compared to chromium/src copy.
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200153
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000154 raise GNException("Unsupported type when printing to GN.")
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200155
156
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200157class Hook(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000158 """Descriptor of command ran before/after sync or on demand."""
159 def __init__(self,
160 action,
161 pattern=None,
162 name=None,
163 cwd=None,
164 condition=None,
165 variables=None,
166 verbose=False,
167 cwd_base=None):
168 """Constructor.
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200169
170 Arguments:
Gavin Mak65c49b12023-08-24 18:06:42 +0000171 action (list of str): argv of the command to run
172 pattern (str regex): noop with git; deprecated
173 name (str): optional name; no effect on operation
174 cwd (str): working directory to use
175 condition (str): condition when to run the hook
Paweł Hajdan, Jr032d5452017-06-22 20:43:53 +0200176 variables (dict): variables for evaluating the condition
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200177 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000178 self._action = gclient_utils.freeze(action)
179 self._pattern = pattern
180 self._name = name
181 self._cwd = cwd
182 self._condition = condition
183 self._variables = variables
184 self._verbose = verbose
185 self._cwd_base = cwd_base
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200186
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000187 @staticmethod
188 def from_dict(d,
189 variables=None,
190 verbose=False,
191 conditions=None,
192 cwd_base=None):
193 """Creates a Hook instance from a dict like in the DEPS file."""
194 # Merge any local and inherited conditions.
195 gclient_eval.UpdateCondition(d, 'and', conditions)
196 return Hook(
197 d['action'],
198 d.get('pattern'),
199 d.get('name'),
200 d.get('cwd'),
201 d.get('condition'),
202 variables=variables,
203 # Always print the header if not printing to a TTY.
204 verbose=verbose or not setup_color.IS_TTY,
205 cwd_base=cwd_base)
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200206
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000207 @property
208 def action(self):
209 return self._action
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200210
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000211 @property
212 def pattern(self):
213 return self._pattern
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200214
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000215 @property
216 def name(self):
217 return self._name
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200218
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000219 @property
220 def condition(self):
221 return self._condition
Paweł Hajdan, Jrecf53fe2017-09-29 18:28:49 +0200222
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000223 @property
224 def effective_cwd(self):
225 cwd = self._cwd_base
226 if self._cwd:
227 cwd = os.path.join(cwd, self._cwd)
228 return cwd
Corentin Walleza68660d2018-09-10 17:33:24 +0000229
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000230 def matches(self, file_list):
231 """Returns true if the pattern matches any of files in the list."""
232 if not self._pattern:
233 return True
234 pattern = re.compile(self._pattern)
235 return bool([f for f in file_list if pattern.search(f)])
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200236
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000237 def run(self):
238 """Executes the hook's command (provided the condition is met)."""
239 if (self._condition and not gclient_eval.EvaluateCondition(
240 self._condition, self._variables)):
241 return
Paweł Hajdan, Jr032d5452017-06-22 20:43:53 +0200242
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000243 cmd = list(self._action)
Paweł Hajdan, Jrfc6196b2017-07-27 13:15:25 +0200244
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000245 if cmd[0] == 'vpython3' and _detect_host_os() == 'win':
246 cmd[0] += '.bat'
Paweł Hajdan, Jrc9364392017-06-14 17:11:56 +0200247
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000248 exit_code = 2
249 try:
250 start_time = time.time()
251 gclient_utils.CheckCallAndFilter(cmd,
252 cwd=self.effective_cwd,
253 print_stdout=True,
254 show_header=True,
255 always_show_header=self._verbose)
256 exit_code = 0
257 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
258 # Use a discrete exit status code of 2 to indicate that a hook
259 # action failed. Users of this script may wish to treat hook action
260 # failures differently from VC failures.
261 print('Error: %s' % str(e), file=sys.stderr)
262 sys.exit(exit_code)
263 finally:
264 elapsed_time = time.time() - start_time
265 metrics.collector.add_repeated(
266 'hooks', {
267 'action':
268 gclient_utils.CommandToStr(cmd),
269 'name':
270 self._name,
271 'cwd':
272 os.path.relpath(os.path.normpath(self.effective_cwd),
273 self._cwd_base),
274 'condition':
275 self._condition,
276 'execution_time':
277 elapsed_time,
278 'exit_code':
279 exit_code,
280 })
281 if elapsed_time > 10:
282 print("Hook '%s' took %.2f secs" %
283 (gclient_utils.CommandToStr(cmd), elapsed_time))
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200284
285
Paweł Hajdan, Jrfc6196b2017-07-27 13:15:25 +0200286class DependencySettings(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000287 """Immutable configuration settings."""
288 def __init__(self, parent, url, managed, custom_deps, custom_vars,
289 custom_hooks, deps_file, should_process, relative, condition):
290 # These are not mutable:
291 self._parent = parent
292 self._deps_file = deps_file
293 self._url = url
294 # The condition as string (or None). Useful to keep e.g. for flatten.
295 self._condition = condition
296 # 'managed' determines whether or not this dependency is synced/updated
297 # by gclient after gclient checks it out initially. The difference
298 # between 'managed' and 'should_process' is that the user specifies
299 # 'managed' via the --unmanaged command-line flag or a .gclient config,
300 # where 'should_process' is dynamically set by gclient if it goes over
301 # its recursion limit and controls gclient's behavior so it does not
302 # misbehave.
303 self._managed = managed
304 self._should_process = should_process
305 # If this is a recursed-upon sub-dependency, and the parent has
306 # use_relative_paths set, then this dependency should check out its own
307 # dependencies relative to that parent's path for this, rather than
308 # relative to the .gclient file.
309 self._relative = relative
310 # This is a mutable value which has the list of 'target_os' OSes listed
311 # in the current deps file.
312 self.local_target_os = None
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000313
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000314 # These are only set in .gclient and not in DEPS files.
315 self._custom_vars = custom_vars or {}
316 self._custom_deps = custom_deps or {}
317 self._custom_hooks = custom_hooks or []
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000318
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000319 # Post process the url to remove trailing slashes.
320 if isinstance(self.url, str):
321 # urls are sometime incorrectly written as proto://host/path/@rev.
322 # Replace it to proto://host/path@rev.
323 self.set_url(self.url.replace('/@', '@'))
324 elif not isinstance(self.url, (None.__class__)):
325 raise gclient_utils.Error(
326 ('dependency url must be either string or None, '
327 'instead of %s') % self.url.__class__.__name__)
Edward Lemure7273d22018-05-10 19:13:51 -0400328
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000329 # Make any deps_file path platform-appropriate.
330 if self._deps_file:
331 for sep in ['/', '\\']:
332 self._deps_file = self._deps_file.replace(sep, os.sep)
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000333
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000334 @property
335 def deps_file(self):
336 return self._deps_file
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000337
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000338 @property
339 def managed(self):
340 return self._managed
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000341
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000342 @property
343 def parent(self):
344 return self._parent
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000345
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000346 @property
347 def root(self):
348 """Returns the root node, a GClient object."""
349 if not self.parent:
350 # This line is to signal pylint that it could be a GClient instance.
351 return self or GClient(None, None)
352 return self.parent.root
maruel@chromium.org8c0d9582011-10-03 21:36:01 +0000353
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000354 @property
355 def should_process(self):
356 """True if this dependency should be processed, i.e. checked out."""
357 return self._should_process
Michael Mossd683d7c2018-06-15 05:05:17 +0000358
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000359 @property
360 def custom_vars(self):
361 return self._custom_vars.copy()
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000362
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000363 @property
364 def custom_deps(self):
365 return self._custom_deps.copy()
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000366
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000367 @property
368 def custom_hooks(self):
369 return self._custom_hooks[:]
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000370
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000371 @property
372 def url(self):
373 """URL after variable expansion."""
374 return self._url
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000375
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000376 @property
377 def condition(self):
378 return self._condition
Paweł Hajdan, Jrf69860b2017-06-05 20:24:28 +0200379
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000380 @property
381 def target_os(self):
382 if self.local_target_os is not None:
383 return tuple(set(self.local_target_os).union(self.parent.target_os))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000384
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000385 return self.parent.target_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +0000386
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000387 @property
388 def target_cpu(self):
389 return self.parent.target_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -0800390
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000391 def set_url(self, url):
392 self._url = url
Edward Lemure7273d22018-05-10 19:13:51 -0400393
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000394 def get_custom_deps(self, name, url):
395 """Returns a custom deps if applicable."""
396 if self.parent:
397 url = self.parent.get_custom_deps(name, url)
398 # None is a valid return value to disable a dependency.
399 return self.custom_deps.get(name, url)
maruel@chromium.org8c0d9582011-10-03 21:36:01 +0000400
maruel@chromium.org064186c2011-09-27 23:53:33 +0000401
402class Dependency(gclient_utils.WorkItem, DependencySettings):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000403 """Object that represents a dependency checkout."""
404 def __init__(self,
405 parent,
406 name,
407 url,
408 managed,
409 custom_deps,
410 custom_vars,
411 custom_hooks,
412 deps_file,
413 should_process,
414 should_recurse,
415 relative,
416 condition,
417 protocol='https',
418 git_dependencies_state=gclient_eval.DEPS,
419 print_outbuf=False):
420 gclient_utils.WorkItem.__init__(self, name)
421 DependencySettings.__init__(self, parent, url, managed, custom_deps,
422 custom_vars, custom_hooks, deps_file,
423 should_process, relative, condition)
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000424
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000425 # This is in both .gclient and DEPS files:
426 self._deps_hooks = []
maruel@chromium.org68988972011-09-20 14:11:42 +0000427
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000428 self._pre_deps_hooks = []
maruel@chromium.org68988972011-09-20 14:11:42 +0000429
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000430 # Calculates properties:
431 self._dependencies = []
432 self._vars = {}
borenet@google.com2d1ee9e2013-10-15 08:13:16 +0000433
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000434 # A cache of the files affected by the current operation, necessary for
435 # hooks.
436 self._file_list = []
437 # List of host names from which dependencies are allowed.
438 # Default is an empty set, meaning unspecified in DEPS file, and hence
439 # all hosts will be allowed. Non-empty set means allowlist of hosts.
440 # allowed_hosts var is scoped to its DEPS file, and so it isn't
441 # recursive.
442 self._allowed_hosts = frozenset()
443 self._gn_args_from = None
444 # Spec for .gni output to write (if any).
445 self._gn_args_file = None
446 self._gn_args = []
447 # If it is not set to True, the dependency wasn't processed for its
448 # child dependency, i.e. its DEPS wasn't read.
449 self._deps_parsed = False
450 # This dependency has been processed, i.e. checked out
451 self._processed = False
452 # This dependency had its pre-DEPS hooks run
453 self._pre_deps_hooks_ran = False
454 # This dependency had its hook run
455 self._hooks_ran = False
456 # This is the scm used to checkout self.url. It may be used by
457 # dependencies to get the datetime of the revision we checked out.
458 self._used_scm = None
459 self._used_revision = None
460 # The actual revision we ended up getting, or None if that information
461 # is unavailable
462 self._got_revision = None
463 # Whether this dependency should use relative paths.
464 self._use_relative_paths = False
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200465
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000466 # recursedeps is a mutable value that selectively overrides the default
467 # 'no recursion' setting on a dep-by-dep basis.
468 #
469 # It will be a dictionary of {deps_name: depfile_namee}
470 self.recursedeps = {}
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000471
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000472 # Whether we should process this dependency's DEPS file.
473 self._should_recurse = should_recurse
Edward Lemurfbb06aa2018-06-11 20:43:06 +0000474
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000475 # Whether we should sync git/cipd dependencies and hooks from the
476 # DEPS file.
477 # This is set based on skip_sync_revisions and must be done
478 # after the patch refs are applied.
479 # If this is False, we will still run custom_hooks and process
480 # custom_deps, if any.
481 self._should_sync = True
Edward Lemure7273d22018-05-10 19:13:51 -0400482
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000483 self._OverrideUrl()
484 # This is inherited from WorkItem. We want the URL to be a resource.
485 if self.url and isinstance(self.url, str):
486 # The url is usually given to gclient either as https://blah@123
487 # or just https://blah. The @123 portion is irrelevant.
488 self.resources.append(self.url.split('@')[0])
Joanna Wang18af7ef2022-07-01 16:51:00 +0000489
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000490 # Controls whether we want to print git's output when we first clone the
491 # dependency
492 self.print_outbuf = print_outbuf
cmp@chromium.orge84ac912014-06-30 23:14:35 +0000493
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000494 self.protocol = protocol
495 self.git_dependencies_state = git_dependencies_state
Edward Lemur231f5ea2018-01-31 19:02:36 +0100496
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000497 if not self.name and self.parent:
498 raise gclient_utils.Error('Dependency without name')
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000499
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000500 def _OverrideUrl(self):
501 """Resolves the parsed url from the parent hierarchy."""
502 parsed_url = self.get_custom_deps(
503 self._name.replace(os.sep, posixpath.sep) \
504 if self._name else self._name, self.url)
505 if parsed_url != self.url:
506 logging.info('Dependency(%s)._OverrideUrl(%s) -> %s', self._name,
507 self.url, parsed_url)
508 self.set_url(parsed_url)
509 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000510
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000511 if self.url is None:
512 logging.info('Dependency(%s)._OverrideUrl(None) -> None',
513 self._name)
514 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000515
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000516 if not isinstance(self.url, str):
517 raise gclient_utils.Error('Unknown url type')
Michael Mossd683d7c2018-06-15 05:05:17 +0000518
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000519 # self.url is a local path
520 path, at, rev = self.url.partition('@')
521 if os.path.isdir(path):
522 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000523
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000524 # self.url is a URL
525 parsed_url = urllib.parse.urlparse(self.url)
526 if parsed_url[0] or re.match(r'^\w+\@[\w\.-]+\:[\w\/]+', parsed_url[2]):
527 return
Edward Lemur1f392b82019-11-15 22:40:11 +0000528
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000529 # self.url is relative to the parent's URL.
530 if not path.startswith('/'):
531 raise gclient_utils.Error(
532 'relative DEPS entry \'%s\' must begin with a slash' % self.url)
Edward Lemur1f392b82019-11-15 22:40:11 +0000533
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000534 parent_url = self.parent.url
535 parent_path = self.parent.url.split('@')[0]
536 if os.path.isdir(parent_path):
537 # Parent's URL is a local path. Get parent's URL dirname and append
538 # self.url.
539 parent_path = os.path.dirname(parent_path)
540 parsed_url = parent_path + path.replace('/', os.sep) + at + rev
541 else:
542 # Parent's URL is a URL. Get parent's URL, strip from the last '/'
543 # (equivalent to unix dirname) and append self.url.
544 parsed_url = parent_url[:parent_url.rfind('/')] + self.url
Edward Lemur1f392b82019-11-15 22:40:11 +0000545
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000546 logging.info('Dependency(%s)._OverrideUrl(%s) -> %s', self.name,
547 self.url, parsed_url)
548 self.set_url(parsed_url)
Edward Lemur1f392b82019-11-15 22:40:11 +0000549
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000550 def PinToActualRevision(self):
551 """Updates self.url to the revision checked out on disk."""
552 if self.url is None:
553 return
554 url = None
555 scm = self.CreateSCM()
556 if scm.name == 'cipd':
557 revision = scm.revinfo(None, None, None)
558 package = self.GetExpandedPackageName()
559 url = '%s/p/%s/+/%s' % (scm.GetActualRemoteURL(None), package,
560 revision)
Edward Lemur1f392b82019-11-15 22:40:11 +0000561
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000562 if os.path.isdir(scm.checkout_path):
563 revision = scm.revinfo(None, None, None)
564 url = '%s@%s' % (gclient_utils.SplitUrlRevision(
565 self.url)[0], revision)
566 self.set_url(url)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +0000567
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000568 def ToLines(self):
569 # () -> Sequence[str]
570 """Returns strings representing the deps (info, graphviz line)"""
571 s = []
572 condition_part = ([' "condition": %r,' %
573 self.condition] if self.condition else [])
574 s.extend([
575 ' # %s' % self.hierarchy(include_url=False),
576 ' "%s": {' % (self.name, ),
577 ' "url": "%s",' % (self.url, ),
578 ] + condition_part + [
579 ' },',
580 '',
581 ])
582 return s
Edward Lemure7273d22018-05-10 19:13:51 -0400583
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000584 @property
585 def requirements(self):
586 """Calculate the list of requirements."""
587 requirements = set()
588 # self.parent is implicitly a requirement. This will be recursive by
589 # definition.
590 if self.parent and self.parent.name:
591 requirements.add(self.parent.name)
John Budorick0f7b2002018-01-19 15:46:17 -0800592
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000593 # For a tree with at least 2 levels*, the leaf node needs to depend
594 # on the level higher up in an orderly way.
595 # This becomes messy for >2 depth as the DEPS file format is a
596 # dictionary, thus unsorted, while the .gclient format is a list thus
597 # sorted.
598 #
599 # Interestingly enough, the following condition only works in the case
600 # we want: self is a 2nd level node. 3rd level node wouldn't need this
601 # since they already have their parent as a requirement.
602 if self.parent and self.parent.parent and not self.parent.parent.parent:
603 requirements |= set(i.name for i in self.root.dependencies
604 if i.name)
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000605
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000606 if self.name:
607 requirements |= set(
608 obj.name for obj in self.root.subtree(False)
609 if (obj is not self and obj.name
610 and self.name.startswith(posixpath.join(obj.name, ''))))
611 requirements = tuple(sorted(requirements))
612 logging.info('Dependency(%s).requirements = %s' %
613 (self.name, requirements))
614 return requirements
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000615
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000616 @property
617 def should_recurse(self):
618 return self._should_recurse
maruel@chromium.org470b5432011-10-11 18:18:19 +0000619
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000620 def verify_validity(self):
621 """Verifies that this Dependency is fine to add as a child of another one.
maruel@chromium.org470b5432011-10-11 18:18:19 +0000622
623 Returns True if this entry should be added, False if it is a duplicate of
624 another entry.
625 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000626 logging.info('Dependency(%s).verify_validity()' % self.name)
627 if self.name in [s.name for s in self.parent.dependencies]:
628 raise gclient_utils.Error(
629 'The same name "%s" appears multiple times in the deps section'
630 % self.name)
631 if not self.should_process:
632 # Return early, no need to set requirements.
633 return not any(d.name == self.name for d in self.root.subtree(True))
maruel@chromium.org470b5432011-10-11 18:18:19 +0000634
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000635 # This require a full tree traversal with locks.
636 siblings = [d for d in self.root.subtree(False) if d.name == self.name]
637 for sibling in siblings:
638 # Allow to have only one to be None or ''.
639 if self.url != sibling.url and bool(self.url) == bool(sibling.url):
640 raise gclient_utils.Error(
641 ('Dependency %s specified more than once:\n'
642 ' %s [%s]\n'
643 'vs\n'
644 ' %s [%s]') % (self.name, sibling.hierarchy(),
645 sibling.url, self.hierarchy(), self.url))
646 # In theory we could keep it as a shadow of the other one. In
647 # practice, simply ignore it.
648 logging.warning("Won't process duplicate dependency %s" % sibling)
649 return False
650 return True
maruel@chromium.org064186c2011-09-27 23:53:33 +0000651
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000652 def _postprocess_deps(self, deps, rel_prefix):
653 # type: (Mapping[str, Mapping[str, str]], str) ->
654 # Mapping[str, Mapping[str, str]]
655 """Performs post-processing of deps compared to what's in the DEPS file."""
656 # If we don't need to sync, only process custom_deps, if any.
657 if not self._should_sync:
658 if not self.custom_deps:
659 return {}
Paweł Hajdan, Jr4426eaf2017-06-13 12:25:47 +0200660
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000661 processed_deps = {}
662 for dep_name, dep_info in self.custom_deps.items():
663 if dep_info and not dep_info.endswith('@unmanaged'):
664 if dep_name in deps:
665 # custom_deps that should override an existing deps gets
666 # applied in the Dependency itself with _OverrideUrl().
667 processed_deps[dep_name] = deps[dep_name]
668 else:
669 processed_deps[dep_name] = {
670 'url': dep_info,
671 'dep_type': 'git'
672 }
673 else:
674 processed_deps = dict(deps)
Joanna Wang18af7ef2022-07-01 16:51:00 +0000675
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000676 # If a line is in custom_deps, but not in the solution, we want to
677 # append this line to the solution.
678 for dep_name, dep_info in self.custom_deps.items():
679 # Don't add it to the solution for the values of "None" and
680 # "unmanaged" in order to force these kinds of custom_deps to
681 # act as revision overrides (via revision_overrides). Having
682 # them function as revision overrides allows them to be applied
683 # to recursive dependencies. https://crbug.com/1031185
684 if (dep_name not in processed_deps and dep_info
685 and not dep_info.endswith('@unmanaged')):
686 processed_deps[dep_name] = {
687 'url': dep_info,
688 'dep_type': 'git'
689 }
Edward Lemur16f4bad2018-05-16 16:53:49 -0400690
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000691 # Make child deps conditional on any parent conditions. This ensures
692 # that, when flattened, recursed entries have the correct restrictions,
693 # even if not explicitly set in the recursed DEPS file. For instance, if
694 # "src/ios_foo" is conditional on "checkout_ios=True", then anything
695 # recursively included by "src/ios_foo/DEPS" should also require
696 # "checkout_ios=True".
697 if self.condition:
698 for value in processed_deps.values():
699 gclient_eval.UpdateCondition(value, 'and', self.condition)
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200700
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000701 if not rel_prefix:
702 return processed_deps
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200703
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000704 logging.warning('use_relative_paths enabled.')
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200705 rel_deps = {}
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000706 for d, url in processed_deps.items():
707 # normpath is required to allow DEPS to use .. in their
708 # dependency local path.
709 # We are following the same pattern when use_relative_paths = False,
710 # which uses slashes.
711 rel_deps[os.path.normpath(os.path.join(rel_prefix, d)).replace(
712 os.path.sep, '/')] = url
713 logging.warning('Updating deps by prepending %s.', rel_prefix)
714 return rel_deps
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200715
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000716 def _deps_to_objects(self, deps, use_relative_paths):
717 # type: (Mapping[str, Mapping[str, str]], bool) -> Sequence[Dependency]
718 """Convert a deps dict to a list of Dependency objects."""
719 deps_to_add = []
720 cached_conditions = {}
721 for name, dep_value in deps.items():
722 should_process = self.should_process
723 if dep_value is None:
724 continue
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200725
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000726 condition = dep_value.get('condition')
727 dep_type = dep_value.get('dep_type')
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000728
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000729 if condition and not self._get_option('process_all_deps', False):
730 if condition not in cached_conditions:
731 cached_conditions[
732 condition] = gclient_eval.EvaluateCondition(
733 condition, self.get_vars())
734 should_process = should_process and cached_conditions[condition]
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000735
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000736 # The following option is only set by the 'revinfo' command.
737 if self._get_option('ignore_dep_type', None) == dep_type:
738 continue
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000739
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000740 if dep_type == 'cipd':
741 cipd_root = self.GetCipdRoot()
742 for package in dep_value.get('packages', []):
743 deps_to_add.append(
744 CipdDependency(parent=self,
745 name=name,
746 dep_value=package,
747 cipd_root=cipd_root,
748 custom_vars=self.custom_vars,
749 should_process=should_process,
750 relative=use_relative_paths,
751 condition=condition))
752 else:
753 url = dep_value.get('url')
754 deps_to_add.append(
755 GitDependency(
756 parent=self,
757 name=name,
758 # Update URL with scheme in protocol_override
759 url=GitDependency.updateProtocol(url, self.protocol),
760 managed=True,
761 custom_deps=None,
762 custom_vars=self.custom_vars,
763 custom_hooks=None,
764 deps_file=self.recursedeps.get(name, self.deps_file),
765 should_process=should_process,
766 should_recurse=name in self.recursedeps,
767 relative=use_relative_paths,
768 condition=condition,
769 protocol=self.protocol,
770 git_dependencies_state=self.git_dependencies_state))
Michael Spang0e99b9b2020-08-12 13:34:48 +0000771
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000772 # TODO(crbug.com/1341285): Understand why we need this and remove
773 # it if we don't.
774 deps_to_add.sort(key=lambda x: x.name)
775 return deps_to_add
Corentin Walleza68660d2018-09-10 17:33:24 +0000776
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000777 def ParseDepsFile(self):
778 # type: () -> None
779 """Parses the DEPS file for this dependency."""
780 assert not self.deps_parsed
781 assert not self.dependencies
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000782
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000783 deps_content = None
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000784
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000785 # First try to locate the configured deps file. If it's missing,
786 # fallback to DEPS.
787 deps_files = [self.deps_file]
788 if 'DEPS' not in deps_files:
789 deps_files.append('DEPS')
790 for deps_file in deps_files:
791 filepath = os.path.join(self.root.root_dir, self.name, deps_file)
792 if os.path.isfile(filepath):
793 logging.info('ParseDepsFile(%s): %s file found at %s',
794 self.name, deps_file, filepath)
795 break
796 logging.info('ParseDepsFile(%s): No %s file found at %s', self.name,
797 deps_file, filepath)
borenet@google.com2d1ee9e2013-10-15 08:13:16 +0000798
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000799 if os.path.isfile(filepath):
800 deps_content = gclient_utils.FileRead(filepath)
801 logging.debug('ParseDepsFile(%s) read:\n%s', self.name,
802 deps_content)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +0000803
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000804 local_scope = {}
805 if deps_content:
806 try:
807 local_scope = gclient_eval.Parse(deps_content, filepath,
808 self.get_vars(),
809 self.get_builtin_vars())
810 except SyntaxError as e:
811 gclient_utils.SyntaxErrorToError(filepath, e)
812
813 if 'git_dependencies' in local_scope:
814 self.git_dependencies_state = local_scope['git_dependencies']
815
816 if 'allowed_hosts' in local_scope:
817 try:
818 self._allowed_hosts = frozenset(
819 local_scope.get('allowed_hosts'))
820 except TypeError: # raised if non-iterable
821 pass
822 if not self._allowed_hosts:
823 logging.warning("allowed_hosts is specified but empty %s",
824 self._allowed_hosts)
825 raise gclient_utils.Error(
826 'ParseDepsFile(%s): allowed_hosts must be absent '
827 'or a non-empty iterable' % self.name)
828
829 self._gn_args_from = local_scope.get('gclient_gn_args_from')
830 self._gn_args_file = local_scope.get('gclient_gn_args_file')
831 self._gn_args = local_scope.get('gclient_gn_args', [])
832 # It doesn't make sense to set all of these, since setting gn_args_from
833 # to another DEPS will make gclient ignore any other local gn_args*
834 # settings.
835 assert not (self._gn_args_from and self._gn_args_file), \
836 'Only specify one of "gclient_gn_args_from" or ' \
837 '"gclient_gn_args_file + gclient_gn_args".'
838
839 self._vars = local_scope.get('vars', {})
840 if self.parent:
841 for key, value in self.parent.get_vars().items():
842 if key in self._vars:
843 self._vars[key] = value
844 # Since we heavily post-process things, freeze ones which should
845 # reflect original state of DEPS.
846 self._vars = gclient_utils.freeze(self._vars)
847
848 # If use_relative_paths is set in the DEPS file, regenerate
849 # the dictionary using paths relative to the directory containing
850 # the DEPS file. Also update recursedeps if use_relative_paths is
851 # enabled.
852 # If the deps file doesn't set use_relative_paths, but the parent did
853 # (and therefore set self.relative on this Dependency object), then we
854 # want to modify the deps and recursedeps by prepending the parent
855 # directory of this dependency.
856 self._use_relative_paths = local_scope.get('use_relative_paths', False)
857 rel_prefix = None
858 if self._use_relative_paths:
859 rel_prefix = self.name
860 elif self._relative:
861 rel_prefix = os.path.dirname(self.name)
862
863 if 'recursion' in local_scope:
864 logging.warning('%s: Ignoring recursion = %d.', self.name,
865 local_scope['recursion'])
866
867 if 'recursedeps' in local_scope:
868 for ent in local_scope['recursedeps']:
869 if isinstance(ent, str):
870 self.recursedeps[ent] = self.deps_file
871 else: # (depname, depsfilename)
872 self.recursedeps[ent[0]] = ent[1]
873 logging.warning('Found recursedeps %r.', repr(self.recursedeps))
874
875 if rel_prefix:
876 logging.warning('Updating recursedeps by prepending %s.',
877 rel_prefix)
878 rel_deps = {}
879 for depname, options in self.recursedeps.items():
880 rel_deps[os.path.normpath(os.path.join(rel_prefix,
881 depname)).replace(
882 os.path.sep,
883 '/')] = options
884 self.recursedeps = rel_deps
885 # To get gn_args from another DEPS, that DEPS must be recursed into.
886 if self._gn_args_from:
887 assert self.recursedeps and self._gn_args_from in self.recursedeps, \
888 'The "gclient_gn_args_from" value must be in recursedeps.'
889
890 # If present, save 'target_os' in the local_target_os property.
891 if 'target_os' in local_scope:
892 self.local_target_os = local_scope['target_os']
893
894 deps = local_scope.get('deps', {})
895
896 # If dependencies are configured within git submodules, add them to
897 # deps. We don't add for SYNC since we expect submodules to be in sync.
898 if self.git_dependencies_state == gclient_eval.SUBMODULES:
899 deps.update(self.ParseGitSubmodules())
900
901 deps_to_add = self._deps_to_objects(
902 self._postprocess_deps(deps, rel_prefix), self._use_relative_paths)
903
904 # compute which working directory should be used for hooks
905 if local_scope.get('use_relative_hooks', False):
906 print('use_relative_hooks is deprecated, please remove it from '
907 '%s DEPS. (it was merged in use_relative_paths)' % self.name,
908 file=sys.stderr)
909
910 hooks_cwd = self.root.root_dir
911 if self._use_relative_paths:
912 hooks_cwd = os.path.join(hooks_cwd, self.name)
913 elif self._relative:
914 hooks_cwd = os.path.join(hooks_cwd, os.path.dirname(self.name))
915 logging.warning('Using hook base working directory: %s.', hooks_cwd)
916
917 # Only add all hooks if we should sync, otherwise just add custom hooks.
918 # override named sets of hooks by the custom hooks
919 hooks_to_run = []
920 if self._should_sync:
921 hook_names_to_suppress = [
922 c.get('name', '') for c in self.custom_hooks
923 ]
924 for hook in local_scope.get('hooks', []):
925 if hook.get('name', '') not in hook_names_to_suppress:
926 hooks_to_run.append(hook)
927
928 # add the replacements and any additions
929 for hook in self.custom_hooks:
930 if 'action' in hook:
931 hooks_to_run.append(hook)
932
933 if self.should_recurse and deps_to_add:
934 self._pre_deps_hooks = [
935 Hook.from_dict(hook,
936 variables=self.get_vars(),
937 verbose=True,
938 conditions=self.condition,
939 cwd_base=hooks_cwd)
940 for hook in local_scope.get('pre_deps_hooks', [])
941 ]
942
943 self.add_dependencies_and_close(deps_to_add,
944 hooks_to_run,
945 hooks_cwd=hooks_cwd)
946 logging.info('ParseDepsFile(%s) done' % self.name)
947
948 def ParseGitSubmodules(self):
949 # type: () -> Mapping[str, str]
950 """
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000951 Parses git submodules and returns a dict of path to DEPS git url entries.
952
953 e.g {<path>: <url>@<commit_hash>}
954 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000955 cwd = os.path.join(self.root.root_dir, self.name)
956 filepath = os.path.join(cwd, '.gitmodules')
957 if not os.path.isfile(filepath):
958 logging.warning('ParseGitSubmodules(): No .gitmodules found at %s',
959 filepath)
960 return {}
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000961
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000962 # Get .gitmodules fields
963 gitmodules_entries = subprocess2.check_output(
964 ['git', 'config', '--file', filepath, '-l']).decode('utf-8')
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000965
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000966 gitmodules = {}
967 for entry in gitmodules_entries.splitlines():
968 key, value = entry.split('=', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000969
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000970 # git config keys consist of section.name.key, e.g.,
971 # submodule.foo.path
972 section, submodule_key = key.split('.', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000973
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000974 # Only parse [submodule "foo"] sections from .gitmodules.
975 if section.strip() != 'submodule':
976 continue
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000977
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000978 # The name of the submodule can contain '.', hence split from the
979 # back.
980 submodule, sub_key = submodule_key.rsplit('.', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000981
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000982 if submodule not in gitmodules:
983 gitmodules[submodule] = {}
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000984
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000985 if sub_key in ('url', 'gclient-condition', 'path'):
986 gitmodules[submodule][sub_key] = value
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000987
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000988 paths = [module['path'] for module in gitmodules.values()]
989 commit_hashes = scm_git.GIT.GetSubmoduleCommits(cwd, paths)
Joanna Wang978f43d2023-08-18 00:16:07 +0000990
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000991 # Structure git submodules into a dict of DEPS git url entries.
992 submodules = {}
993 for module in gitmodules.values():
994 if self._use_relative_paths:
995 path = module['path']
996 else:
997 path = f'{self.name}/{module["path"]}'
998 # TODO(crbug.com/1471685): Temporary hack. In case of applied
999 # patches where the changes are staged but not committed, any
1000 # gitlinks from the patch are not returned by `git ls-tree`. The
1001 # path won't be found in commit_hashes. Use a temporary '0000000'
1002 # value that will be replaced with w/e is found in DEPS later.
1003 submodules[path] = {
1004 'dep_type':
1005 'git',
1006 'url':
1007 '{}@{}'.format(module['url'],
1008 commit_hashes.get(module['path'], '0000000'))
1009 }
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001010
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001011 if 'gclient-condition' in module:
1012 submodules[path]['condition'] = module['gclient-condition']
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001013
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001014 return submodules
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001015
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001016 def _get_option(self, attr, default):
1017 obj = self
1018 while not hasattr(obj, '_options'):
1019 obj = obj.parent
1020 return getattr(obj._options, attr, default)
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02001021
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001022 def add_dependencies_and_close(self, deps_to_add, hooks, hooks_cwd=None):
1023 """Adds the dependencies, hooks and mark the parsing as done."""
1024 if hooks_cwd == None:
1025 hooks_cwd = self.root.root_dir
Corentin Walleza68660d2018-09-10 17:33:24 +00001026
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001027 for dep in deps_to_add:
1028 if dep.verify_validity():
1029 self.add_dependency(dep)
1030 self._mark_as_parsed([
1031 Hook.from_dict(h,
1032 variables=self.get_vars(),
1033 verbose=self.root._options.verbose,
1034 conditions=self.condition,
1035 cwd_base=hooks_cwd) for h in hooks
1036 ])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001037
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001038 def findDepsFromNotAllowedHosts(self):
1039 """Returns a list of dependencies from not allowed hosts.
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001040
1041 If allowed_hosts is not set, allows all hosts and returns empty list.
1042 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001043 if not self._allowed_hosts:
1044 return []
1045 bad_deps = []
1046 for dep in self._dependencies:
1047 # Don't enforce this for custom_deps.
1048 if dep.name in self._custom_deps:
1049 continue
1050 if isinstance(dep.url, str):
1051 parsed_url = urllib.parse.urlparse(dep.url)
1052 if parsed_url.netloc and parsed_url.netloc not in self._allowed_hosts:
1053 bad_deps.append(dep)
1054 return bad_deps
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001055
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001056 def FuzzyMatchUrl(self, candidates):
1057 # type: (Union[Mapping[str, str], Collection[str]]) -> Optional[str]
1058 """Attempts to find this dependency in the list of candidates.
Edward Lesmesbb16e332018-03-30 17:54:51 -04001059
Edward Lemure7273d22018-05-10 19:13:51 -04001060 It looks first for the URL of this dependency in the list of
Edward Lesmesbb16e332018-03-30 17:54:51 -04001061 candidates. If it doesn't succeed, and the URL ends in '.git', it will try
1062 looking for the URL minus '.git'. Finally it will try to look for the name
1063 of the dependency.
1064
1065 Args:
Edward Lesmesbb16e332018-03-30 17:54:51 -04001066 candidates: list, dict. The list of candidates in which to look for this
1067 dependency. It can contain URLs as above, or dependency names like
1068 "src/some/dep".
1069
1070 Returns:
1071 If this dependency is not found in the list of candidates, returns None.
1072 Otherwise, it returns under which name did we find this dependency:
1073 - Its parsed url: "https://example.com/src.git'
1074 - Its parsed url minus '.git': "https://example.com/src"
1075 - Its name: "src"
1076 """
Michael Mossd683d7c2018-06-15 05:05:17 +00001077 if self.url:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001078 origin, _ = gclient_utils.SplitUrlRevision(self.url)
1079 match = gclient_utils.FuzzyMatchRepo(origin, candidates)
agabled437d762016-10-17 09:35:11 -07001080 if match:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001081 return match
1082 if self.name in candidates:
1083 return self.name
1084 return None
ilevy@chromium.org0233ac22012-11-28 20:27:02 +00001085
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001086 # Arguments number differs from overridden method
1087 # pylint: disable=arguments-differ
1088 def run(
1089 self,
1090 revision_overrides, # type: Mapping[str, str]
1091 command, # type: str
1092 args, # type: Sequence[str]
1093 work_queue, # type: ExecutionQueue
1094 options, # type: optparse.Values
1095 patch_refs, # type: Mapping[str, str]
1096 target_branches, # type: Mapping[str, str]
1097 skip_sync_revisions, # type: Mapping[str, str]
1098 ):
1099 # type: () -> None
1100 """Runs |command| then parse the DEPS file."""
1101 logging.info('Dependency(%s).run()' % self.name)
1102 assert self._file_list == []
1103 # When running runhooks, there's no need to consult the SCM.
1104 # All known hooks are expected to run unconditionally regardless of
1105 # working copy state, so skip the SCM status check.
1106 run_scm = command not in ('flatten', 'runhooks', 'recurse', 'validate',
1107 None)
1108 file_list = [] if not options.nohooks else None
1109 revision_override = revision_overrides.pop(
1110 self.FuzzyMatchUrl(revision_overrides), None)
1111 if not revision_override and not self.managed:
1112 revision_override = 'unmanaged'
1113 if run_scm and self.url:
1114 # Create a shallow copy to mutate revision.
1115 options = copy.copy(options)
1116 options.revision = revision_override
1117 self._used_revision = options.revision
1118 self._used_scm = self.CreateSCM(out_cb=work_queue.out_cb)
1119 if command != 'update' or self.GetScmName() != 'git':
1120 self._got_revision = self._used_scm.RunCommand(
1121 command, options, args, file_list)
agabled437d762016-10-17 09:35:11 -07001122 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001123 try:
1124 start = time.time()
1125 sync_status = metrics_utils.SYNC_STATUS_FAILURE
1126 self._got_revision = self._used_scm.RunCommand(
1127 command, options, args, file_list)
1128 sync_status = metrics_utils.SYNC_STATUS_SUCCESS
1129 finally:
1130 url, revision = gclient_utils.SplitUrlRevision(self.url)
1131 metrics.collector.add_repeated(
1132 'git_deps', {
1133 'path': self.name,
1134 'url': url,
1135 'revision': revision,
1136 'execution_time': time.time() - start,
1137 'sync_status': sync_status,
1138 })
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00001139
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001140 if isinstance(self, GitDependency) and command == 'update':
1141 patch_repo = self.url.split('@')[0]
1142 patch_ref = patch_refs.pop(self.FuzzyMatchUrl(patch_refs), None)
1143 target_branch = target_branches.pop(
1144 self.FuzzyMatchUrl(target_branches), None)
1145 if patch_ref:
1146 latest_commit = self._used_scm.apply_patch_ref(
1147 patch_repo, patch_ref, target_branch, options,
1148 file_list)
1149 else:
1150 latest_commit = self._used_scm.revinfo(None, None, None)
1151 existing_sync_commits = json.loads(
1152 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
1153 existing_sync_commits[self.name] = latest_commit
1154 os.environ[PREVIOUS_SYNC_COMMITS] = json.dumps(
1155 existing_sync_commits)
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001156
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001157 if file_list:
1158 file_list = [
1159 os.path.join(self.name, f.strip()) for f in file_list
1160 ]
John Budorick0f7b2002018-01-19 15:46:17 -08001161
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001162 # TODO(phajdan.jr): We should know exactly when the paths are
1163 # absolute. Convert all absolute paths to relative.
1164 for i in range(len(file_list or [])):
1165 # It depends on the command being executed (like runhooks vs
1166 # sync).
1167 if not os.path.isabs(file_list[i]):
1168 continue
1169 prefix = os.path.commonprefix(
1170 [self.root.root_dir.lower(), file_list[i].lower()])
1171 file_list[i] = file_list[i][len(prefix):]
1172 # Strip any leading path separators.
1173 while file_list[i].startswith(('\\', '/')):
1174 file_list[i] = file_list[i][1:]
John Budorick0f7b2002018-01-19 15:46:17 -08001175
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001176 # We must check for diffs AFTER any patch_refs have been applied.
1177 if skip_sync_revisions:
1178 skip_sync_rev = skip_sync_revisions.pop(
1179 self.FuzzyMatchUrl(skip_sync_revisions), None)
1180 self._should_sync = (skip_sync_rev is None
1181 or self._used_scm.check_diff(skip_sync_rev,
1182 files=['DEPS']))
1183 if not self._should_sync:
1184 logging.debug(
1185 'Skipping sync for %s. No DEPS changes since last '
1186 'sync at %s' % (self.name, skip_sync_rev))
1187 else:
1188 logging.debug('DEPS changes detected for %s since last sync at '
1189 '%s. Not skipping deps sync' %
1190 (self.name, skip_sync_rev))
Dirk Pranke9f20d022017-10-11 18:36:54 -07001191
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001192 if self.should_recurse:
1193 self.ParseDepsFile()
Corentin Wallez271a78a2020-07-12 15:41:46 +00001194
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001195 self._run_is_done(file_list or [])
Corentin Wallez271a78a2020-07-12 15:41:46 +00001196
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001197 # TODO(crbug.com/1339471): If should_recurse is false, ParseDepsFile
1198 # never gets called meaning we never fetch hooks and dependencies. So
1199 # there's no need to check should_recurse again here.
1200 if self.should_recurse:
1201 if command in ('update', 'revert') and not options.noprehooks:
1202 self.RunPreDepsHooks()
1203 # Parse the dependencies of this dependency.
1204 for s in self.dependencies:
1205 if s.should_process:
1206 work_queue.enqueue(s)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001207
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001208 if command == 'recurse':
1209 # Skip file only checkout.
1210 scm = self.GetScmName()
1211 if not options.scm or scm in options.scm:
1212 cwd = os.path.normpath(
1213 os.path.join(self.root.root_dir, self.name))
1214 # Pass in the SCM type as an env variable. Make sure we don't
1215 # put unicode strings in the environment.
1216 env = os.environ.copy()
1217 if scm:
1218 env['GCLIENT_SCM'] = str(scm)
1219 if self.url:
1220 env['GCLIENT_URL'] = str(self.url)
1221 env['GCLIENT_DEP_PATH'] = str(self.name)
1222 if options.prepend_dir and scm == 'git':
1223 print_stdout = False
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001224
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001225 def filter_fn(line):
1226 """Git-specific path marshaling. It is optimized for git-grep."""
1227 def mod_path(git_pathspec):
1228 match = re.match('^(\\S+?:)?([^\0]+)$',
1229 git_pathspec)
1230 modified_path = os.path.join(
1231 self.name, match.group(2))
1232 branch = match.group(1) or ''
1233 return '%s%s' % (branch, modified_path)
1234
1235 match = re.match('^Binary file ([^\0]+) matches$', line)
1236 if match:
1237 print('Binary file %s matches\n' %
1238 mod_path(match.group(1)))
1239 return
1240
1241 items = line.split('\0')
1242 if len(items) == 2 and items[1]:
1243 print('%s : %s' % (mod_path(items[0]), items[1]))
1244 elif len(items) >= 2:
1245 # Multiple null bytes or a single trailing null byte
1246 # indicate git is likely displaying filenames only
1247 # (such as with -l)
1248 print('\n'.join(
1249 mod_path(path) for path in items if path))
1250 else:
1251 print(line)
1252 else:
1253 print_stdout = True
1254 filter_fn = None
1255
1256 if self.url is None:
1257 print('Skipped omitted dependency %s' % cwd,
1258 file=sys.stderr)
1259 elif os.path.isdir(cwd):
1260 try:
1261 gclient_utils.CheckCallAndFilter(
1262 args,
1263 cwd=cwd,
1264 env=env,
1265 print_stdout=print_stdout,
1266 filter_fn=filter_fn,
1267 )
1268 except subprocess2.CalledProcessError:
1269 if not options.ignore:
1270 raise
1271 else:
1272 print('Skipped missing %s' % cwd, file=sys.stderr)
1273
1274 def GetScmName(self):
1275 raise NotImplementedError()
1276
1277 def CreateSCM(self, out_cb=None):
1278 raise NotImplementedError()
1279
1280 def HasGNArgsFile(self):
1281 return self._gn_args_file is not None
1282
1283 def WriteGNArgsFile(self):
1284 lines = ['# Generated from %r' % self.deps_file]
1285 variables = self.get_vars()
1286 for arg in self._gn_args:
1287 value = variables[arg]
1288 if isinstance(value, gclient_eval.ConstantString):
1289 value = value.value
1290 elif isinstance(value, str):
1291 value = gclient_eval.EvaluateCondition(value, variables)
1292 lines.append('%s = %s' % (arg, ToGNString(value)))
1293
1294 # When use_relative_paths is set, gn_args_file is relative to this DEPS
1295 path_prefix = self.root.root_dir
1296 if self._use_relative_paths:
1297 path_prefix = os.path.join(path_prefix, self.name)
1298
1299 with open(os.path.join(path_prefix, self._gn_args_file), 'wb') as f:
1300 f.write('\n'.join(lines).encode('utf-8', 'replace'))
1301
1302 @gclient_utils.lockedmethod
1303 def _run_is_done(self, file_list):
1304 # Both these are kept for hooks that are run as a separate tree
1305 # traversal.
1306 self._file_list = file_list
1307 self._processed = True
1308
1309 def GetHooks(self, options):
1310 """Evaluates all hooks, and return them in a flat list.
szager@google.comb9a78d32012-03-13 18:46:21 +00001311
1312 RunOnDeps() must have been called before to load the DEPS.
1313 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001314 result = []
1315 if not self.should_process or not self.should_recurse:
1316 # Don't run the hook when it is above recursion_limit.
1317 return result
1318 # If "--force" was specified, run all hooks regardless of what files
1319 # have changed.
1320 if self.deps_hooks:
1321 # TODO(maruel): If the user is using git, then we don't know
1322 # what files have changed so we always run all hooks. It'd be nice
1323 # to fix that.
1324 result.extend(self.deps_hooks)
1325 for s in self.dependencies:
1326 result.extend(s.GetHooks(options))
1327 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001328
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001329 def RunHooksRecursively(self, options, progress):
1330 assert self.hooks_ran == False
1331 self._hooks_ran = True
1332 hooks = self.GetHooks(options)
1333 if progress:
1334 progress._total = len(hooks)
1335 for hook in hooks:
1336 if progress:
1337 progress.update(extra=hook.name or '')
1338 hook.run()
1339 if progress:
1340 progress.end()
maruel@chromium.orgeaf61062010-07-07 18:42:39 +00001341
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001342 def RunPreDepsHooks(self):
1343 assert self.processed
1344 assert self.deps_parsed
1345 assert not self.pre_deps_hooks_ran
1346 assert not self.hooks_ran
1347 for s in self.dependencies:
1348 assert not s.processed
1349 self._pre_deps_hooks_ran = True
1350 for hook in self.pre_deps_hooks:
1351 hook.run()
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001352
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001353 def GetCipdRoot(self):
1354 if self.root is self:
1355 # Let's not infinitely recurse. If this is root and isn't an
1356 # instance of GClient, do nothing.
1357 return None
1358 return self.root.GetCipdRoot()
John Budorickd3ba72b2018-03-20 12:27:42 -07001359
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001360 def subtree(self, include_all):
1361 """Breadth first recursion excluding root node."""
1362 dependencies = self.dependencies
1363 for d in dependencies:
1364 if d.should_process or include_all:
1365 yield d
1366 for d in dependencies:
1367 for i in d.subtree(include_all):
1368 yield i
maruel@chromium.orgad3287e2011-10-03 19:15:10 +00001369
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001370 @gclient_utils.lockedmethod
1371 def add_dependency(self, new_dep):
1372 self._dependencies.append(new_dep)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001373
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001374 @gclient_utils.lockedmethod
1375 def _mark_as_parsed(self, new_hooks):
1376 self._deps_hooks.extend(new_hooks)
1377 self._deps_parsed = True
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001378
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001379 @property
1380 @gclient_utils.lockedmethod
1381 def dependencies(self):
1382 return tuple(self._dependencies)
maruel@chromium.org4bdd5fd2011-09-26 19:41:17 +00001383
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001384 @property
1385 @gclient_utils.lockedmethod
1386 def deps_hooks(self):
1387 return tuple(self._deps_hooks)
maruel@chromium.org064186c2011-09-27 23:53:33 +00001388
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001389 @property
1390 @gclient_utils.lockedmethod
1391 def pre_deps_hooks(self):
1392 return tuple(self._pre_deps_hooks)
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001393
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001394 @property
1395 @gclient_utils.lockedmethod
1396 def deps_parsed(self):
1397 """This is purely for debugging purposes. It's not used anywhere."""
1398 return self._deps_parsed
maruel@chromium.org064186c2011-09-27 23:53:33 +00001399
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001400 @property
1401 @gclient_utils.lockedmethod
1402 def processed(self):
1403 return self._processed
maruel@chromium.org064186c2011-09-27 23:53:33 +00001404
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001405 @property
1406 @gclient_utils.lockedmethod
1407 def pre_deps_hooks_ran(self):
1408 return self._pre_deps_hooks_ran
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001409
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001410 @property
1411 @gclient_utils.lockedmethod
1412 def hooks_ran(self):
1413 return self._hooks_ran
maruel@chromium.org064186c2011-09-27 23:53:33 +00001414
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001415 @property
1416 @gclient_utils.lockedmethod
1417 def allowed_hosts(self):
1418 return self._allowed_hosts
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001419
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001420 @property
1421 @gclient_utils.lockedmethod
1422 def file_list(self):
1423 return tuple(self._file_list)
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001424
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001425 @property
1426 def used_scm(self):
1427 """SCMWrapper instance for this dependency or None if not processed yet."""
1428 return self._used_scm
kustermann@google.coma692e8f2013-04-18 08:32:04 +00001429
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001430 @property
1431 @gclient_utils.lockedmethod
1432 def got_revision(self):
1433 return self._got_revision
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00001434
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001435 @property
1436 def file_list_and_children(self):
1437 result = list(self.file_list)
1438 for d in self.dependencies:
1439 result.extend(d.file_list_and_children)
1440 return tuple(result)
maruel@chromium.org861fd0f2010-07-23 03:05:05 +00001441
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001442 def __str__(self):
1443 out = []
1444 for i in ('name', 'url', 'custom_deps', 'custom_vars', 'deps_hooks',
1445 'file_list', 'should_process', 'processed', 'hooks_ran',
1446 'deps_parsed', 'requirements', 'allowed_hosts'):
1447 # First try the native property if it exists.
1448 if hasattr(self, '_' + i):
1449 value = getattr(self, '_' + i, False)
1450 else:
1451 value = getattr(self, i, False)
1452 if value:
1453 out.append('%s: %s' % (i, value))
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001454
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001455 for d in self.dependencies:
1456 out.extend([' ' + x for x in str(d).splitlines()])
1457 out.append('')
1458 return '\n'.join(out)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001459
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001460 def __repr__(self):
1461 return '%s: %s' % (self.name, self.url)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001462
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001463 def hierarchy(self, include_url=True, graphviz=False):
1464 """Returns a human-readable hierarchical reference to a Dependency."""
1465 def format_name(d):
1466 if include_url:
1467 return '%s(%s)' % (d.name, d.url)
1468 return '"%s"' % d.name # quotes required for graph dot file.
Joanna Wang9144b672023-02-24 23:36:17 +00001469
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001470 out = format_name(self)
1471 i = self.parent
1472 while i and i.name:
1473 out = '%s -> %s' % (format_name(i), out)
1474 if graphviz:
1475 # for graphviz we just need each parent->child relationship
1476 # listed once.
1477 return out
1478 i = i.parent
Joanna Wang9144b672023-02-24 23:36:17 +00001479 return out
maruel@chromium.orgbffb9042010-07-22 20:59:36 +00001480
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001481 def hierarchy_data(self):
1482 """Returns a machine-readable hierarchical reference to a Dependency."""
1483 d = self
1484 out = []
1485 while d and d.name:
1486 out.insert(0, (d.name, d.url))
1487 d = d.parent
1488 return tuple(out)
Michael Mossfe68c912018-03-22 19:19:35 -07001489
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001490 def get_builtin_vars(self):
1491 return {
1492 'checkout_android': 'android' in self.target_os,
1493 'checkout_chromeos': 'chromeos' in self.target_os,
1494 'checkout_fuchsia': 'fuchsia' in self.target_os,
1495 'checkout_ios': 'ios' in self.target_os,
1496 'checkout_linux': 'unix' in self.target_os,
1497 'checkout_mac': 'mac' in self.target_os,
1498 'checkout_win': 'win' in self.target_os,
1499 'host_os': _detect_host_os(),
1500 'checkout_arm': 'arm' in self.target_cpu,
1501 'checkout_arm64': 'arm64' in self.target_cpu,
1502 'checkout_x86': 'x86' in self.target_cpu,
1503 'checkout_mips': 'mips' in self.target_cpu,
1504 'checkout_mips64': 'mips64' in self.target_cpu,
1505 'checkout_ppc': 'ppc' in self.target_cpu,
1506 'checkout_s390': 's390' in self.target_cpu,
1507 'checkout_x64': 'x64' in self.target_cpu,
1508 'host_cpu': detect_host_arch.HostArch(),
1509 }
Robbie Iannucci3db32762023-07-05 19:02:44 +00001510
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001511 def get_vars(self):
1512 """Returns a dictionary of effective variable values
Edward Lemur8f8a50d2018-11-01 22:03:02 +00001513 (DEPS file contents with applied custom_vars overrides)."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001514 # Variable precedence (last has highest):
1515 # - DEPS vars
1516 # - parents, from first to last
1517 # - built-in
1518 # - custom_vars overrides
1519 result = {}
1520 result.update(self._vars)
1521 if self.parent:
1522 merge_vars(result, self.parent.get_vars())
1523 # Provide some built-in variables.
1524 result.update(self.get_builtin_vars())
1525 merge_vars(result, self.custom_vars)
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001526
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001527 return result
Paweł Hajdan, Jrd3790252017-07-03 21:06:24 +02001528
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001529
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001530_PLATFORM_MAPPING = {
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001531 'cygwin': 'win',
1532 'darwin': 'mac',
1533 'linux2': 'linux',
1534 'linux': 'linux',
1535 'win32': 'win',
1536 'aix6': 'aix',
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001537}
1538
1539
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001540def merge_vars(result, new_vars):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001541 for k, v in new_vars.items():
1542 if k in result:
1543 if isinstance(result[k], gclient_eval.ConstantString):
1544 if isinstance(v, gclient_eval.ConstantString):
1545 result[k] = v
1546 else:
1547 result[k].value = v
1548 else:
1549 result[k] = v
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001550 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001551 result[k] = v
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001552
1553
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001554def _detect_host_os():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001555 if sys.platform in _PLATFORM_MAPPING:
1556 return _PLATFORM_MAPPING[sys.platform]
Jonas Termansenbf7eb522023-01-19 17:56:40 +00001557
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001558 try:
1559 return os.uname().sysname.lower()
1560 except AttributeError:
1561 return sys.platform
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001562
1563
Edward Lemurb61d3872018-05-09 18:42:47 -04001564class GitDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001565 """A Dependency object that represents a single git checkout."""
1566 @staticmethod
1567 def updateProtocol(url, protocol):
1568 """Updates given URL's protocol"""
1569 # only works on urls, skips local paths
1570 if not url or not protocol or not re.match('([a-z]+)://', url) or \
1571 re.match('file://', url):
1572 return url
Edward Lemurb61d3872018-05-09 18:42:47 -04001573
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001574 return re.sub('^([a-z]+):', protocol + ':', url)
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +00001575
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001576 #override
1577 def GetScmName(self):
1578 """Always 'git'."""
1579 return 'git'
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +00001580
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001581 #override
1582 def CreateSCM(self, out_cb=None):
1583 """Create a Wrapper instance suitable for handling this git dependency."""
1584 return gclient_scm.GitWrapper(self.url,
1585 self.root.root_dir,
1586 self.name,
1587 self.outbuf,
1588 out_cb,
1589 print_outbuf=self.print_outbuf)
Edward Lemurb61d3872018-05-09 18:42:47 -04001590
1591
1592class GClient(GitDependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001593 """Object that represent a gclient checkout. A tree of Dependency(), one per
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001594 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001595
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001596 DEPS_OS_CHOICES = {
1597 "aix6": "unix",
1598 "win32": "win",
1599 "win": "win",
1600 "cygwin": "win",
1601 "darwin": "mac",
1602 "mac": "mac",
1603 "unix": "unix",
1604 "linux": "unix",
1605 "linux2": "unix",
1606 "linux3": "unix",
1607 "android": "android",
1608 "ios": "ios",
1609 "fuchsia": "fuchsia",
1610 "chromeos": "chromeos",
1611 }
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001612
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001613 DEFAULT_CLIENT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001614solutions = [
Edward Lesmes05934952019-12-19 20:38:09 +00001615 { "name" : %(solution_name)r,
1616 "url" : %(solution_url)r,
1617 "deps_file" : %(deps_file)r,
1618 "managed" : %(managed)r,
smutae7ea312016-07-18 11:59:41 -07001619 "custom_deps" : {
1620 },
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02001621 "custom_vars": %(custom_vars)r,
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001622 },
1623]
Robert Iannuccia19649b2018-06-29 16:31:45 +00001624""")
1625
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001626 DEFAULT_CLIENT_CACHE_DIR_TEXT = ("""\
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001627cache_dir = %(cache_dir)r
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001628""")
1629
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001630 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001631# Snapshot generated with gclient revinfo --snapshot
Edward Lesmesc2960242018-03-06 20:50:15 -05001632solutions = %(solution_list)s
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001633""")
1634
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001635 def __init__(self, root_dir, options):
1636 # Do not change previous behavior. Only solution level and immediate
1637 # DEPS are processed.
1638 self._recursion_limit = 2
1639 super(GClient, self).__init__(parent=None,
1640 name=None,
1641 url=None,
1642 managed=True,
1643 custom_deps=None,
1644 custom_vars=None,
1645 custom_hooks=None,
1646 deps_file='unused',
1647 should_process=True,
1648 should_recurse=True,
1649 relative=None,
1650 condition=None,
1651 print_outbuf=True)
Edward Lemure05f18d2018-06-08 17:36:53 +00001652
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001653 self._options = options
1654 if options.deps_os:
1655 enforced_os = options.deps_os.split(',')
1656 else:
1657 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
1658 if 'all' in enforced_os:
1659 enforced_os = self.DEPS_OS_CHOICES.values()
1660 self._enforced_os = tuple(set(enforced_os))
1661 self._enforced_cpu = (detect_host_arch.HostArch(), )
1662 self._root_dir = root_dir
1663 self._cipd_root = None
1664 self.config_content = None
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001665
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001666 def _CheckConfig(self):
1667 """Verify that the config matches the state of the existing checked-out
borenet@google.com88d10082014-03-21 17:24:48 +00001668 solutions."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001669 for dep in self.dependencies:
1670 if dep.managed and dep.url:
1671 scm = dep.CreateSCM()
1672 actual_url = scm.GetActualRemoteURL(self._options)
1673 if actual_url and not scm.DoesRemoteURLMatch(self._options):
1674 mirror = scm.GetCacheMirror()
1675 if mirror:
1676 mirror_string = '%s (exists=%s)' % (mirror.mirror_path,
1677 mirror.exists())
1678 else:
1679 mirror_string = 'not used'
1680 raise gclient_utils.Error(
1681 '''
borenet@google.com88d10082014-03-21 17:24:48 +00001682Your .gclient file seems to be broken. The requested URL is different from what
borenet@google.com0a427372014-04-02 19:12:13 +00001683is actually checked out in %(checkout_path)s.
borenet@google.com88d10082014-03-21 17:24:48 +00001684
borenet@google.com97882362014-04-07 20:06:02 +00001685The .gclient file contains:
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +00001686URL: %(expected_url)s (%(expected_scm)s)
1687Cache mirror: %(mirror_string)s
borenet@google.com97882362014-04-07 20:06:02 +00001688
1689The local checkout in %(checkout_path)s reports:
1690%(actual_url)s (%(actual_scm)s)
borenet@google.com88d10082014-03-21 17:24:48 +00001691
1692You should ensure that the URL listed in .gclient is correct and either change
agabled437d762016-10-17 09:35:11 -07001693it or fix the checkout.
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00001694''' % {
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001695 'checkout_path': os.path.join(
1696 self.root_dir, dep.name),
1697 'expected_url': dep.url,
1698 'expected_scm': dep.GetScmName(),
1699 'mirror_string': mirror_string,
1700 'actual_url': actual_url,
1701 'actual_scm': dep.GetScmName()
1702 })
borenet@google.com88d10082014-03-21 17:24:48 +00001703
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001704 def SetConfig(self, content):
1705 assert not self.dependencies
1706 config_dict = {}
1707 self.config_content = content
1708 try:
1709 exec(content, config_dict)
1710 except SyntaxError as e:
1711 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001712
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001713 # Append any target OS that is not already being enforced to the tuple.
1714 target_os = config_dict.get('target_os', [])
1715 if config_dict.get('target_os_only', False):
1716 self._enforced_os = tuple(set(target_os))
1717 else:
1718 self._enforced_os = tuple(set(self._enforced_os).union(target_os))
stuartmorgan@chromium.org18a4f6a2012-11-02 13:37:24 +00001719
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001720 # Append any target CPU that is not already being enforced to the tuple.
1721 target_cpu = config_dict.get('target_cpu', [])
1722 if config_dict.get('target_cpu_only', False):
1723 self._enforced_cpu = tuple(set(target_cpu))
1724 else:
1725 self._enforced_cpu = tuple(
1726 set(self._enforced_cpu).union(target_cpu))
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001727
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001728 cache_dir = config_dict.get('cache_dir', UNSET_CACHE_DIR)
1729 if cache_dir is not UNSET_CACHE_DIR:
1730 if cache_dir:
1731 cache_dir = os.path.join(self.root_dir, cache_dir)
1732 cache_dir = os.path.abspath(cache_dir)
Andrii Shyshkalov77ce4bd2017-11-27 12:38:18 -08001733
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001734 git_cache.Mirror.SetCachePath(cache_dir)
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001735
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001736 if not target_os and config_dict.get('target_os_only', False):
1737 raise gclient_utils.Error(
1738 'Can\'t use target_os_only if target_os is '
1739 'not specified')
peter@chromium.org1efccc82012-04-27 16:34:38 +00001740
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001741 if not target_cpu and config_dict.get('target_cpu_only', False):
1742 raise gclient_utils.Error(
1743 'Can\'t use target_cpu_only if target_cpu is '
1744 'not specified')
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001745
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001746 deps_to_add = []
1747 for s in config_dict.get('solutions', []):
1748 try:
1749 deps_to_add.append(
1750 GitDependency(
1751 parent=self,
1752 name=s['name'],
1753 # Update URL with scheme in protocol_override
1754 url=GitDependency.updateProtocol(
1755 s['url'], s.get('protocol_override', None)),
1756 managed=s.get('managed', True),
1757 custom_deps=s.get('custom_deps', {}),
1758 custom_vars=s.get('custom_vars', {}),
1759 custom_hooks=s.get('custom_hooks', []),
1760 deps_file=s.get('deps_file', 'DEPS'),
1761 should_process=True,
1762 should_recurse=True,
1763 relative=None,
1764 condition=None,
1765 print_outbuf=True,
1766 # Pass protocol_override down the tree for child deps to
1767 # use.
1768 protocol=s.get('protocol_override', None),
1769 git_dependencies_state=self.git_dependencies_state))
1770 except KeyError:
1771 raise gclient_utils.Error('Invalid .gclient file. Solution is '
1772 'incomplete: %s' % s)
1773 metrics.collector.add('project_urls', [
Edward Lemuraffd4102019-06-05 18:07:49 +00001774 dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Edward Lemur40764b02018-07-20 18:50:29 +00001775 for dep in deps_to_add
1776 if dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001777 ])
Edward Lemur40764b02018-07-20 18:50:29 +00001778
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001779 self.add_dependencies_and_close(deps_to_add,
1780 config_dict.get('hooks', []))
1781 logging.info('SetConfig() done')
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001782
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001783 def SaveConfig(self):
1784 gclient_utils.FileWrite(
1785 os.path.join(self.root_dir, self._options.config_filename),
1786 self.config_content)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001787
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001788 @staticmethod
1789 def LoadCurrentConfig(options):
1790 # type: (optparse.Values) -> GClient
1791 """Searches for and loads a .gclient file relative to the current working
Joanna Wang66286612022-06-30 19:59:13 +00001792 dir."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001793 if options.spec:
1794 client = GClient('.', options)
1795 client.SetConfig(options.spec)
1796 else:
1797 if options.verbose:
1798 print('Looking for %s starting from %s\n' %
1799 (options.config_filename, os.getcwd()))
1800 path = gclient_paths.FindGclientRoot(os.getcwd(),
1801 options.config_filename)
1802 if not path:
1803 if options.verbose:
1804 print('Couldn\'t find configuration file.')
1805 return None
1806 client = GClient(path, options)
1807 client.SetConfig(
1808 gclient_utils.FileRead(
1809 os.path.join(path, options.config_filename)))
maruel@chromium.org69392e72011-10-13 22:09:00 +00001810
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001811 if (options.revisions and len(client.dependencies) > 1
1812 and any('@' not in r for r in options.revisions)):
1813 print((
1814 'You must specify the full solution name like --revision %s@%s\n'
1815 'when you have multiple solutions setup in your .gclient file.\n'
1816 'Other solutions present are: %s.') %
1817 (client.dependencies[0].name, options.revisions[0], ', '.join(
1818 s.name for s in client.dependencies[1:])),
1819 file=sys.stderr)
Joanna Wang66286612022-06-30 19:59:13 +00001820
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001821 return client
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001822
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001823 def SetDefaultConfig(self,
1824 solution_name,
1825 deps_file,
1826 solution_url,
1827 managed=True,
1828 cache_dir=UNSET_CACHE_DIR,
1829 custom_vars=None):
1830 text = self.DEFAULT_CLIENT_FILE_TEXT
1831 format_dict = {
1832 'solution_name': solution_name,
1833 'solution_url': solution_url,
1834 'deps_file': deps_file,
1835 'managed': managed,
1836 'custom_vars': custom_vars or {},
1837 }
Robert Iannuccia19649b2018-06-29 16:31:45 +00001838
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001839 if cache_dir is not UNSET_CACHE_DIR:
1840 text += self.DEFAULT_CLIENT_CACHE_DIR_TEXT
1841 format_dict['cache_dir'] = cache_dir
Robert Iannuccia19649b2018-06-29 16:31:45 +00001842
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001843 self.SetConfig(text % format_dict)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001844
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001845 def _SaveEntries(self):
1846 """Creates a .gclient_entries file to record the list of unique checkouts.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001847
1848 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001849 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001850 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
1851 # makes testing a bit too fun.
1852 result = 'entries = {\n'
1853 for entry in self.root.subtree(False):
1854 result += ' %s: %s,\n' % (pprint.pformat(
1855 entry.name), pprint.pformat(entry.url))
1856 result += '}\n'
1857 file_path = os.path.join(self.root_dir, self._options.entries_filename)
1858 logging.debug(result)
1859 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001860
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001861 def _ReadEntries(self):
1862 """Read the .gclient_entries file for the given client.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001863
1864 Returns:
1865 A sequence of solution names, which will be empty if there is the
1866 entries file hasn't been created yet.
1867 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001868 scope = {}
1869 filename = os.path.join(self.root_dir, self._options.entries_filename)
1870 if not os.path.exists(filename):
1871 return {}
1872 try:
1873 exec(gclient_utils.FileRead(filename), scope)
1874 except SyntaxError as e:
1875 gclient_utils.SyntaxErrorToError(filename, e)
1876 return scope.get('entries', {})
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001877
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001878 def _ExtractFileJsonContents(self, default_filename):
1879 # type: (str) -> Mapping[str,Any]
1880 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001881
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001882 if not os.path.exists(f):
1883 logging.info('File %s does not exist.' % f)
1884 return {}
Joanna Wang01870792022-08-01 19:02:57 +00001885
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001886 with open(f, 'r') as open_f:
1887 logging.info('Reading content from file %s' % f)
1888 content = open_f.read().rstrip()
1889 if content:
1890 return json.loads(content)
Joanna Wang66286612022-06-30 19:59:13 +00001891 return {}
1892
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001893 def _WriteFileContents(self, default_filename, content):
1894 # type: (str, str) -> None
1895 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001896
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001897 with open(f, 'w') as open_f:
1898 logging.info('Writing to file %s' % f)
1899 open_f.write(content)
Joanna Wangf3edc502022-07-20 00:12:10 +00001900
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001901 def _EnforceSkipSyncRevisions(self, patch_refs):
1902 # type: (Mapping[str, str]) -> Mapping[str, str]
1903 """Checks for and enforces revisions for skipping deps syncing."""
1904 previous_sync_commits = self._ExtractFileJsonContents(
1905 PREVIOUS_SYNC_COMMITS_FILE)
Joanna Wang66286612022-06-30 19:59:13 +00001906
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001907 if not previous_sync_commits:
1908 return {}
maruel@chromium.org918a9ae2010-05-28 15:50:30 +00001909
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001910 # Current `self.dependencies` only contain solutions. If a patch_ref is
1911 # not for a solution, then it is for a solution's dependency or recursed
1912 # dependency which we cannot support while skipping sync.
1913 if patch_refs:
1914 unclaimed_prs = []
1915 candidates = []
1916 for dep in self.dependencies:
1917 origin, _ = gclient_utils.SplitUrlRevision(dep.url)
1918 candidates.extend([origin, dep.name])
1919 for patch_repo in patch_refs:
1920 if not gclient_utils.FuzzyMatchRepo(patch_repo, candidates):
1921 unclaimed_prs.append(patch_repo)
1922 if unclaimed_prs:
1923 print(
1924 'We cannot skip syncs when there are --patch-refs flags for '
1925 'non-solution dependencies. To skip syncing, remove patch_refs '
1926 'for: \n%s' % '\n'.join(unclaimed_prs))
1927 return {}
Edward Lesmesc621b212018-03-21 20:26:56 -04001928
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001929 # We cannot skip syncing if there are custom_vars that differ from the
1930 # previous run's custom_vars.
1931 previous_custom_vars = self._ExtractFileJsonContents(
1932 PREVIOUS_CUSTOM_VARS_FILE)
1933
1934 cvs_by_name = {s.name: s.custom_vars for s in self.dependencies}
1935
1936 skip_sync_revisions = {}
1937 for name, commit in previous_sync_commits.items():
1938 previous_vars = previous_custom_vars.get(name)
1939 if previous_vars == cvs_by_name.get(name) or (
1940 not previous_vars and not cvs_by_name.get(name)):
1941 skip_sync_revisions[name] = commit
1942 else:
1943 print(
1944 'We cannot skip syncs when custom_vars for solutions have '
1945 'changed since the last sync run on this machine.\n'
1946 '\nRemoving skip_sync_revision for:\n'
1947 'solution: %s, current: %r, previous: %r.' %
1948 (name, cvs_by_name.get(name), previous_vars))
1949 print('no-sync experiment enabled with %r' % skip_sync_revisions)
1950 return skip_sync_revisions
1951
1952 # TODO(crbug.com/1340695): Remove handling revisions without '@'.
1953 def _EnforceRevisions(self):
1954 """Checks for revision overrides."""
1955 revision_overrides = {}
1956 if self._options.head:
1957 return revision_overrides
1958 if not self._options.revisions:
1959 return revision_overrides
1960 solutions_names = [s.name for s in self.dependencies]
1961 for index, revision in enumerate(self._options.revisions):
1962 if not '@' in revision:
1963 # Support for --revision 123
1964 revision = '%s@%s' % (solutions_names[index], revision)
1965 name, rev = revision.split('@', 1)
1966 revision_overrides[name] = rev
1967 return revision_overrides
1968
1969 def _EnforcePatchRefsAndBranches(self):
1970 # type: () -> Tuple[Mapping[str, str], Mapping[str, str]]
1971 """Checks for patch refs."""
1972 patch_refs = {}
1973 target_branches = {}
1974 if not self._options.patch_refs:
1975 return patch_refs, target_branches
1976 for given_patch_ref in self._options.patch_refs:
1977 patch_repo, _, patch_ref = given_patch_ref.partition('@')
1978 if not patch_repo or not patch_ref or ':' not in patch_ref:
1979 raise gclient_utils.Error(
1980 'Wrong revision format: %s should be of the form '
1981 'patch_repo@target_branch:patch_ref.' % given_patch_ref)
1982 target_branch, _, patch_ref = patch_ref.partition(':')
1983 target_branches[patch_repo] = target_branch
1984 patch_refs[patch_repo] = patch_ref
1985 return patch_refs, target_branches
1986
Gavin Mak50b27a52023-09-19 22:44:59 +00001987 def _InstallPreCommitHook(self):
1988 # On Windows, this path is written to the file as
1989 # "dir\hooks\pre-commit.py" but it gets interpreted as
1990 # "dirhookspre-commit.py".
1991 gclient_hook_path = os.path.join(DEPOT_TOOLS_DIR, 'hooks',
1992 'pre-commit.py').replace('\\', '\\\\')
1993 gclient_hook_content = '\n'.join((
1994 f'{PRECOMMIT_HOOK_VAR}={gclient_hook_path}',
1995 f'if [ -f "${PRECOMMIT_HOOK_VAR}" ]; then',
1996 f' python3 "${PRECOMMIT_HOOK_VAR}" || exit 1',
1997 'fi',
1998 ))
1999
2000 soln = gclient_paths.GetPrimarySolutionPath()
2001 if not soln:
2002 print('Could not find gclient solution.')
2003 return
2004
2005 git_dir = os.path.join(soln, '.git')
2006 if not os.path.exists(git_dir):
2007 return
2008
2009 hook = os.path.join(git_dir, 'hooks', 'pre-commit')
2010 if os.path.exists(hook):
2011 with open(hook, 'r') as f:
2012 content = f.read()
2013 if PRECOMMIT_HOOK_VAR in content:
2014 print(f'{hook} already contains the gclient pre-commit hook.')
2015 else:
2016 print(f'A pre-commit hook already exists at {hook}.\n'
2017 f'Please append the following lines to the hook:\n\n'
2018 f'{gclient_hook_content}')
2019 return
2020
2021 print(f'Creating a pre-commit hook at {hook}')
2022 with open(hook, 'w') as f:
2023 f.write('#!/bin/sh\n')
2024 f.write(f'{gclient_hook_content}\n')
2025 os.chmod(hook, 0o755)
2026
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002027 def _RemoveUnversionedGitDirs(self):
2028 """Remove directories that are no longer part of the checkout.
Edward Lemur5b1fa942018-10-04 23:22:09 +00002029
2030 Notify the user if there is an orphaned entry in their working copy.
2031 Only delete the directory if there are no changes in it, and
2032 delete_unversioned_trees is set to true.
Josip Sokcevic1b8211f2022-09-30 17:46:53 +00002033
2034 Returns CIPD packages that are no longer versioned.
Edward Lemur5b1fa942018-10-04 23:22:09 +00002035 """
2036
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002037 entry_names_and_sync = [(i.name, i._should_sync)
2038 for i in self.root.subtree(False) if i.url]
2039 entries = []
2040 if entry_names_and_sync:
2041 entries, _ = zip(*entry_names_and_sync)
2042 full_entries = [
2043 os.path.join(self.root_dir, e.replace('/', os.path.sep))
2044 for e in entries
2045 ]
2046 no_sync_entries = [
2047 name for name, should_sync in entry_names_and_sync
2048 if not should_sync
2049 ]
Edward Lemur5b1fa942018-10-04 23:22:09 +00002050
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002051 removed_cipd_entries = []
2052 for entry, prev_url in self._ReadEntries().items():
2053 if not prev_url:
2054 # entry must have been overridden via .gclient custom_deps
2055 continue
2056 if any(entry.startswith(sln) for sln in no_sync_entries):
2057 # Dependencies of solutions that skipped syncing would not
2058 # show up in `entries`.
2059 continue
2060 if (':' in entry):
2061 # This is a cipd package. Don't clean it up, but prepare for
2062 # return
2063 if entry not in entries:
2064 removed_cipd_entries.append(entry)
2065 continue
2066 # Fix path separator on Windows.
2067 entry_fixed = entry.replace('/', os.path.sep)
2068 e_dir = os.path.join(self.root_dir, entry_fixed)
2069 # Use entry and not entry_fixed there.
2070 if (entry not in entries and
2071 (not any(path.startswith(entry + '/') for path in entries))
2072 and os.path.exists(e_dir)):
2073 # The entry has been removed from DEPS.
2074 scm = gclient_scm.GitWrapper(prev_url, self.root_dir,
2075 entry_fixed, self.outbuf)
Edward Lemur5b1fa942018-10-04 23:22:09 +00002076
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002077 # Check to see if this directory is now part of a higher-up
2078 # checkout.
2079 scm_root = None
2080 try:
2081 scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2082 scm.checkout_path)
2083 except subprocess2.CalledProcessError:
2084 pass
2085 if not scm_root:
2086 logging.warning(
2087 'Could not find checkout root for %s. Unable to '
2088 'determine whether it is part of a higher-level '
2089 'checkout, so not removing.' % entry)
2090 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002091
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002092 # This is to handle the case of third_party/WebKit migrating
2093 # from being a DEPS entry to being part of the main project. If
2094 # the subproject is a Git project, we need to remove its .git
2095 # folder. Otherwise git operations on that folder will have
2096 # different effects depending on the current working directory.
2097 if os.path.abspath(scm_root) == os.path.abspath(e_dir):
2098 e_par_dir = os.path.join(e_dir, os.pardir)
2099 if gclient_scm.scm.GIT.IsInsideWorkTree(e_par_dir):
2100 par_scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2101 e_par_dir)
2102 # rel_e_dir : relative path of entry w.r.t. its parent
2103 # repo.
2104 rel_e_dir = os.path.relpath(e_dir, par_scm_root)
2105 if gclient_scm.scm.GIT.IsDirectoryVersioned(
2106 par_scm_root, rel_e_dir):
2107 save_dir = scm.GetGitBackupDirPath()
2108 # Remove any eventual stale backup dir for the same
2109 # project.
2110 if os.path.exists(save_dir):
2111 gclient_utils.rmtree(save_dir)
2112 os.rename(os.path.join(e_dir, '.git'), save_dir)
2113 # When switching between the two states (entry/ is a
2114 # subproject -> entry/ is part of the outer
2115 # project), it is very likely that some files are
2116 # changed in the checkout, unless we are jumping
2117 # *exactly* across the commit which changed just
2118 # DEPS. In such case we want to cleanup any eventual
2119 # stale files (coming from the old subproject) in
2120 # order to end up with a clean checkout.
2121 gclient_scm.scm.GIT.CleanupDir(
2122 par_scm_root, rel_e_dir)
2123 assert not os.path.exists(
2124 os.path.join(e_dir, '.git'))
2125 print(
2126 '\nWARNING: \'%s\' has been moved from DEPS to a higher '
2127 'level checkout. The git folder containing all the local'
2128 ' branches has been saved to %s.\n'
2129 'If you don\'t care about its state you can safely '
2130 'remove that folder to free up space.' %
2131 (entry, save_dir))
2132 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002133
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002134 if scm_root in full_entries:
2135 logging.info(
2136 '%s is part of a higher level checkout, not removing',
2137 scm.GetCheckoutRoot())
2138 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002139
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002140 file_list = []
2141 scm.status(self._options, [], file_list)
2142 modified_files = file_list != []
2143 if (not self._options.delete_unversioned_trees
2144 or (modified_files and not self._options.force)):
2145 # There are modified files in this entry. Keep warning until
2146 # removed.
2147 self.add_dependency(
2148 GitDependency(
2149 parent=self,
2150 name=entry,
2151 # Update URL with scheme in protocol_override
2152 url=GitDependency.updateProtocol(
2153 prev_url, self.protocol),
2154 managed=False,
2155 custom_deps={},
2156 custom_vars={},
2157 custom_hooks=[],
2158 deps_file=None,
2159 should_process=True,
2160 should_recurse=False,
2161 relative=None,
2162 condition=None,
2163 protocol=self.protocol,
2164 git_dependencies_state=self.git_dependencies_state))
2165 if modified_files and self._options.delete_unversioned_trees:
2166 print(
2167 '\nWARNING: \'%s\' is no longer part of this client.\n'
2168 'Despite running \'gclient sync -D\' no action was taken '
2169 'as there are modifications.\nIt is recommended you revert '
2170 'all changes or run \'gclient sync -D --force\' next '
2171 'time.' % entry_fixed)
2172 else:
2173 print(
2174 '\nWARNING: \'%s\' is no longer part of this client.\n'
2175 'It is recommended that you manually remove it or use '
2176 '\'gclient sync -D\' next time.' % entry_fixed)
2177 else:
2178 # Delete the entry
2179 print('\n________ deleting \'%s\' in \'%s\'' %
2180 (entry_fixed, self.root_dir))
2181 gclient_utils.rmtree(e_dir)
2182 # record the current list of entries for next time
2183 self._SaveEntries()
2184 return removed_cipd_entries
Edward Lemur5b1fa942018-10-04 23:22:09 +00002185
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002186 def RunOnDeps(self,
2187 command,
2188 args,
2189 ignore_requirements=False,
2190 progress=True):
2191 """Runs a command on each dependency in a client and its dependencies.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002192
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002193 Args:
2194 command: The command to use (e.g., 'status' or 'diff')
2195 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002196 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002197 if not self.dependencies:
2198 raise gclient_utils.Error('No solution specified')
Michael Mossd683d7c2018-06-15 05:05:17 +00002199
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002200 revision_overrides = {}
2201 patch_refs = {}
2202 target_branches = {}
2203 skip_sync_revisions = {}
2204 # It's unnecessary to check for revision overrides for 'recurse'.
2205 # Save a few seconds by not calling _EnforceRevisions() in that case.
2206 if command not in ('diff', 'recurse', 'runhooks', 'status', 'revert',
2207 'validate'):
2208 self._CheckConfig()
2209 revision_overrides = self._EnforceRevisions()
Edward Lesmesc621b212018-03-21 20:26:56 -04002210
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002211 if command == 'update':
2212 patch_refs, target_branches = self._EnforcePatchRefsAndBranches()
2213 if NO_SYNC_EXPERIMENT in self._options.experiments:
2214 skip_sync_revisions = self._EnforceSkipSyncRevisions(patch_refs)
Joanna Wang66286612022-06-30 19:59:13 +00002215
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002216 # Store solutions' custom_vars on memory to compare in the next run.
2217 # All dependencies added later are inherited from the current
2218 # self.dependencies.
2219 custom_vars = {
2220 dep.name: dep.custom_vars
2221 for dep in self.dependencies if dep.custom_vars
Michael Mossd683d7c2018-06-15 05:05:17 +00002222 }
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002223 if custom_vars:
2224 self._WriteFileContents(PREVIOUS_CUSTOM_VARS_FILE,
2225 json.dumps(custom_vars))
2226
2227 # Disable progress for non-tty stdout.
2228 should_show_progress = (setup_color.IS_TTY and not self._options.verbose
2229 and progress)
2230 pm = None
2231 if should_show_progress:
2232 if command in ('update', 'revert'):
2233 pm = Progress('Syncing projects', 1)
2234 elif command in ('recurse', 'validate'):
2235 pm = Progress(' '.join(args), 1)
2236 work_queue = gclient_utils.ExecutionQueue(
2237 self._options.jobs,
2238 pm,
2239 ignore_requirements=ignore_requirements,
2240 verbose=self._options.verbose)
2241 for s in self.dependencies:
2242 if s.should_process:
2243 work_queue.enqueue(s)
2244 work_queue.flush(revision_overrides,
2245 command,
2246 args,
2247 options=self._options,
2248 patch_refs=patch_refs,
2249 target_branches=target_branches,
2250 skip_sync_revisions=skip_sync_revisions)
2251
2252 if revision_overrides:
2253 print(
2254 'Please fix your script, having invalid --revision flags will soon '
2255 'be considered an error.',
2256 file=sys.stderr)
2257
2258 if patch_refs:
2259 raise gclient_utils.Error(
2260 'The following --patch-ref flags were not used. Please fix it:\n%s'
2261 % ('\n'.join(patch_repo + '@' + patch_ref
2262 for patch_repo, patch_ref in patch_refs.items())))
2263
2264 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
2265 # they have fsmonitor enabled.
2266 if command == 'update':
2267 # Check if any of the root dependency have submodules.
2268 is_submoduled = any(
2269 map(
2270 lambda d: d.git_dependencies_state in
2271 (gclient_eval.SUBMODULES, gclient_eval.SYNC),
2272 self.dependencies))
2273 if is_submoduled:
2274 git_common.warn_submodule()
2275
2276 # Once all the dependencies have been processed, it's now safe to write
2277 # out the gn_args_file and run the hooks.
2278 removed_cipd_entries = []
2279 if command == 'update':
2280 for dependency in self.dependencies:
2281 gn_args_dep = dependency
2282 if gn_args_dep._gn_args_from:
2283 deps_map = {
2284 dep.name: dep
2285 for dep in gn_args_dep.dependencies
2286 }
2287 gn_args_dep = deps_map.get(gn_args_dep._gn_args_from)
2288 if gn_args_dep and gn_args_dep.HasGNArgsFile():
2289 gn_args_dep.WriteGNArgsFile()
2290
2291 removed_cipd_entries = self._RemoveUnversionedGitDirs()
2292
2293 # Sync CIPD dependencies once removed deps are deleted. In case a git
2294 # dependency was moved to CIPD, we want to remove the old git directory
2295 # first and then sync the CIPD dep.
2296 if self._cipd_root:
2297 self._cipd_root.run(command)
2298 # It's possible that CIPD removed some entries that are now part of
2299 # git worktree. Try to checkout those directories
2300 if removed_cipd_entries:
2301 for cipd_entry in removed_cipd_entries:
2302 cwd = os.path.join(self._root_dir, cipd_entry.split(':')[0])
2303 cwd, tail = os.path.split(cwd)
2304 if cwd:
2305 try:
2306 gclient_scm.scm.GIT.Capture(['checkout', tail],
2307 cwd=cwd)
2308 except subprocess2.CalledProcessError:
2309 pass
2310
2311 if not self._options.nohooks:
2312 if should_show_progress:
2313 pm = Progress('Running hooks', 1)
2314 self.RunHooksRecursively(self._options, pm)
2315
2316 self._WriteFileContents(PREVIOUS_SYNC_COMMITS_FILE,
2317 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
2318
2319 return 0
2320
2321 def PrintRevInfo(self):
2322 if not self.dependencies:
2323 raise gclient_utils.Error('No solution specified')
2324 # Load all the settings.
2325 work_queue = gclient_utils.ExecutionQueue(self._options.jobs,
2326 None,
2327 False,
2328 verbose=self._options.verbose)
2329 for s in self.dependencies:
2330 if s.should_process:
2331 work_queue.enqueue(s)
2332 work_queue.flush({},
2333 None, [],
2334 options=self._options,
2335 patch_refs=None,
2336 target_branches=None,
2337 skip_sync_revisions=None)
2338
2339 def ShouldPrintRevision(dep):
2340 return (not self._options.filter
2341 or dep.FuzzyMatchUrl(self._options.filter))
2342
2343 if self._options.snapshot:
2344 json_output = []
2345 # First level at .gclient
2346 for d in self.dependencies:
2347 entries = {}
2348
2349 def GrabDeps(dep):
2350 """Recursively grab dependencies."""
2351 for rec_d in dep.dependencies:
2352 rec_d.PinToActualRevision()
2353 if ShouldPrintRevision(rec_d):
2354 entries[rec_d.name] = rec_d.url
2355 GrabDeps(rec_d)
2356
2357 GrabDeps(d)
2358 json_output.append({
2359 'name': d.name,
2360 'solution_url': d.url,
2361 'deps_file': d.deps_file,
2362 'managed': d.managed,
2363 'custom_deps': entries,
2364 })
2365 if self._options.output_json == '-':
2366 print(json.dumps(json_output, indent=2, separators=(',', ': ')))
2367 elif self._options.output_json:
2368 with open(self._options.output_json, 'w') as f:
2369 json.dump(json_output, f)
2370 else:
2371 # Print the snapshot configuration file
2372 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {
2373 'solution_list': pprint.pformat(json_output, indent=2),
2374 })
Michael Mossd683d7c2018-06-15 05:05:17 +00002375 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002376 entries = {}
2377 for d in self.root.subtree(False):
2378 if self._options.actual:
2379 d.PinToActualRevision()
2380 if ShouldPrintRevision(d):
2381 entries[d.name] = d.url
2382 if self._options.output_json:
2383 json_output = {
2384 name: {
2385 'url': rev.split('@')[0] if rev else None,
2386 'rev':
2387 rev.split('@')[1] if rev and '@' in rev else None,
2388 }
2389 for name, rev in entries.items()
2390 }
2391 if self._options.output_json == '-':
2392 print(
2393 json.dumps(json_output,
2394 indent=2,
2395 separators=(',', ': ')))
2396 else:
2397 with open(self._options.output_json, 'w') as f:
2398 json.dump(json_output, f)
2399 else:
2400 keys = sorted(entries.keys())
2401 for x in keys:
2402 print('%s: %s' % (x, entries[x]))
2403 logging.info(str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002404
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002405 def ParseDepsFile(self):
2406 """No DEPS to parse for a .gclient file."""
2407 raise gclient_utils.Error('Internal error')
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00002408
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002409 def PrintLocationAndContents(self):
2410 # Print out the .gclient file. This is longer than if we just printed
2411 # the client dict, but more legible, and it might contain helpful
2412 # comments.
2413 print('Loaded .gclient config in %s:\n%s' %
2414 (self.root_dir, self.config_content))
sergiyb@chromium.orgfa2707e2016-03-12 00:40:56 +00002415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002416 def GetCipdRoot(self):
2417 if not self._cipd_root:
2418 self._cipd_root = gclient_scm.CipdRoot(
2419 self.root_dir,
2420 # TODO(jbudorick): Support other service URLs as necessary.
2421 # Service URLs should be constant over the scope of a cipd
2422 # root, so a var per DEPS file specifying the service URL
2423 # should suffice.
Yiwei Zhang52353702023-09-18 15:53:52 +00002424 'https://chrome-infra-packages.appspot.com',
2425 log_level='info' if self._options.verbose else None)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002426 return self._cipd_root
John Budorickd3ba72b2018-03-20 12:27:42 -07002427
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002428 @property
2429 def root_dir(self):
2430 """Root directory of gclient checkout."""
2431 return self._root_dir
maruel@chromium.org75a59272010-06-11 22:34:03 +00002432
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002433 @property
2434 def enforced_os(self):
2435 """What deps_os entries that are to be parsed."""
2436 return self._enforced_os
maruel@chromium.org271375b2010-06-23 19:17:38 +00002437
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002438 @property
2439 def target_os(self):
2440 return self._enforced_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +00002441
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002442 @property
2443 def target_cpu(self):
2444 return self._enforced_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -08002445
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002446
John Budorick0f7b2002018-01-19 15:46:17 -08002447class CipdDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002448 """A Dependency object that represents a single CIPD package."""
2449 def __init__(self, parent, name, dep_value, cipd_root, custom_vars,
2450 should_process, relative, condition):
2451 package = dep_value['package']
2452 version = dep_value['version']
2453 url = urllib.parse.urljoin(cipd_root.service_url,
2454 '%s@%s' % (package, version))
2455 super(CipdDependency, self).__init__(parent=parent,
2456 name=name + ':' + package,
2457 url=url,
2458 managed=None,
2459 custom_deps=None,
2460 custom_vars=custom_vars,
2461 custom_hooks=None,
2462 deps_file=None,
2463 should_process=should_process,
2464 should_recurse=False,
2465 relative=relative,
2466 condition=condition)
2467 self._cipd_package = None
2468 self._cipd_root = cipd_root
2469 # CIPD wants /-separated paths, even on Windows.
2470 native_subdir_path = os.path.relpath(
2471 os.path.join(self.root.root_dir, name), cipd_root.root_dir)
2472 self._cipd_subdir = posixpath.join(*native_subdir_path.split(os.sep))
2473 self._package_name = package
2474 self._package_version = version
John Budorick0f7b2002018-01-19 15:46:17 -08002475
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002476 #override
2477 def run(self, revision_overrides, command, args, work_queue, options,
2478 patch_refs, target_branches, skip_sync_revisions):
2479 """Runs |command| then parse the DEPS file."""
2480 logging.info('CipdDependency(%s).run()' % self.name)
2481 if not self.should_process:
2482 return
2483 self._CreatePackageIfNecessary()
2484 super(CipdDependency,
2485 self).run(revision_overrides, command, args, work_queue, options,
2486 patch_refs, target_branches, skip_sync_revisions)
John Budorickd3ba72b2018-03-20 12:27:42 -07002487
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002488 def _CreatePackageIfNecessary(self):
2489 # We lazily create the CIPD package to make sure that only packages
2490 # that we want (as opposed to all packages defined in all DEPS files
2491 # we parse) get added to the root and subsequently ensured.
2492 if not self._cipd_package:
2493 self._cipd_package = self._cipd_root.add_package(
2494 self._cipd_subdir, self._package_name, self._package_version)
John Budorickd3ba72b2018-03-20 12:27:42 -07002495
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002496 def ParseDepsFile(self):
2497 """CIPD dependencies are not currently allowed to have nested deps."""
2498 self.add_dependencies_and_close([], [])
John Budorick0f7b2002018-01-19 15:46:17 -08002499
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002500 #override
2501 def verify_validity(self):
2502 """CIPD dependencies allow duplicate name for packages in same directory."""
2503 logging.info('Dependency(%s).verify_validity()' % self.name)
2504 return True
John Budorick0f7b2002018-01-19 15:46:17 -08002505
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002506 #override
2507 def GetScmName(self):
2508 """Always 'cipd'."""
2509 return 'cipd'
Shenghua Zhang6f830312018-02-26 11:45:07 -08002510
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002511 def GetExpandedPackageName(self):
2512 """Return the CIPD package name with the variables evaluated."""
2513 package = self._cipd_root.expand_package_name(self._package_name)
2514 if package:
2515 return package
2516 return self._package_name
John Budorick0f7b2002018-01-19 15:46:17 -08002517
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002518 #override
2519 def CreateSCM(self, out_cb=None):
2520 """Create a Wrapper instance suitable for handling this CIPD dependency."""
2521 self._CreatePackageIfNecessary()
2522 return gclient_scm.CipdWrapper(self.url,
2523 self.root.root_dir,
2524 self.name,
2525 self.outbuf,
2526 out_cb,
2527 root=self._cipd_root,
2528 package=self._cipd_package)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +00002529
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002530 def hierarchy(self, include_url=False, graphviz=False):
2531 if graphviz:
2532 return '' # graphviz lines not implemented for cipd deps.
2533 return self.parent.hierarchy(include_url) + ' -> ' + self._cipd_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08002534
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002535 def ToLines(self):
2536 # () -> Sequence[str]
2537 """Return a list of lines representing this in a DEPS file."""
2538 def escape_cipd_var(package):
2539 return package.replace('{', '{{').replace('}', '}}')
Edward Lemure4e15042018-06-28 18:07:00 +00002540
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002541 s = []
2542 self._CreatePackageIfNecessary()
2543 if self._cipd_package.authority_for_subdir:
2544 condition_part = ([' "condition": %r,' %
2545 self.condition] if self.condition else [])
2546 s.extend([
2547 ' # %s' % self.hierarchy(include_url=False),
2548 ' "%s": {' % (self.name.split(':')[0], ),
2549 ' "packages": [',
2550 ])
2551 for p in sorted(self._cipd_root.packages(self._cipd_subdir),
2552 key=lambda x: x.name):
2553 s.extend([
2554 ' {',
2555 ' "package": "%s",' % escape_cipd_var(p.name),
2556 ' "version": "%s",' % p.version,
2557 ' },',
2558 ])
John Budorickc35aba52018-06-28 20:57:03 +00002559
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002560 s.extend([
2561 ' ],',
2562 ' "dep_type": "cipd",',
2563 ] + condition_part + [
2564 ' },',
2565 '',
2566 ])
2567 return s
John Budorick0f7b2002018-01-19 15:46:17 -08002568
2569
maruel@chromium.org5ca27692010-05-26 19:32:41 +00002570#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002571
2572
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002573@subcommand.usage('[command] [args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002574@metrics.collector.collect_metrics('gclient recurse')
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002575def CMDrecurse(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002576 """Operates [command args ...] on all the dependencies.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002577
Arthur Milchior08cd5fe2022-07-28 20:38:47 +00002578 Change directory to each dependency's directory, and call [command
2579 args ...] there. Sets GCLIENT_DEP_PATH environment variable as the
2580 dep's relative location to root directory of the checkout.
2581
2582 Examples:
2583 * `gclient recurse --no-progress -j1 sh -c 'echo "$GCLIENT_DEP_PATH"'`
2584 print the relative path of each dependency.
2585 * `gclient recurse --no-progress -j1 sh -c "pwd"`
2586 print the absolute path of each dependency.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002587 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002588 # Stop parsing at the first non-arg so that these go through to the command
2589 parser.disable_interspersed_args()
2590 parser.add_option('-s',
2591 '--scm',
2592 action='append',
2593 default=[],
2594 help='Choose scm types to operate upon.')
2595 parser.add_option('-i',
2596 '--ignore',
2597 action='store_true',
2598 help='Ignore non-zero return codes from subcommands.')
2599 parser.add_option(
2600 '--prepend-dir',
2601 action='store_true',
2602 help='Prepend relative dir for use with git <cmd> --null.')
2603 parser.add_option(
2604 '--no-progress',
2605 action='store_true',
2606 help='Disable progress bar that shows sub-command updates')
2607 options, args = parser.parse_args(args)
2608 if not args:
2609 print('Need to supply a command!', file=sys.stderr)
2610 return 1
2611 root_and_entries = gclient_utils.GetGClientRootAndEntries()
2612 if not root_and_entries:
2613 print(
2614 'You need to run gclient sync at least once to use \'recurse\'.\n'
2615 'This is because .gclient_entries needs to exist and be up to date.',
2616 file=sys.stderr)
2617 return 1
davidbarr@chromium.org47ca0ee2012-03-02 16:06:11 +00002618
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002619 # Normalize options.scm to a set()
2620 scm_set = set()
2621 for scm in options.scm:
2622 scm_set.update(scm.split(','))
2623 options.scm = scm_set
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002624
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002625 options.nohooks = True
2626 client = GClient.LoadCurrentConfig(options)
2627 if not client:
2628 raise gclient_utils.Error(
2629 'client not configured; see \'gclient config\'')
2630 return client.RunOnDeps('recurse',
2631 args,
2632 ignore_requirements=True,
2633 progress=not options.no_progress)
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002634
2635
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002636@subcommand.usage('[args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002637@metrics.collector.collect_metrics('gclient fetch')
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002638def CMDfetch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002639 """Fetches upstream commits for all modules.
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002640
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002641 Completely git-specific. Simply runs 'git fetch [args ...]' for each module.
2642 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002643 (options, args) = parser.parse_args(args)
2644 return CMDrecurse(
2645 OptionParser(),
2646 ['--jobs=%d' % options.jobs, '--scm=git', 'git', 'fetch'] + args)
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00002647
2648
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002649class Flattener(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002650 """Flattens a gclient solution."""
2651 def __init__(self, client, pin_all_deps=False):
2652 """Constructor.
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002653
2654 Arguments:
2655 client (GClient): client to flatten
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002656 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2657 in DEPS
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002658 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002659 self._client = client
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002660
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002661 self._deps_string = None
2662 self._deps_graph_lines = None
2663 self._deps_files = set()
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002664
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002665 self._allowed_hosts = set()
2666 self._deps = {}
2667 self._hooks = []
2668 self._pre_deps_hooks = []
2669 self._vars = {}
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002670
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002671 self._flatten(pin_all_deps=pin_all_deps)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002672
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002673 @property
2674 def deps_string(self):
2675 assert self._deps_string is not None
2676 return self._deps_string
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002677
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002678 @property
2679 def deps_graph_lines(self):
2680 assert self._deps_graph_lines is not None
2681 return self._deps_graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00002682
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002683 @property
2684 def deps_files(self):
2685 return self._deps_files
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002686
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002687 def _pin_dep(self, dep):
2688 """Pins a dependency to specific full revision sha.
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002689
2690 Arguments:
2691 dep (Dependency): dependency to process
2692 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002693 if dep.url is None:
2694 return
Michael Mossd683d7c2018-06-15 05:05:17 +00002695
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002696 # Make sure the revision is always fully specified (a hash),
2697 # as opposed to refs or tags which might change. Similarly,
2698 # shortened shas might become ambiguous; make sure to always
2699 # use full one for pinning.
2700 revision = gclient_utils.SplitUrlRevision(dep.url)[1]
2701 if not revision or not gclient_utils.IsFullGitSha(revision):
2702 dep.PinToActualRevision()
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002703
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002704 def _flatten(self, pin_all_deps=False):
2705 """Runs the flattener. Saves resulting DEPS string.
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002706
2707 Arguments:
2708 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2709 in DEPS
2710 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002711 for solution in self._client.dependencies:
2712 self._add_dep(solution)
2713 self._flatten_dep(solution)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002714
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002715 if pin_all_deps:
2716 for dep in self._deps.values():
2717 self._pin_dep(dep)
Paweł Hajdan, Jr39300ba2017-08-11 16:52:38 +02002718
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002719 def add_deps_file(dep):
2720 # Only include DEPS files referenced by recursedeps.
2721 if not dep.should_recurse:
2722 return
2723 deps_file = dep.deps_file
2724 deps_path = os.path.join(self._client.root_dir, dep.name, deps_file)
2725 if not os.path.exists(deps_path):
2726 # gclient has a fallback that if deps_file doesn't exist, it'll
2727 # try DEPS. Do the same here.
2728 deps_file = 'DEPS'
2729 deps_path = os.path.join(self._client.root_dir, dep.name,
2730 deps_file)
2731 if not os.path.exists(deps_path):
2732 return
2733 assert dep.url
2734 self._deps_files.add((dep.url, deps_file, dep.hierarchy_data()))
Paweł Hajdan, Jr76a9d042017-08-18 20:05:41 +02002735
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002736 for dep in self._deps.values():
2737 add_deps_file(dep)
Joanna Wang9144b672023-02-24 23:36:17 +00002738
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002739 gn_args_dep = self._deps.get(self._client.dependencies[0]._gn_args_from,
2740 self._client.dependencies[0])
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002741
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002742 self._deps_graph_lines = _DepsToDotGraphLines(self._deps)
2743 self._deps_string = '\n'.join(
2744 _GNSettingsToLines(gn_args_dep._gn_args_file, gn_args_dep._gn_args)
2745 + _AllowedHostsToLines(self._allowed_hosts) +
2746 _DepsToLines(self._deps) + _HooksToLines('hooks', self._hooks) +
2747 _HooksToLines('pre_deps_hooks', self._pre_deps_hooks) +
2748 _VarsToLines(self._vars) + [
2749 '# %s, %s' % (url, deps_file)
2750 for url, deps_file, _ in sorted(self._deps_files)
2751 ] + ['']) # Ensure newline at end of file.
2752
2753 def _add_dep(self, dep):
2754 """Helper to add a dependency to flattened DEPS.
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002755
2756 Arguments:
2757 dep (Dependency): dependency to add
2758 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002759 assert dep.name not in self._deps or self._deps.get(
2760 dep.name) == dep, (dep.name, self._deps.get(dep.name))
2761 if dep.url:
2762 self._deps[dep.name] = dep
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002763
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002764 def _flatten_dep(self, dep):
2765 """Visits a dependency in order to flatten it (see CMDflatten).
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002766
2767 Arguments:
2768 dep (Dependency): dependency to process
2769 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002770 logging.debug('_flatten_dep(%s)', dep.name)
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002771
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002772 assert dep.deps_parsed, (
2773 "Attempted to flatten %s but it has not been processed." % dep.name)
Paweł Hajdan, Jrc69b32e2017-08-17 18:47:48 +02002774
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002775 self._allowed_hosts.update(dep.allowed_hosts)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002776
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002777 # Only include vars explicitly listed in the DEPS files or gclient
2778 # solution, not automatic, local overrides (i.e. not all of
2779 # dep.get_vars()).
2780 hierarchy = dep.hierarchy(include_url=False)
2781 for key, value in dep._vars.items():
2782 # Make sure there are no conflicting variables. It is fine however
2783 # to use same variable name, as long as the value is consistent.
2784 assert key not in self._vars or self._vars[key][1] == value, (
2785 "dep:%s key:%s value:%s != %s" %
2786 (dep.name, key, value, self._vars[key][1]))
2787 self._vars[key] = (hierarchy, value)
2788 # Override explicit custom variables.
2789 for key, value in dep.custom_vars.items():
2790 # Do custom_vars that don't correspond to DEPS vars ever make sense?
2791 # DEPS conditionals shouldn't be using vars that aren't also defined
2792 # in the DEPS (presubmit actually disallows this), so any new
2793 # custom_var must be unused in the DEPS, so no need to add it to the
2794 # flattened output either.
2795 if key not in self._vars:
2796 continue
2797 # Don't "override" existing vars if it's actually the same value.
2798 if self._vars[key][1] == value:
2799 continue
2800 # Anything else is overriding a default value from the DEPS.
2801 self._vars[key] = (hierarchy + ' [custom_var override]', value)
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02002802
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002803 self._pre_deps_hooks.extend([(dep, hook)
2804 for hook in dep.pre_deps_hooks])
2805 self._hooks.extend([(dep, hook) for hook in dep.deps_hooks])
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002806
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002807 for sub_dep in dep.dependencies:
2808 self._add_dep(sub_dep)
Paweł Hajdan, Jrb0ad16e2017-08-03 15:33:21 +02002809
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002810 for d in dep.dependencies:
2811 if d.should_recurse:
2812 self._flatten_dep(d)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002813
2814
Joanna Wang3ab2f212023-08-09 01:25:15 +00002815@metrics.collector.collect_metrics('gclient gitmodules')
2816def CMDgitmodules(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002817 """Adds or updates Git Submodules based on the contents of the DEPS file.
Joanna Wang3ab2f212023-08-09 01:25:15 +00002818
2819 This command should be run in the root director of the repo.
2820 It will create or update the .gitmodules file and include
2821 `gclient-condition` values. Commits in gitlinks will also be updated.
2822 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002823 parser.add_option('--output-gitmodules',
2824 help='name of the .gitmodules file to write to',
2825 default='.gitmodules')
2826 parser.add_option(
2827 '--deps-file',
2828 help=
2829 'name of the deps file to parse for git dependency paths and commits.',
2830 default='DEPS')
2831 parser.add_option(
2832 '--skip-dep',
2833 action="append",
2834 help='skip adding gitmodules for the git dependency at the given path',
2835 default=[])
2836 options, args = parser.parse_args(args)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002837
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002838 deps_dir = os.path.dirname(os.path.abspath(options.deps_file))
2839 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
2840 if not gclient_path:
2841 logging.error(
2842 '.gclient not found\n'
2843 'Make sure you are running this script from a gclient workspace.')
2844 sys.exit(1)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002845
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002846 deps_content = gclient_utils.FileRead(options.deps_file)
2847 ls = gclient_eval.Parse(deps_content, options.deps_file, None, None)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002848
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002849 prefix_length = 0
2850 if not 'use_relative_paths' in ls or ls['use_relative_paths'] != True:
2851 delta_path = os.path.relpath(deps_dir, os.path.abspath(gclient_path))
2852 if delta_path:
2853 prefix_length = len(delta_path.replace(os.path.sep, '/')) + 1
Joanna Wang3ab2f212023-08-09 01:25:15 +00002854
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002855 cache_info = []
2856 with open(options.output_gitmodules, 'w', newline='') as f:
2857 for path, dep in ls.get('deps').items():
2858 if path in options.skip_dep:
2859 continue
2860 if dep.get('dep_type') == 'cipd':
2861 continue
2862 try:
2863 url, commit = dep['url'].split('@', maxsplit=1)
2864 except ValueError:
2865 logging.error('error on %s; %s, not adding it', path,
2866 dep["url"])
2867 continue
2868 if prefix_length:
2869 path = path[prefix_length:]
Joanna Wang3ab2f212023-08-09 01:25:15 +00002870
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002871 cache_info += ['--cacheinfo', f'160000,{commit},{path}']
2872 f.write(f'[submodule "{path}"]\n\tpath = {path}\n\turl = {url}\n')
2873 if 'condition' in dep:
2874 f.write(f'\tgclient-condition = {dep["condition"]}\n')
2875 # Windows has limit how long, so let's chunk those calls.
2876 if len(cache_info) >= 100:
2877 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
2878 cache_info = []
2879
2880 if cache_info:
Josip Sokcevic293aa652023-08-23 18:55:20 +00002881 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002882 subprocess2.call(['git', 'add', '.gitmodules'])
2883 print('.gitmodules and gitlinks updated. Please check git diff and '
2884 'commit changes.')
Joanna Wang3ab2f212023-08-09 01:25:15 +00002885
2886
Edward Lemur3298e7b2018-07-17 18:21:27 +00002887@metrics.collector.collect_metrics('gclient flatten')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002888def CMDflatten(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002889 """Flattens the solutions into a single DEPS file."""
2890 parser.add_option('--output-deps', help='Path to the output DEPS file')
2891 parser.add_option(
2892 '--output-deps-files',
2893 help=('Path to the output metadata about DEPS files referenced by '
2894 'recursedeps.'))
2895 parser.add_option(
2896 '--pin-all-deps',
2897 action='store_true',
2898 help=('Pin all deps, even if not pinned in DEPS. CAVEAT: only does so '
2899 'for checked out deps, NOT deps_os.'))
2900 parser.add_option('--deps-graph-file',
2901 help='Provide a path for the output graph file')
2902 options, args = parser.parse_args(args)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002903
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002904 options.nohooks = True
2905 options.process_all_deps = True
2906 client = GClient.LoadCurrentConfig(options)
2907 if not client:
2908 raise gclient_utils.Error(
2909 'client not configured; see \'gclient config\'')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002910
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002911 # Only print progress if we're writing to a file. Otherwise, progress
2912 # updates could obscure intended output.
2913 code = client.RunOnDeps('flatten', args, progress=options.output_deps)
2914 if code != 0:
2915 return code
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002916
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002917 flattener = Flattener(client, pin_all_deps=options.pin_all_deps)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002918
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002919 if options.output_deps:
2920 with open(options.output_deps, 'w') as f:
2921 f.write(flattener.deps_string)
2922 else:
2923 print(flattener.deps_string)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002924
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002925 if options.deps_graph_file:
2926 with open(options.deps_graph_file, 'w') as f:
2927 f.write('\n'.join(flattener.deps_graph_lines))
Joanna Wang9144b672023-02-24 23:36:17 +00002928
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002929 deps_files = [{
2930 'url': d[0],
2931 'deps_file': d[1],
2932 'hierarchy': d[2]
2933 } for d in sorted(flattener.deps_files)]
2934 if options.output_deps_files:
2935 with open(options.output_deps_files, 'w') as f:
2936 json.dump(deps_files, f)
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002937
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002938 return 0
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002939
2940
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002941def _GNSettingsToLines(gn_args_file, gn_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002942 s = []
2943 if gn_args_file:
2944 s.extend([
2945 'gclient_gn_args_file = "%s"' % gn_args_file,
2946 'gclient_gn_args = %r' % gn_args,
2947 ])
2948 return s
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002949
2950
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02002951def _AllowedHostsToLines(allowed_hosts):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002952 """Converts |allowed_hosts| set to list of lines for output."""
2953 if not allowed_hosts:
2954 return []
2955 s = ['allowed_hosts = [']
2956 for h in sorted(allowed_hosts):
2957 s.append(' "%s",' % h)
2958 s.extend([']', ''])
2959 return s
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02002960
2961
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002962def _DepsToLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002963 # type: (Mapping[str, Dependency]) -> Sequence[str]
2964 """Converts |deps| dict to list of lines for output."""
2965 if not deps:
2966 return []
2967 s = ['deps = {']
2968 for _, dep in sorted(deps.items()):
2969 s.extend(dep.ToLines())
2970 s.extend(['}', ''])
2971 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002972
2973
Joanna Wang9144b672023-02-24 23:36:17 +00002974def _DepsToDotGraphLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002975 # type: (Mapping[str, Dependency]) -> Sequence[str]
2976 """Converts |deps| dict to list of lines for dot graphs"""
2977 if not deps:
2978 return []
2979 graph_lines = ["digraph {\n\trankdir=\"LR\";"]
2980 for _, dep in sorted(deps.items()):
2981 line = dep.hierarchy(include_url=False, graphviz=True)
2982 if line:
2983 graph_lines.append("\t%s" % line)
2984 graph_lines.append("}")
2985 return graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00002986
2987
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02002988def _DepsOsToLines(deps_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002989 """Converts |deps_os| dict to list of lines for output."""
2990 if not deps_os:
2991 return []
2992 s = ['deps_os = {']
2993 for dep_os, os_deps in sorted(deps_os.items()):
2994 s.append(' "%s": {' % dep_os)
2995 for name, dep in sorted(os_deps.items()):
2996 condition_part = ([' "condition": %r,' %
2997 dep.condition] if dep.condition else [])
2998 s.extend([
2999 ' # %s' % dep.hierarchy(include_url=False),
3000 ' "%s": {' % (name, ),
3001 ' "url": "%s",' % (dep.url, ),
3002 ] + condition_part + [
3003 ' },',
3004 '',
3005 ])
3006 s.extend([' },', ''])
3007 s.extend(['}', ''])
3008 return s
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02003009
3010
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003011def _HooksToLines(name, hooks):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003012 """Converts |hooks| list to list of lines for output."""
3013 if not hooks:
3014 return []
3015 s = ['%s = [' % name]
3016 for dep, hook in hooks:
3017 s.extend([
3018 ' # %s' % dep.hierarchy(include_url=False),
3019 ' {',
3020 ])
3021 if hook.name is not None:
3022 s.append(' "name": "%s",' % hook.name)
3023 if hook.pattern is not None:
3024 s.append(' "pattern": "%s",' % hook.pattern)
3025 if hook.condition is not None:
3026 s.append(' "condition": %r,' % hook.condition)
3027 # Flattened hooks need to be written relative to the root gclient dir
3028 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3029 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3030 [' "%s",' % arg
3031 for arg in hook.action] + [' ]', ' },', ''])
3032 s.extend([']', ''])
3033 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003034
3035
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003036def _HooksOsToLines(hooks_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003037 """Converts |hooks| list to list of lines for output."""
3038 if not hooks_os:
3039 return []
3040 s = ['hooks_os = {']
3041 for hook_os, os_hooks in hooks_os.items():
3042 s.append(' "%s": [' % hook_os)
3043 for dep, hook in os_hooks:
3044 s.extend([
3045 ' # %s' % dep.hierarchy(include_url=False),
3046 ' {',
3047 ])
3048 if hook.name is not None:
3049 s.append(' "name": "%s",' % hook.name)
3050 if hook.pattern is not None:
3051 s.append(' "pattern": "%s",' % hook.pattern)
3052 if hook.condition is not None:
3053 s.append(' "condition": %r,' % hook.condition)
3054 # Flattened hooks need to be written relative to the root gclient
3055 # dir
3056 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3057 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3058 [' "%s",' % arg
3059 for arg in hook.action] + [' ]', ' },', ''])
3060 s.extend([' ],', ''])
3061 s.extend(['}', ''])
3062 return s
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003063
3064
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003065def _VarsToLines(variables):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003066 """Converts |variables| dict to list of lines for output."""
3067 if not variables:
3068 return []
3069 s = ['vars = {']
3070 for key, tup in sorted(variables.items()):
3071 hierarchy, value = tup
3072 s.extend([
3073 ' # %s' % hierarchy,
3074 ' "%s": %r,' % (key, value),
3075 '',
3076 ])
3077 s.extend(['}', ''])
3078 return s
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003079
3080
Edward Lemur3298e7b2018-07-17 18:21:27 +00003081@metrics.collector.collect_metrics('gclient grep')
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003082def CMDgrep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003083 """Greps through git repos managed by gclient.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003084
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003085 Runs 'git grep [args...]' for each module.
3086 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003087 # We can't use optparse because it will try to parse arguments sent
3088 # to git grep and throw an error. :-(
3089 if not args or re.match('(-h|--help)$', args[0]):
3090 print(
3091 'Usage: gclient grep [-j <N>] git-grep-args...\n\n'
3092 'Example: "gclient grep -j10 -A2 RefCountedBase" runs\n"git grep '
3093 '-A2 RefCountedBase" on each of gclient\'s git\nrepos with up to '
3094 '10 jobs.\n\nBonus: page output by appending "|& less -FRSX" to the'
3095 ' end of your query.',
3096 file=sys.stderr)
3097 return 1
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003098
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003099 jobs_arg = ['--jobs=1']
3100 if re.match(r'(-j|--jobs=)\d+$', args[0]):
3101 jobs_arg, args = args[:1], args[1:]
3102 elif re.match(r'(-j|--jobs)$', args[0]):
3103 jobs_arg, args = args[:2], args[2:]
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003104
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003105 return CMDrecurse(
3106 parser, jobs_arg + [
3107 '--ignore', '--prepend-dir', '--no-progress', '--scm=git', 'git',
3108 'grep', '--null', '--color=Always'
3109 ] + args)
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00003110
3111
Edward Lemur3298e7b2018-07-17 18:21:27 +00003112@metrics.collector.collect_metrics('gclient root')
stip@chromium.orga735da22015-04-29 23:18:20 +00003113def CMDroot(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003114 """Outputs the solution root (or current dir if there isn't one)."""
3115 (options, args) = parser.parse_args(args)
3116 client = GClient.LoadCurrentConfig(options)
3117 if client:
3118 print(os.path.abspath(client.root_dir))
3119 else:
3120 print(os.path.abspath('.'))
stip@chromium.orga735da22015-04-29 23:18:20 +00003121
3122
agablea98a6cd2016-11-15 14:30:10 -08003123@subcommand.usage('[url]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00003124@metrics.collector.collect_metrics('gclient config')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003125def CMDconfig(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003126 """Creates a .gclient file in the current directory.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003127
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003128 This specifies the configuration for further commands. After update/sync,
3129 top-level DEPS files in each module are read to determine dependent
3130 modules to operate on as well. If optional [url] parameter is
3131 provided, then configuration is read from a specified Subversion server
3132 URL.
3133 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003134 # We do a little dance with the --gclientfile option. 'gclient config' is
3135 # the only command where it's acceptable to have both '--gclientfile' and
3136 # '--spec' arguments. So, we temporarily stash any --gclientfile parameter
3137 # into options.output_config_file until after the (gclientfile xor spec)
3138 # error check.
3139 parser.remove_option('--gclientfile')
3140 parser.add_option('--gclientfile',
3141 dest='output_config_file',
3142 help='Specify an alternate .gclient file')
3143 parser.add_option('--name',
3144 help='overrides the default name for the solution')
3145 parser.add_option(
3146 '--deps-file',
3147 default='DEPS',
3148 help='overrides the default name for the DEPS file for the '
3149 'main solutions and all sub-dependencies')
3150 parser.add_option('--unmanaged',
3151 action='store_true',
3152 default=False,
3153 help='overrides the default behavior to make it possible '
3154 'to have the main solution untouched by gclient '
3155 '(gclient will check out unmanaged dependencies but '
3156 'will never sync them)')
3157 parser.add_option('--cache-dir',
3158 default=UNSET_CACHE_DIR,
3159 help='Cache all git repos into this dir and do shared '
3160 'clones from the cache, instead of cloning directly '
3161 'from the remote. Pass "None" to disable cache, even '
3162 'if globally enabled due to $GIT_CACHE_PATH.')
3163 parser.add_option('--custom-var',
3164 action='append',
3165 dest='custom_vars',
3166 default=[],
3167 help='overrides variables; key=value syntax')
3168 parser.set_defaults(config_filename=None)
3169 (options, args) = parser.parse_args(args)
3170 if options.output_config_file:
3171 setattr(options, 'config_filename',
3172 getattr(options, 'output_config_file'))
3173 if ((options.spec and args) or len(args) > 2
3174 or (not options.spec and not args)):
3175 parser.error(
3176 'Inconsistent arguments. Use either --spec or one or 2 args')
maruel@chromium.org5fc2a332010-05-26 19:37:15 +00003177
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003178 if (options.cache_dir is not UNSET_CACHE_DIR
3179 and options.cache_dir.lower() == 'none'):
3180 options.cache_dir = None
Robert Iannuccia19649b2018-06-29 16:31:45 +00003181
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003182 custom_vars = {}
3183 for arg in options.custom_vars:
3184 kv = arg.split('=', 1)
3185 if len(kv) != 2:
3186 parser.error('Invalid --custom-var argument: %r' % arg)
3187 custom_vars[kv[0]] = gclient_eval.EvaluateCondition(kv[1], {})
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02003188
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003189 client = GClient('.', options)
3190 if options.spec:
3191 client.SetConfig(options.spec)
iposva@chromium.org8cf7a392010-04-07 17:20:26 +00003192 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003193 base_url = args[0].rstrip('/')
3194 if not options.name:
3195 name = base_url.split('/')[-1]
3196 if name.endswith('.git'):
3197 name = name[:-4]
3198 else:
3199 # specify an alternate relpath for the given URL.
3200 name = options.name
3201 if not os.path.abspath(os.path.join(os.getcwd(), name)).startswith(
3202 os.getcwd()):
3203 parser.error('Do not pass a relative path for --name.')
3204 if any(x in ('..', '.', '/', '\\') for x in name.split(os.sep)):
3205 parser.error(
3206 'Do not include relative path components in --name.')
agable@chromium.orgf2214672015-10-27 21:02:48 +00003207
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003208 deps_file = options.deps_file
3209 client.SetDefaultConfig(name,
3210 deps_file,
3211 base_url,
3212 managed=not options.unmanaged,
3213 cache_dir=options.cache_dir,
3214 custom_vars=custom_vars)
3215 client.SaveConfig()
3216 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003217
3218
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003219@subcommand.epilog("""Example:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003220 gclient pack > patch.txt
3221 generate simple patch for configured client and dependences
3222""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003223@metrics.collector.collect_metrics('gclient pack')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003224def CMDpack(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003225 """Generates a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003226
agabled437d762016-10-17 09:35:11 -07003227 Internally, runs 'git diff' on each checked out module and
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003228 dependencies, and performs minimal postprocessing of the output. The
3229 resulting patch is printed to stdout and can be applied to a freshly
3230 checked out tree via 'patch -p0 < patchfile'.
3231 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003232 parser.add_option('--deps',
3233 dest='deps_os',
3234 metavar='OS_LIST',
3235 help='override deps for the specified (comma-separated) '
3236 'platform(s); \'all\' will process all deps_os '
3237 'references')
3238 parser.remove_option('--jobs')
3239 (options, args) = parser.parse_args(args)
3240 # Force jobs to 1 so the stdout is not annotated with the thread ids
3241 options.jobs = 1
3242 client = GClient.LoadCurrentConfig(options)
3243 if not client:
3244 raise gclient_utils.Error(
3245 'client not configured; see \'gclient config\'')
3246 if options.verbose:
3247 client.PrintLocationAndContents()
3248 return client.RunOnDeps('pack', args)
kbr@google.comab318592009-09-04 00:54:55 +00003249
3250
Edward Lemur3298e7b2018-07-17 18:21:27 +00003251@metrics.collector.collect_metrics('gclient status')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003252def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003253 """Shows modification status for every dependencies."""
3254 parser.add_option('--deps',
3255 dest='deps_os',
3256 metavar='OS_LIST',
3257 help='override deps for the specified (comma-separated) '
3258 'platform(s); \'all\' will process all deps_os '
3259 'references')
3260 (options, args) = parser.parse_args(args)
3261 client = GClient.LoadCurrentConfig(options)
3262 if not client:
3263 raise gclient_utils.Error(
3264 'client not configured; see \'gclient config\'')
3265 if options.verbose:
3266 client.PrintLocationAndContents()
3267 return client.RunOnDeps('status', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003268
3269
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003270@subcommand.epilog("""Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +00003271 gclient sync
3272 update files from SCM according to current configuration,
3273 *for modules which have changed since last update or sync*
3274 gclient sync --force
3275 update files from SCM according to current configuration, for
3276 all modules (useful for recovering files deleted from local copy)
Edward Lesmes3ffca4b2021-05-19 19:36:17 +00003277 gclient sync --revision src@GIT_COMMIT_OR_REF
3278 update src directory to GIT_COMMIT_OR_REF
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003279
3280JSON output format:
3281If the --output-json option is specified, the following document structure will
3282be emitted to the provided file. 'null' entries may occur for subprojects which
3283are present in the gclient solution, but were not processed (due to custom_deps,
3284os_deps, etc.)
3285
3286{
3287 "solutions" : {
3288 "<name>": { # <name> is the posix-normalized path to the solution.
agabled437d762016-10-17 09:35:11 -07003289 "revision": [<git id hex string>|null],
3290 "scm": ["git"|null],
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003291 }
3292 }
3293}
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003294""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003295@metrics.collector.collect_metrics('gclient sync')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003296def CMDsync(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003297 """Checkout/update all modules."""
3298 parser.add_option('-f',
3299 '--force',
3300 action='store_true',
3301 help='force update even for unchanged modules')
3302 parser.add_option('-n',
3303 '--nohooks',
3304 action='store_true',
3305 help='don\'t run hooks after the update is complete')
3306 parser.add_option('-p',
3307 '--noprehooks',
3308 action='store_true',
3309 help='don\'t run pre-DEPS hooks',
3310 default=False)
3311 parser.add_option('-r',
3312 '--revision',
3313 action='append',
3314 dest='revisions',
3315 metavar='REV',
3316 default=[],
3317 help='Enforces git ref/hash for the solutions with the '
3318 'format src@rev. The src@ part is optional and can be '
3319 'skipped. You can also specify URLs instead of paths '
3320 'and gclient will find the solution corresponding to '
3321 'the given URL. If a path is also specified, the URL '
3322 'takes precedence. -r can be used multiple times when '
3323 '.gclient has multiple solutions configured, and will '
3324 'work even if the src@ part is skipped. Revision '
3325 'numbers (e.g. 31000 or r31000) are not supported.')
3326 parser.add_option('--patch-ref',
3327 action='append',
3328 dest='patch_refs',
3329 metavar='GERRIT_REF',
3330 default=[],
3331 help='Patches the given reference with the format '
3332 'dep@target-ref:patch-ref. '
3333 'For |dep|, you can specify URLs as well as paths, '
3334 'with URLs taking preference. '
3335 '|patch-ref| will be applied to |dep|, rebased on top '
3336 'of what |dep| was synced to, and a soft reset will '
3337 'be done. Use --no-rebase-patch-ref and '
3338 '--no-reset-patch-ref to disable this behavior. '
3339 '|target-ref| is the target branch against which a '
3340 'patch was created, it is used to determine which '
3341 'commits from the |patch-ref| actually constitute a '
3342 'patch.')
3343 parser.add_option(
3344 '-t',
3345 '--download-topics',
3346 action='store_true',
3347 help='Downloads and patches locally changes from all open '
3348 'Gerrit CLs that have the same topic as the changes '
3349 'in the specified patch_refs. Only works if atleast '
3350 'one --patch-ref is specified.')
3351 parser.add_option('--with_branch_heads',
3352 action='store_true',
3353 help='Clone git "branch_heads" refspecs in addition to '
3354 'the default refspecs. This adds about 1/2GB to a '
3355 'full checkout. (git only)')
3356 parser.add_option(
3357 '--with_tags',
3358 action='store_true',
3359 help='Clone git tags in addition to the default refspecs.')
3360 parser.add_option('-H',
3361 '--head',
3362 action='store_true',
3363 help='DEPRECATED: only made sense with safesync urls.')
3364 parser.add_option(
3365 '-D',
3366 '--delete_unversioned_trees',
3367 action='store_true',
3368 help='Deletes from the working copy any dependencies that '
3369 'have been removed since the last sync, as long as '
3370 'there are no local modifications. When used with '
3371 '--force, such dependencies are removed even if they '
3372 'have local modifications. When used with --reset, '
3373 'all untracked directories are removed from the '
3374 'working copy, excluding those which are explicitly '
3375 'ignored in the repository.')
3376 parser.add_option(
3377 '-R',
3378 '--reset',
3379 action='store_true',
3380 help='resets any local changes before updating (git only)')
3381 parser.add_option('-M',
3382 '--merge',
3383 action='store_true',
3384 help='merge upstream changes instead of trying to '
3385 'fast-forward or rebase')
3386 parser.add_option('-A',
3387 '--auto_rebase',
3388 action='store_true',
3389 help='Automatically rebase repositories against local '
3390 'checkout during update (git only).')
3391 parser.add_option('--deps',
3392 dest='deps_os',
3393 metavar='OS_LIST',
3394 help='override deps for the specified (comma-separated) '
3395 'platform(s); \'all\' will process all deps_os '
3396 'references')
3397 parser.add_option('--process-all-deps',
3398 action='store_true',
3399 help='Check out all deps, even for different OS-es, '
3400 'or with conditions evaluating to false')
3401 parser.add_option('--upstream',
3402 action='store_true',
3403 help='Make repo state match upstream branch.')
3404 parser.add_option('--output-json',
3405 help='Output a json document to this path containing '
3406 'summary information about the sync.')
3407 parser.add_option(
3408 '--no-history',
3409 action='store_true',
3410 help='GIT ONLY - Reduces the size/time of the checkout at '
3411 'the cost of no history. Requires Git 1.9+')
3412 parser.add_option('--shallow',
3413 action='store_true',
3414 help='GIT ONLY - Do a shallow clone into the cache dir. '
3415 'Requires Git 1.9+')
3416 parser.add_option('--no_bootstrap',
3417 '--no-bootstrap',
3418 action='store_true',
3419 help='Don\'t bootstrap from Google Storage.')
3420 parser.add_option('--ignore_locks',
3421 action='store_true',
3422 help='No longer used.')
3423 parser.add_option('--break_repo_locks',
3424 action='store_true',
3425 help='No longer used.')
3426 parser.add_option('--lock_timeout',
3427 type='int',
3428 default=5000,
3429 help='GIT ONLY - Deadline (in seconds) to wait for git '
3430 'cache lock to become available. Default is %default.')
3431 parser.add_option('--no-rebase-patch-ref',
3432 action='store_false',
3433 dest='rebase_patch_ref',
3434 default=True,
3435 help='Bypass rebase of the patch ref after checkout.')
3436 parser.add_option('--no-reset-patch-ref',
3437 action='store_false',
3438 dest='reset_patch_ref',
3439 default=True,
3440 help='Bypass calling reset after patching the ref.')
3441 parser.add_option('--experiment',
3442 action='append',
3443 dest='experiments',
3444 default=[],
3445 help='Which experiments should be enabled.')
3446 (options, args) = parser.parse_args(args)
3447 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003448
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003449 if not client:
3450 raise gclient_utils.Error(
3451 'client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003452
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003453 if options.download_topics and not options.rebase_patch_ref:
3454 raise gclient_utils.Error(
3455 'Warning: You cannot download topics and not rebase each patch ref')
Ravi Mistryecda7822022-02-28 16:22:20 +00003456
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003457 if options.ignore_locks:
3458 print(
3459 'Warning: ignore_locks is no longer used. Please remove its usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003460
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003461 if options.break_repo_locks:
3462 print('Warning: break_repo_locks is no longer used. Please remove its '
3463 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003464
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003465 if options.revisions and options.head:
3466 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
3467 print('Warning: you cannot use both --head and --revision')
smutae7ea312016-07-18 11:59:41 -07003468
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003469 if options.verbose:
3470 client.PrintLocationAndContents()
3471 ret = client.RunOnDeps('update', args)
3472 if options.output_json:
3473 slns = {}
3474 for d in client.subtree(True):
3475 normed = d.name.replace('\\', '/').rstrip('/') + '/'
3476 slns[normed] = {
3477 'revision': d.got_revision,
3478 'scm': d.used_scm.name if d.used_scm else None,
3479 'url': str(d.url) if d.url else None,
3480 'was_processed': d.should_process,
3481 'was_synced': d._should_sync,
3482 }
3483 with open(options.output_json, 'w') as f:
3484 json.dump({'solutions': slns}, f)
3485 return ret
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003486
3487
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003488CMDupdate = CMDsync
3489
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003490
Edward Lemur3298e7b2018-07-17 18:21:27 +00003491@metrics.collector.collect_metrics('gclient validate')
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003492def CMDvalidate(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003493 """Validates the .gclient and DEPS syntax."""
3494 options, args = parser.parse_args(args)
3495 client = GClient.LoadCurrentConfig(options)
3496 if not client:
3497 raise gclient_utils.Error(
3498 'client not configured; see \'gclient config\'')
3499 rv = client.RunOnDeps('validate', args)
3500 if rv == 0:
3501 print('validate: SUCCESS')
3502 else:
3503 print('validate: FAILURE')
3504 return rv
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003505
3506
Edward Lemur3298e7b2018-07-17 18:21:27 +00003507@metrics.collector.collect_metrics('gclient diff')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003508def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003509 """Displays local diff for every dependencies."""
3510 parser.add_option('--deps',
3511 dest='deps_os',
3512 metavar='OS_LIST',
3513 help='override deps for the specified (comma-separated) '
3514 'platform(s); \'all\' will process all deps_os '
3515 'references')
3516 (options, args) = parser.parse_args(args)
3517 client = GClient.LoadCurrentConfig(options)
3518 if not client:
3519 raise gclient_utils.Error(
3520 'client not configured; see \'gclient config\'')
3521 if options.verbose:
3522 client.PrintLocationAndContents()
3523 return client.RunOnDeps('diff', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003524
3525
Edward Lemur3298e7b2018-07-17 18:21:27 +00003526@metrics.collector.collect_metrics('gclient revert')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003527def CMDrevert(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003528 """Reverts all modifications in every dependencies.
maruel@chromium.org28d14bd2010-11-11 20:37:09 +00003529
3530 That's the nuclear option to get back to a 'clean' state. It removes anything
agabled437d762016-10-17 09:35:11 -07003531 that shows up in git status."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003532 parser.add_option('--deps',
3533 dest='deps_os',
3534 metavar='OS_LIST',
3535 help='override deps for the specified (comma-separated) '
3536 'platform(s); \'all\' will process all deps_os '
3537 'references')
3538 parser.add_option('-n',
3539 '--nohooks',
3540 action='store_true',
3541 help='don\'t run hooks after the revert is complete')
3542 parser.add_option('-p',
3543 '--noprehooks',
3544 action='store_true',
3545 help='don\'t run pre-DEPS hooks',
3546 default=False)
3547 parser.add_option('--upstream',
3548 action='store_true',
3549 help='Make repo state match upstream branch.')
3550 parser.add_option('--break_repo_locks',
3551 action='store_true',
3552 help='No longer used.')
3553 (options, args) = parser.parse_args(args)
3554 if options.break_repo_locks:
3555 print(
3556 'Warning: break_repo_locks is no longer used. Please remove its ' +
3557 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003558
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003559 # --force is implied.
3560 options.force = True
3561 options.reset = False
3562 options.delete_unversioned_trees = False
3563 options.merge = False
3564 client = GClient.LoadCurrentConfig(options)
3565 if not client:
3566 raise gclient_utils.Error(
3567 'client not configured; see \'gclient config\'')
3568 return client.RunOnDeps('revert', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003569
3570
Edward Lemur3298e7b2018-07-17 18:21:27 +00003571@metrics.collector.collect_metrics('gclient runhooks')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003572def CMDrunhooks(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003573 """Runs hooks for files that have been modified in the local working copy."""
3574 parser.add_option('--deps',
3575 dest='deps_os',
3576 metavar='OS_LIST',
3577 help='override deps for the specified (comma-separated) '
3578 'platform(s); \'all\' will process all deps_os '
3579 'references')
3580 parser.add_option('-f',
3581 '--force',
3582 action='store_true',
3583 default=True,
3584 help='Deprecated. No effect.')
3585 (options, args) = parser.parse_args(args)
3586 client = GClient.LoadCurrentConfig(options)
3587 if not client:
3588 raise gclient_utils.Error(
3589 'client not configured; see \'gclient config\'')
3590 if options.verbose:
3591 client.PrintLocationAndContents()
3592 options.force = True
3593 options.nohooks = False
3594 return client.RunOnDeps('runhooks', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003595
3596
Gavin Mak50b27a52023-09-19 22:44:59 +00003597# TODO(crbug.com/1481266): Collect merics for installhooks.
3598def CMDinstallhooks(parser, args):
3599 """Installs gclient git hooks.
3600
3601 Currently only installs a pre-commit hook to drop staged gitlinks. To
3602 bypass this pre-commit hook once it's installed, set the environment
3603 variable SKIP_GITLINK_PRECOMMIT=1.
3604 """
3605 (options, args) = parser.parse_args(args)
3606 client = GClient.LoadCurrentConfig(options)
3607 if not client:
3608 raise gclient_utils.Error(
3609 'client not configured; see \'gclient config\'')
3610 client._InstallPreCommitHook()
3611 return 0
3612
3613
Edward Lemur3298e7b2018-07-17 18:21:27 +00003614@metrics.collector.collect_metrics('gclient revinfo')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003615def CMDrevinfo(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003616 """Outputs revision info mapping for the client and its dependencies.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003617
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00003618 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003619 can be used to reproduce the same tree in the future. It is only useful for
agabled437d762016-10-17 09:35:11 -07003620 'unpinned dependencies', i.e. DEPS/deps references without a git hash.
3621 A git branch name isn't 'pinned' since the actual commit can change.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003622 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003623 parser.add_option('--deps',
3624 dest='deps_os',
3625 metavar='OS_LIST',
3626 help='override deps for the specified (comma-separated) '
3627 'platform(s); \'all\' will process all deps_os '
3628 'references')
3629 parser.add_option(
3630 '-a',
3631 '--actual',
3632 action='store_true',
3633 help='gets the actual checked out revisions instead of the '
3634 'ones specified in the DEPS and .gclient files')
3635 parser.add_option('-s',
3636 '--snapshot',
3637 action='store_true',
3638 help='creates a snapshot .gclient file of the current '
3639 'version of all repositories to reproduce the tree, '
3640 'implies -a')
3641 parser.add_option(
3642 '--filter',
3643 action='append',
3644 dest='filter',
3645 help='Display revision information only for the specified '
3646 'dependencies (filtered by URL or path).')
3647 parser.add_option('--output-json',
3648 help='Output a json document to this path containing '
3649 'information about the revisions.')
3650 parser.add_option(
3651 '--ignore-dep-type',
3652 choices=['git', 'cipd'],
3653 help='Specify to skip processing of a certain type of dep.')
3654 (options, args) = parser.parse_args(args)
3655 client = GClient.LoadCurrentConfig(options)
3656 if not client:
3657 raise gclient_utils.Error(
3658 'client not configured; see \'gclient config\'')
3659 client.PrintRevInfo()
3660 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003661
3662
Edward Lemur3298e7b2018-07-17 18:21:27 +00003663@metrics.collector.collect_metrics('gclient getdep')
Edward Lesmes411041f2018-04-05 20:12:55 -04003664def CMDgetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003665 """Gets revision information and variable values from a DEPS file.
Josip Sokcevic7b5e3d72023-06-13 00:28:23 +00003666
3667 If key doesn't exist or is incorrectly declared, this script exits with exit
3668 code 2."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003669 parser.add_option('--var',
3670 action='append',
3671 dest='vars',
3672 metavar='VAR',
3673 default=[],
3674 help='Gets the value of a given variable.')
3675 parser.add_option(
3676 '-r',
3677 '--revision',
3678 action='append',
3679 dest='getdep_revisions',
3680 metavar='DEP',
3681 default=[],
3682 help='Gets the revision/version for the given dependency. '
3683 'If it is a git dependency, dep must be a path. If it '
3684 'is a CIPD dependency, dep must be of the form '
3685 'path:package.')
3686 parser.add_option(
3687 '--deps-file',
3688 default='DEPS',
3689 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3690 # .gclient first.
3691 help='The DEPS file to be edited. Defaults to the DEPS '
3692 'file in the current directory.')
3693 (options, args) = parser.parse_args(args)
Edward Lesmes411041f2018-04-05 20:12:55 -04003694
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003695 if not os.path.isfile(options.deps_file):
3696 raise gclient_utils.Error('DEPS file %s does not exist.' %
3697 options.deps_file)
3698 with open(options.deps_file) as f:
3699 contents = f.read()
3700 client = GClient.LoadCurrentConfig(options)
3701 if client is not None:
3702 builtin_vars = client.get_builtin_vars()
Edward Lesmes411041f2018-04-05 20:12:55 -04003703 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003704 logging.warning(
3705 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3706 'file without support for built-in variables.')
3707 builtin_vars = None
3708 local_scope = gclient_eval.Exec(contents,
3709 options.deps_file,
3710 builtin_vars=builtin_vars)
3711
3712 for var in options.vars:
3713 print(gclient_eval.GetVar(local_scope, var))
3714
3715 commits = {}
3716 if local_scope.get(
3717 'git_dependencies'
3718 ) == gclient_eval.SUBMODULES and options.getdep_revisions:
3719 commits.update(
3720 scm_git.GIT.GetSubmoduleCommits(
3721 os.getcwd(),
3722 [path for path in options.getdep_revisions if ':' not in path]))
3723
3724 for name in options.getdep_revisions:
3725 if ':' in name:
3726 name, _, package = name.partition(':')
3727 if not name or not package:
3728 parser.error(
3729 'Wrong CIPD format: %s:%s should be of the form path:pkg.' %
3730 (name, package))
3731 print(gclient_eval.GetCIPD(local_scope, name, package))
3732 elif commits:
3733 print(commits[name])
3734 else:
3735 try:
3736 print(gclient_eval.GetRevision(local_scope, name))
3737 except KeyError as e:
3738 print(repr(e), file=sys.stderr)
3739 sys.exit(2)
Edward Lesmes411041f2018-04-05 20:12:55 -04003740
3741
Edward Lemur3298e7b2018-07-17 18:21:27 +00003742@metrics.collector.collect_metrics('gclient setdep')
Edward Lesmes6f64a052018-03-20 17:35:49 -04003743def CMDsetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003744 """Modifies dependency revisions and variable values in a DEPS file"""
3745 parser.add_option('--var',
3746 action='append',
3747 dest='vars',
3748 metavar='VAR=VAL',
3749 default=[],
3750 help='Sets a variable to the given value with the format '
3751 'name=value.')
3752 parser.add_option('-r',
3753 '--revision',
3754 action='append',
3755 dest='setdep_revisions',
3756 metavar='DEP@REV',
3757 default=[],
3758 help='Sets the revision/version for the dependency with '
3759 'the format dep@rev. If it is a git dependency, dep '
3760 'must be a path and rev must be a git hash or '
3761 'reference (e.g. src/dep@deadbeef). If it is a CIPD '
3762 'dependency, dep must be of the form path:package and '
3763 'rev must be the package version '
3764 '(e.g. src/pkg:chromium/pkg@2.1-cr0).')
3765 parser.add_option(
3766 '--deps-file',
3767 default='DEPS',
3768 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3769 # .gclient first.
3770 help='The DEPS file to be edited. Defaults to the DEPS '
3771 'file in the current directory.')
3772 (options, args) = parser.parse_args(args)
3773 if args:
3774 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3775 if not options.setdep_revisions and not options.vars:
Edward Lesmes0ecf6d62018-04-05 18:28:55 -04003776 parser.error(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003777 'You must specify at least one variable or revision to modify.')
3778
3779 if not os.path.isfile(options.deps_file):
3780 raise gclient_utils.Error('DEPS file %s does not exist.' %
3781 options.deps_file)
3782 with open(options.deps_file) as f:
3783 contents = f.read()
3784
3785 client = GClient.LoadCurrentConfig(options)
3786 if client is not None:
3787 builtin_vars = client.get_builtin_vars()
Edward Lesmes6f64a052018-03-20 17:35:49 -04003788 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003789 logging.warning(
3790 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3791 'file without support for built-in variables.')
3792 builtin_vars = None
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003793
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003794 local_scope = gclient_eval.Exec(contents,
3795 options.deps_file,
3796 builtin_vars=builtin_vars)
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003797
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003798 # Create a set of all git submodules.
3799 cwd = os.path.dirname(options.deps_file) or os.getcwd()
3800 git_modules = None
3801 if 'git_dependencies' in local_scope and local_scope[
3802 'git_dependencies'] in (gclient_eval.SUBMODULES, gclient_eval.SYNC):
3803 try:
3804 submodule_status = subprocess2.check_output(
3805 ['git', 'submodule', 'status'], cwd=cwd).decode('utf-8')
3806 git_modules = {l.split()[1] for l in submodule_status.splitlines()}
3807 except subprocess2.CalledProcessError as e:
3808 print('Warning: gitlinks won\'t be updated: ', e)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003809
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003810 for var in options.vars:
3811 name, _, value = var.partition('=')
3812 if not name or not value:
3813 parser.error(
3814 'Wrong var format: %s should be of the form name=value.' % var)
3815 if name in local_scope['vars']:
3816 gclient_eval.SetVar(local_scope, name, value)
3817 else:
3818 gclient_eval.AddVar(local_scope, name, value)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003819
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003820 for revision in options.setdep_revisions:
3821 name, _, value = revision.partition('@')
3822 if not name or not value:
3823 parser.error('Wrong dep format: %s should be of the form dep@rev.' %
3824 revision)
3825 if ':' in name:
3826 name, _, package = name.partition(':')
3827 if not name or not package:
3828 parser.error(
3829 'Wrong CIPD format: %s:%s should be of the form path:pkg@version.'
3830 % (name, package))
3831 gclient_eval.SetCIPD(local_scope, name, package, value)
3832 else:
3833 # Update DEPS only when `git_dependencies` == DEPS or SYNC.
3834 # git_dependencies is defaulted to DEPS when not set.
3835 if 'git_dependencies' not in local_scope or local_scope[
3836 'git_dependencies'] in (gclient_eval.DEPS,
3837 gclient_eval.SYNC):
3838 gclient_eval.SetRevision(local_scope, name, value)
3839
3840 # Update git submodules when `git_dependencies` == SYNC or
3841 # SUBMODULES.
3842 if git_modules and 'git_dependencies' in local_scope and local_scope[
3843 'git_dependencies'] in (gclient_eval.SUBMODULES,
3844 gclient_eval.SYNC):
3845 git_module_name = name
3846 if not 'use_relative_paths' in local_scope or \
3847 local_scope['use_relative_paths'] != True:
3848 deps_dir = os.path.dirname(
3849 os.path.abspath(options.deps_file))
3850 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
3851 delta_path = None
3852 if gclient_path:
3853 delta_path = os.path.relpath(
3854 deps_dir, os.path.abspath(gclient_path))
3855 if delta_path:
3856 prefix_length = len(delta_path.replace(
3857 os.path.sep, '/')) + 1
3858 git_module_name = name[prefix_length:]
3859 # gclient setdep should update the revision, i.e., the gitlink
3860 # only when the submodule entry is already present within
3861 # .gitmodules.
3862 if git_module_name not in git_modules:
3863 raise KeyError(
3864 f'Could not find any dependency called "{git_module_name}" in '
3865 f'.gitmodules.')
3866
3867 # Update the gitlink for the submodule.
3868 subprocess2.call([
3869 'git', 'update-index', '--add', '--cacheinfo',
3870 f'160000,{value},{git_module_name}'
3871 ],
3872 cwd=cwd)
3873
3874 with open(options.deps_file, 'wb') as f:
3875 f.write(gclient_eval.RenderDEPSFile(local_scope).encode('utf-8'))
3876
3877 if git_modules:
3878 subprocess2.call(['git', 'add', options.deps_file], cwd=cwd)
3879 print('Changes have been staged. See changes with `git status`.\n'
3880 'Use `git commit -m "Manual roll"` to commit your changes. \n'
3881 'Run gclient sync to update your local dependency checkout.')
Josip Sokcevic5561f8b2023-08-21 16:00:42 +00003882
Edward Lesmes6f64a052018-03-20 17:35:49 -04003883
Edward Lemur3298e7b2018-07-17 18:21:27 +00003884@metrics.collector.collect_metrics('gclient verify')
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003885def CMDverify(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003886 """Verifies the DEPS file deps are only from allowed_hosts."""
3887 (options, args) = parser.parse_args(args)
3888 client = GClient.LoadCurrentConfig(options)
3889 if not client:
3890 raise gclient_utils.Error(
3891 'client not configured; see \'gclient config\'')
3892 client.RunOnDeps(None, [])
3893 # Look at each first-level dependency of this gclient only.
3894 for dep in client.dependencies:
3895 bad_deps = dep.findDepsFromNotAllowedHosts()
3896 if not bad_deps:
3897 continue
3898 print("There are deps from not allowed hosts in file %s" %
3899 dep.deps_file)
3900 for bad_dep in bad_deps:
3901 print("\t%s at %s" % (bad_dep.name, bad_dep.url))
3902 print("allowed_hosts:", ', '.join(dep.allowed_hosts))
3903 sys.stdout.flush()
3904 raise gclient_utils.Error(
3905 'dependencies from disallowed hosts; check your DEPS file.')
3906 return 0
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003907
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003908
3909@subcommand.epilog("""For more information on what metrics are we collecting and
Edward Lemur8a2e3312018-07-12 21:15:09 +00003910why, please read metrics.README.md or visit https://bit.ly/2ufRS4p""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003911@metrics.collector.collect_metrics('gclient metrics')
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003912def CMDmetrics(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003913 """Reports, and optionally modifies, the status of metric collection."""
3914 parser.add_option('--opt-in',
3915 action='store_true',
3916 dest='enable_metrics',
3917 help='Opt-in to metrics collection.',
3918 default=None)
3919 parser.add_option('--opt-out',
3920 action='store_false',
3921 dest='enable_metrics',
3922 help='Opt-out of metrics collection.')
3923 options, args = parser.parse_args(args)
3924 if args:
3925 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3926 if not metrics.collector.config.is_googler:
3927 print("You're not a Googler. Metrics collection is disabled for you.")
3928 return 0
3929
3930 if options.enable_metrics is not None:
3931 metrics.collector.config.opted_in = options.enable_metrics
3932
3933 if metrics.collector.config.opted_in is None:
3934 print("You haven't opted in or out of metrics collection.")
3935 elif metrics.collector.config.opted_in:
3936 print("You have opted in. Thanks!")
3937 else:
3938 print("You have opted out. Please consider opting in.")
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003939 return 0
3940
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003941
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003942class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003943 gclientfile_default = os.environ.get('GCLIENT_FILE', '.gclient')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003944
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003945 def __init__(self, **kwargs):
3946 optparse.OptionParser.__init__(self,
3947 version='%prog ' + __version__,
3948 **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003949
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003950 # Some arm boards have issues with parallel sync.
3951 if platform.machine().startswith('arm'):
3952 jobs = 1
3953 else:
3954 jobs = max(8, gclient_utils.NumLocalCpus())
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003955
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003956 self.add_option(
3957 '-j',
3958 '--jobs',
3959 default=jobs,
3960 type='int',
3961 help='Specify how many SCM commands can run in parallel; defaults to '
3962 '%default on this machine')
3963 self.add_option(
3964 '-v',
3965 '--verbose',
3966 action='count',
3967 default=0,
3968 help='Produces additional output for diagnostics. Can be used up to '
3969 'three times for more logging info.')
3970 self.add_option('--gclientfile',
3971 dest='config_filename',
3972 help='Specify an alternate %s file' %
3973 self.gclientfile_default)
3974 self.add_option(
3975 '--spec',
3976 help='create a gclient file containing the provided string. Due to '
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003977 'Cygwin/Python brokenness, it can\'t contain any newlines.')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003978 self.add_option('--no-nag-max',
3979 default=False,
3980 action='store_true',
3981 help='Ignored for backwards compatibility.')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003982
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003983 def parse_args(self, args=None, _values=None):
3984 """Integrates standard options processing."""
3985 # Create an optparse.Values object that will store only the actual
3986 # passed options, without the defaults.
3987 actual_options = optparse.Values()
3988 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
3989 # Create an optparse.Values object with the default options.
3990 options = optparse.Values(self.get_default_values().__dict__)
3991 # Update it with the options passed by the user.
3992 options._update_careful(actual_options.__dict__)
3993 # Store the options passed by the user in an _actual_options attribute.
3994 # We store only the keys, and not the values, since the values can
3995 # contain arbitrary information, which might be PII.
3996 metrics.collector.add('arguments', list(actual_options.__dict__))
Edward Lemur3298e7b2018-07-17 18:21:27 +00003997
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003998 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
3999 logging.basicConfig(
4000 level=levels[min(options.verbose,
4001 len(levels) - 1)],
4002 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
4003 if options.config_filename and options.spec:
4004 self.error('Cannot specify both --gclientfile and --spec')
4005 if (options.config_filename and options.config_filename !=
4006 os.path.basename(options.config_filename)):
4007 self.error('--gclientfile target must be a filename, not a path')
4008 if not options.config_filename:
4009 options.config_filename = self.gclientfile_default
4010 options.entries_filename = options.config_filename + '_entries'
4011 if options.jobs < 1:
4012 self.error('--jobs must be 1 or higher')
maruel@chromium.org0895b752011-08-26 20:40:33 +00004013
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004014 # These hacks need to die.
4015 if not hasattr(options, 'revisions'):
4016 # GClient.RunOnDeps expects it even if not applicable.
4017 options.revisions = []
4018 if not hasattr(options, 'experiments'):
4019 options.experiments = []
4020 if not hasattr(options, 'head'):
4021 options.head = None
4022 if not hasattr(options, 'nohooks'):
4023 options.nohooks = True
4024 if not hasattr(options, 'noprehooks'):
4025 options.noprehooks = True
4026 if not hasattr(options, 'deps_os'):
4027 options.deps_os = None
4028 if not hasattr(options, 'force'):
4029 options.force = None
4030 return (options, args)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004031
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004032
4033def disable_buffering():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004034 # Make stdout auto-flush so buildbot doesn't kill us during lengthy
4035 # operations. Python as a strong tendency to buffer sys.stdout.
4036 sys.stdout = gclient_utils.MakeFileAutoFlush(sys.stdout)
4037 # Make stdout annotated with the thread ids.
4038 sys.stdout = gclient_utils.MakeFileAnnotated(sys.stdout)
maruel@chromium.org0895b752011-08-26 20:40:33 +00004039
4040
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004041def path_contains_tilde():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004042 for element in os.environ['PATH'].split(os.pathsep):
4043 if element.startswith('~') and os.path.abspath(
4044 os.path.realpath(
4045 os.path.expanduser(element))) == DEPOT_TOOLS_DIR:
4046 return True
4047 return False
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004048
4049
4050def can_run_gclient_and_helpers():
Gavin Mak7f5b53f2023-09-07 18:13:01 +00004051 if sys.version_info[0] < 3:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004052 print('\nYour python version %s is unsupported, please upgrade.\n' %
4053 sys.version.split(' ', 1)[0],
4054 file=sys.stderr)
4055 return False
4056 if not sys.executable:
4057 print('\nPython cannot find the location of it\'s own executable.\n',
4058 file=sys.stderr)
4059 return False
4060 if path_contains_tilde():
4061 print(
4062 '\nYour PATH contains a literal "~", which works in some shells ' +
4063 'but will break when python tries to run subprocesses. ' +
4064 'Replace the "~" with $HOME.\n' + 'See https://crbug.com/952865.\n',
4065 file=sys.stderr)
4066 return False
4067 return True
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004068
4069
4070def main(argv):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004071 """Doesn't parse the arguments here, just find the right subcommand to
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004072 execute."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004073 if not can_run_gclient_and_helpers():
4074 return 2
4075 fix_encoding.fix_encoding()
4076 disable_buffering()
4077 setup_color.init()
4078 dispatcher = subcommand.CommandDispatcher(__name__)
4079 try:
4080 return dispatcher.execute(OptionParser(), argv)
4081 except KeyboardInterrupt:
4082 gclient_utils.GClientChildren.KillAllRemainingChildren()
4083 raise
4084 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
4085 print('Error: %s' % str(e), file=sys.stderr)
4086 return 1
4087 finally:
4088 gclient_utils.PrintWarnings()
4089 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004090
4091
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00004092if '__main__' == __name__:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004093 with metrics.collector.print_notice_and_exit():
4094 sys.exit(main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004095
4096# vim: ts=2:sw=2:tw=80:et: