blob: 388799f82a9b4cc901f4b457601af5f00d7dc233 [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
Philipp Wollermanna45d2d42023-09-21 02:45:42 +00002009 git_hooks_dir = os.path.join(git_dir, 'hooks')
2010 os.makedirs(git_hooks_dir, exist_ok=True)
2011
Gavin Mak50b27a52023-09-19 22:44:59 +00002012 hook = os.path.join(git_dir, 'hooks', 'pre-commit')
2013 if os.path.exists(hook):
2014 with open(hook, 'r') as f:
2015 content = f.read()
2016 if PRECOMMIT_HOOK_VAR in content:
2017 print(f'{hook} already contains the gclient pre-commit hook.')
2018 else:
2019 print(f'A pre-commit hook already exists at {hook}.\n'
2020 f'Please append the following lines to the hook:\n\n'
2021 f'{gclient_hook_content}')
2022 return
2023
2024 print(f'Creating a pre-commit hook at {hook}')
2025 with open(hook, 'w') as f:
2026 f.write('#!/bin/sh\n')
2027 f.write(f'{gclient_hook_content}\n')
2028 os.chmod(hook, 0o755)
2029
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002030 def _RemoveUnversionedGitDirs(self):
2031 """Remove directories that are no longer part of the checkout.
Edward Lemur5b1fa942018-10-04 23:22:09 +00002032
2033 Notify the user if there is an orphaned entry in their working copy.
2034 Only delete the directory if there are no changes in it, and
2035 delete_unversioned_trees is set to true.
Josip Sokcevic1b8211f2022-09-30 17:46:53 +00002036
2037 Returns CIPD packages that are no longer versioned.
Edward Lemur5b1fa942018-10-04 23:22:09 +00002038 """
2039
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002040 entry_names_and_sync = [(i.name, i._should_sync)
2041 for i in self.root.subtree(False) if i.url]
2042 entries = []
2043 if entry_names_and_sync:
2044 entries, _ = zip(*entry_names_and_sync)
2045 full_entries = [
2046 os.path.join(self.root_dir, e.replace('/', os.path.sep))
2047 for e in entries
2048 ]
2049 no_sync_entries = [
2050 name for name, should_sync in entry_names_and_sync
2051 if not should_sync
2052 ]
Edward Lemur5b1fa942018-10-04 23:22:09 +00002053
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002054 removed_cipd_entries = []
Joanna Wang60adf7b2023-10-06 00:04:28 +00002055 read_entries = self._ReadEntries()
2056 # We process entries sorted in reverse to ensure a child dir is
2057 # always deleted before its parent dir.
2058 # This is especially important for submodules with pinned revisions
2059 # overwritten by a vars or custom_deps. In this case, if a parent
2060 # submodule is encountered first in the loop, it cannot tell the
2061 # difference between modifications from the vars or actual user
2062 # modifications that should be kept. http://crbug/1486677#c9 for
2063 # more details.
2064 for entry in sorted(read_entries, reverse=True):
2065 prev_url = read_entries[entry]
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002066 if not prev_url:
2067 # entry must have been overridden via .gclient custom_deps
2068 continue
2069 if any(entry.startswith(sln) for sln in no_sync_entries):
2070 # Dependencies of solutions that skipped syncing would not
2071 # show up in `entries`.
2072 continue
2073 if (':' in entry):
2074 # This is a cipd package. Don't clean it up, but prepare for
2075 # return
2076 if entry not in entries:
2077 removed_cipd_entries.append(entry)
2078 continue
2079 # Fix path separator on Windows.
2080 entry_fixed = entry.replace('/', os.path.sep)
2081 e_dir = os.path.join(self.root_dir, entry_fixed)
2082 # Use entry and not entry_fixed there.
2083 if (entry not in entries and
2084 (not any(path.startswith(entry + '/') for path in entries))
2085 and os.path.exists(e_dir)):
2086 # The entry has been removed from DEPS.
2087 scm = gclient_scm.GitWrapper(prev_url, self.root_dir,
2088 entry_fixed, self.outbuf)
Edward Lemur5b1fa942018-10-04 23:22:09 +00002089
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002090 # Check to see if this directory is now part of a higher-up
2091 # checkout.
2092 scm_root = None
2093 try:
2094 scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2095 scm.checkout_path)
2096 except subprocess2.CalledProcessError:
2097 pass
2098 if not scm_root:
2099 logging.warning(
2100 'Could not find checkout root for %s. Unable to '
2101 'determine whether it is part of a higher-level '
2102 'checkout, so not removing.' % entry)
2103 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002104
Joanna Wang60adf7b2023-10-06 00:04:28 +00002105 versioned_state = None
2106 # Check if this is a submodule or versioned directory.
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002107 if os.path.abspath(scm_root) == os.path.abspath(e_dir):
2108 e_par_dir = os.path.join(e_dir, os.pardir)
2109 if gclient_scm.scm.GIT.IsInsideWorkTree(e_par_dir):
2110 par_scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2111 e_par_dir)
2112 # rel_e_dir : relative path of entry w.r.t. its parent
2113 # repo.
2114 rel_e_dir = os.path.relpath(e_dir, par_scm_root)
Joanna Wang60adf7b2023-10-06 00:04:28 +00002115 versioned_state = gclient_scm.scm.GIT.IsVersioned(
2116 par_scm_root, rel_e_dir)
2117 # This is to handle the case of third_party/WebKit migrating
2118 # from being a DEPS entry to being part of the main project. If
2119 # the subproject is a Git project, we need to remove its .git
2120 # folder. Otherwise git operations on that folder will have
2121 # different effects depending on the current working directory.
2122 if versioned_state == gclient_scm.scm.VERSIONED_DIR:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002123 save_dir = scm.GetGitBackupDirPath()
2124 # Remove any eventual stale backup dir for the same
2125 # project.
2126 if os.path.exists(save_dir):
2127 gclient_utils.rmtree(save_dir)
2128 os.rename(os.path.join(e_dir, '.git'), save_dir)
2129 # When switching between the two states (entry/ is a
2130 # subproject -> entry/ is part of the outer
2131 # project), it is very likely that some files are
2132 # changed in the checkout, unless we are jumping
2133 # *exactly* across the commit which changed just
2134 # DEPS. In such case we want to cleanup any eventual
2135 # stale files (coming from the old subproject) in
2136 # order to end up with a clean checkout.
2137 gclient_scm.scm.GIT.CleanupDir(
2138 par_scm_root, rel_e_dir)
2139 assert not os.path.exists(
2140 os.path.join(e_dir, '.git'))
2141 print(
2142 '\nWARNING: \'%s\' has been moved from DEPS to a higher '
2143 'level checkout. The git folder containing all the local'
2144 ' branches has been saved to %s.\n'
2145 'If you don\'t care about its state you can safely '
2146 'remove that folder to free up space.' %
2147 (entry, save_dir))
2148 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002149
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002150 if scm_root in full_entries:
2151 logging.info(
2152 '%s is part of a higher level checkout, not removing',
2153 scm.GetCheckoutRoot())
2154 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002155
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002156 file_list = []
2157 scm.status(self._options, [], file_list)
2158 modified_files = file_list != []
2159 if (not self._options.delete_unversioned_trees
2160 or (modified_files and not self._options.force)):
2161 # There are modified files in this entry. Keep warning until
2162 # removed.
2163 self.add_dependency(
2164 GitDependency(
2165 parent=self,
2166 name=entry,
2167 # Update URL with scheme in protocol_override
2168 url=GitDependency.updateProtocol(
2169 prev_url, self.protocol),
2170 managed=False,
2171 custom_deps={},
2172 custom_vars={},
2173 custom_hooks=[],
2174 deps_file=None,
2175 should_process=True,
2176 should_recurse=False,
2177 relative=None,
2178 condition=None,
2179 protocol=self.protocol,
2180 git_dependencies_state=self.git_dependencies_state))
2181 if modified_files and self._options.delete_unversioned_trees:
2182 print(
2183 '\nWARNING: \'%s\' is no longer part of this client.\n'
2184 'Despite running \'gclient sync -D\' no action was taken '
2185 'as there are modifications.\nIt is recommended you revert '
2186 'all changes or run \'gclient sync -D --force\' next '
2187 'time.' % entry_fixed)
2188 else:
2189 print(
2190 '\nWARNING: \'%s\' is no longer part of this client.\n'
2191 'It is recommended that you manually remove it or use '
2192 '\'gclient sync -D\' next time.' % entry_fixed)
2193 else:
2194 # Delete the entry
2195 print('\n________ deleting \'%s\' in \'%s\'' %
2196 (entry_fixed, self.root_dir))
2197 gclient_utils.rmtree(e_dir)
Joanna Wang60adf7b2023-10-06 00:04:28 +00002198 # We restore empty directories of submodule paths.
2199 if versioned_state == gclient_scm.scm.VERSIONED_SUBMODULE:
2200 gclient_scm.scm.GIT.Capture(
2201 ['restore', '--', rel_e_dir], cwd=par_scm_root)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002202 # record the current list of entries for next time
2203 self._SaveEntries()
2204 return removed_cipd_entries
Edward Lemur5b1fa942018-10-04 23:22:09 +00002205
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002206 def RunOnDeps(self,
2207 command,
2208 args,
2209 ignore_requirements=False,
2210 progress=True):
2211 """Runs a command on each dependency in a client and its dependencies.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002212
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002213 Args:
2214 command: The command to use (e.g., 'status' or 'diff')
2215 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002216 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002217 if not self.dependencies:
2218 raise gclient_utils.Error('No solution specified')
Michael Mossd683d7c2018-06-15 05:05:17 +00002219
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002220 revision_overrides = {}
2221 patch_refs = {}
2222 target_branches = {}
2223 skip_sync_revisions = {}
2224 # It's unnecessary to check for revision overrides for 'recurse'.
2225 # Save a few seconds by not calling _EnforceRevisions() in that case.
2226 if command not in ('diff', 'recurse', 'runhooks', 'status', 'revert',
2227 'validate'):
2228 self._CheckConfig()
2229 revision_overrides = self._EnforceRevisions()
Edward Lesmesc621b212018-03-21 20:26:56 -04002230
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002231 if command == 'update':
2232 patch_refs, target_branches = self._EnforcePatchRefsAndBranches()
2233 if NO_SYNC_EXPERIMENT in self._options.experiments:
2234 skip_sync_revisions = self._EnforceSkipSyncRevisions(patch_refs)
Joanna Wang66286612022-06-30 19:59:13 +00002235
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002236 # Store solutions' custom_vars on memory to compare in the next run.
2237 # All dependencies added later are inherited from the current
2238 # self.dependencies.
2239 custom_vars = {
2240 dep.name: dep.custom_vars
2241 for dep in self.dependencies if dep.custom_vars
Michael Mossd683d7c2018-06-15 05:05:17 +00002242 }
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002243 if custom_vars:
2244 self._WriteFileContents(PREVIOUS_CUSTOM_VARS_FILE,
2245 json.dumps(custom_vars))
2246
2247 # Disable progress for non-tty stdout.
2248 should_show_progress = (setup_color.IS_TTY and not self._options.verbose
2249 and progress)
2250 pm = None
2251 if should_show_progress:
2252 if command in ('update', 'revert'):
2253 pm = Progress('Syncing projects', 1)
2254 elif command in ('recurse', 'validate'):
2255 pm = Progress(' '.join(args), 1)
2256 work_queue = gclient_utils.ExecutionQueue(
2257 self._options.jobs,
2258 pm,
2259 ignore_requirements=ignore_requirements,
2260 verbose=self._options.verbose)
2261 for s in self.dependencies:
2262 if s.should_process:
2263 work_queue.enqueue(s)
2264 work_queue.flush(revision_overrides,
2265 command,
2266 args,
2267 options=self._options,
2268 patch_refs=patch_refs,
2269 target_branches=target_branches,
2270 skip_sync_revisions=skip_sync_revisions)
2271
2272 if revision_overrides:
2273 print(
2274 'Please fix your script, having invalid --revision flags will soon '
2275 'be considered an error.',
2276 file=sys.stderr)
2277
2278 if patch_refs:
2279 raise gclient_utils.Error(
2280 'The following --patch-ref flags were not used. Please fix it:\n%s'
2281 % ('\n'.join(patch_repo + '@' + patch_ref
2282 for patch_repo, patch_ref in patch_refs.items())))
2283
2284 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
2285 # they have fsmonitor enabled.
2286 if command == 'update':
2287 # Check if any of the root dependency have submodules.
2288 is_submoduled = any(
2289 map(
2290 lambda d: d.git_dependencies_state in
2291 (gclient_eval.SUBMODULES, gclient_eval.SYNC),
2292 self.dependencies))
2293 if is_submoduled:
2294 git_common.warn_submodule()
2295
2296 # Once all the dependencies have been processed, it's now safe to write
2297 # out the gn_args_file and run the hooks.
2298 removed_cipd_entries = []
2299 if command == 'update':
2300 for dependency in self.dependencies:
2301 gn_args_dep = dependency
2302 if gn_args_dep._gn_args_from:
2303 deps_map = {
2304 dep.name: dep
2305 for dep in gn_args_dep.dependencies
2306 }
2307 gn_args_dep = deps_map.get(gn_args_dep._gn_args_from)
2308 if gn_args_dep and gn_args_dep.HasGNArgsFile():
2309 gn_args_dep.WriteGNArgsFile()
2310
2311 removed_cipd_entries = self._RemoveUnversionedGitDirs()
2312
2313 # Sync CIPD dependencies once removed deps are deleted. In case a git
2314 # dependency was moved to CIPD, we want to remove the old git directory
2315 # first and then sync the CIPD dep.
2316 if self._cipd_root:
2317 self._cipd_root.run(command)
2318 # It's possible that CIPD removed some entries that are now part of
2319 # git worktree. Try to checkout those directories
2320 if removed_cipd_entries:
2321 for cipd_entry in removed_cipd_entries:
2322 cwd = os.path.join(self._root_dir, cipd_entry.split(':')[0])
2323 cwd, tail = os.path.split(cwd)
2324 if cwd:
2325 try:
2326 gclient_scm.scm.GIT.Capture(['checkout', tail],
2327 cwd=cwd)
Joanna Wang60adf7b2023-10-06 00:04:28 +00002328 except (subprocess2.CalledProcessError, OSError):
2329 # repo of the deleted cipd may also have been deleted.
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002330 pass
2331
2332 if not self._options.nohooks:
2333 if should_show_progress:
2334 pm = Progress('Running hooks', 1)
2335 self.RunHooksRecursively(self._options, pm)
2336
2337 self._WriteFileContents(PREVIOUS_SYNC_COMMITS_FILE,
2338 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
2339
2340 return 0
2341
2342 def PrintRevInfo(self):
2343 if not self.dependencies:
2344 raise gclient_utils.Error('No solution specified')
2345 # Load all the settings.
2346 work_queue = gclient_utils.ExecutionQueue(self._options.jobs,
2347 None,
2348 False,
2349 verbose=self._options.verbose)
2350 for s in self.dependencies:
2351 if s.should_process:
2352 work_queue.enqueue(s)
2353 work_queue.flush({},
2354 None, [],
2355 options=self._options,
2356 patch_refs=None,
2357 target_branches=None,
2358 skip_sync_revisions=None)
2359
2360 def ShouldPrintRevision(dep):
2361 return (not self._options.filter
2362 or dep.FuzzyMatchUrl(self._options.filter))
2363
2364 if self._options.snapshot:
2365 json_output = []
2366 # First level at .gclient
2367 for d in self.dependencies:
2368 entries = {}
2369
2370 def GrabDeps(dep):
2371 """Recursively grab dependencies."""
2372 for rec_d in dep.dependencies:
2373 rec_d.PinToActualRevision()
2374 if ShouldPrintRevision(rec_d):
2375 entries[rec_d.name] = rec_d.url
2376 GrabDeps(rec_d)
2377
2378 GrabDeps(d)
2379 json_output.append({
2380 'name': d.name,
2381 'solution_url': d.url,
2382 'deps_file': d.deps_file,
2383 'managed': d.managed,
2384 'custom_deps': entries,
2385 })
2386 if self._options.output_json == '-':
2387 print(json.dumps(json_output, indent=2, separators=(',', ': ')))
2388 elif self._options.output_json:
2389 with open(self._options.output_json, 'w') as f:
2390 json.dump(json_output, f)
2391 else:
2392 # Print the snapshot configuration file
2393 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {
2394 'solution_list': pprint.pformat(json_output, indent=2),
2395 })
Michael Mossd683d7c2018-06-15 05:05:17 +00002396 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002397 entries = {}
2398 for d in self.root.subtree(False):
2399 if self._options.actual:
2400 d.PinToActualRevision()
2401 if ShouldPrintRevision(d):
2402 entries[d.name] = d.url
2403 if self._options.output_json:
2404 json_output = {
2405 name: {
2406 'url': rev.split('@')[0] if rev else None,
2407 'rev':
2408 rev.split('@')[1] if rev and '@' in rev else None,
2409 }
2410 for name, rev in entries.items()
2411 }
2412 if self._options.output_json == '-':
2413 print(
2414 json.dumps(json_output,
2415 indent=2,
2416 separators=(',', ': ')))
2417 else:
2418 with open(self._options.output_json, 'w') as f:
2419 json.dump(json_output, f)
2420 else:
2421 keys = sorted(entries.keys())
2422 for x in keys:
2423 print('%s: %s' % (x, entries[x]))
2424 logging.info(str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002425
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002426 def ParseDepsFile(self):
2427 """No DEPS to parse for a .gclient file."""
2428 raise gclient_utils.Error('Internal error')
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00002429
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002430 def PrintLocationAndContents(self):
2431 # Print out the .gclient file. This is longer than if we just printed
2432 # the client dict, but more legible, and it might contain helpful
2433 # comments.
2434 print('Loaded .gclient config in %s:\n%s' %
2435 (self.root_dir, self.config_content))
sergiyb@chromium.orgfa2707e2016-03-12 00:40:56 +00002436
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002437 def GetCipdRoot(self):
2438 if not self._cipd_root:
2439 self._cipd_root = gclient_scm.CipdRoot(
2440 self.root_dir,
2441 # TODO(jbudorick): Support other service URLs as necessary.
2442 # Service URLs should be constant over the scope of a cipd
2443 # root, so a var per DEPS file specifying the service URL
2444 # should suffice.
Yiwei Zhang52353702023-09-18 15:53:52 +00002445 'https://chrome-infra-packages.appspot.com',
2446 log_level='info' if self._options.verbose else None)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002447 return self._cipd_root
John Budorickd3ba72b2018-03-20 12:27:42 -07002448
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002449 @property
2450 def root_dir(self):
2451 """Root directory of gclient checkout."""
2452 return self._root_dir
maruel@chromium.org75a59272010-06-11 22:34:03 +00002453
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002454 @property
2455 def enforced_os(self):
2456 """What deps_os entries that are to be parsed."""
2457 return self._enforced_os
maruel@chromium.org271375b2010-06-23 19:17:38 +00002458
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002459 @property
2460 def target_os(self):
2461 return self._enforced_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +00002462
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002463 @property
2464 def target_cpu(self):
2465 return self._enforced_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -08002466
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002467
John Budorick0f7b2002018-01-19 15:46:17 -08002468class CipdDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002469 """A Dependency object that represents a single CIPD package."""
2470 def __init__(self, parent, name, dep_value, cipd_root, custom_vars,
2471 should_process, relative, condition):
2472 package = dep_value['package']
2473 version = dep_value['version']
2474 url = urllib.parse.urljoin(cipd_root.service_url,
2475 '%s@%s' % (package, version))
2476 super(CipdDependency, self).__init__(parent=parent,
2477 name=name + ':' + package,
2478 url=url,
2479 managed=None,
2480 custom_deps=None,
2481 custom_vars=custom_vars,
2482 custom_hooks=None,
2483 deps_file=None,
2484 should_process=should_process,
2485 should_recurse=False,
2486 relative=relative,
2487 condition=condition)
2488 self._cipd_package = None
2489 self._cipd_root = cipd_root
2490 # CIPD wants /-separated paths, even on Windows.
2491 native_subdir_path = os.path.relpath(
2492 os.path.join(self.root.root_dir, name), cipd_root.root_dir)
2493 self._cipd_subdir = posixpath.join(*native_subdir_path.split(os.sep))
2494 self._package_name = package
2495 self._package_version = version
John Budorick0f7b2002018-01-19 15:46:17 -08002496
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002497 #override
2498 def run(self, revision_overrides, command, args, work_queue, options,
2499 patch_refs, target_branches, skip_sync_revisions):
2500 """Runs |command| then parse the DEPS file."""
2501 logging.info('CipdDependency(%s).run()' % self.name)
2502 if not self.should_process:
2503 return
2504 self._CreatePackageIfNecessary()
2505 super(CipdDependency,
2506 self).run(revision_overrides, command, args, work_queue, options,
2507 patch_refs, target_branches, skip_sync_revisions)
John Budorickd3ba72b2018-03-20 12:27:42 -07002508
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002509 def _CreatePackageIfNecessary(self):
2510 # We lazily create the CIPD package to make sure that only packages
2511 # that we want (as opposed to all packages defined in all DEPS files
2512 # we parse) get added to the root and subsequently ensured.
2513 if not self._cipd_package:
2514 self._cipd_package = self._cipd_root.add_package(
2515 self._cipd_subdir, self._package_name, self._package_version)
John Budorickd3ba72b2018-03-20 12:27:42 -07002516
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002517 def ParseDepsFile(self):
2518 """CIPD dependencies are not currently allowed to have nested deps."""
2519 self.add_dependencies_and_close([], [])
John Budorick0f7b2002018-01-19 15:46:17 -08002520
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002521 #override
2522 def verify_validity(self):
2523 """CIPD dependencies allow duplicate name for packages in same directory."""
2524 logging.info('Dependency(%s).verify_validity()' % self.name)
2525 return True
John Budorick0f7b2002018-01-19 15:46:17 -08002526
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002527 #override
2528 def GetScmName(self):
2529 """Always 'cipd'."""
2530 return 'cipd'
Shenghua Zhang6f830312018-02-26 11:45:07 -08002531
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002532 def GetExpandedPackageName(self):
2533 """Return the CIPD package name with the variables evaluated."""
2534 package = self._cipd_root.expand_package_name(self._package_name)
2535 if package:
2536 return package
2537 return self._package_name
John Budorick0f7b2002018-01-19 15:46:17 -08002538
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002539 #override
2540 def CreateSCM(self, out_cb=None):
2541 """Create a Wrapper instance suitable for handling this CIPD dependency."""
2542 self._CreatePackageIfNecessary()
2543 return gclient_scm.CipdWrapper(self.url,
2544 self.root.root_dir,
2545 self.name,
2546 self.outbuf,
2547 out_cb,
2548 root=self._cipd_root,
2549 package=self._cipd_package)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +00002550
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002551 def hierarchy(self, include_url=False, graphviz=False):
2552 if graphviz:
2553 return '' # graphviz lines not implemented for cipd deps.
2554 return self.parent.hierarchy(include_url) + ' -> ' + self._cipd_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08002555
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002556 def ToLines(self):
2557 # () -> Sequence[str]
2558 """Return a list of lines representing this in a DEPS file."""
2559 def escape_cipd_var(package):
2560 return package.replace('{', '{{').replace('}', '}}')
Edward Lemure4e15042018-06-28 18:07:00 +00002561
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002562 s = []
2563 self._CreatePackageIfNecessary()
2564 if self._cipd_package.authority_for_subdir:
2565 condition_part = ([' "condition": %r,' %
2566 self.condition] if self.condition else [])
2567 s.extend([
2568 ' # %s' % self.hierarchy(include_url=False),
2569 ' "%s": {' % (self.name.split(':')[0], ),
2570 ' "packages": [',
2571 ])
2572 for p in sorted(self._cipd_root.packages(self._cipd_subdir),
2573 key=lambda x: x.name):
2574 s.extend([
2575 ' {',
2576 ' "package": "%s",' % escape_cipd_var(p.name),
2577 ' "version": "%s",' % p.version,
2578 ' },',
2579 ])
John Budorickc35aba52018-06-28 20:57:03 +00002580
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002581 s.extend([
2582 ' ],',
2583 ' "dep_type": "cipd",',
2584 ] + condition_part + [
2585 ' },',
2586 '',
2587 ])
2588 return s
John Budorick0f7b2002018-01-19 15:46:17 -08002589
2590
maruel@chromium.org5ca27692010-05-26 19:32:41 +00002591#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002592
2593
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002594@subcommand.usage('[command] [args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002595@metrics.collector.collect_metrics('gclient recurse')
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002596def CMDrecurse(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002597 """Operates [command args ...] on all the dependencies.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002598
Arthur Milchior08cd5fe2022-07-28 20:38:47 +00002599 Change directory to each dependency's directory, and call [command
2600 args ...] there. Sets GCLIENT_DEP_PATH environment variable as the
2601 dep's relative location to root directory of the checkout.
2602
2603 Examples:
2604 * `gclient recurse --no-progress -j1 sh -c 'echo "$GCLIENT_DEP_PATH"'`
2605 print the relative path of each dependency.
2606 * `gclient recurse --no-progress -j1 sh -c "pwd"`
2607 print the absolute path of each dependency.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002608 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002609 # Stop parsing at the first non-arg so that these go through to the command
2610 parser.disable_interspersed_args()
2611 parser.add_option('-s',
2612 '--scm',
2613 action='append',
2614 default=[],
2615 help='Choose scm types to operate upon.')
2616 parser.add_option('-i',
2617 '--ignore',
2618 action='store_true',
2619 help='Ignore non-zero return codes from subcommands.')
2620 parser.add_option(
2621 '--prepend-dir',
2622 action='store_true',
2623 help='Prepend relative dir for use with git <cmd> --null.')
2624 parser.add_option(
2625 '--no-progress',
2626 action='store_true',
2627 help='Disable progress bar that shows sub-command updates')
2628 options, args = parser.parse_args(args)
2629 if not args:
2630 print('Need to supply a command!', file=sys.stderr)
2631 return 1
2632 root_and_entries = gclient_utils.GetGClientRootAndEntries()
2633 if not root_and_entries:
2634 print(
2635 'You need to run gclient sync at least once to use \'recurse\'.\n'
2636 'This is because .gclient_entries needs to exist and be up to date.',
2637 file=sys.stderr)
2638 return 1
davidbarr@chromium.org47ca0ee2012-03-02 16:06:11 +00002639
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002640 # Normalize options.scm to a set()
2641 scm_set = set()
2642 for scm in options.scm:
2643 scm_set.update(scm.split(','))
2644 options.scm = scm_set
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002645
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002646 options.nohooks = True
2647 client = GClient.LoadCurrentConfig(options)
2648 if not client:
2649 raise gclient_utils.Error(
2650 'client not configured; see \'gclient config\'')
2651 return client.RunOnDeps('recurse',
2652 args,
2653 ignore_requirements=True,
2654 progress=not options.no_progress)
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002655
2656
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002657@subcommand.usage('[args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002658@metrics.collector.collect_metrics('gclient fetch')
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002659def CMDfetch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002660 """Fetches upstream commits for all modules.
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002661
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002662 Completely git-specific. Simply runs 'git fetch [args ...]' for each module.
2663 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002664 (options, args) = parser.parse_args(args)
2665 return CMDrecurse(
2666 OptionParser(),
2667 ['--jobs=%d' % options.jobs, '--scm=git', 'git', 'fetch'] + args)
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00002668
2669
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002670class Flattener(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002671 """Flattens a gclient solution."""
2672 def __init__(self, client, pin_all_deps=False):
2673 """Constructor.
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002674
2675 Arguments:
2676 client (GClient): client to flatten
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002677 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2678 in DEPS
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002679 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002680 self._client = client
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002681
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002682 self._deps_string = None
2683 self._deps_graph_lines = None
2684 self._deps_files = set()
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002685
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002686 self._allowed_hosts = set()
2687 self._deps = {}
2688 self._hooks = []
2689 self._pre_deps_hooks = []
2690 self._vars = {}
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002691
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002692 self._flatten(pin_all_deps=pin_all_deps)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002693
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002694 @property
2695 def deps_string(self):
2696 assert self._deps_string is not None
2697 return self._deps_string
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002698
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002699 @property
2700 def deps_graph_lines(self):
2701 assert self._deps_graph_lines is not None
2702 return self._deps_graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00002703
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002704 @property
2705 def deps_files(self):
2706 return self._deps_files
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002707
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002708 def _pin_dep(self, dep):
2709 """Pins a dependency to specific full revision sha.
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002710
2711 Arguments:
2712 dep (Dependency): dependency to process
2713 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002714 if dep.url is None:
2715 return
Michael Mossd683d7c2018-06-15 05:05:17 +00002716
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002717 # Make sure the revision is always fully specified (a hash),
2718 # as opposed to refs or tags which might change. Similarly,
2719 # shortened shas might become ambiguous; make sure to always
2720 # use full one for pinning.
2721 revision = gclient_utils.SplitUrlRevision(dep.url)[1]
2722 if not revision or not gclient_utils.IsFullGitSha(revision):
2723 dep.PinToActualRevision()
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002724
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002725 def _flatten(self, pin_all_deps=False):
2726 """Runs the flattener. Saves resulting DEPS string.
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002727
2728 Arguments:
2729 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2730 in DEPS
2731 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002732 for solution in self._client.dependencies:
2733 self._add_dep(solution)
2734 self._flatten_dep(solution)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002735
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002736 if pin_all_deps:
2737 for dep in self._deps.values():
2738 self._pin_dep(dep)
Paweł Hajdan, Jr39300ba2017-08-11 16:52:38 +02002739
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002740 def add_deps_file(dep):
2741 # Only include DEPS files referenced by recursedeps.
2742 if not dep.should_recurse:
2743 return
2744 deps_file = dep.deps_file
2745 deps_path = os.path.join(self._client.root_dir, dep.name, deps_file)
2746 if not os.path.exists(deps_path):
2747 # gclient has a fallback that if deps_file doesn't exist, it'll
2748 # try DEPS. Do the same here.
2749 deps_file = 'DEPS'
2750 deps_path = os.path.join(self._client.root_dir, dep.name,
2751 deps_file)
2752 if not os.path.exists(deps_path):
2753 return
2754 assert dep.url
2755 self._deps_files.add((dep.url, deps_file, dep.hierarchy_data()))
Paweł Hajdan, Jr76a9d042017-08-18 20:05:41 +02002756
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002757 for dep in self._deps.values():
2758 add_deps_file(dep)
Joanna Wang9144b672023-02-24 23:36:17 +00002759
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002760 gn_args_dep = self._deps.get(self._client.dependencies[0]._gn_args_from,
2761 self._client.dependencies[0])
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002762
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002763 self._deps_graph_lines = _DepsToDotGraphLines(self._deps)
2764 self._deps_string = '\n'.join(
2765 _GNSettingsToLines(gn_args_dep._gn_args_file, gn_args_dep._gn_args)
2766 + _AllowedHostsToLines(self._allowed_hosts) +
2767 _DepsToLines(self._deps) + _HooksToLines('hooks', self._hooks) +
2768 _HooksToLines('pre_deps_hooks', self._pre_deps_hooks) +
2769 _VarsToLines(self._vars) + [
2770 '# %s, %s' % (url, deps_file)
2771 for url, deps_file, _ in sorted(self._deps_files)
2772 ] + ['']) # Ensure newline at end of file.
2773
2774 def _add_dep(self, dep):
2775 """Helper to add a dependency to flattened DEPS.
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002776
2777 Arguments:
2778 dep (Dependency): dependency to add
2779 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002780 assert dep.name not in self._deps or self._deps.get(
2781 dep.name) == dep, (dep.name, self._deps.get(dep.name))
2782 if dep.url:
2783 self._deps[dep.name] = dep
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002784
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002785 def _flatten_dep(self, dep):
2786 """Visits a dependency in order to flatten it (see CMDflatten).
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002787
2788 Arguments:
2789 dep (Dependency): dependency to process
2790 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002791 logging.debug('_flatten_dep(%s)', dep.name)
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002792
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002793 assert dep.deps_parsed, (
2794 "Attempted to flatten %s but it has not been processed." % dep.name)
Paweł Hajdan, Jrc69b32e2017-08-17 18:47:48 +02002795
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002796 self._allowed_hosts.update(dep.allowed_hosts)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002797
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002798 # Only include vars explicitly listed in the DEPS files or gclient
2799 # solution, not automatic, local overrides (i.e. not all of
2800 # dep.get_vars()).
2801 hierarchy = dep.hierarchy(include_url=False)
2802 for key, value in dep._vars.items():
2803 # Make sure there are no conflicting variables. It is fine however
2804 # to use same variable name, as long as the value is consistent.
2805 assert key not in self._vars or self._vars[key][1] == value, (
2806 "dep:%s key:%s value:%s != %s" %
2807 (dep.name, key, value, self._vars[key][1]))
2808 self._vars[key] = (hierarchy, value)
2809 # Override explicit custom variables.
2810 for key, value in dep.custom_vars.items():
2811 # Do custom_vars that don't correspond to DEPS vars ever make sense?
2812 # DEPS conditionals shouldn't be using vars that aren't also defined
2813 # in the DEPS (presubmit actually disallows this), so any new
2814 # custom_var must be unused in the DEPS, so no need to add it to the
2815 # flattened output either.
2816 if key not in self._vars:
2817 continue
2818 # Don't "override" existing vars if it's actually the same value.
2819 if self._vars[key][1] == value:
2820 continue
2821 # Anything else is overriding a default value from the DEPS.
2822 self._vars[key] = (hierarchy + ' [custom_var override]', value)
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02002823
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002824 self._pre_deps_hooks.extend([(dep, hook)
2825 for hook in dep.pre_deps_hooks])
2826 self._hooks.extend([(dep, hook) for hook in dep.deps_hooks])
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002827
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002828 for sub_dep in dep.dependencies:
2829 self._add_dep(sub_dep)
Paweł Hajdan, Jrb0ad16e2017-08-03 15:33:21 +02002830
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002831 for d in dep.dependencies:
2832 if d.should_recurse:
2833 self._flatten_dep(d)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002834
2835
Joanna Wang3ab2f212023-08-09 01:25:15 +00002836@metrics.collector.collect_metrics('gclient gitmodules')
2837def CMDgitmodules(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002838 """Adds or updates Git Submodules based on the contents of the DEPS file.
Joanna Wang3ab2f212023-08-09 01:25:15 +00002839
2840 This command should be run in the root director of the repo.
2841 It will create or update the .gitmodules file and include
2842 `gclient-condition` values. Commits in gitlinks will also be updated.
2843 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002844 parser.add_option('--output-gitmodules',
2845 help='name of the .gitmodules file to write to',
2846 default='.gitmodules')
2847 parser.add_option(
2848 '--deps-file',
2849 help=
2850 'name of the deps file to parse for git dependency paths and commits.',
2851 default='DEPS')
2852 parser.add_option(
2853 '--skip-dep',
2854 action="append",
2855 help='skip adding gitmodules for the git dependency at the given path',
2856 default=[])
2857 options, args = parser.parse_args(args)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002858
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002859 deps_dir = os.path.dirname(os.path.abspath(options.deps_file))
2860 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
2861 if not gclient_path:
2862 logging.error(
2863 '.gclient not found\n'
2864 'Make sure you are running this script from a gclient workspace.')
2865 sys.exit(1)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002866
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002867 deps_content = gclient_utils.FileRead(options.deps_file)
2868 ls = gclient_eval.Parse(deps_content, options.deps_file, None, None)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002869
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002870 prefix_length = 0
2871 if not 'use_relative_paths' in ls or ls['use_relative_paths'] != True:
2872 delta_path = os.path.relpath(deps_dir, os.path.abspath(gclient_path))
2873 if delta_path:
2874 prefix_length = len(delta_path.replace(os.path.sep, '/')) + 1
Joanna Wang3ab2f212023-08-09 01:25:15 +00002875
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002876 cache_info = []
2877 with open(options.output_gitmodules, 'w', newline='') as f:
2878 for path, dep in ls.get('deps').items():
2879 if path in options.skip_dep:
2880 continue
2881 if dep.get('dep_type') == 'cipd':
2882 continue
2883 try:
2884 url, commit = dep['url'].split('@', maxsplit=1)
2885 except ValueError:
2886 logging.error('error on %s; %s, not adding it', path,
2887 dep["url"])
2888 continue
2889 if prefix_length:
2890 path = path[prefix_length:]
Joanna Wang3ab2f212023-08-09 01:25:15 +00002891
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002892 cache_info += ['--cacheinfo', f'160000,{commit},{path}']
2893 f.write(f'[submodule "{path}"]\n\tpath = {path}\n\turl = {url}\n')
2894 if 'condition' in dep:
2895 f.write(f'\tgclient-condition = {dep["condition"]}\n')
2896 # Windows has limit how long, so let's chunk those calls.
2897 if len(cache_info) >= 100:
2898 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
2899 cache_info = []
2900
2901 if cache_info:
Josip Sokcevic293aa652023-08-23 18:55:20 +00002902 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002903 subprocess2.call(['git', 'add', '.gitmodules'])
2904 print('.gitmodules and gitlinks updated. Please check git diff and '
2905 'commit changes.')
Joanna Wang3ab2f212023-08-09 01:25:15 +00002906
2907
Edward Lemur3298e7b2018-07-17 18:21:27 +00002908@metrics.collector.collect_metrics('gclient flatten')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002909def CMDflatten(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002910 """Flattens the solutions into a single DEPS file."""
2911 parser.add_option('--output-deps', help='Path to the output DEPS file')
2912 parser.add_option(
2913 '--output-deps-files',
2914 help=('Path to the output metadata about DEPS files referenced by '
2915 'recursedeps.'))
2916 parser.add_option(
2917 '--pin-all-deps',
2918 action='store_true',
2919 help=('Pin all deps, even if not pinned in DEPS. CAVEAT: only does so '
2920 'for checked out deps, NOT deps_os.'))
2921 parser.add_option('--deps-graph-file',
2922 help='Provide a path for the output graph file')
2923 options, args = parser.parse_args(args)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002924
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002925 options.nohooks = True
2926 options.process_all_deps = True
2927 client = GClient.LoadCurrentConfig(options)
2928 if not client:
2929 raise gclient_utils.Error(
2930 'client not configured; see \'gclient config\'')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002931
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002932 # Only print progress if we're writing to a file. Otherwise, progress
2933 # updates could obscure intended output.
2934 code = client.RunOnDeps('flatten', args, progress=options.output_deps)
2935 if code != 0:
2936 return code
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002937
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002938 flattener = Flattener(client, pin_all_deps=options.pin_all_deps)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002939
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002940 if options.output_deps:
2941 with open(options.output_deps, 'w') as f:
2942 f.write(flattener.deps_string)
2943 else:
2944 print(flattener.deps_string)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002945
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002946 if options.deps_graph_file:
2947 with open(options.deps_graph_file, 'w') as f:
2948 f.write('\n'.join(flattener.deps_graph_lines))
Joanna Wang9144b672023-02-24 23:36:17 +00002949
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002950 deps_files = [{
2951 'url': d[0],
2952 'deps_file': d[1],
2953 'hierarchy': d[2]
2954 } for d in sorted(flattener.deps_files)]
2955 if options.output_deps_files:
2956 with open(options.output_deps_files, 'w') as f:
2957 json.dump(deps_files, f)
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002958
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002959 return 0
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002960
2961
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002962def _GNSettingsToLines(gn_args_file, gn_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002963 s = []
2964 if gn_args_file:
2965 s.extend([
2966 'gclient_gn_args_file = "%s"' % gn_args_file,
2967 'gclient_gn_args = %r' % gn_args,
2968 ])
2969 return s
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002970
2971
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02002972def _AllowedHostsToLines(allowed_hosts):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002973 """Converts |allowed_hosts| set to list of lines for output."""
2974 if not allowed_hosts:
2975 return []
2976 s = ['allowed_hosts = [']
2977 for h in sorted(allowed_hosts):
2978 s.append(' "%s",' % h)
2979 s.extend([']', ''])
2980 return s
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02002981
2982
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002983def _DepsToLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002984 # type: (Mapping[str, Dependency]) -> Sequence[str]
2985 """Converts |deps| dict to list of lines for output."""
2986 if not deps:
2987 return []
2988 s = ['deps = {']
2989 for _, dep in sorted(deps.items()):
2990 s.extend(dep.ToLines())
2991 s.extend(['}', ''])
2992 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002993
2994
Joanna Wang9144b672023-02-24 23:36:17 +00002995def _DepsToDotGraphLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002996 # type: (Mapping[str, Dependency]) -> Sequence[str]
2997 """Converts |deps| dict to list of lines for dot graphs"""
2998 if not deps:
2999 return []
3000 graph_lines = ["digraph {\n\trankdir=\"LR\";"]
3001 for _, dep in sorted(deps.items()):
3002 line = dep.hierarchy(include_url=False, graphviz=True)
3003 if line:
3004 graph_lines.append("\t%s" % line)
3005 graph_lines.append("}")
3006 return graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00003007
3008
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02003009def _DepsOsToLines(deps_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003010 """Converts |deps_os| dict to list of lines for output."""
3011 if not deps_os:
3012 return []
3013 s = ['deps_os = {']
3014 for dep_os, os_deps in sorted(deps_os.items()):
3015 s.append(' "%s": {' % dep_os)
3016 for name, dep in sorted(os_deps.items()):
3017 condition_part = ([' "condition": %r,' %
3018 dep.condition] if dep.condition else [])
3019 s.extend([
3020 ' # %s' % dep.hierarchy(include_url=False),
3021 ' "%s": {' % (name, ),
3022 ' "url": "%s",' % (dep.url, ),
3023 ] + condition_part + [
3024 ' },',
3025 '',
3026 ])
3027 s.extend([' },', ''])
3028 s.extend(['}', ''])
3029 return s
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02003030
3031
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003032def _HooksToLines(name, hooks):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003033 """Converts |hooks| list to list of lines for output."""
3034 if not hooks:
3035 return []
3036 s = ['%s = [' % name]
3037 for dep, hook in hooks:
3038 s.extend([
3039 ' # %s' % dep.hierarchy(include_url=False),
3040 ' {',
3041 ])
3042 if hook.name is not None:
3043 s.append(' "name": "%s",' % hook.name)
3044 if hook.pattern is not None:
3045 s.append(' "pattern": "%s",' % hook.pattern)
3046 if hook.condition is not None:
3047 s.append(' "condition": %r,' % hook.condition)
3048 # Flattened hooks need to be written relative to the root gclient dir
3049 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3050 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3051 [' "%s",' % arg
3052 for arg in hook.action] + [' ]', ' },', ''])
3053 s.extend([']', ''])
3054 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003055
3056
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003057def _HooksOsToLines(hooks_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003058 """Converts |hooks| list to list of lines for output."""
3059 if not hooks_os:
3060 return []
3061 s = ['hooks_os = {']
3062 for hook_os, os_hooks in hooks_os.items():
3063 s.append(' "%s": [' % hook_os)
3064 for dep, hook in os_hooks:
3065 s.extend([
3066 ' # %s' % dep.hierarchy(include_url=False),
3067 ' {',
3068 ])
3069 if hook.name is not None:
3070 s.append(' "name": "%s",' % hook.name)
3071 if hook.pattern is not None:
3072 s.append(' "pattern": "%s",' % hook.pattern)
3073 if hook.condition is not None:
3074 s.append(' "condition": %r,' % hook.condition)
3075 # Flattened hooks need to be written relative to the root gclient
3076 # dir
3077 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3078 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3079 [' "%s",' % arg
3080 for arg in hook.action] + [' ]', ' },', ''])
3081 s.extend([' ],', ''])
3082 s.extend(['}', ''])
3083 return s
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003084
3085
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003086def _VarsToLines(variables):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003087 """Converts |variables| dict to list of lines for output."""
3088 if not variables:
3089 return []
3090 s = ['vars = {']
3091 for key, tup in sorted(variables.items()):
3092 hierarchy, value = tup
3093 s.extend([
3094 ' # %s' % hierarchy,
3095 ' "%s": %r,' % (key, value),
3096 '',
3097 ])
3098 s.extend(['}', ''])
3099 return s
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003100
3101
Edward Lemur3298e7b2018-07-17 18:21:27 +00003102@metrics.collector.collect_metrics('gclient grep')
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003103def CMDgrep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003104 """Greps through git repos managed by gclient.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003105
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003106 Runs 'git grep [args...]' for each module.
3107 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003108 # We can't use optparse because it will try to parse arguments sent
3109 # to git grep and throw an error. :-(
3110 if not args or re.match('(-h|--help)$', args[0]):
3111 print(
3112 'Usage: gclient grep [-j <N>] git-grep-args...\n\n'
3113 'Example: "gclient grep -j10 -A2 RefCountedBase" runs\n"git grep '
3114 '-A2 RefCountedBase" on each of gclient\'s git\nrepos with up to '
3115 '10 jobs.\n\nBonus: page output by appending "|& less -FRSX" to the'
3116 ' end of your query.',
3117 file=sys.stderr)
3118 return 1
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003119
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003120 jobs_arg = ['--jobs=1']
3121 if re.match(r'(-j|--jobs=)\d+$', args[0]):
3122 jobs_arg, args = args[:1], args[1:]
3123 elif re.match(r'(-j|--jobs)$', args[0]):
3124 jobs_arg, args = args[:2], args[2:]
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003125
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003126 return CMDrecurse(
3127 parser, jobs_arg + [
3128 '--ignore', '--prepend-dir', '--no-progress', '--scm=git', 'git',
3129 'grep', '--null', '--color=Always'
3130 ] + args)
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00003131
3132
Edward Lemur3298e7b2018-07-17 18:21:27 +00003133@metrics.collector.collect_metrics('gclient root')
stip@chromium.orga735da22015-04-29 23:18:20 +00003134def CMDroot(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003135 """Outputs the solution root (or current dir if there isn't one)."""
3136 (options, args) = parser.parse_args(args)
3137 client = GClient.LoadCurrentConfig(options)
3138 if client:
3139 print(os.path.abspath(client.root_dir))
3140 else:
3141 print(os.path.abspath('.'))
stip@chromium.orga735da22015-04-29 23:18:20 +00003142
3143
agablea98a6cd2016-11-15 14:30:10 -08003144@subcommand.usage('[url]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00003145@metrics.collector.collect_metrics('gclient config')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003146def CMDconfig(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003147 """Creates a .gclient file in the current directory.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003148
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003149 This specifies the configuration for further commands. After update/sync,
3150 top-level DEPS files in each module are read to determine dependent
3151 modules to operate on as well. If optional [url] parameter is
3152 provided, then configuration is read from a specified Subversion server
3153 URL.
3154 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003155 # We do a little dance with the --gclientfile option. 'gclient config' is
3156 # the only command where it's acceptable to have both '--gclientfile' and
3157 # '--spec' arguments. So, we temporarily stash any --gclientfile parameter
3158 # into options.output_config_file until after the (gclientfile xor spec)
3159 # error check.
3160 parser.remove_option('--gclientfile')
3161 parser.add_option('--gclientfile',
3162 dest='output_config_file',
3163 help='Specify an alternate .gclient file')
3164 parser.add_option('--name',
3165 help='overrides the default name for the solution')
3166 parser.add_option(
3167 '--deps-file',
3168 default='DEPS',
3169 help='overrides the default name for the DEPS file for the '
3170 'main solutions and all sub-dependencies')
3171 parser.add_option('--unmanaged',
3172 action='store_true',
3173 default=False,
3174 help='overrides the default behavior to make it possible '
3175 'to have the main solution untouched by gclient '
3176 '(gclient will check out unmanaged dependencies but '
3177 'will never sync them)')
3178 parser.add_option('--cache-dir',
3179 default=UNSET_CACHE_DIR,
3180 help='Cache all git repos into this dir and do shared '
3181 'clones from the cache, instead of cloning directly '
3182 'from the remote. Pass "None" to disable cache, even '
3183 'if globally enabled due to $GIT_CACHE_PATH.')
3184 parser.add_option('--custom-var',
3185 action='append',
3186 dest='custom_vars',
3187 default=[],
3188 help='overrides variables; key=value syntax')
3189 parser.set_defaults(config_filename=None)
3190 (options, args) = parser.parse_args(args)
3191 if options.output_config_file:
3192 setattr(options, 'config_filename',
3193 getattr(options, 'output_config_file'))
3194 if ((options.spec and args) or len(args) > 2
3195 or (not options.spec and not args)):
3196 parser.error(
3197 'Inconsistent arguments. Use either --spec or one or 2 args')
maruel@chromium.org5fc2a332010-05-26 19:37:15 +00003198
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003199 if (options.cache_dir is not UNSET_CACHE_DIR
3200 and options.cache_dir.lower() == 'none'):
3201 options.cache_dir = None
Robert Iannuccia19649b2018-06-29 16:31:45 +00003202
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003203 custom_vars = {}
3204 for arg in options.custom_vars:
3205 kv = arg.split('=', 1)
3206 if len(kv) != 2:
3207 parser.error('Invalid --custom-var argument: %r' % arg)
3208 custom_vars[kv[0]] = gclient_eval.EvaluateCondition(kv[1], {})
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02003209
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003210 client = GClient('.', options)
3211 if options.spec:
3212 client.SetConfig(options.spec)
iposva@chromium.org8cf7a392010-04-07 17:20:26 +00003213 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003214 base_url = args[0].rstrip('/')
3215 if not options.name:
3216 name = base_url.split('/')[-1]
3217 if name.endswith('.git'):
3218 name = name[:-4]
3219 else:
3220 # specify an alternate relpath for the given URL.
3221 name = options.name
3222 if not os.path.abspath(os.path.join(os.getcwd(), name)).startswith(
3223 os.getcwd()):
3224 parser.error('Do not pass a relative path for --name.')
3225 if any(x in ('..', '.', '/', '\\') for x in name.split(os.sep)):
3226 parser.error(
3227 'Do not include relative path components in --name.')
agable@chromium.orgf2214672015-10-27 21:02:48 +00003228
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003229 deps_file = options.deps_file
3230 client.SetDefaultConfig(name,
3231 deps_file,
3232 base_url,
3233 managed=not options.unmanaged,
3234 cache_dir=options.cache_dir,
3235 custom_vars=custom_vars)
3236 client.SaveConfig()
3237 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003238
3239
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003240@subcommand.epilog("""Example:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003241 gclient pack > patch.txt
3242 generate simple patch for configured client and dependences
3243""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003244@metrics.collector.collect_metrics('gclient pack')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003245def CMDpack(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003246 """Generates a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003247
agabled437d762016-10-17 09:35:11 -07003248 Internally, runs 'git diff' on each checked out module and
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003249 dependencies, and performs minimal postprocessing of the output. The
3250 resulting patch is printed to stdout and can be applied to a freshly
3251 checked out tree via 'patch -p0 < patchfile'.
3252 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003253 parser.add_option('--deps',
3254 dest='deps_os',
3255 metavar='OS_LIST',
3256 help='override deps for the specified (comma-separated) '
3257 'platform(s); \'all\' will process all deps_os '
3258 'references')
3259 parser.remove_option('--jobs')
3260 (options, args) = parser.parse_args(args)
3261 # Force jobs to 1 so the stdout is not annotated with the thread ids
3262 options.jobs = 1
3263 client = GClient.LoadCurrentConfig(options)
3264 if not client:
3265 raise gclient_utils.Error(
3266 'client not configured; see \'gclient config\'')
3267 if options.verbose:
3268 client.PrintLocationAndContents()
3269 return client.RunOnDeps('pack', args)
kbr@google.comab318592009-09-04 00:54:55 +00003270
3271
Edward Lemur3298e7b2018-07-17 18:21:27 +00003272@metrics.collector.collect_metrics('gclient status')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003273def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003274 """Shows modification status for every dependencies."""
3275 parser.add_option('--deps',
3276 dest='deps_os',
3277 metavar='OS_LIST',
3278 help='override deps for the specified (comma-separated) '
3279 'platform(s); \'all\' will process all deps_os '
3280 'references')
3281 (options, args) = parser.parse_args(args)
3282 client = GClient.LoadCurrentConfig(options)
3283 if not client:
3284 raise gclient_utils.Error(
3285 'client not configured; see \'gclient config\'')
3286 if options.verbose:
3287 client.PrintLocationAndContents()
3288 return client.RunOnDeps('status', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003289
3290
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003291@subcommand.epilog("""Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +00003292 gclient sync
3293 update files from SCM according to current configuration,
3294 *for modules which have changed since last update or sync*
3295 gclient sync --force
3296 update files from SCM according to current configuration, for
3297 all modules (useful for recovering files deleted from local copy)
Edward Lesmes3ffca4b2021-05-19 19:36:17 +00003298 gclient sync --revision src@GIT_COMMIT_OR_REF
3299 update src directory to GIT_COMMIT_OR_REF
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003300
3301JSON output format:
3302If the --output-json option is specified, the following document structure will
3303be emitted to the provided file. 'null' entries may occur for subprojects which
3304are present in the gclient solution, but were not processed (due to custom_deps,
3305os_deps, etc.)
3306
3307{
3308 "solutions" : {
3309 "<name>": { # <name> is the posix-normalized path to the solution.
agabled437d762016-10-17 09:35:11 -07003310 "revision": [<git id hex string>|null],
3311 "scm": ["git"|null],
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003312 }
3313 }
3314}
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003315""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003316@metrics.collector.collect_metrics('gclient sync')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003317def CMDsync(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003318 """Checkout/update all modules."""
3319 parser.add_option('-f',
3320 '--force',
3321 action='store_true',
3322 help='force update even for unchanged modules')
3323 parser.add_option('-n',
3324 '--nohooks',
3325 action='store_true',
3326 help='don\'t run hooks after the update is complete')
3327 parser.add_option('-p',
3328 '--noprehooks',
3329 action='store_true',
3330 help='don\'t run pre-DEPS hooks',
3331 default=False)
3332 parser.add_option('-r',
3333 '--revision',
3334 action='append',
3335 dest='revisions',
3336 metavar='REV',
3337 default=[],
3338 help='Enforces git ref/hash for the solutions with the '
3339 'format src@rev. The src@ part is optional and can be '
3340 'skipped. You can also specify URLs instead of paths '
3341 'and gclient will find the solution corresponding to '
3342 'the given URL. If a path is also specified, the URL '
3343 'takes precedence. -r can be used multiple times when '
3344 '.gclient has multiple solutions configured, and will '
3345 'work even if the src@ part is skipped. Revision '
3346 'numbers (e.g. 31000 or r31000) are not supported.')
3347 parser.add_option('--patch-ref',
3348 action='append',
3349 dest='patch_refs',
3350 metavar='GERRIT_REF',
3351 default=[],
3352 help='Patches the given reference with the format '
3353 'dep@target-ref:patch-ref. '
3354 'For |dep|, you can specify URLs as well as paths, '
3355 'with URLs taking preference. '
3356 '|patch-ref| will be applied to |dep|, rebased on top '
3357 'of what |dep| was synced to, and a soft reset will '
3358 'be done. Use --no-rebase-patch-ref and '
3359 '--no-reset-patch-ref to disable this behavior. '
3360 '|target-ref| is the target branch against which a '
3361 'patch was created, it is used to determine which '
3362 'commits from the |patch-ref| actually constitute a '
3363 'patch.')
3364 parser.add_option(
3365 '-t',
3366 '--download-topics',
3367 action='store_true',
3368 help='Downloads and patches locally changes from all open '
3369 'Gerrit CLs that have the same topic as the changes '
3370 'in the specified patch_refs. Only works if atleast '
3371 'one --patch-ref is specified.')
3372 parser.add_option('--with_branch_heads',
3373 action='store_true',
3374 help='Clone git "branch_heads" refspecs in addition to '
3375 'the default refspecs. This adds about 1/2GB to a '
3376 'full checkout. (git only)')
3377 parser.add_option(
3378 '--with_tags',
3379 action='store_true',
3380 help='Clone git tags in addition to the default refspecs.')
3381 parser.add_option('-H',
3382 '--head',
3383 action='store_true',
3384 help='DEPRECATED: only made sense with safesync urls.')
3385 parser.add_option(
3386 '-D',
3387 '--delete_unversioned_trees',
3388 action='store_true',
3389 help='Deletes from the working copy any dependencies that '
3390 'have been removed since the last sync, as long as '
3391 'there are no local modifications. When used with '
3392 '--force, such dependencies are removed even if they '
3393 'have local modifications. When used with --reset, '
3394 'all untracked directories are removed from the '
3395 'working copy, excluding those which are explicitly '
3396 'ignored in the repository.')
3397 parser.add_option(
3398 '-R',
3399 '--reset',
3400 action='store_true',
3401 help='resets any local changes before updating (git only)')
3402 parser.add_option('-M',
3403 '--merge',
3404 action='store_true',
3405 help='merge upstream changes instead of trying to '
3406 'fast-forward or rebase')
3407 parser.add_option('-A',
3408 '--auto_rebase',
3409 action='store_true',
3410 help='Automatically rebase repositories against local '
3411 'checkout during update (git only).')
3412 parser.add_option('--deps',
3413 dest='deps_os',
3414 metavar='OS_LIST',
3415 help='override deps for the specified (comma-separated) '
3416 'platform(s); \'all\' will process all deps_os '
3417 'references')
3418 parser.add_option('--process-all-deps',
3419 action='store_true',
3420 help='Check out all deps, even for different OS-es, '
3421 'or with conditions evaluating to false')
3422 parser.add_option('--upstream',
3423 action='store_true',
3424 help='Make repo state match upstream branch.')
3425 parser.add_option('--output-json',
3426 help='Output a json document to this path containing '
3427 'summary information about the sync.')
3428 parser.add_option(
3429 '--no-history',
3430 action='store_true',
3431 help='GIT ONLY - Reduces the size/time of the checkout at '
3432 'the cost of no history. Requires Git 1.9+')
3433 parser.add_option('--shallow',
3434 action='store_true',
3435 help='GIT ONLY - Do a shallow clone into the cache dir. '
3436 'Requires Git 1.9+')
3437 parser.add_option('--no_bootstrap',
3438 '--no-bootstrap',
3439 action='store_true',
3440 help='Don\'t bootstrap from Google Storage.')
3441 parser.add_option('--ignore_locks',
3442 action='store_true',
3443 help='No longer used.')
3444 parser.add_option('--break_repo_locks',
3445 action='store_true',
3446 help='No longer used.')
3447 parser.add_option('--lock_timeout',
3448 type='int',
3449 default=5000,
3450 help='GIT ONLY - Deadline (in seconds) to wait for git '
3451 'cache lock to become available. Default is %default.')
3452 parser.add_option('--no-rebase-patch-ref',
3453 action='store_false',
3454 dest='rebase_patch_ref',
3455 default=True,
3456 help='Bypass rebase of the patch ref after checkout.')
3457 parser.add_option('--no-reset-patch-ref',
3458 action='store_false',
3459 dest='reset_patch_ref',
3460 default=True,
3461 help='Bypass calling reset after patching the ref.')
3462 parser.add_option('--experiment',
3463 action='append',
3464 dest='experiments',
3465 default=[],
3466 help='Which experiments should be enabled.')
3467 (options, args) = parser.parse_args(args)
3468 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003469
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003470 if not client:
3471 raise gclient_utils.Error(
3472 'client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003473
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003474 if options.download_topics and not options.rebase_patch_ref:
3475 raise gclient_utils.Error(
3476 'Warning: You cannot download topics and not rebase each patch ref')
Ravi Mistryecda7822022-02-28 16:22:20 +00003477
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003478 if options.ignore_locks:
3479 print(
3480 'Warning: ignore_locks is no longer used. Please remove its usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003481
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003482 if options.break_repo_locks:
3483 print('Warning: break_repo_locks is no longer used. Please remove its '
3484 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003485
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003486 if options.revisions and options.head:
3487 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
3488 print('Warning: you cannot use both --head and --revision')
smutae7ea312016-07-18 11:59:41 -07003489
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003490 if options.verbose:
3491 client.PrintLocationAndContents()
3492 ret = client.RunOnDeps('update', args)
3493 if options.output_json:
3494 slns = {}
3495 for d in client.subtree(True):
3496 normed = d.name.replace('\\', '/').rstrip('/') + '/'
3497 slns[normed] = {
3498 'revision': d.got_revision,
3499 'scm': d.used_scm.name if d.used_scm else None,
3500 'url': str(d.url) if d.url else None,
3501 'was_processed': d.should_process,
3502 'was_synced': d._should_sync,
3503 }
3504 with open(options.output_json, 'w') as f:
3505 json.dump({'solutions': slns}, f)
3506 return ret
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003507
3508
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003509CMDupdate = CMDsync
3510
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003511
Edward Lemur3298e7b2018-07-17 18:21:27 +00003512@metrics.collector.collect_metrics('gclient validate')
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003513def CMDvalidate(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003514 """Validates the .gclient and DEPS syntax."""
3515 options, args = parser.parse_args(args)
3516 client = GClient.LoadCurrentConfig(options)
3517 if not client:
3518 raise gclient_utils.Error(
3519 'client not configured; see \'gclient config\'')
3520 rv = client.RunOnDeps('validate', args)
3521 if rv == 0:
3522 print('validate: SUCCESS')
3523 else:
3524 print('validate: FAILURE')
3525 return rv
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003526
3527
Edward Lemur3298e7b2018-07-17 18:21:27 +00003528@metrics.collector.collect_metrics('gclient diff')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003529def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003530 """Displays local diff for every dependencies."""
3531 parser.add_option('--deps',
3532 dest='deps_os',
3533 metavar='OS_LIST',
3534 help='override deps for the specified (comma-separated) '
3535 'platform(s); \'all\' will process all deps_os '
3536 'references')
3537 (options, args) = parser.parse_args(args)
3538 client = GClient.LoadCurrentConfig(options)
3539 if not client:
3540 raise gclient_utils.Error(
3541 'client not configured; see \'gclient config\'')
3542 if options.verbose:
3543 client.PrintLocationAndContents()
3544 return client.RunOnDeps('diff', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003545
3546
Edward Lemur3298e7b2018-07-17 18:21:27 +00003547@metrics.collector.collect_metrics('gclient revert')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003548def CMDrevert(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003549 """Reverts all modifications in every dependencies.
maruel@chromium.org28d14bd2010-11-11 20:37:09 +00003550
3551 That's the nuclear option to get back to a 'clean' state. It removes anything
agabled437d762016-10-17 09:35:11 -07003552 that shows up in git status."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003553 parser.add_option('--deps',
3554 dest='deps_os',
3555 metavar='OS_LIST',
3556 help='override deps for the specified (comma-separated) '
3557 'platform(s); \'all\' will process all deps_os '
3558 'references')
3559 parser.add_option('-n',
3560 '--nohooks',
3561 action='store_true',
3562 help='don\'t run hooks after the revert is complete')
3563 parser.add_option('-p',
3564 '--noprehooks',
3565 action='store_true',
3566 help='don\'t run pre-DEPS hooks',
3567 default=False)
3568 parser.add_option('--upstream',
3569 action='store_true',
3570 help='Make repo state match upstream branch.')
3571 parser.add_option('--break_repo_locks',
3572 action='store_true',
3573 help='No longer used.')
3574 (options, args) = parser.parse_args(args)
3575 if options.break_repo_locks:
3576 print(
3577 'Warning: break_repo_locks is no longer used. Please remove its ' +
3578 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003579
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003580 # --force is implied.
3581 options.force = True
3582 options.reset = False
3583 options.delete_unversioned_trees = False
3584 options.merge = False
3585 client = GClient.LoadCurrentConfig(options)
3586 if not client:
3587 raise gclient_utils.Error(
3588 'client not configured; see \'gclient config\'')
3589 return client.RunOnDeps('revert', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003590
3591
Edward Lemur3298e7b2018-07-17 18:21:27 +00003592@metrics.collector.collect_metrics('gclient runhooks')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003593def CMDrunhooks(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003594 """Runs hooks for files that have been modified in the local working copy."""
3595 parser.add_option('--deps',
3596 dest='deps_os',
3597 metavar='OS_LIST',
3598 help='override deps for the specified (comma-separated) '
3599 'platform(s); \'all\' will process all deps_os '
3600 'references')
3601 parser.add_option('-f',
3602 '--force',
3603 action='store_true',
3604 default=True,
3605 help='Deprecated. No effect.')
3606 (options, args) = parser.parse_args(args)
3607 client = GClient.LoadCurrentConfig(options)
3608 if not client:
3609 raise gclient_utils.Error(
3610 'client not configured; see \'gclient config\'')
3611 if options.verbose:
3612 client.PrintLocationAndContents()
3613 options.force = True
3614 options.nohooks = False
3615 return client.RunOnDeps('runhooks', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003616
3617
Gavin Mak50b27a52023-09-19 22:44:59 +00003618# TODO(crbug.com/1481266): Collect merics for installhooks.
3619def CMDinstallhooks(parser, args):
3620 """Installs gclient git hooks.
3621
3622 Currently only installs a pre-commit hook to drop staged gitlinks. To
3623 bypass this pre-commit hook once it's installed, set the environment
3624 variable SKIP_GITLINK_PRECOMMIT=1.
3625 """
3626 (options, args) = parser.parse_args(args)
3627 client = GClient.LoadCurrentConfig(options)
3628 if not client:
3629 raise gclient_utils.Error(
3630 'client not configured; see \'gclient config\'')
3631 client._InstallPreCommitHook()
3632 return 0
3633
3634
Edward Lemur3298e7b2018-07-17 18:21:27 +00003635@metrics.collector.collect_metrics('gclient revinfo')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003636def CMDrevinfo(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003637 """Outputs revision info mapping for the client and its dependencies.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003638
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00003639 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003640 can be used to reproduce the same tree in the future. It is only useful for
agabled437d762016-10-17 09:35:11 -07003641 'unpinned dependencies', i.e. DEPS/deps references without a git hash.
3642 A git branch name isn't 'pinned' since the actual commit can change.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003643 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003644 parser.add_option('--deps',
3645 dest='deps_os',
3646 metavar='OS_LIST',
3647 help='override deps for the specified (comma-separated) '
3648 'platform(s); \'all\' will process all deps_os '
3649 'references')
3650 parser.add_option(
3651 '-a',
3652 '--actual',
3653 action='store_true',
3654 help='gets the actual checked out revisions instead of the '
3655 'ones specified in the DEPS and .gclient files')
3656 parser.add_option('-s',
3657 '--snapshot',
3658 action='store_true',
3659 help='creates a snapshot .gclient file of the current '
3660 'version of all repositories to reproduce the tree, '
3661 'implies -a')
3662 parser.add_option(
3663 '--filter',
3664 action='append',
3665 dest='filter',
3666 help='Display revision information only for the specified '
3667 'dependencies (filtered by URL or path).')
3668 parser.add_option('--output-json',
3669 help='Output a json document to this path containing '
3670 'information about the revisions.')
3671 parser.add_option(
3672 '--ignore-dep-type',
3673 choices=['git', 'cipd'],
3674 help='Specify to skip processing of a certain type of dep.')
3675 (options, args) = parser.parse_args(args)
3676 client = GClient.LoadCurrentConfig(options)
3677 if not client:
3678 raise gclient_utils.Error(
3679 'client not configured; see \'gclient config\'')
3680 client.PrintRevInfo()
3681 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003682
3683
Edward Lemur3298e7b2018-07-17 18:21:27 +00003684@metrics.collector.collect_metrics('gclient getdep')
Edward Lesmes411041f2018-04-05 20:12:55 -04003685def CMDgetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003686 """Gets revision information and variable values from a DEPS file.
Josip Sokcevic7b5e3d72023-06-13 00:28:23 +00003687
3688 If key doesn't exist or is incorrectly declared, this script exits with exit
3689 code 2."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003690 parser.add_option('--var',
3691 action='append',
3692 dest='vars',
3693 metavar='VAR',
3694 default=[],
3695 help='Gets the value of a given variable.')
3696 parser.add_option(
3697 '-r',
3698 '--revision',
3699 action='append',
3700 dest='getdep_revisions',
3701 metavar='DEP',
3702 default=[],
3703 help='Gets the revision/version for the given dependency. '
3704 'If it is a git dependency, dep must be a path. If it '
3705 'is a CIPD dependency, dep must be of the form '
3706 'path:package.')
3707 parser.add_option(
3708 '--deps-file',
3709 default='DEPS',
3710 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3711 # .gclient first.
3712 help='The DEPS file to be edited. Defaults to the DEPS '
3713 'file in the current directory.')
3714 (options, args) = parser.parse_args(args)
Edward Lesmes411041f2018-04-05 20:12:55 -04003715
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003716 if not os.path.isfile(options.deps_file):
3717 raise gclient_utils.Error('DEPS file %s does not exist.' %
3718 options.deps_file)
3719 with open(options.deps_file) as f:
3720 contents = f.read()
3721 client = GClient.LoadCurrentConfig(options)
3722 if client is not None:
3723 builtin_vars = client.get_builtin_vars()
Edward Lesmes411041f2018-04-05 20:12:55 -04003724 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003725 logging.warning(
3726 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3727 'file without support for built-in variables.')
3728 builtin_vars = None
3729 local_scope = gclient_eval.Exec(contents,
3730 options.deps_file,
3731 builtin_vars=builtin_vars)
3732
3733 for var in options.vars:
3734 print(gclient_eval.GetVar(local_scope, var))
3735
3736 commits = {}
3737 if local_scope.get(
3738 'git_dependencies'
3739 ) == gclient_eval.SUBMODULES and options.getdep_revisions:
3740 commits.update(
3741 scm_git.GIT.GetSubmoduleCommits(
3742 os.getcwd(),
3743 [path for path in options.getdep_revisions if ':' not in path]))
3744
3745 for name in options.getdep_revisions:
3746 if ':' in name:
3747 name, _, package = name.partition(':')
3748 if not name or not package:
3749 parser.error(
3750 'Wrong CIPD format: %s:%s should be of the form path:pkg.' %
3751 (name, package))
3752 print(gclient_eval.GetCIPD(local_scope, name, package))
3753 elif commits:
3754 print(commits[name])
3755 else:
3756 try:
3757 print(gclient_eval.GetRevision(local_scope, name))
3758 except KeyError as e:
3759 print(repr(e), file=sys.stderr)
3760 sys.exit(2)
Edward Lesmes411041f2018-04-05 20:12:55 -04003761
3762
Edward Lemur3298e7b2018-07-17 18:21:27 +00003763@metrics.collector.collect_metrics('gclient setdep')
Edward Lesmes6f64a052018-03-20 17:35:49 -04003764def CMDsetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003765 """Modifies dependency revisions and variable values in a DEPS file"""
3766 parser.add_option('--var',
3767 action='append',
3768 dest='vars',
3769 metavar='VAR=VAL',
3770 default=[],
3771 help='Sets a variable to the given value with the format '
3772 'name=value.')
3773 parser.add_option('-r',
3774 '--revision',
3775 action='append',
3776 dest='setdep_revisions',
3777 metavar='DEP@REV',
3778 default=[],
3779 help='Sets the revision/version for the dependency with '
3780 'the format dep@rev. If it is a git dependency, dep '
3781 'must be a path and rev must be a git hash or '
3782 'reference (e.g. src/dep@deadbeef). If it is a CIPD '
3783 'dependency, dep must be of the form path:package and '
3784 'rev must be the package version '
3785 '(e.g. src/pkg:chromium/pkg@2.1-cr0).')
3786 parser.add_option(
3787 '--deps-file',
3788 default='DEPS',
3789 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3790 # .gclient first.
3791 help='The DEPS file to be edited. Defaults to the DEPS '
3792 'file in the current directory.')
3793 (options, args) = parser.parse_args(args)
3794 if args:
3795 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3796 if not options.setdep_revisions and not options.vars:
Edward Lesmes0ecf6d62018-04-05 18:28:55 -04003797 parser.error(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003798 'You must specify at least one variable or revision to modify.')
3799
3800 if not os.path.isfile(options.deps_file):
3801 raise gclient_utils.Error('DEPS file %s does not exist.' %
3802 options.deps_file)
3803 with open(options.deps_file) as f:
3804 contents = f.read()
3805
3806 client = GClient.LoadCurrentConfig(options)
3807 if client is not None:
3808 builtin_vars = client.get_builtin_vars()
Edward Lesmes6f64a052018-03-20 17:35:49 -04003809 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003810 logging.warning(
3811 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3812 'file without support for built-in variables.')
3813 builtin_vars = None
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003814
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003815 local_scope = gclient_eval.Exec(contents,
3816 options.deps_file,
3817 builtin_vars=builtin_vars)
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003818
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003819 # Create a set of all git submodules.
3820 cwd = os.path.dirname(options.deps_file) or os.getcwd()
3821 git_modules = None
3822 if 'git_dependencies' in local_scope and local_scope[
3823 'git_dependencies'] in (gclient_eval.SUBMODULES, gclient_eval.SYNC):
3824 try:
3825 submodule_status = subprocess2.check_output(
3826 ['git', 'submodule', 'status'], cwd=cwd).decode('utf-8')
3827 git_modules = {l.split()[1] for l in submodule_status.splitlines()}
3828 except subprocess2.CalledProcessError as e:
3829 print('Warning: gitlinks won\'t be updated: ', e)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003830
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003831 for var in options.vars:
3832 name, _, value = var.partition('=')
3833 if not name or not value:
3834 parser.error(
3835 'Wrong var format: %s should be of the form name=value.' % var)
3836 if name in local_scope['vars']:
3837 gclient_eval.SetVar(local_scope, name, value)
3838 else:
3839 gclient_eval.AddVar(local_scope, name, value)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003840
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003841 for revision in options.setdep_revisions:
3842 name, _, value = revision.partition('@')
3843 if not name or not value:
3844 parser.error('Wrong dep format: %s should be of the form dep@rev.' %
3845 revision)
3846 if ':' in name:
3847 name, _, package = name.partition(':')
3848 if not name or not package:
3849 parser.error(
3850 'Wrong CIPD format: %s:%s should be of the form path:pkg@version.'
3851 % (name, package))
3852 gclient_eval.SetCIPD(local_scope, name, package, value)
3853 else:
3854 # Update DEPS only when `git_dependencies` == DEPS or SYNC.
3855 # git_dependencies is defaulted to DEPS when not set.
3856 if 'git_dependencies' not in local_scope or local_scope[
3857 'git_dependencies'] in (gclient_eval.DEPS,
3858 gclient_eval.SYNC):
3859 gclient_eval.SetRevision(local_scope, name, value)
3860
3861 # Update git submodules when `git_dependencies` == SYNC or
3862 # SUBMODULES.
3863 if git_modules and 'git_dependencies' in local_scope and local_scope[
3864 'git_dependencies'] in (gclient_eval.SUBMODULES,
3865 gclient_eval.SYNC):
3866 git_module_name = name
3867 if not 'use_relative_paths' in local_scope or \
3868 local_scope['use_relative_paths'] != True:
3869 deps_dir = os.path.dirname(
3870 os.path.abspath(options.deps_file))
3871 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
3872 delta_path = None
3873 if gclient_path:
3874 delta_path = os.path.relpath(
3875 deps_dir, os.path.abspath(gclient_path))
3876 if delta_path:
3877 prefix_length = len(delta_path.replace(
3878 os.path.sep, '/')) + 1
3879 git_module_name = name[prefix_length:]
3880 # gclient setdep should update the revision, i.e., the gitlink
3881 # only when the submodule entry is already present within
3882 # .gitmodules.
3883 if git_module_name not in git_modules:
3884 raise KeyError(
3885 f'Could not find any dependency called "{git_module_name}" in '
3886 f'.gitmodules.')
3887
3888 # Update the gitlink for the submodule.
3889 subprocess2.call([
3890 'git', 'update-index', '--add', '--cacheinfo',
3891 f'160000,{value},{git_module_name}'
3892 ],
3893 cwd=cwd)
3894
3895 with open(options.deps_file, 'wb') as f:
3896 f.write(gclient_eval.RenderDEPSFile(local_scope).encode('utf-8'))
3897
3898 if git_modules:
3899 subprocess2.call(['git', 'add', options.deps_file], cwd=cwd)
3900 print('Changes have been staged. See changes with `git status`.\n'
3901 'Use `git commit -m "Manual roll"` to commit your changes. \n'
3902 'Run gclient sync to update your local dependency checkout.')
Josip Sokcevic5561f8b2023-08-21 16:00:42 +00003903
Edward Lesmes6f64a052018-03-20 17:35:49 -04003904
Edward Lemur3298e7b2018-07-17 18:21:27 +00003905@metrics.collector.collect_metrics('gclient verify')
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003906def CMDverify(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003907 """Verifies the DEPS file deps are only from allowed_hosts."""
3908 (options, args) = parser.parse_args(args)
3909 client = GClient.LoadCurrentConfig(options)
3910 if not client:
3911 raise gclient_utils.Error(
3912 'client not configured; see \'gclient config\'')
3913 client.RunOnDeps(None, [])
3914 # Look at each first-level dependency of this gclient only.
3915 for dep in client.dependencies:
3916 bad_deps = dep.findDepsFromNotAllowedHosts()
3917 if not bad_deps:
3918 continue
3919 print("There are deps from not allowed hosts in file %s" %
3920 dep.deps_file)
3921 for bad_dep in bad_deps:
3922 print("\t%s at %s" % (bad_dep.name, bad_dep.url))
3923 print("allowed_hosts:", ', '.join(dep.allowed_hosts))
3924 sys.stdout.flush()
3925 raise gclient_utils.Error(
3926 'dependencies from disallowed hosts; check your DEPS file.')
3927 return 0
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003928
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003929
3930@subcommand.epilog("""For more information on what metrics are we collecting and
Edward Lemur8a2e3312018-07-12 21:15:09 +00003931why, please read metrics.README.md or visit https://bit.ly/2ufRS4p""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003932@metrics.collector.collect_metrics('gclient metrics')
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003933def CMDmetrics(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003934 """Reports, and optionally modifies, the status of metric collection."""
3935 parser.add_option('--opt-in',
3936 action='store_true',
3937 dest='enable_metrics',
3938 help='Opt-in to metrics collection.',
3939 default=None)
3940 parser.add_option('--opt-out',
3941 action='store_false',
3942 dest='enable_metrics',
3943 help='Opt-out of metrics collection.')
3944 options, args = parser.parse_args(args)
3945 if args:
3946 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3947 if not metrics.collector.config.is_googler:
3948 print("You're not a Googler. Metrics collection is disabled for you.")
3949 return 0
3950
3951 if options.enable_metrics is not None:
3952 metrics.collector.config.opted_in = options.enable_metrics
3953
3954 if metrics.collector.config.opted_in is None:
3955 print("You haven't opted in or out of metrics collection.")
3956 elif metrics.collector.config.opted_in:
3957 print("You have opted in. Thanks!")
3958 else:
3959 print("You have opted out. Please consider opting in.")
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003960 return 0
3961
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003962
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003963class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003964 gclientfile_default = os.environ.get('GCLIENT_FILE', '.gclient')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003965
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003966 def __init__(self, **kwargs):
3967 optparse.OptionParser.__init__(self,
3968 version='%prog ' + __version__,
3969 **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003970
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003971 # Some arm boards have issues with parallel sync.
3972 if platform.machine().startswith('arm'):
3973 jobs = 1
3974 else:
3975 jobs = max(8, gclient_utils.NumLocalCpus())
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003976
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003977 self.add_option(
3978 '-j',
3979 '--jobs',
3980 default=jobs,
3981 type='int',
3982 help='Specify how many SCM commands can run in parallel; defaults to '
3983 '%default on this machine')
3984 self.add_option(
3985 '-v',
3986 '--verbose',
3987 action='count',
3988 default=0,
3989 help='Produces additional output for diagnostics. Can be used up to '
3990 'three times for more logging info.')
3991 self.add_option('--gclientfile',
3992 dest='config_filename',
3993 help='Specify an alternate %s file' %
3994 self.gclientfile_default)
3995 self.add_option(
3996 '--spec',
3997 help='create a gclient file containing the provided string. Due to '
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003998 'Cygwin/Python brokenness, it can\'t contain any newlines.')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003999 self.add_option('--no-nag-max',
4000 default=False,
4001 action='store_true',
4002 help='Ignored for backwards compatibility.')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004003
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004004 def parse_args(self, args=None, _values=None):
4005 """Integrates standard options processing."""
4006 # Create an optparse.Values object that will store only the actual
4007 # passed options, without the defaults.
4008 actual_options = optparse.Values()
4009 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
4010 # Create an optparse.Values object with the default options.
4011 options = optparse.Values(self.get_default_values().__dict__)
4012 # Update it with the options passed by the user.
4013 options._update_careful(actual_options.__dict__)
4014 # Store the options passed by the user in an _actual_options attribute.
4015 # We store only the keys, and not the values, since the values can
4016 # contain arbitrary information, which might be PII.
4017 metrics.collector.add('arguments', list(actual_options.__dict__))
Edward Lemur3298e7b2018-07-17 18:21:27 +00004018
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004019 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
4020 logging.basicConfig(
4021 level=levels[min(options.verbose,
4022 len(levels) - 1)],
4023 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
4024 if options.config_filename and options.spec:
4025 self.error('Cannot specify both --gclientfile and --spec')
4026 if (options.config_filename and options.config_filename !=
4027 os.path.basename(options.config_filename)):
4028 self.error('--gclientfile target must be a filename, not a path')
4029 if not options.config_filename:
4030 options.config_filename = self.gclientfile_default
4031 options.entries_filename = options.config_filename + '_entries'
4032 if options.jobs < 1:
4033 self.error('--jobs must be 1 or higher')
maruel@chromium.org0895b752011-08-26 20:40:33 +00004034
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004035 # These hacks need to die.
4036 if not hasattr(options, 'revisions'):
4037 # GClient.RunOnDeps expects it even if not applicable.
4038 options.revisions = []
4039 if not hasattr(options, 'experiments'):
4040 options.experiments = []
4041 if not hasattr(options, 'head'):
4042 options.head = None
4043 if not hasattr(options, 'nohooks'):
4044 options.nohooks = True
4045 if not hasattr(options, 'noprehooks'):
4046 options.noprehooks = True
4047 if not hasattr(options, 'deps_os'):
4048 options.deps_os = None
4049 if not hasattr(options, 'force'):
4050 options.force = None
4051 return (options, args)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004052
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004053
4054def disable_buffering():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004055 # Make stdout auto-flush so buildbot doesn't kill us during lengthy
4056 # operations. Python as a strong tendency to buffer sys.stdout.
4057 sys.stdout = gclient_utils.MakeFileAutoFlush(sys.stdout)
4058 # Make stdout annotated with the thread ids.
4059 sys.stdout = gclient_utils.MakeFileAnnotated(sys.stdout)
maruel@chromium.org0895b752011-08-26 20:40:33 +00004060
4061
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004062def path_contains_tilde():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004063 for element in os.environ['PATH'].split(os.pathsep):
4064 if element.startswith('~') and os.path.abspath(
4065 os.path.realpath(
4066 os.path.expanduser(element))) == DEPOT_TOOLS_DIR:
4067 return True
4068 return False
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004069
4070
4071def can_run_gclient_and_helpers():
Gavin Mak7f5b53f2023-09-07 18:13:01 +00004072 if sys.version_info[0] < 3:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004073 print('\nYour python version %s is unsupported, please upgrade.\n' %
4074 sys.version.split(' ', 1)[0],
4075 file=sys.stderr)
4076 return False
4077 if not sys.executable:
4078 print('\nPython cannot find the location of it\'s own executable.\n',
4079 file=sys.stderr)
4080 return False
4081 if path_contains_tilde():
4082 print(
4083 '\nYour PATH contains a literal "~", which works in some shells ' +
4084 'but will break when python tries to run subprocesses. ' +
4085 'Replace the "~" with $HOME.\n' + 'See https://crbug.com/952865.\n',
4086 file=sys.stderr)
4087 return False
4088 return True
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004089
4090
4091def main(argv):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004092 """Doesn't parse the arguments here, just find the right subcommand to
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004093 execute."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004094 if not can_run_gclient_and_helpers():
4095 return 2
4096 fix_encoding.fix_encoding()
4097 disable_buffering()
4098 setup_color.init()
4099 dispatcher = subcommand.CommandDispatcher(__name__)
4100 try:
4101 return dispatcher.execute(OptionParser(), argv)
4102 except KeyboardInterrupt:
4103 gclient_utils.GClientChildren.KillAllRemainingChildren()
4104 raise
4105 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
4106 print('Error: %s' % str(e), file=sys.stderr)
4107 return 1
4108 finally:
4109 gclient_utils.PrintWarnings()
4110 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004111
4112
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00004113if '__main__' == __name__:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004114 with metrics.collector.print_notice_and_exit():
4115 sys.exit(main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004116
4117# vim: ts=2:sw=2:tw=80:et: