blob: e7da3e5a8b0d41d5ef3dcce16a9fb07382c551b3 [file] [log] [blame]
Josip Sokceviced6aa2b2022-01-26 18:14:05 +00001#!/usr/bin/env python3
thakis@chromium.org4f474b62012-01-18 01:31:29 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.orgba551772010-02-03 18:21:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
agabled437d762016-10-17 09:35:11 -07005"""Meta checkout dependency manager for Git."""
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006# Files
7# .gclient : Current client configuration, written by 'config' command.
8# Format is a Python script defining 'solutions', a list whose
9# entries each are maps binding the strings "name" and "url"
10# to strings specifying the name and location of the client
11# module, as well as "custom_deps" to a map similar to the
12# deps section of the DEPS file below, as well as
13# "custom_hooks" to a list similar to the hooks sections of
14# the DEPS file below.
15# .gclient_entries : A cache constructed by 'update' command. Format is a
16# Python script defining 'entries', a list of the names
17# of all modules in the client
18# <module>/DEPS : Python script defining var 'deps' as a map from each
19# requisite submodule name to a URL where it can be found (via
20# one SCM)
21#
22# Hooks
23# .gclient and DEPS files may optionally contain a list named "hooks" to
24# allow custom actions to be performed based on files that have changed in the
25# working copy as a result of a "sync"/"update" or "revert" operation. This
26# can be prevented by using --nohooks (hooks run by default). Hooks can also
27# be forced to run with the "runhooks" operation. If "sync" is run with
28# --force, all known but not suppressed hooks will run regardless of the state
29# of the working copy.
30#
31# Each item in a "hooks" list is a dict, containing these two keys:
32# "pattern" The associated value is a string containing a regular
33# expression. When a file whose pathname matches the expression
34# is checked out, updated, or reverted, the hook's "action" will
35# run.
36# "action" A list describing a command to run along with its arguments, if
37# any. An action command will run at most one time per gclient
38# invocation, regardless of how many files matched the pattern.
39# The action is executed in the same directory as the .gclient
40# file. If the first item in the list is the string "python",
41# the current Python interpreter (sys.executable) will be used
42# to run the command. If the list contains string
43# "$matching_files" it will be removed from the list and the list
44# will be extended by the list of matching files.
45# "name" An optional string specifying the group to which a hook belongs
46# for overriding and organizing.
47#
48# Example:
49# hooks = [
50# { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
51# "action": ["python", "image_indexer.py", "--all"]},
52# { "pattern": ".",
53# "name": "gyp",
54# "action": ["python", "src/build/gyp_chromium"]},
55# ]
56#
borenet@google.com2d1ee9e2013-10-15 08:13:16 +000057# Pre-DEPS Hooks
58# DEPS files may optionally contain a list named "pre_deps_hooks". These are
59# the same as normal hooks, except that they run before the DEPS are
60# processed. Pre-DEPS run with "sync" and "revert" unless the --noprehooks
61# flag is used.
rdsmith@chromium.orgd9591f02014-02-05 19:28:20 +000062#
maruel@chromium.org39c0b222013-08-17 16:57:01 +000063# Specifying a target OS
64# An optional key named "target_os" may be added to a gclient file to specify
65# one or more additional operating systems that should be considered when
Scott Grahamc4826742017-05-11 16:59:23 -070066# processing the deps_os/hooks_os dict of a DEPS file.
maruel@chromium.org39c0b222013-08-17 16:57:01 +000067#
68# Example:
69# target_os = [ "android" ]
70#
71# If the "target_os_only" key is also present and true, then *only* the
72# operating systems listed in "target_os" will be used.
73#
74# Example:
75# target_os = [ "ios" ]
76# target_os_only = True
Tom Andersonc31ae0b2018-02-06 14:48:56 -080077#
78# Specifying a target CPU
79# To specify a target CPU, the variables target_cpu and target_cpu_only
Quinten Yearsley925cedb2020-04-13 17:49:39 +000080# are available and are analogous to target_os and target_os_only.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000081
maruel@chromium.org39c0b222013-08-17 16:57:01 +000082__version__ = '0.7'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000083
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000084import copy
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +000085import json
maruel@chromium.org754960e2009-09-21 12:31:05 +000086import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000087import optparse
88import os
bradnelson@google.com4949dab2012-04-19 16:41:07 +000089import platform
maruel@chromium.org621939b2010-08-10 20:12:00 +000090import posixpath
msb@chromium.org2e38de72009-09-28 17:04:47 +000091import pprint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000092import re
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000093import sys
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000094import time
Gavin Mak65c49b12023-08-24 18:06:42 +000095import urllib.parse
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000096
Yiwei Zhang52353702023-09-18 15:53:52 +000097from collections.abc import Collection, Mapping, Sequence
98
Tom Andersonc31ae0b2018-02-06 14:48:56 -080099import detect_host_arch
maruel@chromium.org35625c72011-03-23 17:34:02 +0000100import fix_encoding
Aravind Vasudevanb8164182023-08-25 21:49:12 +0000101import git_common
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200102import gclient_eval
Nico Weber09e0b382019-03-11 16:54:07 +0000103import gclient_paths
Gavin Mak65c49b12023-08-24 18:06:42 +0000104import gclient_scm
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000105import gclient_utils
szager@chromium.org848fd492014-04-09 19:06:44 +0000106import git_cache
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000107import metrics
Edward Lemur40764b02018-07-20 18:50:29 +0000108import metrics_utils
Joanna Wange36c6bb2023-08-30 22:09:59 +0000109import scm as scm_git
Gavin Mak65c49b12023-08-24 18:06:42 +0000110import setup_color
maruel@chromium.org39c0b222013-08-17 16:57:01 +0000111import subcommand
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000112import subprocess2
Gavin Mak65c49b12023-08-24 18:06:42 +0000113from third_party.repo.progress import Progress
Aaron Gableac9b0f32019-04-18 17:38:37 +0000114
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000115# TODO: Should fix these warnings.
116# pylint: disable=line-too-long
Aaron Gableac9b0f32019-04-18 17:38:37 +0000117
Henrique Ferreiro4ef32212019-04-29 23:32:31 +0000118DEPOT_TOOLS_DIR = os.path.dirname(os.path.abspath(os.path.realpath(__file__)))
119
Robert Iannuccia19649b2018-06-29 16:31:45 +0000120# Singleton object to represent an unset cache_dir (as opposed to a disabled
121# one, e.g. if a spec explicitly says `cache_dir = None`.)
122UNSET_CACHE_DIR = object()
123
Joanna Wang01870792022-08-01 19:02:57 +0000124PREVIOUS_CUSTOM_VARS_FILE = '.gclient_previous_custom_vars'
125PREVIOUS_SYNC_COMMITS_FILE = '.gclient_previous_sync_commits'
Robert Iannuccia19649b2018-06-29 16:31:45 +0000126
Joanna Wangf3edc502022-07-20 00:12:10 +0000127PREVIOUS_SYNC_COMMITS = 'GCLIENT_PREVIOUS_SYNC_COMMITS'
Joanna Wang66286612022-06-30 19:59:13 +0000128
Joanna Wanga84a16b2022-07-27 18:52:17 +0000129NO_SYNC_EXPERIMENT = 'no-sync'
130
Gavin Mak50b27a52023-09-19 22:44:59 +0000131PRECOMMIT_HOOK_VAR = 'GCLIENT_PRECOMMIT'
132
Joanna Wang66286612022-06-30 19:59:13 +0000133
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200134class GNException(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000135 pass
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200136
137
Aravind Vasudevaned935cf2023-08-24 23:52:20 +0000138def ToGNString(value):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000139 """Returns a stringified GN equivalent of the Python value."""
140 if isinstance(value, str):
141 if value.find('\n') >= 0:
142 raise GNException("Trying to print a string with a newline in it.")
143 return '"' + \
144 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
145 '"'
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200146
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000147 if isinstance(value, bool):
148 if value:
149 return "true"
150 return "false"
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200151
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000152 # NOTE: some type handling removed compared to chromium/src copy.
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200153
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000154 raise GNException("Unsupported type when printing to GN.")
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200155
156
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200157class Hook(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000158 """Descriptor of command ran before/after sync or on demand."""
159 def __init__(self,
160 action,
161 pattern=None,
162 name=None,
163 cwd=None,
164 condition=None,
165 variables=None,
166 verbose=False,
167 cwd_base=None):
168 """Constructor.
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200169
170 Arguments:
Gavin Mak65c49b12023-08-24 18:06:42 +0000171 action (list of str): argv of the command to run
172 pattern (str regex): noop with git; deprecated
173 name (str): optional name; no effect on operation
174 cwd (str): working directory to use
175 condition (str): condition when to run the hook
Paweł Hajdan, Jr032d5452017-06-22 20:43:53 +0200176 variables (dict): variables for evaluating the condition
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200177 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000178 self._action = gclient_utils.freeze(action)
179 self._pattern = pattern
180 self._name = name
181 self._cwd = cwd
182 self._condition = condition
183 self._variables = variables
184 self._verbose = verbose
185 self._cwd_base = cwd_base
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200186
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000187 @staticmethod
188 def from_dict(d,
189 variables=None,
190 verbose=False,
191 conditions=None,
192 cwd_base=None):
193 """Creates a Hook instance from a dict like in the DEPS file."""
194 # Merge any local and inherited conditions.
195 gclient_eval.UpdateCondition(d, 'and', conditions)
196 return Hook(
197 d['action'],
198 d.get('pattern'),
199 d.get('name'),
200 d.get('cwd'),
201 d.get('condition'),
202 variables=variables,
203 # Always print the header if not printing to a TTY.
204 verbose=verbose or not setup_color.IS_TTY,
205 cwd_base=cwd_base)
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200206
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000207 @property
208 def action(self):
209 return self._action
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200210
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000211 @property
212 def pattern(self):
213 return self._pattern
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200214
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000215 @property
216 def name(self):
217 return self._name
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200218
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000219 @property
220 def condition(self):
221 return self._condition
Paweł Hajdan, Jrecf53fe2017-09-29 18:28:49 +0200222
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000223 @property
224 def effective_cwd(self):
225 cwd = self._cwd_base
226 if self._cwd:
227 cwd = os.path.join(cwd, self._cwd)
228 return cwd
Corentin Walleza68660d2018-09-10 17:33:24 +0000229
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000230 def matches(self, file_list):
231 """Returns true if the pattern matches any of files in the list."""
232 if not self._pattern:
233 return True
234 pattern = re.compile(self._pattern)
235 return bool([f for f in file_list if pattern.search(f)])
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200236
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000237 def run(self):
238 """Executes the hook's command (provided the condition is met)."""
239 if (self._condition and not gclient_eval.EvaluateCondition(
240 self._condition, self._variables)):
241 return
Paweł Hajdan, Jr032d5452017-06-22 20:43:53 +0200242
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000243 cmd = list(self._action)
Paweł Hajdan, Jrfc6196b2017-07-27 13:15:25 +0200244
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000245 if cmd[0] == 'vpython3' and _detect_host_os() == 'win':
246 cmd[0] += '.bat'
Paweł Hajdan, Jrc9364392017-06-14 17:11:56 +0200247
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000248 exit_code = 2
249 try:
250 start_time = time.time()
251 gclient_utils.CheckCallAndFilter(cmd,
252 cwd=self.effective_cwd,
253 print_stdout=True,
254 show_header=True,
255 always_show_header=self._verbose)
256 exit_code = 0
257 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
258 # Use a discrete exit status code of 2 to indicate that a hook
259 # action failed. Users of this script may wish to treat hook action
260 # failures differently from VC failures.
261 print('Error: %s' % str(e), file=sys.stderr)
262 sys.exit(exit_code)
263 finally:
264 elapsed_time = time.time() - start_time
265 metrics.collector.add_repeated(
266 'hooks', {
267 'action':
268 gclient_utils.CommandToStr(cmd),
269 'name':
270 self._name,
271 'cwd':
272 os.path.relpath(os.path.normpath(self.effective_cwd),
273 self._cwd_base),
274 'condition':
275 self._condition,
276 'execution_time':
277 elapsed_time,
278 'exit_code':
279 exit_code,
280 })
281 if elapsed_time > 10:
282 print("Hook '%s' took %.2f secs" %
283 (gclient_utils.CommandToStr(cmd), elapsed_time))
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200284
285
Paweł Hajdan, Jrfc6196b2017-07-27 13:15:25 +0200286class DependencySettings(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000287 """Immutable configuration settings."""
288 def __init__(self, parent, url, managed, custom_deps, custom_vars,
289 custom_hooks, deps_file, should_process, relative, condition):
290 # These are not mutable:
291 self._parent = parent
292 self._deps_file = deps_file
293 self._url = url
294 # The condition as string (or None). Useful to keep e.g. for flatten.
295 self._condition = condition
296 # 'managed' determines whether or not this dependency is synced/updated
297 # by gclient after gclient checks it out initially. The difference
298 # between 'managed' and 'should_process' is that the user specifies
299 # 'managed' via the --unmanaged command-line flag or a .gclient config,
300 # where 'should_process' is dynamically set by gclient if it goes over
301 # its recursion limit and controls gclient's behavior so it does not
302 # misbehave.
303 self._managed = managed
304 self._should_process = should_process
305 # If this is a recursed-upon sub-dependency, and the parent has
306 # use_relative_paths set, then this dependency should check out its own
307 # dependencies relative to that parent's path for this, rather than
308 # relative to the .gclient file.
309 self._relative = relative
310 # This is a mutable value which has the list of 'target_os' OSes listed
311 # in the current deps file.
312 self.local_target_os = None
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000313
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000314 # These are only set in .gclient and not in DEPS files.
315 self._custom_vars = custom_vars or {}
316 self._custom_deps = custom_deps or {}
317 self._custom_hooks = custom_hooks or []
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000318
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000319 # Post process the url to remove trailing slashes.
320 if isinstance(self.url, str):
321 # urls are sometime incorrectly written as proto://host/path/@rev.
322 # Replace it to proto://host/path@rev.
323 self.set_url(self.url.replace('/@', '@'))
324 elif not isinstance(self.url, (None.__class__)):
325 raise gclient_utils.Error(
326 ('dependency url must be either string or None, '
327 'instead of %s') % self.url.__class__.__name__)
Edward Lemure7273d22018-05-10 19:13:51 -0400328
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000329 # Make any deps_file path platform-appropriate.
330 if self._deps_file:
331 for sep in ['/', '\\']:
332 self._deps_file = self._deps_file.replace(sep, os.sep)
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000333
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000334 @property
335 def deps_file(self):
336 return self._deps_file
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000337
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000338 @property
339 def managed(self):
340 return self._managed
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000341
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000342 @property
343 def parent(self):
344 return self._parent
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000345
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000346 @property
347 def root(self):
348 """Returns the root node, a GClient object."""
349 if not self.parent:
350 # This line is to signal pylint that it could be a GClient instance.
351 return self or GClient(None, None)
352 return self.parent.root
maruel@chromium.org8c0d9582011-10-03 21:36:01 +0000353
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000354 @property
355 def should_process(self):
356 """True if this dependency should be processed, i.e. checked out."""
357 return self._should_process
Michael Mossd683d7c2018-06-15 05:05:17 +0000358
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000359 @property
360 def custom_vars(self):
361 return self._custom_vars.copy()
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000362
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000363 @property
364 def custom_deps(self):
365 return self._custom_deps.copy()
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000366
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000367 @property
368 def custom_hooks(self):
369 return self._custom_hooks[:]
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000370
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000371 @property
372 def url(self):
373 """URL after variable expansion."""
374 return self._url
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000375
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000376 @property
377 def condition(self):
378 return self._condition
Paweł Hajdan, Jrf69860b2017-06-05 20:24:28 +0200379
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000380 @property
381 def target_os(self):
382 if self.local_target_os is not None:
383 return tuple(set(self.local_target_os).union(self.parent.target_os))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000384
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000385 return self.parent.target_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +0000386
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000387 @property
388 def target_cpu(self):
389 return self.parent.target_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -0800390
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000391 def set_url(self, url):
392 self._url = url
Edward Lemure7273d22018-05-10 19:13:51 -0400393
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000394 def get_custom_deps(self, name, url):
395 """Returns a custom deps if applicable."""
396 if self.parent:
397 url = self.parent.get_custom_deps(name, url)
398 # None is a valid return value to disable a dependency.
399 return self.custom_deps.get(name, url)
maruel@chromium.org8c0d9582011-10-03 21:36:01 +0000400
maruel@chromium.org064186c2011-09-27 23:53:33 +0000401
402class Dependency(gclient_utils.WorkItem, DependencySettings):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000403 """Object that represents a dependency checkout."""
404 def __init__(self,
405 parent,
406 name,
407 url,
408 managed,
409 custom_deps,
410 custom_vars,
411 custom_hooks,
412 deps_file,
413 should_process,
414 should_recurse,
415 relative,
416 condition,
417 protocol='https',
418 git_dependencies_state=gclient_eval.DEPS,
419 print_outbuf=False):
420 gclient_utils.WorkItem.__init__(self, name)
421 DependencySettings.__init__(self, parent, url, managed, custom_deps,
422 custom_vars, custom_hooks, deps_file,
423 should_process, relative, condition)
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000424
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000425 # This is in both .gclient and DEPS files:
426 self._deps_hooks = []
maruel@chromium.org68988972011-09-20 14:11:42 +0000427
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000428 self._pre_deps_hooks = []
maruel@chromium.org68988972011-09-20 14:11:42 +0000429
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000430 # Calculates properties:
431 self._dependencies = []
432 self._vars = {}
borenet@google.com2d1ee9e2013-10-15 08:13:16 +0000433
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000434 # A cache of the files affected by the current operation, necessary for
435 # hooks.
436 self._file_list = []
437 # List of host names from which dependencies are allowed.
438 # Default is an empty set, meaning unspecified in DEPS file, and hence
439 # all hosts will be allowed. Non-empty set means allowlist of hosts.
440 # allowed_hosts var is scoped to its DEPS file, and so it isn't
441 # recursive.
442 self._allowed_hosts = frozenset()
443 self._gn_args_from = None
444 # Spec for .gni output to write (if any).
445 self._gn_args_file = None
446 self._gn_args = []
447 # If it is not set to True, the dependency wasn't processed for its
448 # child dependency, i.e. its DEPS wasn't read.
449 self._deps_parsed = False
450 # This dependency has been processed, i.e. checked out
451 self._processed = False
452 # This dependency had its pre-DEPS hooks run
453 self._pre_deps_hooks_ran = False
454 # This dependency had its hook run
455 self._hooks_ran = False
456 # This is the scm used to checkout self.url. It may be used by
457 # dependencies to get the datetime of the revision we checked out.
458 self._used_scm = None
459 self._used_revision = None
460 # The actual revision we ended up getting, or None if that information
461 # is unavailable
462 self._got_revision = None
463 # Whether this dependency should use relative paths.
464 self._use_relative_paths = False
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200465
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000466 # recursedeps is a mutable value that selectively overrides the default
467 # 'no recursion' setting on a dep-by-dep basis.
468 #
469 # It will be a dictionary of {deps_name: depfile_namee}
470 self.recursedeps = {}
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000471
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000472 # Whether we should process this dependency's DEPS file.
473 self._should_recurse = should_recurse
Edward Lemurfbb06aa2018-06-11 20:43:06 +0000474
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000475 # Whether we should sync git/cipd dependencies and hooks from the
476 # DEPS file.
477 # This is set based on skip_sync_revisions and must be done
478 # after the patch refs are applied.
479 # If this is False, we will still run custom_hooks and process
480 # custom_deps, if any.
481 self._should_sync = True
Edward Lemure7273d22018-05-10 19:13:51 -0400482
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000483 self._OverrideUrl()
484 # This is inherited from WorkItem. We want the URL to be a resource.
485 if self.url and isinstance(self.url, str):
486 # The url is usually given to gclient either as https://blah@123
487 # or just https://blah. The @123 portion is irrelevant.
488 self.resources.append(self.url.split('@')[0])
Joanna Wang18af7ef2022-07-01 16:51:00 +0000489
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000490 # Controls whether we want to print git's output when we first clone the
491 # dependency
492 self.print_outbuf = print_outbuf
cmp@chromium.orge84ac912014-06-30 23:14:35 +0000493
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000494 self.protocol = protocol
495 self.git_dependencies_state = git_dependencies_state
Edward Lemur231f5ea2018-01-31 19:02:36 +0100496
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000497 if not self.name and self.parent:
498 raise gclient_utils.Error('Dependency without name')
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000499
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000500 def _OverrideUrl(self):
501 """Resolves the parsed url from the parent hierarchy."""
502 parsed_url = self.get_custom_deps(
503 self._name.replace(os.sep, posixpath.sep) \
504 if self._name else self._name, self.url)
505 if parsed_url != self.url:
506 logging.info('Dependency(%s)._OverrideUrl(%s) -> %s', self._name,
507 self.url, parsed_url)
508 self.set_url(parsed_url)
509 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000510
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000511 if self.url is None:
512 logging.info('Dependency(%s)._OverrideUrl(None) -> None',
513 self._name)
514 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000515
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000516 if not isinstance(self.url, str):
517 raise gclient_utils.Error('Unknown url type')
Michael Mossd683d7c2018-06-15 05:05:17 +0000518
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000519 # self.url is a local path
520 path, at, rev = self.url.partition('@')
521 if os.path.isdir(path):
522 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000523
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000524 # self.url is a URL
525 parsed_url = urllib.parse.urlparse(self.url)
526 if parsed_url[0] or re.match(r'^\w+\@[\w\.-]+\:[\w\/]+', parsed_url[2]):
527 return
Edward Lemur1f392b82019-11-15 22:40:11 +0000528
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000529 # self.url is relative to the parent's URL.
530 if not path.startswith('/'):
531 raise gclient_utils.Error(
532 'relative DEPS entry \'%s\' must begin with a slash' % self.url)
Edward Lemur1f392b82019-11-15 22:40:11 +0000533
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000534 parent_url = self.parent.url
535 parent_path = self.parent.url.split('@')[0]
536 if os.path.isdir(parent_path):
537 # Parent's URL is a local path. Get parent's URL dirname and append
538 # self.url.
539 parent_path = os.path.dirname(parent_path)
540 parsed_url = parent_path + path.replace('/', os.sep) + at + rev
541 else:
542 # Parent's URL is a URL. Get parent's URL, strip from the last '/'
543 # (equivalent to unix dirname) and append self.url.
544 parsed_url = parent_url[:parent_url.rfind('/')] + self.url
Edward Lemur1f392b82019-11-15 22:40:11 +0000545
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000546 logging.info('Dependency(%s)._OverrideUrl(%s) -> %s', self.name,
547 self.url, parsed_url)
548 self.set_url(parsed_url)
Edward Lemur1f392b82019-11-15 22:40:11 +0000549
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000550 def PinToActualRevision(self):
551 """Updates self.url to the revision checked out on disk."""
552 if self.url is None:
553 return
554 url = None
555 scm = self.CreateSCM()
556 if scm.name == 'cipd':
557 revision = scm.revinfo(None, None, None)
558 package = self.GetExpandedPackageName()
559 url = '%s/p/%s/+/%s' % (scm.GetActualRemoteURL(None), package,
560 revision)
Edward Lemur1f392b82019-11-15 22:40:11 +0000561
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000562 if os.path.isdir(scm.checkout_path):
563 revision = scm.revinfo(None, None, None)
564 url = '%s@%s' % (gclient_utils.SplitUrlRevision(
565 self.url)[0], revision)
566 self.set_url(url)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +0000567
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000568 def ToLines(self):
569 # () -> Sequence[str]
570 """Returns strings representing the deps (info, graphviz line)"""
571 s = []
572 condition_part = ([' "condition": %r,' %
573 self.condition] if self.condition else [])
574 s.extend([
575 ' # %s' % self.hierarchy(include_url=False),
576 ' "%s": {' % (self.name, ),
577 ' "url": "%s",' % (self.url, ),
578 ] + condition_part + [
579 ' },',
580 '',
581 ])
582 return s
Edward Lemure7273d22018-05-10 19:13:51 -0400583
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000584 @property
585 def requirements(self):
586 """Calculate the list of requirements."""
587 requirements = set()
588 # self.parent is implicitly a requirement. This will be recursive by
589 # definition.
590 if self.parent and self.parent.name:
591 requirements.add(self.parent.name)
John Budorick0f7b2002018-01-19 15:46:17 -0800592
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000593 # For a tree with at least 2 levels*, the leaf node needs to depend
594 # on the level higher up in an orderly way.
595 # This becomes messy for >2 depth as the DEPS file format is a
596 # dictionary, thus unsorted, while the .gclient format is a list thus
597 # sorted.
598 #
599 # Interestingly enough, the following condition only works in the case
600 # we want: self is a 2nd level node. 3rd level node wouldn't need this
601 # since they already have their parent as a requirement.
602 if self.parent and self.parent.parent and not self.parent.parent.parent:
603 requirements |= set(i.name for i in self.root.dependencies
604 if i.name)
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000605
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000606 if self.name:
607 requirements |= set(
608 obj.name for obj in self.root.subtree(False)
609 if (obj is not self and obj.name
610 and self.name.startswith(posixpath.join(obj.name, ''))))
611 requirements = tuple(sorted(requirements))
612 logging.info('Dependency(%s).requirements = %s' %
613 (self.name, requirements))
614 return requirements
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000615
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000616 @property
617 def should_recurse(self):
618 return self._should_recurse
maruel@chromium.org470b5432011-10-11 18:18:19 +0000619
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000620 def verify_validity(self):
621 """Verifies that this Dependency is fine to add as a child of another one.
maruel@chromium.org470b5432011-10-11 18:18:19 +0000622
623 Returns True if this entry should be added, False if it is a duplicate of
624 another entry.
625 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000626 logging.info('Dependency(%s).verify_validity()' % self.name)
627 if self.name in [s.name for s in self.parent.dependencies]:
628 raise gclient_utils.Error(
629 'The same name "%s" appears multiple times in the deps section'
630 % self.name)
631 if not self.should_process:
632 # Return early, no need to set requirements.
633 return not any(d.name == self.name for d in self.root.subtree(True))
maruel@chromium.org470b5432011-10-11 18:18:19 +0000634
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000635 # This require a full tree traversal with locks.
636 siblings = [d for d in self.root.subtree(False) if d.name == self.name]
637 for sibling in siblings:
638 # Allow to have only one to be None or ''.
639 if self.url != sibling.url and bool(self.url) == bool(sibling.url):
640 raise gclient_utils.Error(
641 ('Dependency %s specified more than once:\n'
642 ' %s [%s]\n'
643 'vs\n'
644 ' %s [%s]') % (self.name, sibling.hierarchy(),
645 sibling.url, self.hierarchy(), self.url))
646 # In theory we could keep it as a shadow of the other one. In
647 # practice, simply ignore it.
648 logging.warning("Won't process duplicate dependency %s" % sibling)
649 return False
650 return True
maruel@chromium.org064186c2011-09-27 23:53:33 +0000651
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000652 def _postprocess_deps(self, deps, rel_prefix):
653 # type: (Mapping[str, Mapping[str, str]], str) ->
654 # Mapping[str, Mapping[str, str]]
655 """Performs post-processing of deps compared to what's in the DEPS file."""
656 # If we don't need to sync, only process custom_deps, if any.
657 if not self._should_sync:
658 if not self.custom_deps:
659 return {}
Paweł Hajdan, Jr4426eaf2017-06-13 12:25:47 +0200660
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000661 processed_deps = {}
662 for dep_name, dep_info in self.custom_deps.items():
663 if dep_info and not dep_info.endswith('@unmanaged'):
664 if dep_name in deps:
665 # custom_deps that should override an existing deps gets
666 # applied in the Dependency itself with _OverrideUrl().
667 processed_deps[dep_name] = deps[dep_name]
668 else:
669 processed_deps[dep_name] = {
670 'url': dep_info,
671 'dep_type': 'git'
672 }
673 else:
674 processed_deps = dict(deps)
Joanna Wang18af7ef2022-07-01 16:51:00 +0000675
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000676 # If a line is in custom_deps, but not in the solution, we want to
677 # append this line to the solution.
678 for dep_name, dep_info in self.custom_deps.items():
679 # Don't add it to the solution for the values of "None" and
680 # "unmanaged" in order to force these kinds of custom_deps to
681 # act as revision overrides (via revision_overrides). Having
682 # them function as revision overrides allows them to be applied
683 # to recursive dependencies. https://crbug.com/1031185
684 if (dep_name not in processed_deps and dep_info
685 and not dep_info.endswith('@unmanaged')):
686 processed_deps[dep_name] = {
687 'url': dep_info,
688 'dep_type': 'git'
689 }
Edward Lemur16f4bad2018-05-16 16:53:49 -0400690
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000691 # Make child deps conditional on any parent conditions. This ensures
692 # that, when flattened, recursed entries have the correct restrictions,
693 # even if not explicitly set in the recursed DEPS file. For instance, if
694 # "src/ios_foo" is conditional on "checkout_ios=True", then anything
695 # recursively included by "src/ios_foo/DEPS" should also require
696 # "checkout_ios=True".
697 if self.condition:
698 for value in processed_deps.values():
699 gclient_eval.UpdateCondition(value, 'and', self.condition)
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200700
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000701 if not rel_prefix:
702 return processed_deps
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200703
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000704 logging.warning('use_relative_paths enabled.')
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200705 rel_deps = {}
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000706 for d, url in processed_deps.items():
707 # normpath is required to allow DEPS to use .. in their
708 # dependency local path.
709 # We are following the same pattern when use_relative_paths = False,
710 # which uses slashes.
711 rel_deps[os.path.normpath(os.path.join(rel_prefix, d)).replace(
712 os.path.sep, '/')] = url
713 logging.warning('Updating deps by prepending %s.', rel_prefix)
714 return rel_deps
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200715
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000716 def _deps_to_objects(self, deps, use_relative_paths):
717 # type: (Mapping[str, Mapping[str, str]], bool) -> Sequence[Dependency]
718 """Convert a deps dict to a list of Dependency objects."""
719 deps_to_add = []
720 cached_conditions = {}
721 for name, dep_value in deps.items():
722 should_process = self.should_process
723 if dep_value is None:
724 continue
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200725
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000726 condition = dep_value.get('condition')
727 dep_type = dep_value.get('dep_type')
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000728
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000729 if condition and not self._get_option('process_all_deps', False):
730 if condition not in cached_conditions:
731 cached_conditions[
732 condition] = gclient_eval.EvaluateCondition(
733 condition, self.get_vars())
734 should_process = should_process and cached_conditions[condition]
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000735
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000736 # The following option is only set by the 'revinfo' command.
737 if self._get_option('ignore_dep_type', None) == dep_type:
738 continue
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000739
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000740 if dep_type == 'cipd':
741 cipd_root = self.GetCipdRoot()
742 for package in dep_value.get('packages', []):
743 deps_to_add.append(
744 CipdDependency(parent=self,
745 name=name,
746 dep_value=package,
747 cipd_root=cipd_root,
748 custom_vars=self.custom_vars,
749 should_process=should_process,
750 relative=use_relative_paths,
751 condition=condition))
752 else:
753 url = dep_value.get('url')
754 deps_to_add.append(
755 GitDependency(
756 parent=self,
757 name=name,
758 # Update URL with scheme in protocol_override
759 url=GitDependency.updateProtocol(url, self.protocol),
760 managed=True,
761 custom_deps=None,
762 custom_vars=self.custom_vars,
763 custom_hooks=None,
764 deps_file=self.recursedeps.get(name, self.deps_file),
765 should_process=should_process,
766 should_recurse=name in self.recursedeps,
767 relative=use_relative_paths,
768 condition=condition,
769 protocol=self.protocol,
770 git_dependencies_state=self.git_dependencies_state))
Michael Spang0e99b9b2020-08-12 13:34:48 +0000771
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000772 # TODO(crbug.com/1341285): Understand why we need this and remove
773 # it if we don't.
774 deps_to_add.sort(key=lambda x: x.name)
775 return deps_to_add
Corentin Walleza68660d2018-09-10 17:33:24 +0000776
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000777 def ParseDepsFile(self):
778 # type: () -> None
779 """Parses the DEPS file for this dependency."""
780 assert not self.deps_parsed
781 assert not self.dependencies
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000782
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000783 deps_content = None
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000784
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000785 # First try to locate the configured deps file. If it's missing,
786 # fallback to DEPS.
787 deps_files = [self.deps_file]
788 if 'DEPS' not in deps_files:
789 deps_files.append('DEPS')
790 for deps_file in deps_files:
791 filepath = os.path.join(self.root.root_dir, self.name, deps_file)
792 if os.path.isfile(filepath):
793 logging.info('ParseDepsFile(%s): %s file found at %s',
794 self.name, deps_file, filepath)
795 break
796 logging.info('ParseDepsFile(%s): No %s file found at %s', self.name,
797 deps_file, filepath)
borenet@google.com2d1ee9e2013-10-15 08:13:16 +0000798
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000799 if os.path.isfile(filepath):
800 deps_content = gclient_utils.FileRead(filepath)
801 logging.debug('ParseDepsFile(%s) read:\n%s', self.name,
802 deps_content)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +0000803
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000804 local_scope = {}
805 if deps_content:
806 try:
807 local_scope = gclient_eval.Parse(deps_content, filepath,
808 self.get_vars(),
809 self.get_builtin_vars())
810 except SyntaxError as e:
811 gclient_utils.SyntaxErrorToError(filepath, e)
812
813 if 'git_dependencies' in local_scope:
814 self.git_dependencies_state = local_scope['git_dependencies']
815
816 if 'allowed_hosts' in local_scope:
817 try:
818 self._allowed_hosts = frozenset(
819 local_scope.get('allowed_hosts'))
820 except TypeError: # raised if non-iterable
821 pass
822 if not self._allowed_hosts:
823 logging.warning("allowed_hosts is specified but empty %s",
824 self._allowed_hosts)
825 raise gclient_utils.Error(
826 'ParseDepsFile(%s): allowed_hosts must be absent '
827 'or a non-empty iterable' % self.name)
828
829 self._gn_args_from = local_scope.get('gclient_gn_args_from')
830 self._gn_args_file = local_scope.get('gclient_gn_args_file')
831 self._gn_args = local_scope.get('gclient_gn_args', [])
832 # It doesn't make sense to set all of these, since setting gn_args_from
833 # to another DEPS will make gclient ignore any other local gn_args*
834 # settings.
835 assert not (self._gn_args_from and self._gn_args_file), \
836 'Only specify one of "gclient_gn_args_from" or ' \
837 '"gclient_gn_args_file + gclient_gn_args".'
838
839 self._vars = local_scope.get('vars', {})
840 if self.parent:
841 for key, value in self.parent.get_vars().items():
842 if key in self._vars:
843 self._vars[key] = value
844 # Since we heavily post-process things, freeze ones which should
845 # reflect original state of DEPS.
846 self._vars = gclient_utils.freeze(self._vars)
847
848 # If use_relative_paths is set in the DEPS file, regenerate
849 # the dictionary using paths relative to the directory containing
850 # the DEPS file. Also update recursedeps if use_relative_paths is
851 # enabled.
852 # If the deps file doesn't set use_relative_paths, but the parent did
853 # (and therefore set self.relative on this Dependency object), then we
854 # want to modify the deps and recursedeps by prepending the parent
855 # directory of this dependency.
856 self._use_relative_paths = local_scope.get('use_relative_paths', False)
857 rel_prefix = None
858 if self._use_relative_paths:
859 rel_prefix = self.name
860 elif self._relative:
861 rel_prefix = os.path.dirname(self.name)
862
863 if 'recursion' in local_scope:
864 logging.warning('%s: Ignoring recursion = %d.', self.name,
865 local_scope['recursion'])
866
867 if 'recursedeps' in local_scope:
868 for ent in local_scope['recursedeps']:
869 if isinstance(ent, str):
870 self.recursedeps[ent] = self.deps_file
871 else: # (depname, depsfilename)
872 self.recursedeps[ent[0]] = ent[1]
873 logging.warning('Found recursedeps %r.', repr(self.recursedeps))
874
875 if rel_prefix:
876 logging.warning('Updating recursedeps by prepending %s.',
877 rel_prefix)
878 rel_deps = {}
879 for depname, options in self.recursedeps.items():
880 rel_deps[os.path.normpath(os.path.join(rel_prefix,
881 depname)).replace(
882 os.path.sep,
883 '/')] = options
884 self.recursedeps = rel_deps
885 # To get gn_args from another DEPS, that DEPS must be recursed into.
886 if self._gn_args_from:
887 assert self.recursedeps and self._gn_args_from in self.recursedeps, \
888 'The "gclient_gn_args_from" value must be in recursedeps.'
889
890 # If present, save 'target_os' in the local_target_os property.
891 if 'target_os' in local_scope:
892 self.local_target_os = local_scope['target_os']
893
894 deps = local_scope.get('deps', {})
895
896 # If dependencies are configured within git submodules, add them to
897 # deps. We don't add for SYNC since we expect submodules to be in sync.
898 if self.git_dependencies_state == gclient_eval.SUBMODULES:
899 deps.update(self.ParseGitSubmodules())
900
901 deps_to_add = self._deps_to_objects(
902 self._postprocess_deps(deps, rel_prefix), self._use_relative_paths)
903
904 # compute which working directory should be used for hooks
905 if local_scope.get('use_relative_hooks', False):
906 print('use_relative_hooks is deprecated, please remove it from '
907 '%s DEPS. (it was merged in use_relative_paths)' % self.name,
908 file=sys.stderr)
909
910 hooks_cwd = self.root.root_dir
911 if self._use_relative_paths:
912 hooks_cwd = os.path.join(hooks_cwd, self.name)
913 elif self._relative:
914 hooks_cwd = os.path.join(hooks_cwd, os.path.dirname(self.name))
915 logging.warning('Using hook base working directory: %s.', hooks_cwd)
916
917 # Only add all hooks if we should sync, otherwise just add custom hooks.
918 # override named sets of hooks by the custom hooks
919 hooks_to_run = []
920 if self._should_sync:
921 hook_names_to_suppress = [
922 c.get('name', '') for c in self.custom_hooks
923 ]
924 for hook in local_scope.get('hooks', []):
925 if hook.get('name', '') not in hook_names_to_suppress:
926 hooks_to_run.append(hook)
927
928 # add the replacements and any additions
929 for hook in self.custom_hooks:
930 if 'action' in hook:
931 hooks_to_run.append(hook)
932
933 if self.should_recurse and deps_to_add:
934 self._pre_deps_hooks = [
935 Hook.from_dict(hook,
936 variables=self.get_vars(),
937 verbose=True,
938 conditions=self.condition,
939 cwd_base=hooks_cwd)
940 for hook in local_scope.get('pre_deps_hooks', [])
941 ]
942
943 self.add_dependencies_and_close(deps_to_add,
944 hooks_to_run,
945 hooks_cwd=hooks_cwd)
946 logging.info('ParseDepsFile(%s) done' % self.name)
947
948 def ParseGitSubmodules(self):
949 # type: () -> Mapping[str, str]
950 """
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000951 Parses git submodules and returns a dict of path to DEPS git url entries.
952
953 e.g {<path>: <url>@<commit_hash>}
954 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000955 cwd = os.path.join(self.root.root_dir, self.name)
956 filepath = os.path.join(cwd, '.gitmodules')
957 if not os.path.isfile(filepath):
958 logging.warning('ParseGitSubmodules(): No .gitmodules found at %s',
959 filepath)
960 return {}
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000961
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000962 # Get .gitmodules fields
963 gitmodules_entries = subprocess2.check_output(
964 ['git', 'config', '--file', filepath, '-l']).decode('utf-8')
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000965
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000966 gitmodules = {}
967 for entry in gitmodules_entries.splitlines():
968 key, value = entry.split('=', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000969
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000970 # git config keys consist of section.name.key, e.g.,
971 # submodule.foo.path
972 section, submodule_key = key.split('.', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000973
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000974 # Only parse [submodule "foo"] sections from .gitmodules.
975 if section.strip() != 'submodule':
976 continue
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000977
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000978 # The name of the submodule can contain '.', hence split from the
979 # back.
980 submodule, sub_key = submodule_key.rsplit('.', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000981
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000982 if submodule not in gitmodules:
983 gitmodules[submodule] = {}
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000984
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000985 if sub_key in ('url', 'gclient-condition', 'path'):
986 gitmodules[submodule][sub_key] = value
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000987
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000988 paths = [module['path'] for module in gitmodules.values()]
989 commit_hashes = scm_git.GIT.GetSubmoduleCommits(cwd, paths)
Joanna Wang978f43d2023-08-18 00:16:07 +0000990
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000991 # Structure git submodules into a dict of DEPS git url entries.
992 submodules = {}
993 for module in gitmodules.values():
994 if self._use_relative_paths:
995 path = module['path']
996 else:
997 path = f'{self.name}/{module["path"]}'
998 # TODO(crbug.com/1471685): Temporary hack. In case of applied
999 # patches where the changes are staged but not committed, any
1000 # gitlinks from the patch are not returned by `git ls-tree`. The
1001 # path won't be found in commit_hashes. Use a temporary '0000000'
1002 # value that will be replaced with w/e is found in DEPS later.
1003 submodules[path] = {
1004 'dep_type':
1005 'git',
1006 'url':
1007 '{}@{}'.format(module['url'],
1008 commit_hashes.get(module['path'], '0000000'))
1009 }
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001010
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001011 if 'gclient-condition' in module:
1012 submodules[path]['condition'] = module['gclient-condition']
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001013
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001014 return submodules
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001015
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001016 def _get_option(self, attr, default):
1017 obj = self
1018 while not hasattr(obj, '_options'):
1019 obj = obj.parent
1020 return getattr(obj._options, attr, default)
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02001021
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001022 def add_dependencies_and_close(self, deps_to_add, hooks, hooks_cwd=None):
1023 """Adds the dependencies, hooks and mark the parsing as done."""
1024 if hooks_cwd == None:
1025 hooks_cwd = self.root.root_dir
Corentin Walleza68660d2018-09-10 17:33:24 +00001026
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001027 for dep in deps_to_add:
1028 if dep.verify_validity():
1029 self.add_dependency(dep)
1030 self._mark_as_parsed([
1031 Hook.from_dict(h,
1032 variables=self.get_vars(),
1033 verbose=self.root._options.verbose,
1034 conditions=self.condition,
1035 cwd_base=hooks_cwd) for h in hooks
1036 ])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001037
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001038 def findDepsFromNotAllowedHosts(self):
1039 """Returns a list of dependencies from not allowed hosts.
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001040
1041 If allowed_hosts is not set, allows all hosts and returns empty list.
1042 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001043 if not self._allowed_hosts:
1044 return []
1045 bad_deps = []
1046 for dep in self._dependencies:
1047 # Don't enforce this for custom_deps.
1048 if dep.name in self._custom_deps:
1049 continue
1050 if isinstance(dep.url, str):
1051 parsed_url = urllib.parse.urlparse(dep.url)
1052 if parsed_url.netloc and parsed_url.netloc not in self._allowed_hosts:
1053 bad_deps.append(dep)
1054 return bad_deps
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001055
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001056 def FuzzyMatchUrl(self, candidates):
1057 # type: (Union[Mapping[str, str], Collection[str]]) -> Optional[str]
1058 """Attempts to find this dependency in the list of candidates.
Edward Lesmesbb16e332018-03-30 17:54:51 -04001059
Edward Lemure7273d22018-05-10 19:13:51 -04001060 It looks first for the URL of this dependency in the list of
Edward Lesmesbb16e332018-03-30 17:54:51 -04001061 candidates. If it doesn't succeed, and the URL ends in '.git', it will try
1062 looking for the URL minus '.git'. Finally it will try to look for the name
1063 of the dependency.
1064
1065 Args:
Edward Lesmesbb16e332018-03-30 17:54:51 -04001066 candidates: list, dict. The list of candidates in which to look for this
1067 dependency. It can contain URLs as above, or dependency names like
1068 "src/some/dep".
1069
1070 Returns:
1071 If this dependency is not found in the list of candidates, returns None.
1072 Otherwise, it returns under which name did we find this dependency:
1073 - Its parsed url: "https://example.com/src.git'
1074 - Its parsed url minus '.git': "https://example.com/src"
1075 - Its name: "src"
1076 """
Michael Mossd683d7c2018-06-15 05:05:17 +00001077 if self.url:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001078 origin, _ = gclient_utils.SplitUrlRevision(self.url)
1079 match = gclient_utils.FuzzyMatchRepo(origin, candidates)
agabled437d762016-10-17 09:35:11 -07001080 if match:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001081 return match
1082 if self.name in candidates:
1083 return self.name
1084 return None
ilevy@chromium.org0233ac22012-11-28 20:27:02 +00001085
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001086 # Arguments number differs from overridden method
1087 # pylint: disable=arguments-differ
1088 def run(
1089 self,
1090 revision_overrides, # type: Mapping[str, str]
1091 command, # type: str
1092 args, # type: Sequence[str]
1093 work_queue, # type: ExecutionQueue
1094 options, # type: optparse.Values
1095 patch_refs, # type: Mapping[str, str]
1096 target_branches, # type: Mapping[str, str]
1097 skip_sync_revisions, # type: Mapping[str, str]
1098 ):
1099 # type: () -> None
1100 """Runs |command| then parse the DEPS file."""
1101 logging.info('Dependency(%s).run()' % self.name)
1102 assert self._file_list == []
1103 # When running runhooks, there's no need to consult the SCM.
1104 # All known hooks are expected to run unconditionally regardless of
1105 # working copy state, so skip the SCM status check.
1106 run_scm = command not in ('flatten', 'runhooks', 'recurse', 'validate',
1107 None)
1108 file_list = [] if not options.nohooks else None
1109 revision_override = revision_overrides.pop(
1110 self.FuzzyMatchUrl(revision_overrides), None)
1111 if not revision_override and not self.managed:
1112 revision_override = 'unmanaged'
1113 if run_scm and self.url:
1114 # Create a shallow copy to mutate revision.
1115 options = copy.copy(options)
1116 options.revision = revision_override
1117 self._used_revision = options.revision
1118 self._used_scm = self.CreateSCM(out_cb=work_queue.out_cb)
1119 if command != 'update' or self.GetScmName() != 'git':
1120 self._got_revision = self._used_scm.RunCommand(
1121 command, options, args, file_list)
agabled437d762016-10-17 09:35:11 -07001122 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001123 try:
1124 start = time.time()
1125 sync_status = metrics_utils.SYNC_STATUS_FAILURE
1126 self._got_revision = self._used_scm.RunCommand(
1127 command, options, args, file_list)
1128 sync_status = metrics_utils.SYNC_STATUS_SUCCESS
1129 finally:
1130 url, revision = gclient_utils.SplitUrlRevision(self.url)
1131 metrics.collector.add_repeated(
1132 'git_deps', {
1133 'path': self.name,
1134 'url': url,
1135 'revision': revision,
1136 'execution_time': time.time() - start,
1137 'sync_status': sync_status,
1138 })
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00001139
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001140 if isinstance(self, GitDependency) and command == 'update':
1141 patch_repo = self.url.split('@')[0]
1142 patch_ref = patch_refs.pop(self.FuzzyMatchUrl(patch_refs), None)
1143 target_branch = target_branches.pop(
1144 self.FuzzyMatchUrl(target_branches), None)
1145 if patch_ref:
1146 latest_commit = self._used_scm.apply_patch_ref(
1147 patch_repo, patch_ref, target_branch, options,
1148 file_list)
1149 else:
1150 latest_commit = self._used_scm.revinfo(None, None, None)
1151 existing_sync_commits = json.loads(
1152 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
1153 existing_sync_commits[self.name] = latest_commit
1154 os.environ[PREVIOUS_SYNC_COMMITS] = json.dumps(
1155 existing_sync_commits)
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001156
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001157 if file_list:
1158 file_list = [
1159 os.path.join(self.name, f.strip()) for f in file_list
1160 ]
John Budorick0f7b2002018-01-19 15:46:17 -08001161
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001162 # TODO(phajdan.jr): We should know exactly when the paths are
1163 # absolute. Convert all absolute paths to relative.
1164 for i in range(len(file_list or [])):
1165 # It depends on the command being executed (like runhooks vs
1166 # sync).
1167 if not os.path.isabs(file_list[i]):
1168 continue
1169 prefix = os.path.commonprefix(
1170 [self.root.root_dir.lower(), file_list[i].lower()])
1171 file_list[i] = file_list[i][len(prefix):]
1172 # Strip any leading path separators.
1173 while file_list[i].startswith(('\\', '/')):
1174 file_list[i] = file_list[i][1:]
John Budorick0f7b2002018-01-19 15:46:17 -08001175
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001176 # We must check for diffs AFTER any patch_refs have been applied.
1177 if skip_sync_revisions:
1178 skip_sync_rev = skip_sync_revisions.pop(
1179 self.FuzzyMatchUrl(skip_sync_revisions), None)
1180 self._should_sync = (skip_sync_rev is None
1181 or self._used_scm.check_diff(skip_sync_rev,
1182 files=['DEPS']))
1183 if not self._should_sync:
1184 logging.debug(
1185 'Skipping sync for %s. No DEPS changes since last '
1186 'sync at %s' % (self.name, skip_sync_rev))
1187 else:
1188 logging.debug('DEPS changes detected for %s since last sync at '
1189 '%s. Not skipping deps sync' %
1190 (self.name, skip_sync_rev))
Dirk Pranke9f20d022017-10-11 18:36:54 -07001191
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001192 if self.should_recurse:
1193 self.ParseDepsFile()
Corentin Wallez271a78a2020-07-12 15:41:46 +00001194
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001195 self._run_is_done(file_list or [])
Corentin Wallez271a78a2020-07-12 15:41:46 +00001196
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001197 # TODO(crbug.com/1339471): If should_recurse is false, ParseDepsFile
1198 # never gets called meaning we never fetch hooks and dependencies. So
1199 # there's no need to check should_recurse again here.
1200 if self.should_recurse:
1201 if command in ('update', 'revert') and not options.noprehooks:
1202 self.RunPreDepsHooks()
1203 # Parse the dependencies of this dependency.
1204 for s in self.dependencies:
1205 if s.should_process:
1206 work_queue.enqueue(s)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001207
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001208 if command == 'recurse':
1209 # Skip file only checkout.
1210 scm = self.GetScmName()
1211 if not options.scm or scm in options.scm:
1212 cwd = os.path.normpath(
1213 os.path.join(self.root.root_dir, self.name))
1214 # Pass in the SCM type as an env variable. Make sure we don't
1215 # put unicode strings in the environment.
1216 env = os.environ.copy()
1217 if scm:
1218 env['GCLIENT_SCM'] = str(scm)
1219 if self.url:
1220 env['GCLIENT_URL'] = str(self.url)
1221 env['GCLIENT_DEP_PATH'] = str(self.name)
1222 if options.prepend_dir and scm == 'git':
1223 print_stdout = False
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001224
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001225 def filter_fn(line):
1226 """Git-specific path marshaling. It is optimized for git-grep."""
1227 def mod_path(git_pathspec):
1228 match = re.match('^(\\S+?:)?([^\0]+)$',
1229 git_pathspec)
1230 modified_path = os.path.join(
1231 self.name, match.group(2))
1232 branch = match.group(1) or ''
1233 return '%s%s' % (branch, modified_path)
1234
1235 match = re.match('^Binary file ([^\0]+) matches$', line)
1236 if match:
1237 print('Binary file %s matches\n' %
1238 mod_path(match.group(1)))
1239 return
1240
1241 items = line.split('\0')
1242 if len(items) == 2 and items[1]:
1243 print('%s : %s' % (mod_path(items[0]), items[1]))
1244 elif len(items) >= 2:
1245 # Multiple null bytes or a single trailing null byte
1246 # indicate git is likely displaying filenames only
1247 # (such as with -l)
1248 print('\n'.join(
1249 mod_path(path) for path in items if path))
1250 else:
1251 print(line)
1252 else:
1253 print_stdout = True
1254 filter_fn = None
1255
1256 if self.url is None:
1257 print('Skipped omitted dependency %s' % cwd,
1258 file=sys.stderr)
1259 elif os.path.isdir(cwd):
1260 try:
1261 gclient_utils.CheckCallAndFilter(
1262 args,
1263 cwd=cwd,
1264 env=env,
1265 print_stdout=print_stdout,
1266 filter_fn=filter_fn,
1267 )
1268 except subprocess2.CalledProcessError:
1269 if not options.ignore:
1270 raise
1271 else:
1272 print('Skipped missing %s' % cwd, file=sys.stderr)
1273
1274 def GetScmName(self):
1275 raise NotImplementedError()
1276
1277 def CreateSCM(self, out_cb=None):
1278 raise NotImplementedError()
1279
1280 def HasGNArgsFile(self):
1281 return self._gn_args_file is not None
1282
1283 def WriteGNArgsFile(self):
1284 lines = ['# Generated from %r' % self.deps_file]
1285 variables = self.get_vars()
1286 for arg in self._gn_args:
1287 value = variables[arg]
1288 if isinstance(value, gclient_eval.ConstantString):
1289 value = value.value
1290 elif isinstance(value, str):
1291 value = gclient_eval.EvaluateCondition(value, variables)
1292 lines.append('%s = %s' % (arg, ToGNString(value)))
1293
1294 # When use_relative_paths is set, gn_args_file is relative to this DEPS
1295 path_prefix = self.root.root_dir
1296 if self._use_relative_paths:
1297 path_prefix = os.path.join(path_prefix, self.name)
1298
1299 with open(os.path.join(path_prefix, self._gn_args_file), 'wb') as f:
1300 f.write('\n'.join(lines).encode('utf-8', 'replace'))
1301
1302 @gclient_utils.lockedmethod
1303 def _run_is_done(self, file_list):
1304 # Both these are kept for hooks that are run as a separate tree
1305 # traversal.
1306 self._file_list = file_list
1307 self._processed = True
1308
1309 def GetHooks(self, options):
1310 """Evaluates all hooks, and return them in a flat list.
szager@google.comb9a78d32012-03-13 18:46:21 +00001311
1312 RunOnDeps() must have been called before to load the DEPS.
1313 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001314 result = []
1315 if not self.should_process or not self.should_recurse:
1316 # Don't run the hook when it is above recursion_limit.
1317 return result
1318 # If "--force" was specified, run all hooks regardless of what files
1319 # have changed.
1320 if self.deps_hooks:
1321 # TODO(maruel): If the user is using git, then we don't know
1322 # what files have changed so we always run all hooks. It'd be nice
1323 # to fix that.
1324 result.extend(self.deps_hooks)
1325 for s in self.dependencies:
1326 result.extend(s.GetHooks(options))
1327 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001328
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001329 def RunHooksRecursively(self, options, progress):
1330 assert self.hooks_ran == False
1331 self._hooks_ran = True
1332 hooks = self.GetHooks(options)
1333 if progress:
1334 progress._total = len(hooks)
1335 for hook in hooks:
1336 if progress:
1337 progress.update(extra=hook.name or '')
1338 hook.run()
1339 if progress:
1340 progress.end()
maruel@chromium.orgeaf61062010-07-07 18:42:39 +00001341
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001342 def RunPreDepsHooks(self):
1343 assert self.processed
1344 assert self.deps_parsed
1345 assert not self.pre_deps_hooks_ran
1346 assert not self.hooks_ran
1347 for s in self.dependencies:
1348 assert not s.processed
1349 self._pre_deps_hooks_ran = True
1350 for hook in self.pre_deps_hooks:
1351 hook.run()
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001352
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001353 def GetCipdRoot(self):
1354 if self.root is self:
1355 # Let's not infinitely recurse. If this is root and isn't an
1356 # instance of GClient, do nothing.
1357 return None
1358 return self.root.GetCipdRoot()
John Budorickd3ba72b2018-03-20 12:27:42 -07001359
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001360 def subtree(self, include_all):
1361 """Breadth first recursion excluding root node."""
1362 dependencies = self.dependencies
1363 for d in dependencies:
1364 if d.should_process or include_all:
1365 yield d
1366 for d in dependencies:
1367 for i in d.subtree(include_all):
1368 yield i
maruel@chromium.orgad3287e2011-10-03 19:15:10 +00001369
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001370 @gclient_utils.lockedmethod
1371 def add_dependency(self, new_dep):
1372 self._dependencies.append(new_dep)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001373
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001374 @gclient_utils.lockedmethod
1375 def _mark_as_parsed(self, new_hooks):
1376 self._deps_hooks.extend(new_hooks)
1377 self._deps_parsed = True
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001378
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001379 @property
1380 @gclient_utils.lockedmethod
1381 def dependencies(self):
1382 return tuple(self._dependencies)
maruel@chromium.org4bdd5fd2011-09-26 19:41:17 +00001383
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001384 @property
1385 @gclient_utils.lockedmethod
1386 def deps_hooks(self):
1387 return tuple(self._deps_hooks)
maruel@chromium.org064186c2011-09-27 23:53:33 +00001388
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001389 @property
1390 @gclient_utils.lockedmethod
1391 def pre_deps_hooks(self):
1392 return tuple(self._pre_deps_hooks)
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001393
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001394 @property
1395 @gclient_utils.lockedmethod
1396 def deps_parsed(self):
1397 """This is purely for debugging purposes. It's not used anywhere."""
1398 return self._deps_parsed
maruel@chromium.org064186c2011-09-27 23:53:33 +00001399
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001400 @property
1401 @gclient_utils.lockedmethod
1402 def processed(self):
1403 return self._processed
maruel@chromium.org064186c2011-09-27 23:53:33 +00001404
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001405 @property
1406 @gclient_utils.lockedmethod
1407 def pre_deps_hooks_ran(self):
1408 return self._pre_deps_hooks_ran
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001409
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001410 @property
1411 @gclient_utils.lockedmethod
1412 def hooks_ran(self):
1413 return self._hooks_ran
maruel@chromium.org064186c2011-09-27 23:53:33 +00001414
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001415 @property
1416 @gclient_utils.lockedmethod
1417 def allowed_hosts(self):
1418 return self._allowed_hosts
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001419
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001420 @property
1421 @gclient_utils.lockedmethod
1422 def file_list(self):
1423 return tuple(self._file_list)
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001424
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001425 @property
1426 def used_scm(self):
1427 """SCMWrapper instance for this dependency or None if not processed yet."""
1428 return self._used_scm
kustermann@google.coma692e8f2013-04-18 08:32:04 +00001429
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001430 @property
1431 @gclient_utils.lockedmethod
1432 def got_revision(self):
1433 return self._got_revision
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00001434
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001435 @property
1436 def file_list_and_children(self):
1437 result = list(self.file_list)
1438 for d in self.dependencies:
1439 result.extend(d.file_list_and_children)
1440 return tuple(result)
maruel@chromium.org861fd0f2010-07-23 03:05:05 +00001441
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001442 def __str__(self):
1443 out = []
1444 for i in ('name', 'url', 'custom_deps', 'custom_vars', 'deps_hooks',
1445 'file_list', 'should_process', 'processed', 'hooks_ran',
1446 'deps_parsed', 'requirements', 'allowed_hosts'):
1447 # First try the native property if it exists.
1448 if hasattr(self, '_' + i):
1449 value = getattr(self, '_' + i, False)
1450 else:
1451 value = getattr(self, i, False)
1452 if value:
1453 out.append('%s: %s' % (i, value))
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001454
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001455 for d in self.dependencies:
1456 out.extend([' ' + x for x in str(d).splitlines()])
1457 out.append('')
1458 return '\n'.join(out)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001459
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001460 def __repr__(self):
1461 return '%s: %s' % (self.name, self.url)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001462
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001463 def hierarchy(self, include_url=True, graphviz=False):
1464 """Returns a human-readable hierarchical reference to a Dependency."""
1465 def format_name(d):
1466 if include_url:
1467 return '%s(%s)' % (d.name, d.url)
1468 return '"%s"' % d.name # quotes required for graph dot file.
Joanna Wang9144b672023-02-24 23:36:17 +00001469
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001470 out = format_name(self)
1471 i = self.parent
1472 while i and i.name:
1473 out = '%s -> %s' % (format_name(i), out)
1474 if graphviz:
1475 # for graphviz we just need each parent->child relationship
1476 # listed once.
1477 return out
1478 i = i.parent
Joanna Wang9144b672023-02-24 23:36:17 +00001479 return out
maruel@chromium.orgbffb9042010-07-22 20:59:36 +00001480
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001481 def hierarchy_data(self):
1482 """Returns a machine-readable hierarchical reference to a Dependency."""
1483 d = self
1484 out = []
1485 while d and d.name:
1486 out.insert(0, (d.name, d.url))
1487 d = d.parent
1488 return tuple(out)
Michael Mossfe68c912018-03-22 19:19:35 -07001489
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001490 def get_builtin_vars(self):
1491 return {
1492 'checkout_android': 'android' in self.target_os,
1493 'checkout_chromeos': 'chromeos' in self.target_os,
1494 'checkout_fuchsia': 'fuchsia' in self.target_os,
1495 'checkout_ios': 'ios' in self.target_os,
1496 'checkout_linux': 'unix' in self.target_os,
1497 'checkout_mac': 'mac' in self.target_os,
1498 'checkout_win': 'win' in self.target_os,
1499 'host_os': _detect_host_os(),
1500 'checkout_arm': 'arm' in self.target_cpu,
1501 'checkout_arm64': 'arm64' in self.target_cpu,
1502 'checkout_x86': 'x86' in self.target_cpu,
1503 'checkout_mips': 'mips' in self.target_cpu,
1504 'checkout_mips64': 'mips64' in self.target_cpu,
1505 'checkout_ppc': 'ppc' in self.target_cpu,
1506 'checkout_s390': 's390' in self.target_cpu,
1507 'checkout_x64': 'x64' in self.target_cpu,
1508 'host_cpu': detect_host_arch.HostArch(),
1509 }
Robbie Iannucci3db32762023-07-05 19:02:44 +00001510
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001511 def get_vars(self):
1512 """Returns a dictionary of effective variable values
Edward Lemur8f8a50d2018-11-01 22:03:02 +00001513 (DEPS file contents with applied custom_vars overrides)."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001514 # Variable precedence (last has highest):
1515 # - DEPS vars
1516 # - parents, from first to last
1517 # - built-in
1518 # - custom_vars overrides
1519 result = {}
1520 result.update(self._vars)
1521 if self.parent:
1522 merge_vars(result, self.parent.get_vars())
1523 # Provide some built-in variables.
1524 result.update(self.get_builtin_vars())
1525 merge_vars(result, self.custom_vars)
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001526
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001527 return result
Paweł Hajdan, Jrd3790252017-07-03 21:06:24 +02001528
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001529
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001530_PLATFORM_MAPPING = {
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001531 'cygwin': 'win',
1532 'darwin': 'mac',
1533 'linux2': 'linux',
1534 'linux': 'linux',
1535 'win32': 'win',
1536 'aix6': 'aix',
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001537}
1538
1539
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001540def merge_vars(result, new_vars):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001541 for k, v in new_vars.items():
1542 if k in result:
1543 if isinstance(result[k], gclient_eval.ConstantString):
1544 if isinstance(v, gclient_eval.ConstantString):
1545 result[k] = v
1546 else:
1547 result[k].value = v
1548 else:
1549 result[k] = v
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001550 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001551 result[k] = v
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001552
1553
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001554def _detect_host_os():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001555 if sys.platform in _PLATFORM_MAPPING:
1556 return _PLATFORM_MAPPING[sys.platform]
Jonas Termansenbf7eb522023-01-19 17:56:40 +00001557
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001558 try:
1559 return os.uname().sysname.lower()
1560 except AttributeError:
1561 return sys.platform
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001562
1563
Edward Lemurb61d3872018-05-09 18:42:47 -04001564class GitDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001565 """A Dependency object that represents a single git checkout."""
1566 @staticmethod
1567 def updateProtocol(url, protocol):
1568 """Updates given URL's protocol"""
1569 # only works on urls, skips local paths
1570 if not url or not protocol or not re.match('([a-z]+)://', url) or \
1571 re.match('file://', url):
1572 return url
Edward Lemurb61d3872018-05-09 18:42:47 -04001573
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001574 return re.sub('^([a-z]+):', protocol + ':', url)
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +00001575
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001576 #override
1577 def GetScmName(self):
1578 """Always 'git'."""
1579 return 'git'
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +00001580
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001581 #override
1582 def CreateSCM(self, out_cb=None):
1583 """Create a Wrapper instance suitable for handling this git dependency."""
1584 return gclient_scm.GitWrapper(self.url,
1585 self.root.root_dir,
1586 self.name,
1587 self.outbuf,
1588 out_cb,
1589 print_outbuf=self.print_outbuf)
Edward Lemurb61d3872018-05-09 18:42:47 -04001590
1591
1592class GClient(GitDependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001593 """Object that represent a gclient checkout. A tree of Dependency(), one per
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001594 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001595
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001596 DEPS_OS_CHOICES = {
1597 "aix6": "unix",
1598 "win32": "win",
1599 "win": "win",
1600 "cygwin": "win",
1601 "darwin": "mac",
1602 "mac": "mac",
1603 "unix": "unix",
1604 "linux": "unix",
1605 "linux2": "unix",
1606 "linux3": "unix",
1607 "android": "android",
1608 "ios": "ios",
1609 "fuchsia": "fuchsia",
1610 "chromeos": "chromeos",
1611 }
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001612
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001613 DEFAULT_CLIENT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001614solutions = [
Edward Lesmes05934952019-12-19 20:38:09 +00001615 { "name" : %(solution_name)r,
1616 "url" : %(solution_url)r,
1617 "deps_file" : %(deps_file)r,
1618 "managed" : %(managed)r,
smutae7ea312016-07-18 11:59:41 -07001619 "custom_deps" : {
1620 },
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02001621 "custom_vars": %(custom_vars)r,
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001622 },
1623]
Robert Iannuccia19649b2018-06-29 16:31:45 +00001624""")
1625
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001626 DEFAULT_CLIENT_CACHE_DIR_TEXT = ("""\
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001627cache_dir = %(cache_dir)r
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001628""")
1629
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001630 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001631# Snapshot generated with gclient revinfo --snapshot
Edward Lesmesc2960242018-03-06 20:50:15 -05001632solutions = %(solution_list)s
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001633""")
1634
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001635 def __init__(self, root_dir, options):
1636 # Do not change previous behavior. Only solution level and immediate
1637 # DEPS are processed.
1638 self._recursion_limit = 2
1639 super(GClient, self).__init__(parent=None,
1640 name=None,
1641 url=None,
1642 managed=True,
1643 custom_deps=None,
1644 custom_vars=None,
1645 custom_hooks=None,
1646 deps_file='unused',
1647 should_process=True,
1648 should_recurse=True,
1649 relative=None,
1650 condition=None,
1651 print_outbuf=True)
Edward Lemure05f18d2018-06-08 17:36:53 +00001652
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001653 self._options = options
1654 if options.deps_os:
1655 enforced_os = options.deps_os.split(',')
1656 else:
1657 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
1658 if 'all' in enforced_os:
1659 enforced_os = self.DEPS_OS_CHOICES.values()
1660 self._enforced_os = tuple(set(enforced_os))
1661 self._enforced_cpu = (detect_host_arch.HostArch(), )
1662 self._root_dir = root_dir
1663 self._cipd_root = None
1664 self.config_content = None
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001665
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001666 def _CheckConfig(self):
1667 """Verify that the config matches the state of the existing checked-out
borenet@google.com88d10082014-03-21 17:24:48 +00001668 solutions."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001669 for dep in self.dependencies:
1670 if dep.managed and dep.url:
1671 scm = dep.CreateSCM()
1672 actual_url = scm.GetActualRemoteURL(self._options)
1673 if actual_url and not scm.DoesRemoteURLMatch(self._options):
1674 mirror = scm.GetCacheMirror()
1675 if mirror:
1676 mirror_string = '%s (exists=%s)' % (mirror.mirror_path,
1677 mirror.exists())
1678 else:
1679 mirror_string = 'not used'
1680 raise gclient_utils.Error(
1681 '''
borenet@google.com88d10082014-03-21 17:24:48 +00001682Your .gclient file seems to be broken. The requested URL is different from what
borenet@google.com0a427372014-04-02 19:12:13 +00001683is actually checked out in %(checkout_path)s.
borenet@google.com88d10082014-03-21 17:24:48 +00001684
borenet@google.com97882362014-04-07 20:06:02 +00001685The .gclient file contains:
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +00001686URL: %(expected_url)s (%(expected_scm)s)
1687Cache mirror: %(mirror_string)s
borenet@google.com97882362014-04-07 20:06:02 +00001688
1689The local checkout in %(checkout_path)s reports:
1690%(actual_url)s (%(actual_scm)s)
borenet@google.com88d10082014-03-21 17:24:48 +00001691
1692You should ensure that the URL listed in .gclient is correct and either change
agabled437d762016-10-17 09:35:11 -07001693it or fix the checkout.
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00001694''' % {
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001695 'checkout_path': os.path.join(
1696 self.root_dir, dep.name),
1697 'expected_url': dep.url,
1698 'expected_scm': dep.GetScmName(),
1699 'mirror_string': mirror_string,
1700 'actual_url': actual_url,
1701 'actual_scm': dep.GetScmName()
1702 })
borenet@google.com88d10082014-03-21 17:24:48 +00001703
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001704 def SetConfig(self, content):
1705 assert not self.dependencies
1706 config_dict = {}
1707 self.config_content = content
1708 try:
1709 exec(content, config_dict)
1710 except SyntaxError as e:
1711 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001712
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001713 # Append any target OS that is not already being enforced to the tuple.
1714 target_os = config_dict.get('target_os', [])
1715 if config_dict.get('target_os_only', False):
1716 self._enforced_os = tuple(set(target_os))
1717 else:
1718 self._enforced_os = tuple(set(self._enforced_os).union(target_os))
stuartmorgan@chromium.org18a4f6a2012-11-02 13:37:24 +00001719
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001720 # Append any target CPU that is not already being enforced to the tuple.
1721 target_cpu = config_dict.get('target_cpu', [])
1722 if config_dict.get('target_cpu_only', False):
1723 self._enforced_cpu = tuple(set(target_cpu))
1724 else:
1725 self._enforced_cpu = tuple(
1726 set(self._enforced_cpu).union(target_cpu))
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001727
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001728 cache_dir = config_dict.get('cache_dir', UNSET_CACHE_DIR)
1729 if cache_dir is not UNSET_CACHE_DIR:
1730 if cache_dir:
1731 cache_dir = os.path.join(self.root_dir, cache_dir)
1732 cache_dir = os.path.abspath(cache_dir)
Andrii Shyshkalov77ce4bd2017-11-27 12:38:18 -08001733
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001734 git_cache.Mirror.SetCachePath(cache_dir)
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001735
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001736 if not target_os and config_dict.get('target_os_only', False):
1737 raise gclient_utils.Error(
1738 'Can\'t use target_os_only if target_os is '
1739 'not specified')
peter@chromium.org1efccc82012-04-27 16:34:38 +00001740
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001741 if not target_cpu and config_dict.get('target_cpu_only', False):
1742 raise gclient_utils.Error(
1743 'Can\'t use target_cpu_only if target_cpu is '
1744 'not specified')
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001745
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001746 deps_to_add = []
1747 for s in config_dict.get('solutions', []):
1748 try:
1749 deps_to_add.append(
1750 GitDependency(
1751 parent=self,
1752 name=s['name'],
1753 # Update URL with scheme in protocol_override
1754 url=GitDependency.updateProtocol(
1755 s['url'], s.get('protocol_override', None)),
1756 managed=s.get('managed', True),
1757 custom_deps=s.get('custom_deps', {}),
1758 custom_vars=s.get('custom_vars', {}),
1759 custom_hooks=s.get('custom_hooks', []),
1760 deps_file=s.get('deps_file', 'DEPS'),
1761 should_process=True,
1762 should_recurse=True,
1763 relative=None,
1764 condition=None,
1765 print_outbuf=True,
1766 # Pass protocol_override down the tree for child deps to
1767 # use.
1768 protocol=s.get('protocol_override', None),
1769 git_dependencies_state=self.git_dependencies_state))
1770 except KeyError:
1771 raise gclient_utils.Error('Invalid .gclient file. Solution is '
1772 'incomplete: %s' % s)
1773 metrics.collector.add('project_urls', [
Edward Lemuraffd4102019-06-05 18:07:49 +00001774 dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Edward Lemur40764b02018-07-20 18:50:29 +00001775 for dep in deps_to_add
1776 if dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001777 ])
Edward Lemur40764b02018-07-20 18:50:29 +00001778
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001779 self.add_dependencies_and_close(deps_to_add,
1780 config_dict.get('hooks', []))
1781 logging.info('SetConfig() done')
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001782
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001783 def SaveConfig(self):
1784 gclient_utils.FileWrite(
1785 os.path.join(self.root_dir, self._options.config_filename),
1786 self.config_content)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001787
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001788 @staticmethod
1789 def LoadCurrentConfig(options):
1790 # type: (optparse.Values) -> GClient
1791 """Searches for and loads a .gclient file relative to the current working
Joanna Wang66286612022-06-30 19:59:13 +00001792 dir."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001793 if options.spec:
1794 client = GClient('.', options)
1795 client.SetConfig(options.spec)
1796 else:
1797 if options.verbose:
1798 print('Looking for %s starting from %s\n' %
1799 (options.config_filename, os.getcwd()))
1800 path = gclient_paths.FindGclientRoot(os.getcwd(),
1801 options.config_filename)
1802 if not path:
1803 if options.verbose:
1804 print('Couldn\'t find configuration file.')
1805 return None
1806 client = GClient(path, options)
1807 client.SetConfig(
1808 gclient_utils.FileRead(
1809 os.path.join(path, options.config_filename)))
maruel@chromium.org69392e72011-10-13 22:09:00 +00001810
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001811 if (options.revisions and len(client.dependencies) > 1
1812 and any('@' not in r for r in options.revisions)):
1813 print((
1814 'You must specify the full solution name like --revision %s@%s\n'
1815 'when you have multiple solutions setup in your .gclient file.\n'
1816 'Other solutions present are: %s.') %
1817 (client.dependencies[0].name, options.revisions[0], ', '.join(
1818 s.name for s in client.dependencies[1:])),
1819 file=sys.stderr)
Joanna Wang66286612022-06-30 19:59:13 +00001820
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001821 return client
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001822
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001823 def SetDefaultConfig(self,
1824 solution_name,
1825 deps_file,
1826 solution_url,
1827 managed=True,
1828 cache_dir=UNSET_CACHE_DIR,
1829 custom_vars=None):
1830 text = self.DEFAULT_CLIENT_FILE_TEXT
1831 format_dict = {
1832 'solution_name': solution_name,
1833 'solution_url': solution_url,
1834 'deps_file': deps_file,
1835 'managed': managed,
1836 'custom_vars': custom_vars or {},
1837 }
Robert Iannuccia19649b2018-06-29 16:31:45 +00001838
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001839 if cache_dir is not UNSET_CACHE_DIR:
1840 text += self.DEFAULT_CLIENT_CACHE_DIR_TEXT
1841 format_dict['cache_dir'] = cache_dir
Robert Iannuccia19649b2018-06-29 16:31:45 +00001842
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001843 self.SetConfig(text % format_dict)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001844
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001845 def _SaveEntries(self):
1846 """Creates a .gclient_entries file to record the list of unique checkouts.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001847
1848 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001849 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001850 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
1851 # makes testing a bit too fun.
1852 result = 'entries = {\n'
1853 for entry in self.root.subtree(False):
1854 result += ' %s: %s,\n' % (pprint.pformat(
1855 entry.name), pprint.pformat(entry.url))
1856 result += '}\n'
1857 file_path = os.path.join(self.root_dir, self._options.entries_filename)
1858 logging.debug(result)
1859 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001860
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001861 def _ReadEntries(self):
1862 """Read the .gclient_entries file for the given client.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001863
1864 Returns:
1865 A sequence of solution names, which will be empty if there is the
1866 entries file hasn't been created yet.
1867 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001868 scope = {}
1869 filename = os.path.join(self.root_dir, self._options.entries_filename)
1870 if not os.path.exists(filename):
1871 return {}
1872 try:
1873 exec(gclient_utils.FileRead(filename), scope)
1874 except SyntaxError as e:
1875 gclient_utils.SyntaxErrorToError(filename, e)
1876 return scope.get('entries', {})
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001877
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001878 def _ExtractFileJsonContents(self, default_filename):
1879 # type: (str) -> Mapping[str,Any]
1880 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001881
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001882 if not os.path.exists(f):
1883 logging.info('File %s does not exist.' % f)
1884 return {}
Joanna Wang01870792022-08-01 19:02:57 +00001885
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001886 with open(f, 'r') as open_f:
1887 logging.info('Reading content from file %s' % f)
1888 content = open_f.read().rstrip()
1889 if content:
1890 return json.loads(content)
Joanna Wang66286612022-06-30 19:59:13 +00001891 return {}
1892
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001893 def _WriteFileContents(self, default_filename, content):
1894 # type: (str, str) -> None
1895 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001896
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001897 with open(f, 'w') as open_f:
1898 logging.info('Writing to file %s' % f)
1899 open_f.write(content)
Joanna Wangf3edc502022-07-20 00:12:10 +00001900
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001901 def _EnforceSkipSyncRevisions(self, patch_refs):
1902 # type: (Mapping[str, str]) -> Mapping[str, str]
1903 """Checks for and enforces revisions for skipping deps syncing."""
1904 previous_sync_commits = self._ExtractFileJsonContents(
1905 PREVIOUS_SYNC_COMMITS_FILE)
Joanna Wang66286612022-06-30 19:59:13 +00001906
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001907 if not previous_sync_commits:
1908 return {}
maruel@chromium.org918a9ae2010-05-28 15:50:30 +00001909
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001910 # Current `self.dependencies` only contain solutions. If a patch_ref is
1911 # not for a solution, then it is for a solution's dependency or recursed
1912 # dependency which we cannot support while skipping sync.
1913 if patch_refs:
1914 unclaimed_prs = []
1915 candidates = []
1916 for dep in self.dependencies:
1917 origin, _ = gclient_utils.SplitUrlRevision(dep.url)
1918 candidates.extend([origin, dep.name])
1919 for patch_repo in patch_refs:
1920 if not gclient_utils.FuzzyMatchRepo(patch_repo, candidates):
1921 unclaimed_prs.append(patch_repo)
1922 if unclaimed_prs:
1923 print(
1924 'We cannot skip syncs when there are --patch-refs flags for '
1925 'non-solution dependencies. To skip syncing, remove patch_refs '
1926 'for: \n%s' % '\n'.join(unclaimed_prs))
1927 return {}
Edward Lesmesc621b212018-03-21 20:26:56 -04001928
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001929 # We cannot skip syncing if there are custom_vars that differ from the
1930 # previous run's custom_vars.
1931 previous_custom_vars = self._ExtractFileJsonContents(
1932 PREVIOUS_CUSTOM_VARS_FILE)
1933
1934 cvs_by_name = {s.name: s.custom_vars for s in self.dependencies}
1935
1936 skip_sync_revisions = {}
1937 for name, commit in previous_sync_commits.items():
1938 previous_vars = previous_custom_vars.get(name)
1939 if previous_vars == cvs_by_name.get(name) or (
1940 not previous_vars and not cvs_by_name.get(name)):
1941 skip_sync_revisions[name] = commit
1942 else:
1943 print(
1944 'We cannot skip syncs when custom_vars for solutions have '
1945 'changed since the last sync run on this machine.\n'
1946 '\nRemoving skip_sync_revision for:\n'
1947 'solution: %s, current: %r, previous: %r.' %
1948 (name, cvs_by_name.get(name), previous_vars))
1949 print('no-sync experiment enabled with %r' % skip_sync_revisions)
1950 return skip_sync_revisions
1951
1952 # TODO(crbug.com/1340695): Remove handling revisions without '@'.
1953 def _EnforceRevisions(self):
1954 """Checks for revision overrides."""
1955 revision_overrides = {}
1956 if self._options.head:
1957 return revision_overrides
1958 if not self._options.revisions:
1959 return revision_overrides
1960 solutions_names = [s.name for s in self.dependencies]
1961 for index, revision in enumerate(self._options.revisions):
1962 if not '@' in revision:
1963 # Support for --revision 123
1964 revision = '%s@%s' % (solutions_names[index], revision)
1965 name, rev = revision.split('@', 1)
1966 revision_overrides[name] = rev
1967 return revision_overrides
1968
1969 def _EnforcePatchRefsAndBranches(self):
1970 # type: () -> Tuple[Mapping[str, str], Mapping[str, str]]
1971 """Checks for patch refs."""
1972 patch_refs = {}
1973 target_branches = {}
1974 if not self._options.patch_refs:
1975 return patch_refs, target_branches
1976 for given_patch_ref in self._options.patch_refs:
1977 patch_repo, _, patch_ref = given_patch_ref.partition('@')
1978 if not patch_repo or not patch_ref or ':' not in patch_ref:
1979 raise gclient_utils.Error(
1980 'Wrong revision format: %s should be of the form '
1981 'patch_repo@target_branch:patch_ref.' % given_patch_ref)
1982 target_branch, _, patch_ref = patch_ref.partition(':')
1983 target_branches[patch_repo] = target_branch
1984 patch_refs[patch_repo] = patch_ref
1985 return patch_refs, target_branches
1986
Gavin Mak50b27a52023-09-19 22:44:59 +00001987 def _InstallPreCommitHook(self):
1988 # On Windows, this path is written to the file as
1989 # "dir\hooks\pre-commit.py" but it gets interpreted as
1990 # "dirhookspre-commit.py".
1991 gclient_hook_path = os.path.join(DEPOT_TOOLS_DIR, 'hooks',
1992 'pre-commit.py').replace('\\', '\\\\')
1993 gclient_hook_content = '\n'.join((
1994 f'{PRECOMMIT_HOOK_VAR}={gclient_hook_path}',
1995 f'if [ -f "${PRECOMMIT_HOOK_VAR}" ]; then',
1996 f' python3 "${PRECOMMIT_HOOK_VAR}" || exit 1',
1997 'fi',
1998 ))
1999
2000 soln = gclient_paths.GetPrimarySolutionPath()
2001 if not soln:
2002 print('Could not find gclient solution.')
2003 return
2004
2005 git_dir = os.path.join(soln, '.git')
2006 if not os.path.exists(git_dir):
2007 return
2008
Philipp Wollermanna45d2d42023-09-21 02:45:42 +00002009 git_hooks_dir = os.path.join(git_dir, 'hooks')
2010 os.makedirs(git_hooks_dir, exist_ok=True)
2011
Gavin Mak50b27a52023-09-19 22:44:59 +00002012 hook = os.path.join(git_dir, 'hooks', 'pre-commit')
2013 if os.path.exists(hook):
2014 with open(hook, 'r') as f:
2015 content = f.read()
2016 if PRECOMMIT_HOOK_VAR in content:
2017 print(f'{hook} already contains the gclient pre-commit hook.')
2018 else:
2019 print(f'A pre-commit hook already exists at {hook}.\n'
2020 f'Please append the following lines to the hook:\n\n'
2021 f'{gclient_hook_content}')
2022 return
2023
2024 print(f'Creating a pre-commit hook at {hook}')
2025 with open(hook, 'w') as f:
2026 f.write('#!/bin/sh\n')
2027 f.write(f'{gclient_hook_content}\n')
2028 os.chmod(hook, 0o755)
2029
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002030 def _RemoveUnversionedGitDirs(self):
2031 """Remove directories that are no longer part of the checkout.
Edward Lemur5b1fa942018-10-04 23:22:09 +00002032
2033 Notify the user if there is an orphaned entry in their working copy.
2034 Only delete the directory if there are no changes in it, and
2035 delete_unversioned_trees is set to true.
Josip Sokcevic1b8211f2022-09-30 17:46:53 +00002036
2037 Returns CIPD packages that are no longer versioned.
Edward Lemur5b1fa942018-10-04 23:22:09 +00002038 """
2039
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002040 entry_names_and_sync = [(i.name, i._should_sync)
2041 for i in self.root.subtree(False) if i.url]
2042 entries = []
2043 if entry_names_and_sync:
2044 entries, _ = zip(*entry_names_and_sync)
2045 full_entries = [
2046 os.path.join(self.root_dir, e.replace('/', os.path.sep))
2047 for e in entries
2048 ]
2049 no_sync_entries = [
2050 name for name, should_sync in entry_names_and_sync
2051 if not should_sync
2052 ]
Edward Lemur5b1fa942018-10-04 23:22:09 +00002053
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002054 removed_cipd_entries = []
2055 for entry, prev_url in self._ReadEntries().items():
2056 if not prev_url:
2057 # entry must have been overridden via .gclient custom_deps
2058 continue
2059 if any(entry.startswith(sln) for sln in no_sync_entries):
2060 # Dependencies of solutions that skipped syncing would not
2061 # show up in `entries`.
2062 continue
2063 if (':' in entry):
2064 # This is a cipd package. Don't clean it up, but prepare for
2065 # return
2066 if entry not in entries:
2067 removed_cipd_entries.append(entry)
2068 continue
2069 # Fix path separator on Windows.
2070 entry_fixed = entry.replace('/', os.path.sep)
2071 e_dir = os.path.join(self.root_dir, entry_fixed)
2072 # Use entry and not entry_fixed there.
2073 if (entry not in entries and
2074 (not any(path.startswith(entry + '/') for path in entries))
2075 and os.path.exists(e_dir)):
2076 # The entry has been removed from DEPS.
2077 scm = gclient_scm.GitWrapper(prev_url, self.root_dir,
2078 entry_fixed, self.outbuf)
Edward Lemur5b1fa942018-10-04 23:22:09 +00002079
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002080 # Check to see if this directory is now part of a higher-up
2081 # checkout.
2082 scm_root = None
2083 try:
2084 scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2085 scm.checkout_path)
2086 except subprocess2.CalledProcessError:
2087 pass
2088 if not scm_root:
2089 logging.warning(
2090 'Could not find checkout root for %s. Unable to '
2091 'determine whether it is part of a higher-level '
2092 'checkout, so not removing.' % entry)
2093 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002094
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002095 # This is to handle the case of third_party/WebKit migrating
2096 # from being a DEPS entry to being part of the main project. If
2097 # the subproject is a Git project, we need to remove its .git
2098 # folder. Otherwise git operations on that folder will have
2099 # different effects depending on the current working directory.
2100 if os.path.abspath(scm_root) == os.path.abspath(e_dir):
2101 e_par_dir = os.path.join(e_dir, os.pardir)
2102 if gclient_scm.scm.GIT.IsInsideWorkTree(e_par_dir):
2103 par_scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2104 e_par_dir)
2105 # rel_e_dir : relative path of entry w.r.t. its parent
2106 # repo.
2107 rel_e_dir = os.path.relpath(e_dir, par_scm_root)
2108 if gclient_scm.scm.GIT.IsDirectoryVersioned(
2109 par_scm_root, rel_e_dir):
2110 save_dir = scm.GetGitBackupDirPath()
2111 # Remove any eventual stale backup dir for the same
2112 # project.
2113 if os.path.exists(save_dir):
2114 gclient_utils.rmtree(save_dir)
2115 os.rename(os.path.join(e_dir, '.git'), save_dir)
2116 # When switching between the two states (entry/ is a
2117 # subproject -> entry/ is part of the outer
2118 # project), it is very likely that some files are
2119 # changed in the checkout, unless we are jumping
2120 # *exactly* across the commit which changed just
2121 # DEPS. In such case we want to cleanup any eventual
2122 # stale files (coming from the old subproject) in
2123 # order to end up with a clean checkout.
2124 gclient_scm.scm.GIT.CleanupDir(
2125 par_scm_root, rel_e_dir)
2126 assert not os.path.exists(
2127 os.path.join(e_dir, '.git'))
2128 print(
2129 '\nWARNING: \'%s\' has been moved from DEPS to a higher '
2130 'level checkout. The git folder containing all the local'
2131 ' branches has been saved to %s.\n'
2132 'If you don\'t care about its state you can safely '
2133 'remove that folder to free up space.' %
2134 (entry, save_dir))
2135 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002136
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002137 if scm_root in full_entries:
2138 logging.info(
2139 '%s is part of a higher level checkout, not removing',
2140 scm.GetCheckoutRoot())
2141 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002142
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002143 file_list = []
2144 scm.status(self._options, [], file_list)
2145 modified_files = file_list != []
2146 if (not self._options.delete_unversioned_trees
2147 or (modified_files and not self._options.force)):
2148 # There are modified files in this entry. Keep warning until
2149 # removed.
2150 self.add_dependency(
2151 GitDependency(
2152 parent=self,
2153 name=entry,
2154 # Update URL with scheme in protocol_override
2155 url=GitDependency.updateProtocol(
2156 prev_url, self.protocol),
2157 managed=False,
2158 custom_deps={},
2159 custom_vars={},
2160 custom_hooks=[],
2161 deps_file=None,
2162 should_process=True,
2163 should_recurse=False,
2164 relative=None,
2165 condition=None,
2166 protocol=self.protocol,
2167 git_dependencies_state=self.git_dependencies_state))
2168 if modified_files and self._options.delete_unversioned_trees:
2169 print(
2170 '\nWARNING: \'%s\' is no longer part of this client.\n'
2171 'Despite running \'gclient sync -D\' no action was taken '
2172 'as there are modifications.\nIt is recommended you revert '
2173 'all changes or run \'gclient sync -D --force\' next '
2174 'time.' % entry_fixed)
2175 else:
2176 print(
2177 '\nWARNING: \'%s\' is no longer part of this client.\n'
2178 'It is recommended that you manually remove it or use '
2179 '\'gclient sync -D\' next time.' % entry_fixed)
2180 else:
2181 # Delete the entry
2182 print('\n________ deleting \'%s\' in \'%s\'' %
2183 (entry_fixed, self.root_dir))
2184 gclient_utils.rmtree(e_dir)
2185 # record the current list of entries for next time
2186 self._SaveEntries()
2187 return removed_cipd_entries
Edward Lemur5b1fa942018-10-04 23:22:09 +00002188
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002189 def RunOnDeps(self,
2190 command,
2191 args,
2192 ignore_requirements=False,
2193 progress=True):
2194 """Runs a command on each dependency in a client and its dependencies.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002195
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002196 Args:
2197 command: The command to use (e.g., 'status' or 'diff')
2198 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002199 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002200 if not self.dependencies:
2201 raise gclient_utils.Error('No solution specified')
Michael Mossd683d7c2018-06-15 05:05:17 +00002202
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002203 revision_overrides = {}
2204 patch_refs = {}
2205 target_branches = {}
2206 skip_sync_revisions = {}
2207 # It's unnecessary to check for revision overrides for 'recurse'.
2208 # Save a few seconds by not calling _EnforceRevisions() in that case.
2209 if command not in ('diff', 'recurse', 'runhooks', 'status', 'revert',
2210 'validate'):
2211 self._CheckConfig()
2212 revision_overrides = self._EnforceRevisions()
Edward Lesmesc621b212018-03-21 20:26:56 -04002213
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002214 if command == 'update':
2215 patch_refs, target_branches = self._EnforcePatchRefsAndBranches()
2216 if NO_SYNC_EXPERIMENT in self._options.experiments:
2217 skip_sync_revisions = self._EnforceSkipSyncRevisions(patch_refs)
Joanna Wang66286612022-06-30 19:59:13 +00002218
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002219 # Store solutions' custom_vars on memory to compare in the next run.
2220 # All dependencies added later are inherited from the current
2221 # self.dependencies.
2222 custom_vars = {
2223 dep.name: dep.custom_vars
2224 for dep in self.dependencies if dep.custom_vars
Michael Mossd683d7c2018-06-15 05:05:17 +00002225 }
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002226 if custom_vars:
2227 self._WriteFileContents(PREVIOUS_CUSTOM_VARS_FILE,
2228 json.dumps(custom_vars))
2229
2230 # Disable progress for non-tty stdout.
2231 should_show_progress = (setup_color.IS_TTY and not self._options.verbose
2232 and progress)
2233 pm = None
2234 if should_show_progress:
2235 if command in ('update', 'revert'):
2236 pm = Progress('Syncing projects', 1)
2237 elif command in ('recurse', 'validate'):
2238 pm = Progress(' '.join(args), 1)
2239 work_queue = gclient_utils.ExecutionQueue(
2240 self._options.jobs,
2241 pm,
2242 ignore_requirements=ignore_requirements,
2243 verbose=self._options.verbose)
2244 for s in self.dependencies:
2245 if s.should_process:
2246 work_queue.enqueue(s)
2247 work_queue.flush(revision_overrides,
2248 command,
2249 args,
2250 options=self._options,
2251 patch_refs=patch_refs,
2252 target_branches=target_branches,
2253 skip_sync_revisions=skip_sync_revisions)
2254
2255 if revision_overrides:
2256 print(
2257 'Please fix your script, having invalid --revision flags will soon '
2258 'be considered an error.',
2259 file=sys.stderr)
2260
2261 if patch_refs:
2262 raise gclient_utils.Error(
2263 'The following --patch-ref flags were not used. Please fix it:\n%s'
2264 % ('\n'.join(patch_repo + '@' + patch_ref
2265 for patch_repo, patch_ref in patch_refs.items())))
2266
2267 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
2268 # they have fsmonitor enabled.
2269 if command == 'update':
2270 # Check if any of the root dependency have submodules.
2271 is_submoduled = any(
2272 map(
2273 lambda d: d.git_dependencies_state in
2274 (gclient_eval.SUBMODULES, gclient_eval.SYNC),
2275 self.dependencies))
2276 if is_submoduled:
2277 git_common.warn_submodule()
2278
2279 # Once all the dependencies have been processed, it's now safe to write
2280 # out the gn_args_file and run the hooks.
2281 removed_cipd_entries = []
2282 if command == 'update':
2283 for dependency in self.dependencies:
2284 gn_args_dep = dependency
2285 if gn_args_dep._gn_args_from:
2286 deps_map = {
2287 dep.name: dep
2288 for dep in gn_args_dep.dependencies
2289 }
2290 gn_args_dep = deps_map.get(gn_args_dep._gn_args_from)
2291 if gn_args_dep and gn_args_dep.HasGNArgsFile():
2292 gn_args_dep.WriteGNArgsFile()
2293
2294 removed_cipd_entries = self._RemoveUnversionedGitDirs()
2295
2296 # Sync CIPD dependencies once removed deps are deleted. In case a git
2297 # dependency was moved to CIPD, we want to remove the old git directory
2298 # first and then sync the CIPD dep.
2299 if self._cipd_root:
2300 self._cipd_root.run(command)
2301 # It's possible that CIPD removed some entries that are now part of
2302 # git worktree. Try to checkout those directories
2303 if removed_cipd_entries:
2304 for cipd_entry in removed_cipd_entries:
2305 cwd = os.path.join(self._root_dir, cipd_entry.split(':')[0])
2306 cwd, tail = os.path.split(cwd)
2307 if cwd:
2308 try:
2309 gclient_scm.scm.GIT.Capture(['checkout', tail],
2310 cwd=cwd)
2311 except subprocess2.CalledProcessError:
2312 pass
2313
2314 if not self._options.nohooks:
2315 if should_show_progress:
2316 pm = Progress('Running hooks', 1)
2317 self.RunHooksRecursively(self._options, pm)
2318
2319 self._WriteFileContents(PREVIOUS_SYNC_COMMITS_FILE,
2320 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
2321
2322 return 0
2323
2324 def PrintRevInfo(self):
2325 if not self.dependencies:
2326 raise gclient_utils.Error('No solution specified')
2327 # Load all the settings.
2328 work_queue = gclient_utils.ExecutionQueue(self._options.jobs,
2329 None,
2330 False,
2331 verbose=self._options.verbose)
2332 for s in self.dependencies:
2333 if s.should_process:
2334 work_queue.enqueue(s)
2335 work_queue.flush({},
2336 None, [],
2337 options=self._options,
2338 patch_refs=None,
2339 target_branches=None,
2340 skip_sync_revisions=None)
2341
2342 def ShouldPrintRevision(dep):
2343 return (not self._options.filter
2344 or dep.FuzzyMatchUrl(self._options.filter))
2345
2346 if self._options.snapshot:
2347 json_output = []
2348 # First level at .gclient
2349 for d in self.dependencies:
2350 entries = {}
2351
2352 def GrabDeps(dep):
2353 """Recursively grab dependencies."""
2354 for rec_d in dep.dependencies:
2355 rec_d.PinToActualRevision()
2356 if ShouldPrintRevision(rec_d):
2357 entries[rec_d.name] = rec_d.url
2358 GrabDeps(rec_d)
2359
2360 GrabDeps(d)
2361 json_output.append({
2362 'name': d.name,
2363 'solution_url': d.url,
2364 'deps_file': d.deps_file,
2365 'managed': d.managed,
2366 'custom_deps': entries,
2367 })
2368 if self._options.output_json == '-':
2369 print(json.dumps(json_output, indent=2, separators=(',', ': ')))
2370 elif self._options.output_json:
2371 with open(self._options.output_json, 'w') as f:
2372 json.dump(json_output, f)
2373 else:
2374 # Print the snapshot configuration file
2375 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {
2376 'solution_list': pprint.pformat(json_output, indent=2),
2377 })
Michael Mossd683d7c2018-06-15 05:05:17 +00002378 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002379 entries = {}
2380 for d in self.root.subtree(False):
2381 if self._options.actual:
2382 d.PinToActualRevision()
2383 if ShouldPrintRevision(d):
2384 entries[d.name] = d.url
2385 if self._options.output_json:
2386 json_output = {
2387 name: {
2388 'url': rev.split('@')[0] if rev else None,
2389 'rev':
2390 rev.split('@')[1] if rev and '@' in rev else None,
2391 }
2392 for name, rev in entries.items()
2393 }
2394 if self._options.output_json == '-':
2395 print(
2396 json.dumps(json_output,
2397 indent=2,
2398 separators=(',', ': ')))
2399 else:
2400 with open(self._options.output_json, 'w') as f:
2401 json.dump(json_output, f)
2402 else:
2403 keys = sorted(entries.keys())
2404 for x in keys:
2405 print('%s: %s' % (x, entries[x]))
2406 logging.info(str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002407
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002408 def ParseDepsFile(self):
2409 """No DEPS to parse for a .gclient file."""
2410 raise gclient_utils.Error('Internal error')
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00002411
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002412 def PrintLocationAndContents(self):
2413 # Print out the .gclient file. This is longer than if we just printed
2414 # the client dict, but more legible, and it might contain helpful
2415 # comments.
2416 print('Loaded .gclient config in %s:\n%s' %
2417 (self.root_dir, self.config_content))
sergiyb@chromium.orgfa2707e2016-03-12 00:40:56 +00002418
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002419 def GetCipdRoot(self):
2420 if not self._cipd_root:
2421 self._cipd_root = gclient_scm.CipdRoot(
2422 self.root_dir,
2423 # TODO(jbudorick): Support other service URLs as necessary.
2424 # Service URLs should be constant over the scope of a cipd
2425 # root, so a var per DEPS file specifying the service URL
2426 # should suffice.
Yiwei Zhang52353702023-09-18 15:53:52 +00002427 'https://chrome-infra-packages.appspot.com',
2428 log_level='info' if self._options.verbose else None)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002429 return self._cipd_root
John Budorickd3ba72b2018-03-20 12:27:42 -07002430
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002431 @property
2432 def root_dir(self):
2433 """Root directory of gclient checkout."""
2434 return self._root_dir
maruel@chromium.org75a59272010-06-11 22:34:03 +00002435
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002436 @property
2437 def enforced_os(self):
2438 """What deps_os entries that are to be parsed."""
2439 return self._enforced_os
maruel@chromium.org271375b2010-06-23 19:17:38 +00002440
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002441 @property
2442 def target_os(self):
2443 return self._enforced_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +00002444
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002445 @property
2446 def target_cpu(self):
2447 return self._enforced_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -08002448
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002449
John Budorick0f7b2002018-01-19 15:46:17 -08002450class CipdDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002451 """A Dependency object that represents a single CIPD package."""
2452 def __init__(self, parent, name, dep_value, cipd_root, custom_vars,
2453 should_process, relative, condition):
2454 package = dep_value['package']
2455 version = dep_value['version']
2456 url = urllib.parse.urljoin(cipd_root.service_url,
2457 '%s@%s' % (package, version))
2458 super(CipdDependency, self).__init__(parent=parent,
2459 name=name + ':' + package,
2460 url=url,
2461 managed=None,
2462 custom_deps=None,
2463 custom_vars=custom_vars,
2464 custom_hooks=None,
2465 deps_file=None,
2466 should_process=should_process,
2467 should_recurse=False,
2468 relative=relative,
2469 condition=condition)
2470 self._cipd_package = None
2471 self._cipd_root = cipd_root
2472 # CIPD wants /-separated paths, even on Windows.
2473 native_subdir_path = os.path.relpath(
2474 os.path.join(self.root.root_dir, name), cipd_root.root_dir)
2475 self._cipd_subdir = posixpath.join(*native_subdir_path.split(os.sep))
2476 self._package_name = package
2477 self._package_version = version
John Budorick0f7b2002018-01-19 15:46:17 -08002478
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002479 #override
2480 def run(self, revision_overrides, command, args, work_queue, options,
2481 patch_refs, target_branches, skip_sync_revisions):
2482 """Runs |command| then parse the DEPS file."""
2483 logging.info('CipdDependency(%s).run()' % self.name)
2484 if not self.should_process:
2485 return
2486 self._CreatePackageIfNecessary()
2487 super(CipdDependency,
2488 self).run(revision_overrides, command, args, work_queue, options,
2489 patch_refs, target_branches, skip_sync_revisions)
John Budorickd3ba72b2018-03-20 12:27:42 -07002490
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002491 def _CreatePackageIfNecessary(self):
2492 # We lazily create the CIPD package to make sure that only packages
2493 # that we want (as opposed to all packages defined in all DEPS files
2494 # we parse) get added to the root and subsequently ensured.
2495 if not self._cipd_package:
2496 self._cipd_package = self._cipd_root.add_package(
2497 self._cipd_subdir, self._package_name, self._package_version)
John Budorickd3ba72b2018-03-20 12:27:42 -07002498
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002499 def ParseDepsFile(self):
2500 """CIPD dependencies are not currently allowed to have nested deps."""
2501 self.add_dependencies_and_close([], [])
John Budorick0f7b2002018-01-19 15:46:17 -08002502
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002503 #override
2504 def verify_validity(self):
2505 """CIPD dependencies allow duplicate name for packages in same directory."""
2506 logging.info('Dependency(%s).verify_validity()' % self.name)
2507 return True
John Budorick0f7b2002018-01-19 15:46:17 -08002508
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002509 #override
2510 def GetScmName(self):
2511 """Always 'cipd'."""
2512 return 'cipd'
Shenghua Zhang6f830312018-02-26 11:45:07 -08002513
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002514 def GetExpandedPackageName(self):
2515 """Return the CIPD package name with the variables evaluated."""
2516 package = self._cipd_root.expand_package_name(self._package_name)
2517 if package:
2518 return package
2519 return self._package_name
John Budorick0f7b2002018-01-19 15:46:17 -08002520
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002521 #override
2522 def CreateSCM(self, out_cb=None):
2523 """Create a Wrapper instance suitable for handling this CIPD dependency."""
2524 self._CreatePackageIfNecessary()
2525 return gclient_scm.CipdWrapper(self.url,
2526 self.root.root_dir,
2527 self.name,
2528 self.outbuf,
2529 out_cb,
2530 root=self._cipd_root,
2531 package=self._cipd_package)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +00002532
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002533 def hierarchy(self, include_url=False, graphviz=False):
2534 if graphviz:
2535 return '' # graphviz lines not implemented for cipd deps.
2536 return self.parent.hierarchy(include_url) + ' -> ' + self._cipd_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08002537
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002538 def ToLines(self):
2539 # () -> Sequence[str]
2540 """Return a list of lines representing this in a DEPS file."""
2541 def escape_cipd_var(package):
2542 return package.replace('{', '{{').replace('}', '}}')
Edward Lemure4e15042018-06-28 18:07:00 +00002543
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002544 s = []
2545 self._CreatePackageIfNecessary()
2546 if self._cipd_package.authority_for_subdir:
2547 condition_part = ([' "condition": %r,' %
2548 self.condition] if self.condition else [])
2549 s.extend([
2550 ' # %s' % self.hierarchy(include_url=False),
2551 ' "%s": {' % (self.name.split(':')[0], ),
2552 ' "packages": [',
2553 ])
2554 for p in sorted(self._cipd_root.packages(self._cipd_subdir),
2555 key=lambda x: x.name):
2556 s.extend([
2557 ' {',
2558 ' "package": "%s",' % escape_cipd_var(p.name),
2559 ' "version": "%s",' % p.version,
2560 ' },',
2561 ])
John Budorickc35aba52018-06-28 20:57:03 +00002562
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002563 s.extend([
2564 ' ],',
2565 ' "dep_type": "cipd",',
2566 ] + condition_part + [
2567 ' },',
2568 '',
2569 ])
2570 return s
John Budorick0f7b2002018-01-19 15:46:17 -08002571
2572
maruel@chromium.org5ca27692010-05-26 19:32:41 +00002573#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002574
2575
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002576@subcommand.usage('[command] [args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002577@metrics.collector.collect_metrics('gclient recurse')
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002578def CMDrecurse(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002579 """Operates [command args ...] on all the dependencies.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002580
Arthur Milchior08cd5fe2022-07-28 20:38:47 +00002581 Change directory to each dependency's directory, and call [command
2582 args ...] there. Sets GCLIENT_DEP_PATH environment variable as the
2583 dep's relative location to root directory of the checkout.
2584
2585 Examples:
2586 * `gclient recurse --no-progress -j1 sh -c 'echo "$GCLIENT_DEP_PATH"'`
2587 print the relative path of each dependency.
2588 * `gclient recurse --no-progress -j1 sh -c "pwd"`
2589 print the absolute path of each dependency.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002590 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002591 # Stop parsing at the first non-arg so that these go through to the command
2592 parser.disable_interspersed_args()
2593 parser.add_option('-s',
2594 '--scm',
2595 action='append',
2596 default=[],
2597 help='Choose scm types to operate upon.')
2598 parser.add_option('-i',
2599 '--ignore',
2600 action='store_true',
2601 help='Ignore non-zero return codes from subcommands.')
2602 parser.add_option(
2603 '--prepend-dir',
2604 action='store_true',
2605 help='Prepend relative dir for use with git <cmd> --null.')
2606 parser.add_option(
2607 '--no-progress',
2608 action='store_true',
2609 help='Disable progress bar that shows sub-command updates')
2610 options, args = parser.parse_args(args)
2611 if not args:
2612 print('Need to supply a command!', file=sys.stderr)
2613 return 1
2614 root_and_entries = gclient_utils.GetGClientRootAndEntries()
2615 if not root_and_entries:
2616 print(
2617 'You need to run gclient sync at least once to use \'recurse\'.\n'
2618 'This is because .gclient_entries needs to exist and be up to date.',
2619 file=sys.stderr)
2620 return 1
davidbarr@chromium.org47ca0ee2012-03-02 16:06:11 +00002621
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002622 # Normalize options.scm to a set()
2623 scm_set = set()
2624 for scm in options.scm:
2625 scm_set.update(scm.split(','))
2626 options.scm = scm_set
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002627
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002628 options.nohooks = True
2629 client = GClient.LoadCurrentConfig(options)
2630 if not client:
2631 raise gclient_utils.Error(
2632 'client not configured; see \'gclient config\'')
2633 return client.RunOnDeps('recurse',
2634 args,
2635 ignore_requirements=True,
2636 progress=not options.no_progress)
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002637
2638
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002639@subcommand.usage('[args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002640@metrics.collector.collect_metrics('gclient fetch')
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002641def CMDfetch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002642 """Fetches upstream commits for all modules.
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002643
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002644 Completely git-specific. Simply runs 'git fetch [args ...]' for each module.
2645 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002646 (options, args) = parser.parse_args(args)
2647 return CMDrecurse(
2648 OptionParser(),
2649 ['--jobs=%d' % options.jobs, '--scm=git', 'git', 'fetch'] + args)
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00002650
2651
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002652class Flattener(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002653 """Flattens a gclient solution."""
2654 def __init__(self, client, pin_all_deps=False):
2655 """Constructor.
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002656
2657 Arguments:
2658 client (GClient): client to flatten
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002659 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2660 in DEPS
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002661 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002662 self._client = client
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002663
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002664 self._deps_string = None
2665 self._deps_graph_lines = None
2666 self._deps_files = set()
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002667
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002668 self._allowed_hosts = set()
2669 self._deps = {}
2670 self._hooks = []
2671 self._pre_deps_hooks = []
2672 self._vars = {}
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002673
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002674 self._flatten(pin_all_deps=pin_all_deps)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002675
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002676 @property
2677 def deps_string(self):
2678 assert self._deps_string is not None
2679 return self._deps_string
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002680
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002681 @property
2682 def deps_graph_lines(self):
2683 assert self._deps_graph_lines is not None
2684 return self._deps_graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00002685
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002686 @property
2687 def deps_files(self):
2688 return self._deps_files
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002689
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002690 def _pin_dep(self, dep):
2691 """Pins a dependency to specific full revision sha.
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002692
2693 Arguments:
2694 dep (Dependency): dependency to process
2695 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002696 if dep.url is None:
2697 return
Michael Mossd683d7c2018-06-15 05:05:17 +00002698
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002699 # Make sure the revision is always fully specified (a hash),
2700 # as opposed to refs or tags which might change. Similarly,
2701 # shortened shas might become ambiguous; make sure to always
2702 # use full one for pinning.
2703 revision = gclient_utils.SplitUrlRevision(dep.url)[1]
2704 if not revision or not gclient_utils.IsFullGitSha(revision):
2705 dep.PinToActualRevision()
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002706
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002707 def _flatten(self, pin_all_deps=False):
2708 """Runs the flattener. Saves resulting DEPS string.
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002709
2710 Arguments:
2711 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2712 in DEPS
2713 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002714 for solution in self._client.dependencies:
2715 self._add_dep(solution)
2716 self._flatten_dep(solution)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002717
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002718 if pin_all_deps:
2719 for dep in self._deps.values():
2720 self._pin_dep(dep)
Paweł Hajdan, Jr39300ba2017-08-11 16:52:38 +02002721
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002722 def add_deps_file(dep):
2723 # Only include DEPS files referenced by recursedeps.
2724 if not dep.should_recurse:
2725 return
2726 deps_file = dep.deps_file
2727 deps_path = os.path.join(self._client.root_dir, dep.name, deps_file)
2728 if not os.path.exists(deps_path):
2729 # gclient has a fallback that if deps_file doesn't exist, it'll
2730 # try DEPS. Do the same here.
2731 deps_file = 'DEPS'
2732 deps_path = os.path.join(self._client.root_dir, dep.name,
2733 deps_file)
2734 if not os.path.exists(deps_path):
2735 return
2736 assert dep.url
2737 self._deps_files.add((dep.url, deps_file, dep.hierarchy_data()))
Paweł Hajdan, Jr76a9d042017-08-18 20:05:41 +02002738
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002739 for dep in self._deps.values():
2740 add_deps_file(dep)
Joanna Wang9144b672023-02-24 23:36:17 +00002741
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002742 gn_args_dep = self._deps.get(self._client.dependencies[0]._gn_args_from,
2743 self._client.dependencies[0])
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002744
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002745 self._deps_graph_lines = _DepsToDotGraphLines(self._deps)
2746 self._deps_string = '\n'.join(
2747 _GNSettingsToLines(gn_args_dep._gn_args_file, gn_args_dep._gn_args)
2748 + _AllowedHostsToLines(self._allowed_hosts) +
2749 _DepsToLines(self._deps) + _HooksToLines('hooks', self._hooks) +
2750 _HooksToLines('pre_deps_hooks', self._pre_deps_hooks) +
2751 _VarsToLines(self._vars) + [
2752 '# %s, %s' % (url, deps_file)
2753 for url, deps_file, _ in sorted(self._deps_files)
2754 ] + ['']) # Ensure newline at end of file.
2755
2756 def _add_dep(self, dep):
2757 """Helper to add a dependency to flattened DEPS.
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002758
2759 Arguments:
2760 dep (Dependency): dependency to add
2761 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002762 assert dep.name not in self._deps or self._deps.get(
2763 dep.name) == dep, (dep.name, self._deps.get(dep.name))
2764 if dep.url:
2765 self._deps[dep.name] = dep
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002766
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002767 def _flatten_dep(self, dep):
2768 """Visits a dependency in order to flatten it (see CMDflatten).
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002769
2770 Arguments:
2771 dep (Dependency): dependency to process
2772 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002773 logging.debug('_flatten_dep(%s)', dep.name)
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002774
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002775 assert dep.deps_parsed, (
2776 "Attempted to flatten %s but it has not been processed." % dep.name)
Paweł Hajdan, Jrc69b32e2017-08-17 18:47:48 +02002777
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002778 self._allowed_hosts.update(dep.allowed_hosts)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002779
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002780 # Only include vars explicitly listed in the DEPS files or gclient
2781 # solution, not automatic, local overrides (i.e. not all of
2782 # dep.get_vars()).
2783 hierarchy = dep.hierarchy(include_url=False)
2784 for key, value in dep._vars.items():
2785 # Make sure there are no conflicting variables. It is fine however
2786 # to use same variable name, as long as the value is consistent.
2787 assert key not in self._vars or self._vars[key][1] == value, (
2788 "dep:%s key:%s value:%s != %s" %
2789 (dep.name, key, value, self._vars[key][1]))
2790 self._vars[key] = (hierarchy, value)
2791 # Override explicit custom variables.
2792 for key, value in dep.custom_vars.items():
2793 # Do custom_vars that don't correspond to DEPS vars ever make sense?
2794 # DEPS conditionals shouldn't be using vars that aren't also defined
2795 # in the DEPS (presubmit actually disallows this), so any new
2796 # custom_var must be unused in the DEPS, so no need to add it to the
2797 # flattened output either.
2798 if key not in self._vars:
2799 continue
2800 # Don't "override" existing vars if it's actually the same value.
2801 if self._vars[key][1] == value:
2802 continue
2803 # Anything else is overriding a default value from the DEPS.
2804 self._vars[key] = (hierarchy + ' [custom_var override]', value)
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02002805
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002806 self._pre_deps_hooks.extend([(dep, hook)
2807 for hook in dep.pre_deps_hooks])
2808 self._hooks.extend([(dep, hook) for hook in dep.deps_hooks])
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002809
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002810 for sub_dep in dep.dependencies:
2811 self._add_dep(sub_dep)
Paweł Hajdan, Jrb0ad16e2017-08-03 15:33:21 +02002812
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002813 for d in dep.dependencies:
2814 if d.should_recurse:
2815 self._flatten_dep(d)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002816
2817
Joanna Wang3ab2f212023-08-09 01:25:15 +00002818@metrics.collector.collect_metrics('gclient gitmodules')
2819def CMDgitmodules(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002820 """Adds or updates Git Submodules based on the contents of the DEPS file.
Joanna Wang3ab2f212023-08-09 01:25:15 +00002821
2822 This command should be run in the root director of the repo.
2823 It will create or update the .gitmodules file and include
2824 `gclient-condition` values. Commits in gitlinks will also be updated.
2825 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002826 parser.add_option('--output-gitmodules',
2827 help='name of the .gitmodules file to write to',
2828 default='.gitmodules')
2829 parser.add_option(
2830 '--deps-file',
2831 help=
2832 'name of the deps file to parse for git dependency paths and commits.',
2833 default='DEPS')
2834 parser.add_option(
2835 '--skip-dep',
2836 action="append",
2837 help='skip adding gitmodules for the git dependency at the given path',
2838 default=[])
2839 options, args = parser.parse_args(args)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002840
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002841 deps_dir = os.path.dirname(os.path.abspath(options.deps_file))
2842 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
2843 if not gclient_path:
2844 logging.error(
2845 '.gclient not found\n'
2846 'Make sure you are running this script from a gclient workspace.')
2847 sys.exit(1)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002848
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002849 deps_content = gclient_utils.FileRead(options.deps_file)
2850 ls = gclient_eval.Parse(deps_content, options.deps_file, None, None)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002851
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002852 prefix_length = 0
2853 if not 'use_relative_paths' in ls or ls['use_relative_paths'] != True:
2854 delta_path = os.path.relpath(deps_dir, os.path.abspath(gclient_path))
2855 if delta_path:
2856 prefix_length = len(delta_path.replace(os.path.sep, '/')) + 1
Joanna Wang3ab2f212023-08-09 01:25:15 +00002857
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002858 cache_info = []
2859 with open(options.output_gitmodules, 'w', newline='') as f:
2860 for path, dep in ls.get('deps').items():
2861 if path in options.skip_dep:
2862 continue
2863 if dep.get('dep_type') == 'cipd':
2864 continue
2865 try:
2866 url, commit = dep['url'].split('@', maxsplit=1)
2867 except ValueError:
2868 logging.error('error on %s; %s, not adding it', path,
2869 dep["url"])
2870 continue
2871 if prefix_length:
2872 path = path[prefix_length:]
Joanna Wang3ab2f212023-08-09 01:25:15 +00002873
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002874 cache_info += ['--cacheinfo', f'160000,{commit},{path}']
2875 f.write(f'[submodule "{path}"]\n\tpath = {path}\n\turl = {url}\n')
2876 if 'condition' in dep:
2877 f.write(f'\tgclient-condition = {dep["condition"]}\n')
2878 # Windows has limit how long, so let's chunk those calls.
2879 if len(cache_info) >= 100:
2880 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
2881 cache_info = []
2882
2883 if cache_info:
Josip Sokcevic293aa652023-08-23 18:55:20 +00002884 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002885 subprocess2.call(['git', 'add', '.gitmodules'])
2886 print('.gitmodules and gitlinks updated. Please check git diff and '
2887 'commit changes.')
Joanna Wang3ab2f212023-08-09 01:25:15 +00002888
2889
Edward Lemur3298e7b2018-07-17 18:21:27 +00002890@metrics.collector.collect_metrics('gclient flatten')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002891def CMDflatten(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002892 """Flattens the solutions into a single DEPS file."""
2893 parser.add_option('--output-deps', help='Path to the output DEPS file')
2894 parser.add_option(
2895 '--output-deps-files',
2896 help=('Path to the output metadata about DEPS files referenced by '
2897 'recursedeps.'))
2898 parser.add_option(
2899 '--pin-all-deps',
2900 action='store_true',
2901 help=('Pin all deps, even if not pinned in DEPS. CAVEAT: only does so '
2902 'for checked out deps, NOT deps_os.'))
2903 parser.add_option('--deps-graph-file',
2904 help='Provide a path for the output graph file')
2905 options, args = parser.parse_args(args)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002906
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002907 options.nohooks = True
2908 options.process_all_deps = True
2909 client = GClient.LoadCurrentConfig(options)
2910 if not client:
2911 raise gclient_utils.Error(
2912 'client not configured; see \'gclient config\'')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002913
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002914 # Only print progress if we're writing to a file. Otherwise, progress
2915 # updates could obscure intended output.
2916 code = client.RunOnDeps('flatten', args, progress=options.output_deps)
2917 if code != 0:
2918 return code
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002919
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002920 flattener = Flattener(client, pin_all_deps=options.pin_all_deps)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002921
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002922 if options.output_deps:
2923 with open(options.output_deps, 'w') as f:
2924 f.write(flattener.deps_string)
2925 else:
2926 print(flattener.deps_string)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002927
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002928 if options.deps_graph_file:
2929 with open(options.deps_graph_file, 'w') as f:
2930 f.write('\n'.join(flattener.deps_graph_lines))
Joanna Wang9144b672023-02-24 23:36:17 +00002931
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002932 deps_files = [{
2933 'url': d[0],
2934 'deps_file': d[1],
2935 'hierarchy': d[2]
2936 } for d in sorted(flattener.deps_files)]
2937 if options.output_deps_files:
2938 with open(options.output_deps_files, 'w') as f:
2939 json.dump(deps_files, f)
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002940
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002941 return 0
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002942
2943
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002944def _GNSettingsToLines(gn_args_file, gn_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002945 s = []
2946 if gn_args_file:
2947 s.extend([
2948 'gclient_gn_args_file = "%s"' % gn_args_file,
2949 'gclient_gn_args = %r' % gn_args,
2950 ])
2951 return s
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002952
2953
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02002954def _AllowedHostsToLines(allowed_hosts):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002955 """Converts |allowed_hosts| set to list of lines for output."""
2956 if not allowed_hosts:
2957 return []
2958 s = ['allowed_hosts = [']
2959 for h in sorted(allowed_hosts):
2960 s.append(' "%s",' % h)
2961 s.extend([']', ''])
2962 return s
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02002963
2964
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002965def _DepsToLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002966 # type: (Mapping[str, Dependency]) -> Sequence[str]
2967 """Converts |deps| dict to list of lines for output."""
2968 if not deps:
2969 return []
2970 s = ['deps = {']
2971 for _, dep in sorted(deps.items()):
2972 s.extend(dep.ToLines())
2973 s.extend(['}', ''])
2974 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002975
2976
Joanna Wang9144b672023-02-24 23:36:17 +00002977def _DepsToDotGraphLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002978 # type: (Mapping[str, Dependency]) -> Sequence[str]
2979 """Converts |deps| dict to list of lines for dot graphs"""
2980 if not deps:
2981 return []
2982 graph_lines = ["digraph {\n\trankdir=\"LR\";"]
2983 for _, dep in sorted(deps.items()):
2984 line = dep.hierarchy(include_url=False, graphviz=True)
2985 if line:
2986 graph_lines.append("\t%s" % line)
2987 graph_lines.append("}")
2988 return graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00002989
2990
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02002991def _DepsOsToLines(deps_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002992 """Converts |deps_os| dict to list of lines for output."""
2993 if not deps_os:
2994 return []
2995 s = ['deps_os = {']
2996 for dep_os, os_deps in sorted(deps_os.items()):
2997 s.append(' "%s": {' % dep_os)
2998 for name, dep in sorted(os_deps.items()):
2999 condition_part = ([' "condition": %r,' %
3000 dep.condition] if dep.condition else [])
3001 s.extend([
3002 ' # %s' % dep.hierarchy(include_url=False),
3003 ' "%s": {' % (name, ),
3004 ' "url": "%s",' % (dep.url, ),
3005 ] + condition_part + [
3006 ' },',
3007 '',
3008 ])
3009 s.extend([' },', ''])
3010 s.extend(['}', ''])
3011 return s
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02003012
3013
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003014def _HooksToLines(name, hooks):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003015 """Converts |hooks| list to list of lines for output."""
3016 if not hooks:
3017 return []
3018 s = ['%s = [' % name]
3019 for dep, hook in hooks:
3020 s.extend([
3021 ' # %s' % dep.hierarchy(include_url=False),
3022 ' {',
3023 ])
3024 if hook.name is not None:
3025 s.append(' "name": "%s",' % hook.name)
3026 if hook.pattern is not None:
3027 s.append(' "pattern": "%s",' % hook.pattern)
3028 if hook.condition is not None:
3029 s.append(' "condition": %r,' % hook.condition)
3030 # Flattened hooks need to be written relative to the root gclient dir
3031 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3032 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3033 [' "%s",' % arg
3034 for arg in hook.action] + [' ]', ' },', ''])
3035 s.extend([']', ''])
3036 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02003037
3038
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003039def _HooksOsToLines(hooks_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003040 """Converts |hooks| list to list of lines for output."""
3041 if not hooks_os:
3042 return []
3043 s = ['hooks_os = {']
3044 for hook_os, os_hooks in hooks_os.items():
3045 s.append(' "%s": [' % hook_os)
3046 for dep, hook in os_hooks:
3047 s.extend([
3048 ' # %s' % dep.hierarchy(include_url=False),
3049 ' {',
3050 ])
3051 if hook.name is not None:
3052 s.append(' "name": "%s",' % hook.name)
3053 if hook.pattern is not None:
3054 s.append(' "pattern": "%s",' % hook.pattern)
3055 if hook.condition is not None:
3056 s.append(' "condition": %r,' % hook.condition)
3057 # Flattened hooks need to be written relative to the root gclient
3058 # dir
3059 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3060 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3061 [' "%s",' % arg
3062 for arg in hook.action] + [' ]', ' },', ''])
3063 s.extend([' ],', ''])
3064 s.extend(['}', ''])
3065 return s
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003066
3067
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003068def _VarsToLines(variables):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003069 """Converts |variables| dict to list of lines for output."""
3070 if not variables:
3071 return []
3072 s = ['vars = {']
3073 for key, tup in sorted(variables.items()):
3074 hierarchy, value = tup
3075 s.extend([
3076 ' # %s' % hierarchy,
3077 ' "%s": %r,' % (key, value),
3078 '',
3079 ])
3080 s.extend(['}', ''])
3081 return s
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003082
3083
Edward Lemur3298e7b2018-07-17 18:21:27 +00003084@metrics.collector.collect_metrics('gclient grep')
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003085def CMDgrep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003086 """Greps through git repos managed by gclient.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003087
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003088 Runs 'git grep [args...]' for each module.
3089 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003090 # We can't use optparse because it will try to parse arguments sent
3091 # to git grep and throw an error. :-(
3092 if not args or re.match('(-h|--help)$', args[0]):
3093 print(
3094 'Usage: gclient grep [-j <N>] git-grep-args...\n\n'
3095 'Example: "gclient grep -j10 -A2 RefCountedBase" runs\n"git grep '
3096 '-A2 RefCountedBase" on each of gclient\'s git\nrepos with up to '
3097 '10 jobs.\n\nBonus: page output by appending "|& less -FRSX" to the'
3098 ' end of your query.',
3099 file=sys.stderr)
3100 return 1
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003101
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003102 jobs_arg = ['--jobs=1']
3103 if re.match(r'(-j|--jobs=)\d+$', args[0]):
3104 jobs_arg, args = args[:1], args[1:]
3105 elif re.match(r'(-j|--jobs)$', args[0]):
3106 jobs_arg, args = args[:2], args[2:]
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003107
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003108 return CMDrecurse(
3109 parser, jobs_arg + [
3110 '--ignore', '--prepend-dir', '--no-progress', '--scm=git', 'git',
3111 'grep', '--null', '--color=Always'
3112 ] + args)
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00003113
3114
Edward Lemur3298e7b2018-07-17 18:21:27 +00003115@metrics.collector.collect_metrics('gclient root')
stip@chromium.orga735da22015-04-29 23:18:20 +00003116def CMDroot(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003117 """Outputs the solution root (or current dir if there isn't one)."""
3118 (options, args) = parser.parse_args(args)
3119 client = GClient.LoadCurrentConfig(options)
3120 if client:
3121 print(os.path.abspath(client.root_dir))
3122 else:
3123 print(os.path.abspath('.'))
stip@chromium.orga735da22015-04-29 23:18:20 +00003124
3125
agablea98a6cd2016-11-15 14:30:10 -08003126@subcommand.usage('[url]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00003127@metrics.collector.collect_metrics('gclient config')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003128def CMDconfig(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003129 """Creates a .gclient file in the current directory.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003130
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003131 This specifies the configuration for further commands. After update/sync,
3132 top-level DEPS files in each module are read to determine dependent
3133 modules to operate on as well. If optional [url] parameter is
3134 provided, then configuration is read from a specified Subversion server
3135 URL.
3136 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003137 # We do a little dance with the --gclientfile option. 'gclient config' is
3138 # the only command where it's acceptable to have both '--gclientfile' and
3139 # '--spec' arguments. So, we temporarily stash any --gclientfile parameter
3140 # into options.output_config_file until after the (gclientfile xor spec)
3141 # error check.
3142 parser.remove_option('--gclientfile')
3143 parser.add_option('--gclientfile',
3144 dest='output_config_file',
3145 help='Specify an alternate .gclient file')
3146 parser.add_option('--name',
3147 help='overrides the default name for the solution')
3148 parser.add_option(
3149 '--deps-file',
3150 default='DEPS',
3151 help='overrides the default name for the DEPS file for the '
3152 'main solutions and all sub-dependencies')
3153 parser.add_option('--unmanaged',
3154 action='store_true',
3155 default=False,
3156 help='overrides the default behavior to make it possible '
3157 'to have the main solution untouched by gclient '
3158 '(gclient will check out unmanaged dependencies but '
3159 'will never sync them)')
3160 parser.add_option('--cache-dir',
3161 default=UNSET_CACHE_DIR,
3162 help='Cache all git repos into this dir and do shared '
3163 'clones from the cache, instead of cloning directly '
3164 'from the remote. Pass "None" to disable cache, even '
3165 'if globally enabled due to $GIT_CACHE_PATH.')
3166 parser.add_option('--custom-var',
3167 action='append',
3168 dest='custom_vars',
3169 default=[],
3170 help='overrides variables; key=value syntax')
3171 parser.set_defaults(config_filename=None)
3172 (options, args) = parser.parse_args(args)
3173 if options.output_config_file:
3174 setattr(options, 'config_filename',
3175 getattr(options, 'output_config_file'))
3176 if ((options.spec and args) or len(args) > 2
3177 or (not options.spec and not args)):
3178 parser.error(
3179 'Inconsistent arguments. Use either --spec or one or 2 args')
maruel@chromium.org5fc2a332010-05-26 19:37:15 +00003180
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003181 if (options.cache_dir is not UNSET_CACHE_DIR
3182 and options.cache_dir.lower() == 'none'):
3183 options.cache_dir = None
Robert Iannuccia19649b2018-06-29 16:31:45 +00003184
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003185 custom_vars = {}
3186 for arg in options.custom_vars:
3187 kv = arg.split('=', 1)
3188 if len(kv) != 2:
3189 parser.error('Invalid --custom-var argument: %r' % arg)
3190 custom_vars[kv[0]] = gclient_eval.EvaluateCondition(kv[1], {})
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02003191
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003192 client = GClient('.', options)
3193 if options.spec:
3194 client.SetConfig(options.spec)
iposva@chromium.org8cf7a392010-04-07 17:20:26 +00003195 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003196 base_url = args[0].rstrip('/')
3197 if not options.name:
3198 name = base_url.split('/')[-1]
3199 if name.endswith('.git'):
3200 name = name[:-4]
3201 else:
3202 # specify an alternate relpath for the given URL.
3203 name = options.name
3204 if not os.path.abspath(os.path.join(os.getcwd(), name)).startswith(
3205 os.getcwd()):
3206 parser.error('Do not pass a relative path for --name.')
3207 if any(x in ('..', '.', '/', '\\') for x in name.split(os.sep)):
3208 parser.error(
3209 'Do not include relative path components in --name.')
agable@chromium.orgf2214672015-10-27 21:02:48 +00003210
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003211 deps_file = options.deps_file
3212 client.SetDefaultConfig(name,
3213 deps_file,
3214 base_url,
3215 managed=not options.unmanaged,
3216 cache_dir=options.cache_dir,
3217 custom_vars=custom_vars)
3218 client.SaveConfig()
3219 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003220
3221
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003222@subcommand.epilog("""Example:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003223 gclient pack > patch.txt
3224 generate simple patch for configured client and dependences
3225""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003226@metrics.collector.collect_metrics('gclient pack')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003227def CMDpack(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003228 """Generates a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003229
agabled437d762016-10-17 09:35:11 -07003230 Internally, runs 'git diff' on each checked out module and
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003231 dependencies, and performs minimal postprocessing of the output. The
3232 resulting patch is printed to stdout and can be applied to a freshly
3233 checked out tree via 'patch -p0 < patchfile'.
3234 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003235 parser.add_option('--deps',
3236 dest='deps_os',
3237 metavar='OS_LIST',
3238 help='override deps for the specified (comma-separated) '
3239 'platform(s); \'all\' will process all deps_os '
3240 'references')
3241 parser.remove_option('--jobs')
3242 (options, args) = parser.parse_args(args)
3243 # Force jobs to 1 so the stdout is not annotated with the thread ids
3244 options.jobs = 1
3245 client = GClient.LoadCurrentConfig(options)
3246 if not client:
3247 raise gclient_utils.Error(
3248 'client not configured; see \'gclient config\'')
3249 if options.verbose:
3250 client.PrintLocationAndContents()
3251 return client.RunOnDeps('pack', args)
kbr@google.comab318592009-09-04 00:54:55 +00003252
3253
Edward Lemur3298e7b2018-07-17 18:21:27 +00003254@metrics.collector.collect_metrics('gclient status')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003255def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003256 """Shows modification status for every dependencies."""
3257 parser.add_option('--deps',
3258 dest='deps_os',
3259 metavar='OS_LIST',
3260 help='override deps for the specified (comma-separated) '
3261 'platform(s); \'all\' will process all deps_os '
3262 'references')
3263 (options, args) = parser.parse_args(args)
3264 client = GClient.LoadCurrentConfig(options)
3265 if not client:
3266 raise gclient_utils.Error(
3267 'client not configured; see \'gclient config\'')
3268 if options.verbose:
3269 client.PrintLocationAndContents()
3270 return client.RunOnDeps('status', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003271
3272
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003273@subcommand.epilog("""Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +00003274 gclient sync
3275 update files from SCM according to current configuration,
3276 *for modules which have changed since last update or sync*
3277 gclient sync --force
3278 update files from SCM according to current configuration, for
3279 all modules (useful for recovering files deleted from local copy)
Edward Lesmes3ffca4b2021-05-19 19:36:17 +00003280 gclient sync --revision src@GIT_COMMIT_OR_REF
3281 update src directory to GIT_COMMIT_OR_REF
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003282
3283JSON output format:
3284If the --output-json option is specified, the following document structure will
3285be emitted to the provided file. 'null' entries may occur for subprojects which
3286are present in the gclient solution, but were not processed (due to custom_deps,
3287os_deps, etc.)
3288
3289{
3290 "solutions" : {
3291 "<name>": { # <name> is the posix-normalized path to the solution.
agabled437d762016-10-17 09:35:11 -07003292 "revision": [<git id hex string>|null],
3293 "scm": ["git"|null],
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003294 }
3295 }
3296}
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003297""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003298@metrics.collector.collect_metrics('gclient sync')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003299def CMDsync(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003300 """Checkout/update all modules."""
3301 parser.add_option('-f',
3302 '--force',
3303 action='store_true',
3304 help='force update even for unchanged modules')
3305 parser.add_option('-n',
3306 '--nohooks',
3307 action='store_true',
3308 help='don\'t run hooks after the update is complete')
3309 parser.add_option('-p',
3310 '--noprehooks',
3311 action='store_true',
3312 help='don\'t run pre-DEPS hooks',
3313 default=False)
3314 parser.add_option('-r',
3315 '--revision',
3316 action='append',
3317 dest='revisions',
3318 metavar='REV',
3319 default=[],
3320 help='Enforces git ref/hash for the solutions with the '
3321 'format src@rev. The src@ part is optional and can be '
3322 'skipped. You can also specify URLs instead of paths '
3323 'and gclient will find the solution corresponding to '
3324 'the given URL. If a path is also specified, the URL '
3325 'takes precedence. -r can be used multiple times when '
3326 '.gclient has multiple solutions configured, and will '
3327 'work even if the src@ part is skipped. Revision '
3328 'numbers (e.g. 31000 or r31000) are not supported.')
3329 parser.add_option('--patch-ref',
3330 action='append',
3331 dest='patch_refs',
3332 metavar='GERRIT_REF',
3333 default=[],
3334 help='Patches the given reference with the format '
3335 'dep@target-ref:patch-ref. '
3336 'For |dep|, you can specify URLs as well as paths, '
3337 'with URLs taking preference. '
3338 '|patch-ref| will be applied to |dep|, rebased on top '
3339 'of what |dep| was synced to, and a soft reset will '
3340 'be done. Use --no-rebase-patch-ref and '
3341 '--no-reset-patch-ref to disable this behavior. '
3342 '|target-ref| is the target branch against which a '
3343 'patch was created, it is used to determine which '
3344 'commits from the |patch-ref| actually constitute a '
3345 'patch.')
3346 parser.add_option(
3347 '-t',
3348 '--download-topics',
3349 action='store_true',
3350 help='Downloads and patches locally changes from all open '
3351 'Gerrit CLs that have the same topic as the changes '
3352 'in the specified patch_refs. Only works if atleast '
3353 'one --patch-ref is specified.')
3354 parser.add_option('--with_branch_heads',
3355 action='store_true',
3356 help='Clone git "branch_heads" refspecs in addition to '
3357 'the default refspecs. This adds about 1/2GB to a '
3358 'full checkout. (git only)')
3359 parser.add_option(
3360 '--with_tags',
3361 action='store_true',
3362 help='Clone git tags in addition to the default refspecs.')
3363 parser.add_option('-H',
3364 '--head',
3365 action='store_true',
3366 help='DEPRECATED: only made sense with safesync urls.')
3367 parser.add_option(
3368 '-D',
3369 '--delete_unversioned_trees',
3370 action='store_true',
3371 help='Deletes from the working copy any dependencies that '
3372 'have been removed since the last sync, as long as '
3373 'there are no local modifications. When used with '
3374 '--force, such dependencies are removed even if they '
3375 'have local modifications. When used with --reset, '
3376 'all untracked directories are removed from the '
3377 'working copy, excluding those which are explicitly '
3378 'ignored in the repository.')
3379 parser.add_option(
3380 '-R',
3381 '--reset',
3382 action='store_true',
3383 help='resets any local changes before updating (git only)')
3384 parser.add_option('-M',
3385 '--merge',
3386 action='store_true',
3387 help='merge upstream changes instead of trying to '
3388 'fast-forward or rebase')
3389 parser.add_option('-A',
3390 '--auto_rebase',
3391 action='store_true',
3392 help='Automatically rebase repositories against local '
3393 'checkout during update (git only).')
3394 parser.add_option('--deps',
3395 dest='deps_os',
3396 metavar='OS_LIST',
3397 help='override deps for the specified (comma-separated) '
3398 'platform(s); \'all\' will process all deps_os '
3399 'references')
3400 parser.add_option('--process-all-deps',
3401 action='store_true',
3402 help='Check out all deps, even for different OS-es, '
3403 'or with conditions evaluating to false')
3404 parser.add_option('--upstream',
3405 action='store_true',
3406 help='Make repo state match upstream branch.')
3407 parser.add_option('--output-json',
3408 help='Output a json document to this path containing '
3409 'summary information about the sync.')
3410 parser.add_option(
3411 '--no-history',
3412 action='store_true',
3413 help='GIT ONLY - Reduces the size/time of the checkout at '
3414 'the cost of no history. Requires Git 1.9+')
3415 parser.add_option('--shallow',
3416 action='store_true',
3417 help='GIT ONLY - Do a shallow clone into the cache dir. '
3418 'Requires Git 1.9+')
3419 parser.add_option('--no_bootstrap',
3420 '--no-bootstrap',
3421 action='store_true',
3422 help='Don\'t bootstrap from Google Storage.')
3423 parser.add_option('--ignore_locks',
3424 action='store_true',
3425 help='No longer used.')
3426 parser.add_option('--break_repo_locks',
3427 action='store_true',
3428 help='No longer used.')
3429 parser.add_option('--lock_timeout',
3430 type='int',
3431 default=5000,
3432 help='GIT ONLY - Deadline (in seconds) to wait for git '
3433 'cache lock to become available. Default is %default.')
3434 parser.add_option('--no-rebase-patch-ref',
3435 action='store_false',
3436 dest='rebase_patch_ref',
3437 default=True,
3438 help='Bypass rebase of the patch ref after checkout.')
3439 parser.add_option('--no-reset-patch-ref',
3440 action='store_false',
3441 dest='reset_patch_ref',
3442 default=True,
3443 help='Bypass calling reset after patching the ref.')
3444 parser.add_option('--experiment',
3445 action='append',
3446 dest='experiments',
3447 default=[],
3448 help='Which experiments should be enabled.')
3449 (options, args) = parser.parse_args(args)
3450 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003451
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003452 if not client:
3453 raise gclient_utils.Error(
3454 'client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003455
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003456 if options.download_topics and not options.rebase_patch_ref:
3457 raise gclient_utils.Error(
3458 'Warning: You cannot download topics and not rebase each patch ref')
Ravi Mistryecda7822022-02-28 16:22:20 +00003459
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003460 if options.ignore_locks:
3461 print(
3462 'Warning: ignore_locks is no longer used. Please remove its usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003463
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003464 if options.break_repo_locks:
3465 print('Warning: break_repo_locks is no longer used. Please remove its '
3466 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003467
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003468 if options.revisions and options.head:
3469 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
3470 print('Warning: you cannot use both --head and --revision')
smutae7ea312016-07-18 11:59:41 -07003471
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003472 if options.verbose:
3473 client.PrintLocationAndContents()
3474 ret = client.RunOnDeps('update', args)
3475 if options.output_json:
3476 slns = {}
3477 for d in client.subtree(True):
3478 normed = d.name.replace('\\', '/').rstrip('/') + '/'
3479 slns[normed] = {
3480 'revision': d.got_revision,
3481 'scm': d.used_scm.name if d.used_scm else None,
3482 'url': str(d.url) if d.url else None,
3483 'was_processed': d.should_process,
3484 'was_synced': d._should_sync,
3485 }
3486 with open(options.output_json, 'w') as f:
3487 json.dump({'solutions': slns}, f)
3488 return ret
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003489
3490
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003491CMDupdate = CMDsync
3492
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003493
Edward Lemur3298e7b2018-07-17 18:21:27 +00003494@metrics.collector.collect_metrics('gclient validate')
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003495def CMDvalidate(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003496 """Validates the .gclient and DEPS syntax."""
3497 options, args = parser.parse_args(args)
3498 client = GClient.LoadCurrentConfig(options)
3499 if not client:
3500 raise gclient_utils.Error(
3501 'client not configured; see \'gclient config\'')
3502 rv = client.RunOnDeps('validate', args)
3503 if rv == 0:
3504 print('validate: SUCCESS')
3505 else:
3506 print('validate: FAILURE')
3507 return rv
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003508
3509
Edward Lemur3298e7b2018-07-17 18:21:27 +00003510@metrics.collector.collect_metrics('gclient diff')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003511def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003512 """Displays local diff for every dependencies."""
3513 parser.add_option('--deps',
3514 dest='deps_os',
3515 metavar='OS_LIST',
3516 help='override deps for the specified (comma-separated) '
3517 'platform(s); \'all\' will process all deps_os '
3518 'references')
3519 (options, args) = parser.parse_args(args)
3520 client = GClient.LoadCurrentConfig(options)
3521 if not client:
3522 raise gclient_utils.Error(
3523 'client not configured; see \'gclient config\'')
3524 if options.verbose:
3525 client.PrintLocationAndContents()
3526 return client.RunOnDeps('diff', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003527
3528
Edward Lemur3298e7b2018-07-17 18:21:27 +00003529@metrics.collector.collect_metrics('gclient revert')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003530def CMDrevert(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003531 """Reverts all modifications in every dependencies.
maruel@chromium.org28d14bd2010-11-11 20:37:09 +00003532
3533 That's the nuclear option to get back to a 'clean' state. It removes anything
agabled437d762016-10-17 09:35:11 -07003534 that shows up in git status."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003535 parser.add_option('--deps',
3536 dest='deps_os',
3537 metavar='OS_LIST',
3538 help='override deps for the specified (comma-separated) '
3539 'platform(s); \'all\' will process all deps_os '
3540 'references')
3541 parser.add_option('-n',
3542 '--nohooks',
3543 action='store_true',
3544 help='don\'t run hooks after the revert is complete')
3545 parser.add_option('-p',
3546 '--noprehooks',
3547 action='store_true',
3548 help='don\'t run pre-DEPS hooks',
3549 default=False)
3550 parser.add_option('--upstream',
3551 action='store_true',
3552 help='Make repo state match upstream branch.')
3553 parser.add_option('--break_repo_locks',
3554 action='store_true',
3555 help='No longer used.')
3556 (options, args) = parser.parse_args(args)
3557 if options.break_repo_locks:
3558 print(
3559 'Warning: break_repo_locks is no longer used. Please remove its ' +
3560 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003561
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003562 # --force is implied.
3563 options.force = True
3564 options.reset = False
3565 options.delete_unversioned_trees = False
3566 options.merge = False
3567 client = GClient.LoadCurrentConfig(options)
3568 if not client:
3569 raise gclient_utils.Error(
3570 'client not configured; see \'gclient config\'')
3571 return client.RunOnDeps('revert', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003572
3573
Edward Lemur3298e7b2018-07-17 18:21:27 +00003574@metrics.collector.collect_metrics('gclient runhooks')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003575def CMDrunhooks(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003576 """Runs hooks for files that have been modified in the local working copy."""
3577 parser.add_option('--deps',
3578 dest='deps_os',
3579 metavar='OS_LIST',
3580 help='override deps for the specified (comma-separated) '
3581 'platform(s); \'all\' will process all deps_os '
3582 'references')
3583 parser.add_option('-f',
3584 '--force',
3585 action='store_true',
3586 default=True,
3587 help='Deprecated. No effect.')
3588 (options, args) = parser.parse_args(args)
3589 client = GClient.LoadCurrentConfig(options)
3590 if not client:
3591 raise gclient_utils.Error(
3592 'client not configured; see \'gclient config\'')
3593 if options.verbose:
3594 client.PrintLocationAndContents()
3595 options.force = True
3596 options.nohooks = False
3597 return client.RunOnDeps('runhooks', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003598
3599
Gavin Mak50b27a52023-09-19 22:44:59 +00003600# TODO(crbug.com/1481266): Collect merics for installhooks.
3601def CMDinstallhooks(parser, args):
3602 """Installs gclient git hooks.
3603
3604 Currently only installs a pre-commit hook to drop staged gitlinks. To
3605 bypass this pre-commit hook once it's installed, set the environment
3606 variable SKIP_GITLINK_PRECOMMIT=1.
3607 """
3608 (options, args) = parser.parse_args(args)
3609 client = GClient.LoadCurrentConfig(options)
3610 if not client:
3611 raise gclient_utils.Error(
3612 'client not configured; see \'gclient config\'')
3613 client._InstallPreCommitHook()
3614 return 0
3615
3616
Edward Lemur3298e7b2018-07-17 18:21:27 +00003617@metrics.collector.collect_metrics('gclient revinfo')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003618def CMDrevinfo(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003619 """Outputs revision info mapping for the client and its dependencies.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003620
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00003621 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003622 can be used to reproduce the same tree in the future. It is only useful for
agabled437d762016-10-17 09:35:11 -07003623 'unpinned dependencies', i.e. DEPS/deps references without a git hash.
3624 A git branch name isn't 'pinned' since the actual commit can change.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003625 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003626 parser.add_option('--deps',
3627 dest='deps_os',
3628 metavar='OS_LIST',
3629 help='override deps for the specified (comma-separated) '
3630 'platform(s); \'all\' will process all deps_os '
3631 'references')
3632 parser.add_option(
3633 '-a',
3634 '--actual',
3635 action='store_true',
3636 help='gets the actual checked out revisions instead of the '
3637 'ones specified in the DEPS and .gclient files')
3638 parser.add_option('-s',
3639 '--snapshot',
3640 action='store_true',
3641 help='creates a snapshot .gclient file of the current '
3642 'version of all repositories to reproduce the tree, '
3643 'implies -a')
3644 parser.add_option(
3645 '--filter',
3646 action='append',
3647 dest='filter',
3648 help='Display revision information only for the specified '
3649 'dependencies (filtered by URL or path).')
3650 parser.add_option('--output-json',
3651 help='Output a json document to this path containing '
3652 'information about the revisions.')
3653 parser.add_option(
3654 '--ignore-dep-type',
3655 choices=['git', 'cipd'],
3656 help='Specify to skip processing of a certain type of dep.')
3657 (options, args) = parser.parse_args(args)
3658 client = GClient.LoadCurrentConfig(options)
3659 if not client:
3660 raise gclient_utils.Error(
3661 'client not configured; see \'gclient config\'')
3662 client.PrintRevInfo()
3663 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003664
3665
Edward Lemur3298e7b2018-07-17 18:21:27 +00003666@metrics.collector.collect_metrics('gclient getdep')
Edward Lesmes411041f2018-04-05 20:12:55 -04003667def CMDgetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003668 """Gets revision information and variable values from a DEPS file.
Josip Sokcevic7b5e3d72023-06-13 00:28:23 +00003669
3670 If key doesn't exist or is incorrectly declared, this script exits with exit
3671 code 2."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003672 parser.add_option('--var',
3673 action='append',
3674 dest='vars',
3675 metavar='VAR',
3676 default=[],
3677 help='Gets the value of a given variable.')
3678 parser.add_option(
3679 '-r',
3680 '--revision',
3681 action='append',
3682 dest='getdep_revisions',
3683 metavar='DEP',
3684 default=[],
3685 help='Gets the revision/version for the given dependency. '
3686 'If it is a git dependency, dep must be a path. If it '
3687 'is a CIPD dependency, dep must be of the form '
3688 'path:package.')
3689 parser.add_option(
3690 '--deps-file',
3691 default='DEPS',
3692 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3693 # .gclient first.
3694 help='The DEPS file to be edited. Defaults to the DEPS '
3695 'file in the current directory.')
3696 (options, args) = parser.parse_args(args)
Edward Lesmes411041f2018-04-05 20:12:55 -04003697
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003698 if not os.path.isfile(options.deps_file):
3699 raise gclient_utils.Error('DEPS file %s does not exist.' %
3700 options.deps_file)
3701 with open(options.deps_file) as f:
3702 contents = f.read()
3703 client = GClient.LoadCurrentConfig(options)
3704 if client is not None:
3705 builtin_vars = client.get_builtin_vars()
Edward Lesmes411041f2018-04-05 20:12:55 -04003706 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003707 logging.warning(
3708 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3709 'file without support for built-in variables.')
3710 builtin_vars = None
3711 local_scope = gclient_eval.Exec(contents,
3712 options.deps_file,
3713 builtin_vars=builtin_vars)
3714
3715 for var in options.vars:
3716 print(gclient_eval.GetVar(local_scope, var))
3717
3718 commits = {}
3719 if local_scope.get(
3720 'git_dependencies'
3721 ) == gclient_eval.SUBMODULES and options.getdep_revisions:
3722 commits.update(
3723 scm_git.GIT.GetSubmoduleCommits(
3724 os.getcwd(),
3725 [path for path in options.getdep_revisions if ':' not in path]))
3726
3727 for name in options.getdep_revisions:
3728 if ':' in name:
3729 name, _, package = name.partition(':')
3730 if not name or not package:
3731 parser.error(
3732 'Wrong CIPD format: %s:%s should be of the form path:pkg.' %
3733 (name, package))
3734 print(gclient_eval.GetCIPD(local_scope, name, package))
3735 elif commits:
3736 print(commits[name])
3737 else:
3738 try:
3739 print(gclient_eval.GetRevision(local_scope, name))
3740 except KeyError as e:
3741 print(repr(e), file=sys.stderr)
3742 sys.exit(2)
Edward Lesmes411041f2018-04-05 20:12:55 -04003743
3744
Edward Lemur3298e7b2018-07-17 18:21:27 +00003745@metrics.collector.collect_metrics('gclient setdep')
Edward Lesmes6f64a052018-03-20 17:35:49 -04003746def CMDsetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003747 """Modifies dependency revisions and variable values in a DEPS file"""
3748 parser.add_option('--var',
3749 action='append',
3750 dest='vars',
3751 metavar='VAR=VAL',
3752 default=[],
3753 help='Sets a variable to the given value with the format '
3754 'name=value.')
3755 parser.add_option('-r',
3756 '--revision',
3757 action='append',
3758 dest='setdep_revisions',
3759 metavar='DEP@REV',
3760 default=[],
3761 help='Sets the revision/version for the dependency with '
3762 'the format dep@rev. If it is a git dependency, dep '
3763 'must be a path and rev must be a git hash or '
3764 'reference (e.g. src/dep@deadbeef). If it is a CIPD '
3765 'dependency, dep must be of the form path:package and '
3766 'rev must be the package version '
3767 '(e.g. src/pkg:chromium/pkg@2.1-cr0).')
3768 parser.add_option(
3769 '--deps-file',
3770 default='DEPS',
3771 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3772 # .gclient first.
3773 help='The DEPS file to be edited. Defaults to the DEPS '
3774 'file in the current directory.')
3775 (options, args) = parser.parse_args(args)
3776 if args:
3777 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3778 if not options.setdep_revisions and not options.vars:
Edward Lesmes0ecf6d62018-04-05 18:28:55 -04003779 parser.error(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003780 'You must specify at least one variable or revision to modify.')
3781
3782 if not os.path.isfile(options.deps_file):
3783 raise gclient_utils.Error('DEPS file %s does not exist.' %
3784 options.deps_file)
3785 with open(options.deps_file) as f:
3786 contents = f.read()
3787
3788 client = GClient.LoadCurrentConfig(options)
3789 if client is not None:
3790 builtin_vars = client.get_builtin_vars()
Edward Lesmes6f64a052018-03-20 17:35:49 -04003791 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003792 logging.warning(
3793 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3794 'file without support for built-in variables.')
3795 builtin_vars = None
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003796
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003797 local_scope = gclient_eval.Exec(contents,
3798 options.deps_file,
3799 builtin_vars=builtin_vars)
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003800
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003801 # Create a set of all git submodules.
3802 cwd = os.path.dirname(options.deps_file) or os.getcwd()
3803 git_modules = None
3804 if 'git_dependencies' in local_scope and local_scope[
3805 'git_dependencies'] in (gclient_eval.SUBMODULES, gclient_eval.SYNC):
3806 try:
3807 submodule_status = subprocess2.check_output(
3808 ['git', 'submodule', 'status'], cwd=cwd).decode('utf-8')
3809 git_modules = {l.split()[1] for l in submodule_status.splitlines()}
3810 except subprocess2.CalledProcessError as e:
3811 print('Warning: gitlinks won\'t be updated: ', e)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003812
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003813 for var in options.vars:
3814 name, _, value = var.partition('=')
3815 if not name or not value:
3816 parser.error(
3817 'Wrong var format: %s should be of the form name=value.' % var)
3818 if name in local_scope['vars']:
3819 gclient_eval.SetVar(local_scope, name, value)
3820 else:
3821 gclient_eval.AddVar(local_scope, name, value)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003822
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003823 for revision in options.setdep_revisions:
3824 name, _, value = revision.partition('@')
3825 if not name or not value:
3826 parser.error('Wrong dep format: %s should be of the form dep@rev.' %
3827 revision)
3828 if ':' in name:
3829 name, _, package = name.partition(':')
3830 if not name or not package:
3831 parser.error(
3832 'Wrong CIPD format: %s:%s should be of the form path:pkg@version.'
3833 % (name, package))
3834 gclient_eval.SetCIPD(local_scope, name, package, value)
3835 else:
3836 # Update DEPS only when `git_dependencies` == DEPS or SYNC.
3837 # git_dependencies is defaulted to DEPS when not set.
3838 if 'git_dependencies' not in local_scope or local_scope[
3839 'git_dependencies'] in (gclient_eval.DEPS,
3840 gclient_eval.SYNC):
3841 gclient_eval.SetRevision(local_scope, name, value)
3842
3843 # Update git submodules when `git_dependencies` == SYNC or
3844 # SUBMODULES.
3845 if git_modules and 'git_dependencies' in local_scope and local_scope[
3846 'git_dependencies'] in (gclient_eval.SUBMODULES,
3847 gclient_eval.SYNC):
3848 git_module_name = name
3849 if not 'use_relative_paths' in local_scope or \
3850 local_scope['use_relative_paths'] != True:
3851 deps_dir = os.path.dirname(
3852 os.path.abspath(options.deps_file))
3853 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
3854 delta_path = None
3855 if gclient_path:
3856 delta_path = os.path.relpath(
3857 deps_dir, os.path.abspath(gclient_path))
3858 if delta_path:
3859 prefix_length = len(delta_path.replace(
3860 os.path.sep, '/')) + 1
3861 git_module_name = name[prefix_length:]
3862 # gclient setdep should update the revision, i.e., the gitlink
3863 # only when the submodule entry is already present within
3864 # .gitmodules.
3865 if git_module_name not in git_modules:
3866 raise KeyError(
3867 f'Could not find any dependency called "{git_module_name}" in '
3868 f'.gitmodules.')
3869
3870 # Update the gitlink for the submodule.
3871 subprocess2.call([
3872 'git', 'update-index', '--add', '--cacheinfo',
3873 f'160000,{value},{git_module_name}'
3874 ],
3875 cwd=cwd)
3876
3877 with open(options.deps_file, 'wb') as f:
3878 f.write(gclient_eval.RenderDEPSFile(local_scope).encode('utf-8'))
3879
3880 if git_modules:
3881 subprocess2.call(['git', 'add', options.deps_file], cwd=cwd)
3882 print('Changes have been staged. See changes with `git status`.\n'
3883 'Use `git commit -m "Manual roll"` to commit your changes. \n'
3884 'Run gclient sync to update your local dependency checkout.')
Josip Sokcevic5561f8b2023-08-21 16:00:42 +00003885
Edward Lesmes6f64a052018-03-20 17:35:49 -04003886
Edward Lemur3298e7b2018-07-17 18:21:27 +00003887@metrics.collector.collect_metrics('gclient verify')
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003888def CMDverify(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003889 """Verifies the DEPS file deps are only from allowed_hosts."""
3890 (options, args) = parser.parse_args(args)
3891 client = GClient.LoadCurrentConfig(options)
3892 if not client:
3893 raise gclient_utils.Error(
3894 'client not configured; see \'gclient config\'')
3895 client.RunOnDeps(None, [])
3896 # Look at each first-level dependency of this gclient only.
3897 for dep in client.dependencies:
3898 bad_deps = dep.findDepsFromNotAllowedHosts()
3899 if not bad_deps:
3900 continue
3901 print("There are deps from not allowed hosts in file %s" %
3902 dep.deps_file)
3903 for bad_dep in bad_deps:
3904 print("\t%s at %s" % (bad_dep.name, bad_dep.url))
3905 print("allowed_hosts:", ', '.join(dep.allowed_hosts))
3906 sys.stdout.flush()
3907 raise gclient_utils.Error(
3908 'dependencies from disallowed hosts; check your DEPS file.')
3909 return 0
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003910
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003911
3912@subcommand.epilog("""For more information on what metrics are we collecting and
Edward Lemur8a2e3312018-07-12 21:15:09 +00003913why, please read metrics.README.md or visit https://bit.ly/2ufRS4p""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003914@metrics.collector.collect_metrics('gclient metrics')
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003915def CMDmetrics(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003916 """Reports, and optionally modifies, the status of metric collection."""
3917 parser.add_option('--opt-in',
3918 action='store_true',
3919 dest='enable_metrics',
3920 help='Opt-in to metrics collection.',
3921 default=None)
3922 parser.add_option('--opt-out',
3923 action='store_false',
3924 dest='enable_metrics',
3925 help='Opt-out of metrics collection.')
3926 options, args = parser.parse_args(args)
3927 if args:
3928 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3929 if not metrics.collector.config.is_googler:
3930 print("You're not a Googler. Metrics collection is disabled for you.")
3931 return 0
3932
3933 if options.enable_metrics is not None:
3934 metrics.collector.config.opted_in = options.enable_metrics
3935
3936 if metrics.collector.config.opted_in is None:
3937 print("You haven't opted in or out of metrics collection.")
3938 elif metrics.collector.config.opted_in:
3939 print("You have opted in. Thanks!")
3940 else:
3941 print("You have opted out. Please consider opting in.")
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003942 return 0
3943
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003944
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003945class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003946 gclientfile_default = os.environ.get('GCLIENT_FILE', '.gclient')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003947
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003948 def __init__(self, **kwargs):
3949 optparse.OptionParser.__init__(self,
3950 version='%prog ' + __version__,
3951 **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003952
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003953 # Some arm boards have issues with parallel sync.
3954 if platform.machine().startswith('arm'):
3955 jobs = 1
3956 else:
3957 jobs = max(8, gclient_utils.NumLocalCpus())
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003958
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003959 self.add_option(
3960 '-j',
3961 '--jobs',
3962 default=jobs,
3963 type='int',
3964 help='Specify how many SCM commands can run in parallel; defaults to '
3965 '%default on this machine')
3966 self.add_option(
3967 '-v',
3968 '--verbose',
3969 action='count',
3970 default=0,
3971 help='Produces additional output for diagnostics. Can be used up to '
3972 'three times for more logging info.')
3973 self.add_option('--gclientfile',
3974 dest='config_filename',
3975 help='Specify an alternate %s file' %
3976 self.gclientfile_default)
3977 self.add_option(
3978 '--spec',
3979 help='create a gclient file containing the provided string. Due to '
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003980 'Cygwin/Python brokenness, it can\'t contain any newlines.')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003981 self.add_option('--no-nag-max',
3982 default=False,
3983 action='store_true',
3984 help='Ignored for backwards compatibility.')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003985
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003986 def parse_args(self, args=None, _values=None):
3987 """Integrates standard options processing."""
3988 # Create an optparse.Values object that will store only the actual
3989 # passed options, without the defaults.
3990 actual_options = optparse.Values()
3991 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
3992 # Create an optparse.Values object with the default options.
3993 options = optparse.Values(self.get_default_values().__dict__)
3994 # Update it with the options passed by the user.
3995 options._update_careful(actual_options.__dict__)
3996 # Store the options passed by the user in an _actual_options attribute.
3997 # We store only the keys, and not the values, since the values can
3998 # contain arbitrary information, which might be PII.
3999 metrics.collector.add('arguments', list(actual_options.__dict__))
Edward Lemur3298e7b2018-07-17 18:21:27 +00004000
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004001 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
4002 logging.basicConfig(
4003 level=levels[min(options.verbose,
4004 len(levels) - 1)],
4005 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
4006 if options.config_filename and options.spec:
4007 self.error('Cannot specify both --gclientfile and --spec')
4008 if (options.config_filename and options.config_filename !=
4009 os.path.basename(options.config_filename)):
4010 self.error('--gclientfile target must be a filename, not a path')
4011 if not options.config_filename:
4012 options.config_filename = self.gclientfile_default
4013 options.entries_filename = options.config_filename + '_entries'
4014 if options.jobs < 1:
4015 self.error('--jobs must be 1 or higher')
maruel@chromium.org0895b752011-08-26 20:40:33 +00004016
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004017 # These hacks need to die.
4018 if not hasattr(options, 'revisions'):
4019 # GClient.RunOnDeps expects it even if not applicable.
4020 options.revisions = []
4021 if not hasattr(options, 'experiments'):
4022 options.experiments = []
4023 if not hasattr(options, 'head'):
4024 options.head = None
4025 if not hasattr(options, 'nohooks'):
4026 options.nohooks = True
4027 if not hasattr(options, 'noprehooks'):
4028 options.noprehooks = True
4029 if not hasattr(options, 'deps_os'):
4030 options.deps_os = None
4031 if not hasattr(options, 'force'):
4032 options.force = None
4033 return (options, args)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004034
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004035
4036def disable_buffering():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004037 # Make stdout auto-flush so buildbot doesn't kill us during lengthy
4038 # operations. Python as a strong tendency to buffer sys.stdout.
4039 sys.stdout = gclient_utils.MakeFileAutoFlush(sys.stdout)
4040 # Make stdout annotated with the thread ids.
4041 sys.stdout = gclient_utils.MakeFileAnnotated(sys.stdout)
maruel@chromium.org0895b752011-08-26 20:40:33 +00004042
4043
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004044def path_contains_tilde():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004045 for element in os.environ['PATH'].split(os.pathsep):
4046 if element.startswith('~') and os.path.abspath(
4047 os.path.realpath(
4048 os.path.expanduser(element))) == DEPOT_TOOLS_DIR:
4049 return True
4050 return False
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004051
4052
4053def can_run_gclient_and_helpers():
Gavin Mak7f5b53f2023-09-07 18:13:01 +00004054 if sys.version_info[0] < 3:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004055 print('\nYour python version %s is unsupported, please upgrade.\n' %
4056 sys.version.split(' ', 1)[0],
4057 file=sys.stderr)
4058 return False
4059 if not sys.executable:
4060 print('\nPython cannot find the location of it\'s own executable.\n',
4061 file=sys.stderr)
4062 return False
4063 if path_contains_tilde():
4064 print(
4065 '\nYour PATH contains a literal "~", which works in some shells ' +
4066 'but will break when python tries to run subprocesses. ' +
4067 'Replace the "~" with $HOME.\n' + 'See https://crbug.com/952865.\n',
4068 file=sys.stderr)
4069 return False
4070 return True
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004071
4072
4073def main(argv):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004074 """Doesn't parse the arguments here, just find the right subcommand to
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004075 execute."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004076 if not can_run_gclient_and_helpers():
4077 return 2
4078 fix_encoding.fix_encoding()
4079 disable_buffering()
4080 setup_color.init()
4081 dispatcher = subcommand.CommandDispatcher(__name__)
4082 try:
4083 return dispatcher.execute(OptionParser(), argv)
4084 except KeyboardInterrupt:
4085 gclient_utils.GClientChildren.KillAllRemainingChildren()
4086 raise
4087 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
4088 print('Error: %s' % str(e), file=sys.stderr)
4089 return 1
4090 finally:
4091 gclient_utils.PrintWarnings()
4092 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004093
4094
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00004095if '__main__' == __name__:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004096 with metrics.collector.print_notice_and_exit():
4097 sys.exit(main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004098
4099# vim: ts=2:sw=2:tw=80:et: