blob: 8ee5bf125f60041d275a8febe6e42edc6c7cfda5 [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."""
Josip Sokcevic8fb358e2023-11-15 22:24:06 +00001577 _is_env_cog = None
1578
1579 @staticmethod
1580 def _IsCog():
1581 """Returns true if the env is cog"""
1582 if GitDependency._is_env_cog is None:
1583 GitDependency._is_env_cog = os.getcwd().startswith(
1584 '/google/cog/cloud')
1585
1586 return GitDependency._is_env_cog
1587
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001588 @staticmethod
1589 def updateProtocol(url, protocol):
1590 """Updates given URL's protocol"""
1591 # only works on urls, skips local paths
1592 if not url or not protocol or not re.match('([a-z]+)://', url) or \
1593 re.match('file://', url):
1594 return url
Edward Lemurb61d3872018-05-09 18:42:47 -04001595
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001596 return re.sub('^([a-z]+):', protocol + ':', url)
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +00001597
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001598 #override
1599 def GetScmName(self):
1600 """Always 'git'."""
1601 return 'git'
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +00001602
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001603 #override
1604 def CreateSCM(self, out_cb=None):
1605 """Create a Wrapper instance suitable for handling this git dependency."""
Josip Sokcevic8fb358e2023-11-15 22:24:06 +00001606 if self._IsCog():
1607 return gclient_scm.CogWrapper()
1608
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001609 return gclient_scm.GitWrapper(self.url,
1610 self.root.root_dir,
1611 self.name,
1612 self.outbuf,
1613 out_cb,
1614 print_outbuf=self.print_outbuf)
Edward Lemurb61d3872018-05-09 18:42:47 -04001615
1616
1617class GClient(GitDependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001618 """Object that represent a gclient checkout. A tree of Dependency(), one per
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001619 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001620
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001621 DEPS_OS_CHOICES = {
1622 "aix6": "unix",
1623 "win32": "win",
1624 "win": "win",
1625 "cygwin": "win",
1626 "darwin": "mac",
1627 "mac": "mac",
1628 "unix": "unix",
1629 "linux": "unix",
1630 "linux2": "unix",
1631 "linux3": "unix",
1632 "android": "android",
1633 "ios": "ios",
1634 "fuchsia": "fuchsia",
1635 "chromeos": "chromeos",
1636 }
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001637
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001638 DEFAULT_CLIENT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001639solutions = [
Edward Lesmes05934952019-12-19 20:38:09 +00001640 { "name" : %(solution_name)r,
1641 "url" : %(solution_url)r,
1642 "deps_file" : %(deps_file)r,
1643 "managed" : %(managed)r,
smutae7ea312016-07-18 11:59:41 -07001644 "custom_deps" : {
1645 },
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02001646 "custom_vars": %(custom_vars)r,
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001647 },
1648]
Robert Iannuccia19649b2018-06-29 16:31:45 +00001649""")
1650
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001651 DEFAULT_CLIENT_CACHE_DIR_TEXT = ("""\
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001652cache_dir = %(cache_dir)r
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001653""")
1654
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001655 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001656# Snapshot generated with gclient revinfo --snapshot
Edward Lesmesc2960242018-03-06 20:50:15 -05001657solutions = %(solution_list)s
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001658""")
1659
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001660 def __init__(self, root_dir, options):
1661 # Do not change previous behavior. Only solution level and immediate
1662 # DEPS are processed.
1663 self._recursion_limit = 2
1664 super(GClient, self).__init__(parent=None,
1665 name=None,
1666 url=None,
1667 managed=True,
1668 custom_deps=None,
1669 custom_vars=None,
1670 custom_hooks=None,
1671 deps_file='unused',
1672 should_process=True,
1673 should_recurse=True,
1674 relative=None,
1675 condition=None,
1676 print_outbuf=True)
Edward Lemure05f18d2018-06-08 17:36:53 +00001677
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001678 self._options = options
1679 if options.deps_os:
1680 enforced_os = options.deps_os.split(',')
1681 else:
1682 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
1683 if 'all' in enforced_os:
1684 enforced_os = self.DEPS_OS_CHOICES.values()
1685 self._enforced_os = tuple(set(enforced_os))
1686 self._enforced_cpu = (detect_host_arch.HostArch(), )
1687 self._root_dir = root_dir
1688 self._cipd_root = None
1689 self.config_content = None
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001690
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001691 def _CheckConfig(self):
1692 """Verify that the config matches the state of the existing checked-out
borenet@google.com88d10082014-03-21 17:24:48 +00001693 solutions."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001694 for dep in self.dependencies:
1695 if dep.managed and dep.url:
1696 scm = dep.CreateSCM()
1697 actual_url = scm.GetActualRemoteURL(self._options)
1698 if actual_url and not scm.DoesRemoteURLMatch(self._options):
1699 mirror = scm.GetCacheMirror()
1700 if mirror:
1701 mirror_string = '%s (exists=%s)' % (mirror.mirror_path,
1702 mirror.exists())
1703 else:
1704 mirror_string = 'not used'
1705 raise gclient_utils.Error(
1706 '''
borenet@google.com88d10082014-03-21 17:24:48 +00001707Your .gclient file seems to be broken. The requested URL is different from what
borenet@google.com0a427372014-04-02 19:12:13 +00001708is actually checked out in %(checkout_path)s.
borenet@google.com88d10082014-03-21 17:24:48 +00001709
borenet@google.com97882362014-04-07 20:06:02 +00001710The .gclient file contains:
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +00001711URL: %(expected_url)s (%(expected_scm)s)
1712Cache mirror: %(mirror_string)s
borenet@google.com97882362014-04-07 20:06:02 +00001713
1714The local checkout in %(checkout_path)s reports:
1715%(actual_url)s (%(actual_scm)s)
borenet@google.com88d10082014-03-21 17:24:48 +00001716
1717You should ensure that the URL listed in .gclient is correct and either change
agabled437d762016-10-17 09:35:11 -07001718it or fix the checkout.
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00001719''' % {
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001720 'checkout_path': os.path.join(
1721 self.root_dir, dep.name),
1722 'expected_url': dep.url,
1723 'expected_scm': dep.GetScmName(),
1724 'mirror_string': mirror_string,
1725 'actual_url': actual_url,
1726 'actual_scm': dep.GetScmName()
1727 })
borenet@google.com88d10082014-03-21 17:24:48 +00001728
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001729 def SetConfig(self, content):
1730 assert not self.dependencies
1731 config_dict = {}
1732 self.config_content = content
1733 try:
1734 exec(content, config_dict)
1735 except SyntaxError as e:
1736 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001737
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001738 # Append any target OS that is not already being enforced to the tuple.
1739 target_os = config_dict.get('target_os', [])
1740 if config_dict.get('target_os_only', False):
1741 self._enforced_os = tuple(set(target_os))
1742 else:
1743 self._enforced_os = tuple(set(self._enforced_os).union(target_os))
stuartmorgan@chromium.org18a4f6a2012-11-02 13:37:24 +00001744
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001745 # Append any target CPU that is not already being enforced to the tuple.
1746 target_cpu = config_dict.get('target_cpu', [])
1747 if config_dict.get('target_cpu_only', False):
1748 self._enforced_cpu = tuple(set(target_cpu))
1749 else:
1750 self._enforced_cpu = tuple(
1751 set(self._enforced_cpu).union(target_cpu))
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001752
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001753 cache_dir = config_dict.get('cache_dir', UNSET_CACHE_DIR)
1754 if cache_dir is not UNSET_CACHE_DIR:
1755 if cache_dir:
1756 cache_dir = os.path.join(self.root_dir, cache_dir)
1757 cache_dir = os.path.abspath(cache_dir)
Andrii Shyshkalov77ce4bd2017-11-27 12:38:18 -08001758
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001759 git_cache.Mirror.SetCachePath(cache_dir)
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001760
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001761 if not target_os and config_dict.get('target_os_only', False):
1762 raise gclient_utils.Error(
1763 'Can\'t use target_os_only if target_os is '
1764 'not specified')
peter@chromium.org1efccc82012-04-27 16:34:38 +00001765
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001766 if not target_cpu and config_dict.get('target_cpu_only', False):
1767 raise gclient_utils.Error(
1768 'Can\'t use target_cpu_only if target_cpu is '
1769 'not specified')
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001770
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001771 deps_to_add = []
1772 for s in config_dict.get('solutions', []):
1773 try:
1774 deps_to_add.append(
1775 GitDependency(
1776 parent=self,
1777 name=s['name'],
1778 # Update URL with scheme in protocol_override
1779 url=GitDependency.updateProtocol(
1780 s['url'], s.get('protocol_override', None)),
1781 managed=s.get('managed', True),
1782 custom_deps=s.get('custom_deps', {}),
1783 custom_vars=s.get('custom_vars', {}),
1784 custom_hooks=s.get('custom_hooks', []),
1785 deps_file=s.get('deps_file', 'DEPS'),
1786 should_process=True,
1787 should_recurse=True,
1788 relative=None,
1789 condition=None,
1790 print_outbuf=True,
1791 # Pass protocol_override down the tree for child deps to
1792 # use.
1793 protocol=s.get('protocol_override', None),
1794 git_dependencies_state=self.git_dependencies_state))
1795 except KeyError:
1796 raise gclient_utils.Error('Invalid .gclient file. Solution is '
1797 'incomplete: %s' % s)
1798 metrics.collector.add('project_urls', [
Edward Lemuraffd4102019-06-05 18:07:49 +00001799 dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Edward Lemur40764b02018-07-20 18:50:29 +00001800 for dep in deps_to_add
1801 if dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001802 ])
Edward Lemur40764b02018-07-20 18:50:29 +00001803
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001804 self.add_dependencies_and_close(deps_to_add,
1805 config_dict.get('hooks', []))
1806 logging.info('SetConfig() done')
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001807
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001808 def SaveConfig(self):
1809 gclient_utils.FileWrite(
1810 os.path.join(self.root_dir, self._options.config_filename),
1811 self.config_content)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001812
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001813 @staticmethod
1814 def LoadCurrentConfig(options):
1815 # type: (optparse.Values) -> GClient
1816 """Searches for and loads a .gclient file relative to the current working
Joanna Wang66286612022-06-30 19:59:13 +00001817 dir."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001818 if options.spec:
1819 client = GClient('.', options)
1820 client.SetConfig(options.spec)
1821 else:
1822 if options.verbose:
1823 print('Looking for %s starting from %s\n' %
1824 (options.config_filename, os.getcwd()))
1825 path = gclient_paths.FindGclientRoot(os.getcwd(),
1826 options.config_filename)
1827 if not path:
1828 if options.verbose:
1829 print('Couldn\'t find configuration file.')
1830 return None
1831 client = GClient(path, options)
1832 client.SetConfig(
1833 gclient_utils.FileRead(
1834 os.path.join(path, options.config_filename)))
maruel@chromium.org69392e72011-10-13 22:09:00 +00001835
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001836 if (options.revisions and len(client.dependencies) > 1
1837 and any('@' not in r for r in options.revisions)):
1838 print((
1839 'You must specify the full solution name like --revision %s@%s\n'
1840 'when you have multiple solutions setup in your .gclient file.\n'
1841 'Other solutions present are: %s.') %
1842 (client.dependencies[0].name, options.revisions[0], ', '.join(
1843 s.name for s in client.dependencies[1:])),
1844 file=sys.stderr)
Joanna Wang66286612022-06-30 19:59:13 +00001845
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001846 return client
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001847
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001848 def SetDefaultConfig(self,
1849 solution_name,
1850 deps_file,
1851 solution_url,
1852 managed=True,
1853 cache_dir=UNSET_CACHE_DIR,
1854 custom_vars=None):
1855 text = self.DEFAULT_CLIENT_FILE_TEXT
1856 format_dict = {
1857 'solution_name': solution_name,
1858 'solution_url': solution_url,
1859 'deps_file': deps_file,
1860 'managed': managed,
1861 'custom_vars': custom_vars or {},
1862 }
Robert Iannuccia19649b2018-06-29 16:31:45 +00001863
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001864 if cache_dir is not UNSET_CACHE_DIR:
1865 text += self.DEFAULT_CLIENT_CACHE_DIR_TEXT
1866 format_dict['cache_dir'] = cache_dir
Robert Iannuccia19649b2018-06-29 16:31:45 +00001867
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001868 self.SetConfig(text % format_dict)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001869
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001870 def _SaveEntries(self):
1871 """Creates a .gclient_entries file to record the list of unique checkouts.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001872
1873 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001874 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001875 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
1876 # makes testing a bit too fun.
1877 result = 'entries = {\n'
1878 for entry in self.root.subtree(False):
1879 result += ' %s: %s,\n' % (pprint.pformat(
1880 entry.name), pprint.pformat(entry.url))
1881 result += '}\n'
1882 file_path = os.path.join(self.root_dir, self._options.entries_filename)
1883 logging.debug(result)
1884 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001885
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001886 def _ReadEntries(self):
1887 """Read the .gclient_entries file for the given client.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001888
1889 Returns:
1890 A sequence of solution names, which will be empty if there is the
1891 entries file hasn't been created yet.
1892 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001893 scope = {}
1894 filename = os.path.join(self.root_dir, self._options.entries_filename)
1895 if not os.path.exists(filename):
1896 return {}
1897 try:
1898 exec(gclient_utils.FileRead(filename), scope)
1899 except SyntaxError as e:
1900 gclient_utils.SyntaxErrorToError(filename, e)
1901 return scope.get('entries', {})
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001902
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001903 def _ExtractFileJsonContents(self, default_filename):
1904 # type: (str) -> Mapping[str,Any]
1905 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001906
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001907 if not os.path.exists(f):
1908 logging.info('File %s does not exist.' % f)
1909 return {}
Joanna Wang01870792022-08-01 19:02:57 +00001910
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001911 with open(f, 'r') as open_f:
1912 logging.info('Reading content from file %s' % f)
1913 content = open_f.read().rstrip()
1914 if content:
1915 return json.loads(content)
Joanna Wang66286612022-06-30 19:59:13 +00001916 return {}
1917
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001918 def _WriteFileContents(self, default_filename, content):
1919 # type: (str, str) -> None
1920 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001921
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001922 with open(f, 'w') as open_f:
1923 logging.info('Writing to file %s' % f)
1924 open_f.write(content)
Joanna Wangf3edc502022-07-20 00:12:10 +00001925
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001926 def _EnforceSkipSyncRevisions(self, patch_refs):
1927 # type: (Mapping[str, str]) -> Mapping[str, str]
1928 """Checks for and enforces revisions for skipping deps syncing."""
1929 previous_sync_commits = self._ExtractFileJsonContents(
1930 PREVIOUS_SYNC_COMMITS_FILE)
Joanna Wang66286612022-06-30 19:59:13 +00001931
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001932 if not previous_sync_commits:
1933 return {}
maruel@chromium.org918a9ae2010-05-28 15:50:30 +00001934
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001935 # Current `self.dependencies` only contain solutions. If a patch_ref is
1936 # not for a solution, then it is for a solution's dependency or recursed
1937 # dependency which we cannot support while skipping sync.
1938 if patch_refs:
1939 unclaimed_prs = []
1940 candidates = []
1941 for dep in self.dependencies:
1942 origin, _ = gclient_utils.SplitUrlRevision(dep.url)
1943 candidates.extend([origin, dep.name])
1944 for patch_repo in patch_refs:
1945 if not gclient_utils.FuzzyMatchRepo(patch_repo, candidates):
1946 unclaimed_prs.append(patch_repo)
1947 if unclaimed_prs:
1948 print(
1949 'We cannot skip syncs when there are --patch-refs flags for '
1950 'non-solution dependencies. To skip syncing, remove patch_refs '
1951 'for: \n%s' % '\n'.join(unclaimed_prs))
1952 return {}
Edward Lesmesc621b212018-03-21 20:26:56 -04001953
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001954 # We cannot skip syncing if there are custom_vars that differ from the
1955 # previous run's custom_vars.
1956 previous_custom_vars = self._ExtractFileJsonContents(
1957 PREVIOUS_CUSTOM_VARS_FILE)
1958
1959 cvs_by_name = {s.name: s.custom_vars for s in self.dependencies}
1960
1961 skip_sync_revisions = {}
1962 for name, commit in previous_sync_commits.items():
1963 previous_vars = previous_custom_vars.get(name)
1964 if previous_vars == cvs_by_name.get(name) or (
1965 not previous_vars and not cvs_by_name.get(name)):
1966 skip_sync_revisions[name] = commit
1967 else:
1968 print(
1969 'We cannot skip syncs when custom_vars for solutions have '
1970 'changed since the last sync run on this machine.\n'
1971 '\nRemoving skip_sync_revision for:\n'
1972 'solution: %s, current: %r, previous: %r.' %
1973 (name, cvs_by_name.get(name), previous_vars))
1974 print('no-sync experiment enabled with %r' % skip_sync_revisions)
1975 return skip_sync_revisions
1976
1977 # TODO(crbug.com/1340695): Remove handling revisions without '@'.
1978 def _EnforceRevisions(self):
1979 """Checks for revision overrides."""
1980 revision_overrides = {}
1981 if self._options.head:
1982 return revision_overrides
1983 if not self._options.revisions:
1984 return revision_overrides
1985 solutions_names = [s.name for s in self.dependencies]
1986 for index, revision in enumerate(self._options.revisions):
1987 if not '@' in revision:
1988 # Support for --revision 123
1989 revision = '%s@%s' % (solutions_names[index], revision)
1990 name, rev = revision.split('@', 1)
1991 revision_overrides[name] = rev
1992 return revision_overrides
1993
1994 def _EnforcePatchRefsAndBranches(self):
1995 # type: () -> Tuple[Mapping[str, str], Mapping[str, str]]
1996 """Checks for patch refs."""
1997 patch_refs = {}
1998 target_branches = {}
1999 if not self._options.patch_refs:
2000 return patch_refs, target_branches
2001 for given_patch_ref in self._options.patch_refs:
2002 patch_repo, _, patch_ref = given_patch_ref.partition('@')
2003 if not patch_repo or not patch_ref or ':' not in patch_ref:
2004 raise gclient_utils.Error(
2005 'Wrong revision format: %s should be of the form '
2006 'patch_repo@target_branch:patch_ref.' % given_patch_ref)
2007 target_branch, _, patch_ref = patch_ref.partition(':')
2008 target_branches[patch_repo] = target_branch
2009 patch_refs[patch_repo] = patch_ref
2010 return patch_refs, target_branches
2011
Gavin Mak50b27a52023-09-19 22:44:59 +00002012 def _InstallPreCommitHook(self):
2013 # On Windows, this path is written to the file as
2014 # "dir\hooks\pre-commit.py" but it gets interpreted as
2015 # "dirhookspre-commit.py".
2016 gclient_hook_path = os.path.join(DEPOT_TOOLS_DIR, 'hooks',
2017 'pre-commit.py').replace('\\', '\\\\')
2018 gclient_hook_content = '\n'.join((
2019 f'{PRECOMMIT_HOOK_VAR}={gclient_hook_path}',
2020 f'if [ -f "${PRECOMMIT_HOOK_VAR}" ]; then',
2021 f' python3 "${PRECOMMIT_HOOK_VAR}" || exit 1',
2022 'fi',
2023 ))
2024
2025 soln = gclient_paths.GetPrimarySolutionPath()
2026 if not soln:
2027 print('Could not find gclient solution.')
2028 return
2029
2030 git_dir = os.path.join(soln, '.git')
2031 if not os.path.exists(git_dir):
2032 return
2033
Philipp Wollermanna45d2d42023-09-21 02:45:42 +00002034 git_hooks_dir = os.path.join(git_dir, 'hooks')
2035 os.makedirs(git_hooks_dir, exist_ok=True)
2036
Gavin Mak50b27a52023-09-19 22:44:59 +00002037 hook = os.path.join(git_dir, 'hooks', 'pre-commit')
2038 if os.path.exists(hook):
2039 with open(hook, 'r') as f:
2040 content = f.read()
2041 if PRECOMMIT_HOOK_VAR in content:
2042 print(f'{hook} already contains the gclient pre-commit hook.')
2043 else:
2044 print(f'A pre-commit hook already exists at {hook}.\n'
2045 f'Please append the following lines to the hook:\n\n'
2046 f'{gclient_hook_content}')
2047 return
2048
2049 print(f'Creating a pre-commit hook at {hook}')
2050 with open(hook, 'w') as f:
2051 f.write('#!/bin/sh\n')
2052 f.write(f'{gclient_hook_content}\n')
2053 os.chmod(hook, 0o755)
2054
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002055 def _RemoveUnversionedGitDirs(self):
2056 """Remove directories that are no longer part of the checkout.
Edward Lemur5b1fa942018-10-04 23:22:09 +00002057
2058 Notify the user if there is an orphaned entry in their working copy.
2059 Only delete the directory if there are no changes in it, and
2060 delete_unversioned_trees is set to true.
Josip Sokcevic1b8211f2022-09-30 17:46:53 +00002061
2062 Returns CIPD packages that are no longer versioned.
Edward Lemur5b1fa942018-10-04 23:22:09 +00002063 """
2064
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002065 entry_names_and_sync = [(i.name, i._should_sync)
2066 for i in self.root.subtree(False) if i.url]
2067 entries = []
2068 if entry_names_and_sync:
2069 entries, _ = zip(*entry_names_and_sync)
2070 full_entries = [
2071 os.path.join(self.root_dir, e.replace('/', os.path.sep))
2072 for e in entries
2073 ]
2074 no_sync_entries = [
2075 name for name, should_sync in entry_names_and_sync
2076 if not should_sync
2077 ]
Edward Lemur5b1fa942018-10-04 23:22:09 +00002078
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002079 removed_cipd_entries = []
Joanna Wang60adf7b2023-10-06 00:04:28 +00002080 read_entries = self._ReadEntries()
2081 # We process entries sorted in reverse to ensure a child dir is
2082 # always deleted before its parent dir.
2083 # This is especially important for submodules with pinned revisions
2084 # overwritten by a vars or custom_deps. In this case, if a parent
2085 # submodule is encountered first in the loop, it cannot tell the
2086 # difference between modifications from the vars or actual user
2087 # modifications that should be kept. http://crbug/1486677#c9 for
2088 # more details.
2089 for entry in sorted(read_entries, reverse=True):
2090 prev_url = read_entries[entry]
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002091 if not prev_url:
2092 # entry must have been overridden via .gclient custom_deps
2093 continue
2094 if any(entry.startswith(sln) for sln in no_sync_entries):
2095 # Dependencies of solutions that skipped syncing would not
2096 # show up in `entries`.
2097 continue
2098 if (':' in entry):
2099 # This is a cipd package. Don't clean it up, but prepare for
2100 # return
2101 if entry not in entries:
2102 removed_cipd_entries.append(entry)
2103 continue
2104 # Fix path separator on Windows.
2105 entry_fixed = entry.replace('/', os.path.sep)
2106 e_dir = os.path.join(self.root_dir, entry_fixed)
2107 # Use entry and not entry_fixed there.
2108 if (entry not in entries and
2109 (not any(path.startswith(entry + '/') for path in entries))
2110 and os.path.exists(e_dir)):
2111 # The entry has been removed from DEPS.
2112 scm = gclient_scm.GitWrapper(prev_url, self.root_dir,
2113 entry_fixed, self.outbuf)
Edward Lemur5b1fa942018-10-04 23:22:09 +00002114
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002115 # Check to see if this directory is now part of a higher-up
2116 # checkout.
2117 scm_root = None
2118 try:
2119 scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2120 scm.checkout_path)
2121 except subprocess2.CalledProcessError:
2122 pass
2123 if not scm_root:
2124 logging.warning(
2125 'Could not find checkout root for %s. Unable to '
2126 'determine whether it is part of a higher-level '
2127 'checkout, so not removing.' % entry)
2128 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002129
Joanna Wang60adf7b2023-10-06 00:04:28 +00002130 versioned_state = None
2131 # Check if this is a submodule or versioned directory.
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002132 if os.path.abspath(scm_root) == os.path.abspath(e_dir):
2133 e_par_dir = os.path.join(e_dir, os.pardir)
2134 if gclient_scm.scm.GIT.IsInsideWorkTree(e_par_dir):
2135 par_scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2136 e_par_dir)
2137 # rel_e_dir : relative path of entry w.r.t. its parent
2138 # repo.
2139 rel_e_dir = os.path.relpath(e_dir, par_scm_root)
Joanna Wang60adf7b2023-10-06 00:04:28 +00002140 versioned_state = gclient_scm.scm.GIT.IsVersioned(
2141 par_scm_root, rel_e_dir)
2142 # This is to handle the case of third_party/WebKit migrating
2143 # from being a DEPS entry to being part of the main project. If
2144 # the subproject is a Git project, we need to remove its .git
2145 # folder. Otherwise git operations on that folder will have
2146 # different effects depending on the current working directory.
2147 if versioned_state == gclient_scm.scm.VERSIONED_DIR:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002148 save_dir = scm.GetGitBackupDirPath()
2149 # Remove any eventual stale backup dir for the same
2150 # project.
2151 if os.path.exists(save_dir):
2152 gclient_utils.rmtree(save_dir)
2153 os.rename(os.path.join(e_dir, '.git'), save_dir)
2154 # When switching between the two states (entry/ is a
2155 # subproject -> entry/ is part of the outer
2156 # project), it is very likely that some files are
2157 # changed in the checkout, unless we are jumping
2158 # *exactly* across the commit which changed just
2159 # DEPS. In such case we want to cleanup any eventual
2160 # stale files (coming from the old subproject) in
2161 # order to end up with a clean checkout.
2162 gclient_scm.scm.GIT.CleanupDir(
2163 par_scm_root, rel_e_dir)
2164 assert not os.path.exists(
2165 os.path.join(e_dir, '.git'))
2166 print(
2167 '\nWARNING: \'%s\' has been moved from DEPS to a higher '
2168 'level checkout. The git folder containing all the local'
2169 ' branches has been saved to %s.\n'
2170 'If you don\'t care about its state you can safely '
2171 'remove that folder to free up space.' %
2172 (entry, save_dir))
2173 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002174
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002175 if scm_root in full_entries:
2176 logging.info(
2177 '%s is part of a higher level checkout, not removing',
2178 scm.GetCheckoutRoot())
2179 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002180
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002181 file_list = []
2182 scm.status(self._options, [], file_list)
2183 modified_files = file_list != []
2184 if (not self._options.delete_unversioned_trees
2185 or (modified_files and not self._options.force)):
2186 # There are modified files in this entry. Keep warning until
2187 # removed.
2188 self.add_dependency(
2189 GitDependency(
2190 parent=self,
2191 name=entry,
2192 # Update URL with scheme in protocol_override
2193 url=GitDependency.updateProtocol(
2194 prev_url, self.protocol),
2195 managed=False,
2196 custom_deps={},
2197 custom_vars={},
2198 custom_hooks=[],
2199 deps_file=None,
2200 should_process=True,
2201 should_recurse=False,
2202 relative=None,
2203 condition=None,
2204 protocol=self.protocol,
2205 git_dependencies_state=self.git_dependencies_state))
2206 if modified_files and self._options.delete_unversioned_trees:
2207 print(
2208 '\nWARNING: \'%s\' is no longer part of this client.\n'
2209 'Despite running \'gclient sync -D\' no action was taken '
2210 'as there are modifications.\nIt is recommended you revert '
2211 'all changes or run \'gclient sync -D --force\' next '
2212 'time.' % entry_fixed)
2213 else:
2214 print(
2215 '\nWARNING: \'%s\' is no longer part of this client.\n'
2216 'It is recommended that you manually remove it or use '
2217 '\'gclient sync -D\' next time.' % entry_fixed)
2218 else:
2219 # Delete the entry
2220 print('\n________ deleting \'%s\' in \'%s\'' %
2221 (entry_fixed, self.root_dir))
2222 gclient_utils.rmtree(e_dir)
Joanna Wang60adf7b2023-10-06 00:04:28 +00002223 # We restore empty directories of submodule paths.
2224 if versioned_state == gclient_scm.scm.VERSIONED_SUBMODULE:
2225 gclient_scm.scm.GIT.Capture(
2226 ['restore', '--', rel_e_dir], cwd=par_scm_root)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002227 # record the current list of entries for next time
2228 self._SaveEntries()
2229 return removed_cipd_entries
Edward Lemur5b1fa942018-10-04 23:22:09 +00002230
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002231 def RunOnDeps(self,
2232 command,
2233 args,
2234 ignore_requirements=False,
2235 progress=True):
2236 """Runs a command on each dependency in a client and its dependencies.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002237
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002238 Args:
2239 command: The command to use (e.g., 'status' or 'diff')
2240 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002241 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002242 if not self.dependencies:
2243 raise gclient_utils.Error('No solution specified')
Michael Mossd683d7c2018-06-15 05:05:17 +00002244
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002245 revision_overrides = {}
2246 patch_refs = {}
2247 target_branches = {}
2248 skip_sync_revisions = {}
2249 # It's unnecessary to check for revision overrides for 'recurse'.
2250 # Save a few seconds by not calling _EnforceRevisions() in that case.
2251 if command not in ('diff', 'recurse', 'runhooks', 'status', 'revert',
2252 'validate'):
2253 self._CheckConfig()
2254 revision_overrides = self._EnforceRevisions()
Edward Lesmesc621b212018-03-21 20:26:56 -04002255
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002256 if command == 'update':
2257 patch_refs, target_branches = self._EnforcePatchRefsAndBranches()
2258 if NO_SYNC_EXPERIMENT in self._options.experiments:
2259 skip_sync_revisions = self._EnforceSkipSyncRevisions(patch_refs)
Joanna Wang66286612022-06-30 19:59:13 +00002260
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002261 # Store solutions' custom_vars on memory to compare in the next run.
2262 # All dependencies added later are inherited from the current
2263 # self.dependencies.
2264 custom_vars = {
2265 dep.name: dep.custom_vars
2266 for dep in self.dependencies if dep.custom_vars
Michael Mossd683d7c2018-06-15 05:05:17 +00002267 }
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002268 if custom_vars:
2269 self._WriteFileContents(PREVIOUS_CUSTOM_VARS_FILE,
2270 json.dumps(custom_vars))
2271
2272 # Disable progress for non-tty stdout.
2273 should_show_progress = (setup_color.IS_TTY and not self._options.verbose
2274 and progress)
2275 pm = None
2276 if should_show_progress:
2277 if command in ('update', 'revert'):
2278 pm = Progress('Syncing projects', 1)
2279 elif command in ('recurse', 'validate'):
2280 pm = Progress(' '.join(args), 1)
2281 work_queue = gclient_utils.ExecutionQueue(
2282 self._options.jobs,
2283 pm,
2284 ignore_requirements=ignore_requirements,
2285 verbose=self._options.verbose)
2286 for s in self.dependencies:
2287 if s.should_process:
2288 work_queue.enqueue(s)
2289 work_queue.flush(revision_overrides,
2290 command,
2291 args,
2292 options=self._options,
2293 patch_refs=patch_refs,
2294 target_branches=target_branches,
2295 skip_sync_revisions=skip_sync_revisions)
2296
2297 if revision_overrides:
2298 print(
2299 'Please fix your script, having invalid --revision flags will soon '
2300 'be considered an error.',
2301 file=sys.stderr)
2302
2303 if patch_refs:
2304 raise gclient_utils.Error(
2305 'The following --patch-ref flags were not used. Please fix it:\n%s'
2306 % ('\n'.join(patch_repo + '@' + patch_ref
2307 for patch_repo, patch_ref in patch_refs.items())))
2308
2309 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
2310 # they have fsmonitor enabled.
2311 if command == 'update':
2312 # Check if any of the root dependency have submodules.
2313 is_submoduled = any(
2314 map(
2315 lambda d: d.git_dependencies_state in
2316 (gclient_eval.SUBMODULES, gclient_eval.SYNC),
2317 self.dependencies))
2318 if is_submoduled:
2319 git_common.warn_submodule()
2320
2321 # Once all the dependencies have been processed, it's now safe to write
2322 # out the gn_args_file and run the hooks.
2323 removed_cipd_entries = []
2324 if command == 'update':
2325 for dependency in self.dependencies:
2326 gn_args_dep = dependency
2327 if gn_args_dep._gn_args_from:
2328 deps_map = {
2329 dep.name: dep
2330 for dep in gn_args_dep.dependencies
2331 }
2332 gn_args_dep = deps_map.get(gn_args_dep._gn_args_from)
2333 if gn_args_dep and gn_args_dep.HasGNArgsFile():
2334 gn_args_dep.WriteGNArgsFile()
2335
2336 removed_cipd_entries = self._RemoveUnversionedGitDirs()
2337
2338 # Sync CIPD dependencies once removed deps are deleted. In case a git
2339 # dependency was moved to CIPD, we want to remove the old git directory
2340 # first and then sync the CIPD dep.
2341 if self._cipd_root:
2342 self._cipd_root.run(command)
2343 # It's possible that CIPD removed some entries that are now part of
2344 # git worktree. Try to checkout those directories
2345 if removed_cipd_entries:
2346 for cipd_entry in removed_cipd_entries:
2347 cwd = os.path.join(self._root_dir, cipd_entry.split(':')[0])
2348 cwd, tail = os.path.split(cwd)
2349 if cwd:
2350 try:
2351 gclient_scm.scm.GIT.Capture(['checkout', tail],
2352 cwd=cwd)
Joanna Wang60adf7b2023-10-06 00:04:28 +00002353 except (subprocess2.CalledProcessError, OSError):
2354 # repo of the deleted cipd may also have been deleted.
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002355 pass
2356
2357 if not self._options.nohooks:
2358 if should_show_progress:
2359 pm = Progress('Running hooks', 1)
2360 self.RunHooksRecursively(self._options, pm)
2361
2362 self._WriteFileContents(PREVIOUS_SYNC_COMMITS_FILE,
2363 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
2364
2365 return 0
2366
2367 def PrintRevInfo(self):
2368 if not self.dependencies:
2369 raise gclient_utils.Error('No solution specified')
2370 # Load all the settings.
2371 work_queue = gclient_utils.ExecutionQueue(self._options.jobs,
2372 None,
2373 False,
2374 verbose=self._options.verbose)
2375 for s in self.dependencies:
2376 if s.should_process:
2377 work_queue.enqueue(s)
2378 work_queue.flush({},
2379 None, [],
2380 options=self._options,
2381 patch_refs=None,
2382 target_branches=None,
2383 skip_sync_revisions=None)
2384
2385 def ShouldPrintRevision(dep):
2386 return (not self._options.filter
2387 or dep.FuzzyMatchUrl(self._options.filter))
2388
2389 if self._options.snapshot:
2390 json_output = []
2391 # First level at .gclient
2392 for d in self.dependencies:
2393 entries = {}
2394
2395 def GrabDeps(dep):
2396 """Recursively grab dependencies."""
2397 for rec_d in dep.dependencies:
2398 rec_d.PinToActualRevision()
2399 if ShouldPrintRevision(rec_d):
2400 entries[rec_d.name] = rec_d.url
2401 GrabDeps(rec_d)
2402
2403 GrabDeps(d)
2404 json_output.append({
2405 'name': d.name,
2406 'solution_url': d.url,
2407 'deps_file': d.deps_file,
2408 'managed': d.managed,
2409 'custom_deps': entries,
2410 })
2411 if self._options.output_json == '-':
2412 print(json.dumps(json_output, indent=2, separators=(',', ': ')))
2413 elif self._options.output_json:
2414 with open(self._options.output_json, 'w') as f:
2415 json.dump(json_output, f)
2416 else:
2417 # Print the snapshot configuration file
2418 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {
2419 'solution_list': pprint.pformat(json_output, indent=2),
2420 })
Michael Mossd683d7c2018-06-15 05:05:17 +00002421 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002422 entries = {}
2423 for d in self.root.subtree(False):
2424 if self._options.actual:
2425 d.PinToActualRevision()
2426 if ShouldPrintRevision(d):
2427 entries[d.name] = d.url
2428 if self._options.output_json:
2429 json_output = {
2430 name: {
2431 'url': rev.split('@')[0] if rev else None,
2432 'rev':
2433 rev.split('@')[1] if rev and '@' in rev else None,
2434 }
2435 for name, rev in entries.items()
2436 }
2437 if self._options.output_json == '-':
2438 print(
2439 json.dumps(json_output,
2440 indent=2,
2441 separators=(',', ': ')))
2442 else:
2443 with open(self._options.output_json, 'w') as f:
2444 json.dump(json_output, f)
2445 else:
2446 keys = sorted(entries.keys())
2447 for x in keys:
2448 print('%s: %s' % (x, entries[x]))
2449 logging.info(str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002450
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002451 def ParseDepsFile(self):
2452 """No DEPS to parse for a .gclient file."""
2453 raise gclient_utils.Error('Internal error')
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00002454
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002455 def PrintLocationAndContents(self):
2456 # Print out the .gclient file. This is longer than if we just printed
2457 # the client dict, but more legible, and it might contain helpful
2458 # comments.
2459 print('Loaded .gclient config in %s:\n%s' %
2460 (self.root_dir, self.config_content))
sergiyb@chromium.orgfa2707e2016-03-12 00:40:56 +00002461
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002462 def GetCipdRoot(self):
2463 if not self._cipd_root:
2464 self._cipd_root = gclient_scm.CipdRoot(
2465 self.root_dir,
2466 # TODO(jbudorick): Support other service URLs as necessary.
2467 # Service URLs should be constant over the scope of a cipd
2468 # root, so a var per DEPS file specifying the service URL
2469 # should suffice.
Yiwei Zhang52353702023-09-18 15:53:52 +00002470 'https://chrome-infra-packages.appspot.com',
2471 log_level='info' if self._options.verbose else None)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002472 return self._cipd_root
John Budorickd3ba72b2018-03-20 12:27:42 -07002473
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002474 @property
2475 def root_dir(self):
2476 """Root directory of gclient checkout."""
2477 return self._root_dir
maruel@chromium.org75a59272010-06-11 22:34:03 +00002478
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002479 @property
2480 def enforced_os(self):
2481 """What deps_os entries that are to be parsed."""
2482 return self._enforced_os
maruel@chromium.org271375b2010-06-23 19:17:38 +00002483
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002484 @property
2485 def target_os(self):
2486 return self._enforced_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +00002487
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002488 @property
2489 def target_cpu(self):
2490 return self._enforced_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -08002491
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002492
John Budorick0f7b2002018-01-19 15:46:17 -08002493class CipdDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002494 """A Dependency object that represents a single CIPD package."""
2495 def __init__(self, parent, name, dep_value, cipd_root, custom_vars,
2496 should_process, relative, condition):
2497 package = dep_value['package']
2498 version = dep_value['version']
2499 url = urllib.parse.urljoin(cipd_root.service_url,
2500 '%s@%s' % (package, version))
2501 super(CipdDependency, self).__init__(parent=parent,
2502 name=name + ':' + package,
2503 url=url,
2504 managed=None,
2505 custom_deps=None,
2506 custom_vars=custom_vars,
2507 custom_hooks=None,
2508 deps_file=None,
2509 should_process=should_process,
2510 should_recurse=False,
2511 relative=relative,
2512 condition=condition)
2513 self._cipd_package = None
2514 self._cipd_root = cipd_root
2515 # CIPD wants /-separated paths, even on Windows.
2516 native_subdir_path = os.path.relpath(
2517 os.path.join(self.root.root_dir, name), cipd_root.root_dir)
2518 self._cipd_subdir = posixpath.join(*native_subdir_path.split(os.sep))
2519 self._package_name = package
2520 self._package_version = version
John Budorick0f7b2002018-01-19 15:46:17 -08002521
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002522 #override
2523 def run(self, revision_overrides, command, args, work_queue, options,
2524 patch_refs, target_branches, skip_sync_revisions):
2525 """Runs |command| then parse the DEPS file."""
2526 logging.info('CipdDependency(%s).run()' % self.name)
2527 if not self.should_process:
2528 return
2529 self._CreatePackageIfNecessary()
2530 super(CipdDependency,
2531 self).run(revision_overrides, command, args, work_queue, options,
2532 patch_refs, target_branches, skip_sync_revisions)
John Budorickd3ba72b2018-03-20 12:27:42 -07002533
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002534 def _CreatePackageIfNecessary(self):
2535 # We lazily create the CIPD package to make sure that only packages
2536 # that we want (as opposed to all packages defined in all DEPS files
2537 # we parse) get added to the root and subsequently ensured.
2538 if not self._cipd_package:
2539 self._cipd_package = self._cipd_root.add_package(
2540 self._cipd_subdir, self._package_name, self._package_version)
John Budorickd3ba72b2018-03-20 12:27:42 -07002541
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002542 def ParseDepsFile(self):
2543 """CIPD dependencies are not currently allowed to have nested deps."""
2544 self.add_dependencies_and_close([], [])
John Budorick0f7b2002018-01-19 15:46:17 -08002545
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002546 #override
2547 def verify_validity(self):
2548 """CIPD dependencies allow duplicate name for packages in same directory."""
2549 logging.info('Dependency(%s).verify_validity()' % self.name)
2550 return True
John Budorick0f7b2002018-01-19 15:46:17 -08002551
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002552 #override
2553 def GetScmName(self):
2554 """Always 'cipd'."""
2555 return 'cipd'
Shenghua Zhang6f830312018-02-26 11:45:07 -08002556
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002557 def GetExpandedPackageName(self):
2558 """Return the CIPD package name with the variables evaluated."""
2559 package = self._cipd_root.expand_package_name(self._package_name)
2560 if package:
2561 return package
2562 return self._package_name
John Budorick0f7b2002018-01-19 15:46:17 -08002563
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002564 #override
2565 def CreateSCM(self, out_cb=None):
2566 """Create a Wrapper instance suitable for handling this CIPD dependency."""
2567 self._CreatePackageIfNecessary()
2568 return gclient_scm.CipdWrapper(self.url,
2569 self.root.root_dir,
2570 self.name,
2571 self.outbuf,
2572 out_cb,
2573 root=self._cipd_root,
2574 package=self._cipd_package)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +00002575
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002576 def hierarchy(self, include_url=False, graphviz=False):
2577 if graphviz:
2578 return '' # graphviz lines not implemented for cipd deps.
2579 return self.parent.hierarchy(include_url) + ' -> ' + self._cipd_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08002580
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002581 def ToLines(self):
2582 # () -> Sequence[str]
2583 """Return a list of lines representing this in a DEPS file."""
2584 def escape_cipd_var(package):
2585 return package.replace('{', '{{').replace('}', '}}')
Edward Lemure4e15042018-06-28 18:07:00 +00002586
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002587 s = []
2588 self._CreatePackageIfNecessary()
2589 if self._cipd_package.authority_for_subdir:
2590 condition_part = ([' "condition": %r,' %
2591 self.condition] if self.condition else [])
2592 s.extend([
2593 ' # %s' % self.hierarchy(include_url=False),
2594 ' "%s": {' % (self.name.split(':')[0], ),
2595 ' "packages": [',
2596 ])
2597 for p in sorted(self._cipd_root.packages(self._cipd_subdir),
2598 key=lambda x: x.name):
2599 s.extend([
2600 ' {',
2601 ' "package": "%s",' % escape_cipd_var(p.name),
2602 ' "version": "%s",' % p.version,
2603 ' },',
2604 ])
John Budorickc35aba52018-06-28 20:57:03 +00002605
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002606 s.extend([
2607 ' ],',
2608 ' "dep_type": "cipd",',
2609 ] + condition_part + [
2610 ' },',
2611 '',
2612 ])
2613 return s
John Budorick0f7b2002018-01-19 15:46:17 -08002614
2615
maruel@chromium.org5ca27692010-05-26 19:32:41 +00002616#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002617
2618
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002619@subcommand.usage('[command] [args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002620@metrics.collector.collect_metrics('gclient recurse')
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002621def CMDrecurse(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002622 """Operates [command args ...] on all the dependencies.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002623
Arthur Milchior08cd5fe2022-07-28 20:38:47 +00002624 Change directory to each dependency's directory, and call [command
2625 args ...] there. Sets GCLIENT_DEP_PATH environment variable as the
2626 dep's relative location to root directory of the checkout.
2627
2628 Examples:
2629 * `gclient recurse --no-progress -j1 sh -c 'echo "$GCLIENT_DEP_PATH"'`
2630 print the relative path of each dependency.
2631 * `gclient recurse --no-progress -j1 sh -c "pwd"`
2632 print the absolute path of each dependency.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002633 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002634 # Stop parsing at the first non-arg so that these go through to the command
2635 parser.disable_interspersed_args()
2636 parser.add_option('-s',
2637 '--scm',
2638 action='append',
2639 default=[],
2640 help='Choose scm types to operate upon.')
2641 parser.add_option('-i',
2642 '--ignore',
2643 action='store_true',
2644 help='Ignore non-zero return codes from subcommands.')
2645 parser.add_option(
2646 '--prepend-dir',
2647 action='store_true',
2648 help='Prepend relative dir for use with git <cmd> --null.')
2649 parser.add_option(
2650 '--no-progress',
2651 action='store_true',
2652 help='Disable progress bar that shows sub-command updates')
2653 options, args = parser.parse_args(args)
2654 if not args:
2655 print('Need to supply a command!', file=sys.stderr)
2656 return 1
2657 root_and_entries = gclient_utils.GetGClientRootAndEntries()
2658 if not root_and_entries:
2659 print(
2660 'You need to run gclient sync at least once to use \'recurse\'.\n'
2661 'This is because .gclient_entries needs to exist and be up to date.',
2662 file=sys.stderr)
2663 return 1
davidbarr@chromium.org47ca0ee2012-03-02 16:06:11 +00002664
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002665 # Normalize options.scm to a set()
2666 scm_set = set()
2667 for scm in options.scm:
2668 scm_set.update(scm.split(','))
2669 options.scm = scm_set
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002670
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002671 options.nohooks = True
2672 client = GClient.LoadCurrentConfig(options)
2673 if not client:
2674 raise gclient_utils.Error(
2675 'client not configured; see \'gclient config\'')
2676 return client.RunOnDeps('recurse',
2677 args,
2678 ignore_requirements=True,
2679 progress=not options.no_progress)
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002680
2681
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002682@subcommand.usage('[args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002683@metrics.collector.collect_metrics('gclient fetch')
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002684def CMDfetch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002685 """Fetches upstream commits for all modules.
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002686
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002687 Completely git-specific. Simply runs 'git fetch [args ...]' for each module.
2688 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002689 (options, args) = parser.parse_args(args)
2690 return CMDrecurse(
2691 OptionParser(),
2692 ['--jobs=%d' % options.jobs, '--scm=git', 'git', 'fetch'] + args)
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00002693
2694
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002695class Flattener(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002696 """Flattens a gclient solution."""
2697 def __init__(self, client, pin_all_deps=False):
2698 """Constructor.
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002699
2700 Arguments:
2701 client (GClient): client to flatten
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002702 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2703 in DEPS
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002704 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002705 self._client = client
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002706
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002707 self._deps_string = None
2708 self._deps_graph_lines = None
2709 self._deps_files = set()
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002710
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002711 self._allowed_hosts = set()
2712 self._deps = {}
2713 self._hooks = []
2714 self._pre_deps_hooks = []
2715 self._vars = {}
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002716
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002717 self._flatten(pin_all_deps=pin_all_deps)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002718
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002719 @property
2720 def deps_string(self):
2721 assert self._deps_string is not None
2722 return self._deps_string
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002723
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002724 @property
2725 def deps_graph_lines(self):
2726 assert self._deps_graph_lines is not None
2727 return self._deps_graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00002728
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002729 @property
2730 def deps_files(self):
2731 return self._deps_files
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002732
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002733 def _pin_dep(self, dep):
2734 """Pins a dependency to specific full revision sha.
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002735
2736 Arguments:
2737 dep (Dependency): dependency to process
2738 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002739 if dep.url is None:
2740 return
Michael Mossd683d7c2018-06-15 05:05:17 +00002741
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002742 # Make sure the revision is always fully specified (a hash),
2743 # as opposed to refs or tags which might change. Similarly,
2744 # shortened shas might become ambiguous; make sure to always
2745 # use full one for pinning.
2746 revision = gclient_utils.SplitUrlRevision(dep.url)[1]
2747 if not revision or not gclient_utils.IsFullGitSha(revision):
2748 dep.PinToActualRevision()
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002749
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002750 def _flatten(self, pin_all_deps=False):
2751 """Runs the flattener. Saves resulting DEPS string.
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002752
2753 Arguments:
2754 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2755 in DEPS
2756 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002757 for solution in self._client.dependencies:
2758 self._add_dep(solution)
2759 self._flatten_dep(solution)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002760
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002761 if pin_all_deps:
2762 for dep in self._deps.values():
2763 self._pin_dep(dep)
Paweł Hajdan, Jr39300ba2017-08-11 16:52:38 +02002764
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002765 def add_deps_file(dep):
2766 # Only include DEPS files referenced by recursedeps.
2767 if not dep.should_recurse:
2768 return
2769 deps_file = dep.deps_file
2770 deps_path = os.path.join(self._client.root_dir, dep.name, deps_file)
2771 if not os.path.exists(deps_path):
2772 # gclient has a fallback that if deps_file doesn't exist, it'll
2773 # try DEPS. Do the same here.
2774 deps_file = 'DEPS'
2775 deps_path = os.path.join(self._client.root_dir, dep.name,
2776 deps_file)
2777 if not os.path.exists(deps_path):
2778 return
2779 assert dep.url
2780 self._deps_files.add((dep.url, deps_file, dep.hierarchy_data()))
Paweł Hajdan, Jr76a9d042017-08-18 20:05:41 +02002781
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002782 for dep in self._deps.values():
2783 add_deps_file(dep)
Joanna Wang9144b672023-02-24 23:36:17 +00002784
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002785 gn_args_dep = self._deps.get(self._client.dependencies[0]._gn_args_from,
2786 self._client.dependencies[0])
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002787
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002788 self._deps_graph_lines = _DepsToDotGraphLines(self._deps)
2789 self._deps_string = '\n'.join(
2790 _GNSettingsToLines(gn_args_dep._gn_args_file, gn_args_dep._gn_args)
2791 + _AllowedHostsToLines(self._allowed_hosts) +
2792 _DepsToLines(self._deps) + _HooksToLines('hooks', self._hooks) +
2793 _HooksToLines('pre_deps_hooks', self._pre_deps_hooks) +
2794 _VarsToLines(self._vars) + [
2795 '# %s, %s' % (url, deps_file)
2796 for url, deps_file, _ in sorted(self._deps_files)
2797 ] + ['']) # Ensure newline at end of file.
2798
2799 def _add_dep(self, dep):
2800 """Helper to add a dependency to flattened DEPS.
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002801
2802 Arguments:
2803 dep (Dependency): dependency to add
2804 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002805 assert dep.name not in self._deps or self._deps.get(
2806 dep.name) == dep, (dep.name, self._deps.get(dep.name))
2807 if dep.url:
2808 self._deps[dep.name] = dep
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002809
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002810 def _flatten_dep(self, dep):
2811 """Visits a dependency in order to flatten it (see CMDflatten).
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002812
2813 Arguments:
2814 dep (Dependency): dependency to process
2815 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002816 logging.debug('_flatten_dep(%s)', dep.name)
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002817
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002818 assert dep.deps_parsed, (
2819 "Attempted to flatten %s but it has not been processed." % dep.name)
Paweł Hajdan, Jrc69b32e2017-08-17 18:47:48 +02002820
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002821 self._allowed_hosts.update(dep.allowed_hosts)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002822
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002823 # Only include vars explicitly listed in the DEPS files or gclient
2824 # solution, not automatic, local overrides (i.e. not all of
2825 # dep.get_vars()).
2826 hierarchy = dep.hierarchy(include_url=False)
2827 for key, value in dep._vars.items():
2828 # Make sure there are no conflicting variables. It is fine however
2829 # to use same variable name, as long as the value is consistent.
2830 assert key not in self._vars or self._vars[key][1] == value, (
2831 "dep:%s key:%s value:%s != %s" %
2832 (dep.name, key, value, self._vars[key][1]))
2833 self._vars[key] = (hierarchy, value)
2834 # Override explicit custom variables.
2835 for key, value in dep.custom_vars.items():
2836 # Do custom_vars that don't correspond to DEPS vars ever make sense?
2837 # DEPS conditionals shouldn't be using vars that aren't also defined
2838 # in the DEPS (presubmit actually disallows this), so any new
2839 # custom_var must be unused in the DEPS, so no need to add it to the
2840 # flattened output either.
2841 if key not in self._vars:
2842 continue
2843 # Don't "override" existing vars if it's actually the same value.
2844 if self._vars[key][1] == value:
2845 continue
2846 # Anything else is overriding a default value from the DEPS.
2847 self._vars[key] = (hierarchy + ' [custom_var override]', value)
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02002848
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002849 self._pre_deps_hooks.extend([(dep, hook)
2850 for hook in dep.pre_deps_hooks])
2851 self._hooks.extend([(dep, hook) for hook in dep.deps_hooks])
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002852
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002853 for sub_dep in dep.dependencies:
2854 self._add_dep(sub_dep)
Paweł Hajdan, Jrb0ad16e2017-08-03 15:33:21 +02002855
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002856 for d in dep.dependencies:
2857 if d.should_recurse:
2858 self._flatten_dep(d)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002859
2860
Joanna Wang3ab2f212023-08-09 01:25:15 +00002861@metrics.collector.collect_metrics('gclient gitmodules')
2862def CMDgitmodules(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002863 """Adds or updates Git Submodules based on the contents of the DEPS file.
Joanna Wang3ab2f212023-08-09 01:25:15 +00002864
2865 This command should be run in the root director of the repo.
2866 It will create or update the .gitmodules file and include
2867 `gclient-condition` values. Commits in gitlinks will also be updated.
2868 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002869 parser.add_option('--output-gitmodules',
2870 help='name of the .gitmodules file to write to',
2871 default='.gitmodules')
2872 parser.add_option(
2873 '--deps-file',
2874 help=
2875 'name of the deps file to parse for git dependency paths and commits.',
2876 default='DEPS')
2877 parser.add_option(
2878 '--skip-dep',
2879 action="append",
2880 help='skip adding gitmodules for the git dependency at the given path',
2881 default=[])
2882 options, args = parser.parse_args(args)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002883
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002884 deps_dir = os.path.dirname(os.path.abspath(options.deps_file))
2885 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
2886 if not gclient_path:
2887 logging.error(
2888 '.gclient not found\n'
2889 'Make sure you are running this script from a gclient workspace.')
2890 sys.exit(1)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002891
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002892 deps_content = gclient_utils.FileRead(options.deps_file)
2893 ls = gclient_eval.Parse(deps_content, options.deps_file, None, None)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002894
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002895 prefix_length = 0
2896 if not 'use_relative_paths' in ls or ls['use_relative_paths'] != True:
2897 delta_path = os.path.relpath(deps_dir, os.path.abspath(gclient_path))
2898 if delta_path:
2899 prefix_length = len(delta_path.replace(os.path.sep, '/')) + 1
Joanna Wang3ab2f212023-08-09 01:25:15 +00002900
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002901 cache_info = []
Josip Sokcevica309d292023-11-07 19:51:39 +00002902
2903 # Git submodules shouldn't use .git suffix since it's not well supported.
2904 # However, we can't update .gitmodules files since there is no guarantee
2905 # that user has the latest version of depot_tools, and also they are not on
2906 # some old branch which contains already contains submodules with .git.
2907 # This check makes the transition easier.
2908 strip_git_suffix = True
2909 if os.path.exists(options.output_gitmodules):
2910 dot_git_pattern = re.compile('^(\s*)url(\s*)=.*\.git$')
2911 with open(options.output_gitmodules) as f:
2912 strip_git_suffix = not any(dot_git_pattern.match(l) for l in f)
2913
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002914 with open(options.output_gitmodules, 'w', newline='') as f:
2915 for path, dep in ls.get('deps').items():
2916 if path in options.skip_dep:
2917 continue
2918 if dep.get('dep_type') == 'cipd':
2919 continue
2920 try:
2921 url, commit = dep['url'].split('@', maxsplit=1)
2922 except ValueError:
2923 logging.error('error on %s; %s, not adding it', path,
2924 dep["url"])
2925 continue
2926 if prefix_length:
2927 path = path[prefix_length:]
Joanna Wang3ab2f212023-08-09 01:25:15 +00002928
Josip Sokcevica309d292023-11-07 19:51:39 +00002929 if strip_git_suffix:
2930 if url.endswith('.git'):
2931 url = url[:-4] # strip .git
2932 url = url.rstrip('/') # remove trailing slash for consistency
2933
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002934 cache_info += ['--cacheinfo', f'160000,{commit},{path}']
2935 f.write(f'[submodule "{path}"]\n\tpath = {path}\n\turl = {url}\n')
2936 if 'condition' in dep:
2937 f.write(f'\tgclient-condition = {dep["condition"]}\n')
2938 # Windows has limit how long, so let's chunk those calls.
2939 if len(cache_info) >= 100:
2940 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
2941 cache_info = []
2942
2943 if cache_info:
Josip Sokcevic293aa652023-08-23 18:55:20 +00002944 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002945 subprocess2.call(['git', 'add', '.gitmodules'])
Joanna Wang6aed4f52023-10-06 19:12:51 +00002946 print('.gitmodules and gitlinks updated. Please check `git diff --staged`'
2947 'and commit those staged changes (`git commit` without -a)')
Joanna Wang3ab2f212023-08-09 01:25:15 +00002948
2949
Edward Lemur3298e7b2018-07-17 18:21:27 +00002950@metrics.collector.collect_metrics('gclient flatten')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002951def CMDflatten(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002952 """Flattens the solutions into a single DEPS file."""
2953 parser.add_option('--output-deps', help='Path to the output DEPS file')
2954 parser.add_option(
2955 '--output-deps-files',
2956 help=('Path to the output metadata about DEPS files referenced by '
2957 'recursedeps.'))
2958 parser.add_option(
2959 '--pin-all-deps',
2960 action='store_true',
2961 help=('Pin all deps, even if not pinned in DEPS. CAVEAT: only does so '
2962 'for checked out deps, NOT deps_os.'))
2963 parser.add_option('--deps-graph-file',
2964 help='Provide a path for the output graph file')
2965 options, args = parser.parse_args(args)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002966
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002967 options.nohooks = True
2968 options.process_all_deps = True
2969 client = GClient.LoadCurrentConfig(options)
2970 if not client:
2971 raise gclient_utils.Error(
2972 'client not configured; see \'gclient config\'')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002973
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002974 # Only print progress if we're writing to a file. Otherwise, progress
2975 # updates could obscure intended output.
2976 code = client.RunOnDeps('flatten', args, progress=options.output_deps)
2977 if code != 0:
2978 return code
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002979
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002980 flattener = Flattener(client, pin_all_deps=options.pin_all_deps)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002981
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002982 if options.output_deps:
2983 with open(options.output_deps, 'w') as f:
2984 f.write(flattener.deps_string)
2985 else:
2986 print(flattener.deps_string)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002987
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002988 if options.deps_graph_file:
2989 with open(options.deps_graph_file, 'w') as f:
2990 f.write('\n'.join(flattener.deps_graph_lines))
Joanna Wang9144b672023-02-24 23:36:17 +00002991
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002992 deps_files = [{
2993 'url': d[0],
2994 'deps_file': d[1],
2995 'hierarchy': d[2]
2996 } for d in sorted(flattener.deps_files)]
2997 if options.output_deps_files:
2998 with open(options.output_deps_files, 'w') as f:
2999 json.dump(deps_files, f)
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02003000
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003001 return 0
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003002
3003
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02003004def _GNSettingsToLines(gn_args_file, gn_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003005 s = []
3006 if gn_args_file:
3007 s.extend([
3008 'gclient_gn_args_file = "%s"' % gn_args_file,
3009 'gclient_gn_args = %r' % gn_args,
3010 ])
3011 return s
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02003012
3013
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02003014def _AllowedHostsToLines(allowed_hosts):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003015 """Converts |allowed_hosts| set to list of lines for output."""
3016 if not allowed_hosts:
3017 return []
3018 s = ['allowed_hosts = [']
3019 for h in sorted(allowed_hosts):
3020 s.append(' "%s",' % h)
3021 s.extend([']', ''])
3022 return s
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02003023
3024
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003025def _DepsToLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003026 # type: (Mapping[str, Dependency]) -> Sequence[str]
3027 """Converts |deps| dict to list of lines for output."""
3028 if not deps:
3029 return []
3030 s = ['deps = {']
3031 for _, dep in sorted(deps.items()):
3032 s.extend(dep.ToLines())
3033 s.extend(['}', ''])
3034 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003035
3036
Joanna Wang9144b672023-02-24 23:36:17 +00003037def _DepsToDotGraphLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003038 # type: (Mapping[str, Dependency]) -> Sequence[str]
3039 """Converts |deps| dict to list of lines for dot graphs"""
3040 if not deps:
3041 return []
3042 graph_lines = ["digraph {\n\trankdir=\"LR\";"]
3043 for _, dep in sorted(deps.items()):
3044 line = dep.hierarchy(include_url=False, graphviz=True)
3045 if line:
3046 graph_lines.append("\t%s" % line)
3047 graph_lines.append("}")
3048 return graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00003049
3050
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02003051def _DepsOsToLines(deps_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003052 """Converts |deps_os| dict to list of lines for output."""
3053 if not deps_os:
3054 return []
3055 s = ['deps_os = {']
3056 for dep_os, os_deps in sorted(deps_os.items()):
3057 s.append(' "%s": {' % dep_os)
3058 for name, dep in sorted(os_deps.items()):
3059 condition_part = ([' "condition": %r,' %
3060 dep.condition] if dep.condition else [])
3061 s.extend([
3062 ' # %s' % dep.hierarchy(include_url=False),
3063 ' "%s": {' % (name, ),
3064 ' "url": "%s",' % (dep.url, ),
3065 ] + condition_part + [
3066 ' },',
3067 '',
3068 ])
3069 s.extend([' },', ''])
3070 s.extend(['}', ''])
3071 return s
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02003072
3073
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003074def _HooksToLines(name, hooks):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003075 """Converts |hooks| list to list of lines for output."""
3076 if not hooks:
3077 return []
3078 s = ['%s = [' % name]
3079 for dep, hook in hooks:
3080 s.extend([
3081 ' # %s' % dep.hierarchy(include_url=False),
3082 ' {',
3083 ])
3084 if hook.name is not None:
3085 s.append(' "name": "%s",' % hook.name)
3086 if hook.pattern is not None:
3087 s.append(' "pattern": "%s",' % hook.pattern)
3088 if hook.condition is not None:
3089 s.append(' "condition": %r,' % hook.condition)
3090 # Flattened hooks need to be written relative to the root gclient dir
3091 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3092 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3093 [' "%s",' % arg
3094 for arg in hook.action] + [' ]', ' },', ''])
3095 s.extend([']', ''])
3096 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003097
3098
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003099def _HooksOsToLines(hooks_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003100 """Converts |hooks| list to list of lines for output."""
3101 if not hooks_os:
3102 return []
3103 s = ['hooks_os = {']
3104 for hook_os, os_hooks in hooks_os.items():
3105 s.append(' "%s": [' % hook_os)
3106 for dep, hook in os_hooks:
3107 s.extend([
3108 ' # %s' % dep.hierarchy(include_url=False),
3109 ' {',
3110 ])
3111 if hook.name is not None:
3112 s.append(' "name": "%s",' % hook.name)
3113 if hook.pattern is not None:
3114 s.append(' "pattern": "%s",' % hook.pattern)
3115 if hook.condition is not None:
3116 s.append(' "condition": %r,' % hook.condition)
3117 # Flattened hooks need to be written relative to the root gclient
3118 # dir
3119 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3120 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3121 [' "%s",' % arg
3122 for arg in hook.action] + [' ]', ' },', ''])
3123 s.extend([' ],', ''])
3124 s.extend(['}', ''])
3125 return s
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003126
3127
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003128def _VarsToLines(variables):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003129 """Converts |variables| dict to list of lines for output."""
3130 if not variables:
3131 return []
3132 s = ['vars = {']
3133 for key, tup in sorted(variables.items()):
3134 hierarchy, value = tup
3135 s.extend([
3136 ' # %s' % hierarchy,
3137 ' "%s": %r,' % (key, value),
3138 '',
3139 ])
3140 s.extend(['}', ''])
3141 return s
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003142
3143
Edward Lemur3298e7b2018-07-17 18:21:27 +00003144@metrics.collector.collect_metrics('gclient grep')
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003145def CMDgrep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003146 """Greps through git repos managed by gclient.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003147
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003148 Runs 'git grep [args...]' for each module.
3149 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003150 # We can't use optparse because it will try to parse arguments sent
3151 # to git grep and throw an error. :-(
3152 if not args or re.match('(-h|--help)$', args[0]):
3153 print(
3154 'Usage: gclient grep [-j <N>] git-grep-args...\n\n'
3155 'Example: "gclient grep -j10 -A2 RefCountedBase" runs\n"git grep '
3156 '-A2 RefCountedBase" on each of gclient\'s git\nrepos with up to '
3157 '10 jobs.\n\nBonus: page output by appending "|& less -FRSX" to the'
3158 ' end of your query.',
3159 file=sys.stderr)
3160 return 1
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003161
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003162 jobs_arg = ['--jobs=1']
3163 if re.match(r'(-j|--jobs=)\d+$', args[0]):
3164 jobs_arg, args = args[:1], args[1:]
3165 elif re.match(r'(-j|--jobs)$', args[0]):
3166 jobs_arg, args = args[:2], args[2:]
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003167
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003168 return CMDrecurse(
3169 parser, jobs_arg + [
3170 '--ignore', '--prepend-dir', '--no-progress', '--scm=git', 'git',
3171 'grep', '--null', '--color=Always'
3172 ] + args)
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00003173
3174
Edward Lemur3298e7b2018-07-17 18:21:27 +00003175@metrics.collector.collect_metrics('gclient root')
stip@chromium.orga735da22015-04-29 23:18:20 +00003176def CMDroot(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003177 """Outputs the solution root (or current dir if there isn't one)."""
3178 (options, args) = parser.parse_args(args)
3179 client = GClient.LoadCurrentConfig(options)
3180 if client:
3181 print(os.path.abspath(client.root_dir))
3182 else:
3183 print(os.path.abspath('.'))
stip@chromium.orga735da22015-04-29 23:18:20 +00003184
3185
agablea98a6cd2016-11-15 14:30:10 -08003186@subcommand.usage('[url]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00003187@metrics.collector.collect_metrics('gclient config')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003188def CMDconfig(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003189 """Creates a .gclient file in the current directory.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003190
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003191 This specifies the configuration for further commands. After update/sync,
3192 top-level DEPS files in each module are read to determine dependent
3193 modules to operate on as well. If optional [url] parameter is
3194 provided, then configuration is read from a specified Subversion server
3195 URL.
3196 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003197 # We do a little dance with the --gclientfile option. 'gclient config' is
3198 # the only command where it's acceptable to have both '--gclientfile' and
3199 # '--spec' arguments. So, we temporarily stash any --gclientfile parameter
3200 # into options.output_config_file until after the (gclientfile xor spec)
3201 # error check.
3202 parser.remove_option('--gclientfile')
3203 parser.add_option('--gclientfile',
3204 dest='output_config_file',
3205 help='Specify an alternate .gclient file')
3206 parser.add_option('--name',
3207 help='overrides the default name for the solution')
3208 parser.add_option(
3209 '--deps-file',
3210 default='DEPS',
3211 help='overrides the default name for the DEPS file for the '
3212 'main solutions and all sub-dependencies')
3213 parser.add_option('--unmanaged',
3214 action='store_true',
3215 default=False,
3216 help='overrides the default behavior to make it possible '
3217 'to have the main solution untouched by gclient '
3218 '(gclient will check out unmanaged dependencies but '
3219 'will never sync them)')
3220 parser.add_option('--cache-dir',
3221 default=UNSET_CACHE_DIR,
3222 help='Cache all git repos into this dir and do shared '
3223 'clones from the cache, instead of cloning directly '
3224 'from the remote. Pass "None" to disable cache, even '
3225 'if globally enabled due to $GIT_CACHE_PATH.')
3226 parser.add_option('--custom-var',
3227 action='append',
3228 dest='custom_vars',
3229 default=[],
3230 help='overrides variables; key=value syntax')
3231 parser.set_defaults(config_filename=None)
3232 (options, args) = parser.parse_args(args)
3233 if options.output_config_file:
3234 setattr(options, 'config_filename',
3235 getattr(options, 'output_config_file'))
3236 if ((options.spec and args) or len(args) > 2
3237 or (not options.spec and not args)):
3238 parser.error(
3239 'Inconsistent arguments. Use either --spec or one or 2 args')
maruel@chromium.org5fc2a332010-05-26 19:37:15 +00003240
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003241 if (options.cache_dir is not UNSET_CACHE_DIR
3242 and options.cache_dir.lower() == 'none'):
3243 options.cache_dir = None
Robert Iannuccia19649b2018-06-29 16:31:45 +00003244
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003245 custom_vars = {}
3246 for arg in options.custom_vars:
3247 kv = arg.split('=', 1)
3248 if len(kv) != 2:
3249 parser.error('Invalid --custom-var argument: %r' % arg)
3250 custom_vars[kv[0]] = gclient_eval.EvaluateCondition(kv[1], {})
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02003251
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003252 client = GClient('.', options)
3253 if options.spec:
3254 client.SetConfig(options.spec)
iposva@chromium.org8cf7a392010-04-07 17:20:26 +00003255 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003256 base_url = args[0].rstrip('/')
3257 if not options.name:
3258 name = base_url.split('/')[-1]
3259 if name.endswith('.git'):
3260 name = name[:-4]
3261 else:
3262 # specify an alternate relpath for the given URL.
3263 name = options.name
3264 if not os.path.abspath(os.path.join(os.getcwd(), name)).startswith(
3265 os.getcwd()):
3266 parser.error('Do not pass a relative path for --name.')
3267 if any(x in ('..', '.', '/', '\\') for x in name.split(os.sep)):
3268 parser.error(
3269 'Do not include relative path components in --name.')
agable@chromium.orgf2214672015-10-27 21:02:48 +00003270
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003271 deps_file = options.deps_file
3272 client.SetDefaultConfig(name,
3273 deps_file,
3274 base_url,
3275 managed=not options.unmanaged,
3276 cache_dir=options.cache_dir,
3277 custom_vars=custom_vars)
3278 client.SaveConfig()
3279 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003280
3281
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003282@subcommand.epilog("""Example:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003283 gclient pack > patch.txt
3284 generate simple patch for configured client and dependences
3285""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003286@metrics.collector.collect_metrics('gclient pack')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003287def CMDpack(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003288 """Generates a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003289
agabled437d762016-10-17 09:35:11 -07003290 Internally, runs 'git diff' on each checked out module and
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003291 dependencies, and performs minimal postprocessing of the output. The
3292 resulting patch is printed to stdout and can be applied to a freshly
3293 checked out tree via 'patch -p0 < patchfile'.
3294 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003295 parser.add_option('--deps',
3296 dest='deps_os',
3297 metavar='OS_LIST',
3298 help='override deps for the specified (comma-separated) '
3299 'platform(s); \'all\' will process all deps_os '
3300 'references')
3301 parser.remove_option('--jobs')
3302 (options, args) = parser.parse_args(args)
3303 # Force jobs to 1 so the stdout is not annotated with the thread ids
3304 options.jobs = 1
3305 client = GClient.LoadCurrentConfig(options)
3306 if not client:
3307 raise gclient_utils.Error(
3308 'client not configured; see \'gclient config\'')
3309 if options.verbose:
3310 client.PrintLocationAndContents()
3311 return client.RunOnDeps('pack', args)
kbr@google.comab318592009-09-04 00:54:55 +00003312
3313
Edward Lemur3298e7b2018-07-17 18:21:27 +00003314@metrics.collector.collect_metrics('gclient status')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003315def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003316 """Shows modification status for every dependencies."""
3317 parser.add_option('--deps',
3318 dest='deps_os',
3319 metavar='OS_LIST',
3320 help='override deps for the specified (comma-separated) '
3321 'platform(s); \'all\' will process all deps_os '
3322 'references')
3323 (options, args) = parser.parse_args(args)
3324 client = GClient.LoadCurrentConfig(options)
3325 if not client:
3326 raise gclient_utils.Error(
3327 'client not configured; see \'gclient config\'')
3328 if options.verbose:
3329 client.PrintLocationAndContents()
3330 return client.RunOnDeps('status', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003331
3332
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003333@subcommand.epilog("""Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +00003334 gclient sync
3335 update files from SCM according to current configuration,
3336 *for modules which have changed since last update or sync*
3337 gclient sync --force
3338 update files from SCM according to current configuration, for
3339 all modules (useful for recovering files deleted from local copy)
Edward Lesmes3ffca4b2021-05-19 19:36:17 +00003340 gclient sync --revision src@GIT_COMMIT_OR_REF
3341 update src directory to GIT_COMMIT_OR_REF
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003342
3343JSON output format:
3344If the --output-json option is specified, the following document structure will
3345be emitted to the provided file. 'null' entries may occur for subprojects which
3346are present in the gclient solution, but were not processed (due to custom_deps,
3347os_deps, etc.)
3348
3349{
3350 "solutions" : {
3351 "<name>": { # <name> is the posix-normalized path to the solution.
agabled437d762016-10-17 09:35:11 -07003352 "revision": [<git id hex string>|null],
3353 "scm": ["git"|null],
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003354 }
3355 }
3356}
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003357""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003358@metrics.collector.collect_metrics('gclient sync')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003359def CMDsync(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003360 """Checkout/update all modules."""
3361 parser.add_option('-f',
3362 '--force',
3363 action='store_true',
3364 help='force update even for unchanged modules')
3365 parser.add_option('-n',
3366 '--nohooks',
3367 action='store_true',
3368 help='don\'t run hooks after the update is complete')
3369 parser.add_option('-p',
3370 '--noprehooks',
3371 action='store_true',
3372 help='don\'t run pre-DEPS hooks',
3373 default=False)
3374 parser.add_option('-r',
3375 '--revision',
3376 action='append',
3377 dest='revisions',
3378 metavar='REV',
3379 default=[],
3380 help='Enforces git ref/hash for the solutions with the '
3381 'format src@rev. The src@ part is optional and can be '
3382 'skipped. You can also specify URLs instead of paths '
3383 'and gclient will find the solution corresponding to '
3384 'the given URL. If a path is also specified, the URL '
3385 'takes precedence. -r can be used multiple times when '
3386 '.gclient has multiple solutions configured, and will '
3387 'work even if the src@ part is skipped. Revision '
3388 'numbers (e.g. 31000 or r31000) are not supported.')
3389 parser.add_option('--patch-ref',
3390 action='append',
3391 dest='patch_refs',
3392 metavar='GERRIT_REF',
3393 default=[],
3394 help='Patches the given reference with the format '
3395 'dep@target-ref:patch-ref. '
3396 'For |dep|, you can specify URLs as well as paths, '
3397 'with URLs taking preference. '
3398 '|patch-ref| will be applied to |dep|, rebased on top '
3399 'of what |dep| was synced to, and a soft reset will '
3400 'be done. Use --no-rebase-patch-ref and '
3401 '--no-reset-patch-ref to disable this behavior. '
3402 '|target-ref| is the target branch against which a '
3403 'patch was created, it is used to determine which '
3404 'commits from the |patch-ref| actually constitute a '
3405 'patch.')
3406 parser.add_option(
3407 '-t',
3408 '--download-topics',
3409 action='store_true',
3410 help='Downloads and patches locally changes from all open '
3411 'Gerrit CLs that have the same topic as the changes '
3412 'in the specified patch_refs. Only works if atleast '
3413 'one --patch-ref is specified.')
3414 parser.add_option('--with_branch_heads',
3415 action='store_true',
3416 help='Clone git "branch_heads" refspecs in addition to '
3417 'the default refspecs. This adds about 1/2GB to a '
3418 'full checkout. (git only)')
3419 parser.add_option(
3420 '--with_tags',
3421 action='store_true',
3422 help='Clone git tags in addition to the default refspecs.')
3423 parser.add_option('-H',
3424 '--head',
3425 action='store_true',
3426 help='DEPRECATED: only made sense with safesync urls.')
3427 parser.add_option(
3428 '-D',
3429 '--delete_unversioned_trees',
3430 action='store_true',
3431 help='Deletes from the working copy any dependencies that '
3432 'have been removed since the last sync, as long as '
3433 'there are no local modifications. When used with '
3434 '--force, such dependencies are removed even if they '
3435 'have local modifications. When used with --reset, '
3436 'all untracked directories are removed from the '
3437 'working copy, excluding those which are explicitly '
3438 'ignored in the repository.')
3439 parser.add_option(
3440 '-R',
3441 '--reset',
3442 action='store_true',
3443 help='resets any local changes before updating (git only)')
3444 parser.add_option('-M',
3445 '--merge',
3446 action='store_true',
3447 help='merge upstream changes instead of trying to '
3448 'fast-forward or rebase')
3449 parser.add_option('-A',
3450 '--auto_rebase',
3451 action='store_true',
3452 help='Automatically rebase repositories against local '
3453 'checkout during update (git only).')
3454 parser.add_option('--deps',
3455 dest='deps_os',
3456 metavar='OS_LIST',
3457 help='override deps for the specified (comma-separated) '
3458 'platform(s); \'all\' will process all deps_os '
3459 'references')
3460 parser.add_option('--process-all-deps',
3461 action='store_true',
3462 help='Check out all deps, even for different OS-es, '
3463 'or with conditions evaluating to false')
3464 parser.add_option('--upstream',
3465 action='store_true',
3466 help='Make repo state match upstream branch.')
3467 parser.add_option('--output-json',
3468 help='Output a json document to this path containing '
3469 'summary information about the sync.')
3470 parser.add_option(
3471 '--no-history',
3472 action='store_true',
3473 help='GIT ONLY - Reduces the size/time of the checkout at '
3474 'the cost of no history. Requires Git 1.9+')
3475 parser.add_option('--shallow',
3476 action='store_true',
3477 help='GIT ONLY - Do a shallow clone into the cache dir. '
3478 'Requires Git 1.9+')
3479 parser.add_option('--no_bootstrap',
3480 '--no-bootstrap',
3481 action='store_true',
3482 help='Don\'t bootstrap from Google Storage.')
3483 parser.add_option('--ignore_locks',
3484 action='store_true',
3485 help='No longer used.')
3486 parser.add_option('--break_repo_locks',
3487 action='store_true',
3488 help='No longer used.')
3489 parser.add_option('--lock_timeout',
3490 type='int',
3491 default=5000,
3492 help='GIT ONLY - Deadline (in seconds) to wait for git '
3493 'cache lock to become available. Default is %default.')
3494 parser.add_option('--no-rebase-patch-ref',
3495 action='store_false',
3496 dest='rebase_patch_ref',
3497 default=True,
3498 help='Bypass rebase of the patch ref after checkout.')
3499 parser.add_option('--no-reset-patch-ref',
3500 action='store_false',
3501 dest='reset_patch_ref',
3502 default=True,
3503 help='Bypass calling reset after patching the ref.')
3504 parser.add_option('--experiment',
3505 action='append',
3506 dest='experiments',
3507 default=[],
3508 help='Which experiments should be enabled.')
3509 (options, args) = parser.parse_args(args)
3510 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003511
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003512 if not client:
3513 raise gclient_utils.Error(
3514 'client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003515
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003516 if options.download_topics and not options.rebase_patch_ref:
3517 raise gclient_utils.Error(
3518 'Warning: You cannot download topics and not rebase each patch ref')
Ravi Mistryecda7822022-02-28 16:22:20 +00003519
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003520 if options.ignore_locks:
3521 print(
3522 'Warning: ignore_locks is no longer used. Please remove its usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003523
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003524 if options.break_repo_locks:
3525 print('Warning: break_repo_locks is no longer used. Please remove its '
3526 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003527
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003528 if options.revisions and options.head:
3529 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
3530 print('Warning: you cannot use both --head and --revision')
smutae7ea312016-07-18 11:59:41 -07003531
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003532 if options.verbose:
3533 client.PrintLocationAndContents()
3534 ret = client.RunOnDeps('update', args)
3535 if options.output_json:
3536 slns = {}
3537 for d in client.subtree(True):
3538 normed = d.name.replace('\\', '/').rstrip('/') + '/'
3539 slns[normed] = {
3540 'revision': d.got_revision,
3541 'scm': d.used_scm.name if d.used_scm else None,
3542 'url': str(d.url) if d.url else None,
3543 'was_processed': d.should_process,
3544 'was_synced': d._should_sync,
3545 }
3546 with open(options.output_json, 'w') as f:
3547 json.dump({'solutions': slns}, f)
3548 return ret
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003549
3550
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003551CMDupdate = CMDsync
3552
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003553
Edward Lemur3298e7b2018-07-17 18:21:27 +00003554@metrics.collector.collect_metrics('gclient validate')
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003555def CMDvalidate(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003556 """Validates the .gclient and DEPS syntax."""
3557 options, args = parser.parse_args(args)
3558 client = GClient.LoadCurrentConfig(options)
3559 if not client:
3560 raise gclient_utils.Error(
3561 'client not configured; see \'gclient config\'')
3562 rv = client.RunOnDeps('validate', args)
3563 if rv == 0:
3564 print('validate: SUCCESS')
3565 else:
3566 print('validate: FAILURE')
3567 return rv
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003568
3569
Edward Lemur3298e7b2018-07-17 18:21:27 +00003570@metrics.collector.collect_metrics('gclient diff')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003571def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003572 """Displays local diff for every dependencies."""
3573 parser.add_option('--deps',
3574 dest='deps_os',
3575 metavar='OS_LIST',
3576 help='override deps for the specified (comma-separated) '
3577 'platform(s); \'all\' will process all deps_os '
3578 'references')
3579 (options, args) = parser.parse_args(args)
3580 client = GClient.LoadCurrentConfig(options)
3581 if not client:
3582 raise gclient_utils.Error(
3583 'client not configured; see \'gclient config\'')
3584 if options.verbose:
3585 client.PrintLocationAndContents()
3586 return client.RunOnDeps('diff', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003587
3588
Edward Lemur3298e7b2018-07-17 18:21:27 +00003589@metrics.collector.collect_metrics('gclient revert')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003590def CMDrevert(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003591 """Reverts all modifications in every dependencies.
maruel@chromium.org28d14bd2010-11-11 20:37:09 +00003592
3593 That's the nuclear option to get back to a 'clean' state. It removes anything
agabled437d762016-10-17 09:35:11 -07003594 that shows up in git status."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003595 parser.add_option('--deps',
3596 dest='deps_os',
3597 metavar='OS_LIST',
3598 help='override deps for the specified (comma-separated) '
3599 'platform(s); \'all\' will process all deps_os '
3600 'references')
3601 parser.add_option('-n',
3602 '--nohooks',
3603 action='store_true',
3604 help='don\'t run hooks after the revert is complete')
3605 parser.add_option('-p',
3606 '--noprehooks',
3607 action='store_true',
3608 help='don\'t run pre-DEPS hooks',
3609 default=False)
3610 parser.add_option('--upstream',
3611 action='store_true',
3612 help='Make repo state match upstream branch.')
3613 parser.add_option('--break_repo_locks',
3614 action='store_true',
3615 help='No longer used.')
3616 (options, args) = parser.parse_args(args)
3617 if options.break_repo_locks:
3618 print(
3619 'Warning: break_repo_locks is no longer used. Please remove its ' +
3620 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003621
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003622 # --force is implied.
3623 options.force = True
3624 options.reset = False
3625 options.delete_unversioned_trees = False
3626 options.merge = False
3627 client = GClient.LoadCurrentConfig(options)
3628 if not client:
3629 raise gclient_utils.Error(
3630 'client not configured; see \'gclient config\'')
3631 return client.RunOnDeps('revert', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003632
3633
Edward Lemur3298e7b2018-07-17 18:21:27 +00003634@metrics.collector.collect_metrics('gclient runhooks')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003635def CMDrunhooks(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003636 """Runs hooks for files that have been modified in the local working copy."""
3637 parser.add_option('--deps',
3638 dest='deps_os',
3639 metavar='OS_LIST',
3640 help='override deps for the specified (comma-separated) '
3641 'platform(s); \'all\' will process all deps_os '
3642 'references')
3643 parser.add_option('-f',
3644 '--force',
3645 action='store_true',
3646 default=True,
3647 help='Deprecated. No effect.')
3648 (options, args) = parser.parse_args(args)
3649 client = GClient.LoadCurrentConfig(options)
3650 if not client:
3651 raise gclient_utils.Error(
3652 'client not configured; see \'gclient config\'')
3653 if options.verbose:
3654 client.PrintLocationAndContents()
3655 options.force = True
3656 options.nohooks = False
3657 return client.RunOnDeps('runhooks', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003658
3659
Gavin Mak50b27a52023-09-19 22:44:59 +00003660# TODO(crbug.com/1481266): Collect merics for installhooks.
3661def CMDinstallhooks(parser, args):
3662 """Installs gclient git hooks.
3663
3664 Currently only installs a pre-commit hook to drop staged gitlinks. To
3665 bypass this pre-commit hook once it's installed, set the environment
3666 variable SKIP_GITLINK_PRECOMMIT=1.
3667 """
3668 (options, args) = parser.parse_args(args)
3669 client = GClient.LoadCurrentConfig(options)
3670 if not client:
3671 raise gclient_utils.Error(
3672 'client not configured; see \'gclient config\'')
3673 client._InstallPreCommitHook()
3674 return 0
3675
3676
Edward Lemur3298e7b2018-07-17 18:21:27 +00003677@metrics.collector.collect_metrics('gclient revinfo')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003678def CMDrevinfo(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003679 """Outputs revision info mapping for the client and its dependencies.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003680
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00003681 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003682 can be used to reproduce the same tree in the future. It is only useful for
agabled437d762016-10-17 09:35:11 -07003683 'unpinned dependencies', i.e. DEPS/deps references without a git hash.
3684 A git branch name isn't 'pinned' since the actual commit can change.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003685 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003686 parser.add_option('--deps',
3687 dest='deps_os',
3688 metavar='OS_LIST',
3689 help='override deps for the specified (comma-separated) '
3690 'platform(s); \'all\' will process all deps_os '
3691 'references')
3692 parser.add_option(
3693 '-a',
3694 '--actual',
3695 action='store_true',
3696 help='gets the actual checked out revisions instead of the '
3697 'ones specified in the DEPS and .gclient files')
3698 parser.add_option('-s',
3699 '--snapshot',
3700 action='store_true',
3701 help='creates a snapshot .gclient file of the current '
3702 'version of all repositories to reproduce the tree, '
3703 'implies -a')
3704 parser.add_option(
3705 '--filter',
3706 action='append',
3707 dest='filter',
3708 help='Display revision information only for the specified '
3709 'dependencies (filtered by URL or path).')
3710 parser.add_option('--output-json',
3711 help='Output a json document to this path containing '
3712 'information about the revisions.')
3713 parser.add_option(
3714 '--ignore-dep-type',
3715 choices=['git', 'cipd'],
3716 help='Specify to skip processing of a certain type of dep.')
3717 (options, args) = parser.parse_args(args)
3718 client = GClient.LoadCurrentConfig(options)
3719 if not client:
3720 raise gclient_utils.Error(
3721 'client not configured; see \'gclient config\'')
3722 client.PrintRevInfo()
3723 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003724
3725
Edward Lemur3298e7b2018-07-17 18:21:27 +00003726@metrics.collector.collect_metrics('gclient getdep')
Edward Lesmes411041f2018-04-05 20:12:55 -04003727def CMDgetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003728 """Gets revision information and variable values from a DEPS file.
Josip Sokcevic7b5e3d72023-06-13 00:28:23 +00003729
3730 If key doesn't exist or is incorrectly declared, this script exits with exit
3731 code 2."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003732 parser.add_option('--var',
3733 action='append',
3734 dest='vars',
3735 metavar='VAR',
3736 default=[],
3737 help='Gets the value of a given variable.')
3738 parser.add_option(
3739 '-r',
3740 '--revision',
3741 action='append',
3742 dest='getdep_revisions',
3743 metavar='DEP',
3744 default=[],
3745 help='Gets the revision/version for the given dependency. '
3746 'If it is a git dependency, dep must be a path. If it '
3747 'is a CIPD dependency, dep must be of the form '
3748 'path:package.')
3749 parser.add_option(
3750 '--deps-file',
3751 default='DEPS',
3752 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3753 # .gclient first.
3754 help='The DEPS file to be edited. Defaults to the DEPS '
3755 'file in the current directory.')
3756 (options, args) = parser.parse_args(args)
Edward Lesmes411041f2018-04-05 20:12:55 -04003757
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003758 if not os.path.isfile(options.deps_file):
3759 raise gclient_utils.Error('DEPS file %s does not exist.' %
3760 options.deps_file)
3761 with open(options.deps_file) as f:
3762 contents = f.read()
3763 client = GClient.LoadCurrentConfig(options)
3764 if client is not None:
3765 builtin_vars = client.get_builtin_vars()
Edward Lesmes411041f2018-04-05 20:12:55 -04003766 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003767 logging.warning(
3768 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3769 'file without support for built-in variables.')
3770 builtin_vars = None
3771 local_scope = gclient_eval.Exec(contents,
3772 options.deps_file,
3773 builtin_vars=builtin_vars)
3774
3775 for var in options.vars:
3776 print(gclient_eval.GetVar(local_scope, var))
3777
3778 commits = {}
3779 if local_scope.get(
3780 'git_dependencies'
3781 ) == gclient_eval.SUBMODULES and options.getdep_revisions:
3782 commits.update(
3783 scm_git.GIT.GetSubmoduleCommits(
3784 os.getcwd(),
3785 [path for path in options.getdep_revisions if ':' not in path]))
3786
3787 for name in options.getdep_revisions:
3788 if ':' in name:
3789 name, _, package = name.partition(':')
3790 if not name or not package:
3791 parser.error(
3792 'Wrong CIPD format: %s:%s should be of the form path:pkg.' %
3793 (name, package))
3794 print(gclient_eval.GetCIPD(local_scope, name, package))
3795 elif commits:
3796 print(commits[name])
3797 else:
3798 try:
3799 print(gclient_eval.GetRevision(local_scope, name))
3800 except KeyError as e:
3801 print(repr(e), file=sys.stderr)
3802 sys.exit(2)
Edward Lesmes411041f2018-04-05 20:12:55 -04003803
3804
Edward Lemur3298e7b2018-07-17 18:21:27 +00003805@metrics.collector.collect_metrics('gclient setdep')
Edward Lesmes6f64a052018-03-20 17:35:49 -04003806def CMDsetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003807 """Modifies dependency revisions and variable values in a DEPS file"""
3808 parser.add_option('--var',
3809 action='append',
3810 dest='vars',
3811 metavar='VAR=VAL',
3812 default=[],
3813 help='Sets a variable to the given value with the format '
3814 'name=value.')
3815 parser.add_option('-r',
3816 '--revision',
3817 action='append',
3818 dest='setdep_revisions',
3819 metavar='DEP@REV',
3820 default=[],
3821 help='Sets the revision/version for the dependency with '
3822 'the format dep@rev. If it is a git dependency, dep '
3823 'must be a path and rev must be a git hash or '
3824 'reference (e.g. src/dep@deadbeef). If it is a CIPD '
3825 'dependency, dep must be of the form path:package and '
3826 'rev must be the package version '
3827 '(e.g. src/pkg:chromium/pkg@2.1-cr0).')
3828 parser.add_option(
3829 '--deps-file',
3830 default='DEPS',
3831 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3832 # .gclient first.
3833 help='The DEPS file to be edited. Defaults to the DEPS '
3834 'file in the current directory.')
3835 (options, args) = parser.parse_args(args)
3836 if args:
3837 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3838 if not options.setdep_revisions and not options.vars:
Edward Lesmes0ecf6d62018-04-05 18:28:55 -04003839 parser.error(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003840 'You must specify at least one variable or revision to modify.')
3841
3842 if not os.path.isfile(options.deps_file):
3843 raise gclient_utils.Error('DEPS file %s does not exist.' %
3844 options.deps_file)
3845 with open(options.deps_file) as f:
3846 contents = f.read()
3847
3848 client = GClient.LoadCurrentConfig(options)
3849 if client is not None:
3850 builtin_vars = client.get_builtin_vars()
Edward Lesmes6f64a052018-03-20 17:35:49 -04003851 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003852 logging.warning(
3853 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3854 'file without support for built-in variables.')
3855 builtin_vars = None
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003856
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003857 local_scope = gclient_eval.Exec(contents,
3858 options.deps_file,
3859 builtin_vars=builtin_vars)
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003860
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003861 # Create a set of all git submodules.
3862 cwd = os.path.dirname(options.deps_file) or os.getcwd()
3863 git_modules = None
3864 if 'git_dependencies' in local_scope and local_scope[
3865 'git_dependencies'] in (gclient_eval.SUBMODULES, gclient_eval.SYNC):
3866 try:
3867 submodule_status = subprocess2.check_output(
3868 ['git', 'submodule', 'status'], cwd=cwd).decode('utf-8')
3869 git_modules = {l.split()[1] for l in submodule_status.splitlines()}
3870 except subprocess2.CalledProcessError as e:
3871 print('Warning: gitlinks won\'t be updated: ', e)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003872
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003873 for var in options.vars:
3874 name, _, value = var.partition('=')
3875 if not name or not value:
3876 parser.error(
3877 'Wrong var format: %s should be of the form name=value.' % var)
3878 if name in local_scope['vars']:
3879 gclient_eval.SetVar(local_scope, name, value)
3880 else:
3881 gclient_eval.AddVar(local_scope, name, value)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003882
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003883 for revision in options.setdep_revisions:
3884 name, _, value = revision.partition('@')
3885 if not name or not value:
3886 parser.error('Wrong dep format: %s should be of the form dep@rev.' %
3887 revision)
3888 if ':' in name:
3889 name, _, package = name.partition(':')
3890 if not name or not package:
3891 parser.error(
3892 'Wrong CIPD format: %s:%s should be of the form path:pkg@version.'
3893 % (name, package))
3894 gclient_eval.SetCIPD(local_scope, name, package, value)
3895 else:
3896 # Update DEPS only when `git_dependencies` == DEPS or SYNC.
3897 # git_dependencies is defaulted to DEPS when not set.
3898 if 'git_dependencies' not in local_scope or local_scope[
3899 'git_dependencies'] in (gclient_eval.DEPS,
3900 gclient_eval.SYNC):
3901 gclient_eval.SetRevision(local_scope, name, value)
3902
3903 # Update git submodules when `git_dependencies` == SYNC or
3904 # SUBMODULES.
3905 if git_modules and 'git_dependencies' in local_scope and local_scope[
3906 'git_dependencies'] in (gclient_eval.SUBMODULES,
3907 gclient_eval.SYNC):
3908 git_module_name = name
3909 if not 'use_relative_paths' in local_scope or \
3910 local_scope['use_relative_paths'] != True:
3911 deps_dir = os.path.dirname(
3912 os.path.abspath(options.deps_file))
3913 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
3914 delta_path = None
3915 if gclient_path:
3916 delta_path = os.path.relpath(
3917 deps_dir, os.path.abspath(gclient_path))
3918 if delta_path:
3919 prefix_length = len(delta_path.replace(
3920 os.path.sep, '/')) + 1
3921 git_module_name = name[prefix_length:]
3922 # gclient setdep should update the revision, i.e., the gitlink
3923 # only when the submodule entry is already present within
3924 # .gitmodules.
3925 if git_module_name not in git_modules:
3926 raise KeyError(
3927 f'Could not find any dependency called "{git_module_name}" in '
3928 f'.gitmodules.')
3929
3930 # Update the gitlink for the submodule.
3931 subprocess2.call([
3932 'git', 'update-index', '--add', '--cacheinfo',
3933 f'160000,{value},{git_module_name}'
3934 ],
3935 cwd=cwd)
3936
3937 with open(options.deps_file, 'wb') as f:
3938 f.write(gclient_eval.RenderDEPSFile(local_scope).encode('utf-8'))
3939
3940 if git_modules:
3941 subprocess2.call(['git', 'add', options.deps_file], cwd=cwd)
3942 print('Changes have been staged. See changes with `git status`.\n'
3943 'Use `git commit -m "Manual roll"` to commit your changes. \n'
3944 'Run gclient sync to update your local dependency checkout.')
Josip Sokcevic5561f8b2023-08-21 16:00:42 +00003945
Edward Lesmes6f64a052018-03-20 17:35:49 -04003946
Edward Lemur3298e7b2018-07-17 18:21:27 +00003947@metrics.collector.collect_metrics('gclient verify')
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003948def CMDverify(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003949 """Verifies the DEPS file deps are only from allowed_hosts."""
3950 (options, args) = parser.parse_args(args)
3951 client = GClient.LoadCurrentConfig(options)
3952 if not client:
3953 raise gclient_utils.Error(
3954 'client not configured; see \'gclient config\'')
3955 client.RunOnDeps(None, [])
3956 # Look at each first-level dependency of this gclient only.
3957 for dep in client.dependencies:
3958 bad_deps = dep.findDepsFromNotAllowedHosts()
3959 if not bad_deps:
3960 continue
3961 print("There are deps from not allowed hosts in file %s" %
3962 dep.deps_file)
3963 for bad_dep in bad_deps:
3964 print("\t%s at %s" % (bad_dep.name, bad_dep.url))
3965 print("allowed_hosts:", ', '.join(dep.allowed_hosts))
3966 sys.stdout.flush()
3967 raise gclient_utils.Error(
3968 'dependencies from disallowed hosts; check your DEPS file.')
3969 return 0
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003970
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003971
3972@subcommand.epilog("""For more information on what metrics are we collecting and
Edward Lemur8a2e3312018-07-12 21:15:09 +00003973why, please read metrics.README.md or visit https://bit.ly/2ufRS4p""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003974@metrics.collector.collect_metrics('gclient metrics')
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003975def CMDmetrics(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003976 """Reports, and optionally modifies, the status of metric collection."""
3977 parser.add_option('--opt-in',
3978 action='store_true',
3979 dest='enable_metrics',
3980 help='Opt-in to metrics collection.',
3981 default=None)
3982 parser.add_option('--opt-out',
3983 action='store_false',
3984 dest='enable_metrics',
3985 help='Opt-out of metrics collection.')
3986 options, args = parser.parse_args(args)
3987 if args:
3988 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3989 if not metrics.collector.config.is_googler:
3990 print("You're not a Googler. Metrics collection is disabled for you.")
3991 return 0
3992
3993 if options.enable_metrics is not None:
3994 metrics.collector.config.opted_in = options.enable_metrics
3995
3996 if metrics.collector.config.opted_in is None:
3997 print("You haven't opted in or out of metrics collection.")
3998 elif metrics.collector.config.opted_in:
3999 print("You have opted in. Thanks!")
4000 else:
4001 print("You have opted out. Please consider opting in.")
Edward Lemur32e3d1e2018-07-12 00:54:05 +00004002 return 0
4003
Edward Lemur32e3d1e2018-07-12 00:54:05 +00004004
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004005class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004006 gclientfile_default = os.environ.get('GCLIENT_FILE', '.gclient')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004007
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004008 def __init__(self, **kwargs):
4009 optparse.OptionParser.__init__(self,
4010 version='%prog ' + __version__,
4011 **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004012
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004013 # Some arm boards have issues with parallel sync.
4014 if platform.machine().startswith('arm'):
4015 jobs = 1
4016 else:
4017 jobs = max(8, gclient_utils.NumLocalCpus())
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004018
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004019 self.add_option(
4020 '-j',
4021 '--jobs',
4022 default=jobs,
4023 type='int',
4024 help='Specify how many SCM commands can run in parallel; defaults to '
4025 '%default on this machine')
4026 self.add_option(
4027 '-v',
4028 '--verbose',
4029 action='count',
4030 default=0,
4031 help='Produces additional output for diagnostics. Can be used up to '
4032 'three times for more logging info.')
4033 self.add_option('--gclientfile',
4034 dest='config_filename',
4035 help='Specify an alternate %s file' %
4036 self.gclientfile_default)
4037 self.add_option(
4038 '--spec',
4039 help='create a gclient file containing the provided string. Due to '
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004040 'Cygwin/Python brokenness, it can\'t contain any newlines.')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004041 self.add_option('--no-nag-max',
4042 default=False,
4043 action='store_true',
4044 help='Ignored for backwards compatibility.')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004045
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004046 def parse_args(self, args=None, _values=None):
4047 """Integrates standard options processing."""
4048 # Create an optparse.Values object that will store only the actual
4049 # passed options, without the defaults.
4050 actual_options = optparse.Values()
4051 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
4052 # Create an optparse.Values object with the default options.
4053 options = optparse.Values(self.get_default_values().__dict__)
4054 # Update it with the options passed by the user.
4055 options._update_careful(actual_options.__dict__)
4056 # Store the options passed by the user in an _actual_options attribute.
4057 # We store only the keys, and not the values, since the values can
4058 # contain arbitrary information, which might be PII.
4059 metrics.collector.add('arguments', list(actual_options.__dict__))
Edward Lemur3298e7b2018-07-17 18:21:27 +00004060
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004061 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
4062 logging.basicConfig(
4063 level=levels[min(options.verbose,
4064 len(levels) - 1)],
4065 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
4066 if options.config_filename and options.spec:
4067 self.error('Cannot specify both --gclientfile and --spec')
4068 if (options.config_filename and options.config_filename !=
4069 os.path.basename(options.config_filename)):
4070 self.error('--gclientfile target must be a filename, not a path')
4071 if not options.config_filename:
4072 options.config_filename = self.gclientfile_default
4073 options.entries_filename = options.config_filename + '_entries'
4074 if options.jobs < 1:
4075 self.error('--jobs must be 1 or higher')
maruel@chromium.org0895b752011-08-26 20:40:33 +00004076
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004077 # These hacks need to die.
4078 if not hasattr(options, 'revisions'):
4079 # GClient.RunOnDeps expects it even if not applicable.
4080 options.revisions = []
4081 if not hasattr(options, 'experiments'):
4082 options.experiments = []
4083 if not hasattr(options, 'head'):
4084 options.head = None
4085 if not hasattr(options, 'nohooks'):
4086 options.nohooks = True
4087 if not hasattr(options, 'noprehooks'):
4088 options.noprehooks = True
4089 if not hasattr(options, 'deps_os'):
4090 options.deps_os = None
4091 if not hasattr(options, 'force'):
4092 options.force = None
4093 return (options, args)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004094
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004095
4096def disable_buffering():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004097 # Make stdout auto-flush so buildbot doesn't kill us during lengthy
4098 # operations. Python as a strong tendency to buffer sys.stdout.
4099 sys.stdout = gclient_utils.MakeFileAutoFlush(sys.stdout)
4100 # Make stdout annotated with the thread ids.
4101 sys.stdout = gclient_utils.MakeFileAnnotated(sys.stdout)
maruel@chromium.org0895b752011-08-26 20:40:33 +00004102
4103
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004104def path_contains_tilde():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004105 for element in os.environ['PATH'].split(os.pathsep):
4106 if element.startswith('~') and os.path.abspath(
4107 os.path.realpath(
4108 os.path.expanduser(element))) == DEPOT_TOOLS_DIR:
4109 return True
4110 return False
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004111
4112
4113def can_run_gclient_and_helpers():
Gavin Mak7f5b53f2023-09-07 18:13:01 +00004114 if sys.version_info[0] < 3:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004115 print('\nYour python version %s is unsupported, please upgrade.\n' %
4116 sys.version.split(' ', 1)[0],
4117 file=sys.stderr)
4118 return False
4119 if not sys.executable:
4120 print('\nPython cannot find the location of it\'s own executable.\n',
4121 file=sys.stderr)
4122 return False
4123 if path_contains_tilde():
4124 print(
4125 '\nYour PATH contains a literal "~", which works in some shells ' +
4126 'but will break when python tries to run subprocesses. ' +
4127 'Replace the "~" with $HOME.\n' + 'See https://crbug.com/952865.\n',
4128 file=sys.stderr)
4129 return False
4130 return True
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004131
4132
4133def main(argv):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004134 """Doesn't parse the arguments here, just find the right subcommand to
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004135 execute."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004136 if not can_run_gclient_and_helpers():
4137 return 2
4138 fix_encoding.fix_encoding()
4139 disable_buffering()
4140 setup_color.init()
4141 dispatcher = subcommand.CommandDispatcher(__name__)
4142 try:
4143 return dispatcher.execute(OptionParser(), argv)
4144 except KeyboardInterrupt:
4145 gclient_utils.GClientChildren.KillAllRemainingChildren()
4146 raise
4147 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
4148 print('Error: %s' % str(e), file=sys.stderr)
4149 return 1
4150 finally:
4151 gclient_utils.PrintWarnings()
4152 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004153
4154
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00004155if '__main__' == __name__:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004156 with metrics.collector.print_notice_and_exit():
4157 sys.exit(main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004158
4159# vim: ts=2:sw=2:tw=80:et: