blob: b692f2ee735c207c3a93a04256d3c7e15df11c56 [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."""
Josip Sokcevic8fb358e2023-11-15 22:24:06 +00001566 _is_env_cog = None
1567
1568 @staticmethod
1569 def _IsCog():
1570 """Returns true if the env is cog"""
1571 if GitDependency._is_env_cog is None:
1572 GitDependency._is_env_cog = os.getcwd().startswith(
1573 '/google/cog/cloud')
1574
1575 return GitDependency._is_env_cog
1576
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001577 @staticmethod
1578 def updateProtocol(url, protocol):
1579 """Updates given URL's protocol"""
1580 # only works on urls, skips local paths
1581 if not url or not protocol or not re.match('([a-z]+)://', url) or \
1582 re.match('file://', url):
1583 return url
Edward Lemurb61d3872018-05-09 18:42:47 -04001584
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001585 return re.sub('^([a-z]+):', protocol + ':', url)
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +00001586
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001587 #override
1588 def GetScmName(self):
1589 """Always 'git'."""
1590 return 'git'
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +00001591
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001592 #override
1593 def CreateSCM(self, out_cb=None):
1594 """Create a Wrapper instance suitable for handling this git dependency."""
Josip Sokcevic8fb358e2023-11-15 22:24:06 +00001595 if self._IsCog():
1596 return gclient_scm.CogWrapper()
1597
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001598 return gclient_scm.GitWrapper(self.url,
1599 self.root.root_dir,
1600 self.name,
1601 self.outbuf,
1602 out_cb,
1603 print_outbuf=self.print_outbuf)
Edward Lemurb61d3872018-05-09 18:42:47 -04001604
1605
1606class GClient(GitDependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001607 """Object that represent a gclient checkout. A tree of Dependency(), one per
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001608 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001609
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001610 DEPS_OS_CHOICES = {
1611 "aix6": "unix",
1612 "win32": "win",
1613 "win": "win",
1614 "cygwin": "win",
1615 "darwin": "mac",
1616 "mac": "mac",
1617 "unix": "unix",
1618 "linux": "unix",
1619 "linux2": "unix",
1620 "linux3": "unix",
1621 "android": "android",
1622 "ios": "ios",
1623 "fuchsia": "fuchsia",
1624 "chromeos": "chromeos",
1625 }
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001626
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001627 DEFAULT_CLIENT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001628solutions = [
Edward Lesmes05934952019-12-19 20:38:09 +00001629 { "name" : %(solution_name)r,
1630 "url" : %(solution_url)r,
1631 "deps_file" : %(deps_file)r,
1632 "managed" : %(managed)r,
smutae7ea312016-07-18 11:59:41 -07001633 "custom_deps" : {
1634 },
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02001635 "custom_vars": %(custom_vars)r,
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001636 },
1637]
Robert Iannuccia19649b2018-06-29 16:31:45 +00001638""")
1639
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001640 DEFAULT_CLIENT_CACHE_DIR_TEXT = ("""\
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001641cache_dir = %(cache_dir)r
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001642""")
1643
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001644 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001645# Snapshot generated with gclient revinfo --snapshot
Edward Lesmesc2960242018-03-06 20:50:15 -05001646solutions = %(solution_list)s
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001647""")
1648
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001649 def __init__(self, root_dir, options):
1650 # Do not change previous behavior. Only solution level and immediate
1651 # DEPS are processed.
1652 self._recursion_limit = 2
1653 super(GClient, self).__init__(parent=None,
1654 name=None,
1655 url=None,
1656 managed=True,
1657 custom_deps=None,
1658 custom_vars=None,
1659 custom_hooks=None,
1660 deps_file='unused',
1661 should_process=True,
1662 should_recurse=True,
1663 relative=None,
1664 condition=None,
1665 print_outbuf=True)
Edward Lemure05f18d2018-06-08 17:36:53 +00001666
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001667 self._options = options
1668 if options.deps_os:
1669 enforced_os = options.deps_os.split(',')
1670 else:
1671 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
1672 if 'all' in enforced_os:
1673 enforced_os = self.DEPS_OS_CHOICES.values()
1674 self._enforced_os = tuple(set(enforced_os))
1675 self._enforced_cpu = (detect_host_arch.HostArch(), )
1676 self._root_dir = root_dir
1677 self._cipd_root = None
1678 self.config_content = None
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001679
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001680 def _CheckConfig(self):
1681 """Verify that the config matches the state of the existing checked-out
borenet@google.com88d10082014-03-21 17:24:48 +00001682 solutions."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001683 for dep in self.dependencies:
1684 if dep.managed and dep.url:
1685 scm = dep.CreateSCM()
1686 actual_url = scm.GetActualRemoteURL(self._options)
1687 if actual_url and not scm.DoesRemoteURLMatch(self._options):
1688 mirror = scm.GetCacheMirror()
1689 if mirror:
1690 mirror_string = '%s (exists=%s)' % (mirror.mirror_path,
1691 mirror.exists())
1692 else:
1693 mirror_string = 'not used'
1694 raise gclient_utils.Error(
1695 '''
borenet@google.com88d10082014-03-21 17:24:48 +00001696Your .gclient file seems to be broken. The requested URL is different from what
borenet@google.com0a427372014-04-02 19:12:13 +00001697is actually checked out in %(checkout_path)s.
borenet@google.com88d10082014-03-21 17:24:48 +00001698
borenet@google.com97882362014-04-07 20:06:02 +00001699The .gclient file contains:
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +00001700URL: %(expected_url)s (%(expected_scm)s)
1701Cache mirror: %(mirror_string)s
borenet@google.com97882362014-04-07 20:06:02 +00001702
1703The local checkout in %(checkout_path)s reports:
1704%(actual_url)s (%(actual_scm)s)
borenet@google.com88d10082014-03-21 17:24:48 +00001705
1706You should ensure that the URL listed in .gclient is correct and either change
agabled437d762016-10-17 09:35:11 -07001707it or fix the checkout.
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00001708''' % {
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001709 'checkout_path': os.path.join(
1710 self.root_dir, dep.name),
1711 'expected_url': dep.url,
1712 'expected_scm': dep.GetScmName(),
1713 'mirror_string': mirror_string,
1714 'actual_url': actual_url,
1715 'actual_scm': dep.GetScmName()
1716 })
borenet@google.com88d10082014-03-21 17:24:48 +00001717
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001718 def SetConfig(self, content):
1719 assert not self.dependencies
1720 config_dict = {}
1721 self.config_content = content
1722 try:
1723 exec(content, config_dict)
1724 except SyntaxError as e:
1725 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001726
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001727 # Append any target OS that is not already being enforced to the tuple.
1728 target_os = config_dict.get('target_os', [])
1729 if config_dict.get('target_os_only', False):
1730 self._enforced_os = tuple(set(target_os))
1731 else:
1732 self._enforced_os = tuple(set(self._enforced_os).union(target_os))
stuartmorgan@chromium.org18a4f6a2012-11-02 13:37:24 +00001733
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001734 # Append any target CPU that is not already being enforced to the tuple.
1735 target_cpu = config_dict.get('target_cpu', [])
1736 if config_dict.get('target_cpu_only', False):
1737 self._enforced_cpu = tuple(set(target_cpu))
1738 else:
1739 self._enforced_cpu = tuple(
1740 set(self._enforced_cpu).union(target_cpu))
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001741
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001742 cache_dir = config_dict.get('cache_dir', UNSET_CACHE_DIR)
1743 if cache_dir is not UNSET_CACHE_DIR:
1744 if cache_dir:
1745 cache_dir = os.path.join(self.root_dir, cache_dir)
1746 cache_dir = os.path.abspath(cache_dir)
Andrii Shyshkalov77ce4bd2017-11-27 12:38:18 -08001747
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001748 git_cache.Mirror.SetCachePath(cache_dir)
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001749
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001750 if not target_os and config_dict.get('target_os_only', False):
1751 raise gclient_utils.Error(
1752 'Can\'t use target_os_only if target_os is '
1753 'not specified')
peter@chromium.org1efccc82012-04-27 16:34:38 +00001754
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001755 if not target_cpu and config_dict.get('target_cpu_only', False):
1756 raise gclient_utils.Error(
1757 'Can\'t use target_cpu_only if target_cpu is '
1758 'not specified')
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001759
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001760 deps_to_add = []
1761 for s in config_dict.get('solutions', []):
1762 try:
1763 deps_to_add.append(
1764 GitDependency(
1765 parent=self,
1766 name=s['name'],
1767 # Update URL with scheme in protocol_override
1768 url=GitDependency.updateProtocol(
1769 s['url'], s.get('protocol_override', None)),
1770 managed=s.get('managed', True),
1771 custom_deps=s.get('custom_deps', {}),
1772 custom_vars=s.get('custom_vars', {}),
1773 custom_hooks=s.get('custom_hooks', []),
1774 deps_file=s.get('deps_file', 'DEPS'),
1775 should_process=True,
1776 should_recurse=True,
1777 relative=None,
1778 condition=None,
1779 print_outbuf=True,
1780 # Pass protocol_override down the tree for child deps to
1781 # use.
1782 protocol=s.get('protocol_override', None),
1783 git_dependencies_state=self.git_dependencies_state))
1784 except KeyError:
1785 raise gclient_utils.Error('Invalid .gclient file. Solution is '
1786 'incomplete: %s' % s)
1787 metrics.collector.add('project_urls', [
Edward Lemuraffd4102019-06-05 18:07:49 +00001788 dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Edward Lemur40764b02018-07-20 18:50:29 +00001789 for dep in deps_to_add
1790 if dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001791 ])
Edward Lemur40764b02018-07-20 18:50:29 +00001792
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001793 self.add_dependencies_and_close(deps_to_add,
1794 config_dict.get('hooks', []))
1795 logging.info('SetConfig() done')
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001796
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001797 def SaveConfig(self):
1798 gclient_utils.FileWrite(
1799 os.path.join(self.root_dir, self._options.config_filename),
1800 self.config_content)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001801
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001802 @staticmethod
1803 def LoadCurrentConfig(options):
1804 # type: (optparse.Values) -> GClient
1805 """Searches for and loads a .gclient file relative to the current working
Joanna Wang66286612022-06-30 19:59:13 +00001806 dir."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001807 if options.spec:
1808 client = GClient('.', options)
1809 client.SetConfig(options.spec)
1810 else:
1811 if options.verbose:
1812 print('Looking for %s starting from %s\n' %
1813 (options.config_filename, os.getcwd()))
1814 path = gclient_paths.FindGclientRoot(os.getcwd(),
1815 options.config_filename)
1816 if not path:
1817 if options.verbose:
1818 print('Couldn\'t find configuration file.')
1819 return None
1820 client = GClient(path, options)
1821 client.SetConfig(
1822 gclient_utils.FileRead(
1823 os.path.join(path, options.config_filename)))
maruel@chromium.org69392e72011-10-13 22:09:00 +00001824
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001825 if (options.revisions and len(client.dependencies) > 1
1826 and any('@' not in r for r in options.revisions)):
1827 print((
1828 'You must specify the full solution name like --revision %s@%s\n'
1829 'when you have multiple solutions setup in your .gclient file.\n'
1830 'Other solutions present are: %s.') %
1831 (client.dependencies[0].name, options.revisions[0], ', '.join(
1832 s.name for s in client.dependencies[1:])),
1833 file=sys.stderr)
Joanna Wang66286612022-06-30 19:59:13 +00001834
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001835 return client
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001836
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001837 def SetDefaultConfig(self,
1838 solution_name,
1839 deps_file,
1840 solution_url,
1841 managed=True,
1842 cache_dir=UNSET_CACHE_DIR,
1843 custom_vars=None):
1844 text = self.DEFAULT_CLIENT_FILE_TEXT
1845 format_dict = {
1846 'solution_name': solution_name,
1847 'solution_url': solution_url,
1848 'deps_file': deps_file,
1849 'managed': managed,
1850 'custom_vars': custom_vars or {},
1851 }
Robert Iannuccia19649b2018-06-29 16:31:45 +00001852
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001853 if cache_dir is not UNSET_CACHE_DIR:
1854 text += self.DEFAULT_CLIENT_CACHE_DIR_TEXT
1855 format_dict['cache_dir'] = cache_dir
Robert Iannuccia19649b2018-06-29 16:31:45 +00001856
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001857 self.SetConfig(text % format_dict)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001858
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001859 def _SaveEntries(self):
1860 """Creates a .gclient_entries file to record the list of unique checkouts.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001861
1862 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001863 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001864 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
1865 # makes testing a bit too fun.
1866 result = 'entries = {\n'
1867 for entry in self.root.subtree(False):
1868 result += ' %s: %s,\n' % (pprint.pformat(
1869 entry.name), pprint.pformat(entry.url))
1870 result += '}\n'
1871 file_path = os.path.join(self.root_dir, self._options.entries_filename)
1872 logging.debug(result)
1873 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001874
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001875 def _ReadEntries(self):
1876 """Read the .gclient_entries file for the given client.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001877
1878 Returns:
1879 A sequence of solution names, which will be empty if there is the
1880 entries file hasn't been created yet.
1881 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001882 scope = {}
1883 filename = os.path.join(self.root_dir, self._options.entries_filename)
1884 if not os.path.exists(filename):
1885 return {}
1886 try:
1887 exec(gclient_utils.FileRead(filename), scope)
1888 except SyntaxError as e:
1889 gclient_utils.SyntaxErrorToError(filename, e)
1890 return scope.get('entries', {})
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001891
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001892 def _ExtractFileJsonContents(self, default_filename):
1893 # type: (str) -> Mapping[str,Any]
1894 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001895
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001896 if not os.path.exists(f):
1897 logging.info('File %s does not exist.' % f)
1898 return {}
Joanna Wang01870792022-08-01 19:02:57 +00001899
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001900 with open(f, 'r') as open_f:
1901 logging.info('Reading content from file %s' % f)
1902 content = open_f.read().rstrip()
1903 if content:
1904 return json.loads(content)
Joanna Wang66286612022-06-30 19:59:13 +00001905 return {}
1906
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001907 def _WriteFileContents(self, default_filename, content):
1908 # type: (str, str) -> None
1909 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001910
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001911 with open(f, 'w') as open_f:
1912 logging.info('Writing to file %s' % f)
1913 open_f.write(content)
Joanna Wangf3edc502022-07-20 00:12:10 +00001914
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001915 def _EnforceSkipSyncRevisions(self, patch_refs):
1916 # type: (Mapping[str, str]) -> Mapping[str, str]
1917 """Checks for and enforces revisions for skipping deps syncing."""
1918 previous_sync_commits = self._ExtractFileJsonContents(
1919 PREVIOUS_SYNC_COMMITS_FILE)
Joanna Wang66286612022-06-30 19:59:13 +00001920
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001921 if not previous_sync_commits:
1922 return {}
maruel@chromium.org918a9ae2010-05-28 15:50:30 +00001923
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001924 # Current `self.dependencies` only contain solutions. If a patch_ref is
1925 # not for a solution, then it is for a solution's dependency or recursed
1926 # dependency which we cannot support while skipping sync.
1927 if patch_refs:
1928 unclaimed_prs = []
1929 candidates = []
1930 for dep in self.dependencies:
1931 origin, _ = gclient_utils.SplitUrlRevision(dep.url)
1932 candidates.extend([origin, dep.name])
1933 for patch_repo in patch_refs:
1934 if not gclient_utils.FuzzyMatchRepo(patch_repo, candidates):
1935 unclaimed_prs.append(patch_repo)
1936 if unclaimed_prs:
1937 print(
1938 'We cannot skip syncs when there are --patch-refs flags for '
1939 'non-solution dependencies. To skip syncing, remove patch_refs '
1940 'for: \n%s' % '\n'.join(unclaimed_prs))
1941 return {}
Edward Lesmesc621b212018-03-21 20:26:56 -04001942
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001943 # We cannot skip syncing if there are custom_vars that differ from the
1944 # previous run's custom_vars.
1945 previous_custom_vars = self._ExtractFileJsonContents(
1946 PREVIOUS_CUSTOM_VARS_FILE)
1947
1948 cvs_by_name = {s.name: s.custom_vars for s in self.dependencies}
1949
1950 skip_sync_revisions = {}
1951 for name, commit in previous_sync_commits.items():
1952 previous_vars = previous_custom_vars.get(name)
1953 if previous_vars == cvs_by_name.get(name) or (
1954 not previous_vars and not cvs_by_name.get(name)):
1955 skip_sync_revisions[name] = commit
1956 else:
1957 print(
1958 'We cannot skip syncs when custom_vars for solutions have '
1959 'changed since the last sync run on this machine.\n'
1960 '\nRemoving skip_sync_revision for:\n'
1961 'solution: %s, current: %r, previous: %r.' %
1962 (name, cvs_by_name.get(name), previous_vars))
1963 print('no-sync experiment enabled with %r' % skip_sync_revisions)
1964 return skip_sync_revisions
1965
1966 # TODO(crbug.com/1340695): Remove handling revisions without '@'.
1967 def _EnforceRevisions(self):
1968 """Checks for revision overrides."""
1969 revision_overrides = {}
1970 if self._options.head:
1971 return revision_overrides
1972 if not self._options.revisions:
1973 return revision_overrides
1974 solutions_names = [s.name for s in self.dependencies]
1975 for index, revision in enumerate(self._options.revisions):
1976 if not '@' in revision:
1977 # Support for --revision 123
1978 revision = '%s@%s' % (solutions_names[index], revision)
1979 name, rev = revision.split('@', 1)
1980 revision_overrides[name] = rev
1981 return revision_overrides
1982
1983 def _EnforcePatchRefsAndBranches(self):
1984 # type: () -> Tuple[Mapping[str, str], Mapping[str, str]]
1985 """Checks for patch refs."""
1986 patch_refs = {}
1987 target_branches = {}
1988 if not self._options.patch_refs:
1989 return patch_refs, target_branches
1990 for given_patch_ref in self._options.patch_refs:
1991 patch_repo, _, patch_ref = given_patch_ref.partition('@')
1992 if not patch_repo or not patch_ref or ':' not in patch_ref:
1993 raise gclient_utils.Error(
1994 'Wrong revision format: %s should be of the form '
1995 'patch_repo@target_branch:patch_ref.' % given_patch_ref)
1996 target_branch, _, patch_ref = patch_ref.partition(':')
1997 target_branches[patch_repo] = target_branch
1998 patch_refs[patch_repo] = patch_ref
1999 return patch_refs, target_branches
2000
Gavin Mak50b27a52023-09-19 22:44:59 +00002001 def _InstallPreCommitHook(self):
2002 # On Windows, this path is written to the file as
2003 # "dir\hooks\pre-commit.py" but it gets interpreted as
2004 # "dirhookspre-commit.py".
2005 gclient_hook_path = os.path.join(DEPOT_TOOLS_DIR, 'hooks',
2006 'pre-commit.py').replace('\\', '\\\\')
2007 gclient_hook_content = '\n'.join((
2008 f'{PRECOMMIT_HOOK_VAR}={gclient_hook_path}',
2009 f'if [ -f "${PRECOMMIT_HOOK_VAR}" ]; then',
2010 f' python3 "${PRECOMMIT_HOOK_VAR}" || exit 1',
2011 'fi',
2012 ))
2013
2014 soln = gclient_paths.GetPrimarySolutionPath()
2015 if not soln:
2016 print('Could not find gclient solution.')
2017 return
2018
2019 git_dir = os.path.join(soln, '.git')
2020 if not os.path.exists(git_dir):
2021 return
2022
Philipp Wollermanna45d2d42023-09-21 02:45:42 +00002023 git_hooks_dir = os.path.join(git_dir, 'hooks')
2024 os.makedirs(git_hooks_dir, exist_ok=True)
2025
Gavin Mak50b27a52023-09-19 22:44:59 +00002026 hook = os.path.join(git_dir, 'hooks', 'pre-commit')
2027 if os.path.exists(hook):
2028 with open(hook, 'r') as f:
2029 content = f.read()
2030 if PRECOMMIT_HOOK_VAR in content:
2031 print(f'{hook} already contains the gclient pre-commit hook.')
2032 else:
2033 print(f'A pre-commit hook already exists at {hook}.\n'
2034 f'Please append the following lines to the hook:\n\n'
2035 f'{gclient_hook_content}')
2036 return
2037
2038 print(f'Creating a pre-commit hook at {hook}')
2039 with open(hook, 'w') as f:
2040 f.write('#!/bin/sh\n')
2041 f.write(f'{gclient_hook_content}\n')
2042 os.chmod(hook, 0o755)
2043
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002044 def _RemoveUnversionedGitDirs(self):
2045 """Remove directories that are no longer part of the checkout.
Edward Lemur5b1fa942018-10-04 23:22:09 +00002046
2047 Notify the user if there is an orphaned entry in their working copy.
2048 Only delete the directory if there are no changes in it, and
2049 delete_unversioned_trees is set to true.
Josip Sokcevic1b8211f2022-09-30 17:46:53 +00002050
2051 Returns CIPD packages that are no longer versioned.
Edward Lemur5b1fa942018-10-04 23:22:09 +00002052 """
2053
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002054 entry_names_and_sync = [(i.name, i._should_sync)
2055 for i in self.root.subtree(False) if i.url]
2056 entries = []
2057 if entry_names_and_sync:
2058 entries, _ = zip(*entry_names_and_sync)
2059 full_entries = [
2060 os.path.join(self.root_dir, e.replace('/', os.path.sep))
2061 for e in entries
2062 ]
2063 no_sync_entries = [
2064 name for name, should_sync in entry_names_and_sync
2065 if not should_sync
2066 ]
Edward Lemur5b1fa942018-10-04 23:22:09 +00002067
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002068 removed_cipd_entries = []
Joanna Wang60adf7b2023-10-06 00:04:28 +00002069 read_entries = self._ReadEntries()
2070 # We process entries sorted in reverse to ensure a child dir is
2071 # always deleted before its parent dir.
2072 # This is especially important for submodules with pinned revisions
2073 # overwritten by a vars or custom_deps. In this case, if a parent
2074 # submodule is encountered first in the loop, it cannot tell the
2075 # difference between modifications from the vars or actual user
2076 # modifications that should be kept. http://crbug/1486677#c9 for
2077 # more details.
2078 for entry in sorted(read_entries, reverse=True):
2079 prev_url = read_entries[entry]
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002080 if not prev_url:
2081 # entry must have been overridden via .gclient custom_deps
2082 continue
2083 if any(entry.startswith(sln) for sln in no_sync_entries):
2084 # Dependencies of solutions that skipped syncing would not
2085 # show up in `entries`.
2086 continue
2087 if (':' in entry):
2088 # This is a cipd package. Don't clean it up, but prepare for
2089 # return
2090 if entry not in entries:
2091 removed_cipd_entries.append(entry)
2092 continue
2093 # Fix path separator on Windows.
2094 entry_fixed = entry.replace('/', os.path.sep)
2095 e_dir = os.path.join(self.root_dir, entry_fixed)
2096 # Use entry and not entry_fixed there.
2097 if (entry not in entries and
2098 (not any(path.startswith(entry + '/') for path in entries))
2099 and os.path.exists(e_dir)):
2100 # The entry has been removed from DEPS.
2101 scm = gclient_scm.GitWrapper(prev_url, self.root_dir,
2102 entry_fixed, self.outbuf)
Edward Lemur5b1fa942018-10-04 23:22:09 +00002103
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002104 # Check to see if this directory is now part of a higher-up
2105 # checkout.
2106 scm_root = None
2107 try:
2108 scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2109 scm.checkout_path)
2110 except subprocess2.CalledProcessError:
2111 pass
2112 if not scm_root:
2113 logging.warning(
2114 'Could not find checkout root for %s. Unable to '
2115 'determine whether it is part of a higher-level '
2116 'checkout, so not removing.' % entry)
2117 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002118
Joanna Wang60adf7b2023-10-06 00:04:28 +00002119 versioned_state = None
2120 # Check if this is a submodule or versioned directory.
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002121 if os.path.abspath(scm_root) == os.path.abspath(e_dir):
2122 e_par_dir = os.path.join(e_dir, os.pardir)
2123 if gclient_scm.scm.GIT.IsInsideWorkTree(e_par_dir):
2124 par_scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2125 e_par_dir)
2126 # rel_e_dir : relative path of entry w.r.t. its parent
2127 # repo.
2128 rel_e_dir = os.path.relpath(e_dir, par_scm_root)
Joanna Wang60adf7b2023-10-06 00:04:28 +00002129 versioned_state = gclient_scm.scm.GIT.IsVersioned(
2130 par_scm_root, rel_e_dir)
2131 # This is to handle the case of third_party/WebKit migrating
2132 # from being a DEPS entry to being part of the main project. If
2133 # the subproject is a Git project, we need to remove its .git
2134 # folder. Otherwise git operations on that folder will have
2135 # different effects depending on the current working directory.
2136 if versioned_state == gclient_scm.scm.VERSIONED_DIR:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002137 save_dir = scm.GetGitBackupDirPath()
2138 # Remove any eventual stale backup dir for the same
2139 # project.
2140 if os.path.exists(save_dir):
2141 gclient_utils.rmtree(save_dir)
2142 os.rename(os.path.join(e_dir, '.git'), save_dir)
2143 # When switching between the two states (entry/ is a
2144 # subproject -> entry/ is part of the outer
2145 # project), it is very likely that some files are
2146 # changed in the checkout, unless we are jumping
2147 # *exactly* across the commit which changed just
2148 # DEPS. In such case we want to cleanup any eventual
2149 # stale files (coming from the old subproject) in
2150 # order to end up with a clean checkout.
2151 gclient_scm.scm.GIT.CleanupDir(
2152 par_scm_root, rel_e_dir)
2153 assert not os.path.exists(
2154 os.path.join(e_dir, '.git'))
2155 print(
2156 '\nWARNING: \'%s\' has been moved from DEPS to a higher '
2157 'level checkout. The git folder containing all the local'
2158 ' branches has been saved to %s.\n'
2159 'If you don\'t care about its state you can safely '
2160 'remove that folder to free up space.' %
2161 (entry, save_dir))
2162 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002163
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002164 if scm_root in full_entries:
2165 logging.info(
2166 '%s is part of a higher level checkout, not removing',
2167 scm.GetCheckoutRoot())
2168 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002169
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002170 file_list = []
2171 scm.status(self._options, [], file_list)
2172 modified_files = file_list != []
2173 if (not self._options.delete_unversioned_trees
2174 or (modified_files and not self._options.force)):
2175 # There are modified files in this entry. Keep warning until
2176 # removed.
2177 self.add_dependency(
2178 GitDependency(
2179 parent=self,
2180 name=entry,
2181 # Update URL with scheme in protocol_override
2182 url=GitDependency.updateProtocol(
2183 prev_url, self.protocol),
2184 managed=False,
2185 custom_deps={},
2186 custom_vars={},
2187 custom_hooks=[],
2188 deps_file=None,
2189 should_process=True,
2190 should_recurse=False,
2191 relative=None,
2192 condition=None,
2193 protocol=self.protocol,
2194 git_dependencies_state=self.git_dependencies_state))
2195 if modified_files and self._options.delete_unversioned_trees:
2196 print(
2197 '\nWARNING: \'%s\' is no longer part of this client.\n'
2198 'Despite running \'gclient sync -D\' no action was taken '
2199 'as there are modifications.\nIt is recommended you revert '
2200 'all changes or run \'gclient sync -D --force\' next '
2201 'time.' % entry_fixed)
2202 else:
2203 print(
2204 '\nWARNING: \'%s\' is no longer part of this client.\n'
2205 'It is recommended that you manually remove it or use '
2206 '\'gclient sync -D\' next time.' % entry_fixed)
2207 else:
2208 # Delete the entry
2209 print('\n________ deleting \'%s\' in \'%s\'' %
2210 (entry_fixed, self.root_dir))
2211 gclient_utils.rmtree(e_dir)
Joanna Wang60adf7b2023-10-06 00:04:28 +00002212 # We restore empty directories of submodule paths.
2213 if versioned_state == gclient_scm.scm.VERSIONED_SUBMODULE:
2214 gclient_scm.scm.GIT.Capture(
2215 ['restore', '--', rel_e_dir], cwd=par_scm_root)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002216 # record the current list of entries for next time
2217 self._SaveEntries()
2218 return removed_cipd_entries
Edward Lemur5b1fa942018-10-04 23:22:09 +00002219
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002220 def RunOnDeps(self,
2221 command,
2222 args,
2223 ignore_requirements=False,
2224 progress=True):
2225 """Runs a command on each dependency in a client and its dependencies.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002226
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002227 Args:
2228 command: The command to use (e.g., 'status' or 'diff')
2229 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002230 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002231 if not self.dependencies:
2232 raise gclient_utils.Error('No solution specified')
Michael Mossd683d7c2018-06-15 05:05:17 +00002233
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002234 revision_overrides = {}
2235 patch_refs = {}
2236 target_branches = {}
2237 skip_sync_revisions = {}
2238 # It's unnecessary to check for revision overrides for 'recurse'.
2239 # Save a few seconds by not calling _EnforceRevisions() in that case.
2240 if command not in ('diff', 'recurse', 'runhooks', 'status', 'revert',
2241 'validate'):
2242 self._CheckConfig()
2243 revision_overrides = self._EnforceRevisions()
Edward Lesmesc621b212018-03-21 20:26:56 -04002244
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002245 if command == 'update':
2246 patch_refs, target_branches = self._EnforcePatchRefsAndBranches()
2247 if NO_SYNC_EXPERIMENT in self._options.experiments:
2248 skip_sync_revisions = self._EnforceSkipSyncRevisions(patch_refs)
Joanna Wang66286612022-06-30 19:59:13 +00002249
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002250 # Store solutions' custom_vars on memory to compare in the next run.
2251 # All dependencies added later are inherited from the current
2252 # self.dependencies.
2253 custom_vars = {
2254 dep.name: dep.custom_vars
2255 for dep in self.dependencies if dep.custom_vars
Michael Mossd683d7c2018-06-15 05:05:17 +00002256 }
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002257 if custom_vars:
2258 self._WriteFileContents(PREVIOUS_CUSTOM_VARS_FILE,
2259 json.dumps(custom_vars))
2260
2261 # Disable progress for non-tty stdout.
2262 should_show_progress = (setup_color.IS_TTY and not self._options.verbose
2263 and progress)
2264 pm = None
2265 if should_show_progress:
2266 if command in ('update', 'revert'):
2267 pm = Progress('Syncing projects', 1)
2268 elif command in ('recurse', 'validate'):
2269 pm = Progress(' '.join(args), 1)
2270 work_queue = gclient_utils.ExecutionQueue(
2271 self._options.jobs,
2272 pm,
2273 ignore_requirements=ignore_requirements,
2274 verbose=self._options.verbose)
2275 for s in self.dependencies:
2276 if s.should_process:
2277 work_queue.enqueue(s)
2278 work_queue.flush(revision_overrides,
2279 command,
2280 args,
2281 options=self._options,
2282 patch_refs=patch_refs,
2283 target_branches=target_branches,
2284 skip_sync_revisions=skip_sync_revisions)
2285
2286 if revision_overrides:
2287 print(
2288 'Please fix your script, having invalid --revision flags will soon '
2289 'be considered an error.',
2290 file=sys.stderr)
2291
2292 if patch_refs:
2293 raise gclient_utils.Error(
2294 'The following --patch-ref flags were not used. Please fix it:\n%s'
2295 % ('\n'.join(patch_repo + '@' + patch_ref
2296 for patch_repo, patch_ref in patch_refs.items())))
2297
2298 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
2299 # they have fsmonitor enabled.
2300 if command == 'update':
2301 # Check if any of the root dependency have submodules.
2302 is_submoduled = any(
2303 map(
2304 lambda d: d.git_dependencies_state in
2305 (gclient_eval.SUBMODULES, gclient_eval.SYNC),
2306 self.dependencies))
2307 if is_submoduled:
2308 git_common.warn_submodule()
2309
2310 # Once all the dependencies have been processed, it's now safe to write
2311 # out the gn_args_file and run the hooks.
2312 removed_cipd_entries = []
2313 if command == 'update':
2314 for dependency in self.dependencies:
2315 gn_args_dep = dependency
2316 if gn_args_dep._gn_args_from:
2317 deps_map = {
2318 dep.name: dep
2319 for dep in gn_args_dep.dependencies
2320 }
2321 gn_args_dep = deps_map.get(gn_args_dep._gn_args_from)
2322 if gn_args_dep and gn_args_dep.HasGNArgsFile():
2323 gn_args_dep.WriteGNArgsFile()
2324
2325 removed_cipd_entries = self._RemoveUnversionedGitDirs()
2326
2327 # Sync CIPD dependencies once removed deps are deleted. In case a git
2328 # dependency was moved to CIPD, we want to remove the old git directory
2329 # first and then sync the CIPD dep.
2330 if self._cipd_root:
2331 self._cipd_root.run(command)
2332 # It's possible that CIPD removed some entries that are now part of
2333 # git worktree. Try to checkout those directories
2334 if removed_cipd_entries:
2335 for cipd_entry in removed_cipd_entries:
2336 cwd = os.path.join(self._root_dir, cipd_entry.split(':')[0])
2337 cwd, tail = os.path.split(cwd)
2338 if cwd:
2339 try:
2340 gclient_scm.scm.GIT.Capture(['checkout', tail],
2341 cwd=cwd)
Joanna Wang60adf7b2023-10-06 00:04:28 +00002342 except (subprocess2.CalledProcessError, OSError):
2343 # repo of the deleted cipd may also have been deleted.
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002344 pass
2345
2346 if not self._options.nohooks:
2347 if should_show_progress:
2348 pm = Progress('Running hooks', 1)
2349 self.RunHooksRecursively(self._options, pm)
2350
2351 self._WriteFileContents(PREVIOUS_SYNC_COMMITS_FILE,
2352 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
2353
2354 return 0
2355
2356 def PrintRevInfo(self):
2357 if not self.dependencies:
2358 raise gclient_utils.Error('No solution specified')
2359 # Load all the settings.
2360 work_queue = gclient_utils.ExecutionQueue(self._options.jobs,
2361 None,
2362 False,
2363 verbose=self._options.verbose)
2364 for s in self.dependencies:
2365 if s.should_process:
2366 work_queue.enqueue(s)
2367 work_queue.flush({},
2368 None, [],
2369 options=self._options,
2370 patch_refs=None,
2371 target_branches=None,
2372 skip_sync_revisions=None)
2373
2374 def ShouldPrintRevision(dep):
2375 return (not self._options.filter
2376 or dep.FuzzyMatchUrl(self._options.filter))
2377
2378 if self._options.snapshot:
2379 json_output = []
2380 # First level at .gclient
2381 for d in self.dependencies:
2382 entries = {}
2383
2384 def GrabDeps(dep):
2385 """Recursively grab dependencies."""
2386 for rec_d in dep.dependencies:
2387 rec_d.PinToActualRevision()
2388 if ShouldPrintRevision(rec_d):
2389 entries[rec_d.name] = rec_d.url
2390 GrabDeps(rec_d)
2391
2392 GrabDeps(d)
2393 json_output.append({
2394 'name': d.name,
2395 'solution_url': d.url,
2396 'deps_file': d.deps_file,
2397 'managed': d.managed,
2398 'custom_deps': entries,
2399 })
2400 if self._options.output_json == '-':
2401 print(json.dumps(json_output, indent=2, separators=(',', ': ')))
2402 elif self._options.output_json:
2403 with open(self._options.output_json, 'w') as f:
2404 json.dump(json_output, f)
2405 else:
2406 # Print the snapshot configuration file
2407 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {
2408 'solution_list': pprint.pformat(json_output, indent=2),
2409 })
Michael Mossd683d7c2018-06-15 05:05:17 +00002410 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002411 entries = {}
2412 for d in self.root.subtree(False):
2413 if self._options.actual:
2414 d.PinToActualRevision()
2415 if ShouldPrintRevision(d):
2416 entries[d.name] = d.url
2417 if self._options.output_json:
2418 json_output = {
2419 name: {
2420 'url': rev.split('@')[0] if rev else None,
2421 'rev':
2422 rev.split('@')[1] if rev and '@' in rev else None,
2423 }
2424 for name, rev in entries.items()
2425 }
2426 if self._options.output_json == '-':
2427 print(
2428 json.dumps(json_output,
2429 indent=2,
2430 separators=(',', ': ')))
2431 else:
2432 with open(self._options.output_json, 'w') as f:
2433 json.dump(json_output, f)
2434 else:
2435 keys = sorted(entries.keys())
2436 for x in keys:
2437 print('%s: %s' % (x, entries[x]))
2438 logging.info(str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002439
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002440 def ParseDepsFile(self):
2441 """No DEPS to parse for a .gclient file."""
2442 raise gclient_utils.Error('Internal error')
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00002443
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002444 def PrintLocationAndContents(self):
2445 # Print out the .gclient file. This is longer than if we just printed
2446 # the client dict, but more legible, and it might contain helpful
2447 # comments.
2448 print('Loaded .gclient config in %s:\n%s' %
2449 (self.root_dir, self.config_content))
sergiyb@chromium.orgfa2707e2016-03-12 00:40:56 +00002450
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002451 def GetCipdRoot(self):
2452 if not self._cipd_root:
2453 self._cipd_root = gclient_scm.CipdRoot(
2454 self.root_dir,
2455 # TODO(jbudorick): Support other service URLs as necessary.
2456 # Service URLs should be constant over the scope of a cipd
2457 # root, so a var per DEPS file specifying the service URL
2458 # should suffice.
Yiwei Zhang52353702023-09-18 15:53:52 +00002459 'https://chrome-infra-packages.appspot.com',
2460 log_level='info' if self._options.verbose else None)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002461 return self._cipd_root
John Budorickd3ba72b2018-03-20 12:27:42 -07002462
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002463 @property
2464 def root_dir(self):
2465 """Root directory of gclient checkout."""
2466 return self._root_dir
maruel@chromium.org75a59272010-06-11 22:34:03 +00002467
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002468 @property
2469 def enforced_os(self):
2470 """What deps_os entries that are to be parsed."""
2471 return self._enforced_os
maruel@chromium.org271375b2010-06-23 19:17:38 +00002472
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002473 @property
2474 def target_os(self):
2475 return self._enforced_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +00002476
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002477 @property
2478 def target_cpu(self):
2479 return self._enforced_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -08002480
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002481
John Budorick0f7b2002018-01-19 15:46:17 -08002482class CipdDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002483 """A Dependency object that represents a single CIPD package."""
2484 def __init__(self, parent, name, dep_value, cipd_root, custom_vars,
2485 should_process, relative, condition):
2486 package = dep_value['package']
2487 version = dep_value['version']
2488 url = urllib.parse.urljoin(cipd_root.service_url,
2489 '%s@%s' % (package, version))
2490 super(CipdDependency, self).__init__(parent=parent,
2491 name=name + ':' + package,
2492 url=url,
2493 managed=None,
2494 custom_deps=None,
2495 custom_vars=custom_vars,
2496 custom_hooks=None,
2497 deps_file=None,
2498 should_process=should_process,
2499 should_recurse=False,
2500 relative=relative,
2501 condition=condition)
2502 self._cipd_package = None
2503 self._cipd_root = cipd_root
2504 # CIPD wants /-separated paths, even on Windows.
2505 native_subdir_path = os.path.relpath(
2506 os.path.join(self.root.root_dir, name), cipd_root.root_dir)
2507 self._cipd_subdir = posixpath.join(*native_subdir_path.split(os.sep))
2508 self._package_name = package
2509 self._package_version = version
John Budorick0f7b2002018-01-19 15:46:17 -08002510
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002511 #override
2512 def run(self, revision_overrides, command, args, work_queue, options,
2513 patch_refs, target_branches, skip_sync_revisions):
2514 """Runs |command| then parse the DEPS file."""
2515 logging.info('CipdDependency(%s).run()' % self.name)
2516 if not self.should_process:
2517 return
2518 self._CreatePackageIfNecessary()
2519 super(CipdDependency,
2520 self).run(revision_overrides, command, args, work_queue, options,
2521 patch_refs, target_branches, skip_sync_revisions)
John Budorickd3ba72b2018-03-20 12:27:42 -07002522
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002523 def _CreatePackageIfNecessary(self):
2524 # We lazily create the CIPD package to make sure that only packages
2525 # that we want (as opposed to all packages defined in all DEPS files
2526 # we parse) get added to the root and subsequently ensured.
2527 if not self._cipd_package:
2528 self._cipd_package = self._cipd_root.add_package(
2529 self._cipd_subdir, self._package_name, self._package_version)
John Budorickd3ba72b2018-03-20 12:27:42 -07002530
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002531 def ParseDepsFile(self):
2532 """CIPD dependencies are not currently allowed to have nested deps."""
2533 self.add_dependencies_and_close([], [])
John Budorick0f7b2002018-01-19 15:46:17 -08002534
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002535 #override
2536 def verify_validity(self):
2537 """CIPD dependencies allow duplicate name for packages in same directory."""
2538 logging.info('Dependency(%s).verify_validity()' % self.name)
2539 return True
John Budorick0f7b2002018-01-19 15:46:17 -08002540
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002541 #override
2542 def GetScmName(self):
2543 """Always 'cipd'."""
2544 return 'cipd'
Shenghua Zhang6f830312018-02-26 11:45:07 -08002545
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002546 def GetExpandedPackageName(self):
2547 """Return the CIPD package name with the variables evaluated."""
2548 package = self._cipd_root.expand_package_name(self._package_name)
2549 if package:
2550 return package
2551 return self._package_name
John Budorick0f7b2002018-01-19 15:46:17 -08002552
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002553 #override
2554 def CreateSCM(self, out_cb=None):
2555 """Create a Wrapper instance suitable for handling this CIPD dependency."""
2556 self._CreatePackageIfNecessary()
2557 return gclient_scm.CipdWrapper(self.url,
2558 self.root.root_dir,
2559 self.name,
2560 self.outbuf,
2561 out_cb,
2562 root=self._cipd_root,
2563 package=self._cipd_package)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +00002564
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002565 def hierarchy(self, include_url=False, graphviz=False):
2566 if graphviz:
2567 return '' # graphviz lines not implemented for cipd deps.
2568 return self.parent.hierarchy(include_url) + ' -> ' + self._cipd_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08002569
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002570 def ToLines(self):
2571 # () -> Sequence[str]
2572 """Return a list of lines representing this in a DEPS file."""
2573 def escape_cipd_var(package):
2574 return package.replace('{', '{{').replace('}', '}}')
Edward Lemure4e15042018-06-28 18:07:00 +00002575
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002576 s = []
2577 self._CreatePackageIfNecessary()
2578 if self._cipd_package.authority_for_subdir:
2579 condition_part = ([' "condition": %r,' %
2580 self.condition] if self.condition else [])
2581 s.extend([
2582 ' # %s' % self.hierarchy(include_url=False),
2583 ' "%s": {' % (self.name.split(':')[0], ),
2584 ' "packages": [',
2585 ])
2586 for p in sorted(self._cipd_root.packages(self._cipd_subdir),
2587 key=lambda x: x.name):
2588 s.extend([
2589 ' {',
2590 ' "package": "%s",' % escape_cipd_var(p.name),
2591 ' "version": "%s",' % p.version,
2592 ' },',
2593 ])
John Budorickc35aba52018-06-28 20:57:03 +00002594
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002595 s.extend([
2596 ' ],',
2597 ' "dep_type": "cipd",',
2598 ] + condition_part + [
2599 ' },',
2600 '',
2601 ])
2602 return s
John Budorick0f7b2002018-01-19 15:46:17 -08002603
2604
maruel@chromium.org5ca27692010-05-26 19:32:41 +00002605#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002606
2607
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002608@subcommand.usage('[command] [args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002609@metrics.collector.collect_metrics('gclient recurse')
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002610def CMDrecurse(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002611 """Operates [command args ...] on all the dependencies.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002612
Arthur Milchior08cd5fe2022-07-28 20:38:47 +00002613 Change directory to each dependency's directory, and call [command
2614 args ...] there. Sets GCLIENT_DEP_PATH environment variable as the
2615 dep's relative location to root directory of the checkout.
2616
2617 Examples:
2618 * `gclient recurse --no-progress -j1 sh -c 'echo "$GCLIENT_DEP_PATH"'`
2619 print the relative path of each dependency.
2620 * `gclient recurse --no-progress -j1 sh -c "pwd"`
2621 print the absolute path of each dependency.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002622 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002623 # Stop parsing at the first non-arg so that these go through to the command
2624 parser.disable_interspersed_args()
2625 parser.add_option('-s',
2626 '--scm',
2627 action='append',
2628 default=[],
2629 help='Choose scm types to operate upon.')
2630 parser.add_option('-i',
2631 '--ignore',
2632 action='store_true',
2633 help='Ignore non-zero return codes from subcommands.')
2634 parser.add_option(
2635 '--prepend-dir',
2636 action='store_true',
2637 help='Prepend relative dir for use with git <cmd> --null.')
2638 parser.add_option(
2639 '--no-progress',
2640 action='store_true',
2641 help='Disable progress bar that shows sub-command updates')
2642 options, args = parser.parse_args(args)
2643 if not args:
2644 print('Need to supply a command!', file=sys.stderr)
2645 return 1
2646 root_and_entries = gclient_utils.GetGClientRootAndEntries()
2647 if not root_and_entries:
2648 print(
2649 'You need to run gclient sync at least once to use \'recurse\'.\n'
2650 'This is because .gclient_entries needs to exist and be up to date.',
2651 file=sys.stderr)
2652 return 1
davidbarr@chromium.org47ca0ee2012-03-02 16:06:11 +00002653
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002654 # Normalize options.scm to a set()
2655 scm_set = set()
2656 for scm in options.scm:
2657 scm_set.update(scm.split(','))
2658 options.scm = scm_set
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002659
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002660 options.nohooks = True
2661 client = GClient.LoadCurrentConfig(options)
2662 if not client:
2663 raise gclient_utils.Error(
2664 'client not configured; see \'gclient config\'')
2665 return client.RunOnDeps('recurse',
2666 args,
2667 ignore_requirements=True,
2668 progress=not options.no_progress)
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002669
2670
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002671@subcommand.usage('[args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002672@metrics.collector.collect_metrics('gclient fetch')
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002673def CMDfetch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002674 """Fetches upstream commits for all modules.
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002675
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002676 Completely git-specific. Simply runs 'git fetch [args ...]' for each module.
2677 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002678 (options, args) = parser.parse_args(args)
2679 return CMDrecurse(
2680 OptionParser(),
2681 ['--jobs=%d' % options.jobs, '--scm=git', 'git', 'fetch'] + args)
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00002682
2683
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002684class Flattener(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002685 """Flattens a gclient solution."""
2686 def __init__(self, client, pin_all_deps=False):
2687 """Constructor.
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002688
2689 Arguments:
2690 client (GClient): client to flatten
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002691 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2692 in DEPS
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002693 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002694 self._client = client
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002695
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002696 self._deps_string = None
2697 self._deps_graph_lines = None
2698 self._deps_files = set()
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002699
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002700 self._allowed_hosts = set()
2701 self._deps = {}
2702 self._hooks = []
2703 self._pre_deps_hooks = []
2704 self._vars = {}
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002705
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002706 self._flatten(pin_all_deps=pin_all_deps)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002707
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002708 @property
2709 def deps_string(self):
2710 assert self._deps_string is not None
2711 return self._deps_string
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002712
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002713 @property
2714 def deps_graph_lines(self):
2715 assert self._deps_graph_lines is not None
2716 return self._deps_graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00002717
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002718 @property
2719 def deps_files(self):
2720 return self._deps_files
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002721
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002722 def _pin_dep(self, dep):
2723 """Pins a dependency to specific full revision sha.
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002724
2725 Arguments:
2726 dep (Dependency): dependency to process
2727 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002728 if dep.url is None:
2729 return
Michael Mossd683d7c2018-06-15 05:05:17 +00002730
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002731 # Make sure the revision is always fully specified (a hash),
2732 # as opposed to refs or tags which might change. Similarly,
2733 # shortened shas might become ambiguous; make sure to always
2734 # use full one for pinning.
2735 revision = gclient_utils.SplitUrlRevision(dep.url)[1]
2736 if not revision or not gclient_utils.IsFullGitSha(revision):
2737 dep.PinToActualRevision()
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002738
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002739 def _flatten(self, pin_all_deps=False):
2740 """Runs the flattener. Saves resulting DEPS string.
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002741
2742 Arguments:
2743 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2744 in DEPS
2745 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002746 for solution in self._client.dependencies:
2747 self._add_dep(solution)
2748 self._flatten_dep(solution)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002749
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002750 if pin_all_deps:
2751 for dep in self._deps.values():
2752 self._pin_dep(dep)
Paweł Hajdan, Jr39300ba2017-08-11 16:52:38 +02002753
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002754 def add_deps_file(dep):
2755 # Only include DEPS files referenced by recursedeps.
2756 if not dep.should_recurse:
2757 return
2758 deps_file = dep.deps_file
2759 deps_path = os.path.join(self._client.root_dir, dep.name, deps_file)
2760 if not os.path.exists(deps_path):
2761 # gclient has a fallback that if deps_file doesn't exist, it'll
2762 # try DEPS. Do the same here.
2763 deps_file = 'DEPS'
2764 deps_path = os.path.join(self._client.root_dir, dep.name,
2765 deps_file)
2766 if not os.path.exists(deps_path):
2767 return
2768 assert dep.url
2769 self._deps_files.add((dep.url, deps_file, dep.hierarchy_data()))
Paweł Hajdan, Jr76a9d042017-08-18 20:05:41 +02002770
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002771 for dep in self._deps.values():
2772 add_deps_file(dep)
Joanna Wang9144b672023-02-24 23:36:17 +00002773
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002774 gn_args_dep = self._deps.get(self._client.dependencies[0]._gn_args_from,
2775 self._client.dependencies[0])
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002776
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002777 self._deps_graph_lines = _DepsToDotGraphLines(self._deps)
2778 self._deps_string = '\n'.join(
2779 _GNSettingsToLines(gn_args_dep._gn_args_file, gn_args_dep._gn_args)
2780 + _AllowedHostsToLines(self._allowed_hosts) +
2781 _DepsToLines(self._deps) + _HooksToLines('hooks', self._hooks) +
2782 _HooksToLines('pre_deps_hooks', self._pre_deps_hooks) +
2783 _VarsToLines(self._vars) + [
2784 '# %s, %s' % (url, deps_file)
2785 for url, deps_file, _ in sorted(self._deps_files)
2786 ] + ['']) # Ensure newline at end of file.
2787
2788 def _add_dep(self, dep):
2789 """Helper to add a dependency to flattened DEPS.
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002790
2791 Arguments:
2792 dep (Dependency): dependency to add
2793 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002794 assert dep.name not in self._deps or self._deps.get(
2795 dep.name) == dep, (dep.name, self._deps.get(dep.name))
2796 if dep.url:
2797 self._deps[dep.name] = dep
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002798
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002799 def _flatten_dep(self, dep):
2800 """Visits a dependency in order to flatten it (see CMDflatten).
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002801
2802 Arguments:
2803 dep (Dependency): dependency to process
2804 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002805 logging.debug('_flatten_dep(%s)', dep.name)
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002806
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002807 assert dep.deps_parsed, (
2808 "Attempted to flatten %s but it has not been processed." % dep.name)
Paweł Hajdan, Jrc69b32e2017-08-17 18:47:48 +02002809
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002810 self._allowed_hosts.update(dep.allowed_hosts)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002811
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002812 # Only include vars explicitly listed in the DEPS files or gclient
2813 # solution, not automatic, local overrides (i.e. not all of
2814 # dep.get_vars()).
2815 hierarchy = dep.hierarchy(include_url=False)
2816 for key, value in dep._vars.items():
2817 # Make sure there are no conflicting variables. It is fine however
2818 # to use same variable name, as long as the value is consistent.
2819 assert key not in self._vars or self._vars[key][1] == value, (
2820 "dep:%s key:%s value:%s != %s" %
2821 (dep.name, key, value, self._vars[key][1]))
2822 self._vars[key] = (hierarchy, value)
2823 # Override explicit custom variables.
2824 for key, value in dep.custom_vars.items():
2825 # Do custom_vars that don't correspond to DEPS vars ever make sense?
2826 # DEPS conditionals shouldn't be using vars that aren't also defined
2827 # in the DEPS (presubmit actually disallows this), so any new
2828 # custom_var must be unused in the DEPS, so no need to add it to the
2829 # flattened output either.
2830 if key not in self._vars:
2831 continue
2832 # Don't "override" existing vars if it's actually the same value.
2833 if self._vars[key][1] == value:
2834 continue
2835 # Anything else is overriding a default value from the DEPS.
2836 self._vars[key] = (hierarchy + ' [custom_var override]', value)
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02002837
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002838 self._pre_deps_hooks.extend([(dep, hook)
2839 for hook in dep.pre_deps_hooks])
2840 self._hooks.extend([(dep, hook) for hook in dep.deps_hooks])
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002841
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002842 for sub_dep in dep.dependencies:
2843 self._add_dep(sub_dep)
Paweł Hajdan, Jrb0ad16e2017-08-03 15:33:21 +02002844
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002845 for d in dep.dependencies:
2846 if d.should_recurse:
2847 self._flatten_dep(d)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002848
2849
Joanna Wang3ab2f212023-08-09 01:25:15 +00002850@metrics.collector.collect_metrics('gclient gitmodules')
2851def CMDgitmodules(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002852 """Adds or updates Git Submodules based on the contents of the DEPS file.
Joanna Wang3ab2f212023-08-09 01:25:15 +00002853
Takuto Ikuta9c44fe52023-11-27 07:18:00 +00002854 This command should be run in the root directory of the repo.
Joanna Wang3ab2f212023-08-09 01:25:15 +00002855 It will create or update the .gitmodules file and include
2856 `gclient-condition` values. Commits in gitlinks will also be updated.
2857 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002858 parser.add_option('--output-gitmodules',
2859 help='name of the .gitmodules file to write to',
2860 default='.gitmodules')
2861 parser.add_option(
2862 '--deps-file',
2863 help=
2864 'name of the deps file to parse for git dependency paths and commits.',
2865 default='DEPS')
2866 parser.add_option(
2867 '--skip-dep',
2868 action="append",
2869 help='skip adding gitmodules for the git dependency at the given path',
2870 default=[])
2871 options, args = parser.parse_args(args)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002872
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002873 deps_dir = os.path.dirname(os.path.abspath(options.deps_file))
2874 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
2875 if not gclient_path:
2876 logging.error(
2877 '.gclient not found\n'
2878 'Make sure you are running this script from a gclient workspace.')
2879 sys.exit(1)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002880
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002881 deps_content = gclient_utils.FileRead(options.deps_file)
2882 ls = gclient_eval.Parse(deps_content, options.deps_file, None, None)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002883
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002884 prefix_length = 0
2885 if not 'use_relative_paths' in ls or ls['use_relative_paths'] != True:
2886 delta_path = os.path.relpath(deps_dir, os.path.abspath(gclient_path))
2887 if delta_path:
2888 prefix_length = len(delta_path.replace(os.path.sep, '/')) + 1
Joanna Wang3ab2f212023-08-09 01:25:15 +00002889
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002890 cache_info = []
Josip Sokcevica309d292023-11-07 19:51:39 +00002891
2892 # Git submodules shouldn't use .git suffix since it's not well supported.
2893 # However, we can't update .gitmodules files since there is no guarantee
2894 # that user has the latest version of depot_tools, and also they are not on
2895 # some old branch which contains already contains submodules with .git.
2896 # This check makes the transition easier.
2897 strip_git_suffix = True
2898 if os.path.exists(options.output_gitmodules):
2899 dot_git_pattern = re.compile('^(\s*)url(\s*)=.*\.git$')
2900 with open(options.output_gitmodules) as f:
2901 strip_git_suffix = not any(dot_git_pattern.match(l) for l in f)
2902
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002903 with open(options.output_gitmodules, 'w', newline='') as f:
2904 for path, dep in ls.get('deps').items():
2905 if path in options.skip_dep:
2906 continue
2907 if dep.get('dep_type') == 'cipd':
2908 continue
2909 try:
2910 url, commit = dep['url'].split('@', maxsplit=1)
2911 except ValueError:
2912 logging.error('error on %s; %s, not adding it', path,
2913 dep["url"])
2914 continue
2915 if prefix_length:
2916 path = path[prefix_length:]
Joanna Wang3ab2f212023-08-09 01:25:15 +00002917
Josip Sokcevica309d292023-11-07 19:51:39 +00002918 if strip_git_suffix:
2919 if url.endswith('.git'):
2920 url = url[:-4] # strip .git
2921 url = url.rstrip('/') # remove trailing slash for consistency
2922
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002923 cache_info += ['--cacheinfo', f'160000,{commit},{path}']
2924 f.write(f'[submodule "{path}"]\n\tpath = {path}\n\turl = {url}\n')
2925 if 'condition' in dep:
2926 f.write(f'\tgclient-condition = {dep["condition"]}\n')
2927 # Windows has limit how long, so let's chunk those calls.
2928 if len(cache_info) >= 100:
2929 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
2930 cache_info = []
2931
2932 if cache_info:
Josip Sokcevic293aa652023-08-23 18:55:20 +00002933 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002934 subprocess2.call(['git', 'add', '.gitmodules'])
Joanna Wang6aed4f52023-10-06 19:12:51 +00002935 print('.gitmodules and gitlinks updated. Please check `git diff --staged`'
2936 'and commit those staged changes (`git commit` without -a)')
Joanna Wang3ab2f212023-08-09 01:25:15 +00002937
2938
Edward Lemur3298e7b2018-07-17 18:21:27 +00002939@metrics.collector.collect_metrics('gclient flatten')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002940def CMDflatten(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002941 """Flattens the solutions into a single DEPS file."""
2942 parser.add_option('--output-deps', help='Path to the output DEPS file')
2943 parser.add_option(
2944 '--output-deps-files',
2945 help=('Path to the output metadata about DEPS files referenced by '
2946 'recursedeps.'))
2947 parser.add_option(
2948 '--pin-all-deps',
2949 action='store_true',
2950 help=('Pin all deps, even if not pinned in DEPS. CAVEAT: only does so '
2951 'for checked out deps, NOT deps_os.'))
2952 parser.add_option('--deps-graph-file',
2953 help='Provide a path for the output graph file')
2954 options, args = parser.parse_args(args)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002955
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002956 options.nohooks = True
2957 options.process_all_deps = True
2958 client = GClient.LoadCurrentConfig(options)
2959 if not client:
2960 raise gclient_utils.Error(
2961 'client not configured; see \'gclient config\'')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002962
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002963 # Only print progress if we're writing to a file. Otherwise, progress
2964 # updates could obscure intended output.
2965 code = client.RunOnDeps('flatten', args, progress=options.output_deps)
2966 if code != 0:
2967 return code
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002968
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002969 flattener = Flattener(client, pin_all_deps=options.pin_all_deps)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002970
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002971 if options.output_deps:
2972 with open(options.output_deps, 'w') as f:
2973 f.write(flattener.deps_string)
2974 else:
2975 print(flattener.deps_string)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002976
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002977 if options.deps_graph_file:
2978 with open(options.deps_graph_file, 'w') as f:
2979 f.write('\n'.join(flattener.deps_graph_lines))
Joanna Wang9144b672023-02-24 23:36:17 +00002980
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002981 deps_files = [{
2982 'url': d[0],
2983 'deps_file': d[1],
2984 'hierarchy': d[2]
2985 } for d in sorted(flattener.deps_files)]
2986 if options.output_deps_files:
2987 with open(options.output_deps_files, 'w') as f:
2988 json.dump(deps_files, f)
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002989
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002990 return 0
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002991
2992
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002993def _GNSettingsToLines(gn_args_file, gn_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002994 s = []
2995 if gn_args_file:
2996 s.extend([
2997 'gclient_gn_args_file = "%s"' % gn_args_file,
2998 'gclient_gn_args = %r' % gn_args,
2999 ])
3000 return s
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02003001
3002
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02003003def _AllowedHostsToLines(allowed_hosts):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003004 """Converts |allowed_hosts| set to list of lines for output."""
3005 if not allowed_hosts:
3006 return []
3007 s = ['allowed_hosts = [']
3008 for h in sorted(allowed_hosts):
3009 s.append(' "%s",' % h)
3010 s.extend([']', ''])
3011 return s
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02003012
3013
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003014def _DepsToLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003015 # type: (Mapping[str, Dependency]) -> Sequence[str]
3016 """Converts |deps| dict to list of lines for output."""
3017 if not deps:
3018 return []
3019 s = ['deps = {']
3020 for _, dep in sorted(deps.items()):
3021 s.extend(dep.ToLines())
3022 s.extend(['}', ''])
3023 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003024
3025
Joanna Wang9144b672023-02-24 23:36:17 +00003026def _DepsToDotGraphLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003027 # type: (Mapping[str, Dependency]) -> Sequence[str]
3028 """Converts |deps| dict to list of lines for dot graphs"""
3029 if not deps:
3030 return []
3031 graph_lines = ["digraph {\n\trankdir=\"LR\";"]
3032 for _, dep in sorted(deps.items()):
3033 line = dep.hierarchy(include_url=False, graphviz=True)
3034 if line:
3035 graph_lines.append("\t%s" % line)
3036 graph_lines.append("}")
3037 return graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00003038
3039
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02003040def _DepsOsToLines(deps_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003041 """Converts |deps_os| dict to list of lines for output."""
3042 if not deps_os:
3043 return []
3044 s = ['deps_os = {']
3045 for dep_os, os_deps in sorted(deps_os.items()):
3046 s.append(' "%s": {' % dep_os)
3047 for name, dep in sorted(os_deps.items()):
3048 condition_part = ([' "condition": %r,' %
3049 dep.condition] if dep.condition else [])
3050 s.extend([
3051 ' # %s' % dep.hierarchy(include_url=False),
3052 ' "%s": {' % (name, ),
3053 ' "url": "%s",' % (dep.url, ),
3054 ] + condition_part + [
3055 ' },',
3056 '',
3057 ])
3058 s.extend([' },', ''])
3059 s.extend(['}', ''])
3060 return s
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02003061
3062
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003063def _HooksToLines(name, hooks):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003064 """Converts |hooks| list to list of lines for output."""
3065 if not hooks:
3066 return []
3067 s = ['%s = [' % name]
3068 for dep, hook in hooks:
3069 s.extend([
3070 ' # %s' % dep.hierarchy(include_url=False),
3071 ' {',
3072 ])
3073 if hook.name is not None:
3074 s.append(' "name": "%s",' % hook.name)
3075 if hook.pattern is not None:
3076 s.append(' "pattern": "%s",' % hook.pattern)
3077 if hook.condition is not None:
3078 s.append(' "condition": %r,' % hook.condition)
3079 # Flattened hooks need to be written relative to the root gclient dir
3080 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3081 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3082 [' "%s",' % arg
3083 for arg in hook.action] + [' ]', ' },', ''])
3084 s.extend([']', ''])
3085 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003086
3087
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003088def _HooksOsToLines(hooks_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003089 """Converts |hooks| list to list of lines for output."""
3090 if not hooks_os:
3091 return []
3092 s = ['hooks_os = {']
3093 for hook_os, os_hooks in hooks_os.items():
3094 s.append(' "%s": [' % hook_os)
3095 for dep, hook in os_hooks:
3096 s.extend([
3097 ' # %s' % dep.hierarchy(include_url=False),
3098 ' {',
3099 ])
3100 if hook.name is not None:
3101 s.append(' "name": "%s",' % hook.name)
3102 if hook.pattern is not None:
3103 s.append(' "pattern": "%s",' % hook.pattern)
3104 if hook.condition is not None:
3105 s.append(' "condition": %r,' % hook.condition)
3106 # Flattened hooks need to be written relative to the root gclient
3107 # dir
3108 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3109 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3110 [' "%s",' % arg
3111 for arg in hook.action] + [' ]', ' },', ''])
3112 s.extend([' ],', ''])
3113 s.extend(['}', ''])
3114 return s
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003115
3116
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003117def _VarsToLines(variables):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003118 """Converts |variables| dict to list of lines for output."""
3119 if not variables:
3120 return []
3121 s = ['vars = {']
3122 for key, tup in sorted(variables.items()):
3123 hierarchy, value = tup
3124 s.extend([
3125 ' # %s' % hierarchy,
3126 ' "%s": %r,' % (key, value),
3127 '',
3128 ])
3129 s.extend(['}', ''])
3130 return s
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003131
3132
Edward Lemur3298e7b2018-07-17 18:21:27 +00003133@metrics.collector.collect_metrics('gclient grep')
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003134def CMDgrep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003135 """Greps through git repos managed by gclient.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003136
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003137 Runs 'git grep [args...]' for each module.
3138 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003139 # We can't use optparse because it will try to parse arguments sent
3140 # to git grep and throw an error. :-(
3141 if not args or re.match('(-h|--help)$', args[0]):
3142 print(
3143 'Usage: gclient grep [-j <N>] git-grep-args...\n\n'
3144 'Example: "gclient grep -j10 -A2 RefCountedBase" runs\n"git grep '
3145 '-A2 RefCountedBase" on each of gclient\'s git\nrepos with up to '
3146 '10 jobs.\n\nBonus: page output by appending "|& less -FRSX" to the'
3147 ' end of your query.',
3148 file=sys.stderr)
3149 return 1
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003150
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003151 jobs_arg = ['--jobs=1']
3152 if re.match(r'(-j|--jobs=)\d+$', args[0]):
3153 jobs_arg, args = args[:1], args[1:]
3154 elif re.match(r'(-j|--jobs)$', args[0]):
3155 jobs_arg, args = args[:2], args[2:]
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003156
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003157 return CMDrecurse(
3158 parser, jobs_arg + [
3159 '--ignore', '--prepend-dir', '--no-progress', '--scm=git', 'git',
3160 'grep', '--null', '--color=Always'
3161 ] + args)
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00003162
3163
Edward Lemur3298e7b2018-07-17 18:21:27 +00003164@metrics.collector.collect_metrics('gclient root')
stip@chromium.orga735da22015-04-29 23:18:20 +00003165def CMDroot(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003166 """Outputs the solution root (or current dir if there isn't one)."""
3167 (options, args) = parser.parse_args(args)
3168 client = GClient.LoadCurrentConfig(options)
3169 if client:
3170 print(os.path.abspath(client.root_dir))
3171 else:
3172 print(os.path.abspath('.'))
stip@chromium.orga735da22015-04-29 23:18:20 +00003173
3174
agablea98a6cd2016-11-15 14:30:10 -08003175@subcommand.usage('[url]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00003176@metrics.collector.collect_metrics('gclient config')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003177def CMDconfig(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003178 """Creates a .gclient file in the current directory.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003179
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003180 This specifies the configuration for further commands. After update/sync,
3181 top-level DEPS files in each module are read to determine dependent
3182 modules to operate on as well. If optional [url] parameter is
3183 provided, then configuration is read from a specified Subversion server
3184 URL.
3185 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003186 # We do a little dance with the --gclientfile option. 'gclient config' is
3187 # the only command where it's acceptable to have both '--gclientfile' and
3188 # '--spec' arguments. So, we temporarily stash any --gclientfile parameter
3189 # into options.output_config_file until after the (gclientfile xor spec)
3190 # error check.
3191 parser.remove_option('--gclientfile')
3192 parser.add_option('--gclientfile',
3193 dest='output_config_file',
3194 help='Specify an alternate .gclient file')
3195 parser.add_option('--name',
3196 help='overrides the default name for the solution')
3197 parser.add_option(
3198 '--deps-file',
3199 default='DEPS',
3200 help='overrides the default name for the DEPS file for the '
3201 'main solutions and all sub-dependencies')
3202 parser.add_option('--unmanaged',
3203 action='store_true',
3204 default=False,
3205 help='overrides the default behavior to make it possible '
3206 'to have the main solution untouched by gclient '
3207 '(gclient will check out unmanaged dependencies but '
3208 'will never sync them)')
3209 parser.add_option('--cache-dir',
3210 default=UNSET_CACHE_DIR,
3211 help='Cache all git repos into this dir and do shared '
3212 'clones from the cache, instead of cloning directly '
3213 'from the remote. Pass "None" to disable cache, even '
3214 'if globally enabled due to $GIT_CACHE_PATH.')
3215 parser.add_option('--custom-var',
3216 action='append',
3217 dest='custom_vars',
3218 default=[],
3219 help='overrides variables; key=value syntax')
3220 parser.set_defaults(config_filename=None)
3221 (options, args) = parser.parse_args(args)
3222 if options.output_config_file:
3223 setattr(options, 'config_filename',
3224 getattr(options, 'output_config_file'))
3225 if ((options.spec and args) or len(args) > 2
3226 or (not options.spec and not args)):
3227 parser.error(
3228 'Inconsistent arguments. Use either --spec or one or 2 args')
maruel@chromium.org5fc2a332010-05-26 19:37:15 +00003229
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003230 if (options.cache_dir is not UNSET_CACHE_DIR
3231 and options.cache_dir.lower() == 'none'):
3232 options.cache_dir = None
Robert Iannuccia19649b2018-06-29 16:31:45 +00003233
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003234 custom_vars = {}
3235 for arg in options.custom_vars:
3236 kv = arg.split('=', 1)
3237 if len(kv) != 2:
3238 parser.error('Invalid --custom-var argument: %r' % arg)
3239 custom_vars[kv[0]] = gclient_eval.EvaluateCondition(kv[1], {})
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02003240
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003241 client = GClient('.', options)
3242 if options.spec:
3243 client.SetConfig(options.spec)
iposva@chromium.org8cf7a392010-04-07 17:20:26 +00003244 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003245 base_url = args[0].rstrip('/')
3246 if not options.name:
3247 name = base_url.split('/')[-1]
3248 if name.endswith('.git'):
3249 name = name[:-4]
3250 else:
3251 # specify an alternate relpath for the given URL.
3252 name = options.name
3253 if not os.path.abspath(os.path.join(os.getcwd(), name)).startswith(
3254 os.getcwd()):
3255 parser.error('Do not pass a relative path for --name.')
3256 if any(x in ('..', '.', '/', '\\') for x in name.split(os.sep)):
3257 parser.error(
3258 'Do not include relative path components in --name.')
agable@chromium.orgf2214672015-10-27 21:02:48 +00003259
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003260 deps_file = options.deps_file
3261 client.SetDefaultConfig(name,
3262 deps_file,
3263 base_url,
3264 managed=not options.unmanaged,
3265 cache_dir=options.cache_dir,
3266 custom_vars=custom_vars)
3267 client.SaveConfig()
3268 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003269
3270
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003271@subcommand.epilog("""Example:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003272 gclient pack > patch.txt
3273 generate simple patch for configured client and dependences
3274""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003275@metrics.collector.collect_metrics('gclient pack')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003276def CMDpack(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003277 """Generates a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003278
agabled437d762016-10-17 09:35:11 -07003279 Internally, runs 'git diff' on each checked out module and
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003280 dependencies, and performs minimal postprocessing of the output. The
3281 resulting patch is printed to stdout and can be applied to a freshly
3282 checked out tree via 'patch -p0 < patchfile'.
3283 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003284 parser.add_option('--deps',
3285 dest='deps_os',
3286 metavar='OS_LIST',
3287 help='override deps for the specified (comma-separated) '
3288 'platform(s); \'all\' will process all deps_os '
3289 'references')
3290 parser.remove_option('--jobs')
3291 (options, args) = parser.parse_args(args)
3292 # Force jobs to 1 so the stdout is not annotated with the thread ids
3293 options.jobs = 1
3294 client = GClient.LoadCurrentConfig(options)
3295 if not client:
3296 raise gclient_utils.Error(
3297 'client not configured; see \'gclient config\'')
3298 if options.verbose:
3299 client.PrintLocationAndContents()
3300 return client.RunOnDeps('pack', args)
kbr@google.comab318592009-09-04 00:54:55 +00003301
3302
Edward Lemur3298e7b2018-07-17 18:21:27 +00003303@metrics.collector.collect_metrics('gclient status')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003304def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003305 """Shows modification status for every dependencies."""
3306 parser.add_option('--deps',
3307 dest='deps_os',
3308 metavar='OS_LIST',
3309 help='override deps for the specified (comma-separated) '
3310 'platform(s); \'all\' will process all deps_os '
3311 'references')
3312 (options, args) = parser.parse_args(args)
3313 client = GClient.LoadCurrentConfig(options)
3314 if not client:
3315 raise gclient_utils.Error(
3316 'client not configured; see \'gclient config\'')
3317 if options.verbose:
3318 client.PrintLocationAndContents()
3319 return client.RunOnDeps('status', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003320
3321
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003322@subcommand.epilog("""Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +00003323 gclient sync
3324 update files from SCM according to current configuration,
3325 *for modules which have changed since last update or sync*
3326 gclient sync --force
3327 update files from SCM according to current configuration, for
3328 all modules (useful for recovering files deleted from local copy)
Edward Lesmes3ffca4b2021-05-19 19:36:17 +00003329 gclient sync --revision src@GIT_COMMIT_OR_REF
3330 update src directory to GIT_COMMIT_OR_REF
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003331
3332JSON output format:
3333If the --output-json option is specified, the following document structure will
3334be emitted to the provided file. 'null' entries may occur for subprojects which
3335are present in the gclient solution, but were not processed (due to custom_deps,
3336os_deps, etc.)
3337
3338{
3339 "solutions" : {
3340 "<name>": { # <name> is the posix-normalized path to the solution.
agabled437d762016-10-17 09:35:11 -07003341 "revision": [<git id hex string>|null],
3342 "scm": ["git"|null],
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003343 }
3344 }
3345}
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003346""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003347@metrics.collector.collect_metrics('gclient sync')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003348def CMDsync(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003349 """Checkout/update all modules."""
3350 parser.add_option('-f',
3351 '--force',
3352 action='store_true',
3353 help='force update even for unchanged modules')
3354 parser.add_option('-n',
3355 '--nohooks',
3356 action='store_true',
3357 help='don\'t run hooks after the update is complete')
3358 parser.add_option('-p',
3359 '--noprehooks',
3360 action='store_true',
3361 help='don\'t run pre-DEPS hooks',
3362 default=False)
3363 parser.add_option('-r',
3364 '--revision',
3365 action='append',
3366 dest='revisions',
3367 metavar='REV',
3368 default=[],
3369 help='Enforces git ref/hash for the solutions with the '
3370 'format src@rev. The src@ part is optional and can be '
3371 'skipped. You can also specify URLs instead of paths '
3372 'and gclient will find the solution corresponding to '
3373 'the given URL. If a path is also specified, the URL '
3374 'takes precedence. -r can be used multiple times when '
3375 '.gclient has multiple solutions configured, and will '
3376 'work even if the src@ part is skipped. Revision '
3377 'numbers (e.g. 31000 or r31000) are not supported.')
3378 parser.add_option('--patch-ref',
3379 action='append',
3380 dest='patch_refs',
3381 metavar='GERRIT_REF',
3382 default=[],
3383 help='Patches the given reference with the format '
3384 'dep@target-ref:patch-ref. '
3385 'For |dep|, you can specify URLs as well as paths, '
3386 'with URLs taking preference. '
3387 '|patch-ref| will be applied to |dep|, rebased on top '
3388 'of what |dep| was synced to, and a soft reset will '
3389 'be done. Use --no-rebase-patch-ref and '
3390 '--no-reset-patch-ref to disable this behavior. '
3391 '|target-ref| is the target branch against which a '
3392 'patch was created, it is used to determine which '
3393 'commits from the |patch-ref| actually constitute a '
3394 'patch.')
3395 parser.add_option(
3396 '-t',
3397 '--download-topics',
3398 action='store_true',
3399 help='Downloads and patches locally changes from all open '
3400 'Gerrit CLs that have the same topic as the changes '
3401 'in the specified patch_refs. Only works if atleast '
3402 'one --patch-ref is specified.')
3403 parser.add_option('--with_branch_heads',
3404 action='store_true',
3405 help='Clone git "branch_heads" refspecs in addition to '
3406 'the default refspecs. This adds about 1/2GB to a '
3407 'full checkout. (git only)')
3408 parser.add_option(
3409 '--with_tags',
3410 action='store_true',
3411 help='Clone git tags in addition to the default refspecs.')
3412 parser.add_option('-H',
3413 '--head',
3414 action='store_true',
3415 help='DEPRECATED: only made sense with safesync urls.')
3416 parser.add_option(
3417 '-D',
3418 '--delete_unversioned_trees',
3419 action='store_true',
3420 help='Deletes from the working copy any dependencies that '
3421 'have been removed since the last sync, as long as '
3422 'there are no local modifications. When used with '
3423 '--force, such dependencies are removed even if they '
3424 'have local modifications. When used with --reset, '
3425 'all untracked directories are removed from the '
3426 'working copy, excluding those which are explicitly '
3427 'ignored in the repository.')
3428 parser.add_option(
3429 '-R',
3430 '--reset',
3431 action='store_true',
3432 help='resets any local changes before updating (git only)')
3433 parser.add_option('-M',
3434 '--merge',
3435 action='store_true',
3436 help='merge upstream changes instead of trying to '
3437 'fast-forward or rebase')
3438 parser.add_option('-A',
3439 '--auto_rebase',
3440 action='store_true',
3441 help='Automatically rebase repositories against local '
3442 'checkout during update (git only).')
3443 parser.add_option('--deps',
3444 dest='deps_os',
3445 metavar='OS_LIST',
3446 help='override deps for the specified (comma-separated) '
3447 'platform(s); \'all\' will process all deps_os '
3448 'references')
3449 parser.add_option('--process-all-deps',
3450 action='store_true',
3451 help='Check out all deps, even for different OS-es, '
3452 'or with conditions evaluating to false')
3453 parser.add_option('--upstream',
3454 action='store_true',
3455 help='Make repo state match upstream branch.')
3456 parser.add_option('--output-json',
3457 help='Output a json document to this path containing '
3458 'summary information about the sync.')
3459 parser.add_option(
3460 '--no-history',
3461 action='store_true',
3462 help='GIT ONLY - Reduces the size/time of the checkout at '
3463 'the cost of no history. Requires Git 1.9+')
3464 parser.add_option('--shallow',
3465 action='store_true',
3466 help='GIT ONLY - Do a shallow clone into the cache dir. '
3467 'Requires Git 1.9+')
3468 parser.add_option('--no_bootstrap',
3469 '--no-bootstrap',
3470 action='store_true',
3471 help='Don\'t bootstrap from Google Storage.')
3472 parser.add_option('--ignore_locks',
3473 action='store_true',
3474 help='No longer used.')
3475 parser.add_option('--break_repo_locks',
3476 action='store_true',
3477 help='No longer used.')
3478 parser.add_option('--lock_timeout',
3479 type='int',
3480 default=5000,
3481 help='GIT ONLY - Deadline (in seconds) to wait for git '
3482 'cache lock to become available. Default is %default.')
3483 parser.add_option('--no-rebase-patch-ref',
3484 action='store_false',
3485 dest='rebase_patch_ref',
3486 default=True,
3487 help='Bypass rebase of the patch ref after checkout.')
3488 parser.add_option('--no-reset-patch-ref',
3489 action='store_false',
3490 dest='reset_patch_ref',
3491 default=True,
3492 help='Bypass calling reset after patching the ref.')
3493 parser.add_option('--experiment',
3494 action='append',
3495 dest='experiments',
3496 default=[],
3497 help='Which experiments should be enabled.')
3498 (options, args) = parser.parse_args(args)
3499 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003500
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003501 if not client:
3502 raise gclient_utils.Error(
3503 'client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003504
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003505 if options.download_topics and not options.rebase_patch_ref:
3506 raise gclient_utils.Error(
3507 'Warning: You cannot download topics and not rebase each patch ref')
Ravi Mistryecda7822022-02-28 16:22:20 +00003508
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003509 if options.ignore_locks:
3510 print(
3511 'Warning: ignore_locks is no longer used. Please remove its usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003512
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003513 if options.break_repo_locks:
3514 print('Warning: break_repo_locks is no longer used. Please remove its '
3515 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003516
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003517 if options.revisions and options.head:
3518 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
3519 print('Warning: you cannot use both --head and --revision')
smutae7ea312016-07-18 11:59:41 -07003520
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003521 if options.verbose:
3522 client.PrintLocationAndContents()
3523 ret = client.RunOnDeps('update', args)
3524 if options.output_json:
3525 slns = {}
3526 for d in client.subtree(True):
3527 normed = d.name.replace('\\', '/').rstrip('/') + '/'
3528 slns[normed] = {
3529 'revision': d.got_revision,
3530 'scm': d.used_scm.name if d.used_scm else None,
3531 'url': str(d.url) if d.url else None,
3532 'was_processed': d.should_process,
3533 'was_synced': d._should_sync,
3534 }
3535 with open(options.output_json, 'w') as f:
3536 json.dump({'solutions': slns}, f)
3537 return ret
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003538
3539
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003540CMDupdate = CMDsync
3541
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003542
Edward Lemur3298e7b2018-07-17 18:21:27 +00003543@metrics.collector.collect_metrics('gclient validate')
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003544def CMDvalidate(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003545 """Validates the .gclient and DEPS syntax."""
3546 options, args = parser.parse_args(args)
3547 client = GClient.LoadCurrentConfig(options)
3548 if not client:
3549 raise gclient_utils.Error(
3550 'client not configured; see \'gclient config\'')
3551 rv = client.RunOnDeps('validate', args)
3552 if rv == 0:
3553 print('validate: SUCCESS')
3554 else:
3555 print('validate: FAILURE')
3556 return rv
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003557
3558
Edward Lemur3298e7b2018-07-17 18:21:27 +00003559@metrics.collector.collect_metrics('gclient diff')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003560def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003561 """Displays local diff for every dependencies."""
3562 parser.add_option('--deps',
3563 dest='deps_os',
3564 metavar='OS_LIST',
3565 help='override deps for the specified (comma-separated) '
3566 'platform(s); \'all\' will process all deps_os '
3567 'references')
3568 (options, args) = parser.parse_args(args)
3569 client = GClient.LoadCurrentConfig(options)
3570 if not client:
3571 raise gclient_utils.Error(
3572 'client not configured; see \'gclient config\'')
3573 if options.verbose:
3574 client.PrintLocationAndContents()
3575 return client.RunOnDeps('diff', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003576
3577
Edward Lemur3298e7b2018-07-17 18:21:27 +00003578@metrics.collector.collect_metrics('gclient revert')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003579def CMDrevert(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003580 """Reverts all modifications in every dependencies.
maruel@chromium.org28d14bd2010-11-11 20:37:09 +00003581
3582 That's the nuclear option to get back to a 'clean' state. It removes anything
agabled437d762016-10-17 09:35:11 -07003583 that shows up in git status."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003584 parser.add_option('--deps',
3585 dest='deps_os',
3586 metavar='OS_LIST',
3587 help='override deps for the specified (comma-separated) '
3588 'platform(s); \'all\' will process all deps_os '
3589 'references')
3590 parser.add_option('-n',
3591 '--nohooks',
3592 action='store_true',
3593 help='don\'t run hooks after the revert is complete')
3594 parser.add_option('-p',
3595 '--noprehooks',
3596 action='store_true',
3597 help='don\'t run pre-DEPS hooks',
3598 default=False)
3599 parser.add_option('--upstream',
3600 action='store_true',
3601 help='Make repo state match upstream branch.')
3602 parser.add_option('--break_repo_locks',
3603 action='store_true',
3604 help='No longer used.')
3605 (options, args) = parser.parse_args(args)
3606 if options.break_repo_locks:
3607 print(
3608 'Warning: break_repo_locks is no longer used. Please remove its ' +
3609 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003610
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003611 # --force is implied.
3612 options.force = True
3613 options.reset = False
3614 options.delete_unversioned_trees = False
3615 options.merge = False
3616 client = GClient.LoadCurrentConfig(options)
3617 if not client:
3618 raise gclient_utils.Error(
3619 'client not configured; see \'gclient config\'')
3620 return client.RunOnDeps('revert', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003621
3622
Edward Lemur3298e7b2018-07-17 18:21:27 +00003623@metrics.collector.collect_metrics('gclient runhooks')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003624def CMDrunhooks(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003625 """Runs hooks for files that have been modified in the local working copy."""
3626 parser.add_option('--deps',
3627 dest='deps_os',
3628 metavar='OS_LIST',
3629 help='override deps for the specified (comma-separated) '
3630 'platform(s); \'all\' will process all deps_os '
3631 'references')
3632 parser.add_option('-f',
3633 '--force',
3634 action='store_true',
3635 default=True,
3636 help='Deprecated. No effect.')
3637 (options, args) = parser.parse_args(args)
3638 client = GClient.LoadCurrentConfig(options)
3639 if not client:
3640 raise gclient_utils.Error(
3641 'client not configured; see \'gclient config\'')
3642 if options.verbose:
3643 client.PrintLocationAndContents()
3644 options.force = True
3645 options.nohooks = False
3646 return client.RunOnDeps('runhooks', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003647
3648
Gavin Mak50b27a52023-09-19 22:44:59 +00003649# TODO(crbug.com/1481266): Collect merics for installhooks.
3650def CMDinstallhooks(parser, args):
3651 """Installs gclient git hooks.
3652
3653 Currently only installs a pre-commit hook to drop staged gitlinks. To
3654 bypass this pre-commit hook once it's installed, set the environment
3655 variable SKIP_GITLINK_PRECOMMIT=1.
3656 """
3657 (options, args) = parser.parse_args(args)
3658 client = GClient.LoadCurrentConfig(options)
3659 if not client:
3660 raise gclient_utils.Error(
3661 'client not configured; see \'gclient config\'')
3662 client._InstallPreCommitHook()
3663 return 0
3664
3665
Edward Lemur3298e7b2018-07-17 18:21:27 +00003666@metrics.collector.collect_metrics('gclient revinfo')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003667def CMDrevinfo(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003668 """Outputs revision info mapping for the client and its dependencies.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003669
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00003670 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003671 can be used to reproduce the same tree in the future. It is only useful for
agabled437d762016-10-17 09:35:11 -07003672 'unpinned dependencies', i.e. DEPS/deps references without a git hash.
3673 A git branch name isn't 'pinned' since the actual commit can change.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003674 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003675 parser.add_option('--deps',
3676 dest='deps_os',
3677 metavar='OS_LIST',
3678 help='override deps for the specified (comma-separated) '
3679 'platform(s); \'all\' will process all deps_os '
3680 'references')
3681 parser.add_option(
3682 '-a',
3683 '--actual',
3684 action='store_true',
3685 help='gets the actual checked out revisions instead of the '
3686 'ones specified in the DEPS and .gclient files')
3687 parser.add_option('-s',
3688 '--snapshot',
3689 action='store_true',
3690 help='creates a snapshot .gclient file of the current '
3691 'version of all repositories to reproduce the tree, '
3692 'implies -a')
3693 parser.add_option(
3694 '--filter',
3695 action='append',
3696 dest='filter',
3697 help='Display revision information only for the specified '
3698 'dependencies (filtered by URL or path).')
3699 parser.add_option('--output-json',
3700 help='Output a json document to this path containing '
3701 'information about the revisions.')
3702 parser.add_option(
3703 '--ignore-dep-type',
3704 choices=['git', 'cipd'],
3705 help='Specify to skip processing of a certain type of dep.')
3706 (options, args) = parser.parse_args(args)
3707 client = GClient.LoadCurrentConfig(options)
3708 if not client:
3709 raise gclient_utils.Error(
3710 'client not configured; see \'gclient config\'')
3711 client.PrintRevInfo()
3712 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003713
3714
Edward Lemur3298e7b2018-07-17 18:21:27 +00003715@metrics.collector.collect_metrics('gclient getdep')
Edward Lesmes411041f2018-04-05 20:12:55 -04003716def CMDgetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003717 """Gets revision information and variable values from a DEPS file.
Josip Sokcevic7b5e3d72023-06-13 00:28:23 +00003718
3719 If key doesn't exist or is incorrectly declared, this script exits with exit
3720 code 2."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003721 parser.add_option('--var',
3722 action='append',
3723 dest='vars',
3724 metavar='VAR',
3725 default=[],
3726 help='Gets the value of a given variable.')
3727 parser.add_option(
3728 '-r',
3729 '--revision',
3730 action='append',
3731 dest='getdep_revisions',
3732 metavar='DEP',
3733 default=[],
3734 help='Gets the revision/version for the given dependency. '
3735 'If it is a git dependency, dep must be a path. If it '
3736 'is a CIPD dependency, dep must be of the form '
3737 'path:package.')
3738 parser.add_option(
3739 '--deps-file',
3740 default='DEPS',
3741 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3742 # .gclient first.
3743 help='The DEPS file to be edited. Defaults to the DEPS '
3744 'file in the current directory.')
3745 (options, args) = parser.parse_args(args)
Edward Lesmes411041f2018-04-05 20:12:55 -04003746
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003747 if not os.path.isfile(options.deps_file):
3748 raise gclient_utils.Error('DEPS file %s does not exist.' %
3749 options.deps_file)
3750 with open(options.deps_file) as f:
3751 contents = f.read()
3752 client = GClient.LoadCurrentConfig(options)
3753 if client is not None:
3754 builtin_vars = client.get_builtin_vars()
Edward Lesmes411041f2018-04-05 20:12:55 -04003755 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003756 logging.warning(
3757 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3758 'file without support for built-in variables.')
3759 builtin_vars = None
3760 local_scope = gclient_eval.Exec(contents,
3761 options.deps_file,
3762 builtin_vars=builtin_vars)
3763
3764 for var in options.vars:
3765 print(gclient_eval.GetVar(local_scope, var))
3766
3767 commits = {}
3768 if local_scope.get(
3769 'git_dependencies'
3770 ) == gclient_eval.SUBMODULES and options.getdep_revisions:
3771 commits.update(
3772 scm_git.GIT.GetSubmoduleCommits(
3773 os.getcwd(),
3774 [path for path in options.getdep_revisions if ':' not in path]))
3775
3776 for name in options.getdep_revisions:
3777 if ':' in name:
3778 name, _, package = name.partition(':')
3779 if not name or not package:
3780 parser.error(
3781 'Wrong CIPD format: %s:%s should be of the form path:pkg.' %
3782 (name, package))
3783 print(gclient_eval.GetCIPD(local_scope, name, package))
3784 elif commits:
3785 print(commits[name])
3786 else:
3787 try:
3788 print(gclient_eval.GetRevision(local_scope, name))
3789 except KeyError as e:
3790 print(repr(e), file=sys.stderr)
3791 sys.exit(2)
Edward Lesmes411041f2018-04-05 20:12:55 -04003792
3793
Edward Lemur3298e7b2018-07-17 18:21:27 +00003794@metrics.collector.collect_metrics('gclient setdep')
Edward Lesmes6f64a052018-03-20 17:35:49 -04003795def CMDsetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003796 """Modifies dependency revisions and variable values in a DEPS file"""
3797 parser.add_option('--var',
3798 action='append',
3799 dest='vars',
3800 metavar='VAR=VAL',
3801 default=[],
3802 help='Sets a variable to the given value with the format '
3803 'name=value.')
3804 parser.add_option('-r',
3805 '--revision',
3806 action='append',
3807 dest='setdep_revisions',
3808 metavar='DEP@REV',
3809 default=[],
3810 help='Sets the revision/version for the dependency with '
3811 'the format dep@rev. If it is a git dependency, dep '
3812 'must be a path and rev must be a git hash or '
3813 'reference (e.g. src/dep@deadbeef). If it is a CIPD '
3814 'dependency, dep must be of the form path:package and '
3815 'rev must be the package version '
3816 '(e.g. src/pkg:chromium/pkg@2.1-cr0).')
3817 parser.add_option(
3818 '--deps-file',
3819 default='DEPS',
3820 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3821 # .gclient first.
3822 help='The DEPS file to be edited. Defaults to the DEPS '
3823 'file in the current directory.')
3824 (options, args) = parser.parse_args(args)
3825 if args:
3826 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3827 if not options.setdep_revisions and not options.vars:
Edward Lesmes0ecf6d62018-04-05 18:28:55 -04003828 parser.error(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003829 'You must specify at least one variable or revision to modify.')
3830
3831 if not os.path.isfile(options.deps_file):
3832 raise gclient_utils.Error('DEPS file %s does not exist.' %
3833 options.deps_file)
3834 with open(options.deps_file) as f:
3835 contents = f.read()
3836
3837 client = GClient.LoadCurrentConfig(options)
3838 if client is not None:
3839 builtin_vars = client.get_builtin_vars()
Edward Lesmes6f64a052018-03-20 17:35:49 -04003840 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003841 logging.warning(
3842 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3843 'file without support for built-in variables.')
3844 builtin_vars = None
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003845
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003846 local_scope = gclient_eval.Exec(contents,
3847 options.deps_file,
3848 builtin_vars=builtin_vars)
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003849
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003850 # Create a set of all git submodules.
3851 cwd = os.path.dirname(options.deps_file) or os.getcwd()
3852 git_modules = None
3853 if 'git_dependencies' in local_scope and local_scope[
3854 'git_dependencies'] in (gclient_eval.SUBMODULES, gclient_eval.SYNC):
3855 try:
3856 submodule_status = subprocess2.check_output(
3857 ['git', 'submodule', 'status'], cwd=cwd).decode('utf-8')
3858 git_modules = {l.split()[1] for l in submodule_status.splitlines()}
3859 except subprocess2.CalledProcessError as e:
3860 print('Warning: gitlinks won\'t be updated: ', e)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003861
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003862 for var in options.vars:
3863 name, _, value = var.partition('=')
3864 if not name or not value:
3865 parser.error(
3866 'Wrong var format: %s should be of the form name=value.' % var)
3867 if name in local_scope['vars']:
3868 gclient_eval.SetVar(local_scope, name, value)
3869 else:
3870 gclient_eval.AddVar(local_scope, name, value)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003871
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003872 for revision in options.setdep_revisions:
3873 name, _, value = revision.partition('@')
3874 if not name or not value:
3875 parser.error('Wrong dep format: %s should be of the form dep@rev.' %
3876 revision)
3877 if ':' in name:
3878 name, _, package = name.partition(':')
3879 if not name or not package:
3880 parser.error(
3881 'Wrong CIPD format: %s:%s should be of the form path:pkg@version.'
3882 % (name, package))
3883 gclient_eval.SetCIPD(local_scope, name, package, value)
3884 else:
3885 # Update DEPS only when `git_dependencies` == DEPS or SYNC.
3886 # git_dependencies is defaulted to DEPS when not set.
3887 if 'git_dependencies' not in local_scope or local_scope[
3888 'git_dependencies'] in (gclient_eval.DEPS,
3889 gclient_eval.SYNC):
3890 gclient_eval.SetRevision(local_scope, name, value)
3891
3892 # Update git submodules when `git_dependencies` == SYNC or
3893 # SUBMODULES.
3894 if git_modules and 'git_dependencies' in local_scope and local_scope[
3895 'git_dependencies'] in (gclient_eval.SUBMODULES,
3896 gclient_eval.SYNC):
3897 git_module_name = name
3898 if not 'use_relative_paths' in local_scope or \
3899 local_scope['use_relative_paths'] != True:
3900 deps_dir = os.path.dirname(
3901 os.path.abspath(options.deps_file))
3902 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
3903 delta_path = None
3904 if gclient_path:
3905 delta_path = os.path.relpath(
3906 deps_dir, os.path.abspath(gclient_path))
3907 if delta_path:
3908 prefix_length = len(delta_path.replace(
3909 os.path.sep, '/')) + 1
3910 git_module_name = name[prefix_length:]
3911 # gclient setdep should update the revision, i.e., the gitlink
3912 # only when the submodule entry is already present within
3913 # .gitmodules.
3914 if git_module_name not in git_modules:
3915 raise KeyError(
3916 f'Could not find any dependency called "{git_module_name}" in '
3917 f'.gitmodules.')
3918
3919 # Update the gitlink for the submodule.
3920 subprocess2.call([
3921 'git', 'update-index', '--add', '--cacheinfo',
3922 f'160000,{value},{git_module_name}'
3923 ],
3924 cwd=cwd)
3925
3926 with open(options.deps_file, 'wb') as f:
3927 f.write(gclient_eval.RenderDEPSFile(local_scope).encode('utf-8'))
3928
3929 if git_modules:
3930 subprocess2.call(['git', 'add', options.deps_file], cwd=cwd)
3931 print('Changes have been staged. See changes with `git status`.\n'
3932 'Use `git commit -m "Manual roll"` to commit your changes. \n'
3933 'Run gclient sync to update your local dependency checkout.')
Josip Sokcevic5561f8b2023-08-21 16:00:42 +00003934
Edward Lesmes6f64a052018-03-20 17:35:49 -04003935
Edward Lemur3298e7b2018-07-17 18:21:27 +00003936@metrics.collector.collect_metrics('gclient verify')
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003937def CMDverify(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003938 """Verifies the DEPS file deps are only from allowed_hosts."""
3939 (options, args) = parser.parse_args(args)
3940 client = GClient.LoadCurrentConfig(options)
3941 if not client:
3942 raise gclient_utils.Error(
3943 'client not configured; see \'gclient config\'')
3944 client.RunOnDeps(None, [])
3945 # Look at each first-level dependency of this gclient only.
3946 for dep in client.dependencies:
3947 bad_deps = dep.findDepsFromNotAllowedHosts()
3948 if not bad_deps:
3949 continue
3950 print("There are deps from not allowed hosts in file %s" %
3951 dep.deps_file)
3952 for bad_dep in bad_deps:
3953 print("\t%s at %s" % (bad_dep.name, bad_dep.url))
3954 print("allowed_hosts:", ', '.join(dep.allowed_hosts))
3955 sys.stdout.flush()
3956 raise gclient_utils.Error(
3957 'dependencies from disallowed hosts; check your DEPS file.')
3958 return 0
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003959
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003960
3961@subcommand.epilog("""For more information on what metrics are we collecting and
Edward Lemur8a2e3312018-07-12 21:15:09 +00003962why, please read metrics.README.md or visit https://bit.ly/2ufRS4p""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003963@metrics.collector.collect_metrics('gclient metrics')
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003964def CMDmetrics(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003965 """Reports, and optionally modifies, the status of metric collection."""
3966 parser.add_option('--opt-in',
3967 action='store_true',
3968 dest='enable_metrics',
3969 help='Opt-in to metrics collection.',
3970 default=None)
3971 parser.add_option('--opt-out',
3972 action='store_false',
3973 dest='enable_metrics',
3974 help='Opt-out of metrics collection.')
3975 options, args = parser.parse_args(args)
3976 if args:
3977 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3978 if not metrics.collector.config.is_googler:
3979 print("You're not a Googler. Metrics collection is disabled for you.")
3980 return 0
3981
3982 if options.enable_metrics is not None:
3983 metrics.collector.config.opted_in = options.enable_metrics
3984
3985 if metrics.collector.config.opted_in is None:
3986 print("You haven't opted in or out of metrics collection.")
3987 elif metrics.collector.config.opted_in:
3988 print("You have opted in. Thanks!")
3989 else:
3990 print("You have opted out. Please consider opting in.")
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003991 return 0
3992
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003993
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003994class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003995 gclientfile_default = os.environ.get('GCLIENT_FILE', '.gclient')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003996
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003997 def __init__(self, **kwargs):
3998 optparse.OptionParser.__init__(self,
3999 version='%prog ' + __version__,
4000 **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004001
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004002 # Some arm boards have issues with parallel sync.
4003 if platform.machine().startswith('arm'):
4004 jobs = 1
4005 else:
4006 jobs = max(8, gclient_utils.NumLocalCpus())
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004007
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004008 self.add_option(
4009 '-j',
4010 '--jobs',
4011 default=jobs,
4012 type='int',
4013 help='Specify how many SCM commands can run in parallel; defaults to '
4014 '%default on this machine')
4015 self.add_option(
4016 '-v',
4017 '--verbose',
4018 action='count',
4019 default=0,
4020 help='Produces additional output for diagnostics. Can be used up to '
4021 'three times for more logging info.')
4022 self.add_option('--gclientfile',
4023 dest='config_filename',
4024 help='Specify an alternate %s file' %
4025 self.gclientfile_default)
4026 self.add_option(
4027 '--spec',
4028 help='create a gclient file containing the provided string. Due to '
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004029 'Cygwin/Python brokenness, it can\'t contain any newlines.')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004030 self.add_option('--no-nag-max',
4031 default=False,
4032 action='store_true',
4033 help='Ignored for backwards compatibility.')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004034
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004035 def parse_args(self, args=None, _values=None):
4036 """Integrates standard options processing."""
4037 # Create an optparse.Values object that will store only the actual
4038 # passed options, without the defaults.
4039 actual_options = optparse.Values()
4040 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
4041 # Create an optparse.Values object with the default options.
4042 options = optparse.Values(self.get_default_values().__dict__)
4043 # Update it with the options passed by the user.
4044 options._update_careful(actual_options.__dict__)
4045 # Store the options passed by the user in an _actual_options attribute.
4046 # We store only the keys, and not the values, since the values can
4047 # contain arbitrary information, which might be PII.
4048 metrics.collector.add('arguments', list(actual_options.__dict__))
Edward Lemur3298e7b2018-07-17 18:21:27 +00004049
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004050 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
4051 logging.basicConfig(
4052 level=levels[min(options.verbose,
4053 len(levels) - 1)],
4054 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
4055 if options.config_filename and options.spec:
4056 self.error('Cannot specify both --gclientfile and --spec')
4057 if (options.config_filename and options.config_filename !=
4058 os.path.basename(options.config_filename)):
4059 self.error('--gclientfile target must be a filename, not a path')
4060 if not options.config_filename:
4061 options.config_filename = self.gclientfile_default
4062 options.entries_filename = options.config_filename + '_entries'
4063 if options.jobs < 1:
4064 self.error('--jobs must be 1 or higher')
maruel@chromium.org0895b752011-08-26 20:40:33 +00004065
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004066 # These hacks need to die.
4067 if not hasattr(options, 'revisions'):
4068 # GClient.RunOnDeps expects it even if not applicable.
4069 options.revisions = []
4070 if not hasattr(options, 'experiments'):
4071 options.experiments = []
4072 if not hasattr(options, 'head'):
4073 options.head = None
4074 if not hasattr(options, 'nohooks'):
4075 options.nohooks = True
4076 if not hasattr(options, 'noprehooks'):
4077 options.noprehooks = True
4078 if not hasattr(options, 'deps_os'):
4079 options.deps_os = None
4080 if not hasattr(options, 'force'):
4081 options.force = None
4082 return (options, args)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004083
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004084
4085def disable_buffering():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004086 # Make stdout auto-flush so buildbot doesn't kill us during lengthy
4087 # operations. Python as a strong tendency to buffer sys.stdout.
4088 sys.stdout = gclient_utils.MakeFileAutoFlush(sys.stdout)
4089 # Make stdout annotated with the thread ids.
4090 sys.stdout = gclient_utils.MakeFileAnnotated(sys.stdout)
maruel@chromium.org0895b752011-08-26 20:40:33 +00004091
4092
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004093def path_contains_tilde():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004094 for element in os.environ['PATH'].split(os.pathsep):
4095 if element.startswith('~') and os.path.abspath(
4096 os.path.realpath(
4097 os.path.expanduser(element))) == DEPOT_TOOLS_DIR:
4098 return True
4099 return False
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004100
4101
4102def can_run_gclient_and_helpers():
Gavin Mak7f5b53f2023-09-07 18:13:01 +00004103 if sys.version_info[0] < 3:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004104 print('\nYour python version %s is unsupported, please upgrade.\n' %
4105 sys.version.split(' ', 1)[0],
4106 file=sys.stderr)
4107 return False
4108 if not sys.executable:
4109 print('\nPython cannot find the location of it\'s own executable.\n',
4110 file=sys.stderr)
4111 return False
4112 if path_contains_tilde():
4113 print(
4114 '\nYour PATH contains a literal "~", which works in some shells ' +
4115 'but will break when python tries to run subprocesses. ' +
4116 'Replace the "~" with $HOME.\n' + 'See https://crbug.com/952865.\n',
4117 file=sys.stderr)
4118 return False
4119 return True
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004120
4121
4122def main(argv):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004123 """Doesn't parse the arguments here, just find the right subcommand to
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004124 execute."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004125 if not can_run_gclient_and_helpers():
4126 return 2
4127 fix_encoding.fix_encoding()
4128 disable_buffering()
4129 setup_color.init()
4130 dispatcher = subcommand.CommandDispatcher(__name__)
4131 try:
4132 return dispatcher.execute(OptionParser(), argv)
4133 except KeyboardInterrupt:
4134 gclient_utils.GClientChildren.KillAllRemainingChildren()
4135 raise
4136 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
4137 print('Error: %s' % str(e), file=sys.stderr)
4138 return 1
4139 finally:
4140 gclient_utils.PrintWarnings()
4141 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004142
4143
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00004144if '__main__' == __name__:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004145 with metrics.collector.print_notice_and_exit():
4146 sys.exit(main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004147
4148# vim: ts=2:sw=2:tw=80:et: