blob: c1d23b4f8c304f8d0d1f8192ba33eaa3d21fee1f [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
Philipp Wollermann8672d512023-11-13 17:47:19 +0000131# This experiment causes all dependencies of type 'git' to be skipped for all
132# commands. This can be useful if you need to build Chromium in a working
133# tree that is guaranteed to contain a consistent, pre-synced snapshot of the
134# code that's managed by Git, but still need to install cipd dependencies and
135# run the hooks to download the remaining dependencies.
136SKIP_GIT_EXPERIMENT = 'skip-git'
137
Gavin Mak50b27a52023-09-19 22:44:59 +0000138PRECOMMIT_HOOK_VAR = 'GCLIENT_PRECOMMIT'
139
Joanna Wang66286612022-06-30 19:59:13 +0000140
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200141class GNException(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000142 pass
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200143
144
Aravind Vasudevaned935cf2023-08-24 23:52:20 +0000145def ToGNString(value):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000146 """Returns a stringified GN equivalent of the Python value."""
147 if isinstance(value, str):
148 if value.find('\n') >= 0:
149 raise GNException("Trying to print a string with a newline in it.")
150 return '"' + \
151 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
152 '"'
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200153
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000154 if isinstance(value, bool):
155 if value:
156 return "true"
157 return "false"
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200158
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000159 # NOTE: some type handling removed compared to chromium/src copy.
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200160
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000161 raise GNException("Unsupported type when printing to GN.")
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200162
163
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200164class Hook(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000165 """Descriptor of command ran before/after sync or on demand."""
166 def __init__(self,
167 action,
168 pattern=None,
169 name=None,
170 cwd=None,
171 condition=None,
172 variables=None,
173 verbose=False,
174 cwd_base=None):
175 """Constructor.
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200176
177 Arguments:
Gavin Mak65c49b12023-08-24 18:06:42 +0000178 action (list of str): argv of the command to run
179 pattern (str regex): noop with git; deprecated
180 name (str): optional name; no effect on operation
181 cwd (str): working directory to use
182 condition (str): condition when to run the hook
Paweł Hajdan, Jr032d5452017-06-22 20:43:53 +0200183 variables (dict): variables for evaluating the condition
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200184 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000185 self._action = gclient_utils.freeze(action)
186 self._pattern = pattern
187 self._name = name
188 self._cwd = cwd
189 self._condition = condition
190 self._variables = variables
191 self._verbose = verbose
192 self._cwd_base = cwd_base
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200193
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000194 @staticmethod
195 def from_dict(d,
196 variables=None,
197 verbose=False,
198 conditions=None,
199 cwd_base=None):
200 """Creates a Hook instance from a dict like in the DEPS file."""
201 # Merge any local and inherited conditions.
202 gclient_eval.UpdateCondition(d, 'and', conditions)
203 return Hook(
204 d['action'],
205 d.get('pattern'),
206 d.get('name'),
207 d.get('cwd'),
208 d.get('condition'),
209 variables=variables,
210 # Always print the header if not printing to a TTY.
211 verbose=verbose or not setup_color.IS_TTY,
212 cwd_base=cwd_base)
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200213
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000214 @property
215 def action(self):
216 return self._action
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200217
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000218 @property
219 def pattern(self):
220 return self._pattern
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200221
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000222 @property
223 def name(self):
224 return self._name
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200225
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000226 @property
227 def condition(self):
228 return self._condition
Paweł Hajdan, Jrecf53fe2017-09-29 18:28:49 +0200229
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000230 @property
231 def effective_cwd(self):
232 cwd = self._cwd_base
233 if self._cwd:
234 cwd = os.path.join(cwd, self._cwd)
235 return cwd
Corentin Walleza68660d2018-09-10 17:33:24 +0000236
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000237 def matches(self, file_list):
238 """Returns true if the pattern matches any of files in the list."""
239 if not self._pattern:
240 return True
241 pattern = re.compile(self._pattern)
242 return bool([f for f in file_list if pattern.search(f)])
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200243
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000244 def run(self):
245 """Executes the hook's command (provided the condition is met)."""
246 if (self._condition and not gclient_eval.EvaluateCondition(
247 self._condition, self._variables)):
248 return
Paweł Hajdan, Jr032d5452017-06-22 20:43:53 +0200249
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000250 cmd = list(self._action)
Paweł Hajdan, Jrfc6196b2017-07-27 13:15:25 +0200251
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000252 if cmd[0] == 'vpython3' and _detect_host_os() == 'win':
253 cmd[0] += '.bat'
Paweł Hajdan, Jrc9364392017-06-14 17:11:56 +0200254
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000255 exit_code = 2
256 try:
257 start_time = time.time()
258 gclient_utils.CheckCallAndFilter(cmd,
259 cwd=self.effective_cwd,
260 print_stdout=True,
261 show_header=True,
262 always_show_header=self._verbose)
263 exit_code = 0
264 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
265 # Use a discrete exit status code of 2 to indicate that a hook
266 # action failed. Users of this script may wish to treat hook action
267 # failures differently from VC failures.
268 print('Error: %s' % str(e), file=sys.stderr)
269 sys.exit(exit_code)
270 finally:
271 elapsed_time = time.time() - start_time
272 metrics.collector.add_repeated(
273 'hooks', {
274 'action':
275 gclient_utils.CommandToStr(cmd),
276 'name':
277 self._name,
278 'cwd':
279 os.path.relpath(os.path.normpath(self.effective_cwd),
280 self._cwd_base),
281 'condition':
282 self._condition,
283 'execution_time':
284 elapsed_time,
285 'exit_code':
286 exit_code,
287 })
288 if elapsed_time > 10:
289 print("Hook '%s' took %.2f secs" %
290 (gclient_utils.CommandToStr(cmd), elapsed_time))
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200291
292
Paweł Hajdan, Jrfc6196b2017-07-27 13:15:25 +0200293class DependencySettings(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000294 """Immutable configuration settings."""
295 def __init__(self, parent, url, managed, custom_deps, custom_vars,
296 custom_hooks, deps_file, should_process, relative, condition):
297 # These are not mutable:
298 self._parent = parent
299 self._deps_file = deps_file
300 self._url = url
301 # The condition as string (or None). Useful to keep e.g. for flatten.
302 self._condition = condition
303 # 'managed' determines whether or not this dependency is synced/updated
304 # by gclient after gclient checks it out initially. The difference
305 # between 'managed' and 'should_process' is that the user specifies
306 # 'managed' via the --unmanaged command-line flag or a .gclient config,
307 # where 'should_process' is dynamically set by gclient if it goes over
308 # its recursion limit and controls gclient's behavior so it does not
309 # misbehave.
310 self._managed = managed
311 self._should_process = should_process
312 # If this is a recursed-upon sub-dependency, and the parent has
313 # use_relative_paths set, then this dependency should check out its own
314 # dependencies relative to that parent's path for this, rather than
315 # relative to the .gclient file.
316 self._relative = relative
317 # This is a mutable value which has the list of 'target_os' OSes listed
318 # in the current deps file.
319 self.local_target_os = None
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000320
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000321 # These are only set in .gclient and not in DEPS files.
322 self._custom_vars = custom_vars or {}
323 self._custom_deps = custom_deps or {}
324 self._custom_hooks = custom_hooks or []
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000325
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000326 # Post process the url to remove trailing slashes.
327 if isinstance(self.url, str):
328 # urls are sometime incorrectly written as proto://host/path/@rev.
329 # Replace it to proto://host/path@rev.
330 self.set_url(self.url.replace('/@', '@'))
331 elif not isinstance(self.url, (None.__class__)):
332 raise gclient_utils.Error(
333 ('dependency url must be either string or None, '
334 'instead of %s') % self.url.__class__.__name__)
Edward Lemure7273d22018-05-10 19:13:51 -0400335
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000336 # Make any deps_file path platform-appropriate.
337 if self._deps_file:
338 for sep in ['/', '\\']:
339 self._deps_file = self._deps_file.replace(sep, os.sep)
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000340
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000341 @property
342 def deps_file(self):
343 return self._deps_file
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000344
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000345 @property
346 def managed(self):
347 return self._managed
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000348
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000349 @property
350 def parent(self):
351 return self._parent
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000352
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000353 @property
354 def root(self):
355 """Returns the root node, a GClient object."""
356 if not self.parent:
357 # This line is to signal pylint that it could be a GClient instance.
358 return self or GClient(None, None)
359 return self.parent.root
maruel@chromium.org8c0d9582011-10-03 21:36:01 +0000360
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000361 @property
362 def should_process(self):
363 """True if this dependency should be processed, i.e. checked out."""
364 return self._should_process
Michael Mossd683d7c2018-06-15 05:05:17 +0000365
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000366 @property
367 def custom_vars(self):
368 return self._custom_vars.copy()
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000369
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000370 @property
371 def custom_deps(self):
372 return self._custom_deps.copy()
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000373
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000374 @property
375 def custom_hooks(self):
376 return self._custom_hooks[:]
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000377
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000378 @property
379 def url(self):
380 """URL after variable expansion."""
381 return self._url
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000382
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000383 @property
384 def condition(self):
385 return self._condition
Paweł Hajdan, Jrf69860b2017-06-05 20:24:28 +0200386
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000387 @property
388 def target_os(self):
389 if self.local_target_os is not None:
390 return tuple(set(self.local_target_os).union(self.parent.target_os))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000391
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000392 return self.parent.target_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +0000393
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000394 @property
395 def target_cpu(self):
396 return self.parent.target_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -0800397
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000398 def set_url(self, url):
399 self._url = url
Edward Lemure7273d22018-05-10 19:13:51 -0400400
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000401 def get_custom_deps(self, name, url):
402 """Returns a custom deps if applicable."""
403 if self.parent:
404 url = self.parent.get_custom_deps(name, url)
405 # None is a valid return value to disable a dependency.
406 return self.custom_deps.get(name, url)
maruel@chromium.org8c0d9582011-10-03 21:36:01 +0000407
maruel@chromium.org064186c2011-09-27 23:53:33 +0000408
409class Dependency(gclient_utils.WorkItem, DependencySettings):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000410 """Object that represents a dependency checkout."""
411 def __init__(self,
412 parent,
413 name,
414 url,
415 managed,
416 custom_deps,
417 custom_vars,
418 custom_hooks,
419 deps_file,
420 should_process,
421 should_recurse,
422 relative,
423 condition,
424 protocol='https',
425 git_dependencies_state=gclient_eval.DEPS,
426 print_outbuf=False):
427 gclient_utils.WorkItem.__init__(self, name)
428 DependencySettings.__init__(self, parent, url, managed, custom_deps,
429 custom_vars, custom_hooks, deps_file,
430 should_process, relative, condition)
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000431
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000432 # This is in both .gclient and DEPS files:
433 self._deps_hooks = []
maruel@chromium.org68988972011-09-20 14:11:42 +0000434
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000435 self._pre_deps_hooks = []
maruel@chromium.org68988972011-09-20 14:11:42 +0000436
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000437 # Calculates properties:
438 self._dependencies = []
439 self._vars = {}
borenet@google.com2d1ee9e2013-10-15 08:13:16 +0000440
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000441 # A cache of the files affected by the current operation, necessary for
442 # hooks.
443 self._file_list = []
444 # List of host names from which dependencies are allowed.
445 # Default is an empty set, meaning unspecified in DEPS file, and hence
446 # all hosts will be allowed. Non-empty set means allowlist of hosts.
447 # allowed_hosts var is scoped to its DEPS file, and so it isn't
448 # recursive.
449 self._allowed_hosts = frozenset()
450 self._gn_args_from = None
451 # Spec for .gni output to write (if any).
452 self._gn_args_file = None
453 self._gn_args = []
454 # If it is not set to True, the dependency wasn't processed for its
455 # child dependency, i.e. its DEPS wasn't read.
456 self._deps_parsed = False
457 # This dependency has been processed, i.e. checked out
458 self._processed = False
459 # This dependency had its pre-DEPS hooks run
460 self._pre_deps_hooks_ran = False
461 # This dependency had its hook run
462 self._hooks_ran = False
463 # This is the scm used to checkout self.url. It may be used by
464 # dependencies to get the datetime of the revision we checked out.
465 self._used_scm = None
466 self._used_revision = None
467 # The actual revision we ended up getting, or None if that information
468 # is unavailable
469 self._got_revision = None
470 # Whether this dependency should use relative paths.
471 self._use_relative_paths = False
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200472
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000473 # recursedeps is a mutable value that selectively overrides the default
474 # 'no recursion' setting on a dep-by-dep basis.
475 #
476 # It will be a dictionary of {deps_name: depfile_namee}
477 self.recursedeps = {}
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000478
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000479 # Whether we should process this dependency's DEPS file.
480 self._should_recurse = should_recurse
Edward Lemurfbb06aa2018-06-11 20:43:06 +0000481
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000482 # Whether we should sync git/cipd dependencies and hooks from the
483 # DEPS file.
484 # This is set based on skip_sync_revisions and must be done
485 # after the patch refs are applied.
486 # If this is False, we will still run custom_hooks and process
487 # custom_deps, if any.
488 self._should_sync = True
Edward Lemure7273d22018-05-10 19:13:51 -0400489
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000490 self._OverrideUrl()
491 # This is inherited from WorkItem. We want the URL to be a resource.
492 if self.url and isinstance(self.url, str):
493 # The url is usually given to gclient either as https://blah@123
494 # or just https://blah. The @123 portion is irrelevant.
495 self.resources.append(self.url.split('@')[0])
Joanna Wang18af7ef2022-07-01 16:51:00 +0000496
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000497 # Controls whether we want to print git's output when we first clone the
498 # dependency
499 self.print_outbuf = print_outbuf
cmp@chromium.orge84ac912014-06-30 23:14:35 +0000500
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000501 self.protocol = protocol
502 self.git_dependencies_state = git_dependencies_state
Edward Lemur231f5ea2018-01-31 19:02:36 +0100503
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000504 if not self.name and self.parent:
505 raise gclient_utils.Error('Dependency without name')
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000506
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000507 def _OverrideUrl(self):
508 """Resolves the parsed url from the parent hierarchy."""
509 parsed_url = self.get_custom_deps(
510 self._name.replace(os.sep, posixpath.sep) \
511 if self._name else self._name, self.url)
512 if parsed_url != self.url:
513 logging.info('Dependency(%s)._OverrideUrl(%s) -> %s', self._name,
514 self.url, parsed_url)
515 self.set_url(parsed_url)
516 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000517
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000518 if self.url is None:
519 logging.info('Dependency(%s)._OverrideUrl(None) -> None',
520 self._name)
521 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000522
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000523 if not isinstance(self.url, str):
524 raise gclient_utils.Error('Unknown url type')
Michael Mossd683d7c2018-06-15 05:05:17 +0000525
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000526 # self.url is a local path
527 path, at, rev = self.url.partition('@')
528 if os.path.isdir(path):
529 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000530
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000531 # self.url is a URL
532 parsed_url = urllib.parse.urlparse(self.url)
533 if parsed_url[0] or re.match(r'^\w+\@[\w\.-]+\:[\w\/]+', parsed_url[2]):
534 return
Edward Lemur1f392b82019-11-15 22:40:11 +0000535
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000536 # self.url is relative to the parent's URL.
537 if not path.startswith('/'):
538 raise gclient_utils.Error(
539 'relative DEPS entry \'%s\' must begin with a slash' % self.url)
Edward Lemur1f392b82019-11-15 22:40:11 +0000540
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000541 parent_url = self.parent.url
542 parent_path = self.parent.url.split('@')[0]
543 if os.path.isdir(parent_path):
544 # Parent's URL is a local path. Get parent's URL dirname and append
545 # self.url.
546 parent_path = os.path.dirname(parent_path)
547 parsed_url = parent_path + path.replace('/', os.sep) + at + rev
548 else:
549 # Parent's URL is a URL. Get parent's URL, strip from the last '/'
550 # (equivalent to unix dirname) and append self.url.
551 parsed_url = parent_url[:parent_url.rfind('/')] + self.url
Edward Lemur1f392b82019-11-15 22:40:11 +0000552
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000553 logging.info('Dependency(%s)._OverrideUrl(%s) -> %s', self.name,
554 self.url, parsed_url)
555 self.set_url(parsed_url)
Edward Lemur1f392b82019-11-15 22:40:11 +0000556
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000557 def PinToActualRevision(self):
558 """Updates self.url to the revision checked out on disk."""
559 if self.url is None:
560 return
561 url = None
562 scm = self.CreateSCM()
563 if scm.name == 'cipd':
564 revision = scm.revinfo(None, None, None)
565 package = self.GetExpandedPackageName()
566 url = '%s/p/%s/+/%s' % (scm.GetActualRemoteURL(None), package,
567 revision)
Edward Lemur1f392b82019-11-15 22:40:11 +0000568
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000569 if os.path.isdir(scm.checkout_path):
570 revision = scm.revinfo(None, None, None)
571 url = '%s@%s' % (gclient_utils.SplitUrlRevision(
572 self.url)[0], revision)
573 self.set_url(url)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +0000574
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000575 def ToLines(self):
576 # () -> Sequence[str]
577 """Returns strings representing the deps (info, graphviz line)"""
578 s = []
579 condition_part = ([' "condition": %r,' %
580 self.condition] if self.condition else [])
581 s.extend([
582 ' # %s' % self.hierarchy(include_url=False),
583 ' "%s": {' % (self.name, ),
584 ' "url": "%s",' % (self.url, ),
585 ] + condition_part + [
586 ' },',
587 '',
588 ])
589 return s
Edward Lemure7273d22018-05-10 19:13:51 -0400590
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000591 @property
592 def requirements(self):
593 """Calculate the list of requirements."""
594 requirements = set()
595 # self.parent is implicitly a requirement. This will be recursive by
596 # definition.
597 if self.parent and self.parent.name:
598 requirements.add(self.parent.name)
John Budorick0f7b2002018-01-19 15:46:17 -0800599
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000600 # For a tree with at least 2 levels*, the leaf node needs to depend
601 # on the level higher up in an orderly way.
602 # This becomes messy for >2 depth as the DEPS file format is a
603 # dictionary, thus unsorted, while the .gclient format is a list thus
604 # sorted.
605 #
606 # Interestingly enough, the following condition only works in the case
607 # we want: self is a 2nd level node. 3rd level node wouldn't need this
608 # since they already have their parent as a requirement.
609 if self.parent and self.parent.parent and not self.parent.parent.parent:
610 requirements |= set(i.name for i in self.root.dependencies
611 if i.name)
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000612
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000613 if self.name:
614 requirements |= set(
615 obj.name for obj in self.root.subtree(False)
616 if (obj is not self and obj.name
617 and self.name.startswith(posixpath.join(obj.name, ''))))
618 requirements = tuple(sorted(requirements))
619 logging.info('Dependency(%s).requirements = %s' %
620 (self.name, requirements))
621 return requirements
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000622
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000623 @property
624 def should_recurse(self):
625 return self._should_recurse
maruel@chromium.org470b5432011-10-11 18:18:19 +0000626
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000627 def verify_validity(self):
628 """Verifies that this Dependency is fine to add as a child of another one.
maruel@chromium.org470b5432011-10-11 18:18:19 +0000629
630 Returns True if this entry should be added, False if it is a duplicate of
631 another entry.
632 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000633 logging.info('Dependency(%s).verify_validity()' % self.name)
634 if self.name in [s.name for s in self.parent.dependencies]:
635 raise gclient_utils.Error(
636 'The same name "%s" appears multiple times in the deps section'
637 % self.name)
638 if not self.should_process:
639 # Return early, no need to set requirements.
640 return not any(d.name == self.name for d in self.root.subtree(True))
maruel@chromium.org470b5432011-10-11 18:18:19 +0000641
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000642 # This require a full tree traversal with locks.
643 siblings = [d for d in self.root.subtree(False) if d.name == self.name]
644 for sibling in siblings:
645 # Allow to have only one to be None or ''.
646 if self.url != sibling.url and bool(self.url) == bool(sibling.url):
647 raise gclient_utils.Error(
648 ('Dependency %s specified more than once:\n'
649 ' %s [%s]\n'
650 'vs\n'
651 ' %s [%s]') % (self.name, sibling.hierarchy(),
652 sibling.url, self.hierarchy(), self.url))
653 # In theory we could keep it as a shadow of the other one. In
654 # practice, simply ignore it.
655 logging.warning("Won't process duplicate dependency %s" % sibling)
656 return False
657 return True
maruel@chromium.org064186c2011-09-27 23:53:33 +0000658
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000659 def _postprocess_deps(self, deps, rel_prefix):
660 # type: (Mapping[str, Mapping[str, str]], str) ->
661 # Mapping[str, Mapping[str, str]]
662 """Performs post-processing of deps compared to what's in the DEPS file."""
663 # If we don't need to sync, only process custom_deps, if any.
664 if not self._should_sync:
665 if not self.custom_deps:
666 return {}
Paweł Hajdan, Jr4426eaf2017-06-13 12:25:47 +0200667
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000668 processed_deps = {}
669 for dep_name, dep_info in self.custom_deps.items():
670 if dep_info and not dep_info.endswith('@unmanaged'):
671 if dep_name in deps:
672 # custom_deps that should override an existing deps gets
673 # applied in the Dependency itself with _OverrideUrl().
674 processed_deps[dep_name] = deps[dep_name]
675 else:
676 processed_deps[dep_name] = {
677 'url': dep_info,
678 'dep_type': 'git'
679 }
680 else:
681 processed_deps = dict(deps)
Joanna Wang18af7ef2022-07-01 16:51:00 +0000682
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000683 # If a line is in custom_deps, but not in the solution, we want to
684 # append this line to the solution.
685 for dep_name, dep_info in self.custom_deps.items():
686 # Don't add it to the solution for the values of "None" and
687 # "unmanaged" in order to force these kinds of custom_deps to
688 # act as revision overrides (via revision_overrides). Having
689 # them function as revision overrides allows them to be applied
690 # to recursive dependencies. https://crbug.com/1031185
691 if (dep_name not in processed_deps and dep_info
692 and not dep_info.endswith('@unmanaged')):
693 processed_deps[dep_name] = {
694 'url': dep_info,
695 'dep_type': 'git'
696 }
Edward Lemur16f4bad2018-05-16 16:53:49 -0400697
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000698 # Make child deps conditional on any parent conditions. This ensures
699 # that, when flattened, recursed entries have the correct restrictions,
700 # even if not explicitly set in the recursed DEPS file. For instance, if
701 # "src/ios_foo" is conditional on "checkout_ios=True", then anything
702 # recursively included by "src/ios_foo/DEPS" should also require
703 # "checkout_ios=True".
704 if self.condition:
705 for value in processed_deps.values():
706 gclient_eval.UpdateCondition(value, 'and', self.condition)
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200707
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000708 if not rel_prefix:
709 return processed_deps
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200710
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000711 logging.warning('use_relative_paths enabled.')
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200712 rel_deps = {}
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000713 for d, url in processed_deps.items():
714 # normpath is required to allow DEPS to use .. in their
715 # dependency local path.
716 # We are following the same pattern when use_relative_paths = False,
717 # which uses slashes.
718 rel_deps[os.path.normpath(os.path.join(rel_prefix, d)).replace(
719 os.path.sep, '/')] = url
720 logging.warning('Updating deps by prepending %s.', rel_prefix)
721 return rel_deps
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200722
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000723 def _deps_to_objects(self, deps, use_relative_paths):
724 # type: (Mapping[str, Mapping[str, str]], bool) -> Sequence[Dependency]
725 """Convert a deps dict to a list of Dependency objects."""
726 deps_to_add = []
727 cached_conditions = {}
728 for name, dep_value in deps.items():
729 should_process = self.should_process
730 if dep_value is None:
731 continue
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200732
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000733 condition = dep_value.get('condition')
734 dep_type = dep_value.get('dep_type')
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000735
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000736 if condition and not self._get_option('process_all_deps', False):
737 if condition not in cached_conditions:
738 cached_conditions[
739 condition] = gclient_eval.EvaluateCondition(
740 condition, self.get_vars())
741 should_process = should_process and cached_conditions[condition]
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000742
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000743 # The following option is only set by the 'revinfo' command.
744 if self._get_option('ignore_dep_type', None) == dep_type:
745 continue
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000746
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000747 if dep_type == 'cipd':
748 cipd_root = self.GetCipdRoot()
749 for package in dep_value.get('packages', []):
750 deps_to_add.append(
751 CipdDependency(parent=self,
752 name=name,
753 dep_value=package,
754 cipd_root=cipd_root,
755 custom_vars=self.custom_vars,
756 should_process=should_process,
757 relative=use_relative_paths,
758 condition=condition))
759 else:
760 url = dep_value.get('url')
761 deps_to_add.append(
762 GitDependency(
763 parent=self,
764 name=name,
765 # Update URL with scheme in protocol_override
766 url=GitDependency.updateProtocol(url, self.protocol),
767 managed=True,
768 custom_deps=None,
769 custom_vars=self.custom_vars,
770 custom_hooks=None,
771 deps_file=self.recursedeps.get(name, self.deps_file),
772 should_process=should_process,
773 should_recurse=name in self.recursedeps,
774 relative=use_relative_paths,
775 condition=condition,
776 protocol=self.protocol,
777 git_dependencies_state=self.git_dependencies_state))
Michael Spang0e99b9b2020-08-12 13:34:48 +0000778
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000779 # TODO(crbug.com/1341285): Understand why we need this and remove
780 # it if we don't.
781 deps_to_add.sort(key=lambda x: x.name)
782 return deps_to_add
Corentin Walleza68660d2018-09-10 17:33:24 +0000783
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000784 def ParseDepsFile(self):
785 # type: () -> None
786 """Parses the DEPS file for this dependency."""
787 assert not self.deps_parsed
788 assert not self.dependencies
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000789
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000790 deps_content = None
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000791
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000792 # First try to locate the configured deps file. If it's missing,
793 # fallback to DEPS.
794 deps_files = [self.deps_file]
795 if 'DEPS' not in deps_files:
796 deps_files.append('DEPS')
797 for deps_file in deps_files:
798 filepath = os.path.join(self.root.root_dir, self.name, deps_file)
799 if os.path.isfile(filepath):
800 logging.info('ParseDepsFile(%s): %s file found at %s',
801 self.name, deps_file, filepath)
802 break
803 logging.info('ParseDepsFile(%s): No %s file found at %s', self.name,
804 deps_file, filepath)
borenet@google.com2d1ee9e2013-10-15 08:13:16 +0000805
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000806 if os.path.isfile(filepath):
807 deps_content = gclient_utils.FileRead(filepath)
808 logging.debug('ParseDepsFile(%s) read:\n%s', self.name,
809 deps_content)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +0000810
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000811 local_scope = {}
812 if deps_content:
813 try:
814 local_scope = gclient_eval.Parse(deps_content, filepath,
815 self.get_vars(),
816 self.get_builtin_vars())
817 except SyntaxError as e:
818 gclient_utils.SyntaxErrorToError(filepath, e)
819
820 if 'git_dependencies' in local_scope:
821 self.git_dependencies_state = local_scope['git_dependencies']
822
823 if 'allowed_hosts' in local_scope:
824 try:
825 self._allowed_hosts = frozenset(
826 local_scope.get('allowed_hosts'))
827 except TypeError: # raised if non-iterable
828 pass
829 if not self._allowed_hosts:
830 logging.warning("allowed_hosts is specified but empty %s",
831 self._allowed_hosts)
832 raise gclient_utils.Error(
833 'ParseDepsFile(%s): allowed_hosts must be absent '
834 'or a non-empty iterable' % self.name)
835
836 self._gn_args_from = local_scope.get('gclient_gn_args_from')
837 self._gn_args_file = local_scope.get('gclient_gn_args_file')
838 self._gn_args = local_scope.get('gclient_gn_args', [])
839 # It doesn't make sense to set all of these, since setting gn_args_from
840 # to another DEPS will make gclient ignore any other local gn_args*
841 # settings.
842 assert not (self._gn_args_from and self._gn_args_file), \
843 'Only specify one of "gclient_gn_args_from" or ' \
844 '"gclient_gn_args_file + gclient_gn_args".'
845
846 self._vars = local_scope.get('vars', {})
847 if self.parent:
848 for key, value in self.parent.get_vars().items():
849 if key in self._vars:
850 self._vars[key] = value
851 # Since we heavily post-process things, freeze ones which should
852 # reflect original state of DEPS.
853 self._vars = gclient_utils.freeze(self._vars)
854
855 # If use_relative_paths is set in the DEPS file, regenerate
856 # the dictionary using paths relative to the directory containing
857 # the DEPS file. Also update recursedeps if use_relative_paths is
858 # enabled.
859 # If the deps file doesn't set use_relative_paths, but the parent did
860 # (and therefore set self.relative on this Dependency object), then we
861 # want to modify the deps and recursedeps by prepending the parent
862 # directory of this dependency.
863 self._use_relative_paths = local_scope.get('use_relative_paths', False)
864 rel_prefix = None
865 if self._use_relative_paths:
866 rel_prefix = self.name
867 elif self._relative:
868 rel_prefix = os.path.dirname(self.name)
869
870 if 'recursion' in local_scope:
871 logging.warning('%s: Ignoring recursion = %d.', self.name,
872 local_scope['recursion'])
873
874 if 'recursedeps' in local_scope:
875 for ent in local_scope['recursedeps']:
876 if isinstance(ent, str):
877 self.recursedeps[ent] = self.deps_file
878 else: # (depname, depsfilename)
879 self.recursedeps[ent[0]] = ent[1]
880 logging.warning('Found recursedeps %r.', repr(self.recursedeps))
881
882 if rel_prefix:
883 logging.warning('Updating recursedeps by prepending %s.',
884 rel_prefix)
885 rel_deps = {}
886 for depname, options in self.recursedeps.items():
887 rel_deps[os.path.normpath(os.path.join(rel_prefix,
888 depname)).replace(
889 os.path.sep,
890 '/')] = options
891 self.recursedeps = rel_deps
892 # To get gn_args from another DEPS, that DEPS must be recursed into.
893 if self._gn_args_from:
894 assert self.recursedeps and self._gn_args_from in self.recursedeps, \
895 'The "gclient_gn_args_from" value must be in recursedeps.'
896
897 # If present, save 'target_os' in the local_target_os property.
898 if 'target_os' in local_scope:
899 self.local_target_os = local_scope['target_os']
900
901 deps = local_scope.get('deps', {})
902
903 # If dependencies are configured within git submodules, add them to
904 # deps. We don't add for SYNC since we expect submodules to be in sync.
905 if self.git_dependencies_state == gclient_eval.SUBMODULES:
906 deps.update(self.ParseGitSubmodules())
907
908 deps_to_add = self._deps_to_objects(
909 self._postprocess_deps(deps, rel_prefix), self._use_relative_paths)
910
911 # compute which working directory should be used for hooks
912 if local_scope.get('use_relative_hooks', False):
913 print('use_relative_hooks is deprecated, please remove it from '
914 '%s DEPS. (it was merged in use_relative_paths)' % self.name,
915 file=sys.stderr)
916
917 hooks_cwd = self.root.root_dir
918 if self._use_relative_paths:
919 hooks_cwd = os.path.join(hooks_cwd, self.name)
920 elif self._relative:
921 hooks_cwd = os.path.join(hooks_cwd, os.path.dirname(self.name))
922 logging.warning('Using hook base working directory: %s.', hooks_cwd)
923
924 # Only add all hooks if we should sync, otherwise just add custom hooks.
925 # override named sets of hooks by the custom hooks
926 hooks_to_run = []
927 if self._should_sync:
928 hook_names_to_suppress = [
929 c.get('name', '') for c in self.custom_hooks
930 ]
931 for hook in local_scope.get('hooks', []):
932 if hook.get('name', '') not in hook_names_to_suppress:
933 hooks_to_run.append(hook)
934
935 # add the replacements and any additions
936 for hook in self.custom_hooks:
937 if 'action' in hook:
938 hooks_to_run.append(hook)
939
940 if self.should_recurse and deps_to_add:
941 self._pre_deps_hooks = [
942 Hook.from_dict(hook,
943 variables=self.get_vars(),
944 verbose=True,
945 conditions=self.condition,
946 cwd_base=hooks_cwd)
947 for hook in local_scope.get('pre_deps_hooks', [])
948 ]
949
950 self.add_dependencies_and_close(deps_to_add,
951 hooks_to_run,
952 hooks_cwd=hooks_cwd)
953 logging.info('ParseDepsFile(%s) done' % self.name)
954
955 def ParseGitSubmodules(self):
956 # type: () -> Mapping[str, str]
957 """
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000958 Parses git submodules and returns a dict of path to DEPS git url entries.
959
960 e.g {<path>: <url>@<commit_hash>}
961 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000962 cwd = os.path.join(self.root.root_dir, self.name)
963 filepath = os.path.join(cwd, '.gitmodules')
964 if not os.path.isfile(filepath):
965 logging.warning('ParseGitSubmodules(): No .gitmodules found at %s',
966 filepath)
967 return {}
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000968
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000969 # Get .gitmodules fields
970 gitmodules_entries = subprocess2.check_output(
971 ['git', 'config', '--file', filepath, '-l']).decode('utf-8')
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000972
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000973 gitmodules = {}
974 for entry in gitmodules_entries.splitlines():
975 key, value = entry.split('=', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000976
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000977 # git config keys consist of section.name.key, e.g.,
978 # submodule.foo.path
979 section, submodule_key = key.split('.', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000980
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000981 # Only parse [submodule "foo"] sections from .gitmodules.
982 if section.strip() != 'submodule':
983 continue
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000984
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000985 # The name of the submodule can contain '.', hence split from the
986 # back.
987 submodule, sub_key = submodule_key.rsplit('.', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000988
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000989 if submodule not in gitmodules:
990 gitmodules[submodule] = {}
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000991
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000992 if sub_key in ('url', 'gclient-condition', 'path'):
993 gitmodules[submodule][sub_key] = value
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000994
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000995 paths = [module['path'] for module in gitmodules.values()]
996 commit_hashes = scm_git.GIT.GetSubmoduleCommits(cwd, paths)
Joanna Wang978f43d2023-08-18 00:16:07 +0000997
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000998 # Structure git submodules into a dict of DEPS git url entries.
999 submodules = {}
1000 for module in gitmodules.values():
1001 if self._use_relative_paths:
1002 path = module['path']
1003 else:
1004 path = f'{self.name}/{module["path"]}'
1005 # TODO(crbug.com/1471685): Temporary hack. In case of applied
1006 # patches where the changes are staged but not committed, any
1007 # gitlinks from the patch are not returned by `git ls-tree`. The
1008 # path won't be found in commit_hashes. Use a temporary '0000000'
1009 # value that will be replaced with w/e is found in DEPS later.
1010 submodules[path] = {
1011 'dep_type':
1012 'git',
1013 'url':
1014 '{}@{}'.format(module['url'],
1015 commit_hashes.get(module['path'], '0000000'))
1016 }
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001017
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001018 if 'gclient-condition' in module:
1019 submodules[path]['condition'] = module['gclient-condition']
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001020
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001021 return submodules
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001022
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001023 def _get_option(self, attr, default):
1024 obj = self
1025 while not hasattr(obj, '_options'):
1026 obj = obj.parent
1027 return getattr(obj._options, attr, default)
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02001028
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001029 def add_dependencies_and_close(self, deps_to_add, hooks, hooks_cwd=None):
1030 """Adds the dependencies, hooks and mark the parsing as done."""
1031 if hooks_cwd == None:
1032 hooks_cwd = self.root.root_dir
Corentin Walleza68660d2018-09-10 17:33:24 +00001033
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001034 for dep in deps_to_add:
1035 if dep.verify_validity():
1036 self.add_dependency(dep)
1037 self._mark_as_parsed([
1038 Hook.from_dict(h,
1039 variables=self.get_vars(),
1040 verbose=self.root._options.verbose,
1041 conditions=self.condition,
1042 cwd_base=hooks_cwd) for h in hooks
1043 ])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001044
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001045 def findDepsFromNotAllowedHosts(self):
1046 """Returns a list of dependencies from not allowed hosts.
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001047
1048 If allowed_hosts is not set, allows all hosts and returns empty list.
1049 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001050 if not self._allowed_hosts:
1051 return []
1052 bad_deps = []
1053 for dep in self._dependencies:
1054 # Don't enforce this for custom_deps.
1055 if dep.name in self._custom_deps:
1056 continue
1057 if isinstance(dep.url, str):
1058 parsed_url = urllib.parse.urlparse(dep.url)
1059 if parsed_url.netloc and parsed_url.netloc not in self._allowed_hosts:
1060 bad_deps.append(dep)
1061 return bad_deps
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001062
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001063 def FuzzyMatchUrl(self, candidates):
1064 # type: (Union[Mapping[str, str], Collection[str]]) -> Optional[str]
1065 """Attempts to find this dependency in the list of candidates.
Edward Lesmesbb16e332018-03-30 17:54:51 -04001066
Edward Lemure7273d22018-05-10 19:13:51 -04001067 It looks first for the URL of this dependency in the list of
Edward Lesmesbb16e332018-03-30 17:54:51 -04001068 candidates. If it doesn't succeed, and the URL ends in '.git', it will try
1069 looking for the URL minus '.git'. Finally it will try to look for the name
1070 of the dependency.
1071
1072 Args:
Edward Lesmesbb16e332018-03-30 17:54:51 -04001073 candidates: list, dict. The list of candidates in which to look for this
1074 dependency. It can contain URLs as above, or dependency names like
1075 "src/some/dep".
1076
1077 Returns:
1078 If this dependency is not found in the list of candidates, returns None.
1079 Otherwise, it returns under which name did we find this dependency:
1080 - Its parsed url: "https://example.com/src.git'
1081 - Its parsed url minus '.git': "https://example.com/src"
1082 - Its name: "src"
1083 """
Michael Mossd683d7c2018-06-15 05:05:17 +00001084 if self.url:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001085 origin, _ = gclient_utils.SplitUrlRevision(self.url)
1086 match = gclient_utils.FuzzyMatchRepo(origin, candidates)
agabled437d762016-10-17 09:35:11 -07001087 if match:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001088 return match
1089 if self.name in candidates:
1090 return self.name
1091 return None
ilevy@chromium.org0233ac22012-11-28 20:27:02 +00001092
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001093 # Arguments number differs from overridden method
1094 # pylint: disable=arguments-differ
1095 def run(
1096 self,
1097 revision_overrides, # type: Mapping[str, str]
1098 command, # type: str
1099 args, # type: Sequence[str]
1100 work_queue, # type: ExecutionQueue
1101 options, # type: optparse.Values
1102 patch_refs, # type: Mapping[str, str]
1103 target_branches, # type: Mapping[str, str]
1104 skip_sync_revisions, # type: Mapping[str, str]
1105 ):
1106 # type: () -> None
1107 """Runs |command| then parse the DEPS file."""
1108 logging.info('Dependency(%s).run()' % self.name)
1109 assert self._file_list == []
1110 # When running runhooks, there's no need to consult the SCM.
1111 # All known hooks are expected to run unconditionally regardless of
1112 # working copy state, so skip the SCM status check.
1113 run_scm = command not in ('flatten', 'runhooks', 'recurse', 'validate',
1114 None)
Philipp Wollermann8672d512023-11-13 17:47:19 +00001115 # See the description of the experiment for the logic behind this.
1116 if SKIP_GIT_EXPERIMENT in options.experiments and isinstance(
1117 self, GitDependency):
1118 run_scm = False
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001119 file_list = [] if not options.nohooks else None
1120 revision_override = revision_overrides.pop(
1121 self.FuzzyMatchUrl(revision_overrides), None)
1122 if not revision_override and not self.managed:
1123 revision_override = 'unmanaged'
1124 if run_scm and self.url:
1125 # Create a shallow copy to mutate revision.
1126 options = copy.copy(options)
1127 options.revision = revision_override
1128 self._used_revision = options.revision
1129 self._used_scm = self.CreateSCM(out_cb=work_queue.out_cb)
1130 if command != 'update' or self.GetScmName() != 'git':
1131 self._got_revision = self._used_scm.RunCommand(
1132 command, options, args, file_list)
agabled437d762016-10-17 09:35:11 -07001133 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001134 try:
1135 start = time.time()
1136 sync_status = metrics_utils.SYNC_STATUS_FAILURE
1137 self._got_revision = self._used_scm.RunCommand(
1138 command, options, args, file_list)
1139 sync_status = metrics_utils.SYNC_STATUS_SUCCESS
1140 finally:
1141 url, revision = gclient_utils.SplitUrlRevision(self.url)
1142 metrics.collector.add_repeated(
1143 'git_deps', {
1144 'path': self.name,
1145 'url': url,
1146 'revision': revision,
1147 'execution_time': time.time() - start,
1148 'sync_status': sync_status,
1149 })
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00001150
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001151 if isinstance(self, GitDependency) and command == 'update':
1152 patch_repo = self.url.split('@')[0]
1153 patch_ref = patch_refs.pop(self.FuzzyMatchUrl(patch_refs), None)
1154 target_branch = target_branches.pop(
1155 self.FuzzyMatchUrl(target_branches), None)
1156 if patch_ref:
1157 latest_commit = self._used_scm.apply_patch_ref(
1158 patch_repo, patch_ref, target_branch, options,
1159 file_list)
1160 else:
1161 latest_commit = self._used_scm.revinfo(None, None, None)
1162 existing_sync_commits = json.loads(
1163 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
1164 existing_sync_commits[self.name] = latest_commit
1165 os.environ[PREVIOUS_SYNC_COMMITS] = json.dumps(
1166 existing_sync_commits)
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001167
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001168 if file_list:
1169 file_list = [
1170 os.path.join(self.name, f.strip()) for f in file_list
1171 ]
John Budorick0f7b2002018-01-19 15:46:17 -08001172
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001173 # TODO(phajdan.jr): We should know exactly when the paths are
1174 # absolute. Convert all absolute paths to relative.
1175 for i in range(len(file_list or [])):
1176 # It depends on the command being executed (like runhooks vs
1177 # sync).
1178 if not os.path.isabs(file_list[i]):
1179 continue
1180 prefix = os.path.commonprefix(
1181 [self.root.root_dir.lower(), file_list[i].lower()])
1182 file_list[i] = file_list[i][len(prefix):]
1183 # Strip any leading path separators.
1184 while file_list[i].startswith(('\\', '/')):
1185 file_list[i] = file_list[i][1:]
John Budorick0f7b2002018-01-19 15:46:17 -08001186
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001187 # We must check for diffs AFTER any patch_refs have been applied.
1188 if skip_sync_revisions:
1189 skip_sync_rev = skip_sync_revisions.pop(
1190 self.FuzzyMatchUrl(skip_sync_revisions), None)
1191 self._should_sync = (skip_sync_rev is None
1192 or self._used_scm.check_diff(skip_sync_rev,
1193 files=['DEPS']))
1194 if not self._should_sync:
1195 logging.debug(
1196 'Skipping sync for %s. No DEPS changes since last '
1197 'sync at %s' % (self.name, skip_sync_rev))
1198 else:
1199 logging.debug('DEPS changes detected for %s since last sync at '
1200 '%s. Not skipping deps sync' %
1201 (self.name, skip_sync_rev))
Dirk Pranke9f20d022017-10-11 18:36:54 -07001202
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001203 if self.should_recurse:
1204 self.ParseDepsFile()
Corentin Wallez271a78a2020-07-12 15:41:46 +00001205
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001206 self._run_is_done(file_list or [])
Corentin Wallez271a78a2020-07-12 15:41:46 +00001207
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001208 # TODO(crbug.com/1339471): If should_recurse is false, ParseDepsFile
1209 # never gets called meaning we never fetch hooks and dependencies. So
1210 # there's no need to check should_recurse again here.
1211 if self.should_recurse:
1212 if command in ('update', 'revert') and not options.noprehooks:
1213 self.RunPreDepsHooks()
1214 # Parse the dependencies of this dependency.
1215 for s in self.dependencies:
1216 if s.should_process:
1217 work_queue.enqueue(s)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001218
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001219 if command == 'recurse':
1220 # Skip file only checkout.
1221 scm = self.GetScmName()
1222 if not options.scm or scm in options.scm:
1223 cwd = os.path.normpath(
1224 os.path.join(self.root.root_dir, self.name))
1225 # Pass in the SCM type as an env variable. Make sure we don't
1226 # put unicode strings in the environment.
1227 env = os.environ.copy()
1228 if scm:
1229 env['GCLIENT_SCM'] = str(scm)
1230 if self.url:
1231 env['GCLIENT_URL'] = str(self.url)
1232 env['GCLIENT_DEP_PATH'] = str(self.name)
1233 if options.prepend_dir and scm == 'git':
1234 print_stdout = False
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001235
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001236 def filter_fn(line):
1237 """Git-specific path marshaling. It is optimized for git-grep."""
1238 def mod_path(git_pathspec):
1239 match = re.match('^(\\S+?:)?([^\0]+)$',
1240 git_pathspec)
1241 modified_path = os.path.join(
1242 self.name, match.group(2))
1243 branch = match.group(1) or ''
1244 return '%s%s' % (branch, modified_path)
1245
1246 match = re.match('^Binary file ([^\0]+) matches$', line)
1247 if match:
1248 print('Binary file %s matches\n' %
1249 mod_path(match.group(1)))
1250 return
1251
1252 items = line.split('\0')
1253 if len(items) == 2 and items[1]:
1254 print('%s : %s' % (mod_path(items[0]), items[1]))
1255 elif len(items) >= 2:
1256 # Multiple null bytes or a single trailing null byte
1257 # indicate git is likely displaying filenames only
1258 # (such as with -l)
1259 print('\n'.join(
1260 mod_path(path) for path in items if path))
1261 else:
1262 print(line)
1263 else:
1264 print_stdout = True
1265 filter_fn = None
1266
1267 if self.url is None:
1268 print('Skipped omitted dependency %s' % cwd,
1269 file=sys.stderr)
1270 elif os.path.isdir(cwd):
1271 try:
1272 gclient_utils.CheckCallAndFilter(
1273 args,
1274 cwd=cwd,
1275 env=env,
1276 print_stdout=print_stdout,
1277 filter_fn=filter_fn,
1278 )
1279 except subprocess2.CalledProcessError:
1280 if not options.ignore:
1281 raise
1282 else:
1283 print('Skipped missing %s' % cwd, file=sys.stderr)
1284
1285 def GetScmName(self):
1286 raise NotImplementedError()
1287
1288 def CreateSCM(self, out_cb=None):
1289 raise NotImplementedError()
1290
1291 def HasGNArgsFile(self):
1292 return self._gn_args_file is not None
1293
1294 def WriteGNArgsFile(self):
1295 lines = ['# Generated from %r' % self.deps_file]
1296 variables = self.get_vars()
1297 for arg in self._gn_args:
1298 value = variables[arg]
1299 if isinstance(value, gclient_eval.ConstantString):
1300 value = value.value
1301 elif isinstance(value, str):
1302 value = gclient_eval.EvaluateCondition(value, variables)
1303 lines.append('%s = %s' % (arg, ToGNString(value)))
1304
1305 # When use_relative_paths is set, gn_args_file is relative to this DEPS
1306 path_prefix = self.root.root_dir
1307 if self._use_relative_paths:
1308 path_prefix = os.path.join(path_prefix, self.name)
1309
1310 with open(os.path.join(path_prefix, self._gn_args_file), 'wb') as f:
1311 f.write('\n'.join(lines).encode('utf-8', 'replace'))
1312
1313 @gclient_utils.lockedmethod
1314 def _run_is_done(self, file_list):
1315 # Both these are kept for hooks that are run as a separate tree
1316 # traversal.
1317 self._file_list = file_list
1318 self._processed = True
1319
1320 def GetHooks(self, options):
1321 """Evaluates all hooks, and return them in a flat list.
szager@google.comb9a78d32012-03-13 18:46:21 +00001322
1323 RunOnDeps() must have been called before to load the DEPS.
1324 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001325 result = []
1326 if not self.should_process or not self.should_recurse:
1327 # Don't run the hook when it is above recursion_limit.
1328 return result
1329 # If "--force" was specified, run all hooks regardless of what files
1330 # have changed.
1331 if self.deps_hooks:
1332 # TODO(maruel): If the user is using git, then we don't know
1333 # what files have changed so we always run all hooks. It'd be nice
1334 # to fix that.
1335 result.extend(self.deps_hooks)
1336 for s in self.dependencies:
1337 result.extend(s.GetHooks(options))
1338 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001339
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001340 def RunHooksRecursively(self, options, progress):
1341 assert self.hooks_ran == False
1342 self._hooks_ran = True
1343 hooks = self.GetHooks(options)
1344 if progress:
1345 progress._total = len(hooks)
1346 for hook in hooks:
1347 if progress:
1348 progress.update(extra=hook.name or '')
1349 hook.run()
1350 if progress:
1351 progress.end()
maruel@chromium.orgeaf61062010-07-07 18:42:39 +00001352
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001353 def RunPreDepsHooks(self):
1354 assert self.processed
1355 assert self.deps_parsed
1356 assert not self.pre_deps_hooks_ran
1357 assert not self.hooks_ran
1358 for s in self.dependencies:
1359 assert not s.processed
1360 self._pre_deps_hooks_ran = True
1361 for hook in self.pre_deps_hooks:
1362 hook.run()
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001363
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001364 def GetCipdRoot(self):
1365 if self.root is self:
1366 # Let's not infinitely recurse. If this is root and isn't an
1367 # instance of GClient, do nothing.
1368 return None
1369 return self.root.GetCipdRoot()
John Budorickd3ba72b2018-03-20 12:27:42 -07001370
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001371 def subtree(self, include_all):
1372 """Breadth first recursion excluding root node."""
1373 dependencies = self.dependencies
1374 for d in dependencies:
1375 if d.should_process or include_all:
1376 yield d
1377 for d in dependencies:
1378 for i in d.subtree(include_all):
1379 yield i
maruel@chromium.orgad3287e2011-10-03 19:15:10 +00001380
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001381 @gclient_utils.lockedmethod
1382 def add_dependency(self, new_dep):
1383 self._dependencies.append(new_dep)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001384
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001385 @gclient_utils.lockedmethod
1386 def _mark_as_parsed(self, new_hooks):
1387 self._deps_hooks.extend(new_hooks)
1388 self._deps_parsed = True
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001389
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001390 @property
1391 @gclient_utils.lockedmethod
1392 def dependencies(self):
1393 return tuple(self._dependencies)
maruel@chromium.org4bdd5fd2011-09-26 19:41:17 +00001394
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001395 @property
1396 @gclient_utils.lockedmethod
1397 def deps_hooks(self):
1398 return tuple(self._deps_hooks)
maruel@chromium.org064186c2011-09-27 23:53:33 +00001399
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001400 @property
1401 @gclient_utils.lockedmethod
1402 def pre_deps_hooks(self):
1403 return tuple(self._pre_deps_hooks)
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001404
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001405 @property
1406 @gclient_utils.lockedmethod
1407 def deps_parsed(self):
1408 """This is purely for debugging purposes. It's not used anywhere."""
1409 return self._deps_parsed
maruel@chromium.org064186c2011-09-27 23:53:33 +00001410
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001411 @property
1412 @gclient_utils.lockedmethod
1413 def processed(self):
1414 return self._processed
maruel@chromium.org064186c2011-09-27 23:53:33 +00001415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001416 @property
1417 @gclient_utils.lockedmethod
1418 def pre_deps_hooks_ran(self):
1419 return self._pre_deps_hooks_ran
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001420
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001421 @property
1422 @gclient_utils.lockedmethod
1423 def hooks_ran(self):
1424 return self._hooks_ran
maruel@chromium.org064186c2011-09-27 23:53:33 +00001425
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001426 @property
1427 @gclient_utils.lockedmethod
1428 def allowed_hosts(self):
1429 return self._allowed_hosts
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001430
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001431 @property
1432 @gclient_utils.lockedmethod
1433 def file_list(self):
1434 return tuple(self._file_list)
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001435
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001436 @property
1437 def used_scm(self):
1438 """SCMWrapper instance for this dependency or None if not processed yet."""
1439 return self._used_scm
kustermann@google.coma692e8f2013-04-18 08:32:04 +00001440
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001441 @property
1442 @gclient_utils.lockedmethod
1443 def got_revision(self):
1444 return self._got_revision
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00001445
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001446 @property
1447 def file_list_and_children(self):
1448 result = list(self.file_list)
1449 for d in self.dependencies:
1450 result.extend(d.file_list_and_children)
1451 return tuple(result)
maruel@chromium.org861fd0f2010-07-23 03:05:05 +00001452
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001453 def __str__(self):
1454 out = []
1455 for i in ('name', 'url', 'custom_deps', 'custom_vars', 'deps_hooks',
1456 'file_list', 'should_process', 'processed', 'hooks_ran',
1457 'deps_parsed', 'requirements', 'allowed_hosts'):
1458 # First try the native property if it exists.
1459 if hasattr(self, '_' + i):
1460 value = getattr(self, '_' + i, False)
1461 else:
1462 value = getattr(self, i, False)
1463 if value:
1464 out.append('%s: %s' % (i, value))
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001465
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001466 for d in self.dependencies:
1467 out.extend([' ' + x for x in str(d).splitlines()])
1468 out.append('')
1469 return '\n'.join(out)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001470
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001471 def __repr__(self):
1472 return '%s: %s' % (self.name, self.url)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001473
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001474 def hierarchy(self, include_url=True, graphviz=False):
1475 """Returns a human-readable hierarchical reference to a Dependency."""
1476 def format_name(d):
1477 if include_url:
1478 return '%s(%s)' % (d.name, d.url)
1479 return '"%s"' % d.name # quotes required for graph dot file.
Joanna Wang9144b672023-02-24 23:36:17 +00001480
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001481 out = format_name(self)
1482 i = self.parent
1483 while i and i.name:
1484 out = '%s -> %s' % (format_name(i), out)
1485 if graphviz:
1486 # for graphviz we just need each parent->child relationship
1487 # listed once.
1488 return out
1489 i = i.parent
Joanna Wang9144b672023-02-24 23:36:17 +00001490 return out
maruel@chromium.orgbffb9042010-07-22 20:59:36 +00001491
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001492 def hierarchy_data(self):
1493 """Returns a machine-readable hierarchical reference to a Dependency."""
1494 d = self
1495 out = []
1496 while d and d.name:
1497 out.insert(0, (d.name, d.url))
1498 d = d.parent
1499 return tuple(out)
Michael Mossfe68c912018-03-22 19:19:35 -07001500
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001501 def get_builtin_vars(self):
1502 return {
1503 'checkout_android': 'android' in self.target_os,
1504 'checkout_chromeos': 'chromeos' in self.target_os,
1505 'checkout_fuchsia': 'fuchsia' in self.target_os,
1506 'checkout_ios': 'ios' in self.target_os,
1507 'checkout_linux': 'unix' in self.target_os,
1508 'checkout_mac': 'mac' in self.target_os,
1509 'checkout_win': 'win' in self.target_os,
1510 'host_os': _detect_host_os(),
1511 'checkout_arm': 'arm' in self.target_cpu,
1512 'checkout_arm64': 'arm64' in self.target_cpu,
1513 'checkout_x86': 'x86' in self.target_cpu,
1514 'checkout_mips': 'mips' in self.target_cpu,
1515 'checkout_mips64': 'mips64' in self.target_cpu,
1516 'checkout_ppc': 'ppc' in self.target_cpu,
1517 'checkout_s390': 's390' in self.target_cpu,
1518 'checkout_x64': 'x64' in self.target_cpu,
1519 'host_cpu': detect_host_arch.HostArch(),
1520 }
Robbie Iannucci3db32762023-07-05 19:02:44 +00001521
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001522 def get_vars(self):
1523 """Returns a dictionary of effective variable values
Edward Lemur8f8a50d2018-11-01 22:03:02 +00001524 (DEPS file contents with applied custom_vars overrides)."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001525 # Variable precedence (last has highest):
1526 # - DEPS vars
1527 # - parents, from first to last
1528 # - built-in
1529 # - custom_vars overrides
1530 result = {}
1531 result.update(self._vars)
1532 if self.parent:
1533 merge_vars(result, self.parent.get_vars())
1534 # Provide some built-in variables.
1535 result.update(self.get_builtin_vars())
1536 merge_vars(result, self.custom_vars)
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001537
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001538 return result
Paweł Hajdan, Jrd3790252017-07-03 21:06:24 +02001539
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001540
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001541_PLATFORM_MAPPING = {
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001542 'cygwin': 'win',
1543 'darwin': 'mac',
1544 'linux2': 'linux',
1545 'linux': 'linux',
1546 'win32': 'win',
1547 'aix6': 'aix',
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001548}
1549
1550
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001551def merge_vars(result, new_vars):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001552 for k, v in new_vars.items():
1553 if k in result:
1554 if isinstance(result[k], gclient_eval.ConstantString):
1555 if isinstance(v, gclient_eval.ConstantString):
1556 result[k] = v
1557 else:
1558 result[k].value = v
1559 else:
1560 result[k] = v
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001561 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001562 result[k] = v
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001563
1564
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001565def _detect_host_os():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001566 if sys.platform in _PLATFORM_MAPPING:
1567 return _PLATFORM_MAPPING[sys.platform]
Jonas Termansenbf7eb522023-01-19 17:56:40 +00001568
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001569 try:
1570 return os.uname().sysname.lower()
1571 except AttributeError:
1572 return sys.platform
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001573
1574
Edward Lemurb61d3872018-05-09 18:42:47 -04001575class GitDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001576 """A Dependency object that represents a single git checkout."""
1577 @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."""
1595 return gclient_scm.GitWrapper(self.url,
1596 self.root.root_dir,
1597 self.name,
1598 self.outbuf,
1599 out_cb,
1600 print_outbuf=self.print_outbuf)
Edward Lemurb61d3872018-05-09 18:42:47 -04001601
1602
1603class GClient(GitDependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001604 """Object that represent a gclient checkout. A tree of Dependency(), one per
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001605 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001606
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001607 DEPS_OS_CHOICES = {
1608 "aix6": "unix",
1609 "win32": "win",
1610 "win": "win",
1611 "cygwin": "win",
1612 "darwin": "mac",
1613 "mac": "mac",
1614 "unix": "unix",
1615 "linux": "unix",
1616 "linux2": "unix",
1617 "linux3": "unix",
1618 "android": "android",
1619 "ios": "ios",
1620 "fuchsia": "fuchsia",
1621 "chromeos": "chromeos",
1622 }
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001623
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001624 DEFAULT_CLIENT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001625solutions = [
Edward Lesmes05934952019-12-19 20:38:09 +00001626 { "name" : %(solution_name)r,
1627 "url" : %(solution_url)r,
1628 "deps_file" : %(deps_file)r,
1629 "managed" : %(managed)r,
smutae7ea312016-07-18 11:59:41 -07001630 "custom_deps" : {
1631 },
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02001632 "custom_vars": %(custom_vars)r,
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001633 },
1634]
Robert Iannuccia19649b2018-06-29 16:31:45 +00001635""")
1636
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001637 DEFAULT_CLIENT_CACHE_DIR_TEXT = ("""\
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001638cache_dir = %(cache_dir)r
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001639""")
1640
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001641 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001642# Snapshot generated with gclient revinfo --snapshot
Edward Lesmesc2960242018-03-06 20:50:15 -05001643solutions = %(solution_list)s
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001644""")
1645
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001646 def __init__(self, root_dir, options):
1647 # Do not change previous behavior. Only solution level and immediate
1648 # DEPS are processed.
1649 self._recursion_limit = 2
1650 super(GClient, self).__init__(parent=None,
1651 name=None,
1652 url=None,
1653 managed=True,
1654 custom_deps=None,
1655 custom_vars=None,
1656 custom_hooks=None,
1657 deps_file='unused',
1658 should_process=True,
1659 should_recurse=True,
1660 relative=None,
1661 condition=None,
1662 print_outbuf=True)
Edward Lemure05f18d2018-06-08 17:36:53 +00001663
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001664 self._options = options
1665 if options.deps_os:
1666 enforced_os = options.deps_os.split(',')
1667 else:
1668 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
1669 if 'all' in enforced_os:
1670 enforced_os = self.DEPS_OS_CHOICES.values()
1671 self._enforced_os = tuple(set(enforced_os))
1672 self._enforced_cpu = (detect_host_arch.HostArch(), )
1673 self._root_dir = root_dir
1674 self._cipd_root = None
1675 self.config_content = None
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001676
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001677 def _CheckConfig(self):
1678 """Verify that the config matches the state of the existing checked-out
borenet@google.com88d10082014-03-21 17:24:48 +00001679 solutions."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001680 for dep in self.dependencies:
1681 if dep.managed and dep.url:
1682 scm = dep.CreateSCM()
1683 actual_url = scm.GetActualRemoteURL(self._options)
1684 if actual_url and not scm.DoesRemoteURLMatch(self._options):
1685 mirror = scm.GetCacheMirror()
1686 if mirror:
1687 mirror_string = '%s (exists=%s)' % (mirror.mirror_path,
1688 mirror.exists())
1689 else:
1690 mirror_string = 'not used'
1691 raise gclient_utils.Error(
1692 '''
borenet@google.com88d10082014-03-21 17:24:48 +00001693Your .gclient file seems to be broken. The requested URL is different from what
borenet@google.com0a427372014-04-02 19:12:13 +00001694is actually checked out in %(checkout_path)s.
borenet@google.com88d10082014-03-21 17:24:48 +00001695
borenet@google.com97882362014-04-07 20:06:02 +00001696The .gclient file contains:
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +00001697URL: %(expected_url)s (%(expected_scm)s)
1698Cache mirror: %(mirror_string)s
borenet@google.com97882362014-04-07 20:06:02 +00001699
1700The local checkout in %(checkout_path)s reports:
1701%(actual_url)s (%(actual_scm)s)
borenet@google.com88d10082014-03-21 17:24:48 +00001702
1703You should ensure that the URL listed in .gclient is correct and either change
agabled437d762016-10-17 09:35:11 -07001704it or fix the checkout.
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00001705''' % {
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001706 'checkout_path': os.path.join(
1707 self.root_dir, dep.name),
1708 'expected_url': dep.url,
1709 'expected_scm': dep.GetScmName(),
1710 'mirror_string': mirror_string,
1711 'actual_url': actual_url,
1712 'actual_scm': dep.GetScmName()
1713 })
borenet@google.com88d10082014-03-21 17:24:48 +00001714
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001715 def SetConfig(self, content):
1716 assert not self.dependencies
1717 config_dict = {}
1718 self.config_content = content
1719 try:
1720 exec(content, config_dict)
1721 except SyntaxError as e:
1722 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001723
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001724 # Append any target OS that is not already being enforced to the tuple.
1725 target_os = config_dict.get('target_os', [])
1726 if config_dict.get('target_os_only', False):
1727 self._enforced_os = tuple(set(target_os))
1728 else:
1729 self._enforced_os = tuple(set(self._enforced_os).union(target_os))
stuartmorgan@chromium.org18a4f6a2012-11-02 13:37:24 +00001730
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001731 # Append any target CPU that is not already being enforced to the tuple.
1732 target_cpu = config_dict.get('target_cpu', [])
1733 if config_dict.get('target_cpu_only', False):
1734 self._enforced_cpu = tuple(set(target_cpu))
1735 else:
1736 self._enforced_cpu = tuple(
1737 set(self._enforced_cpu).union(target_cpu))
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001738
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001739 cache_dir = config_dict.get('cache_dir', UNSET_CACHE_DIR)
1740 if cache_dir is not UNSET_CACHE_DIR:
1741 if cache_dir:
1742 cache_dir = os.path.join(self.root_dir, cache_dir)
1743 cache_dir = os.path.abspath(cache_dir)
Andrii Shyshkalov77ce4bd2017-11-27 12:38:18 -08001744
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001745 git_cache.Mirror.SetCachePath(cache_dir)
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001746
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001747 if not target_os and config_dict.get('target_os_only', False):
1748 raise gclient_utils.Error(
1749 'Can\'t use target_os_only if target_os is '
1750 'not specified')
peter@chromium.org1efccc82012-04-27 16:34:38 +00001751
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001752 if not target_cpu and config_dict.get('target_cpu_only', False):
1753 raise gclient_utils.Error(
1754 'Can\'t use target_cpu_only if target_cpu is '
1755 'not specified')
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001756
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001757 deps_to_add = []
1758 for s in config_dict.get('solutions', []):
1759 try:
1760 deps_to_add.append(
1761 GitDependency(
1762 parent=self,
1763 name=s['name'],
1764 # Update URL with scheme in protocol_override
1765 url=GitDependency.updateProtocol(
1766 s['url'], s.get('protocol_override', None)),
1767 managed=s.get('managed', True),
1768 custom_deps=s.get('custom_deps', {}),
1769 custom_vars=s.get('custom_vars', {}),
1770 custom_hooks=s.get('custom_hooks', []),
1771 deps_file=s.get('deps_file', 'DEPS'),
1772 should_process=True,
1773 should_recurse=True,
1774 relative=None,
1775 condition=None,
1776 print_outbuf=True,
1777 # Pass protocol_override down the tree for child deps to
1778 # use.
1779 protocol=s.get('protocol_override', None),
1780 git_dependencies_state=self.git_dependencies_state))
1781 except KeyError:
1782 raise gclient_utils.Error('Invalid .gclient file. Solution is '
1783 'incomplete: %s' % s)
1784 metrics.collector.add('project_urls', [
Edward Lemuraffd4102019-06-05 18:07:49 +00001785 dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Edward Lemur40764b02018-07-20 18:50:29 +00001786 for dep in deps_to_add
1787 if dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001788 ])
Edward Lemur40764b02018-07-20 18:50:29 +00001789
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001790 self.add_dependencies_and_close(deps_to_add,
1791 config_dict.get('hooks', []))
1792 logging.info('SetConfig() done')
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001793
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001794 def SaveConfig(self):
1795 gclient_utils.FileWrite(
1796 os.path.join(self.root_dir, self._options.config_filename),
1797 self.config_content)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001798
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001799 @staticmethod
1800 def LoadCurrentConfig(options):
1801 # type: (optparse.Values) -> GClient
1802 """Searches for and loads a .gclient file relative to the current working
Joanna Wang66286612022-06-30 19:59:13 +00001803 dir."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001804 if options.spec:
1805 client = GClient('.', options)
1806 client.SetConfig(options.spec)
1807 else:
1808 if options.verbose:
1809 print('Looking for %s starting from %s\n' %
1810 (options.config_filename, os.getcwd()))
1811 path = gclient_paths.FindGclientRoot(os.getcwd(),
1812 options.config_filename)
1813 if not path:
1814 if options.verbose:
1815 print('Couldn\'t find configuration file.')
1816 return None
1817 client = GClient(path, options)
1818 client.SetConfig(
1819 gclient_utils.FileRead(
1820 os.path.join(path, options.config_filename)))
maruel@chromium.org69392e72011-10-13 22:09:00 +00001821
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001822 if (options.revisions and len(client.dependencies) > 1
1823 and any('@' not in r for r in options.revisions)):
1824 print((
1825 'You must specify the full solution name like --revision %s@%s\n'
1826 'when you have multiple solutions setup in your .gclient file.\n'
1827 'Other solutions present are: %s.') %
1828 (client.dependencies[0].name, options.revisions[0], ', '.join(
1829 s.name for s in client.dependencies[1:])),
1830 file=sys.stderr)
Joanna Wang66286612022-06-30 19:59:13 +00001831
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001832 return client
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001833
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001834 def SetDefaultConfig(self,
1835 solution_name,
1836 deps_file,
1837 solution_url,
1838 managed=True,
1839 cache_dir=UNSET_CACHE_DIR,
1840 custom_vars=None):
1841 text = self.DEFAULT_CLIENT_FILE_TEXT
1842 format_dict = {
1843 'solution_name': solution_name,
1844 'solution_url': solution_url,
1845 'deps_file': deps_file,
1846 'managed': managed,
1847 'custom_vars': custom_vars or {},
1848 }
Robert Iannuccia19649b2018-06-29 16:31:45 +00001849
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001850 if cache_dir is not UNSET_CACHE_DIR:
1851 text += self.DEFAULT_CLIENT_CACHE_DIR_TEXT
1852 format_dict['cache_dir'] = cache_dir
Robert Iannuccia19649b2018-06-29 16:31:45 +00001853
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001854 self.SetConfig(text % format_dict)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001855
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001856 def _SaveEntries(self):
1857 """Creates a .gclient_entries file to record the list of unique checkouts.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001858
1859 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001860 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001861 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
1862 # makes testing a bit too fun.
1863 result = 'entries = {\n'
1864 for entry in self.root.subtree(False):
1865 result += ' %s: %s,\n' % (pprint.pformat(
1866 entry.name), pprint.pformat(entry.url))
1867 result += '}\n'
1868 file_path = os.path.join(self.root_dir, self._options.entries_filename)
1869 logging.debug(result)
1870 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001871
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001872 def _ReadEntries(self):
1873 """Read the .gclient_entries file for the given client.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001874
1875 Returns:
1876 A sequence of solution names, which will be empty if there is the
1877 entries file hasn't been created yet.
1878 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001879 scope = {}
1880 filename = os.path.join(self.root_dir, self._options.entries_filename)
1881 if not os.path.exists(filename):
1882 return {}
1883 try:
1884 exec(gclient_utils.FileRead(filename), scope)
1885 except SyntaxError as e:
1886 gclient_utils.SyntaxErrorToError(filename, e)
1887 return scope.get('entries', {})
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001888
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001889 def _ExtractFileJsonContents(self, default_filename):
1890 # type: (str) -> Mapping[str,Any]
1891 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001892
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001893 if not os.path.exists(f):
1894 logging.info('File %s does not exist.' % f)
1895 return {}
Joanna Wang01870792022-08-01 19:02:57 +00001896
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001897 with open(f, 'r') as open_f:
1898 logging.info('Reading content from file %s' % f)
1899 content = open_f.read().rstrip()
1900 if content:
1901 return json.loads(content)
Joanna Wang66286612022-06-30 19:59:13 +00001902 return {}
1903
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001904 def _WriteFileContents(self, default_filename, content):
1905 # type: (str, str) -> None
1906 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001907
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001908 with open(f, 'w') as open_f:
1909 logging.info('Writing to file %s' % f)
1910 open_f.write(content)
Joanna Wangf3edc502022-07-20 00:12:10 +00001911
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001912 def _EnforceSkipSyncRevisions(self, patch_refs):
1913 # type: (Mapping[str, str]) -> Mapping[str, str]
1914 """Checks for and enforces revisions for skipping deps syncing."""
1915 previous_sync_commits = self._ExtractFileJsonContents(
1916 PREVIOUS_SYNC_COMMITS_FILE)
Joanna Wang66286612022-06-30 19:59:13 +00001917
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001918 if not previous_sync_commits:
1919 return {}
maruel@chromium.org918a9ae2010-05-28 15:50:30 +00001920
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001921 # Current `self.dependencies` only contain solutions. If a patch_ref is
1922 # not for a solution, then it is for a solution's dependency or recursed
1923 # dependency which we cannot support while skipping sync.
1924 if patch_refs:
1925 unclaimed_prs = []
1926 candidates = []
1927 for dep in self.dependencies:
1928 origin, _ = gclient_utils.SplitUrlRevision(dep.url)
1929 candidates.extend([origin, dep.name])
1930 for patch_repo in patch_refs:
1931 if not gclient_utils.FuzzyMatchRepo(patch_repo, candidates):
1932 unclaimed_prs.append(patch_repo)
1933 if unclaimed_prs:
1934 print(
1935 'We cannot skip syncs when there are --patch-refs flags for '
1936 'non-solution dependencies. To skip syncing, remove patch_refs '
1937 'for: \n%s' % '\n'.join(unclaimed_prs))
1938 return {}
Edward Lesmesc621b212018-03-21 20:26:56 -04001939
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001940 # We cannot skip syncing if there are custom_vars that differ from the
1941 # previous run's custom_vars.
1942 previous_custom_vars = self._ExtractFileJsonContents(
1943 PREVIOUS_CUSTOM_VARS_FILE)
1944
1945 cvs_by_name = {s.name: s.custom_vars for s in self.dependencies}
1946
1947 skip_sync_revisions = {}
1948 for name, commit in previous_sync_commits.items():
1949 previous_vars = previous_custom_vars.get(name)
1950 if previous_vars == cvs_by_name.get(name) or (
1951 not previous_vars and not cvs_by_name.get(name)):
1952 skip_sync_revisions[name] = commit
1953 else:
1954 print(
1955 'We cannot skip syncs when custom_vars for solutions have '
1956 'changed since the last sync run on this machine.\n'
1957 '\nRemoving skip_sync_revision for:\n'
1958 'solution: %s, current: %r, previous: %r.' %
1959 (name, cvs_by_name.get(name), previous_vars))
1960 print('no-sync experiment enabled with %r' % skip_sync_revisions)
1961 return skip_sync_revisions
1962
1963 # TODO(crbug.com/1340695): Remove handling revisions without '@'.
1964 def _EnforceRevisions(self):
1965 """Checks for revision overrides."""
1966 revision_overrides = {}
1967 if self._options.head:
1968 return revision_overrides
1969 if not self._options.revisions:
1970 return revision_overrides
1971 solutions_names = [s.name for s in self.dependencies]
1972 for index, revision in enumerate(self._options.revisions):
1973 if not '@' in revision:
1974 # Support for --revision 123
1975 revision = '%s@%s' % (solutions_names[index], revision)
1976 name, rev = revision.split('@', 1)
1977 revision_overrides[name] = rev
1978 return revision_overrides
1979
1980 def _EnforcePatchRefsAndBranches(self):
1981 # type: () -> Tuple[Mapping[str, str], Mapping[str, str]]
1982 """Checks for patch refs."""
1983 patch_refs = {}
1984 target_branches = {}
1985 if not self._options.patch_refs:
1986 return patch_refs, target_branches
1987 for given_patch_ref in self._options.patch_refs:
1988 patch_repo, _, patch_ref = given_patch_ref.partition('@')
1989 if not patch_repo or not patch_ref or ':' not in patch_ref:
1990 raise gclient_utils.Error(
1991 'Wrong revision format: %s should be of the form '
1992 'patch_repo@target_branch:patch_ref.' % given_patch_ref)
1993 target_branch, _, patch_ref = patch_ref.partition(':')
1994 target_branches[patch_repo] = target_branch
1995 patch_refs[patch_repo] = patch_ref
1996 return patch_refs, target_branches
1997
Gavin Mak50b27a52023-09-19 22:44:59 +00001998 def _InstallPreCommitHook(self):
1999 # On Windows, this path is written to the file as
2000 # "dir\hooks\pre-commit.py" but it gets interpreted as
2001 # "dirhookspre-commit.py".
2002 gclient_hook_path = os.path.join(DEPOT_TOOLS_DIR, 'hooks',
2003 'pre-commit.py').replace('\\', '\\\\')
2004 gclient_hook_content = '\n'.join((
2005 f'{PRECOMMIT_HOOK_VAR}={gclient_hook_path}',
2006 f'if [ -f "${PRECOMMIT_HOOK_VAR}" ]; then',
2007 f' python3 "${PRECOMMIT_HOOK_VAR}" || exit 1',
2008 'fi',
2009 ))
2010
2011 soln = gclient_paths.GetPrimarySolutionPath()
2012 if not soln:
2013 print('Could not find gclient solution.')
2014 return
2015
2016 git_dir = os.path.join(soln, '.git')
2017 if not os.path.exists(git_dir):
2018 return
2019
Philipp Wollermanna45d2d42023-09-21 02:45:42 +00002020 git_hooks_dir = os.path.join(git_dir, 'hooks')
2021 os.makedirs(git_hooks_dir, exist_ok=True)
2022
Gavin Mak50b27a52023-09-19 22:44:59 +00002023 hook = os.path.join(git_dir, 'hooks', 'pre-commit')
2024 if os.path.exists(hook):
2025 with open(hook, 'r') as f:
2026 content = f.read()
2027 if PRECOMMIT_HOOK_VAR in content:
2028 print(f'{hook} already contains the gclient pre-commit hook.')
2029 else:
2030 print(f'A pre-commit hook already exists at {hook}.\n'
2031 f'Please append the following lines to the hook:\n\n'
2032 f'{gclient_hook_content}')
2033 return
2034
2035 print(f'Creating a pre-commit hook at {hook}')
2036 with open(hook, 'w') as f:
2037 f.write('#!/bin/sh\n')
2038 f.write(f'{gclient_hook_content}\n')
2039 os.chmod(hook, 0o755)
2040
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002041 def _RemoveUnversionedGitDirs(self):
2042 """Remove directories that are no longer part of the checkout.
Edward Lemur5b1fa942018-10-04 23:22:09 +00002043
2044 Notify the user if there is an orphaned entry in their working copy.
2045 Only delete the directory if there are no changes in it, and
2046 delete_unversioned_trees is set to true.
Josip Sokcevic1b8211f2022-09-30 17:46:53 +00002047
2048 Returns CIPD packages that are no longer versioned.
Edward Lemur5b1fa942018-10-04 23:22:09 +00002049 """
2050
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002051 entry_names_and_sync = [(i.name, i._should_sync)
2052 for i in self.root.subtree(False) if i.url]
2053 entries = []
2054 if entry_names_and_sync:
2055 entries, _ = zip(*entry_names_and_sync)
2056 full_entries = [
2057 os.path.join(self.root_dir, e.replace('/', os.path.sep))
2058 for e in entries
2059 ]
2060 no_sync_entries = [
2061 name for name, should_sync in entry_names_and_sync
2062 if not should_sync
2063 ]
Edward Lemur5b1fa942018-10-04 23:22:09 +00002064
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002065 removed_cipd_entries = []
Joanna Wang60adf7b2023-10-06 00:04:28 +00002066 read_entries = self._ReadEntries()
2067 # We process entries sorted in reverse to ensure a child dir is
2068 # always deleted before its parent dir.
2069 # This is especially important for submodules with pinned revisions
2070 # overwritten by a vars or custom_deps. In this case, if a parent
2071 # submodule is encountered first in the loop, it cannot tell the
2072 # difference between modifications from the vars or actual user
2073 # modifications that should be kept. http://crbug/1486677#c9 for
2074 # more details.
2075 for entry in sorted(read_entries, reverse=True):
2076 prev_url = read_entries[entry]
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002077 if not prev_url:
2078 # entry must have been overridden via .gclient custom_deps
2079 continue
2080 if any(entry.startswith(sln) for sln in no_sync_entries):
2081 # Dependencies of solutions that skipped syncing would not
2082 # show up in `entries`.
2083 continue
2084 if (':' in entry):
2085 # This is a cipd package. Don't clean it up, but prepare for
2086 # return
2087 if entry not in entries:
2088 removed_cipd_entries.append(entry)
2089 continue
2090 # Fix path separator on Windows.
2091 entry_fixed = entry.replace('/', os.path.sep)
2092 e_dir = os.path.join(self.root_dir, entry_fixed)
2093 # Use entry and not entry_fixed there.
2094 if (entry not in entries and
2095 (not any(path.startswith(entry + '/') for path in entries))
2096 and os.path.exists(e_dir)):
2097 # The entry has been removed from DEPS.
2098 scm = gclient_scm.GitWrapper(prev_url, self.root_dir,
2099 entry_fixed, self.outbuf)
Edward Lemur5b1fa942018-10-04 23:22:09 +00002100
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002101 # Check to see if this directory is now part of a higher-up
2102 # checkout.
2103 scm_root = None
2104 try:
2105 scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2106 scm.checkout_path)
2107 except subprocess2.CalledProcessError:
2108 pass
2109 if not scm_root:
2110 logging.warning(
2111 'Could not find checkout root for %s. Unable to '
2112 'determine whether it is part of a higher-level '
2113 'checkout, so not removing.' % entry)
2114 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002115
Joanna Wang60adf7b2023-10-06 00:04:28 +00002116 versioned_state = None
2117 # Check if this is a submodule or versioned directory.
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002118 if os.path.abspath(scm_root) == os.path.abspath(e_dir):
2119 e_par_dir = os.path.join(e_dir, os.pardir)
2120 if gclient_scm.scm.GIT.IsInsideWorkTree(e_par_dir):
2121 par_scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2122 e_par_dir)
2123 # rel_e_dir : relative path of entry w.r.t. its parent
2124 # repo.
2125 rel_e_dir = os.path.relpath(e_dir, par_scm_root)
Joanna Wang60adf7b2023-10-06 00:04:28 +00002126 versioned_state = gclient_scm.scm.GIT.IsVersioned(
2127 par_scm_root, rel_e_dir)
2128 # This is to handle the case of third_party/WebKit migrating
2129 # from being a DEPS entry to being part of the main project. If
2130 # the subproject is a Git project, we need to remove its .git
2131 # folder. Otherwise git operations on that folder will have
2132 # different effects depending on the current working directory.
2133 if versioned_state == gclient_scm.scm.VERSIONED_DIR:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002134 save_dir = scm.GetGitBackupDirPath()
2135 # Remove any eventual stale backup dir for the same
2136 # project.
2137 if os.path.exists(save_dir):
2138 gclient_utils.rmtree(save_dir)
2139 os.rename(os.path.join(e_dir, '.git'), save_dir)
2140 # When switching between the two states (entry/ is a
2141 # subproject -> entry/ is part of the outer
2142 # project), it is very likely that some files are
2143 # changed in the checkout, unless we are jumping
2144 # *exactly* across the commit which changed just
2145 # DEPS. In such case we want to cleanup any eventual
2146 # stale files (coming from the old subproject) in
2147 # order to end up with a clean checkout.
2148 gclient_scm.scm.GIT.CleanupDir(
2149 par_scm_root, rel_e_dir)
2150 assert not os.path.exists(
2151 os.path.join(e_dir, '.git'))
2152 print(
2153 '\nWARNING: \'%s\' has been moved from DEPS to a higher '
2154 'level checkout. The git folder containing all the local'
2155 ' branches has been saved to %s.\n'
2156 'If you don\'t care about its state you can safely '
2157 'remove that folder to free up space.' %
2158 (entry, save_dir))
2159 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002160
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002161 if scm_root in full_entries:
2162 logging.info(
2163 '%s is part of a higher level checkout, not removing',
2164 scm.GetCheckoutRoot())
2165 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002166
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002167 file_list = []
2168 scm.status(self._options, [], file_list)
2169 modified_files = file_list != []
2170 if (not self._options.delete_unversioned_trees
2171 or (modified_files and not self._options.force)):
2172 # There are modified files in this entry. Keep warning until
2173 # removed.
2174 self.add_dependency(
2175 GitDependency(
2176 parent=self,
2177 name=entry,
2178 # Update URL with scheme in protocol_override
2179 url=GitDependency.updateProtocol(
2180 prev_url, self.protocol),
2181 managed=False,
2182 custom_deps={},
2183 custom_vars={},
2184 custom_hooks=[],
2185 deps_file=None,
2186 should_process=True,
2187 should_recurse=False,
2188 relative=None,
2189 condition=None,
2190 protocol=self.protocol,
2191 git_dependencies_state=self.git_dependencies_state))
2192 if modified_files and self._options.delete_unversioned_trees:
2193 print(
2194 '\nWARNING: \'%s\' is no longer part of this client.\n'
2195 'Despite running \'gclient sync -D\' no action was taken '
2196 'as there are modifications.\nIt is recommended you revert '
2197 'all changes or run \'gclient sync -D --force\' next '
2198 'time.' % entry_fixed)
2199 else:
2200 print(
2201 '\nWARNING: \'%s\' is no longer part of this client.\n'
2202 'It is recommended that you manually remove it or use '
2203 '\'gclient sync -D\' next time.' % entry_fixed)
2204 else:
2205 # Delete the entry
2206 print('\n________ deleting \'%s\' in \'%s\'' %
2207 (entry_fixed, self.root_dir))
2208 gclient_utils.rmtree(e_dir)
Joanna Wang60adf7b2023-10-06 00:04:28 +00002209 # We restore empty directories of submodule paths.
2210 if versioned_state == gclient_scm.scm.VERSIONED_SUBMODULE:
2211 gclient_scm.scm.GIT.Capture(
2212 ['restore', '--', rel_e_dir], cwd=par_scm_root)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002213 # record the current list of entries for next time
2214 self._SaveEntries()
2215 return removed_cipd_entries
Edward Lemur5b1fa942018-10-04 23:22:09 +00002216
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002217 def RunOnDeps(self,
2218 command,
2219 args,
2220 ignore_requirements=False,
2221 progress=True):
2222 """Runs a command on each dependency in a client and its dependencies.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002223
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002224 Args:
2225 command: The command to use (e.g., 'status' or 'diff')
2226 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002227 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002228 if not self.dependencies:
2229 raise gclient_utils.Error('No solution specified')
Michael Mossd683d7c2018-06-15 05:05:17 +00002230
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002231 revision_overrides = {}
2232 patch_refs = {}
2233 target_branches = {}
2234 skip_sync_revisions = {}
2235 # It's unnecessary to check for revision overrides for 'recurse'.
2236 # Save a few seconds by not calling _EnforceRevisions() in that case.
2237 if command not in ('diff', 'recurse', 'runhooks', 'status', 'revert',
2238 'validate'):
2239 self._CheckConfig()
2240 revision_overrides = self._EnforceRevisions()
Edward Lesmesc621b212018-03-21 20:26:56 -04002241
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002242 if command == 'update':
2243 patch_refs, target_branches = self._EnforcePatchRefsAndBranches()
2244 if NO_SYNC_EXPERIMENT in self._options.experiments:
2245 skip_sync_revisions = self._EnforceSkipSyncRevisions(patch_refs)
Joanna Wang66286612022-06-30 19:59:13 +00002246
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002247 # Store solutions' custom_vars on memory to compare in the next run.
2248 # All dependencies added later are inherited from the current
2249 # self.dependencies.
2250 custom_vars = {
2251 dep.name: dep.custom_vars
2252 for dep in self.dependencies if dep.custom_vars
Michael Mossd683d7c2018-06-15 05:05:17 +00002253 }
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002254 if custom_vars:
2255 self._WriteFileContents(PREVIOUS_CUSTOM_VARS_FILE,
2256 json.dumps(custom_vars))
2257
2258 # Disable progress for non-tty stdout.
2259 should_show_progress = (setup_color.IS_TTY and not self._options.verbose
2260 and progress)
2261 pm = None
2262 if should_show_progress:
2263 if command in ('update', 'revert'):
2264 pm = Progress('Syncing projects', 1)
2265 elif command in ('recurse', 'validate'):
2266 pm = Progress(' '.join(args), 1)
2267 work_queue = gclient_utils.ExecutionQueue(
2268 self._options.jobs,
2269 pm,
2270 ignore_requirements=ignore_requirements,
2271 verbose=self._options.verbose)
2272 for s in self.dependencies:
2273 if s.should_process:
2274 work_queue.enqueue(s)
2275 work_queue.flush(revision_overrides,
2276 command,
2277 args,
2278 options=self._options,
2279 patch_refs=patch_refs,
2280 target_branches=target_branches,
2281 skip_sync_revisions=skip_sync_revisions)
2282
2283 if revision_overrides:
2284 print(
2285 'Please fix your script, having invalid --revision flags will soon '
2286 'be considered an error.',
2287 file=sys.stderr)
2288
2289 if patch_refs:
2290 raise gclient_utils.Error(
2291 'The following --patch-ref flags were not used. Please fix it:\n%s'
2292 % ('\n'.join(patch_repo + '@' + patch_ref
2293 for patch_repo, patch_ref in patch_refs.items())))
2294
2295 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
2296 # they have fsmonitor enabled.
2297 if command == 'update':
2298 # Check if any of the root dependency have submodules.
2299 is_submoduled = any(
2300 map(
2301 lambda d: d.git_dependencies_state in
2302 (gclient_eval.SUBMODULES, gclient_eval.SYNC),
2303 self.dependencies))
2304 if is_submoduled:
2305 git_common.warn_submodule()
2306
2307 # Once all the dependencies have been processed, it's now safe to write
2308 # out the gn_args_file and run the hooks.
2309 removed_cipd_entries = []
2310 if command == 'update':
2311 for dependency in self.dependencies:
2312 gn_args_dep = dependency
2313 if gn_args_dep._gn_args_from:
2314 deps_map = {
2315 dep.name: dep
2316 for dep in gn_args_dep.dependencies
2317 }
2318 gn_args_dep = deps_map.get(gn_args_dep._gn_args_from)
2319 if gn_args_dep and gn_args_dep.HasGNArgsFile():
2320 gn_args_dep.WriteGNArgsFile()
2321
2322 removed_cipd_entries = self._RemoveUnversionedGitDirs()
2323
2324 # Sync CIPD dependencies once removed deps are deleted. In case a git
2325 # dependency was moved to CIPD, we want to remove the old git directory
2326 # first and then sync the CIPD dep.
2327 if self._cipd_root:
2328 self._cipd_root.run(command)
2329 # It's possible that CIPD removed some entries that are now part of
2330 # git worktree. Try to checkout those directories
2331 if removed_cipd_entries:
2332 for cipd_entry in removed_cipd_entries:
2333 cwd = os.path.join(self._root_dir, cipd_entry.split(':')[0])
2334 cwd, tail = os.path.split(cwd)
2335 if cwd:
2336 try:
2337 gclient_scm.scm.GIT.Capture(['checkout', tail],
2338 cwd=cwd)
Joanna Wang60adf7b2023-10-06 00:04:28 +00002339 except (subprocess2.CalledProcessError, OSError):
2340 # repo of the deleted cipd may also have been deleted.
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002341 pass
2342
2343 if not self._options.nohooks:
2344 if should_show_progress:
2345 pm = Progress('Running hooks', 1)
2346 self.RunHooksRecursively(self._options, pm)
2347
2348 self._WriteFileContents(PREVIOUS_SYNC_COMMITS_FILE,
2349 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
2350
2351 return 0
2352
2353 def PrintRevInfo(self):
2354 if not self.dependencies:
2355 raise gclient_utils.Error('No solution specified')
2356 # Load all the settings.
2357 work_queue = gclient_utils.ExecutionQueue(self._options.jobs,
2358 None,
2359 False,
2360 verbose=self._options.verbose)
2361 for s in self.dependencies:
2362 if s.should_process:
2363 work_queue.enqueue(s)
2364 work_queue.flush({},
2365 None, [],
2366 options=self._options,
2367 patch_refs=None,
2368 target_branches=None,
2369 skip_sync_revisions=None)
2370
2371 def ShouldPrintRevision(dep):
2372 return (not self._options.filter
2373 or dep.FuzzyMatchUrl(self._options.filter))
2374
2375 if self._options.snapshot:
2376 json_output = []
2377 # First level at .gclient
2378 for d in self.dependencies:
2379 entries = {}
2380
2381 def GrabDeps(dep):
2382 """Recursively grab dependencies."""
2383 for rec_d in dep.dependencies:
2384 rec_d.PinToActualRevision()
2385 if ShouldPrintRevision(rec_d):
2386 entries[rec_d.name] = rec_d.url
2387 GrabDeps(rec_d)
2388
2389 GrabDeps(d)
2390 json_output.append({
2391 'name': d.name,
2392 'solution_url': d.url,
2393 'deps_file': d.deps_file,
2394 'managed': d.managed,
2395 'custom_deps': entries,
2396 })
2397 if self._options.output_json == '-':
2398 print(json.dumps(json_output, indent=2, separators=(',', ': ')))
2399 elif self._options.output_json:
2400 with open(self._options.output_json, 'w') as f:
2401 json.dump(json_output, f)
2402 else:
2403 # Print the snapshot configuration file
2404 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {
2405 'solution_list': pprint.pformat(json_output, indent=2),
2406 })
Michael Mossd683d7c2018-06-15 05:05:17 +00002407 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002408 entries = {}
2409 for d in self.root.subtree(False):
2410 if self._options.actual:
2411 d.PinToActualRevision()
2412 if ShouldPrintRevision(d):
2413 entries[d.name] = d.url
2414 if self._options.output_json:
2415 json_output = {
2416 name: {
2417 'url': rev.split('@')[0] if rev else None,
2418 'rev':
2419 rev.split('@')[1] if rev and '@' in rev else None,
2420 }
2421 for name, rev in entries.items()
2422 }
2423 if self._options.output_json == '-':
2424 print(
2425 json.dumps(json_output,
2426 indent=2,
2427 separators=(',', ': ')))
2428 else:
2429 with open(self._options.output_json, 'w') as f:
2430 json.dump(json_output, f)
2431 else:
2432 keys = sorted(entries.keys())
2433 for x in keys:
2434 print('%s: %s' % (x, entries[x]))
2435 logging.info(str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002436
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002437 def ParseDepsFile(self):
2438 """No DEPS to parse for a .gclient file."""
2439 raise gclient_utils.Error('Internal error')
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00002440
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002441 def PrintLocationAndContents(self):
2442 # Print out the .gclient file. This is longer than if we just printed
2443 # the client dict, but more legible, and it might contain helpful
2444 # comments.
2445 print('Loaded .gclient config in %s:\n%s' %
2446 (self.root_dir, self.config_content))
sergiyb@chromium.orgfa2707e2016-03-12 00:40:56 +00002447
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002448 def GetCipdRoot(self):
2449 if not self._cipd_root:
2450 self._cipd_root = gclient_scm.CipdRoot(
2451 self.root_dir,
2452 # TODO(jbudorick): Support other service URLs as necessary.
2453 # Service URLs should be constant over the scope of a cipd
2454 # root, so a var per DEPS file specifying the service URL
2455 # should suffice.
Yiwei Zhang52353702023-09-18 15:53:52 +00002456 'https://chrome-infra-packages.appspot.com',
2457 log_level='info' if self._options.verbose else None)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002458 return self._cipd_root
John Budorickd3ba72b2018-03-20 12:27:42 -07002459
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002460 @property
2461 def root_dir(self):
2462 """Root directory of gclient checkout."""
2463 return self._root_dir
maruel@chromium.org75a59272010-06-11 22:34:03 +00002464
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002465 @property
2466 def enforced_os(self):
2467 """What deps_os entries that are to be parsed."""
2468 return self._enforced_os
maruel@chromium.org271375b2010-06-23 19:17:38 +00002469
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002470 @property
2471 def target_os(self):
2472 return self._enforced_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +00002473
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002474 @property
2475 def target_cpu(self):
2476 return self._enforced_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -08002477
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002478
John Budorick0f7b2002018-01-19 15:46:17 -08002479class CipdDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002480 """A Dependency object that represents a single CIPD package."""
2481 def __init__(self, parent, name, dep_value, cipd_root, custom_vars,
2482 should_process, relative, condition):
2483 package = dep_value['package']
2484 version = dep_value['version']
2485 url = urllib.parse.urljoin(cipd_root.service_url,
2486 '%s@%s' % (package, version))
2487 super(CipdDependency, self).__init__(parent=parent,
2488 name=name + ':' + package,
2489 url=url,
2490 managed=None,
2491 custom_deps=None,
2492 custom_vars=custom_vars,
2493 custom_hooks=None,
2494 deps_file=None,
2495 should_process=should_process,
2496 should_recurse=False,
2497 relative=relative,
2498 condition=condition)
2499 self._cipd_package = None
2500 self._cipd_root = cipd_root
2501 # CIPD wants /-separated paths, even on Windows.
2502 native_subdir_path = os.path.relpath(
2503 os.path.join(self.root.root_dir, name), cipd_root.root_dir)
2504 self._cipd_subdir = posixpath.join(*native_subdir_path.split(os.sep))
2505 self._package_name = package
2506 self._package_version = version
John Budorick0f7b2002018-01-19 15:46:17 -08002507
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002508 #override
2509 def run(self, revision_overrides, command, args, work_queue, options,
2510 patch_refs, target_branches, skip_sync_revisions):
2511 """Runs |command| then parse the DEPS file."""
2512 logging.info('CipdDependency(%s).run()' % self.name)
2513 if not self.should_process:
2514 return
2515 self._CreatePackageIfNecessary()
2516 super(CipdDependency,
2517 self).run(revision_overrides, command, args, work_queue, options,
2518 patch_refs, target_branches, skip_sync_revisions)
John Budorickd3ba72b2018-03-20 12:27:42 -07002519
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002520 def _CreatePackageIfNecessary(self):
2521 # We lazily create the CIPD package to make sure that only packages
2522 # that we want (as opposed to all packages defined in all DEPS files
2523 # we parse) get added to the root and subsequently ensured.
2524 if not self._cipd_package:
2525 self._cipd_package = self._cipd_root.add_package(
2526 self._cipd_subdir, self._package_name, self._package_version)
John Budorickd3ba72b2018-03-20 12:27:42 -07002527
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002528 def ParseDepsFile(self):
2529 """CIPD dependencies are not currently allowed to have nested deps."""
2530 self.add_dependencies_and_close([], [])
John Budorick0f7b2002018-01-19 15:46:17 -08002531
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002532 #override
2533 def verify_validity(self):
2534 """CIPD dependencies allow duplicate name for packages in same directory."""
2535 logging.info('Dependency(%s).verify_validity()' % self.name)
2536 return True
John Budorick0f7b2002018-01-19 15:46:17 -08002537
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002538 #override
2539 def GetScmName(self):
2540 """Always 'cipd'."""
2541 return 'cipd'
Shenghua Zhang6f830312018-02-26 11:45:07 -08002542
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002543 def GetExpandedPackageName(self):
2544 """Return the CIPD package name with the variables evaluated."""
2545 package = self._cipd_root.expand_package_name(self._package_name)
2546 if package:
2547 return package
2548 return self._package_name
John Budorick0f7b2002018-01-19 15:46:17 -08002549
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002550 #override
2551 def CreateSCM(self, out_cb=None):
2552 """Create a Wrapper instance suitable for handling this CIPD dependency."""
2553 self._CreatePackageIfNecessary()
2554 return gclient_scm.CipdWrapper(self.url,
2555 self.root.root_dir,
2556 self.name,
2557 self.outbuf,
2558 out_cb,
2559 root=self._cipd_root,
2560 package=self._cipd_package)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +00002561
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002562 def hierarchy(self, include_url=False, graphviz=False):
2563 if graphviz:
2564 return '' # graphviz lines not implemented for cipd deps.
2565 return self.parent.hierarchy(include_url) + ' -> ' + self._cipd_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08002566
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002567 def ToLines(self):
2568 # () -> Sequence[str]
2569 """Return a list of lines representing this in a DEPS file."""
2570 def escape_cipd_var(package):
2571 return package.replace('{', '{{').replace('}', '}}')
Edward Lemure4e15042018-06-28 18:07:00 +00002572
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002573 s = []
2574 self._CreatePackageIfNecessary()
2575 if self._cipd_package.authority_for_subdir:
2576 condition_part = ([' "condition": %r,' %
2577 self.condition] if self.condition else [])
2578 s.extend([
2579 ' # %s' % self.hierarchy(include_url=False),
2580 ' "%s": {' % (self.name.split(':')[0], ),
2581 ' "packages": [',
2582 ])
2583 for p in sorted(self._cipd_root.packages(self._cipd_subdir),
2584 key=lambda x: x.name):
2585 s.extend([
2586 ' {',
2587 ' "package": "%s",' % escape_cipd_var(p.name),
2588 ' "version": "%s",' % p.version,
2589 ' },',
2590 ])
John Budorickc35aba52018-06-28 20:57:03 +00002591
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002592 s.extend([
2593 ' ],',
2594 ' "dep_type": "cipd",',
2595 ] + condition_part + [
2596 ' },',
2597 '',
2598 ])
2599 return s
John Budorick0f7b2002018-01-19 15:46:17 -08002600
2601
maruel@chromium.org5ca27692010-05-26 19:32:41 +00002602#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002603
2604
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002605@subcommand.usage('[command] [args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002606@metrics.collector.collect_metrics('gclient recurse')
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002607def CMDrecurse(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002608 """Operates [command args ...] on all the dependencies.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002609
Arthur Milchior08cd5fe2022-07-28 20:38:47 +00002610 Change directory to each dependency's directory, and call [command
2611 args ...] there. Sets GCLIENT_DEP_PATH environment variable as the
2612 dep's relative location to root directory of the checkout.
2613
2614 Examples:
2615 * `gclient recurse --no-progress -j1 sh -c 'echo "$GCLIENT_DEP_PATH"'`
2616 print the relative path of each dependency.
2617 * `gclient recurse --no-progress -j1 sh -c "pwd"`
2618 print the absolute path of each dependency.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002619 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002620 # Stop parsing at the first non-arg so that these go through to the command
2621 parser.disable_interspersed_args()
2622 parser.add_option('-s',
2623 '--scm',
2624 action='append',
2625 default=[],
2626 help='Choose scm types to operate upon.')
2627 parser.add_option('-i',
2628 '--ignore',
2629 action='store_true',
2630 help='Ignore non-zero return codes from subcommands.')
2631 parser.add_option(
2632 '--prepend-dir',
2633 action='store_true',
2634 help='Prepend relative dir for use with git <cmd> --null.')
2635 parser.add_option(
2636 '--no-progress',
2637 action='store_true',
2638 help='Disable progress bar that shows sub-command updates')
2639 options, args = parser.parse_args(args)
2640 if not args:
2641 print('Need to supply a command!', file=sys.stderr)
2642 return 1
2643 root_and_entries = gclient_utils.GetGClientRootAndEntries()
2644 if not root_and_entries:
2645 print(
2646 'You need to run gclient sync at least once to use \'recurse\'.\n'
2647 'This is because .gclient_entries needs to exist and be up to date.',
2648 file=sys.stderr)
2649 return 1
davidbarr@chromium.org47ca0ee2012-03-02 16:06:11 +00002650
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002651 # Normalize options.scm to a set()
2652 scm_set = set()
2653 for scm in options.scm:
2654 scm_set.update(scm.split(','))
2655 options.scm = scm_set
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002656
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002657 options.nohooks = True
2658 client = GClient.LoadCurrentConfig(options)
2659 if not client:
2660 raise gclient_utils.Error(
2661 'client not configured; see \'gclient config\'')
2662 return client.RunOnDeps('recurse',
2663 args,
2664 ignore_requirements=True,
2665 progress=not options.no_progress)
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002666
2667
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002668@subcommand.usage('[args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002669@metrics.collector.collect_metrics('gclient fetch')
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002670def CMDfetch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002671 """Fetches upstream commits for all modules.
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002672
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002673 Completely git-specific. Simply runs 'git fetch [args ...]' for each module.
2674 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002675 (options, args) = parser.parse_args(args)
2676 return CMDrecurse(
2677 OptionParser(),
2678 ['--jobs=%d' % options.jobs, '--scm=git', 'git', 'fetch'] + args)
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00002679
2680
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002681class Flattener(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002682 """Flattens a gclient solution."""
2683 def __init__(self, client, pin_all_deps=False):
2684 """Constructor.
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002685
2686 Arguments:
2687 client (GClient): client to flatten
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002688 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2689 in DEPS
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002690 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002691 self._client = client
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002692
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002693 self._deps_string = None
2694 self._deps_graph_lines = None
2695 self._deps_files = set()
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002696
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002697 self._allowed_hosts = set()
2698 self._deps = {}
2699 self._hooks = []
2700 self._pre_deps_hooks = []
2701 self._vars = {}
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002702
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002703 self._flatten(pin_all_deps=pin_all_deps)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002704
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002705 @property
2706 def deps_string(self):
2707 assert self._deps_string is not None
2708 return self._deps_string
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002709
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002710 @property
2711 def deps_graph_lines(self):
2712 assert self._deps_graph_lines is not None
2713 return self._deps_graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00002714
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002715 @property
2716 def deps_files(self):
2717 return self._deps_files
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002718
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002719 def _pin_dep(self, dep):
2720 """Pins a dependency to specific full revision sha.
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002721
2722 Arguments:
2723 dep (Dependency): dependency to process
2724 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002725 if dep.url is None:
2726 return
Michael Mossd683d7c2018-06-15 05:05:17 +00002727
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002728 # Make sure the revision is always fully specified (a hash),
2729 # as opposed to refs or tags which might change. Similarly,
2730 # shortened shas might become ambiguous; make sure to always
2731 # use full one for pinning.
2732 revision = gclient_utils.SplitUrlRevision(dep.url)[1]
2733 if not revision or not gclient_utils.IsFullGitSha(revision):
2734 dep.PinToActualRevision()
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002735
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002736 def _flatten(self, pin_all_deps=False):
2737 """Runs the flattener. Saves resulting DEPS string.
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002738
2739 Arguments:
2740 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2741 in DEPS
2742 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002743 for solution in self._client.dependencies:
2744 self._add_dep(solution)
2745 self._flatten_dep(solution)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002746
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002747 if pin_all_deps:
2748 for dep in self._deps.values():
2749 self._pin_dep(dep)
Paweł Hajdan, Jr39300ba2017-08-11 16:52:38 +02002750
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002751 def add_deps_file(dep):
2752 # Only include DEPS files referenced by recursedeps.
2753 if not dep.should_recurse:
2754 return
2755 deps_file = dep.deps_file
2756 deps_path = os.path.join(self._client.root_dir, dep.name, deps_file)
2757 if not os.path.exists(deps_path):
2758 # gclient has a fallback that if deps_file doesn't exist, it'll
2759 # try DEPS. Do the same here.
2760 deps_file = 'DEPS'
2761 deps_path = os.path.join(self._client.root_dir, dep.name,
2762 deps_file)
2763 if not os.path.exists(deps_path):
2764 return
2765 assert dep.url
2766 self._deps_files.add((dep.url, deps_file, dep.hierarchy_data()))
Paweł Hajdan, Jr76a9d042017-08-18 20:05:41 +02002767
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002768 for dep in self._deps.values():
2769 add_deps_file(dep)
Joanna Wang9144b672023-02-24 23:36:17 +00002770
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002771 gn_args_dep = self._deps.get(self._client.dependencies[0]._gn_args_from,
2772 self._client.dependencies[0])
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002773
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002774 self._deps_graph_lines = _DepsToDotGraphLines(self._deps)
2775 self._deps_string = '\n'.join(
2776 _GNSettingsToLines(gn_args_dep._gn_args_file, gn_args_dep._gn_args)
2777 + _AllowedHostsToLines(self._allowed_hosts) +
2778 _DepsToLines(self._deps) + _HooksToLines('hooks', self._hooks) +
2779 _HooksToLines('pre_deps_hooks', self._pre_deps_hooks) +
2780 _VarsToLines(self._vars) + [
2781 '# %s, %s' % (url, deps_file)
2782 for url, deps_file, _ in sorted(self._deps_files)
2783 ] + ['']) # Ensure newline at end of file.
2784
2785 def _add_dep(self, dep):
2786 """Helper to add a dependency to flattened DEPS.
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002787
2788 Arguments:
2789 dep (Dependency): dependency to add
2790 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002791 assert dep.name not in self._deps or self._deps.get(
2792 dep.name) == dep, (dep.name, self._deps.get(dep.name))
2793 if dep.url:
2794 self._deps[dep.name] = dep
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002795
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002796 def _flatten_dep(self, dep):
2797 """Visits a dependency in order to flatten it (see CMDflatten).
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002798
2799 Arguments:
2800 dep (Dependency): dependency to process
2801 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002802 logging.debug('_flatten_dep(%s)', dep.name)
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002803
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002804 assert dep.deps_parsed, (
2805 "Attempted to flatten %s but it has not been processed." % dep.name)
Paweł Hajdan, Jrc69b32e2017-08-17 18:47:48 +02002806
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002807 self._allowed_hosts.update(dep.allowed_hosts)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002808
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002809 # Only include vars explicitly listed in the DEPS files or gclient
2810 # solution, not automatic, local overrides (i.e. not all of
2811 # dep.get_vars()).
2812 hierarchy = dep.hierarchy(include_url=False)
2813 for key, value in dep._vars.items():
2814 # Make sure there are no conflicting variables. It is fine however
2815 # to use same variable name, as long as the value is consistent.
2816 assert key not in self._vars or self._vars[key][1] == value, (
2817 "dep:%s key:%s value:%s != %s" %
2818 (dep.name, key, value, self._vars[key][1]))
2819 self._vars[key] = (hierarchy, value)
2820 # Override explicit custom variables.
2821 for key, value in dep.custom_vars.items():
2822 # Do custom_vars that don't correspond to DEPS vars ever make sense?
2823 # DEPS conditionals shouldn't be using vars that aren't also defined
2824 # in the DEPS (presubmit actually disallows this), so any new
2825 # custom_var must be unused in the DEPS, so no need to add it to the
2826 # flattened output either.
2827 if key not in self._vars:
2828 continue
2829 # Don't "override" existing vars if it's actually the same value.
2830 if self._vars[key][1] == value:
2831 continue
2832 # Anything else is overriding a default value from the DEPS.
2833 self._vars[key] = (hierarchy + ' [custom_var override]', value)
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02002834
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002835 self._pre_deps_hooks.extend([(dep, hook)
2836 for hook in dep.pre_deps_hooks])
2837 self._hooks.extend([(dep, hook) for hook in dep.deps_hooks])
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002838
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002839 for sub_dep in dep.dependencies:
2840 self._add_dep(sub_dep)
Paweł Hajdan, Jrb0ad16e2017-08-03 15:33:21 +02002841
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002842 for d in dep.dependencies:
2843 if d.should_recurse:
2844 self._flatten_dep(d)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002845
2846
Joanna Wang3ab2f212023-08-09 01:25:15 +00002847@metrics.collector.collect_metrics('gclient gitmodules')
2848def CMDgitmodules(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002849 """Adds or updates Git Submodules based on the contents of the DEPS file.
Joanna Wang3ab2f212023-08-09 01:25:15 +00002850
2851 This command should be run in the root director of the repo.
2852 It will create or update the .gitmodules file and include
2853 `gclient-condition` values. Commits in gitlinks will also be updated.
2854 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002855 parser.add_option('--output-gitmodules',
2856 help='name of the .gitmodules file to write to',
2857 default='.gitmodules')
2858 parser.add_option(
2859 '--deps-file',
2860 help=
2861 'name of the deps file to parse for git dependency paths and commits.',
2862 default='DEPS')
2863 parser.add_option(
2864 '--skip-dep',
2865 action="append",
2866 help='skip adding gitmodules for the git dependency at the given path',
2867 default=[])
2868 options, args = parser.parse_args(args)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002869
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002870 deps_dir = os.path.dirname(os.path.abspath(options.deps_file))
2871 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
2872 if not gclient_path:
2873 logging.error(
2874 '.gclient not found\n'
2875 'Make sure you are running this script from a gclient workspace.')
2876 sys.exit(1)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002877
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002878 deps_content = gclient_utils.FileRead(options.deps_file)
2879 ls = gclient_eval.Parse(deps_content, options.deps_file, None, None)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002880
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002881 prefix_length = 0
2882 if not 'use_relative_paths' in ls or ls['use_relative_paths'] != True:
2883 delta_path = os.path.relpath(deps_dir, os.path.abspath(gclient_path))
2884 if delta_path:
2885 prefix_length = len(delta_path.replace(os.path.sep, '/')) + 1
Joanna Wang3ab2f212023-08-09 01:25:15 +00002886
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002887 cache_info = []
Josip Sokcevica309d292023-11-07 19:51:39 +00002888
2889 # Git submodules shouldn't use .git suffix since it's not well supported.
2890 # However, we can't update .gitmodules files since there is no guarantee
2891 # that user has the latest version of depot_tools, and also they are not on
2892 # some old branch which contains already contains submodules with .git.
2893 # This check makes the transition easier.
2894 strip_git_suffix = True
2895 if os.path.exists(options.output_gitmodules):
2896 dot_git_pattern = re.compile('^(\s*)url(\s*)=.*\.git$')
2897 with open(options.output_gitmodules) as f:
2898 strip_git_suffix = not any(dot_git_pattern.match(l) for l in f)
2899
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002900 with open(options.output_gitmodules, 'w', newline='') as f:
2901 for path, dep in ls.get('deps').items():
2902 if path in options.skip_dep:
2903 continue
2904 if dep.get('dep_type') == 'cipd':
2905 continue
2906 try:
2907 url, commit = dep['url'].split('@', maxsplit=1)
2908 except ValueError:
2909 logging.error('error on %s; %s, not adding it', path,
2910 dep["url"])
2911 continue
2912 if prefix_length:
2913 path = path[prefix_length:]
Joanna Wang3ab2f212023-08-09 01:25:15 +00002914
Josip Sokcevica309d292023-11-07 19:51:39 +00002915 if strip_git_suffix:
2916 if url.endswith('.git'):
2917 url = url[:-4] # strip .git
2918 url = url.rstrip('/') # remove trailing slash for consistency
2919
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002920 cache_info += ['--cacheinfo', f'160000,{commit},{path}']
2921 f.write(f'[submodule "{path}"]\n\tpath = {path}\n\turl = {url}\n')
2922 if 'condition' in dep:
2923 f.write(f'\tgclient-condition = {dep["condition"]}\n')
2924 # Windows has limit how long, so let's chunk those calls.
2925 if len(cache_info) >= 100:
2926 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
2927 cache_info = []
2928
2929 if cache_info:
Josip Sokcevic293aa652023-08-23 18:55:20 +00002930 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002931 subprocess2.call(['git', 'add', '.gitmodules'])
Joanna Wang6aed4f52023-10-06 19:12:51 +00002932 print('.gitmodules and gitlinks updated. Please check `git diff --staged`'
2933 'and commit those staged changes (`git commit` without -a)')
Joanna Wang3ab2f212023-08-09 01:25:15 +00002934
2935
Edward Lemur3298e7b2018-07-17 18:21:27 +00002936@metrics.collector.collect_metrics('gclient flatten')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002937def CMDflatten(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002938 """Flattens the solutions into a single DEPS file."""
2939 parser.add_option('--output-deps', help='Path to the output DEPS file')
2940 parser.add_option(
2941 '--output-deps-files',
2942 help=('Path to the output metadata about DEPS files referenced by '
2943 'recursedeps.'))
2944 parser.add_option(
2945 '--pin-all-deps',
2946 action='store_true',
2947 help=('Pin all deps, even if not pinned in DEPS. CAVEAT: only does so '
2948 'for checked out deps, NOT deps_os.'))
2949 parser.add_option('--deps-graph-file',
2950 help='Provide a path for the output graph file')
2951 options, args = parser.parse_args(args)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002952
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002953 options.nohooks = True
2954 options.process_all_deps = True
2955 client = GClient.LoadCurrentConfig(options)
2956 if not client:
2957 raise gclient_utils.Error(
2958 'client not configured; see \'gclient config\'')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002959
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002960 # Only print progress if we're writing to a file. Otherwise, progress
2961 # updates could obscure intended output.
2962 code = client.RunOnDeps('flatten', args, progress=options.output_deps)
2963 if code != 0:
2964 return code
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002965
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002966 flattener = Flattener(client, pin_all_deps=options.pin_all_deps)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002967
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002968 if options.output_deps:
2969 with open(options.output_deps, 'w') as f:
2970 f.write(flattener.deps_string)
2971 else:
2972 print(flattener.deps_string)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002973
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002974 if options.deps_graph_file:
2975 with open(options.deps_graph_file, 'w') as f:
2976 f.write('\n'.join(flattener.deps_graph_lines))
Joanna Wang9144b672023-02-24 23:36:17 +00002977
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002978 deps_files = [{
2979 'url': d[0],
2980 'deps_file': d[1],
2981 'hierarchy': d[2]
2982 } for d in sorted(flattener.deps_files)]
2983 if options.output_deps_files:
2984 with open(options.output_deps_files, 'w') as f:
2985 json.dump(deps_files, f)
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002986
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002987 return 0
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002988
2989
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002990def _GNSettingsToLines(gn_args_file, gn_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002991 s = []
2992 if gn_args_file:
2993 s.extend([
2994 'gclient_gn_args_file = "%s"' % gn_args_file,
2995 'gclient_gn_args = %r' % gn_args,
2996 ])
2997 return s
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002998
2999
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02003000def _AllowedHostsToLines(allowed_hosts):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003001 """Converts |allowed_hosts| set to list of lines for output."""
3002 if not allowed_hosts:
3003 return []
3004 s = ['allowed_hosts = [']
3005 for h in sorted(allowed_hosts):
3006 s.append(' "%s",' % h)
3007 s.extend([']', ''])
3008 return s
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02003009
3010
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003011def _DepsToLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003012 # type: (Mapping[str, Dependency]) -> Sequence[str]
3013 """Converts |deps| dict to list of lines for output."""
3014 if not deps:
3015 return []
3016 s = ['deps = {']
3017 for _, dep in sorted(deps.items()):
3018 s.extend(dep.ToLines())
3019 s.extend(['}', ''])
3020 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003021
3022
Joanna Wang9144b672023-02-24 23:36:17 +00003023def _DepsToDotGraphLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003024 # type: (Mapping[str, Dependency]) -> Sequence[str]
3025 """Converts |deps| dict to list of lines for dot graphs"""
3026 if not deps:
3027 return []
3028 graph_lines = ["digraph {\n\trankdir=\"LR\";"]
3029 for _, dep in sorted(deps.items()):
3030 line = dep.hierarchy(include_url=False, graphviz=True)
3031 if line:
3032 graph_lines.append("\t%s" % line)
3033 graph_lines.append("}")
3034 return graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00003035
3036
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02003037def _DepsOsToLines(deps_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003038 """Converts |deps_os| dict to list of lines for output."""
3039 if not deps_os:
3040 return []
3041 s = ['deps_os = {']
3042 for dep_os, os_deps in sorted(deps_os.items()):
3043 s.append(' "%s": {' % dep_os)
3044 for name, dep in sorted(os_deps.items()):
3045 condition_part = ([' "condition": %r,' %
3046 dep.condition] if dep.condition else [])
3047 s.extend([
3048 ' # %s' % dep.hierarchy(include_url=False),
3049 ' "%s": {' % (name, ),
3050 ' "url": "%s",' % (dep.url, ),
3051 ] + condition_part + [
3052 ' },',
3053 '',
3054 ])
3055 s.extend([' },', ''])
3056 s.extend(['}', ''])
3057 return s
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02003058
3059
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003060def _HooksToLines(name, hooks):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003061 """Converts |hooks| list to list of lines for output."""
3062 if not hooks:
3063 return []
3064 s = ['%s = [' % name]
3065 for dep, hook in hooks:
3066 s.extend([
3067 ' # %s' % dep.hierarchy(include_url=False),
3068 ' {',
3069 ])
3070 if hook.name is not None:
3071 s.append(' "name": "%s",' % hook.name)
3072 if hook.pattern is not None:
3073 s.append(' "pattern": "%s",' % hook.pattern)
3074 if hook.condition is not None:
3075 s.append(' "condition": %r,' % hook.condition)
3076 # Flattened hooks need to be written relative to the root gclient dir
3077 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3078 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3079 [' "%s",' % arg
3080 for arg in hook.action] + [' ]', ' },', ''])
3081 s.extend([']', ''])
3082 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003083
3084
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003085def _HooksOsToLines(hooks_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003086 """Converts |hooks| list to list of lines for output."""
3087 if not hooks_os:
3088 return []
3089 s = ['hooks_os = {']
3090 for hook_os, os_hooks in hooks_os.items():
3091 s.append(' "%s": [' % hook_os)
3092 for dep, hook in os_hooks:
3093 s.extend([
3094 ' # %s' % dep.hierarchy(include_url=False),
3095 ' {',
3096 ])
3097 if hook.name is not None:
3098 s.append(' "name": "%s",' % hook.name)
3099 if hook.pattern is not None:
3100 s.append(' "pattern": "%s",' % hook.pattern)
3101 if hook.condition is not None:
3102 s.append(' "condition": %r,' % hook.condition)
3103 # Flattened hooks need to be written relative to the root gclient
3104 # dir
3105 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3106 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3107 [' "%s",' % arg
3108 for arg in hook.action] + [' ]', ' },', ''])
3109 s.extend([' ],', ''])
3110 s.extend(['}', ''])
3111 return s
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003112
3113
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003114def _VarsToLines(variables):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003115 """Converts |variables| dict to list of lines for output."""
3116 if not variables:
3117 return []
3118 s = ['vars = {']
3119 for key, tup in sorted(variables.items()):
3120 hierarchy, value = tup
3121 s.extend([
3122 ' # %s' % hierarchy,
3123 ' "%s": %r,' % (key, value),
3124 '',
3125 ])
3126 s.extend(['}', ''])
3127 return s
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003128
3129
Edward Lemur3298e7b2018-07-17 18:21:27 +00003130@metrics.collector.collect_metrics('gclient grep')
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003131def CMDgrep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003132 """Greps through git repos managed by gclient.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003133
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003134 Runs 'git grep [args...]' for each module.
3135 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003136 # We can't use optparse because it will try to parse arguments sent
3137 # to git grep and throw an error. :-(
3138 if not args or re.match('(-h|--help)$', args[0]):
3139 print(
3140 'Usage: gclient grep [-j <N>] git-grep-args...\n\n'
3141 'Example: "gclient grep -j10 -A2 RefCountedBase" runs\n"git grep '
3142 '-A2 RefCountedBase" on each of gclient\'s git\nrepos with up to '
3143 '10 jobs.\n\nBonus: page output by appending "|& less -FRSX" to the'
3144 ' end of your query.',
3145 file=sys.stderr)
3146 return 1
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003147
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003148 jobs_arg = ['--jobs=1']
3149 if re.match(r'(-j|--jobs=)\d+$', args[0]):
3150 jobs_arg, args = args[:1], args[1:]
3151 elif re.match(r'(-j|--jobs)$', args[0]):
3152 jobs_arg, args = args[:2], args[2:]
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003153
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003154 return CMDrecurse(
3155 parser, jobs_arg + [
3156 '--ignore', '--prepend-dir', '--no-progress', '--scm=git', 'git',
3157 'grep', '--null', '--color=Always'
3158 ] + args)
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00003159
3160
Edward Lemur3298e7b2018-07-17 18:21:27 +00003161@metrics.collector.collect_metrics('gclient root')
stip@chromium.orga735da22015-04-29 23:18:20 +00003162def CMDroot(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003163 """Outputs the solution root (or current dir if there isn't one)."""
3164 (options, args) = parser.parse_args(args)
3165 client = GClient.LoadCurrentConfig(options)
3166 if client:
3167 print(os.path.abspath(client.root_dir))
3168 else:
3169 print(os.path.abspath('.'))
stip@chromium.orga735da22015-04-29 23:18:20 +00003170
3171
agablea98a6cd2016-11-15 14:30:10 -08003172@subcommand.usage('[url]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00003173@metrics.collector.collect_metrics('gclient config')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003174def CMDconfig(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003175 """Creates a .gclient file in the current directory.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003176
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003177 This specifies the configuration for further commands. After update/sync,
3178 top-level DEPS files in each module are read to determine dependent
3179 modules to operate on as well. If optional [url] parameter is
3180 provided, then configuration is read from a specified Subversion server
3181 URL.
3182 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003183 # We do a little dance with the --gclientfile option. 'gclient config' is
3184 # the only command where it's acceptable to have both '--gclientfile' and
3185 # '--spec' arguments. So, we temporarily stash any --gclientfile parameter
3186 # into options.output_config_file until after the (gclientfile xor spec)
3187 # error check.
3188 parser.remove_option('--gclientfile')
3189 parser.add_option('--gclientfile',
3190 dest='output_config_file',
3191 help='Specify an alternate .gclient file')
3192 parser.add_option('--name',
3193 help='overrides the default name for the solution')
3194 parser.add_option(
3195 '--deps-file',
3196 default='DEPS',
3197 help='overrides the default name for the DEPS file for the '
3198 'main solutions and all sub-dependencies')
3199 parser.add_option('--unmanaged',
3200 action='store_true',
3201 default=False,
3202 help='overrides the default behavior to make it possible '
3203 'to have the main solution untouched by gclient '
3204 '(gclient will check out unmanaged dependencies but '
3205 'will never sync them)')
3206 parser.add_option('--cache-dir',
3207 default=UNSET_CACHE_DIR,
3208 help='Cache all git repos into this dir and do shared '
3209 'clones from the cache, instead of cloning directly '
3210 'from the remote. Pass "None" to disable cache, even '
3211 'if globally enabled due to $GIT_CACHE_PATH.')
3212 parser.add_option('--custom-var',
3213 action='append',
3214 dest='custom_vars',
3215 default=[],
3216 help='overrides variables; key=value syntax')
3217 parser.set_defaults(config_filename=None)
3218 (options, args) = parser.parse_args(args)
3219 if options.output_config_file:
3220 setattr(options, 'config_filename',
3221 getattr(options, 'output_config_file'))
3222 if ((options.spec and args) or len(args) > 2
3223 or (not options.spec and not args)):
3224 parser.error(
3225 'Inconsistent arguments. Use either --spec or one or 2 args')
maruel@chromium.org5fc2a332010-05-26 19:37:15 +00003226
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003227 if (options.cache_dir is not UNSET_CACHE_DIR
3228 and options.cache_dir.lower() == 'none'):
3229 options.cache_dir = None
Robert Iannuccia19649b2018-06-29 16:31:45 +00003230
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003231 custom_vars = {}
3232 for arg in options.custom_vars:
3233 kv = arg.split('=', 1)
3234 if len(kv) != 2:
3235 parser.error('Invalid --custom-var argument: %r' % arg)
3236 custom_vars[kv[0]] = gclient_eval.EvaluateCondition(kv[1], {})
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02003237
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003238 client = GClient('.', options)
3239 if options.spec:
3240 client.SetConfig(options.spec)
iposva@chromium.org8cf7a392010-04-07 17:20:26 +00003241 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003242 base_url = args[0].rstrip('/')
3243 if not options.name:
3244 name = base_url.split('/')[-1]
3245 if name.endswith('.git'):
3246 name = name[:-4]
3247 else:
3248 # specify an alternate relpath for the given URL.
3249 name = options.name
3250 if not os.path.abspath(os.path.join(os.getcwd(), name)).startswith(
3251 os.getcwd()):
3252 parser.error('Do not pass a relative path for --name.')
3253 if any(x in ('..', '.', '/', '\\') for x in name.split(os.sep)):
3254 parser.error(
3255 'Do not include relative path components in --name.')
agable@chromium.orgf2214672015-10-27 21:02:48 +00003256
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003257 deps_file = options.deps_file
3258 client.SetDefaultConfig(name,
3259 deps_file,
3260 base_url,
3261 managed=not options.unmanaged,
3262 cache_dir=options.cache_dir,
3263 custom_vars=custom_vars)
3264 client.SaveConfig()
3265 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003266
3267
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003268@subcommand.epilog("""Example:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003269 gclient pack > patch.txt
3270 generate simple patch for configured client and dependences
3271""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003272@metrics.collector.collect_metrics('gclient pack')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003273def CMDpack(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003274 """Generates a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003275
agabled437d762016-10-17 09:35:11 -07003276 Internally, runs 'git diff' on each checked out module and
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003277 dependencies, and performs minimal postprocessing of the output. The
3278 resulting patch is printed to stdout and can be applied to a freshly
3279 checked out tree via 'patch -p0 < patchfile'.
3280 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003281 parser.add_option('--deps',
3282 dest='deps_os',
3283 metavar='OS_LIST',
3284 help='override deps for the specified (comma-separated) '
3285 'platform(s); \'all\' will process all deps_os '
3286 'references')
3287 parser.remove_option('--jobs')
3288 (options, args) = parser.parse_args(args)
3289 # Force jobs to 1 so the stdout is not annotated with the thread ids
3290 options.jobs = 1
3291 client = GClient.LoadCurrentConfig(options)
3292 if not client:
3293 raise gclient_utils.Error(
3294 'client not configured; see \'gclient config\'')
3295 if options.verbose:
3296 client.PrintLocationAndContents()
3297 return client.RunOnDeps('pack', args)
kbr@google.comab318592009-09-04 00:54:55 +00003298
3299
Edward Lemur3298e7b2018-07-17 18:21:27 +00003300@metrics.collector.collect_metrics('gclient status')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003301def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003302 """Shows modification status for every dependencies."""
3303 parser.add_option('--deps',
3304 dest='deps_os',
3305 metavar='OS_LIST',
3306 help='override deps for the specified (comma-separated) '
3307 'platform(s); \'all\' will process all deps_os '
3308 'references')
3309 (options, args) = parser.parse_args(args)
3310 client = GClient.LoadCurrentConfig(options)
3311 if not client:
3312 raise gclient_utils.Error(
3313 'client not configured; see \'gclient config\'')
3314 if options.verbose:
3315 client.PrintLocationAndContents()
3316 return client.RunOnDeps('status', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003317
3318
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003319@subcommand.epilog("""Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +00003320 gclient sync
3321 update files from SCM according to current configuration,
3322 *for modules which have changed since last update or sync*
3323 gclient sync --force
3324 update files from SCM according to current configuration, for
3325 all modules (useful for recovering files deleted from local copy)
Edward Lesmes3ffca4b2021-05-19 19:36:17 +00003326 gclient sync --revision src@GIT_COMMIT_OR_REF
3327 update src directory to GIT_COMMIT_OR_REF
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003328
3329JSON output format:
3330If the --output-json option is specified, the following document structure will
3331be emitted to the provided file. 'null' entries may occur for subprojects which
3332are present in the gclient solution, but were not processed (due to custom_deps,
3333os_deps, etc.)
3334
3335{
3336 "solutions" : {
3337 "<name>": { # <name> is the posix-normalized path to the solution.
agabled437d762016-10-17 09:35:11 -07003338 "revision": [<git id hex string>|null],
3339 "scm": ["git"|null],
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003340 }
3341 }
3342}
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003343""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003344@metrics.collector.collect_metrics('gclient sync')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003345def CMDsync(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003346 """Checkout/update all modules."""
3347 parser.add_option('-f',
3348 '--force',
3349 action='store_true',
3350 help='force update even for unchanged modules')
3351 parser.add_option('-n',
3352 '--nohooks',
3353 action='store_true',
3354 help='don\'t run hooks after the update is complete')
3355 parser.add_option('-p',
3356 '--noprehooks',
3357 action='store_true',
3358 help='don\'t run pre-DEPS hooks',
3359 default=False)
3360 parser.add_option('-r',
3361 '--revision',
3362 action='append',
3363 dest='revisions',
3364 metavar='REV',
3365 default=[],
3366 help='Enforces git ref/hash for the solutions with the '
3367 'format src@rev. The src@ part is optional and can be '
3368 'skipped. You can also specify URLs instead of paths '
3369 'and gclient will find the solution corresponding to '
3370 'the given URL. If a path is also specified, the URL '
3371 'takes precedence. -r can be used multiple times when '
3372 '.gclient has multiple solutions configured, and will '
3373 'work even if the src@ part is skipped. Revision '
3374 'numbers (e.g. 31000 or r31000) are not supported.')
3375 parser.add_option('--patch-ref',
3376 action='append',
3377 dest='patch_refs',
3378 metavar='GERRIT_REF',
3379 default=[],
3380 help='Patches the given reference with the format '
3381 'dep@target-ref:patch-ref. '
3382 'For |dep|, you can specify URLs as well as paths, '
3383 'with URLs taking preference. '
3384 '|patch-ref| will be applied to |dep|, rebased on top '
3385 'of what |dep| was synced to, and a soft reset will '
3386 'be done. Use --no-rebase-patch-ref and '
3387 '--no-reset-patch-ref to disable this behavior. '
3388 '|target-ref| is the target branch against which a '
3389 'patch was created, it is used to determine which '
3390 'commits from the |patch-ref| actually constitute a '
3391 'patch.')
3392 parser.add_option(
3393 '-t',
3394 '--download-topics',
3395 action='store_true',
3396 help='Downloads and patches locally changes from all open '
3397 'Gerrit CLs that have the same topic as the changes '
3398 'in the specified patch_refs. Only works if atleast '
3399 'one --patch-ref is specified.')
3400 parser.add_option('--with_branch_heads',
3401 action='store_true',
3402 help='Clone git "branch_heads" refspecs in addition to '
3403 'the default refspecs. This adds about 1/2GB to a '
3404 'full checkout. (git only)')
3405 parser.add_option(
3406 '--with_tags',
3407 action='store_true',
3408 help='Clone git tags in addition to the default refspecs.')
3409 parser.add_option('-H',
3410 '--head',
3411 action='store_true',
3412 help='DEPRECATED: only made sense with safesync urls.')
3413 parser.add_option(
3414 '-D',
3415 '--delete_unversioned_trees',
3416 action='store_true',
3417 help='Deletes from the working copy any dependencies that '
3418 'have been removed since the last sync, as long as '
3419 'there are no local modifications. When used with '
3420 '--force, such dependencies are removed even if they '
3421 'have local modifications. When used with --reset, '
3422 'all untracked directories are removed from the '
3423 'working copy, excluding those which are explicitly '
3424 'ignored in the repository.')
3425 parser.add_option(
3426 '-R',
3427 '--reset',
3428 action='store_true',
3429 help='resets any local changes before updating (git only)')
3430 parser.add_option('-M',
3431 '--merge',
3432 action='store_true',
3433 help='merge upstream changes instead of trying to '
3434 'fast-forward or rebase')
3435 parser.add_option('-A',
3436 '--auto_rebase',
3437 action='store_true',
3438 help='Automatically rebase repositories against local '
3439 'checkout during update (git only).')
3440 parser.add_option('--deps',
3441 dest='deps_os',
3442 metavar='OS_LIST',
3443 help='override deps for the specified (comma-separated) '
3444 'platform(s); \'all\' will process all deps_os '
3445 'references')
3446 parser.add_option('--process-all-deps',
3447 action='store_true',
3448 help='Check out all deps, even for different OS-es, '
3449 'or with conditions evaluating to false')
3450 parser.add_option('--upstream',
3451 action='store_true',
3452 help='Make repo state match upstream branch.')
3453 parser.add_option('--output-json',
3454 help='Output a json document to this path containing '
3455 'summary information about the sync.')
3456 parser.add_option(
3457 '--no-history',
3458 action='store_true',
3459 help='GIT ONLY - Reduces the size/time of the checkout at '
3460 'the cost of no history. Requires Git 1.9+')
3461 parser.add_option('--shallow',
3462 action='store_true',
3463 help='GIT ONLY - Do a shallow clone into the cache dir. '
3464 'Requires Git 1.9+')
3465 parser.add_option('--no_bootstrap',
3466 '--no-bootstrap',
3467 action='store_true',
3468 help='Don\'t bootstrap from Google Storage.')
3469 parser.add_option('--ignore_locks',
3470 action='store_true',
3471 help='No longer used.')
3472 parser.add_option('--break_repo_locks',
3473 action='store_true',
3474 help='No longer used.')
3475 parser.add_option('--lock_timeout',
3476 type='int',
3477 default=5000,
3478 help='GIT ONLY - Deadline (in seconds) to wait for git '
3479 'cache lock to become available. Default is %default.')
3480 parser.add_option('--no-rebase-patch-ref',
3481 action='store_false',
3482 dest='rebase_patch_ref',
3483 default=True,
3484 help='Bypass rebase of the patch ref after checkout.')
3485 parser.add_option('--no-reset-patch-ref',
3486 action='store_false',
3487 dest='reset_patch_ref',
3488 default=True,
3489 help='Bypass calling reset after patching the ref.')
3490 parser.add_option('--experiment',
3491 action='append',
3492 dest='experiments',
3493 default=[],
3494 help='Which experiments should be enabled.')
3495 (options, args) = parser.parse_args(args)
3496 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003497
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003498 if not client:
3499 raise gclient_utils.Error(
3500 'client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003501
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003502 if options.download_topics and not options.rebase_patch_ref:
3503 raise gclient_utils.Error(
3504 'Warning: You cannot download topics and not rebase each patch ref')
Ravi Mistryecda7822022-02-28 16:22:20 +00003505
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003506 if options.ignore_locks:
3507 print(
3508 'Warning: ignore_locks is no longer used. Please remove its usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003509
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003510 if options.break_repo_locks:
3511 print('Warning: break_repo_locks is no longer used. Please remove its '
3512 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003513
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003514 if options.revisions and options.head:
3515 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
3516 print('Warning: you cannot use both --head and --revision')
smutae7ea312016-07-18 11:59:41 -07003517
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003518 if options.verbose:
3519 client.PrintLocationAndContents()
3520 ret = client.RunOnDeps('update', args)
3521 if options.output_json:
3522 slns = {}
3523 for d in client.subtree(True):
3524 normed = d.name.replace('\\', '/').rstrip('/') + '/'
3525 slns[normed] = {
3526 'revision': d.got_revision,
3527 'scm': d.used_scm.name if d.used_scm else None,
3528 'url': str(d.url) if d.url else None,
3529 'was_processed': d.should_process,
3530 'was_synced': d._should_sync,
3531 }
3532 with open(options.output_json, 'w') as f:
3533 json.dump({'solutions': slns}, f)
3534 return ret
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003535
3536
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003537CMDupdate = CMDsync
3538
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003539
Edward Lemur3298e7b2018-07-17 18:21:27 +00003540@metrics.collector.collect_metrics('gclient validate')
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003541def CMDvalidate(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003542 """Validates the .gclient and DEPS syntax."""
3543 options, args = parser.parse_args(args)
3544 client = GClient.LoadCurrentConfig(options)
3545 if not client:
3546 raise gclient_utils.Error(
3547 'client not configured; see \'gclient config\'')
3548 rv = client.RunOnDeps('validate', args)
3549 if rv == 0:
3550 print('validate: SUCCESS')
3551 else:
3552 print('validate: FAILURE')
3553 return rv
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003554
3555
Edward Lemur3298e7b2018-07-17 18:21:27 +00003556@metrics.collector.collect_metrics('gclient diff')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003557def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003558 """Displays local diff for every dependencies."""
3559 parser.add_option('--deps',
3560 dest='deps_os',
3561 metavar='OS_LIST',
3562 help='override deps for the specified (comma-separated) '
3563 'platform(s); \'all\' will process all deps_os '
3564 'references')
3565 (options, args) = parser.parse_args(args)
3566 client = GClient.LoadCurrentConfig(options)
3567 if not client:
3568 raise gclient_utils.Error(
3569 'client not configured; see \'gclient config\'')
3570 if options.verbose:
3571 client.PrintLocationAndContents()
3572 return client.RunOnDeps('diff', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003573
3574
Edward Lemur3298e7b2018-07-17 18:21:27 +00003575@metrics.collector.collect_metrics('gclient revert')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003576def CMDrevert(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003577 """Reverts all modifications in every dependencies.
maruel@chromium.org28d14bd2010-11-11 20:37:09 +00003578
3579 That's the nuclear option to get back to a 'clean' state. It removes anything
agabled437d762016-10-17 09:35:11 -07003580 that shows up in git status."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003581 parser.add_option('--deps',
3582 dest='deps_os',
3583 metavar='OS_LIST',
3584 help='override deps for the specified (comma-separated) '
3585 'platform(s); \'all\' will process all deps_os '
3586 'references')
3587 parser.add_option('-n',
3588 '--nohooks',
3589 action='store_true',
3590 help='don\'t run hooks after the revert is complete')
3591 parser.add_option('-p',
3592 '--noprehooks',
3593 action='store_true',
3594 help='don\'t run pre-DEPS hooks',
3595 default=False)
3596 parser.add_option('--upstream',
3597 action='store_true',
3598 help='Make repo state match upstream branch.')
3599 parser.add_option('--break_repo_locks',
3600 action='store_true',
3601 help='No longer used.')
3602 (options, args) = parser.parse_args(args)
3603 if options.break_repo_locks:
3604 print(
3605 'Warning: break_repo_locks is no longer used. Please remove its ' +
3606 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003607
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003608 # --force is implied.
3609 options.force = True
3610 options.reset = False
3611 options.delete_unversioned_trees = False
3612 options.merge = False
3613 client = GClient.LoadCurrentConfig(options)
3614 if not client:
3615 raise gclient_utils.Error(
3616 'client not configured; see \'gclient config\'')
3617 return client.RunOnDeps('revert', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003618
3619
Edward Lemur3298e7b2018-07-17 18:21:27 +00003620@metrics.collector.collect_metrics('gclient runhooks')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003621def CMDrunhooks(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003622 """Runs hooks for files that have been modified in the local working copy."""
3623 parser.add_option('--deps',
3624 dest='deps_os',
3625 metavar='OS_LIST',
3626 help='override deps for the specified (comma-separated) '
3627 'platform(s); \'all\' will process all deps_os '
3628 'references')
3629 parser.add_option('-f',
3630 '--force',
3631 action='store_true',
3632 default=True,
3633 help='Deprecated. No effect.')
3634 (options, args) = parser.parse_args(args)
3635 client = GClient.LoadCurrentConfig(options)
3636 if not client:
3637 raise gclient_utils.Error(
3638 'client not configured; see \'gclient config\'')
3639 if options.verbose:
3640 client.PrintLocationAndContents()
3641 options.force = True
3642 options.nohooks = False
3643 return client.RunOnDeps('runhooks', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003644
3645
Gavin Mak50b27a52023-09-19 22:44:59 +00003646# TODO(crbug.com/1481266): Collect merics for installhooks.
3647def CMDinstallhooks(parser, args):
3648 """Installs gclient git hooks.
3649
3650 Currently only installs a pre-commit hook to drop staged gitlinks. To
3651 bypass this pre-commit hook once it's installed, set the environment
3652 variable SKIP_GITLINK_PRECOMMIT=1.
3653 """
3654 (options, args) = parser.parse_args(args)
3655 client = GClient.LoadCurrentConfig(options)
3656 if not client:
3657 raise gclient_utils.Error(
3658 'client not configured; see \'gclient config\'')
3659 client._InstallPreCommitHook()
3660 return 0
3661
3662
Edward Lemur3298e7b2018-07-17 18:21:27 +00003663@metrics.collector.collect_metrics('gclient revinfo')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003664def CMDrevinfo(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003665 """Outputs revision info mapping for the client and its dependencies.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003666
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00003667 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003668 can be used to reproduce the same tree in the future. It is only useful for
agabled437d762016-10-17 09:35:11 -07003669 'unpinned dependencies', i.e. DEPS/deps references without a git hash.
3670 A git branch name isn't 'pinned' since the actual commit can change.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003671 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003672 parser.add_option('--deps',
3673 dest='deps_os',
3674 metavar='OS_LIST',
3675 help='override deps for the specified (comma-separated) '
3676 'platform(s); \'all\' will process all deps_os '
3677 'references')
3678 parser.add_option(
3679 '-a',
3680 '--actual',
3681 action='store_true',
3682 help='gets the actual checked out revisions instead of the '
3683 'ones specified in the DEPS and .gclient files')
3684 parser.add_option('-s',
3685 '--snapshot',
3686 action='store_true',
3687 help='creates a snapshot .gclient file of the current '
3688 'version of all repositories to reproduce the tree, '
3689 'implies -a')
3690 parser.add_option(
3691 '--filter',
3692 action='append',
3693 dest='filter',
3694 help='Display revision information only for the specified '
3695 'dependencies (filtered by URL or path).')
3696 parser.add_option('--output-json',
3697 help='Output a json document to this path containing '
3698 'information about the revisions.')
3699 parser.add_option(
3700 '--ignore-dep-type',
3701 choices=['git', 'cipd'],
3702 help='Specify to skip processing of a certain type of dep.')
3703 (options, args) = parser.parse_args(args)
3704 client = GClient.LoadCurrentConfig(options)
3705 if not client:
3706 raise gclient_utils.Error(
3707 'client not configured; see \'gclient config\'')
3708 client.PrintRevInfo()
3709 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003710
3711
Edward Lemur3298e7b2018-07-17 18:21:27 +00003712@metrics.collector.collect_metrics('gclient getdep')
Edward Lesmes411041f2018-04-05 20:12:55 -04003713def CMDgetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003714 """Gets revision information and variable values from a DEPS file.
Josip Sokcevic7b5e3d72023-06-13 00:28:23 +00003715
3716 If key doesn't exist or is incorrectly declared, this script exits with exit
3717 code 2."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003718 parser.add_option('--var',
3719 action='append',
3720 dest='vars',
3721 metavar='VAR',
3722 default=[],
3723 help='Gets the value of a given variable.')
3724 parser.add_option(
3725 '-r',
3726 '--revision',
3727 action='append',
3728 dest='getdep_revisions',
3729 metavar='DEP',
3730 default=[],
3731 help='Gets the revision/version for the given dependency. '
3732 'If it is a git dependency, dep must be a path. If it '
3733 'is a CIPD dependency, dep must be of the form '
3734 'path:package.')
3735 parser.add_option(
3736 '--deps-file',
3737 default='DEPS',
3738 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3739 # .gclient first.
3740 help='The DEPS file to be edited. Defaults to the DEPS '
3741 'file in the current directory.')
3742 (options, args) = parser.parse_args(args)
Edward Lesmes411041f2018-04-05 20:12:55 -04003743
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003744 if not os.path.isfile(options.deps_file):
3745 raise gclient_utils.Error('DEPS file %s does not exist.' %
3746 options.deps_file)
3747 with open(options.deps_file) as f:
3748 contents = f.read()
3749 client = GClient.LoadCurrentConfig(options)
3750 if client is not None:
3751 builtin_vars = client.get_builtin_vars()
Edward Lesmes411041f2018-04-05 20:12:55 -04003752 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003753 logging.warning(
3754 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3755 'file without support for built-in variables.')
3756 builtin_vars = None
3757 local_scope = gclient_eval.Exec(contents,
3758 options.deps_file,
3759 builtin_vars=builtin_vars)
3760
3761 for var in options.vars:
3762 print(gclient_eval.GetVar(local_scope, var))
3763
3764 commits = {}
3765 if local_scope.get(
3766 'git_dependencies'
3767 ) == gclient_eval.SUBMODULES and options.getdep_revisions:
3768 commits.update(
3769 scm_git.GIT.GetSubmoduleCommits(
3770 os.getcwd(),
3771 [path for path in options.getdep_revisions if ':' not in path]))
3772
3773 for name in options.getdep_revisions:
3774 if ':' in name:
3775 name, _, package = name.partition(':')
3776 if not name or not package:
3777 parser.error(
3778 'Wrong CIPD format: %s:%s should be of the form path:pkg.' %
3779 (name, package))
3780 print(gclient_eval.GetCIPD(local_scope, name, package))
3781 elif commits:
3782 print(commits[name])
3783 else:
3784 try:
3785 print(gclient_eval.GetRevision(local_scope, name))
3786 except KeyError as e:
3787 print(repr(e), file=sys.stderr)
3788 sys.exit(2)
Edward Lesmes411041f2018-04-05 20:12:55 -04003789
3790
Edward Lemur3298e7b2018-07-17 18:21:27 +00003791@metrics.collector.collect_metrics('gclient setdep')
Edward Lesmes6f64a052018-03-20 17:35:49 -04003792def CMDsetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003793 """Modifies dependency revisions and variable values in a DEPS file"""
3794 parser.add_option('--var',
3795 action='append',
3796 dest='vars',
3797 metavar='VAR=VAL',
3798 default=[],
3799 help='Sets a variable to the given value with the format '
3800 'name=value.')
3801 parser.add_option('-r',
3802 '--revision',
3803 action='append',
3804 dest='setdep_revisions',
3805 metavar='DEP@REV',
3806 default=[],
3807 help='Sets the revision/version for the dependency with '
3808 'the format dep@rev. If it is a git dependency, dep '
3809 'must be a path and rev must be a git hash or '
3810 'reference (e.g. src/dep@deadbeef). If it is a CIPD '
3811 'dependency, dep must be of the form path:package and '
3812 'rev must be the package version '
3813 '(e.g. src/pkg:chromium/pkg@2.1-cr0).')
3814 parser.add_option(
3815 '--deps-file',
3816 default='DEPS',
3817 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3818 # .gclient first.
3819 help='The DEPS file to be edited. Defaults to the DEPS '
3820 'file in the current directory.')
3821 (options, args) = parser.parse_args(args)
3822 if args:
3823 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3824 if not options.setdep_revisions and not options.vars:
Edward Lesmes0ecf6d62018-04-05 18:28:55 -04003825 parser.error(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003826 'You must specify at least one variable or revision to modify.')
3827
3828 if not os.path.isfile(options.deps_file):
3829 raise gclient_utils.Error('DEPS file %s does not exist.' %
3830 options.deps_file)
3831 with open(options.deps_file) as f:
3832 contents = f.read()
3833
3834 client = GClient.LoadCurrentConfig(options)
3835 if client is not None:
3836 builtin_vars = client.get_builtin_vars()
Edward Lesmes6f64a052018-03-20 17:35:49 -04003837 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003838 logging.warning(
3839 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3840 'file without support for built-in variables.')
3841 builtin_vars = None
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003842
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003843 local_scope = gclient_eval.Exec(contents,
3844 options.deps_file,
3845 builtin_vars=builtin_vars)
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003846
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003847 # Create a set of all git submodules.
3848 cwd = os.path.dirname(options.deps_file) or os.getcwd()
3849 git_modules = None
3850 if 'git_dependencies' in local_scope and local_scope[
3851 'git_dependencies'] in (gclient_eval.SUBMODULES, gclient_eval.SYNC):
3852 try:
3853 submodule_status = subprocess2.check_output(
3854 ['git', 'submodule', 'status'], cwd=cwd).decode('utf-8')
3855 git_modules = {l.split()[1] for l in submodule_status.splitlines()}
3856 except subprocess2.CalledProcessError as e:
3857 print('Warning: gitlinks won\'t be updated: ', e)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003858
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003859 for var in options.vars:
3860 name, _, value = var.partition('=')
3861 if not name or not value:
3862 parser.error(
3863 'Wrong var format: %s should be of the form name=value.' % var)
3864 if name in local_scope['vars']:
3865 gclient_eval.SetVar(local_scope, name, value)
3866 else:
3867 gclient_eval.AddVar(local_scope, name, value)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003868
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003869 for revision in options.setdep_revisions:
3870 name, _, value = revision.partition('@')
3871 if not name or not value:
3872 parser.error('Wrong dep format: %s should be of the form dep@rev.' %
3873 revision)
3874 if ':' in name:
3875 name, _, package = name.partition(':')
3876 if not name or not package:
3877 parser.error(
3878 'Wrong CIPD format: %s:%s should be of the form path:pkg@version.'
3879 % (name, package))
3880 gclient_eval.SetCIPD(local_scope, name, package, value)
3881 else:
3882 # Update DEPS only when `git_dependencies` == DEPS or SYNC.
3883 # git_dependencies is defaulted to DEPS when not set.
3884 if 'git_dependencies' not in local_scope or local_scope[
3885 'git_dependencies'] in (gclient_eval.DEPS,
3886 gclient_eval.SYNC):
3887 gclient_eval.SetRevision(local_scope, name, value)
3888
3889 # Update git submodules when `git_dependencies` == SYNC or
3890 # SUBMODULES.
3891 if git_modules and 'git_dependencies' in local_scope and local_scope[
3892 'git_dependencies'] in (gclient_eval.SUBMODULES,
3893 gclient_eval.SYNC):
3894 git_module_name = name
3895 if not 'use_relative_paths' in local_scope or \
3896 local_scope['use_relative_paths'] != True:
3897 deps_dir = os.path.dirname(
3898 os.path.abspath(options.deps_file))
3899 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
3900 delta_path = None
3901 if gclient_path:
3902 delta_path = os.path.relpath(
3903 deps_dir, os.path.abspath(gclient_path))
3904 if delta_path:
3905 prefix_length = len(delta_path.replace(
3906 os.path.sep, '/')) + 1
3907 git_module_name = name[prefix_length:]
3908 # gclient setdep should update the revision, i.e., the gitlink
3909 # only when the submodule entry is already present within
3910 # .gitmodules.
3911 if git_module_name not in git_modules:
3912 raise KeyError(
3913 f'Could not find any dependency called "{git_module_name}" in '
3914 f'.gitmodules.')
3915
3916 # Update the gitlink for the submodule.
3917 subprocess2.call([
3918 'git', 'update-index', '--add', '--cacheinfo',
3919 f'160000,{value},{git_module_name}'
3920 ],
3921 cwd=cwd)
3922
3923 with open(options.deps_file, 'wb') as f:
3924 f.write(gclient_eval.RenderDEPSFile(local_scope).encode('utf-8'))
3925
3926 if git_modules:
3927 subprocess2.call(['git', 'add', options.deps_file], cwd=cwd)
3928 print('Changes have been staged. See changes with `git status`.\n'
3929 'Use `git commit -m "Manual roll"` to commit your changes. \n'
3930 'Run gclient sync to update your local dependency checkout.')
Josip Sokcevic5561f8b2023-08-21 16:00:42 +00003931
Edward Lesmes6f64a052018-03-20 17:35:49 -04003932
Edward Lemur3298e7b2018-07-17 18:21:27 +00003933@metrics.collector.collect_metrics('gclient verify')
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003934def CMDverify(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003935 """Verifies the DEPS file deps are only from allowed_hosts."""
3936 (options, args) = parser.parse_args(args)
3937 client = GClient.LoadCurrentConfig(options)
3938 if not client:
3939 raise gclient_utils.Error(
3940 'client not configured; see \'gclient config\'')
3941 client.RunOnDeps(None, [])
3942 # Look at each first-level dependency of this gclient only.
3943 for dep in client.dependencies:
3944 bad_deps = dep.findDepsFromNotAllowedHosts()
3945 if not bad_deps:
3946 continue
3947 print("There are deps from not allowed hosts in file %s" %
3948 dep.deps_file)
3949 for bad_dep in bad_deps:
3950 print("\t%s at %s" % (bad_dep.name, bad_dep.url))
3951 print("allowed_hosts:", ', '.join(dep.allowed_hosts))
3952 sys.stdout.flush()
3953 raise gclient_utils.Error(
3954 'dependencies from disallowed hosts; check your DEPS file.')
3955 return 0
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003956
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003957
3958@subcommand.epilog("""For more information on what metrics are we collecting and
Edward Lemur8a2e3312018-07-12 21:15:09 +00003959why, please read metrics.README.md or visit https://bit.ly/2ufRS4p""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003960@metrics.collector.collect_metrics('gclient metrics')
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003961def CMDmetrics(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003962 """Reports, and optionally modifies, the status of metric collection."""
3963 parser.add_option('--opt-in',
3964 action='store_true',
3965 dest='enable_metrics',
3966 help='Opt-in to metrics collection.',
3967 default=None)
3968 parser.add_option('--opt-out',
3969 action='store_false',
3970 dest='enable_metrics',
3971 help='Opt-out of metrics collection.')
3972 options, args = parser.parse_args(args)
3973 if args:
3974 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3975 if not metrics.collector.config.is_googler:
3976 print("You're not a Googler. Metrics collection is disabled for you.")
3977 return 0
3978
3979 if options.enable_metrics is not None:
3980 metrics.collector.config.opted_in = options.enable_metrics
3981
3982 if metrics.collector.config.opted_in is None:
3983 print("You haven't opted in or out of metrics collection.")
3984 elif metrics.collector.config.opted_in:
3985 print("You have opted in. Thanks!")
3986 else:
3987 print("You have opted out. Please consider opting in.")
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003988 return 0
3989
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003990
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003991class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003992 gclientfile_default = os.environ.get('GCLIENT_FILE', '.gclient')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003993
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003994 def __init__(self, **kwargs):
3995 optparse.OptionParser.__init__(self,
3996 version='%prog ' + __version__,
3997 **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003998
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003999 # Some arm boards have issues with parallel sync.
4000 if platform.machine().startswith('arm'):
4001 jobs = 1
4002 else:
4003 jobs = max(8, gclient_utils.NumLocalCpus())
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004004
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004005 self.add_option(
4006 '-j',
4007 '--jobs',
4008 default=jobs,
4009 type='int',
4010 help='Specify how many SCM commands can run in parallel; defaults to '
4011 '%default on this machine')
4012 self.add_option(
4013 '-v',
4014 '--verbose',
4015 action='count',
4016 default=0,
4017 help='Produces additional output for diagnostics. Can be used up to '
4018 'three times for more logging info.')
4019 self.add_option('--gclientfile',
4020 dest='config_filename',
4021 help='Specify an alternate %s file' %
4022 self.gclientfile_default)
4023 self.add_option(
4024 '--spec',
4025 help='create a gclient file containing the provided string. Due to '
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004026 'Cygwin/Python brokenness, it can\'t contain any newlines.')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004027 self.add_option('--no-nag-max',
4028 default=False,
4029 action='store_true',
4030 help='Ignored for backwards compatibility.')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004031
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004032 def parse_args(self, args=None, _values=None):
4033 """Integrates standard options processing."""
4034 # Create an optparse.Values object that will store only the actual
4035 # passed options, without the defaults.
4036 actual_options = optparse.Values()
4037 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
4038 # Create an optparse.Values object with the default options.
4039 options = optparse.Values(self.get_default_values().__dict__)
4040 # Update it with the options passed by the user.
4041 options._update_careful(actual_options.__dict__)
4042 # Store the options passed by the user in an _actual_options attribute.
4043 # We store only the keys, and not the values, since the values can
4044 # contain arbitrary information, which might be PII.
4045 metrics.collector.add('arguments', list(actual_options.__dict__))
Edward Lemur3298e7b2018-07-17 18:21:27 +00004046
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004047 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
4048 logging.basicConfig(
4049 level=levels[min(options.verbose,
4050 len(levels) - 1)],
4051 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
4052 if options.config_filename and options.spec:
4053 self.error('Cannot specify both --gclientfile and --spec')
4054 if (options.config_filename and options.config_filename !=
4055 os.path.basename(options.config_filename)):
4056 self.error('--gclientfile target must be a filename, not a path')
4057 if not options.config_filename:
4058 options.config_filename = self.gclientfile_default
4059 options.entries_filename = options.config_filename + '_entries'
4060 if options.jobs < 1:
4061 self.error('--jobs must be 1 or higher')
maruel@chromium.org0895b752011-08-26 20:40:33 +00004062
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004063 # These hacks need to die.
4064 if not hasattr(options, 'revisions'):
4065 # GClient.RunOnDeps expects it even if not applicable.
4066 options.revisions = []
4067 if not hasattr(options, 'experiments'):
4068 options.experiments = []
4069 if not hasattr(options, 'head'):
4070 options.head = None
4071 if not hasattr(options, 'nohooks'):
4072 options.nohooks = True
4073 if not hasattr(options, 'noprehooks'):
4074 options.noprehooks = True
4075 if not hasattr(options, 'deps_os'):
4076 options.deps_os = None
4077 if not hasattr(options, 'force'):
4078 options.force = None
4079 return (options, args)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004080
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004081
4082def disable_buffering():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004083 # Make stdout auto-flush so buildbot doesn't kill us during lengthy
4084 # operations. Python as a strong tendency to buffer sys.stdout.
4085 sys.stdout = gclient_utils.MakeFileAutoFlush(sys.stdout)
4086 # Make stdout annotated with the thread ids.
4087 sys.stdout = gclient_utils.MakeFileAnnotated(sys.stdout)
maruel@chromium.org0895b752011-08-26 20:40:33 +00004088
4089
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004090def path_contains_tilde():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004091 for element in os.environ['PATH'].split(os.pathsep):
4092 if element.startswith('~') and os.path.abspath(
4093 os.path.realpath(
4094 os.path.expanduser(element))) == DEPOT_TOOLS_DIR:
4095 return True
4096 return False
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004097
4098
4099def can_run_gclient_and_helpers():
Gavin Mak7f5b53f2023-09-07 18:13:01 +00004100 if sys.version_info[0] < 3:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004101 print('\nYour python version %s is unsupported, please upgrade.\n' %
4102 sys.version.split(' ', 1)[0],
4103 file=sys.stderr)
4104 return False
4105 if not sys.executable:
4106 print('\nPython cannot find the location of it\'s own executable.\n',
4107 file=sys.stderr)
4108 return False
4109 if path_contains_tilde():
4110 print(
4111 '\nYour PATH contains a literal "~", which works in some shells ' +
4112 'but will break when python tries to run subprocesses. ' +
4113 'Replace the "~" with $HOME.\n' + 'See https://crbug.com/952865.\n',
4114 file=sys.stderr)
4115 return False
4116 return True
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004117
4118
4119def main(argv):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004120 """Doesn't parse the arguments here, just find the right subcommand to
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004121 execute."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004122 if not can_run_gclient_and_helpers():
4123 return 2
4124 fix_encoding.fix_encoding()
4125 disable_buffering()
4126 setup_color.init()
4127 dispatcher = subcommand.CommandDispatcher(__name__)
4128 try:
4129 return dispatcher.execute(OptionParser(), argv)
4130 except KeyboardInterrupt:
4131 gclient_utils.GClientChildren.KillAllRemainingChildren()
4132 raise
4133 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
4134 print('Error: %s' % str(e), file=sys.stderr)
4135 return 1
4136 finally:
4137 gclient_utils.PrintWarnings()
4138 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004139
4140
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00004141if '__main__' == __name__:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004142 with metrics.collector.print_notice_and_exit():
4143 sys.exit(main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004144
4145# vim: ts=2:sw=2:tw=80:et: