blob: 659fdb482093c3c6bfd43b01e93ba65a05bd60b0 [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
Joanna Wang66286612022-06-30 19:59:13 +0000131
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200132class GNException(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000133 pass
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200134
135
Aravind Vasudevaned935cf2023-08-24 23:52:20 +0000136def ToGNString(value):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000137 """Returns a stringified GN equivalent of the Python value."""
138 if isinstance(value, str):
139 if value.find('\n') >= 0:
140 raise GNException("Trying to print a string with a newline in it.")
141 return '"' + \
142 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
143 '"'
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200144
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000145 if isinstance(value, bool):
146 if value:
147 return "true"
148 return "false"
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200149
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000150 # NOTE: some type handling removed compared to chromium/src copy.
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200151
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000152 raise GNException("Unsupported type when printing to GN.")
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200153
154
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200155class Hook(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000156 """Descriptor of command ran before/after sync or on demand."""
157 def __init__(self,
158 action,
159 pattern=None,
160 name=None,
161 cwd=None,
162 condition=None,
163 variables=None,
164 verbose=False,
165 cwd_base=None):
166 """Constructor.
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200167
168 Arguments:
Gavin Mak65c49b12023-08-24 18:06:42 +0000169 action (list of str): argv of the command to run
170 pattern (str regex): noop with git; deprecated
171 name (str): optional name; no effect on operation
172 cwd (str): working directory to use
173 condition (str): condition when to run the hook
Paweł Hajdan, Jr032d5452017-06-22 20:43:53 +0200174 variables (dict): variables for evaluating the condition
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200175 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000176 self._action = gclient_utils.freeze(action)
177 self._pattern = pattern
178 self._name = name
179 self._cwd = cwd
180 self._condition = condition
181 self._variables = variables
182 self._verbose = verbose
183 self._cwd_base = cwd_base
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200184
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000185 @staticmethod
186 def from_dict(d,
187 variables=None,
188 verbose=False,
189 conditions=None,
190 cwd_base=None):
191 """Creates a Hook instance from a dict like in the DEPS file."""
192 # Merge any local and inherited conditions.
193 gclient_eval.UpdateCondition(d, 'and', conditions)
194 return Hook(
195 d['action'],
196 d.get('pattern'),
197 d.get('name'),
198 d.get('cwd'),
199 d.get('condition'),
200 variables=variables,
201 # Always print the header if not printing to a TTY.
202 verbose=verbose or not setup_color.IS_TTY,
203 cwd_base=cwd_base)
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200204
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000205 @property
206 def action(self):
207 return self._action
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200208
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000209 @property
210 def pattern(self):
211 return self._pattern
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200212
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000213 @property
214 def name(self):
215 return self._name
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200216
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000217 @property
218 def condition(self):
219 return self._condition
Paweł Hajdan, Jrecf53fe2017-09-29 18:28:49 +0200220
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000221 @property
222 def effective_cwd(self):
223 cwd = self._cwd_base
224 if self._cwd:
225 cwd = os.path.join(cwd, self._cwd)
226 return cwd
Corentin Walleza68660d2018-09-10 17:33:24 +0000227
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000228 def matches(self, file_list):
229 """Returns true if the pattern matches any of files in the list."""
230 if not self._pattern:
231 return True
232 pattern = re.compile(self._pattern)
233 return bool([f for f in file_list if pattern.search(f)])
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200234
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000235 def run(self):
236 """Executes the hook's command (provided the condition is met)."""
237 if (self._condition and not gclient_eval.EvaluateCondition(
238 self._condition, self._variables)):
239 return
Paweł Hajdan, Jr032d5452017-06-22 20:43:53 +0200240
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000241 cmd = list(self._action)
Paweł Hajdan, Jrfc6196b2017-07-27 13:15:25 +0200242
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000243 if cmd[0] == 'vpython3' and _detect_host_os() == 'win':
244 cmd[0] += '.bat'
Paweł Hajdan, Jrc9364392017-06-14 17:11:56 +0200245
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000246 exit_code = 2
247 try:
248 start_time = time.time()
249 gclient_utils.CheckCallAndFilter(cmd,
250 cwd=self.effective_cwd,
251 print_stdout=True,
252 show_header=True,
253 always_show_header=self._verbose)
254 exit_code = 0
255 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
256 # Use a discrete exit status code of 2 to indicate that a hook
257 # action failed. Users of this script may wish to treat hook action
258 # failures differently from VC failures.
259 print('Error: %s' % str(e), file=sys.stderr)
260 sys.exit(exit_code)
261 finally:
262 elapsed_time = time.time() - start_time
263 metrics.collector.add_repeated(
264 'hooks', {
265 'action':
266 gclient_utils.CommandToStr(cmd),
267 'name':
268 self._name,
269 'cwd':
270 os.path.relpath(os.path.normpath(self.effective_cwd),
271 self._cwd_base),
272 'condition':
273 self._condition,
274 'execution_time':
275 elapsed_time,
276 'exit_code':
277 exit_code,
278 })
279 if elapsed_time > 10:
280 print("Hook '%s' took %.2f secs" %
281 (gclient_utils.CommandToStr(cmd), elapsed_time))
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200282
283
Paweł Hajdan, Jrfc6196b2017-07-27 13:15:25 +0200284class DependencySettings(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000285 """Immutable configuration settings."""
286 def __init__(self, parent, url, managed, custom_deps, custom_vars,
287 custom_hooks, deps_file, should_process, relative, condition):
288 # These are not mutable:
289 self._parent = parent
290 self._deps_file = deps_file
291 self._url = url
292 # The condition as string (or None). Useful to keep e.g. for flatten.
293 self._condition = condition
294 # 'managed' determines whether or not this dependency is synced/updated
295 # by gclient after gclient checks it out initially. The difference
296 # between 'managed' and 'should_process' is that the user specifies
297 # 'managed' via the --unmanaged command-line flag or a .gclient config,
298 # where 'should_process' is dynamically set by gclient if it goes over
299 # its recursion limit and controls gclient's behavior so it does not
300 # misbehave.
301 self._managed = managed
302 self._should_process = should_process
303 # If this is a recursed-upon sub-dependency, and the parent has
304 # use_relative_paths set, then this dependency should check out its own
305 # dependencies relative to that parent's path for this, rather than
306 # relative to the .gclient file.
307 self._relative = relative
308 # This is a mutable value which has the list of 'target_os' OSes listed
309 # in the current deps file.
310 self.local_target_os = None
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000311
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000312 # These are only set in .gclient and not in DEPS files.
313 self._custom_vars = custom_vars or {}
314 self._custom_deps = custom_deps or {}
315 self._custom_hooks = custom_hooks or []
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000316
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000317 # Post process the url to remove trailing slashes.
318 if isinstance(self.url, str):
319 # urls are sometime incorrectly written as proto://host/path/@rev.
320 # Replace it to proto://host/path@rev.
321 self.set_url(self.url.replace('/@', '@'))
322 elif not isinstance(self.url, (None.__class__)):
323 raise gclient_utils.Error(
324 ('dependency url must be either string or None, '
325 'instead of %s') % self.url.__class__.__name__)
Edward Lemure7273d22018-05-10 19:13:51 -0400326
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000327 # Make any deps_file path platform-appropriate.
328 if self._deps_file:
329 for sep in ['/', '\\']:
330 self._deps_file = self._deps_file.replace(sep, os.sep)
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000331
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000332 @property
333 def deps_file(self):
334 return self._deps_file
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000335
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000336 @property
337 def managed(self):
338 return self._managed
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000339
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000340 @property
341 def parent(self):
342 return self._parent
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000343
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000344 @property
345 def root(self):
346 """Returns the root node, a GClient object."""
347 if not self.parent:
348 # This line is to signal pylint that it could be a GClient instance.
349 return self or GClient(None, None)
350 return self.parent.root
maruel@chromium.org8c0d9582011-10-03 21:36:01 +0000351
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000352 @property
353 def should_process(self):
354 """True if this dependency should be processed, i.e. checked out."""
355 return self._should_process
Michael Mossd683d7c2018-06-15 05:05:17 +0000356
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000357 @property
358 def custom_vars(self):
359 return self._custom_vars.copy()
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000360
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000361 @property
362 def custom_deps(self):
363 return self._custom_deps.copy()
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000364
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000365 @property
366 def custom_hooks(self):
367 return self._custom_hooks[:]
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000368
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000369 @property
370 def url(self):
371 """URL after variable expansion."""
372 return self._url
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000373
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000374 @property
375 def condition(self):
376 return self._condition
Paweł Hajdan, Jrf69860b2017-06-05 20:24:28 +0200377
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000378 @property
379 def target_os(self):
380 if self.local_target_os is not None:
381 return tuple(set(self.local_target_os).union(self.parent.target_os))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000382
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000383 return self.parent.target_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +0000384
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000385 @property
386 def target_cpu(self):
387 return self.parent.target_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -0800388
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000389 def set_url(self, url):
390 self._url = url
Edward Lemure7273d22018-05-10 19:13:51 -0400391
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000392 def get_custom_deps(self, name, url):
393 """Returns a custom deps if applicable."""
394 if self.parent:
395 url = self.parent.get_custom_deps(name, url)
396 # None is a valid return value to disable a dependency.
397 return self.custom_deps.get(name, url)
maruel@chromium.org8c0d9582011-10-03 21:36:01 +0000398
maruel@chromium.org064186c2011-09-27 23:53:33 +0000399
400class Dependency(gclient_utils.WorkItem, DependencySettings):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000401 """Object that represents a dependency checkout."""
402 def __init__(self,
403 parent,
404 name,
405 url,
406 managed,
407 custom_deps,
408 custom_vars,
409 custom_hooks,
410 deps_file,
411 should_process,
412 should_recurse,
413 relative,
414 condition,
415 protocol='https',
416 git_dependencies_state=gclient_eval.DEPS,
417 print_outbuf=False):
418 gclient_utils.WorkItem.__init__(self, name)
419 DependencySettings.__init__(self, parent, url, managed, custom_deps,
420 custom_vars, custom_hooks, deps_file,
421 should_process, relative, condition)
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000422
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000423 # This is in both .gclient and DEPS files:
424 self._deps_hooks = []
maruel@chromium.org68988972011-09-20 14:11:42 +0000425
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000426 self._pre_deps_hooks = []
maruel@chromium.org68988972011-09-20 14:11:42 +0000427
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000428 # Calculates properties:
429 self._dependencies = []
430 self._vars = {}
borenet@google.com2d1ee9e2013-10-15 08:13:16 +0000431
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000432 # A cache of the files affected by the current operation, necessary for
433 # hooks.
434 self._file_list = []
435 # List of host names from which dependencies are allowed.
436 # Default is an empty set, meaning unspecified in DEPS file, and hence
437 # all hosts will be allowed. Non-empty set means allowlist of hosts.
438 # allowed_hosts var is scoped to its DEPS file, and so it isn't
439 # recursive.
440 self._allowed_hosts = frozenset()
441 self._gn_args_from = None
442 # Spec for .gni output to write (if any).
443 self._gn_args_file = None
444 self._gn_args = []
445 # If it is not set to True, the dependency wasn't processed for its
446 # child dependency, i.e. its DEPS wasn't read.
447 self._deps_parsed = False
448 # This dependency has been processed, i.e. checked out
449 self._processed = False
450 # This dependency had its pre-DEPS hooks run
451 self._pre_deps_hooks_ran = False
452 # This dependency had its hook run
453 self._hooks_ran = False
454 # This is the scm used to checkout self.url. It may be used by
455 # dependencies to get the datetime of the revision we checked out.
456 self._used_scm = None
457 self._used_revision = None
458 # The actual revision we ended up getting, or None if that information
459 # is unavailable
460 self._got_revision = None
461 # Whether this dependency should use relative paths.
462 self._use_relative_paths = False
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200463
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000464 # recursedeps is a mutable value that selectively overrides the default
465 # 'no recursion' setting on a dep-by-dep basis.
466 #
467 # It will be a dictionary of {deps_name: depfile_namee}
468 self.recursedeps = {}
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000469
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000470 # Whether we should process this dependency's DEPS file.
471 self._should_recurse = should_recurse
Edward Lemurfbb06aa2018-06-11 20:43:06 +0000472
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000473 # Whether we should sync git/cipd dependencies and hooks from the
474 # DEPS file.
475 # This is set based on skip_sync_revisions and must be done
476 # after the patch refs are applied.
477 # If this is False, we will still run custom_hooks and process
478 # custom_deps, if any.
479 self._should_sync = True
Edward Lemure7273d22018-05-10 19:13:51 -0400480
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000481 self._OverrideUrl()
482 # This is inherited from WorkItem. We want the URL to be a resource.
483 if self.url and isinstance(self.url, str):
484 # The url is usually given to gclient either as https://blah@123
485 # or just https://blah. The @123 portion is irrelevant.
486 self.resources.append(self.url.split('@')[0])
Joanna Wang18af7ef2022-07-01 16:51:00 +0000487
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000488 # Controls whether we want to print git's output when we first clone the
489 # dependency
490 self.print_outbuf = print_outbuf
cmp@chromium.orge84ac912014-06-30 23:14:35 +0000491
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000492 self.protocol = protocol
493 self.git_dependencies_state = git_dependencies_state
Edward Lemur231f5ea2018-01-31 19:02:36 +0100494
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000495 if not self.name and self.parent:
496 raise gclient_utils.Error('Dependency without name')
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000497
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000498 def _OverrideUrl(self):
499 """Resolves the parsed url from the parent hierarchy."""
500 parsed_url = self.get_custom_deps(
501 self._name.replace(os.sep, posixpath.sep) \
502 if self._name else self._name, self.url)
503 if parsed_url != self.url:
504 logging.info('Dependency(%s)._OverrideUrl(%s) -> %s', self._name,
505 self.url, parsed_url)
506 self.set_url(parsed_url)
507 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000508
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000509 if self.url is None:
510 logging.info('Dependency(%s)._OverrideUrl(None) -> None',
511 self._name)
512 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000513
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000514 if not isinstance(self.url, str):
515 raise gclient_utils.Error('Unknown url type')
Michael Mossd683d7c2018-06-15 05:05:17 +0000516
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000517 # self.url is a local path
518 path, at, rev = self.url.partition('@')
519 if os.path.isdir(path):
520 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000521
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000522 # self.url is a URL
523 parsed_url = urllib.parse.urlparse(self.url)
524 if parsed_url[0] or re.match(r'^\w+\@[\w\.-]+\:[\w\/]+', parsed_url[2]):
525 return
Edward Lemur1f392b82019-11-15 22:40:11 +0000526
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000527 # self.url is relative to the parent's URL.
528 if not path.startswith('/'):
529 raise gclient_utils.Error(
530 'relative DEPS entry \'%s\' must begin with a slash' % self.url)
Edward Lemur1f392b82019-11-15 22:40:11 +0000531
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000532 parent_url = self.parent.url
533 parent_path = self.parent.url.split('@')[0]
534 if os.path.isdir(parent_path):
535 # Parent's URL is a local path. Get parent's URL dirname and append
536 # self.url.
537 parent_path = os.path.dirname(parent_path)
538 parsed_url = parent_path + path.replace('/', os.sep) + at + rev
539 else:
540 # Parent's URL is a URL. Get parent's URL, strip from the last '/'
541 # (equivalent to unix dirname) and append self.url.
542 parsed_url = parent_url[:parent_url.rfind('/')] + self.url
Edward Lemur1f392b82019-11-15 22:40:11 +0000543
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000544 logging.info('Dependency(%s)._OverrideUrl(%s) -> %s', self.name,
545 self.url, parsed_url)
546 self.set_url(parsed_url)
Edward Lemur1f392b82019-11-15 22:40:11 +0000547
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000548 def PinToActualRevision(self):
549 """Updates self.url to the revision checked out on disk."""
550 if self.url is None:
551 return
552 url = None
553 scm = self.CreateSCM()
554 if scm.name == 'cipd':
555 revision = scm.revinfo(None, None, None)
556 package = self.GetExpandedPackageName()
557 url = '%s/p/%s/+/%s' % (scm.GetActualRemoteURL(None), package,
558 revision)
Edward Lemur1f392b82019-11-15 22:40:11 +0000559
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000560 if os.path.isdir(scm.checkout_path):
561 revision = scm.revinfo(None, None, None)
562 url = '%s@%s' % (gclient_utils.SplitUrlRevision(
563 self.url)[0], revision)
564 self.set_url(url)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +0000565
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000566 def ToLines(self):
567 # () -> Sequence[str]
568 """Returns strings representing the deps (info, graphviz line)"""
569 s = []
570 condition_part = ([' "condition": %r,' %
571 self.condition] if self.condition else [])
572 s.extend([
573 ' # %s' % self.hierarchy(include_url=False),
574 ' "%s": {' % (self.name, ),
575 ' "url": "%s",' % (self.url, ),
576 ] + condition_part + [
577 ' },',
578 '',
579 ])
580 return s
Edward Lemure7273d22018-05-10 19:13:51 -0400581
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000582 @property
583 def requirements(self):
584 """Calculate the list of requirements."""
585 requirements = set()
586 # self.parent is implicitly a requirement. This will be recursive by
587 # definition.
588 if self.parent and self.parent.name:
589 requirements.add(self.parent.name)
John Budorick0f7b2002018-01-19 15:46:17 -0800590
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000591 # For a tree with at least 2 levels*, the leaf node needs to depend
592 # on the level higher up in an orderly way.
593 # This becomes messy for >2 depth as the DEPS file format is a
594 # dictionary, thus unsorted, while the .gclient format is a list thus
595 # sorted.
596 #
597 # Interestingly enough, the following condition only works in the case
598 # we want: self is a 2nd level node. 3rd level node wouldn't need this
599 # since they already have their parent as a requirement.
600 if self.parent and self.parent.parent and not self.parent.parent.parent:
601 requirements |= set(i.name for i in self.root.dependencies
602 if i.name)
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000603
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000604 if self.name:
605 requirements |= set(
606 obj.name for obj in self.root.subtree(False)
607 if (obj is not self and obj.name
608 and self.name.startswith(posixpath.join(obj.name, ''))))
609 requirements = tuple(sorted(requirements))
610 logging.info('Dependency(%s).requirements = %s' %
611 (self.name, requirements))
612 return requirements
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000613
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000614 @property
615 def should_recurse(self):
616 return self._should_recurse
maruel@chromium.org470b5432011-10-11 18:18:19 +0000617
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000618 def verify_validity(self):
619 """Verifies that this Dependency is fine to add as a child of another one.
maruel@chromium.org470b5432011-10-11 18:18:19 +0000620
621 Returns True if this entry should be added, False if it is a duplicate of
622 another entry.
623 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000624 logging.info('Dependency(%s).verify_validity()' % self.name)
625 if self.name in [s.name for s in self.parent.dependencies]:
626 raise gclient_utils.Error(
627 'The same name "%s" appears multiple times in the deps section'
628 % self.name)
629 if not self.should_process:
630 # Return early, no need to set requirements.
631 return not any(d.name == self.name for d in self.root.subtree(True))
maruel@chromium.org470b5432011-10-11 18:18:19 +0000632
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000633 # This require a full tree traversal with locks.
634 siblings = [d for d in self.root.subtree(False) if d.name == self.name]
635 for sibling in siblings:
636 # Allow to have only one to be None or ''.
637 if self.url != sibling.url and bool(self.url) == bool(sibling.url):
638 raise gclient_utils.Error(
639 ('Dependency %s specified more than once:\n'
640 ' %s [%s]\n'
641 'vs\n'
642 ' %s [%s]') % (self.name, sibling.hierarchy(),
643 sibling.url, self.hierarchy(), self.url))
644 # In theory we could keep it as a shadow of the other one. In
645 # practice, simply ignore it.
646 logging.warning("Won't process duplicate dependency %s" % sibling)
647 return False
648 return True
maruel@chromium.org064186c2011-09-27 23:53:33 +0000649
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000650 def _postprocess_deps(self, deps, rel_prefix):
651 # type: (Mapping[str, Mapping[str, str]], str) ->
652 # Mapping[str, Mapping[str, str]]
653 """Performs post-processing of deps compared to what's in the DEPS file."""
654 # If we don't need to sync, only process custom_deps, if any.
655 if not self._should_sync:
656 if not self.custom_deps:
657 return {}
Paweł Hajdan, Jr4426eaf2017-06-13 12:25:47 +0200658
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000659 processed_deps = {}
660 for dep_name, dep_info in self.custom_deps.items():
661 if dep_info and not dep_info.endswith('@unmanaged'):
662 if dep_name in deps:
663 # custom_deps that should override an existing deps gets
664 # applied in the Dependency itself with _OverrideUrl().
665 processed_deps[dep_name] = deps[dep_name]
666 else:
667 processed_deps[dep_name] = {
668 'url': dep_info,
669 'dep_type': 'git'
670 }
671 else:
672 processed_deps = dict(deps)
Joanna Wang18af7ef2022-07-01 16:51:00 +0000673
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000674 # If a line is in custom_deps, but not in the solution, we want to
675 # append this line to the solution.
676 for dep_name, dep_info in self.custom_deps.items():
677 # Don't add it to the solution for the values of "None" and
678 # "unmanaged" in order to force these kinds of custom_deps to
679 # act as revision overrides (via revision_overrides). Having
680 # them function as revision overrides allows them to be applied
681 # to recursive dependencies. https://crbug.com/1031185
682 if (dep_name not in processed_deps and dep_info
683 and not dep_info.endswith('@unmanaged')):
684 processed_deps[dep_name] = {
685 'url': dep_info,
686 'dep_type': 'git'
687 }
Edward Lemur16f4bad2018-05-16 16:53:49 -0400688
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000689 # Make child deps conditional on any parent conditions. This ensures
690 # that, when flattened, recursed entries have the correct restrictions,
691 # even if not explicitly set in the recursed DEPS file. For instance, if
692 # "src/ios_foo" is conditional on "checkout_ios=True", then anything
693 # recursively included by "src/ios_foo/DEPS" should also require
694 # "checkout_ios=True".
695 if self.condition:
696 for value in processed_deps.values():
697 gclient_eval.UpdateCondition(value, 'and', self.condition)
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200698
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000699 if not rel_prefix:
700 return processed_deps
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200701
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000702 logging.warning('use_relative_paths enabled.')
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200703 rel_deps = {}
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000704 for d, url in processed_deps.items():
705 # normpath is required to allow DEPS to use .. in their
706 # dependency local path.
707 # We are following the same pattern when use_relative_paths = False,
708 # which uses slashes.
709 rel_deps[os.path.normpath(os.path.join(rel_prefix, d)).replace(
710 os.path.sep, '/')] = url
711 logging.warning('Updating deps by prepending %s.', rel_prefix)
712 return rel_deps
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200713
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000714 def _deps_to_objects(self, deps, use_relative_paths):
715 # type: (Mapping[str, Mapping[str, str]], bool) -> Sequence[Dependency]
716 """Convert a deps dict to a list of Dependency objects."""
717 deps_to_add = []
718 cached_conditions = {}
719 for name, dep_value in deps.items():
720 should_process = self.should_process
721 if dep_value is None:
722 continue
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200723
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000724 condition = dep_value.get('condition')
725 dep_type = dep_value.get('dep_type')
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000726
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000727 if condition and not self._get_option('process_all_deps', False):
728 if condition not in cached_conditions:
729 cached_conditions[
730 condition] = gclient_eval.EvaluateCondition(
731 condition, self.get_vars())
732 should_process = should_process and cached_conditions[condition]
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000733
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000734 # The following option is only set by the 'revinfo' command.
735 if self._get_option('ignore_dep_type', None) == dep_type:
736 continue
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000737
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000738 if dep_type == 'cipd':
739 cipd_root = self.GetCipdRoot()
740 for package in dep_value.get('packages', []):
741 deps_to_add.append(
742 CipdDependency(parent=self,
743 name=name,
744 dep_value=package,
745 cipd_root=cipd_root,
746 custom_vars=self.custom_vars,
747 should_process=should_process,
748 relative=use_relative_paths,
749 condition=condition))
750 else:
751 url = dep_value.get('url')
752 deps_to_add.append(
753 GitDependency(
754 parent=self,
755 name=name,
756 # Update URL with scheme in protocol_override
757 url=GitDependency.updateProtocol(url, self.protocol),
758 managed=True,
759 custom_deps=None,
760 custom_vars=self.custom_vars,
761 custom_hooks=None,
762 deps_file=self.recursedeps.get(name, self.deps_file),
763 should_process=should_process,
764 should_recurse=name in self.recursedeps,
765 relative=use_relative_paths,
766 condition=condition,
767 protocol=self.protocol,
768 git_dependencies_state=self.git_dependencies_state))
Michael Spang0e99b9b2020-08-12 13:34:48 +0000769
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000770 # TODO(crbug.com/1341285): Understand why we need this and remove
771 # it if we don't.
772 deps_to_add.sort(key=lambda x: x.name)
773 return deps_to_add
Corentin Walleza68660d2018-09-10 17:33:24 +0000774
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000775 def ParseDepsFile(self):
776 # type: () -> None
777 """Parses the DEPS file for this dependency."""
778 assert not self.deps_parsed
779 assert not self.dependencies
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000780
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000781 deps_content = None
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000782
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000783 # First try to locate the configured deps file. If it's missing,
784 # fallback to DEPS.
785 deps_files = [self.deps_file]
786 if 'DEPS' not in deps_files:
787 deps_files.append('DEPS')
788 for deps_file in deps_files:
789 filepath = os.path.join(self.root.root_dir, self.name, deps_file)
790 if os.path.isfile(filepath):
791 logging.info('ParseDepsFile(%s): %s file found at %s',
792 self.name, deps_file, filepath)
793 break
794 logging.info('ParseDepsFile(%s): No %s file found at %s', self.name,
795 deps_file, filepath)
borenet@google.com2d1ee9e2013-10-15 08:13:16 +0000796
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000797 if os.path.isfile(filepath):
798 deps_content = gclient_utils.FileRead(filepath)
799 logging.debug('ParseDepsFile(%s) read:\n%s', self.name,
800 deps_content)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +0000801
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000802 local_scope = {}
803 if deps_content:
804 try:
805 local_scope = gclient_eval.Parse(deps_content, filepath,
806 self.get_vars(),
807 self.get_builtin_vars())
808 except SyntaxError as e:
809 gclient_utils.SyntaxErrorToError(filepath, e)
810
811 if 'git_dependencies' in local_scope:
812 self.git_dependencies_state = local_scope['git_dependencies']
813
814 if 'allowed_hosts' in local_scope:
815 try:
816 self._allowed_hosts = frozenset(
817 local_scope.get('allowed_hosts'))
818 except TypeError: # raised if non-iterable
819 pass
820 if not self._allowed_hosts:
821 logging.warning("allowed_hosts is specified but empty %s",
822 self._allowed_hosts)
823 raise gclient_utils.Error(
824 'ParseDepsFile(%s): allowed_hosts must be absent '
825 'or a non-empty iterable' % self.name)
826
827 self._gn_args_from = local_scope.get('gclient_gn_args_from')
828 self._gn_args_file = local_scope.get('gclient_gn_args_file')
829 self._gn_args = local_scope.get('gclient_gn_args', [])
830 # It doesn't make sense to set all of these, since setting gn_args_from
831 # to another DEPS will make gclient ignore any other local gn_args*
832 # settings.
833 assert not (self._gn_args_from and self._gn_args_file), \
834 'Only specify one of "gclient_gn_args_from" or ' \
835 '"gclient_gn_args_file + gclient_gn_args".'
836
837 self._vars = local_scope.get('vars', {})
838 if self.parent:
839 for key, value in self.parent.get_vars().items():
840 if key in self._vars:
841 self._vars[key] = value
842 # Since we heavily post-process things, freeze ones which should
843 # reflect original state of DEPS.
844 self._vars = gclient_utils.freeze(self._vars)
845
846 # If use_relative_paths is set in the DEPS file, regenerate
847 # the dictionary using paths relative to the directory containing
848 # the DEPS file. Also update recursedeps if use_relative_paths is
849 # enabled.
850 # If the deps file doesn't set use_relative_paths, but the parent did
851 # (and therefore set self.relative on this Dependency object), then we
852 # want to modify the deps and recursedeps by prepending the parent
853 # directory of this dependency.
854 self._use_relative_paths = local_scope.get('use_relative_paths', False)
855 rel_prefix = None
856 if self._use_relative_paths:
857 rel_prefix = self.name
858 elif self._relative:
859 rel_prefix = os.path.dirname(self.name)
860
861 if 'recursion' in local_scope:
862 logging.warning('%s: Ignoring recursion = %d.', self.name,
863 local_scope['recursion'])
864
865 if 'recursedeps' in local_scope:
866 for ent in local_scope['recursedeps']:
867 if isinstance(ent, str):
868 self.recursedeps[ent] = self.deps_file
869 else: # (depname, depsfilename)
870 self.recursedeps[ent[0]] = ent[1]
871 logging.warning('Found recursedeps %r.', repr(self.recursedeps))
872
873 if rel_prefix:
874 logging.warning('Updating recursedeps by prepending %s.',
875 rel_prefix)
876 rel_deps = {}
877 for depname, options in self.recursedeps.items():
878 rel_deps[os.path.normpath(os.path.join(rel_prefix,
879 depname)).replace(
880 os.path.sep,
881 '/')] = options
882 self.recursedeps = rel_deps
883 # To get gn_args from another DEPS, that DEPS must be recursed into.
884 if self._gn_args_from:
885 assert self.recursedeps and self._gn_args_from in self.recursedeps, \
886 'The "gclient_gn_args_from" value must be in recursedeps.'
887
888 # If present, save 'target_os' in the local_target_os property.
889 if 'target_os' in local_scope:
890 self.local_target_os = local_scope['target_os']
891
892 deps = local_scope.get('deps', {})
893
894 # If dependencies are configured within git submodules, add them to
895 # deps. We don't add for SYNC since we expect submodules to be in sync.
896 if self.git_dependencies_state == gclient_eval.SUBMODULES:
897 deps.update(self.ParseGitSubmodules())
898
899 deps_to_add = self._deps_to_objects(
900 self._postprocess_deps(deps, rel_prefix), self._use_relative_paths)
901
902 # compute which working directory should be used for hooks
903 if local_scope.get('use_relative_hooks', False):
904 print('use_relative_hooks is deprecated, please remove it from '
905 '%s DEPS. (it was merged in use_relative_paths)' % self.name,
906 file=sys.stderr)
907
908 hooks_cwd = self.root.root_dir
909 if self._use_relative_paths:
910 hooks_cwd = os.path.join(hooks_cwd, self.name)
911 elif self._relative:
912 hooks_cwd = os.path.join(hooks_cwd, os.path.dirname(self.name))
913 logging.warning('Using hook base working directory: %s.', hooks_cwd)
914
915 # Only add all hooks if we should sync, otherwise just add custom hooks.
916 # override named sets of hooks by the custom hooks
917 hooks_to_run = []
918 if self._should_sync:
919 hook_names_to_suppress = [
920 c.get('name', '') for c in self.custom_hooks
921 ]
922 for hook in local_scope.get('hooks', []):
923 if hook.get('name', '') not in hook_names_to_suppress:
924 hooks_to_run.append(hook)
925
926 # add the replacements and any additions
927 for hook in self.custom_hooks:
928 if 'action' in hook:
929 hooks_to_run.append(hook)
930
931 if self.should_recurse and deps_to_add:
932 self._pre_deps_hooks = [
933 Hook.from_dict(hook,
934 variables=self.get_vars(),
935 verbose=True,
936 conditions=self.condition,
937 cwd_base=hooks_cwd)
938 for hook in local_scope.get('pre_deps_hooks', [])
939 ]
940
941 self.add_dependencies_and_close(deps_to_add,
942 hooks_to_run,
943 hooks_cwd=hooks_cwd)
944 logging.info('ParseDepsFile(%s) done' % self.name)
945
946 def ParseGitSubmodules(self):
947 # type: () -> Mapping[str, str]
948 """
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000949 Parses git submodules and returns a dict of path to DEPS git url entries.
950
951 e.g {<path>: <url>@<commit_hash>}
952 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000953 cwd = os.path.join(self.root.root_dir, self.name)
954 filepath = os.path.join(cwd, '.gitmodules')
955 if not os.path.isfile(filepath):
956 logging.warning('ParseGitSubmodules(): No .gitmodules found at %s',
957 filepath)
958 return {}
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000959
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000960 # Get .gitmodules fields
961 gitmodules_entries = subprocess2.check_output(
962 ['git', 'config', '--file', filepath, '-l']).decode('utf-8')
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000963
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000964 gitmodules = {}
965 for entry in gitmodules_entries.splitlines():
966 key, value = entry.split('=', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000967
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000968 # git config keys consist of section.name.key, e.g.,
969 # submodule.foo.path
970 section, submodule_key = key.split('.', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000971
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000972 # Only parse [submodule "foo"] sections from .gitmodules.
973 if section.strip() != 'submodule':
974 continue
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000975
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000976 # The name of the submodule can contain '.', hence split from the
977 # back.
978 submodule, sub_key = submodule_key.rsplit('.', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000979
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000980 if submodule not in gitmodules:
981 gitmodules[submodule] = {}
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000982
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000983 if sub_key in ('url', 'gclient-condition', 'path'):
984 gitmodules[submodule][sub_key] = value
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000985
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000986 paths = [module['path'] for module in gitmodules.values()]
987 commit_hashes = scm_git.GIT.GetSubmoduleCommits(cwd, paths)
Joanna Wang978f43d2023-08-18 00:16:07 +0000988
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000989 # Structure git submodules into a dict of DEPS git url entries.
990 submodules = {}
991 for module in gitmodules.values():
992 if self._use_relative_paths:
993 path = module['path']
994 else:
995 path = f'{self.name}/{module["path"]}'
996 # TODO(crbug.com/1471685): Temporary hack. In case of applied
997 # patches where the changes are staged but not committed, any
998 # gitlinks from the patch are not returned by `git ls-tree`. The
999 # path won't be found in commit_hashes. Use a temporary '0000000'
1000 # value that will be replaced with w/e is found in DEPS later.
1001 submodules[path] = {
1002 'dep_type':
1003 'git',
1004 'url':
1005 '{}@{}'.format(module['url'],
1006 commit_hashes.get(module['path'], '0000000'))
1007 }
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001008
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001009 if 'gclient-condition' in module:
1010 submodules[path]['condition'] = module['gclient-condition']
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001011
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001012 return submodules
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001013
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001014 def _get_option(self, attr, default):
1015 obj = self
1016 while not hasattr(obj, '_options'):
1017 obj = obj.parent
1018 return getattr(obj._options, attr, default)
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02001019
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001020 def add_dependencies_and_close(self, deps_to_add, hooks, hooks_cwd=None):
1021 """Adds the dependencies, hooks and mark the parsing as done."""
1022 if hooks_cwd == None:
1023 hooks_cwd = self.root.root_dir
Corentin Walleza68660d2018-09-10 17:33:24 +00001024
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001025 for dep in deps_to_add:
1026 if dep.verify_validity():
1027 self.add_dependency(dep)
1028 self._mark_as_parsed([
1029 Hook.from_dict(h,
1030 variables=self.get_vars(),
1031 verbose=self.root._options.verbose,
1032 conditions=self.condition,
1033 cwd_base=hooks_cwd) for h in hooks
1034 ])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001036 def findDepsFromNotAllowedHosts(self):
1037 """Returns a list of dependencies from not allowed hosts.
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001038
1039 If allowed_hosts is not set, allows all hosts and returns empty list.
1040 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001041 if not self._allowed_hosts:
1042 return []
1043 bad_deps = []
1044 for dep in self._dependencies:
1045 # Don't enforce this for custom_deps.
1046 if dep.name in self._custom_deps:
1047 continue
1048 if isinstance(dep.url, str):
1049 parsed_url = urllib.parse.urlparse(dep.url)
1050 if parsed_url.netloc and parsed_url.netloc not in self._allowed_hosts:
1051 bad_deps.append(dep)
1052 return bad_deps
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001053
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001054 def FuzzyMatchUrl(self, candidates):
1055 # type: (Union[Mapping[str, str], Collection[str]]) -> Optional[str]
1056 """Attempts to find this dependency in the list of candidates.
Edward Lesmesbb16e332018-03-30 17:54:51 -04001057
Edward Lemure7273d22018-05-10 19:13:51 -04001058 It looks first for the URL of this dependency in the list of
Edward Lesmesbb16e332018-03-30 17:54:51 -04001059 candidates. If it doesn't succeed, and the URL ends in '.git', it will try
1060 looking for the URL minus '.git'. Finally it will try to look for the name
1061 of the dependency.
1062
1063 Args:
Edward Lesmesbb16e332018-03-30 17:54:51 -04001064 candidates: list, dict. The list of candidates in which to look for this
1065 dependency. It can contain URLs as above, or dependency names like
1066 "src/some/dep".
1067
1068 Returns:
1069 If this dependency is not found in the list of candidates, returns None.
1070 Otherwise, it returns under which name did we find this dependency:
1071 - Its parsed url: "https://example.com/src.git'
1072 - Its parsed url minus '.git': "https://example.com/src"
1073 - Its name: "src"
1074 """
Michael Mossd683d7c2018-06-15 05:05:17 +00001075 if self.url:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001076 origin, _ = gclient_utils.SplitUrlRevision(self.url)
1077 match = gclient_utils.FuzzyMatchRepo(origin, candidates)
agabled437d762016-10-17 09:35:11 -07001078 if match:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001079 return match
1080 if self.name in candidates:
1081 return self.name
1082 return None
ilevy@chromium.org0233ac22012-11-28 20:27:02 +00001083
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001084 # Arguments number differs from overridden method
1085 # pylint: disable=arguments-differ
1086 def run(
1087 self,
1088 revision_overrides, # type: Mapping[str, str]
1089 command, # type: str
1090 args, # type: Sequence[str]
1091 work_queue, # type: ExecutionQueue
1092 options, # type: optparse.Values
1093 patch_refs, # type: Mapping[str, str]
1094 target_branches, # type: Mapping[str, str]
1095 skip_sync_revisions, # type: Mapping[str, str]
1096 ):
1097 # type: () -> None
1098 """Runs |command| then parse the DEPS file."""
1099 logging.info('Dependency(%s).run()' % self.name)
1100 assert self._file_list == []
1101 # When running runhooks, there's no need to consult the SCM.
1102 # All known hooks are expected to run unconditionally regardless of
1103 # working copy state, so skip the SCM status check.
1104 run_scm = command not in ('flatten', 'runhooks', 'recurse', 'validate',
1105 None)
1106 file_list = [] if not options.nohooks else None
1107 revision_override = revision_overrides.pop(
1108 self.FuzzyMatchUrl(revision_overrides), None)
1109 if not revision_override and not self.managed:
1110 revision_override = 'unmanaged'
1111 if run_scm and self.url:
1112 # Create a shallow copy to mutate revision.
1113 options = copy.copy(options)
1114 options.revision = revision_override
1115 self._used_revision = options.revision
1116 self._used_scm = self.CreateSCM(out_cb=work_queue.out_cb)
1117 if command != 'update' or self.GetScmName() != 'git':
1118 self._got_revision = self._used_scm.RunCommand(
1119 command, options, args, file_list)
agabled437d762016-10-17 09:35:11 -07001120 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001121 try:
1122 start = time.time()
1123 sync_status = metrics_utils.SYNC_STATUS_FAILURE
1124 self._got_revision = self._used_scm.RunCommand(
1125 command, options, args, file_list)
1126 sync_status = metrics_utils.SYNC_STATUS_SUCCESS
1127 finally:
1128 url, revision = gclient_utils.SplitUrlRevision(self.url)
1129 metrics.collector.add_repeated(
1130 'git_deps', {
1131 'path': self.name,
1132 'url': url,
1133 'revision': revision,
1134 'execution_time': time.time() - start,
1135 'sync_status': sync_status,
1136 })
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00001137
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001138 if isinstance(self, GitDependency) and command == 'update':
1139 patch_repo = self.url.split('@')[0]
1140 patch_ref = patch_refs.pop(self.FuzzyMatchUrl(patch_refs), None)
1141 target_branch = target_branches.pop(
1142 self.FuzzyMatchUrl(target_branches), None)
1143 if patch_ref:
1144 latest_commit = self._used_scm.apply_patch_ref(
1145 patch_repo, patch_ref, target_branch, options,
1146 file_list)
1147 else:
1148 latest_commit = self._used_scm.revinfo(None, None, None)
1149 existing_sync_commits = json.loads(
1150 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
1151 existing_sync_commits[self.name] = latest_commit
1152 os.environ[PREVIOUS_SYNC_COMMITS] = json.dumps(
1153 existing_sync_commits)
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001154
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001155 if file_list:
1156 file_list = [
1157 os.path.join(self.name, f.strip()) for f in file_list
1158 ]
John Budorick0f7b2002018-01-19 15:46:17 -08001159
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001160 # TODO(phajdan.jr): We should know exactly when the paths are
1161 # absolute. Convert all absolute paths to relative.
1162 for i in range(len(file_list or [])):
1163 # It depends on the command being executed (like runhooks vs
1164 # sync).
1165 if not os.path.isabs(file_list[i]):
1166 continue
1167 prefix = os.path.commonprefix(
1168 [self.root.root_dir.lower(), file_list[i].lower()])
1169 file_list[i] = file_list[i][len(prefix):]
1170 # Strip any leading path separators.
1171 while file_list[i].startswith(('\\', '/')):
1172 file_list[i] = file_list[i][1:]
John Budorick0f7b2002018-01-19 15:46:17 -08001173
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001174 # We must check for diffs AFTER any patch_refs have been applied.
1175 if skip_sync_revisions:
1176 skip_sync_rev = skip_sync_revisions.pop(
1177 self.FuzzyMatchUrl(skip_sync_revisions), None)
1178 self._should_sync = (skip_sync_rev is None
1179 or self._used_scm.check_diff(skip_sync_rev,
1180 files=['DEPS']))
1181 if not self._should_sync:
1182 logging.debug(
1183 'Skipping sync for %s. No DEPS changes since last '
1184 'sync at %s' % (self.name, skip_sync_rev))
1185 else:
1186 logging.debug('DEPS changes detected for %s since last sync at '
1187 '%s. Not skipping deps sync' %
1188 (self.name, skip_sync_rev))
Dirk Pranke9f20d022017-10-11 18:36:54 -07001189
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001190 if self.should_recurse:
1191 self.ParseDepsFile()
Corentin Wallez271a78a2020-07-12 15:41:46 +00001192
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001193 self._run_is_done(file_list or [])
Corentin Wallez271a78a2020-07-12 15:41:46 +00001194
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001195 # TODO(crbug.com/1339471): If should_recurse is false, ParseDepsFile
1196 # never gets called meaning we never fetch hooks and dependencies. So
1197 # there's no need to check should_recurse again here.
1198 if self.should_recurse:
1199 if command in ('update', 'revert') and not options.noprehooks:
1200 self.RunPreDepsHooks()
1201 # Parse the dependencies of this dependency.
1202 for s in self.dependencies:
1203 if s.should_process:
1204 work_queue.enqueue(s)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001205
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001206 if command == 'recurse':
1207 # Skip file only checkout.
1208 scm = self.GetScmName()
1209 if not options.scm or scm in options.scm:
1210 cwd = os.path.normpath(
1211 os.path.join(self.root.root_dir, self.name))
1212 # Pass in the SCM type as an env variable. Make sure we don't
1213 # put unicode strings in the environment.
1214 env = os.environ.copy()
1215 if scm:
1216 env['GCLIENT_SCM'] = str(scm)
1217 if self.url:
1218 env['GCLIENT_URL'] = str(self.url)
1219 env['GCLIENT_DEP_PATH'] = str(self.name)
1220 if options.prepend_dir and scm == 'git':
1221 print_stdout = False
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001222
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001223 def filter_fn(line):
1224 """Git-specific path marshaling. It is optimized for git-grep."""
1225 def mod_path(git_pathspec):
1226 match = re.match('^(\\S+?:)?([^\0]+)$',
1227 git_pathspec)
1228 modified_path = os.path.join(
1229 self.name, match.group(2))
1230 branch = match.group(1) or ''
1231 return '%s%s' % (branch, modified_path)
1232
1233 match = re.match('^Binary file ([^\0]+) matches$', line)
1234 if match:
1235 print('Binary file %s matches\n' %
1236 mod_path(match.group(1)))
1237 return
1238
1239 items = line.split('\0')
1240 if len(items) == 2 and items[1]:
1241 print('%s : %s' % (mod_path(items[0]), items[1]))
1242 elif len(items) >= 2:
1243 # Multiple null bytes or a single trailing null byte
1244 # indicate git is likely displaying filenames only
1245 # (such as with -l)
1246 print('\n'.join(
1247 mod_path(path) for path in items if path))
1248 else:
1249 print(line)
1250 else:
1251 print_stdout = True
1252 filter_fn = None
1253
1254 if self.url is None:
1255 print('Skipped omitted dependency %s' % cwd,
1256 file=sys.stderr)
1257 elif os.path.isdir(cwd):
1258 try:
1259 gclient_utils.CheckCallAndFilter(
1260 args,
1261 cwd=cwd,
1262 env=env,
1263 print_stdout=print_stdout,
1264 filter_fn=filter_fn,
1265 )
1266 except subprocess2.CalledProcessError:
1267 if not options.ignore:
1268 raise
1269 else:
1270 print('Skipped missing %s' % cwd, file=sys.stderr)
1271
1272 def GetScmName(self):
1273 raise NotImplementedError()
1274
1275 def CreateSCM(self, out_cb=None):
1276 raise NotImplementedError()
1277
1278 def HasGNArgsFile(self):
1279 return self._gn_args_file is not None
1280
1281 def WriteGNArgsFile(self):
1282 lines = ['# Generated from %r' % self.deps_file]
1283 variables = self.get_vars()
1284 for arg in self._gn_args:
1285 value = variables[arg]
1286 if isinstance(value, gclient_eval.ConstantString):
1287 value = value.value
1288 elif isinstance(value, str):
1289 value = gclient_eval.EvaluateCondition(value, variables)
1290 lines.append('%s = %s' % (arg, ToGNString(value)))
1291
1292 # When use_relative_paths is set, gn_args_file is relative to this DEPS
1293 path_prefix = self.root.root_dir
1294 if self._use_relative_paths:
1295 path_prefix = os.path.join(path_prefix, self.name)
1296
1297 with open(os.path.join(path_prefix, self._gn_args_file), 'wb') as f:
1298 f.write('\n'.join(lines).encode('utf-8', 'replace'))
1299
1300 @gclient_utils.lockedmethod
1301 def _run_is_done(self, file_list):
1302 # Both these are kept for hooks that are run as a separate tree
1303 # traversal.
1304 self._file_list = file_list
1305 self._processed = True
1306
1307 def GetHooks(self, options):
1308 """Evaluates all hooks, and return them in a flat list.
szager@google.comb9a78d32012-03-13 18:46:21 +00001309
1310 RunOnDeps() must have been called before to load the DEPS.
1311 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001312 result = []
1313 if not self.should_process or not self.should_recurse:
1314 # Don't run the hook when it is above recursion_limit.
1315 return result
1316 # If "--force" was specified, run all hooks regardless of what files
1317 # have changed.
1318 if self.deps_hooks:
1319 # TODO(maruel): If the user is using git, then we don't know
1320 # what files have changed so we always run all hooks. It'd be nice
1321 # to fix that.
1322 result.extend(self.deps_hooks)
1323 for s in self.dependencies:
1324 result.extend(s.GetHooks(options))
1325 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001326
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001327 def RunHooksRecursively(self, options, progress):
1328 assert self.hooks_ran == False
1329 self._hooks_ran = True
1330 hooks = self.GetHooks(options)
1331 if progress:
1332 progress._total = len(hooks)
1333 for hook in hooks:
1334 if progress:
1335 progress.update(extra=hook.name or '')
1336 hook.run()
1337 if progress:
1338 progress.end()
maruel@chromium.orgeaf61062010-07-07 18:42:39 +00001339
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001340 def RunPreDepsHooks(self):
1341 assert self.processed
1342 assert self.deps_parsed
1343 assert not self.pre_deps_hooks_ran
1344 assert not self.hooks_ran
1345 for s in self.dependencies:
1346 assert not s.processed
1347 self._pre_deps_hooks_ran = True
1348 for hook in self.pre_deps_hooks:
1349 hook.run()
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001350
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001351 def GetCipdRoot(self):
1352 if self.root is self:
1353 # Let's not infinitely recurse. If this is root and isn't an
1354 # instance of GClient, do nothing.
1355 return None
1356 return self.root.GetCipdRoot()
John Budorickd3ba72b2018-03-20 12:27:42 -07001357
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001358 def subtree(self, include_all):
1359 """Breadth first recursion excluding root node."""
1360 dependencies = self.dependencies
1361 for d in dependencies:
1362 if d.should_process or include_all:
1363 yield d
1364 for d in dependencies:
1365 for i in d.subtree(include_all):
1366 yield i
maruel@chromium.orgad3287e2011-10-03 19:15:10 +00001367
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001368 @gclient_utils.lockedmethod
1369 def add_dependency(self, new_dep):
1370 self._dependencies.append(new_dep)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001371
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001372 @gclient_utils.lockedmethod
1373 def _mark_as_parsed(self, new_hooks):
1374 self._deps_hooks.extend(new_hooks)
1375 self._deps_parsed = True
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001376
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001377 @property
1378 @gclient_utils.lockedmethod
1379 def dependencies(self):
1380 return tuple(self._dependencies)
maruel@chromium.org4bdd5fd2011-09-26 19:41:17 +00001381
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001382 @property
1383 @gclient_utils.lockedmethod
1384 def deps_hooks(self):
1385 return tuple(self._deps_hooks)
maruel@chromium.org064186c2011-09-27 23:53:33 +00001386
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001387 @property
1388 @gclient_utils.lockedmethod
1389 def pre_deps_hooks(self):
1390 return tuple(self._pre_deps_hooks)
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001391
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001392 @property
1393 @gclient_utils.lockedmethod
1394 def deps_parsed(self):
1395 """This is purely for debugging purposes. It's not used anywhere."""
1396 return self._deps_parsed
maruel@chromium.org064186c2011-09-27 23:53:33 +00001397
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001398 @property
1399 @gclient_utils.lockedmethod
1400 def processed(self):
1401 return self._processed
maruel@chromium.org064186c2011-09-27 23:53:33 +00001402
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001403 @property
1404 @gclient_utils.lockedmethod
1405 def pre_deps_hooks_ran(self):
1406 return self._pre_deps_hooks_ran
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001407
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001408 @property
1409 @gclient_utils.lockedmethod
1410 def hooks_ran(self):
1411 return self._hooks_ran
maruel@chromium.org064186c2011-09-27 23:53:33 +00001412
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001413 @property
1414 @gclient_utils.lockedmethod
1415 def allowed_hosts(self):
1416 return self._allowed_hosts
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001417
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001418 @property
1419 @gclient_utils.lockedmethod
1420 def file_list(self):
1421 return tuple(self._file_list)
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001422
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001423 @property
1424 def used_scm(self):
1425 """SCMWrapper instance for this dependency or None if not processed yet."""
1426 return self._used_scm
kustermann@google.coma692e8f2013-04-18 08:32:04 +00001427
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001428 @property
1429 @gclient_utils.lockedmethod
1430 def got_revision(self):
1431 return self._got_revision
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00001432
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001433 @property
1434 def file_list_and_children(self):
1435 result = list(self.file_list)
1436 for d in self.dependencies:
1437 result.extend(d.file_list_and_children)
1438 return tuple(result)
maruel@chromium.org861fd0f2010-07-23 03:05:05 +00001439
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001440 def __str__(self):
1441 out = []
1442 for i in ('name', 'url', 'custom_deps', 'custom_vars', 'deps_hooks',
1443 'file_list', 'should_process', 'processed', 'hooks_ran',
1444 'deps_parsed', 'requirements', 'allowed_hosts'):
1445 # First try the native property if it exists.
1446 if hasattr(self, '_' + i):
1447 value = getattr(self, '_' + i, False)
1448 else:
1449 value = getattr(self, i, False)
1450 if value:
1451 out.append('%s: %s' % (i, value))
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001452
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001453 for d in self.dependencies:
1454 out.extend([' ' + x for x in str(d).splitlines()])
1455 out.append('')
1456 return '\n'.join(out)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001457
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001458 def __repr__(self):
1459 return '%s: %s' % (self.name, self.url)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001460
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001461 def hierarchy(self, include_url=True, graphviz=False):
1462 """Returns a human-readable hierarchical reference to a Dependency."""
1463 def format_name(d):
1464 if include_url:
1465 return '%s(%s)' % (d.name, d.url)
1466 return '"%s"' % d.name # quotes required for graph dot file.
Joanna Wang9144b672023-02-24 23:36:17 +00001467
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001468 out = format_name(self)
1469 i = self.parent
1470 while i and i.name:
1471 out = '%s -> %s' % (format_name(i), out)
1472 if graphviz:
1473 # for graphviz we just need each parent->child relationship
1474 # listed once.
1475 return out
1476 i = i.parent
Joanna Wang9144b672023-02-24 23:36:17 +00001477 return out
maruel@chromium.orgbffb9042010-07-22 20:59:36 +00001478
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001479 def hierarchy_data(self):
1480 """Returns a machine-readable hierarchical reference to a Dependency."""
1481 d = self
1482 out = []
1483 while d and d.name:
1484 out.insert(0, (d.name, d.url))
1485 d = d.parent
1486 return tuple(out)
Michael Mossfe68c912018-03-22 19:19:35 -07001487
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001488 def get_builtin_vars(self):
1489 return {
1490 'checkout_android': 'android' in self.target_os,
1491 'checkout_chromeos': 'chromeos' in self.target_os,
1492 'checkout_fuchsia': 'fuchsia' in self.target_os,
1493 'checkout_ios': 'ios' in self.target_os,
1494 'checkout_linux': 'unix' in self.target_os,
1495 'checkout_mac': 'mac' in self.target_os,
1496 'checkout_win': 'win' in self.target_os,
1497 'host_os': _detect_host_os(),
1498 'checkout_arm': 'arm' in self.target_cpu,
1499 'checkout_arm64': 'arm64' in self.target_cpu,
1500 'checkout_x86': 'x86' in self.target_cpu,
1501 'checkout_mips': 'mips' in self.target_cpu,
1502 'checkout_mips64': 'mips64' in self.target_cpu,
1503 'checkout_ppc': 'ppc' in self.target_cpu,
1504 'checkout_s390': 's390' in self.target_cpu,
1505 'checkout_x64': 'x64' in self.target_cpu,
1506 'host_cpu': detect_host_arch.HostArch(),
1507 }
Robbie Iannucci3db32762023-07-05 19:02:44 +00001508
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001509 def get_vars(self):
1510 """Returns a dictionary of effective variable values
Edward Lemur8f8a50d2018-11-01 22:03:02 +00001511 (DEPS file contents with applied custom_vars overrides)."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001512 # Variable precedence (last has highest):
1513 # - DEPS vars
1514 # - parents, from first to last
1515 # - built-in
1516 # - custom_vars overrides
1517 result = {}
1518 result.update(self._vars)
1519 if self.parent:
1520 merge_vars(result, self.parent.get_vars())
1521 # Provide some built-in variables.
1522 result.update(self.get_builtin_vars())
1523 merge_vars(result, self.custom_vars)
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001524
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001525 return result
Paweł Hajdan, Jrd3790252017-07-03 21:06:24 +02001526
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001527
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001528_PLATFORM_MAPPING = {
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001529 'cygwin': 'win',
1530 'darwin': 'mac',
1531 'linux2': 'linux',
1532 'linux': 'linux',
1533 'win32': 'win',
1534 'aix6': 'aix',
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001535}
1536
1537
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001538def merge_vars(result, new_vars):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001539 for k, v in new_vars.items():
1540 if k in result:
1541 if isinstance(result[k], gclient_eval.ConstantString):
1542 if isinstance(v, gclient_eval.ConstantString):
1543 result[k] = v
1544 else:
1545 result[k].value = v
1546 else:
1547 result[k] = v
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001548 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001549 result[k] = v
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001550
1551
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001552def _detect_host_os():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001553 if sys.platform in _PLATFORM_MAPPING:
1554 return _PLATFORM_MAPPING[sys.platform]
Jonas Termansenbf7eb522023-01-19 17:56:40 +00001555
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001556 try:
1557 return os.uname().sysname.lower()
1558 except AttributeError:
1559 return sys.platform
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001560
1561
Edward Lemurb61d3872018-05-09 18:42:47 -04001562class GitDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001563 """A Dependency object that represents a single git checkout."""
1564 @staticmethod
1565 def updateProtocol(url, protocol):
1566 """Updates given URL's protocol"""
1567 # only works on urls, skips local paths
1568 if not url or not protocol or not re.match('([a-z]+)://', url) or \
1569 re.match('file://', url):
1570 return url
Edward Lemurb61d3872018-05-09 18:42:47 -04001571
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001572 return re.sub('^([a-z]+):', protocol + ':', url)
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +00001573
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001574 #override
1575 def GetScmName(self):
1576 """Always 'git'."""
1577 return 'git'
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +00001578
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001579 #override
1580 def CreateSCM(self, out_cb=None):
1581 """Create a Wrapper instance suitable for handling this git dependency."""
1582 return gclient_scm.GitWrapper(self.url,
1583 self.root.root_dir,
1584 self.name,
1585 self.outbuf,
1586 out_cb,
1587 print_outbuf=self.print_outbuf)
Edward Lemurb61d3872018-05-09 18:42:47 -04001588
1589
1590class GClient(GitDependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001591 """Object that represent a gclient checkout. A tree of Dependency(), one per
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001592 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001593
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001594 DEPS_OS_CHOICES = {
1595 "aix6": "unix",
1596 "win32": "win",
1597 "win": "win",
1598 "cygwin": "win",
1599 "darwin": "mac",
1600 "mac": "mac",
1601 "unix": "unix",
1602 "linux": "unix",
1603 "linux2": "unix",
1604 "linux3": "unix",
1605 "android": "android",
1606 "ios": "ios",
1607 "fuchsia": "fuchsia",
1608 "chromeos": "chromeos",
1609 }
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001610
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001611 DEFAULT_CLIENT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001612solutions = [
Edward Lesmes05934952019-12-19 20:38:09 +00001613 { "name" : %(solution_name)r,
1614 "url" : %(solution_url)r,
1615 "deps_file" : %(deps_file)r,
1616 "managed" : %(managed)r,
smutae7ea312016-07-18 11:59:41 -07001617 "custom_deps" : {
1618 },
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02001619 "custom_vars": %(custom_vars)r,
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001620 },
1621]
Robert Iannuccia19649b2018-06-29 16:31:45 +00001622""")
1623
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001624 DEFAULT_CLIENT_CACHE_DIR_TEXT = ("""\
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001625cache_dir = %(cache_dir)r
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001626""")
1627
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001628 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001629# Snapshot generated with gclient revinfo --snapshot
Edward Lesmesc2960242018-03-06 20:50:15 -05001630solutions = %(solution_list)s
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001631""")
1632
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001633 def __init__(self, root_dir, options):
1634 # Do not change previous behavior. Only solution level and immediate
1635 # DEPS are processed.
1636 self._recursion_limit = 2
1637 super(GClient, self).__init__(parent=None,
1638 name=None,
1639 url=None,
1640 managed=True,
1641 custom_deps=None,
1642 custom_vars=None,
1643 custom_hooks=None,
1644 deps_file='unused',
1645 should_process=True,
1646 should_recurse=True,
1647 relative=None,
1648 condition=None,
1649 print_outbuf=True)
Edward Lemure05f18d2018-06-08 17:36:53 +00001650
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001651 self._options = options
1652 if options.deps_os:
1653 enforced_os = options.deps_os.split(',')
1654 else:
1655 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
1656 if 'all' in enforced_os:
1657 enforced_os = self.DEPS_OS_CHOICES.values()
1658 self._enforced_os = tuple(set(enforced_os))
1659 self._enforced_cpu = (detect_host_arch.HostArch(), )
1660 self._root_dir = root_dir
1661 self._cipd_root = None
1662 self.config_content = None
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001663
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001664 def _CheckConfig(self):
1665 """Verify that the config matches the state of the existing checked-out
borenet@google.com88d10082014-03-21 17:24:48 +00001666 solutions."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001667 for dep in self.dependencies:
1668 if dep.managed and dep.url:
1669 scm = dep.CreateSCM()
1670 actual_url = scm.GetActualRemoteURL(self._options)
1671 if actual_url and not scm.DoesRemoteURLMatch(self._options):
1672 mirror = scm.GetCacheMirror()
1673 if mirror:
1674 mirror_string = '%s (exists=%s)' % (mirror.mirror_path,
1675 mirror.exists())
1676 else:
1677 mirror_string = 'not used'
1678 raise gclient_utils.Error(
1679 '''
borenet@google.com88d10082014-03-21 17:24:48 +00001680Your .gclient file seems to be broken. The requested URL is different from what
borenet@google.com0a427372014-04-02 19:12:13 +00001681is actually checked out in %(checkout_path)s.
borenet@google.com88d10082014-03-21 17:24:48 +00001682
borenet@google.com97882362014-04-07 20:06:02 +00001683The .gclient file contains:
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +00001684URL: %(expected_url)s (%(expected_scm)s)
1685Cache mirror: %(mirror_string)s
borenet@google.com97882362014-04-07 20:06:02 +00001686
1687The local checkout in %(checkout_path)s reports:
1688%(actual_url)s (%(actual_scm)s)
borenet@google.com88d10082014-03-21 17:24:48 +00001689
1690You should ensure that the URL listed in .gclient is correct and either change
agabled437d762016-10-17 09:35:11 -07001691it or fix the checkout.
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00001692''' % {
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001693 'checkout_path': os.path.join(
1694 self.root_dir, dep.name),
1695 'expected_url': dep.url,
1696 'expected_scm': dep.GetScmName(),
1697 'mirror_string': mirror_string,
1698 'actual_url': actual_url,
1699 'actual_scm': dep.GetScmName()
1700 })
borenet@google.com88d10082014-03-21 17:24:48 +00001701
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001702 def SetConfig(self, content):
1703 assert not self.dependencies
1704 config_dict = {}
1705 self.config_content = content
1706 try:
1707 exec(content, config_dict)
1708 except SyntaxError as e:
1709 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001710
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001711 # Append any target OS that is not already being enforced to the tuple.
1712 target_os = config_dict.get('target_os', [])
1713 if config_dict.get('target_os_only', False):
1714 self._enforced_os = tuple(set(target_os))
1715 else:
1716 self._enforced_os = tuple(set(self._enforced_os).union(target_os))
stuartmorgan@chromium.org18a4f6a2012-11-02 13:37:24 +00001717
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001718 # Append any target CPU that is not already being enforced to the tuple.
1719 target_cpu = config_dict.get('target_cpu', [])
1720 if config_dict.get('target_cpu_only', False):
1721 self._enforced_cpu = tuple(set(target_cpu))
1722 else:
1723 self._enforced_cpu = tuple(
1724 set(self._enforced_cpu).union(target_cpu))
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001725
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001726 cache_dir = config_dict.get('cache_dir', UNSET_CACHE_DIR)
1727 if cache_dir is not UNSET_CACHE_DIR:
1728 if cache_dir:
1729 cache_dir = os.path.join(self.root_dir, cache_dir)
1730 cache_dir = os.path.abspath(cache_dir)
Andrii Shyshkalov77ce4bd2017-11-27 12:38:18 -08001731
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001732 git_cache.Mirror.SetCachePath(cache_dir)
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001733
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001734 if not target_os and config_dict.get('target_os_only', False):
1735 raise gclient_utils.Error(
1736 'Can\'t use target_os_only if target_os is '
1737 'not specified')
peter@chromium.org1efccc82012-04-27 16:34:38 +00001738
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001739 if not target_cpu and config_dict.get('target_cpu_only', False):
1740 raise gclient_utils.Error(
1741 'Can\'t use target_cpu_only if target_cpu is '
1742 'not specified')
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001743
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001744 deps_to_add = []
1745 for s in config_dict.get('solutions', []):
1746 try:
1747 deps_to_add.append(
1748 GitDependency(
1749 parent=self,
1750 name=s['name'],
1751 # Update URL with scheme in protocol_override
1752 url=GitDependency.updateProtocol(
1753 s['url'], s.get('protocol_override', None)),
1754 managed=s.get('managed', True),
1755 custom_deps=s.get('custom_deps', {}),
1756 custom_vars=s.get('custom_vars', {}),
1757 custom_hooks=s.get('custom_hooks', []),
1758 deps_file=s.get('deps_file', 'DEPS'),
1759 should_process=True,
1760 should_recurse=True,
1761 relative=None,
1762 condition=None,
1763 print_outbuf=True,
1764 # Pass protocol_override down the tree for child deps to
1765 # use.
1766 protocol=s.get('protocol_override', None),
1767 git_dependencies_state=self.git_dependencies_state))
1768 except KeyError:
1769 raise gclient_utils.Error('Invalid .gclient file. Solution is '
1770 'incomplete: %s' % s)
1771 metrics.collector.add('project_urls', [
Edward Lemuraffd4102019-06-05 18:07:49 +00001772 dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Edward Lemur40764b02018-07-20 18:50:29 +00001773 for dep in deps_to_add
1774 if dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001775 ])
Edward Lemur40764b02018-07-20 18:50:29 +00001776
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001777 self.add_dependencies_and_close(deps_to_add,
1778 config_dict.get('hooks', []))
1779 logging.info('SetConfig() done')
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001780
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001781 def SaveConfig(self):
1782 gclient_utils.FileWrite(
1783 os.path.join(self.root_dir, self._options.config_filename),
1784 self.config_content)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001785
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001786 @staticmethod
1787 def LoadCurrentConfig(options):
1788 # type: (optparse.Values) -> GClient
1789 """Searches for and loads a .gclient file relative to the current working
Joanna Wang66286612022-06-30 19:59:13 +00001790 dir."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001791 if options.spec:
1792 client = GClient('.', options)
1793 client.SetConfig(options.spec)
1794 else:
1795 if options.verbose:
1796 print('Looking for %s starting from %s\n' %
1797 (options.config_filename, os.getcwd()))
1798 path = gclient_paths.FindGclientRoot(os.getcwd(),
1799 options.config_filename)
1800 if not path:
1801 if options.verbose:
1802 print('Couldn\'t find configuration file.')
1803 return None
1804 client = GClient(path, options)
1805 client.SetConfig(
1806 gclient_utils.FileRead(
1807 os.path.join(path, options.config_filename)))
maruel@chromium.org69392e72011-10-13 22:09:00 +00001808
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001809 if (options.revisions and len(client.dependencies) > 1
1810 and any('@' not in r for r in options.revisions)):
1811 print((
1812 'You must specify the full solution name like --revision %s@%s\n'
1813 'when you have multiple solutions setup in your .gclient file.\n'
1814 'Other solutions present are: %s.') %
1815 (client.dependencies[0].name, options.revisions[0], ', '.join(
1816 s.name for s in client.dependencies[1:])),
1817 file=sys.stderr)
Joanna Wang66286612022-06-30 19:59:13 +00001818
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001819 return client
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001820
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001821 def SetDefaultConfig(self,
1822 solution_name,
1823 deps_file,
1824 solution_url,
1825 managed=True,
1826 cache_dir=UNSET_CACHE_DIR,
1827 custom_vars=None):
1828 text = self.DEFAULT_CLIENT_FILE_TEXT
1829 format_dict = {
1830 'solution_name': solution_name,
1831 'solution_url': solution_url,
1832 'deps_file': deps_file,
1833 'managed': managed,
1834 'custom_vars': custom_vars or {},
1835 }
Robert Iannuccia19649b2018-06-29 16:31:45 +00001836
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001837 if cache_dir is not UNSET_CACHE_DIR:
1838 text += self.DEFAULT_CLIENT_CACHE_DIR_TEXT
1839 format_dict['cache_dir'] = cache_dir
Robert Iannuccia19649b2018-06-29 16:31:45 +00001840
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001841 self.SetConfig(text % format_dict)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001842
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001843 def _SaveEntries(self):
1844 """Creates a .gclient_entries file to record the list of unique checkouts.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001845
1846 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001847 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001848 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
1849 # makes testing a bit too fun.
1850 result = 'entries = {\n'
1851 for entry in self.root.subtree(False):
1852 result += ' %s: %s,\n' % (pprint.pformat(
1853 entry.name), pprint.pformat(entry.url))
1854 result += '}\n'
1855 file_path = os.path.join(self.root_dir, self._options.entries_filename)
1856 logging.debug(result)
1857 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001858
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001859 def _ReadEntries(self):
1860 """Read the .gclient_entries file for the given client.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001861
1862 Returns:
1863 A sequence of solution names, which will be empty if there is the
1864 entries file hasn't been created yet.
1865 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001866 scope = {}
1867 filename = os.path.join(self.root_dir, self._options.entries_filename)
1868 if not os.path.exists(filename):
1869 return {}
1870 try:
1871 exec(gclient_utils.FileRead(filename), scope)
1872 except SyntaxError as e:
1873 gclient_utils.SyntaxErrorToError(filename, e)
1874 return scope.get('entries', {})
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001875
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001876 def _ExtractFileJsonContents(self, default_filename):
1877 # type: (str) -> Mapping[str,Any]
1878 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001879
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001880 if not os.path.exists(f):
1881 logging.info('File %s does not exist.' % f)
1882 return {}
Joanna Wang01870792022-08-01 19:02:57 +00001883
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001884 with open(f, 'r') as open_f:
1885 logging.info('Reading content from file %s' % f)
1886 content = open_f.read().rstrip()
1887 if content:
1888 return json.loads(content)
Joanna Wang66286612022-06-30 19:59:13 +00001889 return {}
1890
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001891 def _WriteFileContents(self, default_filename, content):
1892 # type: (str, str) -> None
1893 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001894
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001895 with open(f, 'w') as open_f:
1896 logging.info('Writing to file %s' % f)
1897 open_f.write(content)
Joanna Wangf3edc502022-07-20 00:12:10 +00001898
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001899 def _EnforceSkipSyncRevisions(self, patch_refs):
1900 # type: (Mapping[str, str]) -> Mapping[str, str]
1901 """Checks for and enforces revisions for skipping deps syncing."""
1902 previous_sync_commits = self._ExtractFileJsonContents(
1903 PREVIOUS_SYNC_COMMITS_FILE)
Joanna Wang66286612022-06-30 19:59:13 +00001904
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001905 if not previous_sync_commits:
1906 return {}
maruel@chromium.org918a9ae2010-05-28 15:50:30 +00001907
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001908 # Current `self.dependencies` only contain solutions. If a patch_ref is
1909 # not for a solution, then it is for a solution's dependency or recursed
1910 # dependency which we cannot support while skipping sync.
1911 if patch_refs:
1912 unclaimed_prs = []
1913 candidates = []
1914 for dep in self.dependencies:
1915 origin, _ = gclient_utils.SplitUrlRevision(dep.url)
1916 candidates.extend([origin, dep.name])
1917 for patch_repo in patch_refs:
1918 if not gclient_utils.FuzzyMatchRepo(patch_repo, candidates):
1919 unclaimed_prs.append(patch_repo)
1920 if unclaimed_prs:
1921 print(
1922 'We cannot skip syncs when there are --patch-refs flags for '
1923 'non-solution dependencies. To skip syncing, remove patch_refs '
1924 'for: \n%s' % '\n'.join(unclaimed_prs))
1925 return {}
Edward Lesmesc621b212018-03-21 20:26:56 -04001926
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001927 # We cannot skip syncing if there are custom_vars that differ from the
1928 # previous run's custom_vars.
1929 previous_custom_vars = self._ExtractFileJsonContents(
1930 PREVIOUS_CUSTOM_VARS_FILE)
1931
1932 cvs_by_name = {s.name: s.custom_vars for s in self.dependencies}
1933
1934 skip_sync_revisions = {}
1935 for name, commit in previous_sync_commits.items():
1936 previous_vars = previous_custom_vars.get(name)
1937 if previous_vars == cvs_by_name.get(name) or (
1938 not previous_vars and not cvs_by_name.get(name)):
1939 skip_sync_revisions[name] = commit
1940 else:
1941 print(
1942 'We cannot skip syncs when custom_vars for solutions have '
1943 'changed since the last sync run on this machine.\n'
1944 '\nRemoving skip_sync_revision for:\n'
1945 'solution: %s, current: %r, previous: %r.' %
1946 (name, cvs_by_name.get(name), previous_vars))
1947 print('no-sync experiment enabled with %r' % skip_sync_revisions)
1948 return skip_sync_revisions
1949
1950 # TODO(crbug.com/1340695): Remove handling revisions without '@'.
1951 def _EnforceRevisions(self):
1952 """Checks for revision overrides."""
1953 revision_overrides = {}
1954 if self._options.head:
1955 return revision_overrides
1956 if not self._options.revisions:
1957 return revision_overrides
1958 solutions_names = [s.name for s in self.dependencies]
1959 for index, revision in enumerate(self._options.revisions):
1960 if not '@' in revision:
1961 # Support for --revision 123
1962 revision = '%s@%s' % (solutions_names[index], revision)
1963 name, rev = revision.split('@', 1)
1964 revision_overrides[name] = rev
1965 return revision_overrides
1966
1967 def _EnforcePatchRefsAndBranches(self):
1968 # type: () -> Tuple[Mapping[str, str], Mapping[str, str]]
1969 """Checks for patch refs."""
1970 patch_refs = {}
1971 target_branches = {}
1972 if not self._options.patch_refs:
1973 return patch_refs, target_branches
1974 for given_patch_ref in self._options.patch_refs:
1975 patch_repo, _, patch_ref = given_patch_ref.partition('@')
1976 if not patch_repo or not patch_ref or ':' not in patch_ref:
1977 raise gclient_utils.Error(
1978 'Wrong revision format: %s should be of the form '
1979 'patch_repo@target_branch:patch_ref.' % given_patch_ref)
1980 target_branch, _, patch_ref = patch_ref.partition(':')
1981 target_branches[patch_repo] = target_branch
1982 patch_refs[patch_repo] = patch_ref
1983 return patch_refs, target_branches
1984
1985 def _RemoveUnversionedGitDirs(self):
1986 """Remove directories that are no longer part of the checkout.
Edward Lemur5b1fa942018-10-04 23:22:09 +00001987
1988 Notify the user if there is an orphaned entry in their working copy.
1989 Only delete the directory if there are no changes in it, and
1990 delete_unversioned_trees is set to true.
Josip Sokcevic1b8211f2022-09-30 17:46:53 +00001991
1992 Returns CIPD packages that are no longer versioned.
Edward Lemur5b1fa942018-10-04 23:22:09 +00001993 """
1994
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001995 entry_names_and_sync = [(i.name, i._should_sync)
1996 for i in self.root.subtree(False) if i.url]
1997 entries = []
1998 if entry_names_and_sync:
1999 entries, _ = zip(*entry_names_and_sync)
2000 full_entries = [
2001 os.path.join(self.root_dir, e.replace('/', os.path.sep))
2002 for e in entries
2003 ]
2004 no_sync_entries = [
2005 name for name, should_sync in entry_names_and_sync
2006 if not should_sync
2007 ]
Edward Lemur5b1fa942018-10-04 23:22:09 +00002008
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002009 removed_cipd_entries = []
2010 for entry, prev_url in self._ReadEntries().items():
2011 if not prev_url:
2012 # entry must have been overridden via .gclient custom_deps
2013 continue
2014 if any(entry.startswith(sln) for sln in no_sync_entries):
2015 # Dependencies of solutions that skipped syncing would not
2016 # show up in `entries`.
2017 continue
2018 if (':' in entry):
2019 # This is a cipd package. Don't clean it up, but prepare for
2020 # return
2021 if entry not in entries:
2022 removed_cipd_entries.append(entry)
2023 continue
2024 # Fix path separator on Windows.
2025 entry_fixed = entry.replace('/', os.path.sep)
2026 e_dir = os.path.join(self.root_dir, entry_fixed)
2027 # Use entry and not entry_fixed there.
2028 if (entry not in entries and
2029 (not any(path.startswith(entry + '/') for path in entries))
2030 and os.path.exists(e_dir)):
2031 # The entry has been removed from DEPS.
2032 scm = gclient_scm.GitWrapper(prev_url, self.root_dir,
2033 entry_fixed, self.outbuf)
Edward Lemur5b1fa942018-10-04 23:22:09 +00002034
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002035 # Check to see if this directory is now part of a higher-up
2036 # checkout.
2037 scm_root = None
2038 try:
2039 scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2040 scm.checkout_path)
2041 except subprocess2.CalledProcessError:
2042 pass
2043 if not scm_root:
2044 logging.warning(
2045 'Could not find checkout root for %s. Unable to '
2046 'determine whether it is part of a higher-level '
2047 'checkout, so not removing.' % entry)
2048 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002049
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002050 # This is to handle the case of third_party/WebKit migrating
2051 # from being a DEPS entry to being part of the main project. If
2052 # the subproject is a Git project, we need to remove its .git
2053 # folder. Otherwise git operations on that folder will have
2054 # different effects depending on the current working directory.
2055 if os.path.abspath(scm_root) == os.path.abspath(e_dir):
2056 e_par_dir = os.path.join(e_dir, os.pardir)
2057 if gclient_scm.scm.GIT.IsInsideWorkTree(e_par_dir):
2058 par_scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2059 e_par_dir)
2060 # rel_e_dir : relative path of entry w.r.t. its parent
2061 # repo.
2062 rel_e_dir = os.path.relpath(e_dir, par_scm_root)
2063 if gclient_scm.scm.GIT.IsDirectoryVersioned(
2064 par_scm_root, rel_e_dir):
2065 save_dir = scm.GetGitBackupDirPath()
2066 # Remove any eventual stale backup dir for the same
2067 # project.
2068 if os.path.exists(save_dir):
2069 gclient_utils.rmtree(save_dir)
2070 os.rename(os.path.join(e_dir, '.git'), save_dir)
2071 # When switching between the two states (entry/ is a
2072 # subproject -> entry/ is part of the outer
2073 # project), it is very likely that some files are
2074 # changed in the checkout, unless we are jumping
2075 # *exactly* across the commit which changed just
2076 # DEPS. In such case we want to cleanup any eventual
2077 # stale files (coming from the old subproject) in
2078 # order to end up with a clean checkout.
2079 gclient_scm.scm.GIT.CleanupDir(
2080 par_scm_root, rel_e_dir)
2081 assert not os.path.exists(
2082 os.path.join(e_dir, '.git'))
2083 print(
2084 '\nWARNING: \'%s\' has been moved from DEPS to a higher '
2085 'level checkout. The git folder containing all the local'
2086 ' branches has been saved to %s.\n'
2087 'If you don\'t care about its state you can safely '
2088 'remove that folder to free up space.' %
2089 (entry, save_dir))
2090 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002091
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002092 if scm_root in full_entries:
2093 logging.info(
2094 '%s is part of a higher level checkout, not removing',
2095 scm.GetCheckoutRoot())
2096 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002097
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002098 file_list = []
2099 scm.status(self._options, [], file_list)
2100 modified_files = file_list != []
2101 if (not self._options.delete_unversioned_trees
2102 or (modified_files and not self._options.force)):
2103 # There are modified files in this entry. Keep warning until
2104 # removed.
2105 self.add_dependency(
2106 GitDependency(
2107 parent=self,
2108 name=entry,
2109 # Update URL with scheme in protocol_override
2110 url=GitDependency.updateProtocol(
2111 prev_url, self.protocol),
2112 managed=False,
2113 custom_deps={},
2114 custom_vars={},
2115 custom_hooks=[],
2116 deps_file=None,
2117 should_process=True,
2118 should_recurse=False,
2119 relative=None,
2120 condition=None,
2121 protocol=self.protocol,
2122 git_dependencies_state=self.git_dependencies_state))
2123 if modified_files and self._options.delete_unversioned_trees:
2124 print(
2125 '\nWARNING: \'%s\' is no longer part of this client.\n'
2126 'Despite running \'gclient sync -D\' no action was taken '
2127 'as there are modifications.\nIt is recommended you revert '
2128 'all changes or run \'gclient sync -D --force\' next '
2129 'time.' % entry_fixed)
2130 else:
2131 print(
2132 '\nWARNING: \'%s\' is no longer part of this client.\n'
2133 'It is recommended that you manually remove it or use '
2134 '\'gclient sync -D\' next time.' % entry_fixed)
2135 else:
2136 # Delete the entry
2137 print('\n________ deleting \'%s\' in \'%s\'' %
2138 (entry_fixed, self.root_dir))
2139 gclient_utils.rmtree(e_dir)
2140 # record the current list of entries for next time
2141 self._SaveEntries()
2142 return removed_cipd_entries
Edward Lemur5b1fa942018-10-04 23:22:09 +00002143
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002144 def RunOnDeps(self,
2145 command,
2146 args,
2147 ignore_requirements=False,
2148 progress=True):
2149 """Runs a command on each dependency in a client and its dependencies.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002150
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002151 Args:
2152 command: The command to use (e.g., 'status' or 'diff')
2153 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002154 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002155 if not self.dependencies:
2156 raise gclient_utils.Error('No solution specified')
Michael Mossd683d7c2018-06-15 05:05:17 +00002157
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002158 revision_overrides = {}
2159 patch_refs = {}
2160 target_branches = {}
2161 skip_sync_revisions = {}
2162 # It's unnecessary to check for revision overrides for 'recurse'.
2163 # Save a few seconds by not calling _EnforceRevisions() in that case.
2164 if command not in ('diff', 'recurse', 'runhooks', 'status', 'revert',
2165 'validate'):
2166 self._CheckConfig()
2167 revision_overrides = self._EnforceRevisions()
Edward Lesmesc621b212018-03-21 20:26:56 -04002168
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002169 if command == 'update':
2170 patch_refs, target_branches = self._EnforcePatchRefsAndBranches()
2171 if NO_SYNC_EXPERIMENT in self._options.experiments:
2172 skip_sync_revisions = self._EnforceSkipSyncRevisions(patch_refs)
Joanna Wang66286612022-06-30 19:59:13 +00002173
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002174 # Store solutions' custom_vars on memory to compare in the next run.
2175 # All dependencies added later are inherited from the current
2176 # self.dependencies.
2177 custom_vars = {
2178 dep.name: dep.custom_vars
2179 for dep in self.dependencies if dep.custom_vars
Michael Mossd683d7c2018-06-15 05:05:17 +00002180 }
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002181 if custom_vars:
2182 self._WriteFileContents(PREVIOUS_CUSTOM_VARS_FILE,
2183 json.dumps(custom_vars))
2184
2185 # Disable progress for non-tty stdout.
2186 should_show_progress = (setup_color.IS_TTY and not self._options.verbose
2187 and progress)
2188 pm = None
2189 if should_show_progress:
2190 if command in ('update', 'revert'):
2191 pm = Progress('Syncing projects', 1)
2192 elif command in ('recurse', 'validate'):
2193 pm = Progress(' '.join(args), 1)
2194 work_queue = gclient_utils.ExecutionQueue(
2195 self._options.jobs,
2196 pm,
2197 ignore_requirements=ignore_requirements,
2198 verbose=self._options.verbose)
2199 for s in self.dependencies:
2200 if s.should_process:
2201 work_queue.enqueue(s)
2202 work_queue.flush(revision_overrides,
2203 command,
2204 args,
2205 options=self._options,
2206 patch_refs=patch_refs,
2207 target_branches=target_branches,
2208 skip_sync_revisions=skip_sync_revisions)
2209
2210 if revision_overrides:
2211 print(
2212 'Please fix your script, having invalid --revision flags will soon '
2213 'be considered an error.',
2214 file=sys.stderr)
2215
2216 if patch_refs:
2217 raise gclient_utils.Error(
2218 'The following --patch-ref flags were not used. Please fix it:\n%s'
2219 % ('\n'.join(patch_repo + '@' + patch_ref
2220 for patch_repo, patch_ref in patch_refs.items())))
2221
2222 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
2223 # they have fsmonitor enabled.
2224 if command == 'update':
2225 # Check if any of the root dependency have submodules.
2226 is_submoduled = any(
2227 map(
2228 lambda d: d.git_dependencies_state in
2229 (gclient_eval.SUBMODULES, gclient_eval.SYNC),
2230 self.dependencies))
2231 if is_submoduled:
2232 git_common.warn_submodule()
2233
2234 # Once all the dependencies have been processed, it's now safe to write
2235 # out the gn_args_file and run the hooks.
2236 removed_cipd_entries = []
2237 if command == 'update':
2238 for dependency in self.dependencies:
2239 gn_args_dep = dependency
2240 if gn_args_dep._gn_args_from:
2241 deps_map = {
2242 dep.name: dep
2243 for dep in gn_args_dep.dependencies
2244 }
2245 gn_args_dep = deps_map.get(gn_args_dep._gn_args_from)
2246 if gn_args_dep and gn_args_dep.HasGNArgsFile():
2247 gn_args_dep.WriteGNArgsFile()
2248
2249 removed_cipd_entries = self._RemoveUnversionedGitDirs()
2250
2251 # Sync CIPD dependencies once removed deps are deleted. In case a git
2252 # dependency was moved to CIPD, we want to remove the old git directory
2253 # first and then sync the CIPD dep.
2254 if self._cipd_root:
2255 self._cipd_root.run(command)
2256 # It's possible that CIPD removed some entries that are now part of
2257 # git worktree. Try to checkout those directories
2258 if removed_cipd_entries:
2259 for cipd_entry in removed_cipd_entries:
2260 cwd = os.path.join(self._root_dir, cipd_entry.split(':')[0])
2261 cwd, tail = os.path.split(cwd)
2262 if cwd:
2263 try:
2264 gclient_scm.scm.GIT.Capture(['checkout', tail],
2265 cwd=cwd)
2266 except subprocess2.CalledProcessError:
2267 pass
2268
2269 if not self._options.nohooks:
2270 if should_show_progress:
2271 pm = Progress('Running hooks', 1)
2272 self.RunHooksRecursively(self._options, pm)
2273
2274 self._WriteFileContents(PREVIOUS_SYNC_COMMITS_FILE,
2275 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
2276
2277 return 0
2278
2279 def PrintRevInfo(self):
2280 if not self.dependencies:
2281 raise gclient_utils.Error('No solution specified')
2282 # Load all the settings.
2283 work_queue = gclient_utils.ExecutionQueue(self._options.jobs,
2284 None,
2285 False,
2286 verbose=self._options.verbose)
2287 for s in self.dependencies:
2288 if s.should_process:
2289 work_queue.enqueue(s)
2290 work_queue.flush({},
2291 None, [],
2292 options=self._options,
2293 patch_refs=None,
2294 target_branches=None,
2295 skip_sync_revisions=None)
2296
2297 def ShouldPrintRevision(dep):
2298 return (not self._options.filter
2299 or dep.FuzzyMatchUrl(self._options.filter))
2300
2301 if self._options.snapshot:
2302 json_output = []
2303 # First level at .gclient
2304 for d in self.dependencies:
2305 entries = {}
2306
2307 def GrabDeps(dep):
2308 """Recursively grab dependencies."""
2309 for rec_d in dep.dependencies:
2310 rec_d.PinToActualRevision()
2311 if ShouldPrintRevision(rec_d):
2312 entries[rec_d.name] = rec_d.url
2313 GrabDeps(rec_d)
2314
2315 GrabDeps(d)
2316 json_output.append({
2317 'name': d.name,
2318 'solution_url': d.url,
2319 'deps_file': d.deps_file,
2320 'managed': d.managed,
2321 'custom_deps': entries,
2322 })
2323 if self._options.output_json == '-':
2324 print(json.dumps(json_output, indent=2, separators=(',', ': ')))
2325 elif self._options.output_json:
2326 with open(self._options.output_json, 'w') as f:
2327 json.dump(json_output, f)
2328 else:
2329 # Print the snapshot configuration file
2330 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {
2331 'solution_list': pprint.pformat(json_output, indent=2),
2332 })
Michael Mossd683d7c2018-06-15 05:05:17 +00002333 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002334 entries = {}
2335 for d in self.root.subtree(False):
2336 if self._options.actual:
2337 d.PinToActualRevision()
2338 if ShouldPrintRevision(d):
2339 entries[d.name] = d.url
2340 if self._options.output_json:
2341 json_output = {
2342 name: {
2343 'url': rev.split('@')[0] if rev else None,
2344 'rev':
2345 rev.split('@')[1] if rev and '@' in rev else None,
2346 }
2347 for name, rev in entries.items()
2348 }
2349 if self._options.output_json == '-':
2350 print(
2351 json.dumps(json_output,
2352 indent=2,
2353 separators=(',', ': ')))
2354 else:
2355 with open(self._options.output_json, 'w') as f:
2356 json.dump(json_output, f)
2357 else:
2358 keys = sorted(entries.keys())
2359 for x in keys:
2360 print('%s: %s' % (x, entries[x]))
2361 logging.info(str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002362
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002363 def ParseDepsFile(self):
2364 """No DEPS to parse for a .gclient file."""
2365 raise gclient_utils.Error('Internal error')
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00002366
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002367 def PrintLocationAndContents(self):
2368 # Print out the .gclient file. This is longer than if we just printed
2369 # the client dict, but more legible, and it might contain helpful
2370 # comments.
2371 print('Loaded .gclient config in %s:\n%s' %
2372 (self.root_dir, self.config_content))
sergiyb@chromium.orgfa2707e2016-03-12 00:40:56 +00002373
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002374 def GetCipdRoot(self):
2375 if not self._cipd_root:
2376 self._cipd_root = gclient_scm.CipdRoot(
2377 self.root_dir,
2378 # TODO(jbudorick): Support other service URLs as necessary.
2379 # Service URLs should be constant over the scope of a cipd
2380 # root, so a var per DEPS file specifying the service URL
2381 # should suffice.
Yiwei Zhang52353702023-09-18 15:53:52 +00002382 'https://chrome-infra-packages.appspot.com',
2383 log_level='info' if self._options.verbose else None)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002384 return self._cipd_root
John Budorickd3ba72b2018-03-20 12:27:42 -07002385
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002386 @property
2387 def root_dir(self):
2388 """Root directory of gclient checkout."""
2389 return self._root_dir
maruel@chromium.org75a59272010-06-11 22:34:03 +00002390
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002391 @property
2392 def enforced_os(self):
2393 """What deps_os entries that are to be parsed."""
2394 return self._enforced_os
maruel@chromium.org271375b2010-06-23 19:17:38 +00002395
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002396 @property
2397 def target_os(self):
2398 return self._enforced_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +00002399
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002400 @property
2401 def target_cpu(self):
2402 return self._enforced_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -08002403
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002404
John Budorick0f7b2002018-01-19 15:46:17 -08002405class CipdDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002406 """A Dependency object that represents a single CIPD package."""
2407 def __init__(self, parent, name, dep_value, cipd_root, custom_vars,
2408 should_process, relative, condition):
2409 package = dep_value['package']
2410 version = dep_value['version']
2411 url = urllib.parse.urljoin(cipd_root.service_url,
2412 '%s@%s' % (package, version))
2413 super(CipdDependency, self).__init__(parent=parent,
2414 name=name + ':' + package,
2415 url=url,
2416 managed=None,
2417 custom_deps=None,
2418 custom_vars=custom_vars,
2419 custom_hooks=None,
2420 deps_file=None,
2421 should_process=should_process,
2422 should_recurse=False,
2423 relative=relative,
2424 condition=condition)
2425 self._cipd_package = None
2426 self._cipd_root = cipd_root
2427 # CIPD wants /-separated paths, even on Windows.
2428 native_subdir_path = os.path.relpath(
2429 os.path.join(self.root.root_dir, name), cipd_root.root_dir)
2430 self._cipd_subdir = posixpath.join(*native_subdir_path.split(os.sep))
2431 self._package_name = package
2432 self._package_version = version
John Budorick0f7b2002018-01-19 15:46:17 -08002433
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002434 #override
2435 def run(self, revision_overrides, command, args, work_queue, options,
2436 patch_refs, target_branches, skip_sync_revisions):
2437 """Runs |command| then parse the DEPS file."""
2438 logging.info('CipdDependency(%s).run()' % self.name)
2439 if not self.should_process:
2440 return
2441 self._CreatePackageIfNecessary()
2442 super(CipdDependency,
2443 self).run(revision_overrides, command, args, work_queue, options,
2444 patch_refs, target_branches, skip_sync_revisions)
John Budorickd3ba72b2018-03-20 12:27:42 -07002445
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002446 def _CreatePackageIfNecessary(self):
2447 # We lazily create the CIPD package to make sure that only packages
2448 # that we want (as opposed to all packages defined in all DEPS files
2449 # we parse) get added to the root and subsequently ensured.
2450 if not self._cipd_package:
2451 self._cipd_package = self._cipd_root.add_package(
2452 self._cipd_subdir, self._package_name, self._package_version)
John Budorickd3ba72b2018-03-20 12:27:42 -07002453
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002454 def ParseDepsFile(self):
2455 """CIPD dependencies are not currently allowed to have nested deps."""
2456 self.add_dependencies_and_close([], [])
John Budorick0f7b2002018-01-19 15:46:17 -08002457
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002458 #override
2459 def verify_validity(self):
2460 """CIPD dependencies allow duplicate name for packages in same directory."""
2461 logging.info('Dependency(%s).verify_validity()' % self.name)
2462 return True
John Budorick0f7b2002018-01-19 15:46:17 -08002463
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002464 #override
2465 def GetScmName(self):
2466 """Always 'cipd'."""
2467 return 'cipd'
Shenghua Zhang6f830312018-02-26 11:45:07 -08002468
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002469 def GetExpandedPackageName(self):
2470 """Return the CIPD package name with the variables evaluated."""
2471 package = self._cipd_root.expand_package_name(self._package_name)
2472 if package:
2473 return package
2474 return self._package_name
John Budorick0f7b2002018-01-19 15:46:17 -08002475
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002476 #override
2477 def CreateSCM(self, out_cb=None):
2478 """Create a Wrapper instance suitable for handling this CIPD dependency."""
2479 self._CreatePackageIfNecessary()
2480 return gclient_scm.CipdWrapper(self.url,
2481 self.root.root_dir,
2482 self.name,
2483 self.outbuf,
2484 out_cb,
2485 root=self._cipd_root,
2486 package=self._cipd_package)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +00002487
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002488 def hierarchy(self, include_url=False, graphviz=False):
2489 if graphviz:
2490 return '' # graphviz lines not implemented for cipd deps.
2491 return self.parent.hierarchy(include_url) + ' -> ' + self._cipd_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08002492
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002493 def ToLines(self):
2494 # () -> Sequence[str]
2495 """Return a list of lines representing this in a DEPS file."""
2496 def escape_cipd_var(package):
2497 return package.replace('{', '{{').replace('}', '}}')
Edward Lemure4e15042018-06-28 18:07:00 +00002498
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002499 s = []
2500 self._CreatePackageIfNecessary()
2501 if self._cipd_package.authority_for_subdir:
2502 condition_part = ([' "condition": %r,' %
2503 self.condition] if self.condition else [])
2504 s.extend([
2505 ' # %s' % self.hierarchy(include_url=False),
2506 ' "%s": {' % (self.name.split(':')[0], ),
2507 ' "packages": [',
2508 ])
2509 for p in sorted(self._cipd_root.packages(self._cipd_subdir),
2510 key=lambda x: x.name):
2511 s.extend([
2512 ' {',
2513 ' "package": "%s",' % escape_cipd_var(p.name),
2514 ' "version": "%s",' % p.version,
2515 ' },',
2516 ])
John Budorickc35aba52018-06-28 20:57:03 +00002517
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002518 s.extend([
2519 ' ],',
2520 ' "dep_type": "cipd",',
2521 ] + condition_part + [
2522 ' },',
2523 '',
2524 ])
2525 return s
John Budorick0f7b2002018-01-19 15:46:17 -08002526
2527
maruel@chromium.org5ca27692010-05-26 19:32:41 +00002528#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002529
2530
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002531@subcommand.usage('[command] [args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002532@metrics.collector.collect_metrics('gclient recurse')
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002533def CMDrecurse(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002534 """Operates [command args ...] on all the dependencies.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002535
Arthur Milchior08cd5fe2022-07-28 20:38:47 +00002536 Change directory to each dependency's directory, and call [command
2537 args ...] there. Sets GCLIENT_DEP_PATH environment variable as the
2538 dep's relative location to root directory of the checkout.
2539
2540 Examples:
2541 * `gclient recurse --no-progress -j1 sh -c 'echo "$GCLIENT_DEP_PATH"'`
2542 print the relative path of each dependency.
2543 * `gclient recurse --no-progress -j1 sh -c "pwd"`
2544 print the absolute path of each dependency.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002545 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002546 # Stop parsing at the first non-arg so that these go through to the command
2547 parser.disable_interspersed_args()
2548 parser.add_option('-s',
2549 '--scm',
2550 action='append',
2551 default=[],
2552 help='Choose scm types to operate upon.')
2553 parser.add_option('-i',
2554 '--ignore',
2555 action='store_true',
2556 help='Ignore non-zero return codes from subcommands.')
2557 parser.add_option(
2558 '--prepend-dir',
2559 action='store_true',
2560 help='Prepend relative dir for use with git <cmd> --null.')
2561 parser.add_option(
2562 '--no-progress',
2563 action='store_true',
2564 help='Disable progress bar that shows sub-command updates')
2565 options, args = parser.parse_args(args)
2566 if not args:
2567 print('Need to supply a command!', file=sys.stderr)
2568 return 1
2569 root_and_entries = gclient_utils.GetGClientRootAndEntries()
2570 if not root_and_entries:
2571 print(
2572 'You need to run gclient sync at least once to use \'recurse\'.\n'
2573 'This is because .gclient_entries needs to exist and be up to date.',
2574 file=sys.stderr)
2575 return 1
davidbarr@chromium.org47ca0ee2012-03-02 16:06:11 +00002576
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002577 # Normalize options.scm to a set()
2578 scm_set = set()
2579 for scm in options.scm:
2580 scm_set.update(scm.split(','))
2581 options.scm = scm_set
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002582
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002583 options.nohooks = True
2584 client = GClient.LoadCurrentConfig(options)
2585 if not client:
2586 raise gclient_utils.Error(
2587 'client not configured; see \'gclient config\'')
2588 return client.RunOnDeps('recurse',
2589 args,
2590 ignore_requirements=True,
2591 progress=not options.no_progress)
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002592
2593
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002594@subcommand.usage('[args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002595@metrics.collector.collect_metrics('gclient fetch')
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002596def CMDfetch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002597 """Fetches upstream commits for all modules.
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002598
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002599 Completely git-specific. Simply runs 'git fetch [args ...]' for each module.
2600 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002601 (options, args) = parser.parse_args(args)
2602 return CMDrecurse(
2603 OptionParser(),
2604 ['--jobs=%d' % options.jobs, '--scm=git', 'git', 'fetch'] + args)
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00002605
2606
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002607class Flattener(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002608 """Flattens a gclient solution."""
2609 def __init__(self, client, pin_all_deps=False):
2610 """Constructor.
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002611
2612 Arguments:
2613 client (GClient): client to flatten
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002614 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2615 in DEPS
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002616 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002617 self._client = client
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002618
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002619 self._deps_string = None
2620 self._deps_graph_lines = None
2621 self._deps_files = set()
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002622
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002623 self._allowed_hosts = set()
2624 self._deps = {}
2625 self._hooks = []
2626 self._pre_deps_hooks = []
2627 self._vars = {}
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002628
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002629 self._flatten(pin_all_deps=pin_all_deps)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002630
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002631 @property
2632 def deps_string(self):
2633 assert self._deps_string is not None
2634 return self._deps_string
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002635
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002636 @property
2637 def deps_graph_lines(self):
2638 assert self._deps_graph_lines is not None
2639 return self._deps_graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00002640
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002641 @property
2642 def deps_files(self):
2643 return self._deps_files
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002644
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002645 def _pin_dep(self, dep):
2646 """Pins a dependency to specific full revision sha.
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002647
2648 Arguments:
2649 dep (Dependency): dependency to process
2650 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002651 if dep.url is None:
2652 return
Michael Mossd683d7c2018-06-15 05:05:17 +00002653
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002654 # Make sure the revision is always fully specified (a hash),
2655 # as opposed to refs or tags which might change. Similarly,
2656 # shortened shas might become ambiguous; make sure to always
2657 # use full one for pinning.
2658 revision = gclient_utils.SplitUrlRevision(dep.url)[1]
2659 if not revision or not gclient_utils.IsFullGitSha(revision):
2660 dep.PinToActualRevision()
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002661
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002662 def _flatten(self, pin_all_deps=False):
2663 """Runs the flattener. Saves resulting DEPS string.
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002664
2665 Arguments:
2666 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2667 in DEPS
2668 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002669 for solution in self._client.dependencies:
2670 self._add_dep(solution)
2671 self._flatten_dep(solution)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002672
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002673 if pin_all_deps:
2674 for dep in self._deps.values():
2675 self._pin_dep(dep)
Paweł Hajdan, Jr39300ba2017-08-11 16:52:38 +02002676
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002677 def add_deps_file(dep):
2678 # Only include DEPS files referenced by recursedeps.
2679 if not dep.should_recurse:
2680 return
2681 deps_file = dep.deps_file
2682 deps_path = os.path.join(self._client.root_dir, dep.name, deps_file)
2683 if not os.path.exists(deps_path):
2684 # gclient has a fallback that if deps_file doesn't exist, it'll
2685 # try DEPS. Do the same here.
2686 deps_file = 'DEPS'
2687 deps_path = os.path.join(self._client.root_dir, dep.name,
2688 deps_file)
2689 if not os.path.exists(deps_path):
2690 return
2691 assert dep.url
2692 self._deps_files.add((dep.url, deps_file, dep.hierarchy_data()))
Paweł Hajdan, Jr76a9d042017-08-18 20:05:41 +02002693
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002694 for dep in self._deps.values():
2695 add_deps_file(dep)
Joanna Wang9144b672023-02-24 23:36:17 +00002696
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002697 gn_args_dep = self._deps.get(self._client.dependencies[0]._gn_args_from,
2698 self._client.dependencies[0])
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002699
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002700 self._deps_graph_lines = _DepsToDotGraphLines(self._deps)
2701 self._deps_string = '\n'.join(
2702 _GNSettingsToLines(gn_args_dep._gn_args_file, gn_args_dep._gn_args)
2703 + _AllowedHostsToLines(self._allowed_hosts) +
2704 _DepsToLines(self._deps) + _HooksToLines('hooks', self._hooks) +
2705 _HooksToLines('pre_deps_hooks', self._pre_deps_hooks) +
2706 _VarsToLines(self._vars) + [
2707 '# %s, %s' % (url, deps_file)
2708 for url, deps_file, _ in sorted(self._deps_files)
2709 ] + ['']) # Ensure newline at end of file.
2710
2711 def _add_dep(self, dep):
2712 """Helper to add a dependency to flattened DEPS.
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002713
2714 Arguments:
2715 dep (Dependency): dependency to add
2716 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002717 assert dep.name not in self._deps or self._deps.get(
2718 dep.name) == dep, (dep.name, self._deps.get(dep.name))
2719 if dep.url:
2720 self._deps[dep.name] = dep
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002721
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002722 def _flatten_dep(self, dep):
2723 """Visits a dependency in order to flatten it (see CMDflatten).
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002724
2725 Arguments:
2726 dep (Dependency): dependency to process
2727 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002728 logging.debug('_flatten_dep(%s)', dep.name)
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002729
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002730 assert dep.deps_parsed, (
2731 "Attempted to flatten %s but it has not been processed." % dep.name)
Paweł Hajdan, Jrc69b32e2017-08-17 18:47:48 +02002732
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002733 self._allowed_hosts.update(dep.allowed_hosts)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002734
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002735 # Only include vars explicitly listed in the DEPS files or gclient
2736 # solution, not automatic, local overrides (i.e. not all of
2737 # dep.get_vars()).
2738 hierarchy = dep.hierarchy(include_url=False)
2739 for key, value in dep._vars.items():
2740 # Make sure there are no conflicting variables. It is fine however
2741 # to use same variable name, as long as the value is consistent.
2742 assert key not in self._vars or self._vars[key][1] == value, (
2743 "dep:%s key:%s value:%s != %s" %
2744 (dep.name, key, value, self._vars[key][1]))
2745 self._vars[key] = (hierarchy, value)
2746 # Override explicit custom variables.
2747 for key, value in dep.custom_vars.items():
2748 # Do custom_vars that don't correspond to DEPS vars ever make sense?
2749 # DEPS conditionals shouldn't be using vars that aren't also defined
2750 # in the DEPS (presubmit actually disallows this), so any new
2751 # custom_var must be unused in the DEPS, so no need to add it to the
2752 # flattened output either.
2753 if key not in self._vars:
2754 continue
2755 # Don't "override" existing vars if it's actually the same value.
2756 if self._vars[key][1] == value:
2757 continue
2758 # Anything else is overriding a default value from the DEPS.
2759 self._vars[key] = (hierarchy + ' [custom_var override]', value)
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02002760
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002761 self._pre_deps_hooks.extend([(dep, hook)
2762 for hook in dep.pre_deps_hooks])
2763 self._hooks.extend([(dep, hook) for hook in dep.deps_hooks])
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002764
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002765 for sub_dep in dep.dependencies:
2766 self._add_dep(sub_dep)
Paweł Hajdan, Jrb0ad16e2017-08-03 15:33:21 +02002767
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002768 for d in dep.dependencies:
2769 if d.should_recurse:
2770 self._flatten_dep(d)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002771
2772
Joanna Wang3ab2f212023-08-09 01:25:15 +00002773@metrics.collector.collect_metrics('gclient gitmodules')
2774def CMDgitmodules(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002775 """Adds or updates Git Submodules based on the contents of the DEPS file.
Joanna Wang3ab2f212023-08-09 01:25:15 +00002776
2777 This command should be run in the root director of the repo.
2778 It will create or update the .gitmodules file and include
2779 `gclient-condition` values. Commits in gitlinks will also be updated.
2780 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002781 parser.add_option('--output-gitmodules',
2782 help='name of the .gitmodules file to write to',
2783 default='.gitmodules')
2784 parser.add_option(
2785 '--deps-file',
2786 help=
2787 'name of the deps file to parse for git dependency paths and commits.',
2788 default='DEPS')
2789 parser.add_option(
2790 '--skip-dep',
2791 action="append",
2792 help='skip adding gitmodules for the git dependency at the given path',
2793 default=[])
2794 options, args = parser.parse_args(args)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002795
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002796 deps_dir = os.path.dirname(os.path.abspath(options.deps_file))
2797 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
2798 if not gclient_path:
2799 logging.error(
2800 '.gclient not found\n'
2801 'Make sure you are running this script from a gclient workspace.')
2802 sys.exit(1)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002803
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002804 deps_content = gclient_utils.FileRead(options.deps_file)
2805 ls = gclient_eval.Parse(deps_content, options.deps_file, None, None)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002806
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002807 prefix_length = 0
2808 if not 'use_relative_paths' in ls or ls['use_relative_paths'] != True:
2809 delta_path = os.path.relpath(deps_dir, os.path.abspath(gclient_path))
2810 if delta_path:
2811 prefix_length = len(delta_path.replace(os.path.sep, '/')) + 1
Joanna Wang3ab2f212023-08-09 01:25:15 +00002812
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002813 cache_info = []
2814 with open(options.output_gitmodules, 'w', newline='') as f:
2815 for path, dep in ls.get('deps').items():
2816 if path in options.skip_dep:
2817 continue
2818 if dep.get('dep_type') == 'cipd':
2819 continue
2820 try:
2821 url, commit = dep['url'].split('@', maxsplit=1)
2822 except ValueError:
2823 logging.error('error on %s; %s, not adding it', path,
2824 dep["url"])
2825 continue
2826 if prefix_length:
2827 path = path[prefix_length:]
Joanna Wang3ab2f212023-08-09 01:25:15 +00002828
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002829 cache_info += ['--cacheinfo', f'160000,{commit},{path}']
2830 f.write(f'[submodule "{path}"]\n\tpath = {path}\n\turl = {url}\n')
2831 if 'condition' in dep:
2832 f.write(f'\tgclient-condition = {dep["condition"]}\n')
2833 # Windows has limit how long, so let's chunk those calls.
2834 if len(cache_info) >= 100:
2835 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
2836 cache_info = []
2837
2838 if cache_info:
Josip Sokcevic293aa652023-08-23 18:55:20 +00002839 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002840 subprocess2.call(['git', 'add', '.gitmodules'])
2841 print('.gitmodules and gitlinks updated. Please check git diff and '
2842 'commit changes.')
Joanna Wang3ab2f212023-08-09 01:25:15 +00002843
2844
Edward Lemur3298e7b2018-07-17 18:21:27 +00002845@metrics.collector.collect_metrics('gclient flatten')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002846def CMDflatten(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002847 """Flattens the solutions into a single DEPS file."""
2848 parser.add_option('--output-deps', help='Path to the output DEPS file')
2849 parser.add_option(
2850 '--output-deps-files',
2851 help=('Path to the output metadata about DEPS files referenced by '
2852 'recursedeps.'))
2853 parser.add_option(
2854 '--pin-all-deps',
2855 action='store_true',
2856 help=('Pin all deps, even if not pinned in DEPS. CAVEAT: only does so '
2857 'for checked out deps, NOT deps_os.'))
2858 parser.add_option('--deps-graph-file',
2859 help='Provide a path for the output graph file')
2860 options, args = parser.parse_args(args)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002861
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002862 options.nohooks = True
2863 options.process_all_deps = True
2864 client = GClient.LoadCurrentConfig(options)
2865 if not client:
2866 raise gclient_utils.Error(
2867 'client not configured; see \'gclient config\'')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002868
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002869 # Only print progress if we're writing to a file. Otherwise, progress
2870 # updates could obscure intended output.
2871 code = client.RunOnDeps('flatten', args, progress=options.output_deps)
2872 if code != 0:
2873 return code
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002874
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002875 flattener = Flattener(client, pin_all_deps=options.pin_all_deps)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002876
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002877 if options.output_deps:
2878 with open(options.output_deps, 'w') as f:
2879 f.write(flattener.deps_string)
2880 else:
2881 print(flattener.deps_string)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002882
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002883 if options.deps_graph_file:
2884 with open(options.deps_graph_file, 'w') as f:
2885 f.write('\n'.join(flattener.deps_graph_lines))
Joanna Wang9144b672023-02-24 23:36:17 +00002886
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002887 deps_files = [{
2888 'url': d[0],
2889 'deps_file': d[1],
2890 'hierarchy': d[2]
2891 } for d in sorted(flattener.deps_files)]
2892 if options.output_deps_files:
2893 with open(options.output_deps_files, 'w') as f:
2894 json.dump(deps_files, f)
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002895
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002896 return 0
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002897
2898
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002899def _GNSettingsToLines(gn_args_file, gn_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002900 s = []
2901 if gn_args_file:
2902 s.extend([
2903 'gclient_gn_args_file = "%s"' % gn_args_file,
2904 'gclient_gn_args = %r' % gn_args,
2905 ])
2906 return s
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002907
2908
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02002909def _AllowedHostsToLines(allowed_hosts):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002910 """Converts |allowed_hosts| set to list of lines for output."""
2911 if not allowed_hosts:
2912 return []
2913 s = ['allowed_hosts = [']
2914 for h in sorted(allowed_hosts):
2915 s.append(' "%s",' % h)
2916 s.extend([']', ''])
2917 return s
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02002918
2919
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002920def _DepsToLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002921 # type: (Mapping[str, Dependency]) -> Sequence[str]
2922 """Converts |deps| dict to list of lines for output."""
2923 if not deps:
2924 return []
2925 s = ['deps = {']
2926 for _, dep in sorted(deps.items()):
2927 s.extend(dep.ToLines())
2928 s.extend(['}', ''])
2929 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002930
2931
Joanna Wang9144b672023-02-24 23:36:17 +00002932def _DepsToDotGraphLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002933 # type: (Mapping[str, Dependency]) -> Sequence[str]
2934 """Converts |deps| dict to list of lines for dot graphs"""
2935 if not deps:
2936 return []
2937 graph_lines = ["digraph {\n\trankdir=\"LR\";"]
2938 for _, dep in sorted(deps.items()):
2939 line = dep.hierarchy(include_url=False, graphviz=True)
2940 if line:
2941 graph_lines.append("\t%s" % line)
2942 graph_lines.append("}")
2943 return graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00002944
2945
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02002946def _DepsOsToLines(deps_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002947 """Converts |deps_os| dict to list of lines for output."""
2948 if not deps_os:
2949 return []
2950 s = ['deps_os = {']
2951 for dep_os, os_deps in sorted(deps_os.items()):
2952 s.append(' "%s": {' % dep_os)
2953 for name, dep in sorted(os_deps.items()):
2954 condition_part = ([' "condition": %r,' %
2955 dep.condition] if dep.condition else [])
2956 s.extend([
2957 ' # %s' % dep.hierarchy(include_url=False),
2958 ' "%s": {' % (name, ),
2959 ' "url": "%s",' % (dep.url, ),
2960 ] + condition_part + [
2961 ' },',
2962 '',
2963 ])
2964 s.extend([' },', ''])
2965 s.extend(['}', ''])
2966 return s
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02002967
2968
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002969def _HooksToLines(name, hooks):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002970 """Converts |hooks| list to list of lines for output."""
2971 if not hooks:
2972 return []
2973 s = ['%s = [' % name]
2974 for dep, hook in hooks:
2975 s.extend([
2976 ' # %s' % dep.hierarchy(include_url=False),
2977 ' {',
2978 ])
2979 if hook.name is not None:
2980 s.append(' "name": "%s",' % hook.name)
2981 if hook.pattern is not None:
2982 s.append(' "pattern": "%s",' % hook.pattern)
2983 if hook.condition is not None:
2984 s.append(' "condition": %r,' % hook.condition)
2985 # Flattened hooks need to be written relative to the root gclient dir
2986 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
2987 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
2988 [' "%s",' % arg
2989 for arg in hook.action] + [' ]', ' },', ''])
2990 s.extend([']', ''])
2991 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002992
2993
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02002994def _HooksOsToLines(hooks_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002995 """Converts |hooks| list to list of lines for output."""
2996 if not hooks_os:
2997 return []
2998 s = ['hooks_os = {']
2999 for hook_os, os_hooks in hooks_os.items():
3000 s.append(' "%s": [' % hook_os)
3001 for dep, hook in os_hooks:
3002 s.extend([
3003 ' # %s' % dep.hierarchy(include_url=False),
3004 ' {',
3005 ])
3006 if hook.name is not None:
3007 s.append(' "name": "%s",' % hook.name)
3008 if hook.pattern is not None:
3009 s.append(' "pattern": "%s",' % hook.pattern)
3010 if hook.condition is not None:
3011 s.append(' "condition": %r,' % hook.condition)
3012 # Flattened hooks need to be written relative to the root gclient
3013 # dir
3014 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3015 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3016 [' "%s",' % arg
3017 for arg in hook.action] + [' ]', ' },', ''])
3018 s.extend([' ],', ''])
3019 s.extend(['}', ''])
3020 return s
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003021
3022
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003023def _VarsToLines(variables):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003024 """Converts |variables| dict to list of lines for output."""
3025 if not variables:
3026 return []
3027 s = ['vars = {']
3028 for key, tup in sorted(variables.items()):
3029 hierarchy, value = tup
3030 s.extend([
3031 ' # %s' % hierarchy,
3032 ' "%s": %r,' % (key, value),
3033 '',
3034 ])
3035 s.extend(['}', ''])
3036 return s
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003037
3038
Edward Lemur3298e7b2018-07-17 18:21:27 +00003039@metrics.collector.collect_metrics('gclient grep')
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003040def CMDgrep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003041 """Greps through git repos managed by gclient.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003042
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003043 Runs 'git grep [args...]' for each module.
3044 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003045 # We can't use optparse because it will try to parse arguments sent
3046 # to git grep and throw an error. :-(
3047 if not args or re.match('(-h|--help)$', args[0]):
3048 print(
3049 'Usage: gclient grep [-j <N>] git-grep-args...\n\n'
3050 'Example: "gclient grep -j10 -A2 RefCountedBase" runs\n"git grep '
3051 '-A2 RefCountedBase" on each of gclient\'s git\nrepos with up to '
3052 '10 jobs.\n\nBonus: page output by appending "|& less -FRSX" to the'
3053 ' end of your query.',
3054 file=sys.stderr)
3055 return 1
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003056
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003057 jobs_arg = ['--jobs=1']
3058 if re.match(r'(-j|--jobs=)\d+$', args[0]):
3059 jobs_arg, args = args[:1], args[1:]
3060 elif re.match(r'(-j|--jobs)$', args[0]):
3061 jobs_arg, args = args[:2], args[2:]
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003062
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003063 return CMDrecurse(
3064 parser, jobs_arg + [
3065 '--ignore', '--prepend-dir', '--no-progress', '--scm=git', 'git',
3066 'grep', '--null', '--color=Always'
3067 ] + args)
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00003068
3069
Edward Lemur3298e7b2018-07-17 18:21:27 +00003070@metrics.collector.collect_metrics('gclient root')
stip@chromium.orga735da22015-04-29 23:18:20 +00003071def CMDroot(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003072 """Outputs the solution root (or current dir if there isn't one)."""
3073 (options, args) = parser.parse_args(args)
3074 client = GClient.LoadCurrentConfig(options)
3075 if client:
3076 print(os.path.abspath(client.root_dir))
3077 else:
3078 print(os.path.abspath('.'))
stip@chromium.orga735da22015-04-29 23:18:20 +00003079
3080
agablea98a6cd2016-11-15 14:30:10 -08003081@subcommand.usage('[url]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00003082@metrics.collector.collect_metrics('gclient config')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003083def CMDconfig(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003084 """Creates a .gclient file in the current directory.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003085
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003086 This specifies the configuration for further commands. After update/sync,
3087 top-level DEPS files in each module are read to determine dependent
3088 modules to operate on as well. If optional [url] parameter is
3089 provided, then configuration is read from a specified Subversion server
3090 URL.
3091 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003092 # We do a little dance with the --gclientfile option. 'gclient config' is
3093 # the only command where it's acceptable to have both '--gclientfile' and
3094 # '--spec' arguments. So, we temporarily stash any --gclientfile parameter
3095 # into options.output_config_file until after the (gclientfile xor spec)
3096 # error check.
3097 parser.remove_option('--gclientfile')
3098 parser.add_option('--gclientfile',
3099 dest='output_config_file',
3100 help='Specify an alternate .gclient file')
3101 parser.add_option('--name',
3102 help='overrides the default name for the solution')
3103 parser.add_option(
3104 '--deps-file',
3105 default='DEPS',
3106 help='overrides the default name for the DEPS file for the '
3107 'main solutions and all sub-dependencies')
3108 parser.add_option('--unmanaged',
3109 action='store_true',
3110 default=False,
3111 help='overrides the default behavior to make it possible '
3112 'to have the main solution untouched by gclient '
3113 '(gclient will check out unmanaged dependencies but '
3114 'will never sync them)')
3115 parser.add_option('--cache-dir',
3116 default=UNSET_CACHE_DIR,
3117 help='Cache all git repos into this dir and do shared '
3118 'clones from the cache, instead of cloning directly '
3119 'from the remote. Pass "None" to disable cache, even '
3120 'if globally enabled due to $GIT_CACHE_PATH.')
3121 parser.add_option('--custom-var',
3122 action='append',
3123 dest='custom_vars',
3124 default=[],
3125 help='overrides variables; key=value syntax')
3126 parser.set_defaults(config_filename=None)
3127 (options, args) = parser.parse_args(args)
3128 if options.output_config_file:
3129 setattr(options, 'config_filename',
3130 getattr(options, 'output_config_file'))
3131 if ((options.spec and args) or len(args) > 2
3132 or (not options.spec and not args)):
3133 parser.error(
3134 'Inconsistent arguments. Use either --spec or one or 2 args')
maruel@chromium.org5fc2a332010-05-26 19:37:15 +00003135
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003136 if (options.cache_dir is not UNSET_CACHE_DIR
3137 and options.cache_dir.lower() == 'none'):
3138 options.cache_dir = None
Robert Iannuccia19649b2018-06-29 16:31:45 +00003139
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003140 custom_vars = {}
3141 for arg in options.custom_vars:
3142 kv = arg.split('=', 1)
3143 if len(kv) != 2:
3144 parser.error('Invalid --custom-var argument: %r' % arg)
3145 custom_vars[kv[0]] = gclient_eval.EvaluateCondition(kv[1], {})
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02003146
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003147 client = GClient('.', options)
3148 if options.spec:
3149 client.SetConfig(options.spec)
iposva@chromium.org8cf7a392010-04-07 17:20:26 +00003150 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003151 base_url = args[0].rstrip('/')
3152 if not options.name:
3153 name = base_url.split('/')[-1]
3154 if name.endswith('.git'):
3155 name = name[:-4]
3156 else:
3157 # specify an alternate relpath for the given URL.
3158 name = options.name
3159 if not os.path.abspath(os.path.join(os.getcwd(), name)).startswith(
3160 os.getcwd()):
3161 parser.error('Do not pass a relative path for --name.')
3162 if any(x in ('..', '.', '/', '\\') for x in name.split(os.sep)):
3163 parser.error(
3164 'Do not include relative path components in --name.')
agable@chromium.orgf2214672015-10-27 21:02:48 +00003165
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003166 deps_file = options.deps_file
3167 client.SetDefaultConfig(name,
3168 deps_file,
3169 base_url,
3170 managed=not options.unmanaged,
3171 cache_dir=options.cache_dir,
3172 custom_vars=custom_vars)
3173 client.SaveConfig()
3174 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003175
3176
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003177@subcommand.epilog("""Example:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003178 gclient pack > patch.txt
3179 generate simple patch for configured client and dependences
3180""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003181@metrics.collector.collect_metrics('gclient pack')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003182def CMDpack(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003183 """Generates a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003184
agabled437d762016-10-17 09:35:11 -07003185 Internally, runs 'git diff' on each checked out module and
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003186 dependencies, and performs minimal postprocessing of the output. The
3187 resulting patch is printed to stdout and can be applied to a freshly
3188 checked out tree via 'patch -p0 < patchfile'.
3189 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003190 parser.add_option('--deps',
3191 dest='deps_os',
3192 metavar='OS_LIST',
3193 help='override deps for the specified (comma-separated) '
3194 'platform(s); \'all\' will process all deps_os '
3195 'references')
3196 parser.remove_option('--jobs')
3197 (options, args) = parser.parse_args(args)
3198 # Force jobs to 1 so the stdout is not annotated with the thread ids
3199 options.jobs = 1
3200 client = GClient.LoadCurrentConfig(options)
3201 if not client:
3202 raise gclient_utils.Error(
3203 'client not configured; see \'gclient config\'')
3204 if options.verbose:
3205 client.PrintLocationAndContents()
3206 return client.RunOnDeps('pack', args)
kbr@google.comab318592009-09-04 00:54:55 +00003207
3208
Edward Lemur3298e7b2018-07-17 18:21:27 +00003209@metrics.collector.collect_metrics('gclient status')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003210def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003211 """Shows modification status for every dependencies."""
3212 parser.add_option('--deps',
3213 dest='deps_os',
3214 metavar='OS_LIST',
3215 help='override deps for the specified (comma-separated) '
3216 'platform(s); \'all\' will process all deps_os '
3217 'references')
3218 (options, args) = parser.parse_args(args)
3219 client = GClient.LoadCurrentConfig(options)
3220 if not client:
3221 raise gclient_utils.Error(
3222 'client not configured; see \'gclient config\'')
3223 if options.verbose:
3224 client.PrintLocationAndContents()
3225 return client.RunOnDeps('status', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003226
3227
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003228@subcommand.epilog("""Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +00003229 gclient sync
3230 update files from SCM according to current configuration,
3231 *for modules which have changed since last update or sync*
3232 gclient sync --force
3233 update files from SCM according to current configuration, for
3234 all modules (useful for recovering files deleted from local copy)
Edward Lesmes3ffca4b2021-05-19 19:36:17 +00003235 gclient sync --revision src@GIT_COMMIT_OR_REF
3236 update src directory to GIT_COMMIT_OR_REF
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003237
3238JSON output format:
3239If the --output-json option is specified, the following document structure will
3240be emitted to the provided file. 'null' entries may occur for subprojects which
3241are present in the gclient solution, but were not processed (due to custom_deps,
3242os_deps, etc.)
3243
3244{
3245 "solutions" : {
3246 "<name>": { # <name> is the posix-normalized path to the solution.
agabled437d762016-10-17 09:35:11 -07003247 "revision": [<git id hex string>|null],
3248 "scm": ["git"|null],
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003249 }
3250 }
3251}
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003252""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003253@metrics.collector.collect_metrics('gclient sync')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003254def CMDsync(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003255 """Checkout/update all modules."""
3256 parser.add_option('-f',
3257 '--force',
3258 action='store_true',
3259 help='force update even for unchanged modules')
3260 parser.add_option('-n',
3261 '--nohooks',
3262 action='store_true',
3263 help='don\'t run hooks after the update is complete')
3264 parser.add_option('-p',
3265 '--noprehooks',
3266 action='store_true',
3267 help='don\'t run pre-DEPS hooks',
3268 default=False)
3269 parser.add_option('-r',
3270 '--revision',
3271 action='append',
3272 dest='revisions',
3273 metavar='REV',
3274 default=[],
3275 help='Enforces git ref/hash for the solutions with the '
3276 'format src@rev. The src@ part is optional and can be '
3277 'skipped. You can also specify URLs instead of paths '
3278 'and gclient will find the solution corresponding to '
3279 'the given URL. If a path is also specified, the URL '
3280 'takes precedence. -r can be used multiple times when '
3281 '.gclient has multiple solutions configured, and will '
3282 'work even if the src@ part is skipped. Revision '
3283 'numbers (e.g. 31000 or r31000) are not supported.')
3284 parser.add_option('--patch-ref',
3285 action='append',
3286 dest='patch_refs',
3287 metavar='GERRIT_REF',
3288 default=[],
3289 help='Patches the given reference with the format '
3290 'dep@target-ref:patch-ref. '
3291 'For |dep|, you can specify URLs as well as paths, '
3292 'with URLs taking preference. '
3293 '|patch-ref| will be applied to |dep|, rebased on top '
3294 'of what |dep| was synced to, and a soft reset will '
3295 'be done. Use --no-rebase-patch-ref and '
3296 '--no-reset-patch-ref to disable this behavior. '
3297 '|target-ref| is the target branch against which a '
3298 'patch was created, it is used to determine which '
3299 'commits from the |patch-ref| actually constitute a '
3300 'patch.')
3301 parser.add_option(
3302 '-t',
3303 '--download-topics',
3304 action='store_true',
3305 help='Downloads and patches locally changes from all open '
3306 'Gerrit CLs that have the same topic as the changes '
3307 'in the specified patch_refs. Only works if atleast '
3308 'one --patch-ref is specified.')
3309 parser.add_option('--with_branch_heads',
3310 action='store_true',
3311 help='Clone git "branch_heads" refspecs in addition to '
3312 'the default refspecs. This adds about 1/2GB to a '
3313 'full checkout. (git only)')
3314 parser.add_option(
3315 '--with_tags',
3316 action='store_true',
3317 help='Clone git tags in addition to the default refspecs.')
3318 parser.add_option('-H',
3319 '--head',
3320 action='store_true',
3321 help='DEPRECATED: only made sense with safesync urls.')
3322 parser.add_option(
3323 '-D',
3324 '--delete_unversioned_trees',
3325 action='store_true',
3326 help='Deletes from the working copy any dependencies that '
3327 'have been removed since the last sync, as long as '
3328 'there are no local modifications. When used with '
3329 '--force, such dependencies are removed even if they '
3330 'have local modifications. When used with --reset, '
3331 'all untracked directories are removed from the '
3332 'working copy, excluding those which are explicitly '
3333 'ignored in the repository.')
3334 parser.add_option(
3335 '-R',
3336 '--reset',
3337 action='store_true',
3338 help='resets any local changes before updating (git only)')
3339 parser.add_option('-M',
3340 '--merge',
3341 action='store_true',
3342 help='merge upstream changes instead of trying to '
3343 'fast-forward or rebase')
3344 parser.add_option('-A',
3345 '--auto_rebase',
3346 action='store_true',
3347 help='Automatically rebase repositories against local '
3348 'checkout during update (git only).')
3349 parser.add_option('--deps',
3350 dest='deps_os',
3351 metavar='OS_LIST',
3352 help='override deps for the specified (comma-separated) '
3353 'platform(s); \'all\' will process all deps_os '
3354 'references')
3355 parser.add_option('--process-all-deps',
3356 action='store_true',
3357 help='Check out all deps, even for different OS-es, '
3358 'or with conditions evaluating to false')
3359 parser.add_option('--upstream',
3360 action='store_true',
3361 help='Make repo state match upstream branch.')
3362 parser.add_option('--output-json',
3363 help='Output a json document to this path containing '
3364 'summary information about the sync.')
3365 parser.add_option(
3366 '--no-history',
3367 action='store_true',
3368 help='GIT ONLY - Reduces the size/time of the checkout at '
3369 'the cost of no history. Requires Git 1.9+')
3370 parser.add_option('--shallow',
3371 action='store_true',
3372 help='GIT ONLY - Do a shallow clone into the cache dir. '
3373 'Requires Git 1.9+')
3374 parser.add_option('--no_bootstrap',
3375 '--no-bootstrap',
3376 action='store_true',
3377 help='Don\'t bootstrap from Google Storage.')
3378 parser.add_option('--ignore_locks',
3379 action='store_true',
3380 help='No longer used.')
3381 parser.add_option('--break_repo_locks',
3382 action='store_true',
3383 help='No longer used.')
3384 parser.add_option('--lock_timeout',
3385 type='int',
3386 default=5000,
3387 help='GIT ONLY - Deadline (in seconds) to wait for git '
3388 'cache lock to become available. Default is %default.')
3389 parser.add_option('--no-rebase-patch-ref',
3390 action='store_false',
3391 dest='rebase_patch_ref',
3392 default=True,
3393 help='Bypass rebase of the patch ref after checkout.')
3394 parser.add_option('--no-reset-patch-ref',
3395 action='store_false',
3396 dest='reset_patch_ref',
3397 default=True,
3398 help='Bypass calling reset after patching the ref.')
3399 parser.add_option('--experiment',
3400 action='append',
3401 dest='experiments',
3402 default=[],
3403 help='Which experiments should be enabled.')
3404 (options, args) = parser.parse_args(args)
3405 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003406
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003407 if not client:
3408 raise gclient_utils.Error(
3409 'client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003410
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003411 if options.download_topics and not options.rebase_patch_ref:
3412 raise gclient_utils.Error(
3413 'Warning: You cannot download topics and not rebase each patch ref')
Ravi Mistryecda7822022-02-28 16:22:20 +00003414
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003415 if options.ignore_locks:
3416 print(
3417 'Warning: ignore_locks is no longer used. Please remove its usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003418
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003419 if options.break_repo_locks:
3420 print('Warning: break_repo_locks is no longer used. Please remove its '
3421 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003422
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003423 if options.revisions and options.head:
3424 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
3425 print('Warning: you cannot use both --head and --revision')
smutae7ea312016-07-18 11:59:41 -07003426
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003427 if options.verbose:
3428 client.PrintLocationAndContents()
3429 ret = client.RunOnDeps('update', args)
3430 if options.output_json:
3431 slns = {}
3432 for d in client.subtree(True):
3433 normed = d.name.replace('\\', '/').rstrip('/') + '/'
3434 slns[normed] = {
3435 'revision': d.got_revision,
3436 'scm': d.used_scm.name if d.used_scm else None,
3437 'url': str(d.url) if d.url else None,
3438 'was_processed': d.should_process,
3439 'was_synced': d._should_sync,
3440 }
3441 with open(options.output_json, 'w') as f:
3442 json.dump({'solutions': slns}, f)
3443 return ret
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003444
3445
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003446CMDupdate = CMDsync
3447
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003448
Edward Lemur3298e7b2018-07-17 18:21:27 +00003449@metrics.collector.collect_metrics('gclient validate')
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003450def CMDvalidate(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003451 """Validates the .gclient and DEPS syntax."""
3452 options, args = parser.parse_args(args)
3453 client = GClient.LoadCurrentConfig(options)
3454 if not client:
3455 raise gclient_utils.Error(
3456 'client not configured; see \'gclient config\'')
3457 rv = client.RunOnDeps('validate', args)
3458 if rv == 0:
3459 print('validate: SUCCESS')
3460 else:
3461 print('validate: FAILURE')
3462 return rv
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003463
3464
Edward Lemur3298e7b2018-07-17 18:21:27 +00003465@metrics.collector.collect_metrics('gclient diff')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003466def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003467 """Displays local diff for every dependencies."""
3468 parser.add_option('--deps',
3469 dest='deps_os',
3470 metavar='OS_LIST',
3471 help='override deps for the specified (comma-separated) '
3472 'platform(s); \'all\' will process all deps_os '
3473 'references')
3474 (options, args) = parser.parse_args(args)
3475 client = GClient.LoadCurrentConfig(options)
3476 if not client:
3477 raise gclient_utils.Error(
3478 'client not configured; see \'gclient config\'')
3479 if options.verbose:
3480 client.PrintLocationAndContents()
3481 return client.RunOnDeps('diff', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003482
3483
Edward Lemur3298e7b2018-07-17 18:21:27 +00003484@metrics.collector.collect_metrics('gclient revert')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003485def CMDrevert(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003486 """Reverts all modifications in every dependencies.
maruel@chromium.org28d14bd2010-11-11 20:37:09 +00003487
3488 That's the nuclear option to get back to a 'clean' state. It removes anything
agabled437d762016-10-17 09:35:11 -07003489 that shows up in git status."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003490 parser.add_option('--deps',
3491 dest='deps_os',
3492 metavar='OS_LIST',
3493 help='override deps for the specified (comma-separated) '
3494 'platform(s); \'all\' will process all deps_os '
3495 'references')
3496 parser.add_option('-n',
3497 '--nohooks',
3498 action='store_true',
3499 help='don\'t run hooks after the revert is complete')
3500 parser.add_option('-p',
3501 '--noprehooks',
3502 action='store_true',
3503 help='don\'t run pre-DEPS hooks',
3504 default=False)
3505 parser.add_option('--upstream',
3506 action='store_true',
3507 help='Make repo state match upstream branch.')
3508 parser.add_option('--break_repo_locks',
3509 action='store_true',
3510 help='No longer used.')
3511 (options, args) = parser.parse_args(args)
3512 if options.break_repo_locks:
3513 print(
3514 'Warning: break_repo_locks is no longer used. Please remove its ' +
3515 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003516
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003517 # --force is implied.
3518 options.force = True
3519 options.reset = False
3520 options.delete_unversioned_trees = False
3521 options.merge = False
3522 client = GClient.LoadCurrentConfig(options)
3523 if not client:
3524 raise gclient_utils.Error(
3525 'client not configured; see \'gclient config\'')
3526 return client.RunOnDeps('revert', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003527
3528
Edward Lemur3298e7b2018-07-17 18:21:27 +00003529@metrics.collector.collect_metrics('gclient runhooks')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003530def CMDrunhooks(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003531 """Runs hooks for files that have been modified in the local working copy."""
3532 parser.add_option('--deps',
3533 dest='deps_os',
3534 metavar='OS_LIST',
3535 help='override deps for the specified (comma-separated) '
3536 'platform(s); \'all\' will process all deps_os '
3537 'references')
3538 parser.add_option('-f',
3539 '--force',
3540 action='store_true',
3541 default=True,
3542 help='Deprecated. No effect.')
3543 (options, args) = parser.parse_args(args)
3544 client = GClient.LoadCurrentConfig(options)
3545 if not client:
3546 raise gclient_utils.Error(
3547 'client not configured; see \'gclient config\'')
3548 if options.verbose:
3549 client.PrintLocationAndContents()
3550 options.force = True
3551 options.nohooks = False
3552 return client.RunOnDeps('runhooks', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003553
3554
Edward Lemur3298e7b2018-07-17 18:21:27 +00003555@metrics.collector.collect_metrics('gclient revinfo')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003556def CMDrevinfo(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003557 """Outputs revision info mapping for the client and its dependencies.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003558
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00003559 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003560 can be used to reproduce the same tree in the future. It is only useful for
agabled437d762016-10-17 09:35:11 -07003561 'unpinned dependencies', i.e. DEPS/deps references without a git hash.
3562 A git branch name isn't 'pinned' since the actual commit can change.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003563 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003564 parser.add_option('--deps',
3565 dest='deps_os',
3566 metavar='OS_LIST',
3567 help='override deps for the specified (comma-separated) '
3568 'platform(s); \'all\' will process all deps_os '
3569 'references')
3570 parser.add_option(
3571 '-a',
3572 '--actual',
3573 action='store_true',
3574 help='gets the actual checked out revisions instead of the '
3575 'ones specified in the DEPS and .gclient files')
3576 parser.add_option('-s',
3577 '--snapshot',
3578 action='store_true',
3579 help='creates a snapshot .gclient file of the current '
3580 'version of all repositories to reproduce the tree, '
3581 'implies -a')
3582 parser.add_option(
3583 '--filter',
3584 action='append',
3585 dest='filter',
3586 help='Display revision information only for the specified '
3587 'dependencies (filtered by URL or path).')
3588 parser.add_option('--output-json',
3589 help='Output a json document to this path containing '
3590 'information about the revisions.')
3591 parser.add_option(
3592 '--ignore-dep-type',
3593 choices=['git', 'cipd'],
3594 help='Specify to skip processing of a certain type of dep.')
3595 (options, args) = parser.parse_args(args)
3596 client = GClient.LoadCurrentConfig(options)
3597 if not client:
3598 raise gclient_utils.Error(
3599 'client not configured; see \'gclient config\'')
3600 client.PrintRevInfo()
3601 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003602
3603
Edward Lemur3298e7b2018-07-17 18:21:27 +00003604@metrics.collector.collect_metrics('gclient getdep')
Edward Lesmes411041f2018-04-05 20:12:55 -04003605def CMDgetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003606 """Gets revision information and variable values from a DEPS file.
Josip Sokcevic7b5e3d72023-06-13 00:28:23 +00003607
3608 If key doesn't exist or is incorrectly declared, this script exits with exit
3609 code 2."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003610 parser.add_option('--var',
3611 action='append',
3612 dest='vars',
3613 metavar='VAR',
3614 default=[],
3615 help='Gets the value of a given variable.')
3616 parser.add_option(
3617 '-r',
3618 '--revision',
3619 action='append',
3620 dest='getdep_revisions',
3621 metavar='DEP',
3622 default=[],
3623 help='Gets the revision/version for the given dependency. '
3624 'If it is a git dependency, dep must be a path. If it '
3625 'is a CIPD dependency, dep must be of the form '
3626 'path:package.')
3627 parser.add_option(
3628 '--deps-file',
3629 default='DEPS',
3630 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3631 # .gclient first.
3632 help='The DEPS file to be edited. Defaults to the DEPS '
3633 'file in the current directory.')
3634 (options, args) = parser.parse_args(args)
Edward Lesmes411041f2018-04-05 20:12:55 -04003635
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003636 if not os.path.isfile(options.deps_file):
3637 raise gclient_utils.Error('DEPS file %s does not exist.' %
3638 options.deps_file)
3639 with open(options.deps_file) as f:
3640 contents = f.read()
3641 client = GClient.LoadCurrentConfig(options)
3642 if client is not None:
3643 builtin_vars = client.get_builtin_vars()
Edward Lesmes411041f2018-04-05 20:12:55 -04003644 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003645 logging.warning(
3646 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3647 'file without support for built-in variables.')
3648 builtin_vars = None
3649 local_scope = gclient_eval.Exec(contents,
3650 options.deps_file,
3651 builtin_vars=builtin_vars)
3652
3653 for var in options.vars:
3654 print(gclient_eval.GetVar(local_scope, var))
3655
3656 commits = {}
3657 if local_scope.get(
3658 'git_dependencies'
3659 ) == gclient_eval.SUBMODULES and options.getdep_revisions:
3660 commits.update(
3661 scm_git.GIT.GetSubmoduleCommits(
3662 os.getcwd(),
3663 [path for path in options.getdep_revisions if ':' not in path]))
3664
3665 for name in options.getdep_revisions:
3666 if ':' in name:
3667 name, _, package = name.partition(':')
3668 if not name or not package:
3669 parser.error(
3670 'Wrong CIPD format: %s:%s should be of the form path:pkg.' %
3671 (name, package))
3672 print(gclient_eval.GetCIPD(local_scope, name, package))
3673 elif commits:
3674 print(commits[name])
3675 else:
3676 try:
3677 print(gclient_eval.GetRevision(local_scope, name))
3678 except KeyError as e:
3679 print(repr(e), file=sys.stderr)
3680 sys.exit(2)
Edward Lesmes411041f2018-04-05 20:12:55 -04003681
3682
Edward Lemur3298e7b2018-07-17 18:21:27 +00003683@metrics.collector.collect_metrics('gclient setdep')
Edward Lesmes6f64a052018-03-20 17:35:49 -04003684def CMDsetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003685 """Modifies dependency revisions and variable values in a DEPS file"""
3686 parser.add_option('--var',
3687 action='append',
3688 dest='vars',
3689 metavar='VAR=VAL',
3690 default=[],
3691 help='Sets a variable to the given value with the format '
3692 'name=value.')
3693 parser.add_option('-r',
3694 '--revision',
3695 action='append',
3696 dest='setdep_revisions',
3697 metavar='DEP@REV',
3698 default=[],
3699 help='Sets the revision/version for the dependency with '
3700 'the format dep@rev. If it is a git dependency, dep '
3701 'must be a path and rev must be a git hash or '
3702 'reference (e.g. src/dep@deadbeef). If it is a CIPD '
3703 'dependency, dep must be of the form path:package and '
3704 'rev must be the package version '
3705 '(e.g. src/pkg:chromium/pkg@2.1-cr0).')
3706 parser.add_option(
3707 '--deps-file',
3708 default='DEPS',
3709 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3710 # .gclient first.
3711 help='The DEPS file to be edited. Defaults to the DEPS '
3712 'file in the current directory.')
3713 (options, args) = parser.parse_args(args)
3714 if args:
3715 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3716 if not options.setdep_revisions and not options.vars:
Edward Lesmes0ecf6d62018-04-05 18:28:55 -04003717 parser.error(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003718 'You must specify at least one variable or revision to modify.')
3719
3720 if not os.path.isfile(options.deps_file):
3721 raise gclient_utils.Error('DEPS file %s does not exist.' %
3722 options.deps_file)
3723 with open(options.deps_file) as f:
3724 contents = f.read()
3725
3726 client = GClient.LoadCurrentConfig(options)
3727 if client is not None:
3728 builtin_vars = client.get_builtin_vars()
Edward Lesmes6f64a052018-03-20 17:35:49 -04003729 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003730 logging.warning(
3731 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3732 'file without support for built-in variables.')
3733 builtin_vars = None
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003734
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003735 local_scope = gclient_eval.Exec(contents,
3736 options.deps_file,
3737 builtin_vars=builtin_vars)
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003738
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003739 # Create a set of all git submodules.
3740 cwd = os.path.dirname(options.deps_file) or os.getcwd()
3741 git_modules = None
3742 if 'git_dependencies' in local_scope and local_scope[
3743 'git_dependencies'] in (gclient_eval.SUBMODULES, gclient_eval.SYNC):
3744 try:
3745 submodule_status = subprocess2.check_output(
3746 ['git', 'submodule', 'status'], cwd=cwd).decode('utf-8')
3747 git_modules = {l.split()[1] for l in submodule_status.splitlines()}
3748 except subprocess2.CalledProcessError as e:
3749 print('Warning: gitlinks won\'t be updated: ', e)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003750
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003751 for var in options.vars:
3752 name, _, value = var.partition('=')
3753 if not name or not value:
3754 parser.error(
3755 'Wrong var format: %s should be of the form name=value.' % var)
3756 if name in local_scope['vars']:
3757 gclient_eval.SetVar(local_scope, name, value)
3758 else:
3759 gclient_eval.AddVar(local_scope, name, value)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003760
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003761 for revision in options.setdep_revisions:
3762 name, _, value = revision.partition('@')
3763 if not name or not value:
3764 parser.error('Wrong dep format: %s should be of the form dep@rev.' %
3765 revision)
3766 if ':' in name:
3767 name, _, package = name.partition(':')
3768 if not name or not package:
3769 parser.error(
3770 'Wrong CIPD format: %s:%s should be of the form path:pkg@version.'
3771 % (name, package))
3772 gclient_eval.SetCIPD(local_scope, name, package, value)
3773 else:
3774 # Update DEPS only when `git_dependencies` == DEPS or SYNC.
3775 # git_dependencies is defaulted to DEPS when not set.
3776 if 'git_dependencies' not in local_scope or local_scope[
3777 'git_dependencies'] in (gclient_eval.DEPS,
3778 gclient_eval.SYNC):
3779 gclient_eval.SetRevision(local_scope, name, value)
3780
3781 # Update git submodules when `git_dependencies` == SYNC or
3782 # SUBMODULES.
3783 if git_modules and 'git_dependencies' in local_scope and local_scope[
3784 'git_dependencies'] in (gclient_eval.SUBMODULES,
3785 gclient_eval.SYNC):
3786 git_module_name = name
3787 if not 'use_relative_paths' in local_scope or \
3788 local_scope['use_relative_paths'] != True:
3789 deps_dir = os.path.dirname(
3790 os.path.abspath(options.deps_file))
3791 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
3792 delta_path = None
3793 if gclient_path:
3794 delta_path = os.path.relpath(
3795 deps_dir, os.path.abspath(gclient_path))
3796 if delta_path:
3797 prefix_length = len(delta_path.replace(
3798 os.path.sep, '/')) + 1
3799 git_module_name = name[prefix_length:]
3800 # gclient setdep should update the revision, i.e., the gitlink
3801 # only when the submodule entry is already present within
3802 # .gitmodules.
3803 if git_module_name not in git_modules:
3804 raise KeyError(
3805 f'Could not find any dependency called "{git_module_name}" in '
3806 f'.gitmodules.')
3807
3808 # Update the gitlink for the submodule.
3809 subprocess2.call([
3810 'git', 'update-index', '--add', '--cacheinfo',
3811 f'160000,{value},{git_module_name}'
3812 ],
3813 cwd=cwd)
3814
3815 with open(options.deps_file, 'wb') as f:
3816 f.write(gclient_eval.RenderDEPSFile(local_scope).encode('utf-8'))
3817
3818 if git_modules:
3819 subprocess2.call(['git', 'add', options.deps_file], cwd=cwd)
3820 print('Changes have been staged. See changes with `git status`.\n'
3821 'Use `git commit -m "Manual roll"` to commit your changes. \n'
3822 'Run gclient sync to update your local dependency checkout.')
Josip Sokcevic5561f8b2023-08-21 16:00:42 +00003823
Edward Lesmes6f64a052018-03-20 17:35:49 -04003824
Edward Lemur3298e7b2018-07-17 18:21:27 +00003825@metrics.collector.collect_metrics('gclient verify')
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003826def CMDverify(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003827 """Verifies the DEPS file deps are only from allowed_hosts."""
3828 (options, args) = parser.parse_args(args)
3829 client = GClient.LoadCurrentConfig(options)
3830 if not client:
3831 raise gclient_utils.Error(
3832 'client not configured; see \'gclient config\'')
3833 client.RunOnDeps(None, [])
3834 # Look at each first-level dependency of this gclient only.
3835 for dep in client.dependencies:
3836 bad_deps = dep.findDepsFromNotAllowedHosts()
3837 if not bad_deps:
3838 continue
3839 print("There are deps from not allowed hosts in file %s" %
3840 dep.deps_file)
3841 for bad_dep in bad_deps:
3842 print("\t%s at %s" % (bad_dep.name, bad_dep.url))
3843 print("allowed_hosts:", ', '.join(dep.allowed_hosts))
3844 sys.stdout.flush()
3845 raise gclient_utils.Error(
3846 'dependencies from disallowed hosts; check your DEPS file.')
3847 return 0
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003848
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003849
3850@subcommand.epilog("""For more information on what metrics are we collecting and
Edward Lemur8a2e3312018-07-12 21:15:09 +00003851why, please read metrics.README.md or visit https://bit.ly/2ufRS4p""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003852@metrics.collector.collect_metrics('gclient metrics')
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003853def CMDmetrics(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003854 """Reports, and optionally modifies, the status of metric collection."""
3855 parser.add_option('--opt-in',
3856 action='store_true',
3857 dest='enable_metrics',
3858 help='Opt-in to metrics collection.',
3859 default=None)
3860 parser.add_option('--opt-out',
3861 action='store_false',
3862 dest='enable_metrics',
3863 help='Opt-out of metrics collection.')
3864 options, args = parser.parse_args(args)
3865 if args:
3866 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3867 if not metrics.collector.config.is_googler:
3868 print("You're not a Googler. Metrics collection is disabled for you.")
3869 return 0
3870
3871 if options.enable_metrics is not None:
3872 metrics.collector.config.opted_in = options.enable_metrics
3873
3874 if metrics.collector.config.opted_in is None:
3875 print("You haven't opted in or out of metrics collection.")
3876 elif metrics.collector.config.opted_in:
3877 print("You have opted in. Thanks!")
3878 else:
3879 print("You have opted out. Please consider opting in.")
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003880 return 0
3881
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003882
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003883class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003884 gclientfile_default = os.environ.get('GCLIENT_FILE', '.gclient')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003885
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003886 def __init__(self, **kwargs):
3887 optparse.OptionParser.__init__(self,
3888 version='%prog ' + __version__,
3889 **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003890
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003891 # Some arm boards have issues with parallel sync.
3892 if platform.machine().startswith('arm'):
3893 jobs = 1
3894 else:
3895 jobs = max(8, gclient_utils.NumLocalCpus())
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003896
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003897 self.add_option(
3898 '-j',
3899 '--jobs',
3900 default=jobs,
3901 type='int',
3902 help='Specify how many SCM commands can run in parallel; defaults to '
3903 '%default on this machine')
3904 self.add_option(
3905 '-v',
3906 '--verbose',
3907 action='count',
3908 default=0,
3909 help='Produces additional output for diagnostics. Can be used up to '
3910 'three times for more logging info.')
3911 self.add_option('--gclientfile',
3912 dest='config_filename',
3913 help='Specify an alternate %s file' %
3914 self.gclientfile_default)
3915 self.add_option(
3916 '--spec',
3917 help='create a gclient file containing the provided string. Due to '
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003918 'Cygwin/Python brokenness, it can\'t contain any newlines.')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003919 self.add_option('--no-nag-max',
3920 default=False,
3921 action='store_true',
3922 help='Ignored for backwards compatibility.')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003923
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003924 def parse_args(self, args=None, _values=None):
3925 """Integrates standard options processing."""
3926 # Create an optparse.Values object that will store only the actual
3927 # passed options, without the defaults.
3928 actual_options = optparse.Values()
3929 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
3930 # Create an optparse.Values object with the default options.
3931 options = optparse.Values(self.get_default_values().__dict__)
3932 # Update it with the options passed by the user.
3933 options._update_careful(actual_options.__dict__)
3934 # Store the options passed by the user in an _actual_options attribute.
3935 # We store only the keys, and not the values, since the values can
3936 # contain arbitrary information, which might be PII.
3937 metrics.collector.add('arguments', list(actual_options.__dict__))
Edward Lemur3298e7b2018-07-17 18:21:27 +00003938
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003939 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
3940 logging.basicConfig(
3941 level=levels[min(options.verbose,
3942 len(levels) - 1)],
3943 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
3944 if options.config_filename and options.spec:
3945 self.error('Cannot specify both --gclientfile and --spec')
3946 if (options.config_filename and options.config_filename !=
3947 os.path.basename(options.config_filename)):
3948 self.error('--gclientfile target must be a filename, not a path')
3949 if not options.config_filename:
3950 options.config_filename = self.gclientfile_default
3951 options.entries_filename = options.config_filename + '_entries'
3952 if options.jobs < 1:
3953 self.error('--jobs must be 1 or higher')
maruel@chromium.org0895b752011-08-26 20:40:33 +00003954
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003955 # These hacks need to die.
3956 if not hasattr(options, 'revisions'):
3957 # GClient.RunOnDeps expects it even if not applicable.
3958 options.revisions = []
3959 if not hasattr(options, 'experiments'):
3960 options.experiments = []
3961 if not hasattr(options, 'head'):
3962 options.head = None
3963 if not hasattr(options, 'nohooks'):
3964 options.nohooks = True
3965 if not hasattr(options, 'noprehooks'):
3966 options.noprehooks = True
3967 if not hasattr(options, 'deps_os'):
3968 options.deps_os = None
3969 if not hasattr(options, 'force'):
3970 options.force = None
3971 return (options, args)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003972
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003973
3974def disable_buffering():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003975 # Make stdout auto-flush so buildbot doesn't kill us during lengthy
3976 # operations. Python as a strong tendency to buffer sys.stdout.
3977 sys.stdout = gclient_utils.MakeFileAutoFlush(sys.stdout)
3978 # Make stdout annotated with the thread ids.
3979 sys.stdout = gclient_utils.MakeFileAnnotated(sys.stdout)
maruel@chromium.org0895b752011-08-26 20:40:33 +00003980
3981
Elly Fong-Jones7b294392019-04-18 18:32:10 +00003982def path_contains_tilde():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003983 for element in os.environ['PATH'].split(os.pathsep):
3984 if element.startswith('~') and os.path.abspath(
3985 os.path.realpath(
3986 os.path.expanduser(element))) == DEPOT_TOOLS_DIR:
3987 return True
3988 return False
Elly Fong-Jones7b294392019-04-18 18:32:10 +00003989
3990
3991def can_run_gclient_and_helpers():
Gavin Mak7f5b53f2023-09-07 18:13:01 +00003992 if sys.version_info[0] < 3:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003993 print('\nYour python version %s is unsupported, please upgrade.\n' %
3994 sys.version.split(' ', 1)[0],
3995 file=sys.stderr)
3996 return False
3997 if not sys.executable:
3998 print('\nPython cannot find the location of it\'s own executable.\n',
3999 file=sys.stderr)
4000 return False
4001 if path_contains_tilde():
4002 print(
4003 '\nYour PATH contains a literal "~", which works in some shells ' +
4004 'but will break when python tries to run subprocesses. ' +
4005 'Replace the "~" with $HOME.\n' + 'See https://crbug.com/952865.\n',
4006 file=sys.stderr)
4007 return False
4008 return True
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004009
4010
4011def main(argv):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004012 """Doesn't parse the arguments here, just find the right subcommand to
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004013 execute."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004014 if not can_run_gclient_and_helpers():
4015 return 2
4016 fix_encoding.fix_encoding()
4017 disable_buffering()
4018 setup_color.init()
4019 dispatcher = subcommand.CommandDispatcher(__name__)
4020 try:
4021 return dispatcher.execute(OptionParser(), argv)
4022 except KeyboardInterrupt:
4023 gclient_utils.GClientChildren.KillAllRemainingChildren()
4024 raise
4025 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
4026 print('Error: %s' % str(e), file=sys.stderr)
4027 return 1
4028 finally:
4029 gclient_utils.PrintWarnings()
4030 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004031
4032
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00004033if '__main__' == __name__:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004034 with metrics.collector.print_notice_and_exit():
4035 sys.exit(main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004036
4037# vim: ts=2:sw=2:tw=80:et: