blob: c305c2480ecf7e0249d6cf4a4e6fd986f2e6949a [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
Tom Andersonc31ae0b2018-02-06 14:48:56 -080097import detect_host_arch
maruel@chromium.org35625c72011-03-23 17:34:02 +000098import fix_encoding
Aravind Vasudevanb8164182023-08-25 21:49:12 +000099import git_common
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200100import gclient_eval
Nico Weber09e0b382019-03-11 16:54:07 +0000101import gclient_paths
Gavin Mak65c49b12023-08-24 18:06:42 +0000102import gclient_scm
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000103import gclient_utils
szager@chromium.org848fd492014-04-09 19:06:44 +0000104import git_cache
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000105import metrics
Edward Lemur40764b02018-07-20 18:50:29 +0000106import metrics_utils
Joanna Wange36c6bb2023-08-30 22:09:59 +0000107import scm as scm_git
Gavin Mak65c49b12023-08-24 18:06:42 +0000108import setup_color
maruel@chromium.org39c0b222013-08-17 16:57:01 +0000109import subcommand
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000110import subprocess2
Gavin Mak65c49b12023-08-24 18:06:42 +0000111from third_party.repo.progress import Progress
Aaron Gableac9b0f32019-04-18 17:38:37 +0000112
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000113# TODO: Should fix these warnings.
114# pylint: disable=line-too-long
Aaron Gableac9b0f32019-04-18 17:38:37 +0000115
Henrique Ferreiro4ef32212019-04-29 23:32:31 +0000116DEPOT_TOOLS_DIR = os.path.dirname(os.path.abspath(os.path.realpath(__file__)))
117
Robert Iannuccia19649b2018-06-29 16:31:45 +0000118# Singleton object to represent an unset cache_dir (as opposed to a disabled
119# one, e.g. if a spec explicitly says `cache_dir = None`.)
120UNSET_CACHE_DIR = object()
121
Joanna Wang01870792022-08-01 19:02:57 +0000122PREVIOUS_CUSTOM_VARS_FILE = '.gclient_previous_custom_vars'
123PREVIOUS_SYNC_COMMITS_FILE = '.gclient_previous_sync_commits'
Robert Iannuccia19649b2018-06-29 16:31:45 +0000124
Joanna Wangf3edc502022-07-20 00:12:10 +0000125PREVIOUS_SYNC_COMMITS = 'GCLIENT_PREVIOUS_SYNC_COMMITS'
Joanna Wang66286612022-06-30 19:59:13 +0000126
Joanna Wanga84a16b2022-07-27 18:52:17 +0000127NO_SYNC_EXPERIMENT = 'no-sync'
128
Joanna Wang66286612022-06-30 19:59:13 +0000129
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200130class GNException(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000131 pass
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200132
133
Aravind Vasudevaned935cf2023-08-24 23:52:20 +0000134def ToGNString(value):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000135 """Returns a stringified GN equivalent of the Python value."""
136 if isinstance(value, str):
137 if value.find('\n') >= 0:
138 raise GNException("Trying to print a string with a newline in it.")
139 return '"' + \
140 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
141 '"'
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200142
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000143 if isinstance(value, bool):
144 if value:
145 return "true"
146 return "false"
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200147
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000148 # NOTE: some type handling removed compared to chromium/src copy.
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200149
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000150 raise GNException("Unsupported type when printing to GN.")
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200151
152
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200153class Hook(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000154 """Descriptor of command ran before/after sync or on demand."""
155 def __init__(self,
156 action,
157 pattern=None,
158 name=None,
159 cwd=None,
160 condition=None,
161 variables=None,
162 verbose=False,
163 cwd_base=None):
164 """Constructor.
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200165
166 Arguments:
Gavin Mak65c49b12023-08-24 18:06:42 +0000167 action (list of str): argv of the command to run
168 pattern (str regex): noop with git; deprecated
169 name (str): optional name; no effect on operation
170 cwd (str): working directory to use
171 condition (str): condition when to run the hook
Paweł Hajdan, Jr032d5452017-06-22 20:43:53 +0200172 variables (dict): variables for evaluating the condition
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200173 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000174 self._action = gclient_utils.freeze(action)
175 self._pattern = pattern
176 self._name = name
177 self._cwd = cwd
178 self._condition = condition
179 self._variables = variables
180 self._verbose = verbose
181 self._cwd_base = cwd_base
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200182
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000183 @staticmethod
184 def from_dict(d,
185 variables=None,
186 verbose=False,
187 conditions=None,
188 cwd_base=None):
189 """Creates a Hook instance from a dict like in the DEPS file."""
190 # Merge any local and inherited conditions.
191 gclient_eval.UpdateCondition(d, 'and', conditions)
192 return Hook(
193 d['action'],
194 d.get('pattern'),
195 d.get('name'),
196 d.get('cwd'),
197 d.get('condition'),
198 variables=variables,
199 # Always print the header if not printing to a TTY.
200 verbose=verbose or not setup_color.IS_TTY,
201 cwd_base=cwd_base)
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200202
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000203 @property
204 def action(self):
205 return self._action
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200206
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000207 @property
208 def pattern(self):
209 return self._pattern
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200210
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000211 @property
212 def name(self):
213 return self._name
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200214
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000215 @property
216 def condition(self):
217 return self._condition
Paweł Hajdan, Jrecf53fe2017-09-29 18:28:49 +0200218
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000219 @property
220 def effective_cwd(self):
221 cwd = self._cwd_base
222 if self._cwd:
223 cwd = os.path.join(cwd, self._cwd)
224 return cwd
Corentin Walleza68660d2018-09-10 17:33:24 +0000225
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000226 def matches(self, file_list):
227 """Returns true if the pattern matches any of files in the list."""
228 if not self._pattern:
229 return True
230 pattern = re.compile(self._pattern)
231 return bool([f for f in file_list if pattern.search(f)])
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200232
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000233 def run(self):
234 """Executes the hook's command (provided the condition is met)."""
235 if (self._condition and not gclient_eval.EvaluateCondition(
236 self._condition, self._variables)):
237 return
Paweł Hajdan, Jr032d5452017-06-22 20:43:53 +0200238
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000239 cmd = list(self._action)
Paweł Hajdan, Jrfc6196b2017-07-27 13:15:25 +0200240
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000241 if cmd[0] == 'vpython3' and _detect_host_os() == 'win':
242 cmd[0] += '.bat'
Paweł Hajdan, Jrc9364392017-06-14 17:11:56 +0200243
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000244 exit_code = 2
245 try:
246 start_time = time.time()
247 gclient_utils.CheckCallAndFilter(cmd,
248 cwd=self.effective_cwd,
249 print_stdout=True,
250 show_header=True,
251 always_show_header=self._verbose)
252 exit_code = 0
253 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
254 # Use a discrete exit status code of 2 to indicate that a hook
255 # action failed. Users of this script may wish to treat hook action
256 # failures differently from VC failures.
257 print('Error: %s' % str(e), file=sys.stderr)
258 sys.exit(exit_code)
259 finally:
260 elapsed_time = time.time() - start_time
261 metrics.collector.add_repeated(
262 'hooks', {
263 'action':
264 gclient_utils.CommandToStr(cmd),
265 'name':
266 self._name,
267 'cwd':
268 os.path.relpath(os.path.normpath(self.effective_cwd),
269 self._cwd_base),
270 'condition':
271 self._condition,
272 'execution_time':
273 elapsed_time,
274 'exit_code':
275 exit_code,
276 })
277 if elapsed_time > 10:
278 print("Hook '%s' took %.2f secs" %
279 (gclient_utils.CommandToStr(cmd), elapsed_time))
Paweł Hajdan, Jrc10a4d82017-06-14 14:06:50 +0200280
281
Paweł Hajdan, Jrfc6196b2017-07-27 13:15:25 +0200282class DependencySettings(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000283 """Immutable configuration settings."""
284 def __init__(self, parent, url, managed, custom_deps, custom_vars,
285 custom_hooks, deps_file, should_process, relative, condition):
286 # These are not mutable:
287 self._parent = parent
288 self._deps_file = deps_file
289 self._url = url
290 # The condition as string (or None). Useful to keep e.g. for flatten.
291 self._condition = condition
292 # 'managed' determines whether or not this dependency is synced/updated
293 # by gclient after gclient checks it out initially. The difference
294 # between 'managed' and 'should_process' is that the user specifies
295 # 'managed' via the --unmanaged command-line flag or a .gclient config,
296 # where 'should_process' is dynamically set by gclient if it goes over
297 # its recursion limit and controls gclient's behavior so it does not
298 # misbehave.
299 self._managed = managed
300 self._should_process = should_process
301 # If this is a recursed-upon sub-dependency, and the parent has
302 # use_relative_paths set, then this dependency should check out its own
303 # dependencies relative to that parent's path for this, rather than
304 # relative to the .gclient file.
305 self._relative = relative
306 # This is a mutable value which has the list of 'target_os' OSes listed
307 # in the current deps file.
308 self.local_target_os = None
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000309
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000310 # These are only set in .gclient and not in DEPS files.
311 self._custom_vars = custom_vars or {}
312 self._custom_deps = custom_deps or {}
313 self._custom_hooks = custom_hooks or []
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000314
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000315 # Post process the url to remove trailing slashes.
316 if isinstance(self.url, str):
317 # urls are sometime incorrectly written as proto://host/path/@rev.
318 # Replace it to proto://host/path@rev.
319 self.set_url(self.url.replace('/@', '@'))
320 elif not isinstance(self.url, (None.__class__)):
321 raise gclient_utils.Error(
322 ('dependency url must be either string or None, '
323 'instead of %s') % self.url.__class__.__name__)
Edward Lemure7273d22018-05-10 19:13:51 -0400324
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000325 # Make any deps_file path platform-appropriate.
326 if self._deps_file:
327 for sep in ['/', '\\']:
328 self._deps_file = self._deps_file.replace(sep, os.sep)
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000329
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000330 @property
331 def deps_file(self):
332 return self._deps_file
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000333
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000334 @property
335 def managed(self):
336 return self._managed
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000337
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000338 @property
339 def parent(self):
340 return self._parent
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000341
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000342 @property
343 def root(self):
344 """Returns the root node, a GClient object."""
345 if not self.parent:
346 # This line is to signal pylint that it could be a GClient instance.
347 return self or GClient(None, None)
348 return self.parent.root
maruel@chromium.org8c0d9582011-10-03 21:36:01 +0000349
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000350 @property
351 def should_process(self):
352 """True if this dependency should be processed, i.e. checked out."""
353 return self._should_process
Michael Mossd683d7c2018-06-15 05:05:17 +0000354
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000355 @property
356 def custom_vars(self):
357 return self._custom_vars.copy()
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000358
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000359 @property
360 def custom_deps(self):
361 return self._custom_deps.copy()
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000362
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000363 @property
364 def custom_hooks(self):
365 return self._custom_hooks[:]
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000366
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000367 @property
368 def url(self):
369 """URL after variable expansion."""
370 return self._url
maruel@chromium.org8ac2b272011-09-26 18:49:49 +0000371
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000372 @property
373 def condition(self):
374 return self._condition
Paweł Hajdan, Jrf69860b2017-06-05 20:24:28 +0200375
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000376 @property
377 def target_os(self):
378 if self.local_target_os is not None:
379 return tuple(set(self.local_target_os).union(self.parent.target_os))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000380
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000381 return self.parent.target_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +0000382
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000383 @property
384 def target_cpu(self):
385 return self.parent.target_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -0800386
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000387 def set_url(self, url):
388 self._url = url
Edward Lemure7273d22018-05-10 19:13:51 -0400389
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000390 def get_custom_deps(self, name, url):
391 """Returns a custom deps if applicable."""
392 if self.parent:
393 url = self.parent.get_custom_deps(name, url)
394 # None is a valid return value to disable a dependency.
395 return self.custom_deps.get(name, url)
maruel@chromium.org8c0d9582011-10-03 21:36:01 +0000396
maruel@chromium.org064186c2011-09-27 23:53:33 +0000397
398class Dependency(gclient_utils.WorkItem, DependencySettings):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000399 """Object that represents a dependency checkout."""
400 def __init__(self,
401 parent,
402 name,
403 url,
404 managed,
405 custom_deps,
406 custom_vars,
407 custom_hooks,
408 deps_file,
409 should_process,
410 should_recurse,
411 relative,
412 condition,
413 protocol='https',
414 git_dependencies_state=gclient_eval.DEPS,
415 print_outbuf=False):
416 gclient_utils.WorkItem.__init__(self, name)
417 DependencySettings.__init__(self, parent, url, managed, custom_deps,
418 custom_vars, custom_hooks, deps_file,
419 should_process, relative, condition)
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000420
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000421 # This is in both .gclient and DEPS files:
422 self._deps_hooks = []
maruel@chromium.org68988972011-09-20 14:11:42 +0000423
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000424 self._pre_deps_hooks = []
maruel@chromium.org68988972011-09-20 14:11:42 +0000425
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000426 # Calculates properties:
427 self._dependencies = []
428 self._vars = {}
borenet@google.com2d1ee9e2013-10-15 08:13:16 +0000429
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000430 # A cache of the files affected by the current operation, necessary for
431 # hooks.
432 self._file_list = []
433 # List of host names from which dependencies are allowed.
434 # Default is an empty set, meaning unspecified in DEPS file, and hence
435 # all hosts will be allowed. Non-empty set means allowlist of hosts.
436 # allowed_hosts var is scoped to its DEPS file, and so it isn't
437 # recursive.
438 self._allowed_hosts = frozenset()
439 self._gn_args_from = None
440 # Spec for .gni output to write (if any).
441 self._gn_args_file = None
442 self._gn_args = []
443 # If it is not set to True, the dependency wasn't processed for its
444 # child dependency, i.e. its DEPS wasn't read.
445 self._deps_parsed = False
446 # This dependency has been processed, i.e. checked out
447 self._processed = False
448 # This dependency had its pre-DEPS hooks run
449 self._pre_deps_hooks_ran = False
450 # This dependency had its hook run
451 self._hooks_ran = False
452 # This is the scm used to checkout self.url. It may be used by
453 # dependencies to get the datetime of the revision we checked out.
454 self._used_scm = None
455 self._used_revision = None
456 # The actual revision we ended up getting, or None if that information
457 # is unavailable
458 self._got_revision = None
459 # Whether this dependency should use relative paths.
460 self._use_relative_paths = False
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200461
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000462 # recursedeps is a mutable value that selectively overrides the default
463 # 'no recursion' setting on a dep-by-dep basis.
464 #
465 # It will be a dictionary of {deps_name: depfile_namee}
466 self.recursedeps = {}
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000467
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000468 # Whether we should process this dependency's DEPS file.
469 self._should_recurse = should_recurse
Edward Lemurfbb06aa2018-06-11 20:43:06 +0000470
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000471 # Whether we should sync git/cipd dependencies and hooks from the
472 # DEPS file.
473 # This is set based on skip_sync_revisions and must be done
474 # after the patch refs are applied.
475 # If this is False, we will still run custom_hooks and process
476 # custom_deps, if any.
477 self._should_sync = True
Edward Lemure7273d22018-05-10 19:13:51 -0400478
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000479 self._OverrideUrl()
480 # This is inherited from WorkItem. We want the URL to be a resource.
481 if self.url and isinstance(self.url, str):
482 # The url is usually given to gclient either as https://blah@123
483 # or just https://blah. The @123 portion is irrelevant.
484 self.resources.append(self.url.split('@')[0])
Joanna Wang18af7ef2022-07-01 16:51:00 +0000485
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000486 # Controls whether we want to print git's output when we first clone the
487 # dependency
488 self.print_outbuf = print_outbuf
cmp@chromium.orge84ac912014-06-30 23:14:35 +0000489
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000490 self.protocol = protocol
491 self.git_dependencies_state = git_dependencies_state
Edward Lemur231f5ea2018-01-31 19:02:36 +0100492
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000493 if not self.name and self.parent:
494 raise gclient_utils.Error('Dependency without name')
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +0000495
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000496 def _OverrideUrl(self):
497 """Resolves the parsed url from the parent hierarchy."""
498 parsed_url = self.get_custom_deps(
499 self._name.replace(os.sep, posixpath.sep) \
500 if self._name else self._name, self.url)
501 if parsed_url != self.url:
502 logging.info('Dependency(%s)._OverrideUrl(%s) -> %s', self._name,
503 self.url, parsed_url)
504 self.set_url(parsed_url)
505 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000506
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000507 if self.url is None:
508 logging.info('Dependency(%s)._OverrideUrl(None) -> None',
509 self._name)
510 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000511
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000512 if not isinstance(self.url, str):
513 raise gclient_utils.Error('Unknown url type')
Michael Mossd683d7c2018-06-15 05:05:17 +0000514
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000515 # self.url is a local path
516 path, at, rev = self.url.partition('@')
517 if os.path.isdir(path):
518 return
Michael Mossd683d7c2018-06-15 05:05:17 +0000519
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000520 # self.url is a URL
521 parsed_url = urllib.parse.urlparse(self.url)
522 if parsed_url[0] or re.match(r'^\w+\@[\w\.-]+\:[\w\/]+', parsed_url[2]):
523 return
Edward Lemur1f392b82019-11-15 22:40:11 +0000524
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000525 # self.url is relative to the parent's URL.
526 if not path.startswith('/'):
527 raise gclient_utils.Error(
528 'relative DEPS entry \'%s\' must begin with a slash' % self.url)
Edward Lemur1f392b82019-11-15 22:40:11 +0000529
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000530 parent_url = self.parent.url
531 parent_path = self.parent.url.split('@')[0]
532 if os.path.isdir(parent_path):
533 # Parent's URL is a local path. Get parent's URL dirname and append
534 # self.url.
535 parent_path = os.path.dirname(parent_path)
536 parsed_url = parent_path + path.replace('/', os.sep) + at + rev
537 else:
538 # Parent's URL is a URL. Get parent's URL, strip from the last '/'
539 # (equivalent to unix dirname) and append self.url.
540 parsed_url = parent_url[:parent_url.rfind('/')] + self.url
Edward Lemur1f392b82019-11-15 22:40:11 +0000541
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000542 logging.info('Dependency(%s)._OverrideUrl(%s) -> %s', self.name,
543 self.url, parsed_url)
544 self.set_url(parsed_url)
Edward Lemur1f392b82019-11-15 22:40:11 +0000545
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000546 def PinToActualRevision(self):
547 """Updates self.url to the revision checked out on disk."""
548 if self.url is None:
549 return
550 url = None
551 scm = self.CreateSCM()
552 if scm.name == 'cipd':
553 revision = scm.revinfo(None, None, None)
554 package = self.GetExpandedPackageName()
555 url = '%s/p/%s/+/%s' % (scm.GetActualRemoteURL(None), package,
556 revision)
Edward Lemur1f392b82019-11-15 22:40:11 +0000557
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000558 if os.path.isdir(scm.checkout_path):
559 revision = scm.revinfo(None, None, None)
560 url = '%s@%s' % (gclient_utils.SplitUrlRevision(
561 self.url)[0], revision)
562 self.set_url(url)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +0000563
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000564 def ToLines(self):
565 # () -> Sequence[str]
566 """Returns strings representing the deps (info, graphviz line)"""
567 s = []
568 condition_part = ([' "condition": %r,' %
569 self.condition] if self.condition else [])
570 s.extend([
571 ' # %s' % self.hierarchy(include_url=False),
572 ' "%s": {' % (self.name, ),
573 ' "url": "%s",' % (self.url, ),
574 ] + condition_part + [
575 ' },',
576 '',
577 ])
578 return s
Edward Lemure7273d22018-05-10 19:13:51 -0400579
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000580 @property
581 def requirements(self):
582 """Calculate the list of requirements."""
583 requirements = set()
584 # self.parent is implicitly a requirement. This will be recursive by
585 # definition.
586 if self.parent and self.parent.name:
587 requirements.add(self.parent.name)
John Budorick0f7b2002018-01-19 15:46:17 -0800588
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000589 # For a tree with at least 2 levels*, the leaf node needs to depend
590 # on the level higher up in an orderly way.
591 # This becomes messy for >2 depth as the DEPS file format is a
592 # dictionary, thus unsorted, while the .gclient format is a list thus
593 # sorted.
594 #
595 # Interestingly enough, the following condition only works in the case
596 # we want: self is a 2nd level node. 3rd level node wouldn't need this
597 # since they already have their parent as a requirement.
598 if self.parent and self.parent.parent and not self.parent.parent.parent:
599 requirements |= set(i.name for i in self.root.dependencies
600 if i.name)
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000601
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000602 if self.name:
603 requirements |= set(
604 obj.name for obj in self.root.subtree(False)
605 if (obj is not self and obj.name
606 and self.name.startswith(posixpath.join(obj.name, ''))))
607 requirements = tuple(sorted(requirements))
608 logging.info('Dependency(%s).requirements = %s' %
609 (self.name, requirements))
610 return requirements
maruel@chromium.org118fb1c2011-09-01 20:04:24 +0000611
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000612 @property
613 def should_recurse(self):
614 return self._should_recurse
maruel@chromium.org470b5432011-10-11 18:18:19 +0000615
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000616 def verify_validity(self):
617 """Verifies that this Dependency is fine to add as a child of another one.
maruel@chromium.org470b5432011-10-11 18:18:19 +0000618
619 Returns True if this entry should be added, False if it is a duplicate of
620 another entry.
621 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000622 logging.info('Dependency(%s).verify_validity()' % self.name)
623 if self.name in [s.name for s in self.parent.dependencies]:
624 raise gclient_utils.Error(
625 'The same name "%s" appears multiple times in the deps section'
626 % self.name)
627 if not self.should_process:
628 # Return early, no need to set requirements.
629 return not any(d.name == self.name for d in self.root.subtree(True))
maruel@chromium.org470b5432011-10-11 18:18:19 +0000630
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000631 # This require a full tree traversal with locks.
632 siblings = [d for d in self.root.subtree(False) if d.name == self.name]
633 for sibling in siblings:
634 # Allow to have only one to be None or ''.
635 if self.url != sibling.url and bool(self.url) == bool(sibling.url):
636 raise gclient_utils.Error(
637 ('Dependency %s specified more than once:\n'
638 ' %s [%s]\n'
639 'vs\n'
640 ' %s [%s]') % (self.name, sibling.hierarchy(),
641 sibling.url, self.hierarchy(), self.url))
642 # In theory we could keep it as a shadow of the other one. In
643 # practice, simply ignore it.
644 logging.warning("Won't process duplicate dependency %s" % sibling)
645 return False
646 return True
maruel@chromium.org064186c2011-09-27 23:53:33 +0000647
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000648 def _postprocess_deps(self, deps, rel_prefix):
649 # type: (Mapping[str, Mapping[str, str]], str) ->
650 # Mapping[str, Mapping[str, str]]
651 """Performs post-processing of deps compared to what's in the DEPS file."""
652 # If we don't need to sync, only process custom_deps, if any.
653 if not self._should_sync:
654 if not self.custom_deps:
655 return {}
Paweł Hajdan, Jr4426eaf2017-06-13 12:25:47 +0200656
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000657 processed_deps = {}
658 for dep_name, dep_info in self.custom_deps.items():
659 if dep_info and not dep_info.endswith('@unmanaged'):
660 if dep_name in deps:
661 # custom_deps that should override an existing deps gets
662 # applied in the Dependency itself with _OverrideUrl().
663 processed_deps[dep_name] = deps[dep_name]
664 else:
665 processed_deps[dep_name] = {
666 'url': dep_info,
667 'dep_type': 'git'
668 }
669 else:
670 processed_deps = dict(deps)
Joanna Wang18af7ef2022-07-01 16:51:00 +0000671
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000672 # If a line is in custom_deps, but not in the solution, we want to
673 # append this line to the solution.
674 for dep_name, dep_info in self.custom_deps.items():
675 # Don't add it to the solution for the values of "None" and
676 # "unmanaged" in order to force these kinds of custom_deps to
677 # act as revision overrides (via revision_overrides). Having
678 # them function as revision overrides allows them to be applied
679 # to recursive dependencies. https://crbug.com/1031185
680 if (dep_name not in processed_deps and dep_info
681 and not dep_info.endswith('@unmanaged')):
682 processed_deps[dep_name] = {
683 'url': dep_info,
684 'dep_type': 'git'
685 }
Edward Lemur16f4bad2018-05-16 16:53:49 -0400686
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000687 # Make child deps conditional on any parent conditions. This ensures
688 # that, when flattened, recursed entries have the correct restrictions,
689 # even if not explicitly set in the recursed DEPS file. For instance, if
690 # "src/ios_foo" is conditional on "checkout_ios=True", then anything
691 # recursively included by "src/ios_foo/DEPS" should also require
692 # "checkout_ios=True".
693 if self.condition:
694 for value in processed_deps.values():
695 gclient_eval.UpdateCondition(value, 'and', self.condition)
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200696
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000697 if not rel_prefix:
698 return processed_deps
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200699
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000700 logging.warning('use_relative_paths enabled.')
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200701 rel_deps = {}
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000702 for d, url in processed_deps.items():
703 # normpath is required to allow DEPS to use .. in their
704 # dependency local path.
705 # We are following the same pattern when use_relative_paths = False,
706 # which uses slashes.
707 rel_deps[os.path.normpath(os.path.join(rel_prefix, d)).replace(
708 os.path.sep, '/')] = url
709 logging.warning('Updating deps by prepending %s.', rel_prefix)
710 return rel_deps
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200711
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000712 def _deps_to_objects(self, deps, use_relative_paths):
713 # type: (Mapping[str, Mapping[str, str]], bool) -> Sequence[Dependency]
714 """Convert a deps dict to a list of Dependency objects."""
715 deps_to_add = []
716 cached_conditions = {}
717 for name, dep_value in deps.items():
718 should_process = self.should_process
719 if dep_value is None:
720 continue
Paweł Hajdan, Jrcd788e32017-06-12 18:42:22 +0200721
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000722 condition = dep_value.get('condition')
723 dep_type = dep_value.get('dep_type')
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000724
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000725 if condition and not self._get_option('process_all_deps', False):
726 if condition not in cached_conditions:
727 cached_conditions[
728 condition] = gclient_eval.EvaluateCondition(
729 condition, self.get_vars())
730 should_process = should_process and cached_conditions[condition]
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000731
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000732 # The following option is only set by the 'revinfo' command.
733 if self._get_option('ignore_dep_type', None) == dep_type:
734 continue
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000735
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000736 if dep_type == 'cipd':
737 cipd_root = self.GetCipdRoot()
738 for package in dep_value.get('packages', []):
739 deps_to_add.append(
740 CipdDependency(parent=self,
741 name=name,
742 dep_value=package,
743 cipd_root=cipd_root,
744 custom_vars=self.custom_vars,
745 should_process=should_process,
746 relative=use_relative_paths,
747 condition=condition))
748 else:
749 url = dep_value.get('url')
750 deps_to_add.append(
751 GitDependency(
752 parent=self,
753 name=name,
754 # Update URL with scheme in protocol_override
755 url=GitDependency.updateProtocol(url, self.protocol),
756 managed=True,
757 custom_deps=None,
758 custom_vars=self.custom_vars,
759 custom_hooks=None,
760 deps_file=self.recursedeps.get(name, self.deps_file),
761 should_process=should_process,
762 should_recurse=name in self.recursedeps,
763 relative=use_relative_paths,
764 condition=condition,
765 protocol=self.protocol,
766 git_dependencies_state=self.git_dependencies_state))
Michael Spang0e99b9b2020-08-12 13:34:48 +0000767
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000768 # TODO(crbug.com/1341285): Understand why we need this and remove
769 # it if we don't.
770 deps_to_add.sort(key=lambda x: x.name)
771 return deps_to_add
Corentin Walleza68660d2018-09-10 17:33:24 +0000772
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000773 def ParseDepsFile(self):
774 # type: () -> None
775 """Parses the DEPS file for this dependency."""
776 assert not self.deps_parsed
777 assert not self.dependencies
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000778
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000779 deps_content = None
petermayo@chromium.orge79161a2013-07-09 14:40:37 +0000780
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000781 # First try to locate the configured deps file. If it's missing,
782 # fallback to DEPS.
783 deps_files = [self.deps_file]
784 if 'DEPS' not in deps_files:
785 deps_files.append('DEPS')
786 for deps_file in deps_files:
787 filepath = os.path.join(self.root.root_dir, self.name, deps_file)
788 if os.path.isfile(filepath):
789 logging.info('ParseDepsFile(%s): %s file found at %s',
790 self.name, deps_file, filepath)
791 break
792 logging.info('ParseDepsFile(%s): No %s file found at %s', self.name,
793 deps_file, filepath)
borenet@google.com2d1ee9e2013-10-15 08:13:16 +0000794
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000795 if os.path.isfile(filepath):
796 deps_content = gclient_utils.FileRead(filepath)
797 logging.debug('ParseDepsFile(%s) read:\n%s', self.name,
798 deps_content)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +0000799
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000800 local_scope = {}
801 if deps_content:
802 try:
803 local_scope = gclient_eval.Parse(deps_content, filepath,
804 self.get_vars(),
805 self.get_builtin_vars())
806 except SyntaxError as e:
807 gclient_utils.SyntaxErrorToError(filepath, e)
808
809 if 'git_dependencies' in local_scope:
810 self.git_dependencies_state = local_scope['git_dependencies']
811
812 if 'allowed_hosts' in local_scope:
813 try:
814 self._allowed_hosts = frozenset(
815 local_scope.get('allowed_hosts'))
816 except TypeError: # raised if non-iterable
817 pass
818 if not self._allowed_hosts:
819 logging.warning("allowed_hosts is specified but empty %s",
820 self._allowed_hosts)
821 raise gclient_utils.Error(
822 'ParseDepsFile(%s): allowed_hosts must be absent '
823 'or a non-empty iterable' % self.name)
824
825 self._gn_args_from = local_scope.get('gclient_gn_args_from')
826 self._gn_args_file = local_scope.get('gclient_gn_args_file')
827 self._gn_args = local_scope.get('gclient_gn_args', [])
828 # It doesn't make sense to set all of these, since setting gn_args_from
829 # to another DEPS will make gclient ignore any other local gn_args*
830 # settings.
831 assert not (self._gn_args_from and self._gn_args_file), \
832 'Only specify one of "gclient_gn_args_from" or ' \
833 '"gclient_gn_args_file + gclient_gn_args".'
834
835 self._vars = local_scope.get('vars', {})
836 if self.parent:
837 for key, value in self.parent.get_vars().items():
838 if key in self._vars:
839 self._vars[key] = value
840 # Since we heavily post-process things, freeze ones which should
841 # reflect original state of DEPS.
842 self._vars = gclient_utils.freeze(self._vars)
843
844 # If use_relative_paths is set in the DEPS file, regenerate
845 # the dictionary using paths relative to the directory containing
846 # the DEPS file. Also update recursedeps if use_relative_paths is
847 # enabled.
848 # If the deps file doesn't set use_relative_paths, but the parent did
849 # (and therefore set self.relative on this Dependency object), then we
850 # want to modify the deps and recursedeps by prepending the parent
851 # directory of this dependency.
852 self._use_relative_paths = local_scope.get('use_relative_paths', False)
853 rel_prefix = None
854 if self._use_relative_paths:
855 rel_prefix = self.name
856 elif self._relative:
857 rel_prefix = os.path.dirname(self.name)
858
859 if 'recursion' in local_scope:
860 logging.warning('%s: Ignoring recursion = %d.', self.name,
861 local_scope['recursion'])
862
863 if 'recursedeps' in local_scope:
864 for ent in local_scope['recursedeps']:
865 if isinstance(ent, str):
866 self.recursedeps[ent] = self.deps_file
867 else: # (depname, depsfilename)
868 self.recursedeps[ent[0]] = ent[1]
869 logging.warning('Found recursedeps %r.', repr(self.recursedeps))
870
871 if rel_prefix:
872 logging.warning('Updating recursedeps by prepending %s.',
873 rel_prefix)
874 rel_deps = {}
875 for depname, options in self.recursedeps.items():
876 rel_deps[os.path.normpath(os.path.join(rel_prefix,
877 depname)).replace(
878 os.path.sep,
879 '/')] = options
880 self.recursedeps = rel_deps
881 # To get gn_args from another DEPS, that DEPS must be recursed into.
882 if self._gn_args_from:
883 assert self.recursedeps and self._gn_args_from in self.recursedeps, \
884 'The "gclient_gn_args_from" value must be in recursedeps.'
885
886 # If present, save 'target_os' in the local_target_os property.
887 if 'target_os' in local_scope:
888 self.local_target_os = local_scope['target_os']
889
890 deps = local_scope.get('deps', {})
891
892 # If dependencies are configured within git submodules, add them to
893 # deps. We don't add for SYNC since we expect submodules to be in sync.
894 if self.git_dependencies_state == gclient_eval.SUBMODULES:
895 deps.update(self.ParseGitSubmodules())
896
897 deps_to_add = self._deps_to_objects(
898 self._postprocess_deps(deps, rel_prefix), self._use_relative_paths)
899
900 # compute which working directory should be used for hooks
901 if local_scope.get('use_relative_hooks', False):
902 print('use_relative_hooks is deprecated, please remove it from '
903 '%s DEPS. (it was merged in use_relative_paths)' % self.name,
904 file=sys.stderr)
905
906 hooks_cwd = self.root.root_dir
907 if self._use_relative_paths:
908 hooks_cwd = os.path.join(hooks_cwd, self.name)
909 elif self._relative:
910 hooks_cwd = os.path.join(hooks_cwd, os.path.dirname(self.name))
911 logging.warning('Using hook base working directory: %s.', hooks_cwd)
912
913 # Only add all hooks if we should sync, otherwise just add custom hooks.
914 # override named sets of hooks by the custom hooks
915 hooks_to_run = []
916 if self._should_sync:
917 hook_names_to_suppress = [
918 c.get('name', '') for c in self.custom_hooks
919 ]
920 for hook in local_scope.get('hooks', []):
921 if hook.get('name', '') not in hook_names_to_suppress:
922 hooks_to_run.append(hook)
923
924 # add the replacements and any additions
925 for hook in self.custom_hooks:
926 if 'action' in hook:
927 hooks_to_run.append(hook)
928
929 if self.should_recurse and deps_to_add:
930 self._pre_deps_hooks = [
931 Hook.from_dict(hook,
932 variables=self.get_vars(),
933 verbose=True,
934 conditions=self.condition,
935 cwd_base=hooks_cwd)
936 for hook in local_scope.get('pre_deps_hooks', [])
937 ]
938
939 self.add_dependencies_and_close(deps_to_add,
940 hooks_to_run,
941 hooks_cwd=hooks_cwd)
942 logging.info('ParseDepsFile(%s) done' % self.name)
943
944 def ParseGitSubmodules(self):
945 # type: () -> Mapping[str, str]
946 """
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000947 Parses git submodules and returns a dict of path to DEPS git url entries.
948
949 e.g {<path>: <url>@<commit_hash>}
950 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000951 cwd = os.path.join(self.root.root_dir, self.name)
952 filepath = os.path.join(cwd, '.gitmodules')
953 if not os.path.isfile(filepath):
954 logging.warning('ParseGitSubmodules(): No .gitmodules found at %s',
955 filepath)
956 return {}
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000957
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000958 # Get .gitmodules fields
959 gitmodules_entries = subprocess2.check_output(
960 ['git', 'config', '--file', filepath, '-l']).decode('utf-8')
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000961
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000962 gitmodules = {}
963 for entry in gitmodules_entries.splitlines():
964 key, value = entry.split('=', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000965
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000966 # git config keys consist of section.name.key, e.g.,
967 # submodule.foo.path
968 section, submodule_key = key.split('.', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000969
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000970 # Only parse [submodule "foo"] sections from .gitmodules.
971 if section.strip() != 'submodule':
972 continue
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000973
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000974 # The name of the submodule can contain '.', hence split from the
975 # back.
976 submodule, sub_key = submodule_key.rsplit('.', maxsplit=1)
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000977
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000978 if submodule not in gitmodules:
979 gitmodules[submodule] = {}
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000980
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000981 if sub_key in ('url', 'gclient-condition', 'path'):
982 gitmodules[submodule][sub_key] = value
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +0000983
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000984 paths = [module['path'] for module in gitmodules.values()]
985 commit_hashes = scm_git.GIT.GetSubmoduleCommits(cwd, paths)
Joanna Wang978f43d2023-08-18 00:16:07 +0000986
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000987 # Structure git submodules into a dict of DEPS git url entries.
988 submodules = {}
989 for module in gitmodules.values():
990 if self._use_relative_paths:
991 path = module['path']
992 else:
993 path = f'{self.name}/{module["path"]}'
994 # TODO(crbug.com/1471685): Temporary hack. In case of applied
995 # patches where the changes are staged but not committed, any
996 # gitlinks from the patch are not returned by `git ls-tree`. The
997 # path won't be found in commit_hashes. Use a temporary '0000000'
998 # value that will be replaced with w/e is found in DEPS later.
999 submodules[path] = {
1000 'dep_type':
1001 'git',
1002 'url':
1003 '{}@{}'.format(module['url'],
1004 commit_hashes.get(module['path'], '0000000'))
1005 }
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001006
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001007 if 'gclient-condition' in module:
1008 submodules[path]['condition'] = module['gclient-condition']
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001009
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001010 return submodules
Aravind Vasudevanaf456fd2023-07-07 00:17:33 +00001011
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001012 def _get_option(self, attr, default):
1013 obj = self
1014 while not hasattr(obj, '_options'):
1015 obj = obj.parent
1016 return getattr(obj._options, attr, default)
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02001017
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001018 def add_dependencies_and_close(self, deps_to_add, hooks, hooks_cwd=None):
1019 """Adds the dependencies, hooks and mark the parsing as done."""
1020 if hooks_cwd == None:
1021 hooks_cwd = self.root.root_dir
Corentin Walleza68660d2018-09-10 17:33:24 +00001022
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001023 for dep in deps_to_add:
1024 if dep.verify_validity():
1025 self.add_dependency(dep)
1026 self._mark_as_parsed([
1027 Hook.from_dict(h,
1028 variables=self.get_vars(),
1029 verbose=self.root._options.verbose,
1030 conditions=self.condition,
1031 cwd_base=hooks_cwd) for h in hooks
1032 ])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001034 def findDepsFromNotAllowedHosts(self):
1035 """Returns a list of dependencies from not allowed hosts.
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001036
1037 If allowed_hosts is not set, allows all hosts and returns empty list.
1038 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001039 if not self._allowed_hosts:
1040 return []
1041 bad_deps = []
1042 for dep in self._dependencies:
1043 # Don't enforce this for custom_deps.
1044 if dep.name in self._custom_deps:
1045 continue
1046 if isinstance(dep.url, str):
1047 parsed_url = urllib.parse.urlparse(dep.url)
1048 if parsed_url.netloc and parsed_url.netloc not in self._allowed_hosts:
1049 bad_deps.append(dep)
1050 return bad_deps
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001051
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001052 def FuzzyMatchUrl(self, candidates):
1053 # type: (Union[Mapping[str, str], Collection[str]]) -> Optional[str]
1054 """Attempts to find this dependency in the list of candidates.
Edward Lesmesbb16e332018-03-30 17:54:51 -04001055
Edward Lemure7273d22018-05-10 19:13:51 -04001056 It looks first for the URL of this dependency in the list of
Edward Lesmesbb16e332018-03-30 17:54:51 -04001057 candidates. If it doesn't succeed, and the URL ends in '.git', it will try
1058 looking for the URL minus '.git'. Finally it will try to look for the name
1059 of the dependency.
1060
1061 Args:
Edward Lesmesbb16e332018-03-30 17:54:51 -04001062 candidates: list, dict. The list of candidates in which to look for this
1063 dependency. It can contain URLs as above, or dependency names like
1064 "src/some/dep".
1065
1066 Returns:
1067 If this dependency is not found in the list of candidates, returns None.
1068 Otherwise, it returns under which name did we find this dependency:
1069 - Its parsed url: "https://example.com/src.git'
1070 - Its parsed url minus '.git': "https://example.com/src"
1071 - Its name: "src"
1072 """
Michael Mossd683d7c2018-06-15 05:05:17 +00001073 if self.url:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001074 origin, _ = gclient_utils.SplitUrlRevision(self.url)
1075 match = gclient_utils.FuzzyMatchRepo(origin, candidates)
agabled437d762016-10-17 09:35:11 -07001076 if match:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001077 return match
1078 if self.name in candidates:
1079 return self.name
1080 return None
ilevy@chromium.org0233ac22012-11-28 20:27:02 +00001081
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001082 # Arguments number differs from overridden method
1083 # pylint: disable=arguments-differ
1084 def run(
1085 self,
1086 revision_overrides, # type: Mapping[str, str]
1087 command, # type: str
1088 args, # type: Sequence[str]
1089 work_queue, # type: ExecutionQueue
1090 options, # type: optparse.Values
1091 patch_refs, # type: Mapping[str, str]
1092 target_branches, # type: Mapping[str, str]
1093 skip_sync_revisions, # type: Mapping[str, str]
1094 ):
1095 # type: () -> None
1096 """Runs |command| then parse the DEPS file."""
1097 logging.info('Dependency(%s).run()' % self.name)
1098 assert self._file_list == []
1099 # When running runhooks, there's no need to consult the SCM.
1100 # All known hooks are expected to run unconditionally regardless of
1101 # working copy state, so skip the SCM status check.
1102 run_scm = command not in ('flatten', 'runhooks', 'recurse', 'validate',
1103 None)
1104 file_list = [] if not options.nohooks else None
1105 revision_override = revision_overrides.pop(
1106 self.FuzzyMatchUrl(revision_overrides), None)
1107 if not revision_override and not self.managed:
1108 revision_override = 'unmanaged'
1109 if run_scm and self.url:
1110 # Create a shallow copy to mutate revision.
1111 options = copy.copy(options)
1112 options.revision = revision_override
1113 self._used_revision = options.revision
1114 self._used_scm = self.CreateSCM(out_cb=work_queue.out_cb)
1115 if command != 'update' or self.GetScmName() != 'git':
1116 self._got_revision = self._used_scm.RunCommand(
1117 command, options, args, file_list)
agabled437d762016-10-17 09:35:11 -07001118 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001119 try:
1120 start = time.time()
1121 sync_status = metrics_utils.SYNC_STATUS_FAILURE
1122 self._got_revision = self._used_scm.RunCommand(
1123 command, options, args, file_list)
1124 sync_status = metrics_utils.SYNC_STATUS_SUCCESS
1125 finally:
1126 url, revision = gclient_utils.SplitUrlRevision(self.url)
1127 metrics.collector.add_repeated(
1128 'git_deps', {
1129 'path': self.name,
1130 'url': url,
1131 'revision': revision,
1132 'execution_time': time.time() - start,
1133 'sync_status': sync_status,
1134 })
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00001135
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001136 if isinstance(self, GitDependency) and command == 'update':
1137 patch_repo = self.url.split('@')[0]
1138 patch_ref = patch_refs.pop(self.FuzzyMatchUrl(patch_refs), None)
1139 target_branch = target_branches.pop(
1140 self.FuzzyMatchUrl(target_branches), None)
1141 if patch_ref:
1142 latest_commit = self._used_scm.apply_patch_ref(
1143 patch_repo, patch_ref, target_branch, options,
1144 file_list)
1145 else:
1146 latest_commit = self._used_scm.revinfo(None, None, None)
1147 existing_sync_commits = json.loads(
1148 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
1149 existing_sync_commits[self.name] = latest_commit
1150 os.environ[PREVIOUS_SYNC_COMMITS] = json.dumps(
1151 existing_sync_commits)
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001152
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001153 if file_list:
1154 file_list = [
1155 os.path.join(self.name, f.strip()) for f in file_list
1156 ]
John Budorick0f7b2002018-01-19 15:46:17 -08001157
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001158 # TODO(phajdan.jr): We should know exactly when the paths are
1159 # absolute. Convert all absolute paths to relative.
1160 for i in range(len(file_list or [])):
1161 # It depends on the command being executed (like runhooks vs
1162 # sync).
1163 if not os.path.isabs(file_list[i]):
1164 continue
1165 prefix = os.path.commonprefix(
1166 [self.root.root_dir.lower(), file_list[i].lower()])
1167 file_list[i] = file_list[i][len(prefix):]
1168 # Strip any leading path separators.
1169 while file_list[i].startswith(('\\', '/')):
1170 file_list[i] = file_list[i][1:]
John Budorick0f7b2002018-01-19 15:46:17 -08001171
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001172 # We must check for diffs AFTER any patch_refs have been applied.
1173 if skip_sync_revisions:
1174 skip_sync_rev = skip_sync_revisions.pop(
1175 self.FuzzyMatchUrl(skip_sync_revisions), None)
1176 self._should_sync = (skip_sync_rev is None
1177 or self._used_scm.check_diff(skip_sync_rev,
1178 files=['DEPS']))
1179 if not self._should_sync:
1180 logging.debug(
1181 'Skipping sync for %s. No DEPS changes since last '
1182 'sync at %s' % (self.name, skip_sync_rev))
1183 else:
1184 logging.debug('DEPS changes detected for %s since last sync at '
1185 '%s. Not skipping deps sync' %
1186 (self.name, skip_sync_rev))
Dirk Pranke9f20d022017-10-11 18:36:54 -07001187
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001188 if self.should_recurse:
1189 self.ParseDepsFile()
Corentin Wallez271a78a2020-07-12 15:41:46 +00001190
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001191 self._run_is_done(file_list or [])
Corentin Wallez271a78a2020-07-12 15:41:46 +00001192
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001193 # TODO(crbug.com/1339471): If should_recurse is false, ParseDepsFile
1194 # never gets called meaning we never fetch hooks and dependencies. So
1195 # there's no need to check should_recurse again here.
1196 if self.should_recurse:
1197 if command in ('update', 'revert') and not options.noprehooks:
1198 self.RunPreDepsHooks()
1199 # Parse the dependencies of this dependency.
1200 for s in self.dependencies:
1201 if s.should_process:
1202 work_queue.enqueue(s)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001203
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001204 if command == 'recurse':
1205 # Skip file only checkout.
1206 scm = self.GetScmName()
1207 if not options.scm or scm in options.scm:
1208 cwd = os.path.normpath(
1209 os.path.join(self.root.root_dir, self.name))
1210 # Pass in the SCM type as an env variable. Make sure we don't
1211 # put unicode strings in the environment.
1212 env = os.environ.copy()
1213 if scm:
1214 env['GCLIENT_SCM'] = str(scm)
1215 if self.url:
1216 env['GCLIENT_URL'] = str(self.url)
1217 env['GCLIENT_DEP_PATH'] = str(self.name)
1218 if options.prepend_dir and scm == 'git':
1219 print_stdout = False
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001220
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001221 def filter_fn(line):
1222 """Git-specific path marshaling. It is optimized for git-grep."""
1223 def mod_path(git_pathspec):
1224 match = re.match('^(\\S+?:)?([^\0]+)$',
1225 git_pathspec)
1226 modified_path = os.path.join(
1227 self.name, match.group(2))
1228 branch = match.group(1) or ''
1229 return '%s%s' % (branch, modified_path)
1230
1231 match = re.match('^Binary file ([^\0]+) matches$', line)
1232 if match:
1233 print('Binary file %s matches\n' %
1234 mod_path(match.group(1)))
1235 return
1236
1237 items = line.split('\0')
1238 if len(items) == 2 and items[1]:
1239 print('%s : %s' % (mod_path(items[0]), items[1]))
1240 elif len(items) >= 2:
1241 # Multiple null bytes or a single trailing null byte
1242 # indicate git is likely displaying filenames only
1243 # (such as with -l)
1244 print('\n'.join(
1245 mod_path(path) for path in items if path))
1246 else:
1247 print(line)
1248 else:
1249 print_stdout = True
1250 filter_fn = None
1251
1252 if self.url is None:
1253 print('Skipped omitted dependency %s' % cwd,
1254 file=sys.stderr)
1255 elif os.path.isdir(cwd):
1256 try:
1257 gclient_utils.CheckCallAndFilter(
1258 args,
1259 cwd=cwd,
1260 env=env,
1261 print_stdout=print_stdout,
1262 filter_fn=filter_fn,
1263 )
1264 except subprocess2.CalledProcessError:
1265 if not options.ignore:
1266 raise
1267 else:
1268 print('Skipped missing %s' % cwd, file=sys.stderr)
1269
1270 def GetScmName(self):
1271 raise NotImplementedError()
1272
1273 def CreateSCM(self, out_cb=None):
1274 raise NotImplementedError()
1275
1276 def HasGNArgsFile(self):
1277 return self._gn_args_file is not None
1278
1279 def WriteGNArgsFile(self):
1280 lines = ['# Generated from %r' % self.deps_file]
1281 variables = self.get_vars()
1282 for arg in self._gn_args:
1283 value = variables[arg]
1284 if isinstance(value, gclient_eval.ConstantString):
1285 value = value.value
1286 elif isinstance(value, str):
1287 value = gclient_eval.EvaluateCondition(value, variables)
1288 lines.append('%s = %s' % (arg, ToGNString(value)))
1289
1290 # When use_relative_paths is set, gn_args_file is relative to this DEPS
1291 path_prefix = self.root.root_dir
1292 if self._use_relative_paths:
1293 path_prefix = os.path.join(path_prefix, self.name)
1294
1295 with open(os.path.join(path_prefix, self._gn_args_file), 'wb') as f:
1296 f.write('\n'.join(lines).encode('utf-8', 'replace'))
1297
1298 @gclient_utils.lockedmethod
1299 def _run_is_done(self, file_list):
1300 # Both these are kept for hooks that are run as a separate tree
1301 # traversal.
1302 self._file_list = file_list
1303 self._processed = True
1304
1305 def GetHooks(self, options):
1306 """Evaluates all hooks, and return them in a flat list.
szager@google.comb9a78d32012-03-13 18:46:21 +00001307
1308 RunOnDeps() must have been called before to load the DEPS.
1309 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001310 result = []
1311 if not self.should_process or not self.should_recurse:
1312 # Don't run the hook when it is above recursion_limit.
1313 return result
1314 # If "--force" was specified, run all hooks regardless of what files
1315 # have changed.
1316 if self.deps_hooks:
1317 # TODO(maruel): If the user is using git, then we don't know
1318 # what files have changed so we always run all hooks. It'd be nice
1319 # to fix that.
1320 result.extend(self.deps_hooks)
1321 for s in self.dependencies:
1322 result.extend(s.GetHooks(options))
1323 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001324
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001325 def RunHooksRecursively(self, options, progress):
1326 assert self.hooks_ran == False
1327 self._hooks_ran = True
1328 hooks = self.GetHooks(options)
1329 if progress:
1330 progress._total = len(hooks)
1331 for hook in hooks:
1332 if progress:
1333 progress.update(extra=hook.name or '')
1334 hook.run()
1335 if progress:
1336 progress.end()
maruel@chromium.orgeaf61062010-07-07 18:42:39 +00001337
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001338 def RunPreDepsHooks(self):
1339 assert self.processed
1340 assert self.deps_parsed
1341 assert not self.pre_deps_hooks_ran
1342 assert not self.hooks_ran
1343 for s in self.dependencies:
1344 assert not s.processed
1345 self._pre_deps_hooks_ran = True
1346 for hook in self.pre_deps_hooks:
1347 hook.run()
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001348
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001349 def GetCipdRoot(self):
1350 if self.root is self:
1351 # Let's not infinitely recurse. If this is root and isn't an
1352 # instance of GClient, do nothing.
1353 return None
1354 return self.root.GetCipdRoot()
John Budorickd3ba72b2018-03-20 12:27:42 -07001355
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001356 def subtree(self, include_all):
1357 """Breadth first recursion excluding root node."""
1358 dependencies = self.dependencies
1359 for d in dependencies:
1360 if d.should_process or include_all:
1361 yield d
1362 for d in dependencies:
1363 for i in d.subtree(include_all):
1364 yield i
maruel@chromium.orgad3287e2011-10-03 19:15:10 +00001365
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001366 @gclient_utils.lockedmethod
1367 def add_dependency(self, new_dep):
1368 self._dependencies.append(new_dep)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001369
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001370 @gclient_utils.lockedmethod
1371 def _mark_as_parsed(self, new_hooks):
1372 self._deps_hooks.extend(new_hooks)
1373 self._deps_parsed = True
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001374
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001375 @property
1376 @gclient_utils.lockedmethod
1377 def dependencies(self):
1378 return tuple(self._dependencies)
maruel@chromium.org4bdd5fd2011-09-26 19:41:17 +00001379
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001380 @property
1381 @gclient_utils.lockedmethod
1382 def deps_hooks(self):
1383 return tuple(self._deps_hooks)
maruel@chromium.org064186c2011-09-27 23:53:33 +00001384
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001385 @property
1386 @gclient_utils.lockedmethod
1387 def pre_deps_hooks(self):
1388 return tuple(self._pre_deps_hooks)
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001389
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001390 @property
1391 @gclient_utils.lockedmethod
1392 def deps_parsed(self):
1393 """This is purely for debugging purposes. It's not used anywhere."""
1394 return self._deps_parsed
maruel@chromium.org064186c2011-09-27 23:53:33 +00001395
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001396 @property
1397 @gclient_utils.lockedmethod
1398 def processed(self):
1399 return self._processed
maruel@chromium.org064186c2011-09-27 23:53:33 +00001400
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001401 @property
1402 @gclient_utils.lockedmethod
1403 def pre_deps_hooks_ran(self):
1404 return self._pre_deps_hooks_ran
borenet@google.com2d1ee9e2013-10-15 08:13:16 +00001405
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001406 @property
1407 @gclient_utils.lockedmethod
1408 def hooks_ran(self):
1409 return self._hooks_ran
maruel@chromium.org064186c2011-09-27 23:53:33 +00001410
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001411 @property
1412 @gclient_utils.lockedmethod
1413 def allowed_hosts(self):
1414 return self._allowed_hosts
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00001415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001416 @property
1417 @gclient_utils.lockedmethod
1418 def file_list(self):
1419 return tuple(self._file_list)
maruel@chromium.orgbaa7be32011-10-10 20:49:47 +00001420
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001421 @property
1422 def used_scm(self):
1423 """SCMWrapper instance for this dependency or None if not processed yet."""
1424 return self._used_scm
kustermann@google.coma692e8f2013-04-18 08:32:04 +00001425
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001426 @property
1427 @gclient_utils.lockedmethod
1428 def got_revision(self):
1429 return self._got_revision
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00001430
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001431 @property
1432 def file_list_and_children(self):
1433 result = list(self.file_list)
1434 for d in self.dependencies:
1435 result.extend(d.file_list_and_children)
1436 return tuple(result)
maruel@chromium.org861fd0f2010-07-23 03:05:05 +00001437
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001438 def __str__(self):
1439 out = []
1440 for i in ('name', 'url', 'custom_deps', 'custom_vars', 'deps_hooks',
1441 'file_list', 'should_process', 'processed', 'hooks_ran',
1442 'deps_parsed', 'requirements', 'allowed_hosts'):
1443 # First try the native property if it exists.
1444 if hasattr(self, '_' + i):
1445 value = getattr(self, '_' + i, False)
1446 else:
1447 value = getattr(self, i, False)
1448 if value:
1449 out.append('%s: %s' % (i, value))
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001450
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001451 for d in self.dependencies:
1452 out.extend([' ' + x for x in str(d).splitlines()])
1453 out.append('')
1454 return '\n'.join(out)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001455
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001456 def __repr__(self):
1457 return '%s: %s' % (self.name, self.url)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001458
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001459 def hierarchy(self, include_url=True, graphviz=False):
1460 """Returns a human-readable hierarchical reference to a Dependency."""
1461 def format_name(d):
1462 if include_url:
1463 return '%s(%s)' % (d.name, d.url)
1464 return '"%s"' % d.name # quotes required for graph dot file.
Joanna Wang9144b672023-02-24 23:36:17 +00001465
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001466 out = format_name(self)
1467 i = self.parent
1468 while i and i.name:
1469 out = '%s -> %s' % (format_name(i), out)
1470 if graphviz:
1471 # for graphviz we just need each parent->child relationship
1472 # listed once.
1473 return out
1474 i = i.parent
Joanna Wang9144b672023-02-24 23:36:17 +00001475 return out
maruel@chromium.orgbffb9042010-07-22 20:59:36 +00001476
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001477 def hierarchy_data(self):
1478 """Returns a machine-readable hierarchical reference to a Dependency."""
1479 d = self
1480 out = []
1481 while d and d.name:
1482 out.insert(0, (d.name, d.url))
1483 d = d.parent
1484 return tuple(out)
Michael Mossfe68c912018-03-22 19:19:35 -07001485
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001486 def get_builtin_vars(self):
1487 return {
1488 'checkout_android': 'android' in self.target_os,
1489 'checkout_chromeos': 'chromeos' in self.target_os,
1490 'checkout_fuchsia': 'fuchsia' in self.target_os,
1491 'checkout_ios': 'ios' in self.target_os,
1492 'checkout_linux': 'unix' in self.target_os,
1493 'checkout_mac': 'mac' in self.target_os,
1494 'checkout_win': 'win' in self.target_os,
1495 'host_os': _detect_host_os(),
1496 'checkout_arm': 'arm' in self.target_cpu,
1497 'checkout_arm64': 'arm64' in self.target_cpu,
1498 'checkout_x86': 'x86' in self.target_cpu,
1499 'checkout_mips': 'mips' in self.target_cpu,
1500 'checkout_mips64': 'mips64' in self.target_cpu,
1501 'checkout_ppc': 'ppc' in self.target_cpu,
1502 'checkout_s390': 's390' in self.target_cpu,
1503 'checkout_x64': 'x64' in self.target_cpu,
1504 'host_cpu': detect_host_arch.HostArch(),
1505 }
Robbie Iannucci3db32762023-07-05 19:02:44 +00001506
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001507 def get_vars(self):
1508 """Returns a dictionary of effective variable values
Edward Lemur8f8a50d2018-11-01 22:03:02 +00001509 (DEPS file contents with applied custom_vars overrides)."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001510 # Variable precedence (last has highest):
1511 # - DEPS vars
1512 # - parents, from first to last
1513 # - built-in
1514 # - custom_vars overrides
1515 result = {}
1516 result.update(self._vars)
1517 if self.parent:
1518 merge_vars(result, self.parent.get_vars())
1519 # Provide some built-in variables.
1520 result.update(self.get_builtin_vars())
1521 merge_vars(result, self.custom_vars)
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001522
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001523 return result
Paweł Hajdan, Jrd3790252017-07-03 21:06:24 +02001524
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001525
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001526_PLATFORM_MAPPING = {
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001527 'cygwin': 'win',
1528 'darwin': 'mac',
1529 'linux2': 'linux',
1530 'linux': 'linux',
1531 'win32': 'win',
1532 'aix6': 'aix',
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001533}
1534
1535
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001536def merge_vars(result, new_vars):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001537 for k, v in new_vars.items():
1538 if k in result:
1539 if isinstance(result[k], gclient_eval.ConstantString):
1540 if isinstance(v, gclient_eval.ConstantString):
1541 result[k] = v
1542 else:
1543 result[k].value = v
1544 else:
1545 result[k] = v
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001546 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001547 result[k] = v
Dirk Prankefdd2cd62020-06-30 23:30:47 +00001548
1549
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001550def _detect_host_os():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001551 if sys.platform in _PLATFORM_MAPPING:
1552 return _PLATFORM_MAPPING[sys.platform]
Jonas Termansenbf7eb522023-01-19 17:56:40 +00001553
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001554 try:
1555 return os.uname().sysname.lower()
1556 except AttributeError:
1557 return sys.platform
Paweł Hajdan, Jra3b67ae2017-08-30 15:18:21 +02001558
1559
Edward Lemurb61d3872018-05-09 18:42:47 -04001560class GitDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001561 """A Dependency object that represents a single git checkout."""
1562 @staticmethod
1563 def updateProtocol(url, protocol):
1564 """Updates given URL's protocol"""
1565 # only works on urls, skips local paths
1566 if not url or not protocol or not re.match('([a-z]+)://', url) or \
1567 re.match('file://', url):
1568 return url
Edward Lemurb61d3872018-05-09 18:42:47 -04001569
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001570 return re.sub('^([a-z]+):', protocol + ':', url)
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +00001571
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001572 #override
1573 def GetScmName(self):
1574 """Always 'git'."""
1575 return 'git'
Aravind Vasudevan5965d3e2022-06-01 21:51:30 +00001576
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001577 #override
1578 def CreateSCM(self, out_cb=None):
1579 """Create a Wrapper instance suitable for handling this git dependency."""
1580 return gclient_scm.GitWrapper(self.url,
1581 self.root.root_dir,
1582 self.name,
1583 self.outbuf,
1584 out_cb,
1585 print_outbuf=self.print_outbuf)
Edward Lemurb61d3872018-05-09 18:42:47 -04001586
1587
1588class GClient(GitDependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001589 """Object that represent a gclient checkout. A tree of Dependency(), one per
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00001590 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001591
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001592 DEPS_OS_CHOICES = {
1593 "aix6": "unix",
1594 "win32": "win",
1595 "win": "win",
1596 "cygwin": "win",
1597 "darwin": "mac",
1598 "mac": "mac",
1599 "unix": "unix",
1600 "linux": "unix",
1601 "linux2": "unix",
1602 "linux3": "unix",
1603 "android": "android",
1604 "ios": "ios",
1605 "fuchsia": "fuchsia",
1606 "chromeos": "chromeos",
1607 }
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001608
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001609 DEFAULT_CLIENT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001610solutions = [
Edward Lesmes05934952019-12-19 20:38:09 +00001611 { "name" : %(solution_name)r,
1612 "url" : %(solution_url)r,
1613 "deps_file" : %(deps_file)r,
1614 "managed" : %(managed)r,
smutae7ea312016-07-18 11:59:41 -07001615 "custom_deps" : {
1616 },
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02001617 "custom_vars": %(custom_vars)r,
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001618 },
1619]
Robert Iannuccia19649b2018-06-29 16:31:45 +00001620""")
1621
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001622 DEFAULT_CLIENT_CACHE_DIR_TEXT = ("""\
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001623cache_dir = %(cache_dir)r
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001624""")
1625
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001626 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001627# Snapshot generated with gclient revinfo --snapshot
Edward Lesmesc2960242018-03-06 20:50:15 -05001628solutions = %(solution_list)s
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001629""")
1630
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001631 def __init__(self, root_dir, options):
1632 # Do not change previous behavior. Only solution level and immediate
1633 # DEPS are processed.
1634 self._recursion_limit = 2
1635 super(GClient, self).__init__(parent=None,
1636 name=None,
1637 url=None,
1638 managed=True,
1639 custom_deps=None,
1640 custom_vars=None,
1641 custom_hooks=None,
1642 deps_file='unused',
1643 should_process=True,
1644 should_recurse=True,
1645 relative=None,
1646 condition=None,
1647 print_outbuf=True)
Edward Lemure05f18d2018-06-08 17:36:53 +00001648
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001649 self._options = options
1650 if options.deps_os:
1651 enforced_os = options.deps_os.split(',')
1652 else:
1653 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
1654 if 'all' in enforced_os:
1655 enforced_os = self.DEPS_OS_CHOICES.values()
1656 self._enforced_os = tuple(set(enforced_os))
1657 self._enforced_cpu = (detect_host_arch.HostArch(), )
1658 self._root_dir = root_dir
1659 self._cipd_root = None
1660 self.config_content = None
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001661
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001662 def _CheckConfig(self):
1663 """Verify that the config matches the state of the existing checked-out
borenet@google.com88d10082014-03-21 17:24:48 +00001664 solutions."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001665 for dep in self.dependencies:
1666 if dep.managed and dep.url:
1667 scm = dep.CreateSCM()
1668 actual_url = scm.GetActualRemoteURL(self._options)
1669 if actual_url and not scm.DoesRemoteURLMatch(self._options):
1670 mirror = scm.GetCacheMirror()
1671 if mirror:
1672 mirror_string = '%s (exists=%s)' % (mirror.mirror_path,
1673 mirror.exists())
1674 else:
1675 mirror_string = 'not used'
1676 raise gclient_utils.Error(
1677 '''
borenet@google.com88d10082014-03-21 17:24:48 +00001678Your .gclient file seems to be broken. The requested URL is different from what
borenet@google.com0a427372014-04-02 19:12:13 +00001679is actually checked out in %(checkout_path)s.
borenet@google.com88d10082014-03-21 17:24:48 +00001680
borenet@google.com97882362014-04-07 20:06:02 +00001681The .gclient file contains:
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +00001682URL: %(expected_url)s (%(expected_scm)s)
1683Cache mirror: %(mirror_string)s
borenet@google.com97882362014-04-07 20:06:02 +00001684
1685The local checkout in %(checkout_path)s reports:
1686%(actual_url)s (%(actual_scm)s)
borenet@google.com88d10082014-03-21 17:24:48 +00001687
1688You should ensure that the URL listed in .gclient is correct and either change
agabled437d762016-10-17 09:35:11 -07001689it or fix the checkout.
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00001690''' % {
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001691 'checkout_path': os.path.join(
1692 self.root_dir, dep.name),
1693 'expected_url': dep.url,
1694 'expected_scm': dep.GetScmName(),
1695 'mirror_string': mirror_string,
1696 'actual_url': actual_url,
1697 'actual_scm': dep.GetScmName()
1698 })
borenet@google.com88d10082014-03-21 17:24:48 +00001699
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001700 def SetConfig(self, content):
1701 assert not self.dependencies
1702 config_dict = {}
1703 self.config_content = content
1704 try:
1705 exec(content, config_dict)
1706 except SyntaxError as e:
1707 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org0bcfd182011-10-10 20:06:09 +00001708
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001709 # Append any target OS that is not already being enforced to the tuple.
1710 target_os = config_dict.get('target_os', [])
1711 if config_dict.get('target_os_only', False):
1712 self._enforced_os = tuple(set(target_os))
1713 else:
1714 self._enforced_os = tuple(set(self._enforced_os).union(target_os))
stuartmorgan@chromium.org18a4f6a2012-11-02 13:37:24 +00001715
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001716 # Append any target CPU that is not already being enforced to the tuple.
1717 target_cpu = config_dict.get('target_cpu', [])
1718 if config_dict.get('target_cpu_only', False):
1719 self._enforced_cpu = tuple(set(target_cpu))
1720 else:
1721 self._enforced_cpu = tuple(
1722 set(self._enforced_cpu).union(target_cpu))
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001723
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001724 cache_dir = config_dict.get('cache_dir', UNSET_CACHE_DIR)
1725 if cache_dir is not UNSET_CACHE_DIR:
1726 if cache_dir:
1727 cache_dir = os.path.join(self.root_dir, cache_dir)
1728 cache_dir = os.path.abspath(cache_dir)
Andrii Shyshkalov77ce4bd2017-11-27 12:38:18 -08001729
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001730 git_cache.Mirror.SetCachePath(cache_dir)
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001731
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001732 if not target_os and config_dict.get('target_os_only', False):
1733 raise gclient_utils.Error(
1734 'Can\'t use target_os_only if target_os is '
1735 'not specified')
peter@chromium.org1efccc82012-04-27 16:34:38 +00001736
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001737 if not target_cpu and config_dict.get('target_cpu_only', False):
1738 raise gclient_utils.Error(
1739 'Can\'t use target_cpu_only if target_cpu is '
1740 'not specified')
Tom Andersonc31ae0b2018-02-06 14:48:56 -08001741
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001742 deps_to_add = []
1743 for s in config_dict.get('solutions', []):
1744 try:
1745 deps_to_add.append(
1746 GitDependency(
1747 parent=self,
1748 name=s['name'],
1749 # Update URL with scheme in protocol_override
1750 url=GitDependency.updateProtocol(
1751 s['url'], s.get('protocol_override', None)),
1752 managed=s.get('managed', True),
1753 custom_deps=s.get('custom_deps', {}),
1754 custom_vars=s.get('custom_vars', {}),
1755 custom_hooks=s.get('custom_hooks', []),
1756 deps_file=s.get('deps_file', 'DEPS'),
1757 should_process=True,
1758 should_recurse=True,
1759 relative=None,
1760 condition=None,
1761 print_outbuf=True,
1762 # Pass protocol_override down the tree for child deps to
1763 # use.
1764 protocol=s.get('protocol_override', None),
1765 git_dependencies_state=self.git_dependencies_state))
1766 except KeyError:
1767 raise gclient_utils.Error('Invalid .gclient file. Solution is '
1768 'incomplete: %s' % s)
1769 metrics.collector.add('project_urls', [
Edward Lemuraffd4102019-06-05 18:07:49 +00001770 dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Edward Lemur40764b02018-07-20 18:50:29 +00001771 for dep in deps_to_add
1772 if dep.FuzzyMatchUrl(metrics_utils.KNOWN_PROJECT_URLS)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001773 ])
Edward Lemur40764b02018-07-20 18:50:29 +00001774
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001775 self.add_dependencies_and_close(deps_to_add,
1776 config_dict.get('hooks', []))
1777 logging.info('SetConfig() done')
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001778
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001779 def SaveConfig(self):
1780 gclient_utils.FileWrite(
1781 os.path.join(self.root_dir, self._options.config_filename),
1782 self.config_content)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001783
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001784 @staticmethod
1785 def LoadCurrentConfig(options):
1786 # type: (optparse.Values) -> GClient
1787 """Searches for and loads a .gclient file relative to the current working
Joanna Wang66286612022-06-30 19:59:13 +00001788 dir."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001789 if options.spec:
1790 client = GClient('.', options)
1791 client.SetConfig(options.spec)
1792 else:
1793 if options.verbose:
1794 print('Looking for %s starting from %s\n' %
1795 (options.config_filename, os.getcwd()))
1796 path = gclient_paths.FindGclientRoot(os.getcwd(),
1797 options.config_filename)
1798 if not path:
1799 if options.verbose:
1800 print('Couldn\'t find configuration file.')
1801 return None
1802 client = GClient(path, options)
1803 client.SetConfig(
1804 gclient_utils.FileRead(
1805 os.path.join(path, options.config_filename)))
maruel@chromium.org69392e72011-10-13 22:09:00 +00001806
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001807 if (options.revisions and len(client.dependencies) > 1
1808 and any('@' not in r for r in options.revisions)):
1809 print((
1810 'You must specify the full solution name like --revision %s@%s\n'
1811 'when you have multiple solutions setup in your .gclient file.\n'
1812 'Other solutions present are: %s.') %
1813 (client.dependencies[0].name, options.revisions[0], ', '.join(
1814 s.name for s in client.dependencies[1:])),
1815 file=sys.stderr)
Joanna Wang66286612022-06-30 19:59:13 +00001816
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001817 return client
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001818
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001819 def SetDefaultConfig(self,
1820 solution_name,
1821 deps_file,
1822 solution_url,
1823 managed=True,
1824 cache_dir=UNSET_CACHE_DIR,
1825 custom_vars=None):
1826 text = self.DEFAULT_CLIENT_FILE_TEXT
1827 format_dict = {
1828 'solution_name': solution_name,
1829 'solution_url': solution_url,
1830 'deps_file': deps_file,
1831 'managed': managed,
1832 'custom_vars': custom_vars or {},
1833 }
Robert Iannuccia19649b2018-06-29 16:31:45 +00001834
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001835 if cache_dir is not UNSET_CACHE_DIR:
1836 text += self.DEFAULT_CLIENT_CACHE_DIR_TEXT
1837 format_dict['cache_dir'] = cache_dir
Robert Iannuccia19649b2018-06-29 16:31:45 +00001838
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001839 self.SetConfig(text % format_dict)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001840
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001841 def _SaveEntries(self):
1842 """Creates a .gclient_entries file to record the list of unique checkouts.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001843
1844 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001845 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001846 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
1847 # makes testing a bit too fun.
1848 result = 'entries = {\n'
1849 for entry in self.root.subtree(False):
1850 result += ' %s: %s,\n' % (pprint.pformat(
1851 entry.name), pprint.pformat(entry.url))
1852 result += '}\n'
1853 file_path = os.path.join(self.root_dir, self._options.entries_filename)
1854 logging.debug(result)
1855 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001856
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001857 def _ReadEntries(self):
1858 """Read the .gclient_entries file for the given client.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001859
1860 Returns:
1861 A sequence of solution names, which will be empty if there is the
1862 entries file hasn't been created yet.
1863 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001864 scope = {}
1865 filename = os.path.join(self.root_dir, self._options.entries_filename)
1866 if not os.path.exists(filename):
1867 return {}
1868 try:
1869 exec(gclient_utils.FileRead(filename), scope)
1870 except SyntaxError as e:
1871 gclient_utils.SyntaxErrorToError(filename, e)
1872 return scope.get('entries', {})
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +00001873
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001874 def _ExtractFileJsonContents(self, default_filename):
1875 # type: (str) -> Mapping[str,Any]
1876 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001877
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001878 if not os.path.exists(f):
1879 logging.info('File %s does not exist.' % f)
1880 return {}
Joanna Wang01870792022-08-01 19:02:57 +00001881
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001882 with open(f, 'r') as open_f:
1883 logging.info('Reading content from file %s' % f)
1884 content = open_f.read().rstrip()
1885 if content:
1886 return json.loads(content)
Joanna Wang66286612022-06-30 19:59:13 +00001887 return {}
1888
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001889 def _WriteFileContents(self, default_filename, content):
1890 # type: (str, str) -> None
1891 f = os.path.join(self.root_dir, default_filename)
Joanna Wang01870792022-08-01 19:02:57 +00001892
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001893 with open(f, 'w') as open_f:
1894 logging.info('Writing to file %s' % f)
1895 open_f.write(content)
Joanna Wangf3edc502022-07-20 00:12:10 +00001896
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001897 def _EnforceSkipSyncRevisions(self, patch_refs):
1898 # type: (Mapping[str, str]) -> Mapping[str, str]
1899 """Checks for and enforces revisions for skipping deps syncing."""
1900 previous_sync_commits = self._ExtractFileJsonContents(
1901 PREVIOUS_SYNC_COMMITS_FILE)
Joanna Wang66286612022-06-30 19:59:13 +00001902
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001903 if not previous_sync_commits:
1904 return {}
maruel@chromium.org918a9ae2010-05-28 15:50:30 +00001905
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001906 # Current `self.dependencies` only contain solutions. If a patch_ref is
1907 # not for a solution, then it is for a solution's dependency or recursed
1908 # dependency which we cannot support while skipping sync.
1909 if patch_refs:
1910 unclaimed_prs = []
1911 candidates = []
1912 for dep in self.dependencies:
1913 origin, _ = gclient_utils.SplitUrlRevision(dep.url)
1914 candidates.extend([origin, dep.name])
1915 for patch_repo in patch_refs:
1916 if not gclient_utils.FuzzyMatchRepo(patch_repo, candidates):
1917 unclaimed_prs.append(patch_repo)
1918 if unclaimed_prs:
1919 print(
1920 'We cannot skip syncs when there are --patch-refs flags for '
1921 'non-solution dependencies. To skip syncing, remove patch_refs '
1922 'for: \n%s' % '\n'.join(unclaimed_prs))
1923 return {}
Edward Lesmesc621b212018-03-21 20:26:56 -04001924
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001925 # We cannot skip syncing if there are custom_vars that differ from the
1926 # previous run's custom_vars.
1927 previous_custom_vars = self._ExtractFileJsonContents(
1928 PREVIOUS_CUSTOM_VARS_FILE)
1929
1930 cvs_by_name = {s.name: s.custom_vars for s in self.dependencies}
1931
1932 skip_sync_revisions = {}
1933 for name, commit in previous_sync_commits.items():
1934 previous_vars = previous_custom_vars.get(name)
1935 if previous_vars == cvs_by_name.get(name) or (
1936 not previous_vars and not cvs_by_name.get(name)):
1937 skip_sync_revisions[name] = commit
1938 else:
1939 print(
1940 'We cannot skip syncs when custom_vars for solutions have '
1941 'changed since the last sync run on this machine.\n'
1942 '\nRemoving skip_sync_revision for:\n'
1943 'solution: %s, current: %r, previous: %r.' %
1944 (name, cvs_by_name.get(name), previous_vars))
1945 print('no-sync experiment enabled with %r' % skip_sync_revisions)
1946 return skip_sync_revisions
1947
1948 # TODO(crbug.com/1340695): Remove handling revisions without '@'.
1949 def _EnforceRevisions(self):
1950 """Checks for revision overrides."""
1951 revision_overrides = {}
1952 if self._options.head:
1953 return revision_overrides
1954 if not self._options.revisions:
1955 return revision_overrides
1956 solutions_names = [s.name for s in self.dependencies]
1957 for index, revision in enumerate(self._options.revisions):
1958 if not '@' in revision:
1959 # Support for --revision 123
1960 revision = '%s@%s' % (solutions_names[index], revision)
1961 name, rev = revision.split('@', 1)
1962 revision_overrides[name] = rev
1963 return revision_overrides
1964
1965 def _EnforcePatchRefsAndBranches(self):
1966 # type: () -> Tuple[Mapping[str, str], Mapping[str, str]]
1967 """Checks for patch refs."""
1968 patch_refs = {}
1969 target_branches = {}
1970 if not self._options.patch_refs:
1971 return patch_refs, target_branches
1972 for given_patch_ref in self._options.patch_refs:
1973 patch_repo, _, patch_ref = given_patch_ref.partition('@')
1974 if not patch_repo or not patch_ref or ':' not in patch_ref:
1975 raise gclient_utils.Error(
1976 'Wrong revision format: %s should be of the form '
1977 'patch_repo@target_branch:patch_ref.' % given_patch_ref)
1978 target_branch, _, patch_ref = patch_ref.partition(':')
1979 target_branches[patch_repo] = target_branch
1980 patch_refs[patch_repo] = patch_ref
1981 return patch_refs, target_branches
1982
1983 def _RemoveUnversionedGitDirs(self):
1984 """Remove directories that are no longer part of the checkout.
Edward Lemur5b1fa942018-10-04 23:22:09 +00001985
1986 Notify the user if there is an orphaned entry in their working copy.
1987 Only delete the directory if there are no changes in it, and
1988 delete_unversioned_trees is set to true.
Josip Sokcevic1b8211f2022-09-30 17:46:53 +00001989
1990 Returns CIPD packages that are no longer versioned.
Edward Lemur5b1fa942018-10-04 23:22:09 +00001991 """
1992
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001993 entry_names_and_sync = [(i.name, i._should_sync)
1994 for i in self.root.subtree(False) if i.url]
1995 entries = []
1996 if entry_names_and_sync:
1997 entries, _ = zip(*entry_names_and_sync)
1998 full_entries = [
1999 os.path.join(self.root_dir, e.replace('/', os.path.sep))
2000 for e in entries
2001 ]
2002 no_sync_entries = [
2003 name for name, should_sync in entry_names_and_sync
2004 if not should_sync
2005 ]
Edward Lemur5b1fa942018-10-04 23:22:09 +00002006
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002007 removed_cipd_entries = []
2008 for entry, prev_url in self._ReadEntries().items():
2009 if not prev_url:
2010 # entry must have been overridden via .gclient custom_deps
2011 continue
2012 if any(entry.startswith(sln) for sln in no_sync_entries):
2013 # Dependencies of solutions that skipped syncing would not
2014 # show up in `entries`.
2015 continue
2016 if (':' in entry):
2017 # This is a cipd package. Don't clean it up, but prepare for
2018 # return
2019 if entry not in entries:
2020 removed_cipd_entries.append(entry)
2021 continue
2022 # Fix path separator on Windows.
2023 entry_fixed = entry.replace('/', os.path.sep)
2024 e_dir = os.path.join(self.root_dir, entry_fixed)
2025 # Use entry and not entry_fixed there.
2026 if (entry not in entries and
2027 (not any(path.startswith(entry + '/') for path in entries))
2028 and os.path.exists(e_dir)):
2029 # The entry has been removed from DEPS.
2030 scm = gclient_scm.GitWrapper(prev_url, self.root_dir,
2031 entry_fixed, self.outbuf)
Edward Lemur5b1fa942018-10-04 23:22:09 +00002032
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002033 # Check to see if this directory is now part of a higher-up
2034 # checkout.
2035 scm_root = None
2036 try:
2037 scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2038 scm.checkout_path)
2039 except subprocess2.CalledProcessError:
2040 pass
2041 if not scm_root:
2042 logging.warning(
2043 'Could not find checkout root for %s. Unable to '
2044 'determine whether it is part of a higher-level '
2045 'checkout, so not removing.' % entry)
2046 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002047
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002048 # This is to handle the case of third_party/WebKit migrating
2049 # from being a DEPS entry to being part of the main project. If
2050 # the subproject is a Git project, we need to remove its .git
2051 # folder. Otherwise git operations on that folder will have
2052 # different effects depending on the current working directory.
2053 if os.path.abspath(scm_root) == os.path.abspath(e_dir):
2054 e_par_dir = os.path.join(e_dir, os.pardir)
2055 if gclient_scm.scm.GIT.IsInsideWorkTree(e_par_dir):
2056 par_scm_root = gclient_scm.scm.GIT.GetCheckoutRoot(
2057 e_par_dir)
2058 # rel_e_dir : relative path of entry w.r.t. its parent
2059 # repo.
2060 rel_e_dir = os.path.relpath(e_dir, par_scm_root)
2061 if gclient_scm.scm.GIT.IsDirectoryVersioned(
2062 par_scm_root, rel_e_dir):
2063 save_dir = scm.GetGitBackupDirPath()
2064 # Remove any eventual stale backup dir for the same
2065 # project.
2066 if os.path.exists(save_dir):
2067 gclient_utils.rmtree(save_dir)
2068 os.rename(os.path.join(e_dir, '.git'), save_dir)
2069 # When switching between the two states (entry/ is a
2070 # subproject -> entry/ is part of the outer
2071 # project), it is very likely that some files are
2072 # changed in the checkout, unless we are jumping
2073 # *exactly* across the commit which changed just
2074 # DEPS. In such case we want to cleanup any eventual
2075 # stale files (coming from the old subproject) in
2076 # order to end up with a clean checkout.
2077 gclient_scm.scm.GIT.CleanupDir(
2078 par_scm_root, rel_e_dir)
2079 assert not os.path.exists(
2080 os.path.join(e_dir, '.git'))
2081 print(
2082 '\nWARNING: \'%s\' has been moved from DEPS to a higher '
2083 'level checkout. The git folder containing all the local'
2084 ' branches has been saved to %s.\n'
2085 'If you don\'t care about its state you can safely '
2086 'remove that folder to free up space.' %
2087 (entry, save_dir))
2088 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002089
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002090 if scm_root in full_entries:
2091 logging.info(
2092 '%s is part of a higher level checkout, not removing',
2093 scm.GetCheckoutRoot())
2094 continue
Edward Lemur5b1fa942018-10-04 23:22:09 +00002095
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002096 file_list = []
2097 scm.status(self._options, [], file_list)
2098 modified_files = file_list != []
2099 if (not self._options.delete_unversioned_trees
2100 or (modified_files and not self._options.force)):
2101 # There are modified files in this entry. Keep warning until
2102 # removed.
2103 self.add_dependency(
2104 GitDependency(
2105 parent=self,
2106 name=entry,
2107 # Update URL with scheme in protocol_override
2108 url=GitDependency.updateProtocol(
2109 prev_url, self.protocol),
2110 managed=False,
2111 custom_deps={},
2112 custom_vars={},
2113 custom_hooks=[],
2114 deps_file=None,
2115 should_process=True,
2116 should_recurse=False,
2117 relative=None,
2118 condition=None,
2119 protocol=self.protocol,
2120 git_dependencies_state=self.git_dependencies_state))
2121 if modified_files and self._options.delete_unversioned_trees:
2122 print(
2123 '\nWARNING: \'%s\' is no longer part of this client.\n'
2124 'Despite running \'gclient sync -D\' no action was taken '
2125 'as there are modifications.\nIt is recommended you revert '
2126 'all changes or run \'gclient sync -D --force\' next '
2127 'time.' % entry_fixed)
2128 else:
2129 print(
2130 '\nWARNING: \'%s\' is no longer part of this client.\n'
2131 'It is recommended that you manually remove it or use '
2132 '\'gclient sync -D\' next time.' % entry_fixed)
2133 else:
2134 # Delete the entry
2135 print('\n________ deleting \'%s\' in \'%s\'' %
2136 (entry_fixed, self.root_dir))
2137 gclient_utils.rmtree(e_dir)
2138 # record the current list of entries for next time
2139 self._SaveEntries()
2140 return removed_cipd_entries
Edward Lemur5b1fa942018-10-04 23:22:09 +00002141
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002142 def RunOnDeps(self,
2143 command,
2144 args,
2145 ignore_requirements=False,
2146 progress=True):
2147 """Runs a command on each dependency in a client and its dependencies.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002148
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002149 Args:
2150 command: The command to use (e.g., 'status' or 'diff')
2151 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002152 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002153 if not self.dependencies:
2154 raise gclient_utils.Error('No solution specified')
Michael Mossd683d7c2018-06-15 05:05:17 +00002155
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002156 revision_overrides = {}
2157 patch_refs = {}
2158 target_branches = {}
2159 skip_sync_revisions = {}
2160 # It's unnecessary to check for revision overrides for 'recurse'.
2161 # Save a few seconds by not calling _EnforceRevisions() in that case.
2162 if command not in ('diff', 'recurse', 'runhooks', 'status', 'revert',
2163 'validate'):
2164 self._CheckConfig()
2165 revision_overrides = self._EnforceRevisions()
Edward Lesmesc621b212018-03-21 20:26:56 -04002166
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002167 if command == 'update':
2168 patch_refs, target_branches = self._EnforcePatchRefsAndBranches()
2169 if NO_SYNC_EXPERIMENT in self._options.experiments:
2170 skip_sync_revisions = self._EnforceSkipSyncRevisions(patch_refs)
Joanna Wang66286612022-06-30 19:59:13 +00002171
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002172 # Store solutions' custom_vars on memory to compare in the next run.
2173 # All dependencies added later are inherited from the current
2174 # self.dependencies.
2175 custom_vars = {
2176 dep.name: dep.custom_vars
2177 for dep in self.dependencies if dep.custom_vars
Michael Mossd683d7c2018-06-15 05:05:17 +00002178 }
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002179 if custom_vars:
2180 self._WriteFileContents(PREVIOUS_CUSTOM_VARS_FILE,
2181 json.dumps(custom_vars))
2182
2183 # Disable progress for non-tty stdout.
2184 should_show_progress = (setup_color.IS_TTY and not self._options.verbose
2185 and progress)
2186 pm = None
2187 if should_show_progress:
2188 if command in ('update', 'revert'):
2189 pm = Progress('Syncing projects', 1)
2190 elif command in ('recurse', 'validate'):
2191 pm = Progress(' '.join(args), 1)
2192 work_queue = gclient_utils.ExecutionQueue(
2193 self._options.jobs,
2194 pm,
2195 ignore_requirements=ignore_requirements,
2196 verbose=self._options.verbose)
2197 for s in self.dependencies:
2198 if s.should_process:
2199 work_queue.enqueue(s)
2200 work_queue.flush(revision_overrides,
2201 command,
2202 args,
2203 options=self._options,
2204 patch_refs=patch_refs,
2205 target_branches=target_branches,
2206 skip_sync_revisions=skip_sync_revisions)
2207
2208 if revision_overrides:
2209 print(
2210 'Please fix your script, having invalid --revision flags will soon '
2211 'be considered an error.',
2212 file=sys.stderr)
2213
2214 if patch_refs:
2215 raise gclient_utils.Error(
2216 'The following --patch-ref flags were not used. Please fix it:\n%s'
2217 % ('\n'.join(patch_repo + '@' + patch_ref
2218 for patch_repo, patch_ref in patch_refs.items())))
2219
2220 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
2221 # they have fsmonitor enabled.
2222 if command == 'update':
2223 # Check if any of the root dependency have submodules.
2224 is_submoduled = any(
2225 map(
2226 lambda d: d.git_dependencies_state in
2227 (gclient_eval.SUBMODULES, gclient_eval.SYNC),
2228 self.dependencies))
2229 if is_submoduled:
2230 git_common.warn_submodule()
2231
2232 # Once all the dependencies have been processed, it's now safe to write
2233 # out the gn_args_file and run the hooks.
2234 removed_cipd_entries = []
2235 if command == 'update':
2236 for dependency in self.dependencies:
2237 gn_args_dep = dependency
2238 if gn_args_dep._gn_args_from:
2239 deps_map = {
2240 dep.name: dep
2241 for dep in gn_args_dep.dependencies
2242 }
2243 gn_args_dep = deps_map.get(gn_args_dep._gn_args_from)
2244 if gn_args_dep and gn_args_dep.HasGNArgsFile():
2245 gn_args_dep.WriteGNArgsFile()
2246
2247 removed_cipd_entries = self._RemoveUnversionedGitDirs()
2248
2249 # Sync CIPD dependencies once removed deps are deleted. In case a git
2250 # dependency was moved to CIPD, we want to remove the old git directory
2251 # first and then sync the CIPD dep.
2252 if self._cipd_root:
2253 self._cipd_root.run(command)
2254 # It's possible that CIPD removed some entries that are now part of
2255 # git worktree. Try to checkout those directories
2256 if removed_cipd_entries:
2257 for cipd_entry in removed_cipd_entries:
2258 cwd = os.path.join(self._root_dir, cipd_entry.split(':')[0])
2259 cwd, tail = os.path.split(cwd)
2260 if cwd:
2261 try:
2262 gclient_scm.scm.GIT.Capture(['checkout', tail],
2263 cwd=cwd)
2264 except subprocess2.CalledProcessError:
2265 pass
2266
2267 if not self._options.nohooks:
2268 if should_show_progress:
2269 pm = Progress('Running hooks', 1)
2270 self.RunHooksRecursively(self._options, pm)
2271
2272 self._WriteFileContents(PREVIOUS_SYNC_COMMITS_FILE,
2273 os.environ.get(PREVIOUS_SYNC_COMMITS, '{}'))
2274
2275 return 0
2276
2277 def PrintRevInfo(self):
2278 if not self.dependencies:
2279 raise gclient_utils.Error('No solution specified')
2280 # Load all the settings.
2281 work_queue = gclient_utils.ExecutionQueue(self._options.jobs,
2282 None,
2283 False,
2284 verbose=self._options.verbose)
2285 for s in self.dependencies:
2286 if s.should_process:
2287 work_queue.enqueue(s)
2288 work_queue.flush({},
2289 None, [],
2290 options=self._options,
2291 patch_refs=None,
2292 target_branches=None,
2293 skip_sync_revisions=None)
2294
2295 def ShouldPrintRevision(dep):
2296 return (not self._options.filter
2297 or dep.FuzzyMatchUrl(self._options.filter))
2298
2299 if self._options.snapshot:
2300 json_output = []
2301 # First level at .gclient
2302 for d in self.dependencies:
2303 entries = {}
2304
2305 def GrabDeps(dep):
2306 """Recursively grab dependencies."""
2307 for rec_d in dep.dependencies:
2308 rec_d.PinToActualRevision()
2309 if ShouldPrintRevision(rec_d):
2310 entries[rec_d.name] = rec_d.url
2311 GrabDeps(rec_d)
2312
2313 GrabDeps(d)
2314 json_output.append({
2315 'name': d.name,
2316 'solution_url': d.url,
2317 'deps_file': d.deps_file,
2318 'managed': d.managed,
2319 'custom_deps': entries,
2320 })
2321 if self._options.output_json == '-':
2322 print(json.dumps(json_output, indent=2, separators=(',', ': ')))
2323 elif self._options.output_json:
2324 with open(self._options.output_json, 'w') as f:
2325 json.dump(json_output, f)
2326 else:
2327 # Print the snapshot configuration file
2328 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {
2329 'solution_list': pprint.pformat(json_output, indent=2),
2330 })
Michael Mossd683d7c2018-06-15 05:05:17 +00002331 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002332 entries = {}
2333 for d in self.root.subtree(False):
2334 if self._options.actual:
2335 d.PinToActualRevision()
2336 if ShouldPrintRevision(d):
2337 entries[d.name] = d.url
2338 if self._options.output_json:
2339 json_output = {
2340 name: {
2341 'url': rev.split('@')[0] if rev else None,
2342 'rev':
2343 rev.split('@')[1] if rev and '@' in rev else None,
2344 }
2345 for name, rev in entries.items()
2346 }
2347 if self._options.output_json == '-':
2348 print(
2349 json.dumps(json_output,
2350 indent=2,
2351 separators=(',', ': ')))
2352 else:
2353 with open(self._options.output_json, 'w') as f:
2354 json.dump(json_output, f)
2355 else:
2356 keys = sorted(entries.keys())
2357 for x in keys:
2358 print('%s: %s' % (x, entries[x]))
2359 logging.info(str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002360
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002361 def ParseDepsFile(self):
2362 """No DEPS to parse for a .gclient file."""
2363 raise gclient_utils.Error('Internal error')
maruel@chromium.orgd36fba82010-06-28 16:50:40 +00002364
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002365 def PrintLocationAndContents(self):
2366 # Print out the .gclient file. This is longer than if we just printed
2367 # the client dict, but more legible, and it might contain helpful
2368 # comments.
2369 print('Loaded .gclient config in %s:\n%s' %
2370 (self.root_dir, self.config_content))
sergiyb@chromium.orgfa2707e2016-03-12 00:40:56 +00002371
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002372 def GetCipdRoot(self):
2373 if not self._cipd_root:
2374 self._cipd_root = gclient_scm.CipdRoot(
2375 self.root_dir,
2376 # TODO(jbudorick): Support other service URLs as necessary.
2377 # Service URLs should be constant over the scope of a cipd
2378 # root, so a var per DEPS file specifying the service URL
2379 # should suffice.
2380 'https://chrome-infra-packages.appspot.com')
2381 return self._cipd_root
John Budorickd3ba72b2018-03-20 12:27:42 -07002382
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002383 @property
2384 def root_dir(self):
2385 """Root directory of gclient checkout."""
2386 return self._root_dir
maruel@chromium.org75a59272010-06-11 22:34:03 +00002387
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002388 @property
2389 def enforced_os(self):
2390 """What deps_os entries that are to be parsed."""
2391 return self._enforced_os
maruel@chromium.org271375b2010-06-23 19:17:38 +00002392
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002393 @property
2394 def target_os(self):
2395 return self._enforced_os
sivachandra@chromium.orgd45e73e2012-10-24 23:42:48 +00002396
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002397 @property
2398 def target_cpu(self):
2399 return self._enforced_cpu
Tom Andersonc31ae0b2018-02-06 14:48:56 -08002400
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002401
John Budorick0f7b2002018-01-19 15:46:17 -08002402class CipdDependency(Dependency):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002403 """A Dependency object that represents a single CIPD package."""
2404 def __init__(self, parent, name, dep_value, cipd_root, custom_vars,
2405 should_process, relative, condition):
2406 package = dep_value['package']
2407 version = dep_value['version']
2408 url = urllib.parse.urljoin(cipd_root.service_url,
2409 '%s@%s' % (package, version))
2410 super(CipdDependency, self).__init__(parent=parent,
2411 name=name + ':' + package,
2412 url=url,
2413 managed=None,
2414 custom_deps=None,
2415 custom_vars=custom_vars,
2416 custom_hooks=None,
2417 deps_file=None,
2418 should_process=should_process,
2419 should_recurse=False,
2420 relative=relative,
2421 condition=condition)
2422 self._cipd_package = None
2423 self._cipd_root = cipd_root
2424 # CIPD wants /-separated paths, even on Windows.
2425 native_subdir_path = os.path.relpath(
2426 os.path.join(self.root.root_dir, name), cipd_root.root_dir)
2427 self._cipd_subdir = posixpath.join(*native_subdir_path.split(os.sep))
2428 self._package_name = package
2429 self._package_version = version
John Budorick0f7b2002018-01-19 15:46:17 -08002430
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002431 #override
2432 def run(self, revision_overrides, command, args, work_queue, options,
2433 patch_refs, target_branches, skip_sync_revisions):
2434 """Runs |command| then parse the DEPS file."""
2435 logging.info('CipdDependency(%s).run()' % self.name)
2436 if not self.should_process:
2437 return
2438 self._CreatePackageIfNecessary()
2439 super(CipdDependency,
2440 self).run(revision_overrides, command, args, work_queue, options,
2441 patch_refs, target_branches, skip_sync_revisions)
John Budorickd3ba72b2018-03-20 12:27:42 -07002442
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002443 def _CreatePackageIfNecessary(self):
2444 # We lazily create the CIPD package to make sure that only packages
2445 # that we want (as opposed to all packages defined in all DEPS files
2446 # we parse) get added to the root and subsequently ensured.
2447 if not self._cipd_package:
2448 self._cipd_package = self._cipd_root.add_package(
2449 self._cipd_subdir, self._package_name, self._package_version)
John Budorickd3ba72b2018-03-20 12:27:42 -07002450
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002451 def ParseDepsFile(self):
2452 """CIPD dependencies are not currently allowed to have nested deps."""
2453 self.add_dependencies_and_close([], [])
John Budorick0f7b2002018-01-19 15:46:17 -08002454
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002455 #override
2456 def verify_validity(self):
2457 """CIPD dependencies allow duplicate name for packages in same directory."""
2458 logging.info('Dependency(%s).verify_validity()' % self.name)
2459 return True
John Budorick0f7b2002018-01-19 15:46:17 -08002460
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002461 #override
2462 def GetScmName(self):
2463 """Always 'cipd'."""
2464 return 'cipd'
Shenghua Zhang6f830312018-02-26 11:45:07 -08002465
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002466 def GetExpandedPackageName(self):
2467 """Return the CIPD package name with the variables evaluated."""
2468 package = self._cipd_root.expand_package_name(self._package_name)
2469 if package:
2470 return package
2471 return self._package_name
John Budorick0f7b2002018-01-19 15:46:17 -08002472
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002473 #override
2474 def CreateSCM(self, out_cb=None):
2475 """Create a Wrapper instance suitable for handling this CIPD dependency."""
2476 self._CreatePackageIfNecessary()
2477 return gclient_scm.CipdWrapper(self.url,
2478 self.root.root_dir,
2479 self.name,
2480 self.outbuf,
2481 out_cb,
2482 root=self._cipd_root,
2483 package=self._cipd_package)
Dan Le Febvreb0e8e7a2023-05-18 23:36:46 +00002484
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002485 def hierarchy(self, include_url=False, graphviz=False):
2486 if graphviz:
2487 return '' # graphviz lines not implemented for cipd deps.
2488 return self.parent.hierarchy(include_url) + ' -> ' + self._cipd_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08002489
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002490 def ToLines(self):
2491 # () -> Sequence[str]
2492 """Return a list of lines representing this in a DEPS file."""
2493 def escape_cipd_var(package):
2494 return package.replace('{', '{{').replace('}', '}}')
Edward Lemure4e15042018-06-28 18:07:00 +00002495
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002496 s = []
2497 self._CreatePackageIfNecessary()
2498 if self._cipd_package.authority_for_subdir:
2499 condition_part = ([' "condition": %r,' %
2500 self.condition] if self.condition else [])
2501 s.extend([
2502 ' # %s' % self.hierarchy(include_url=False),
2503 ' "%s": {' % (self.name.split(':')[0], ),
2504 ' "packages": [',
2505 ])
2506 for p in sorted(self._cipd_root.packages(self._cipd_subdir),
2507 key=lambda x: x.name):
2508 s.extend([
2509 ' {',
2510 ' "package": "%s",' % escape_cipd_var(p.name),
2511 ' "version": "%s",' % p.version,
2512 ' },',
2513 ])
John Budorickc35aba52018-06-28 20:57:03 +00002514
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002515 s.extend([
2516 ' ],',
2517 ' "dep_type": "cipd",',
2518 ] + condition_part + [
2519 ' },',
2520 '',
2521 ])
2522 return s
John Budorick0f7b2002018-01-19 15:46:17 -08002523
2524
maruel@chromium.org5ca27692010-05-26 19:32:41 +00002525#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002526
2527
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002528@subcommand.usage('[command] [args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002529@metrics.collector.collect_metrics('gclient recurse')
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002530def CMDrecurse(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002531 """Operates [command args ...] on all the dependencies.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002532
Arthur Milchior08cd5fe2022-07-28 20:38:47 +00002533 Change directory to each dependency's directory, and call [command
2534 args ...] there. Sets GCLIENT_DEP_PATH environment variable as the
2535 dep's relative location to root directory of the checkout.
2536
2537 Examples:
2538 * `gclient recurse --no-progress -j1 sh -c 'echo "$GCLIENT_DEP_PATH"'`
2539 print the relative path of each dependency.
2540 * `gclient recurse --no-progress -j1 sh -c "pwd"`
2541 print the absolute path of each dependency.
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002542 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002543 # Stop parsing at the first non-arg so that these go through to the command
2544 parser.disable_interspersed_args()
2545 parser.add_option('-s',
2546 '--scm',
2547 action='append',
2548 default=[],
2549 help='Choose scm types to operate upon.')
2550 parser.add_option('-i',
2551 '--ignore',
2552 action='store_true',
2553 help='Ignore non-zero return codes from subcommands.')
2554 parser.add_option(
2555 '--prepend-dir',
2556 action='store_true',
2557 help='Prepend relative dir for use with git <cmd> --null.')
2558 parser.add_option(
2559 '--no-progress',
2560 action='store_true',
2561 help='Disable progress bar that shows sub-command updates')
2562 options, args = parser.parse_args(args)
2563 if not args:
2564 print('Need to supply a command!', file=sys.stderr)
2565 return 1
2566 root_and_entries = gclient_utils.GetGClientRootAndEntries()
2567 if not root_and_entries:
2568 print(
2569 'You need to run gclient sync at least once to use \'recurse\'.\n'
2570 'This is because .gclient_entries needs to exist and be up to date.',
2571 file=sys.stderr)
2572 return 1
davidbarr@chromium.org47ca0ee2012-03-02 16:06:11 +00002573
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002574 # Normalize options.scm to a set()
2575 scm_set = set()
2576 for scm in options.scm:
2577 scm_set.update(scm.split(','))
2578 options.scm = scm_set
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002579
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002580 options.nohooks = True
2581 client = GClient.LoadCurrentConfig(options)
2582 if not client:
2583 raise gclient_utils.Error(
2584 'client not configured; see \'gclient config\'')
2585 return client.RunOnDeps('recurse',
2586 args,
2587 ignore_requirements=True,
2588 progress=not options.no_progress)
piman@chromium.org4b90e3a2010-07-01 20:28:26 +00002589
2590
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002591@subcommand.usage('[args ...]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00002592@metrics.collector.collect_metrics('gclient fetch')
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002593def CMDfetch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002594 """Fetches upstream commits for all modules.
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00002595
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002596 Completely git-specific. Simply runs 'git fetch [args ...]' for each module.
2597 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002598 (options, args) = parser.parse_args(args)
2599 return CMDrecurse(
2600 OptionParser(),
2601 ['--jobs=%d' % options.jobs, '--scm=git', 'git', 'fetch'] + args)
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00002602
2603
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002604class Flattener(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002605 """Flattens a gclient solution."""
2606 def __init__(self, client, pin_all_deps=False):
2607 """Constructor.
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002608
2609 Arguments:
2610 client (GClient): client to flatten
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002611 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2612 in DEPS
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002613 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002614 self._client = client
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002615
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002616 self._deps_string = None
2617 self._deps_graph_lines = None
2618 self._deps_files = set()
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002619
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002620 self._allowed_hosts = set()
2621 self._deps = {}
2622 self._hooks = []
2623 self._pre_deps_hooks = []
2624 self._vars = {}
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002625
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002626 self._flatten(pin_all_deps=pin_all_deps)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002627
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002628 @property
2629 def deps_string(self):
2630 assert self._deps_string is not None
2631 return self._deps_string
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002632
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002633 @property
2634 def deps_graph_lines(self):
2635 assert self._deps_graph_lines is not None
2636 return self._deps_graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00002637
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002638 @property
2639 def deps_files(self):
2640 return self._deps_files
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002641
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002642 def _pin_dep(self, dep):
2643 """Pins a dependency to specific full revision sha.
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002644
2645 Arguments:
2646 dep (Dependency): dependency to process
2647 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002648 if dep.url is None:
2649 return
Michael Mossd683d7c2018-06-15 05:05:17 +00002650
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002651 # Make sure the revision is always fully specified (a hash),
2652 # as opposed to refs or tags which might change. Similarly,
2653 # shortened shas might become ambiguous; make sure to always
2654 # use full one for pinning.
2655 revision = gclient_utils.SplitUrlRevision(dep.url)[1]
2656 if not revision or not gclient_utils.IsFullGitSha(revision):
2657 dep.PinToActualRevision()
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +02002658
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002659 def _flatten(self, pin_all_deps=False):
2660 """Runs the flattener. Saves resulting DEPS string.
Paweł Hajdan, Jr271a1682017-07-06 20:54:30 +02002661
2662 Arguments:
2663 pin_all_deps (bool): whether to pin all deps, even if they're not pinned
2664 in DEPS
2665 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002666 for solution in self._client.dependencies:
2667 self._add_dep(solution)
2668 self._flatten_dep(solution)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002669
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002670 if pin_all_deps:
2671 for dep in self._deps.values():
2672 self._pin_dep(dep)
Paweł Hajdan, Jr39300ba2017-08-11 16:52:38 +02002673
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002674 def add_deps_file(dep):
2675 # Only include DEPS files referenced by recursedeps.
2676 if not dep.should_recurse:
2677 return
2678 deps_file = dep.deps_file
2679 deps_path = os.path.join(self._client.root_dir, dep.name, deps_file)
2680 if not os.path.exists(deps_path):
2681 # gclient has a fallback that if deps_file doesn't exist, it'll
2682 # try DEPS. Do the same here.
2683 deps_file = 'DEPS'
2684 deps_path = os.path.join(self._client.root_dir, dep.name,
2685 deps_file)
2686 if not os.path.exists(deps_path):
2687 return
2688 assert dep.url
2689 self._deps_files.add((dep.url, deps_file, dep.hierarchy_data()))
Paweł Hajdan, Jr76a9d042017-08-18 20:05:41 +02002690
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002691 for dep in self._deps.values():
2692 add_deps_file(dep)
Joanna Wang9144b672023-02-24 23:36:17 +00002693
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002694 gn_args_dep = self._deps.get(self._client.dependencies[0]._gn_args_from,
2695 self._client.dependencies[0])
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002696
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002697 self._deps_graph_lines = _DepsToDotGraphLines(self._deps)
2698 self._deps_string = '\n'.join(
2699 _GNSettingsToLines(gn_args_dep._gn_args_file, gn_args_dep._gn_args)
2700 + _AllowedHostsToLines(self._allowed_hosts) +
2701 _DepsToLines(self._deps) + _HooksToLines('hooks', self._hooks) +
2702 _HooksToLines('pre_deps_hooks', self._pre_deps_hooks) +
2703 _VarsToLines(self._vars) + [
2704 '# %s, %s' % (url, deps_file)
2705 for url, deps_file, _ in sorted(self._deps_files)
2706 ] + ['']) # Ensure newline at end of file.
2707
2708 def _add_dep(self, dep):
2709 """Helper to add a dependency to flattened DEPS.
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002710
2711 Arguments:
2712 dep (Dependency): dependency to add
2713 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002714 assert dep.name not in self._deps or self._deps.get(
2715 dep.name) == dep, (dep.name, self._deps.get(dep.name))
2716 if dep.url:
2717 self._deps[dep.name] = dep
Paweł Hajdan, Jr11eb7152017-08-10 12:50:11 +02002718
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002719 def _flatten_dep(self, dep):
2720 """Visits a dependency in order to flatten it (see CMDflatten).
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002721
2722 Arguments:
2723 dep (Dependency): dependency to process
2724 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002725 logging.debug('_flatten_dep(%s)', dep.name)
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002726
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002727 assert dep.deps_parsed, (
2728 "Attempted to flatten %s but it has not been processed." % dep.name)
Paweł Hajdan, Jrc69b32e2017-08-17 18:47:48 +02002729
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002730 self._allowed_hosts.update(dep.allowed_hosts)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002731
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002732 # Only include vars explicitly listed in the DEPS files or gclient
2733 # solution, not automatic, local overrides (i.e. not all of
2734 # dep.get_vars()).
2735 hierarchy = dep.hierarchy(include_url=False)
2736 for key, value in dep._vars.items():
2737 # Make sure there are no conflicting variables. It is fine however
2738 # to use same variable name, as long as the value is consistent.
2739 assert key not in self._vars or self._vars[key][1] == value, (
2740 "dep:%s key:%s value:%s != %s" %
2741 (dep.name, key, value, self._vars[key][1]))
2742 self._vars[key] = (hierarchy, value)
2743 # Override explicit custom variables.
2744 for key, value in dep.custom_vars.items():
2745 # Do custom_vars that don't correspond to DEPS vars ever make sense?
2746 # DEPS conditionals shouldn't be using vars that aren't also defined
2747 # in the DEPS (presubmit actually disallows this), so any new
2748 # custom_var must be unused in the DEPS, so no need to add it to the
2749 # flattened output either.
2750 if key not in self._vars:
2751 continue
2752 # Don't "override" existing vars if it's actually the same value.
2753 if self._vars[key][1] == value:
2754 continue
2755 # Anything else is overriding a default value from the DEPS.
2756 self._vars[key] = (hierarchy + ' [custom_var override]', value)
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02002757
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002758 self._pre_deps_hooks.extend([(dep, hook)
2759 for hook in dep.pre_deps_hooks])
2760 self._hooks.extend([(dep, hook) for hook in dep.deps_hooks])
Paweł Hajdan, Jradae2a62017-08-18 16:49:57 +02002761
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002762 for sub_dep in dep.dependencies:
2763 self._add_dep(sub_dep)
Paweł Hajdan, Jrb0ad16e2017-08-03 15:33:21 +02002764
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002765 for d in dep.dependencies:
2766 if d.should_recurse:
2767 self._flatten_dep(d)
Paweł Hajdan, Jraaf93f42017-07-06 17:37:46 +02002768
2769
Joanna Wang3ab2f212023-08-09 01:25:15 +00002770@metrics.collector.collect_metrics('gclient gitmodules')
2771def CMDgitmodules(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002772 """Adds or updates Git Submodules based on the contents of the DEPS file.
Joanna Wang3ab2f212023-08-09 01:25:15 +00002773
2774 This command should be run in the root director of the repo.
2775 It will create or update the .gitmodules file and include
2776 `gclient-condition` values. Commits in gitlinks will also be updated.
2777 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002778 parser.add_option('--output-gitmodules',
2779 help='name of the .gitmodules file to write to',
2780 default='.gitmodules')
2781 parser.add_option(
2782 '--deps-file',
2783 help=
2784 'name of the deps file to parse for git dependency paths and commits.',
2785 default='DEPS')
2786 parser.add_option(
2787 '--skip-dep',
2788 action="append",
2789 help='skip adding gitmodules for the git dependency at the given path',
2790 default=[])
2791 options, args = parser.parse_args(args)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002792
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002793 deps_dir = os.path.dirname(os.path.abspath(options.deps_file))
2794 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
2795 if not gclient_path:
2796 logging.error(
2797 '.gclient not found\n'
2798 'Make sure you are running this script from a gclient workspace.')
2799 sys.exit(1)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002800
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002801 deps_content = gclient_utils.FileRead(options.deps_file)
2802 ls = gclient_eval.Parse(deps_content, options.deps_file, None, None)
Joanna Wang3ab2f212023-08-09 01:25:15 +00002803
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002804 prefix_length = 0
2805 if not 'use_relative_paths' in ls or ls['use_relative_paths'] != True:
2806 delta_path = os.path.relpath(deps_dir, os.path.abspath(gclient_path))
2807 if delta_path:
2808 prefix_length = len(delta_path.replace(os.path.sep, '/')) + 1
Joanna Wang3ab2f212023-08-09 01:25:15 +00002809
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002810 cache_info = []
2811 with open(options.output_gitmodules, 'w', newline='') as f:
2812 for path, dep in ls.get('deps').items():
2813 if path in options.skip_dep:
2814 continue
2815 if dep.get('dep_type') == 'cipd':
2816 continue
2817 try:
2818 url, commit = dep['url'].split('@', maxsplit=1)
2819 except ValueError:
2820 logging.error('error on %s; %s, not adding it', path,
2821 dep["url"])
2822 continue
2823 if prefix_length:
2824 path = path[prefix_length:]
Joanna Wang3ab2f212023-08-09 01:25:15 +00002825
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002826 cache_info += ['--cacheinfo', f'160000,{commit},{path}']
2827 f.write(f'[submodule "{path}"]\n\tpath = {path}\n\turl = {url}\n')
2828 if 'condition' in dep:
2829 f.write(f'\tgclient-condition = {dep["condition"]}\n')
2830 # Windows has limit how long, so let's chunk those calls.
2831 if len(cache_info) >= 100:
2832 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
2833 cache_info = []
2834
2835 if cache_info:
Josip Sokcevic293aa652023-08-23 18:55:20 +00002836 subprocess2.call(['git', 'update-index', '--add'] + cache_info)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002837 subprocess2.call(['git', 'add', '.gitmodules'])
2838 print('.gitmodules and gitlinks updated. Please check git diff and '
2839 'commit changes.')
Joanna Wang3ab2f212023-08-09 01:25:15 +00002840
2841
Edward Lemur3298e7b2018-07-17 18:21:27 +00002842@metrics.collector.collect_metrics('gclient flatten')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002843def CMDflatten(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002844 """Flattens the solutions into a single DEPS file."""
2845 parser.add_option('--output-deps', help='Path to the output DEPS file')
2846 parser.add_option(
2847 '--output-deps-files',
2848 help=('Path to the output metadata about DEPS files referenced by '
2849 'recursedeps.'))
2850 parser.add_option(
2851 '--pin-all-deps',
2852 action='store_true',
2853 help=('Pin all deps, even if not pinned in DEPS. CAVEAT: only does so '
2854 'for checked out deps, NOT deps_os.'))
2855 parser.add_option('--deps-graph-file',
2856 help='Provide a path for the output graph file')
2857 options, args = parser.parse_args(args)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002858
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002859 options.nohooks = True
2860 options.process_all_deps = True
2861 client = GClient.LoadCurrentConfig(options)
2862 if not client:
2863 raise gclient_utils.Error(
2864 'client not configured; see \'gclient config\'')
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002865
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002866 # Only print progress if we're writing to a file. Otherwise, progress
2867 # updates could obscure intended output.
2868 code = client.RunOnDeps('flatten', args, progress=options.output_deps)
2869 if code != 0:
2870 return code
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002871
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002872 flattener = Flattener(client, pin_all_deps=options.pin_all_deps)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002873
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002874 if options.output_deps:
2875 with open(options.output_deps, 'w') as f:
2876 f.write(flattener.deps_string)
2877 else:
2878 print(flattener.deps_string)
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002879
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002880 if options.deps_graph_file:
2881 with open(options.deps_graph_file, 'w') as f:
2882 f.write('\n'.join(flattener.deps_graph_lines))
Joanna Wang9144b672023-02-24 23:36:17 +00002883
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002884 deps_files = [{
2885 'url': d[0],
2886 'deps_file': d[1],
2887 'hierarchy': d[2]
2888 } for d in sorted(flattener.deps_files)]
2889 if options.output_deps_files:
2890 with open(options.output_deps_files, 'w') as f:
2891 json.dump(deps_files, f)
Paweł Hajdan, Jrfeb01642017-09-12 15:50:46 +02002892
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002893 return 0
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002894
2895
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002896def _GNSettingsToLines(gn_args_file, gn_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002897 s = []
2898 if gn_args_file:
2899 s.extend([
2900 'gclient_gn_args_file = "%s"' % gn_args_file,
2901 'gclient_gn_args = %r' % gn_args,
2902 ])
2903 return s
Paweł Hajdan, Jr3c2aa832017-06-07 20:22:16 +02002904
2905
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02002906def _AllowedHostsToLines(allowed_hosts):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002907 """Converts |allowed_hosts| set to list of lines for output."""
2908 if not allowed_hosts:
2909 return []
2910 s = ['allowed_hosts = [']
2911 for h in sorted(allowed_hosts):
2912 s.append(' "%s",' % h)
2913 s.extend([']', ''])
2914 return s
Paweł Hajdan, Jr6014b562017-06-30 17:43:42 +02002915
2916
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002917def _DepsToLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002918 # type: (Mapping[str, Dependency]) -> Sequence[str]
2919 """Converts |deps| dict to list of lines for output."""
2920 if not deps:
2921 return []
2922 s = ['deps = {']
2923 for _, dep in sorted(deps.items()):
2924 s.extend(dep.ToLines())
2925 s.extend(['}', ''])
2926 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002927
2928
Joanna Wang9144b672023-02-24 23:36:17 +00002929def _DepsToDotGraphLines(deps):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002930 # type: (Mapping[str, Dependency]) -> Sequence[str]
2931 """Converts |deps| dict to list of lines for dot graphs"""
2932 if not deps:
2933 return []
2934 graph_lines = ["digraph {\n\trankdir=\"LR\";"]
2935 for _, dep in sorted(deps.items()):
2936 line = dep.hierarchy(include_url=False, graphviz=True)
2937 if line:
2938 graph_lines.append("\t%s" % line)
2939 graph_lines.append("}")
2940 return graph_lines
Joanna Wang9144b672023-02-24 23:36:17 +00002941
2942
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02002943def _DepsOsToLines(deps_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002944 """Converts |deps_os| dict to list of lines for output."""
2945 if not deps_os:
2946 return []
2947 s = ['deps_os = {']
2948 for dep_os, os_deps in sorted(deps_os.items()):
2949 s.append(' "%s": {' % dep_os)
2950 for name, dep in sorted(os_deps.items()):
2951 condition_part = ([' "condition": %r,' %
2952 dep.condition] if dep.condition else [])
2953 s.extend([
2954 ' # %s' % dep.hierarchy(include_url=False),
2955 ' "%s": {' % (name, ),
2956 ' "url": "%s",' % (dep.url, ),
2957 ] + condition_part + [
2958 ' },',
2959 '',
2960 ])
2961 s.extend([' },', ''])
2962 s.extend(['}', ''])
2963 return s
Paweł Hajdan, Jrc9603f52017-06-13 22:14:24 +02002964
2965
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002966def _HooksToLines(name, hooks):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002967 """Converts |hooks| list to list of lines for output."""
2968 if not hooks:
2969 return []
2970 s = ['%s = [' % name]
2971 for dep, hook in hooks:
2972 s.extend([
2973 ' # %s' % dep.hierarchy(include_url=False),
2974 ' {',
2975 ])
2976 if hook.name is not None:
2977 s.append(' "name": "%s",' % hook.name)
2978 if hook.pattern is not None:
2979 s.append(' "pattern": "%s",' % hook.pattern)
2980 if hook.condition is not None:
2981 s.append(' "condition": %r,' % hook.condition)
2982 # Flattened hooks need to be written relative to the root gclient dir
2983 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
2984 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
2985 [' "%s",' % arg
2986 for arg in hook.action] + [' ]', ' },', ''])
2987 s.extend([']', ''])
2988 return s
Paweł Hajdan, Jr064f6f42017-05-18 22:17:55 +02002989
2990
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02002991def _HooksOsToLines(hooks_os):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002992 """Converts |hooks| list to list of lines for output."""
2993 if not hooks_os:
2994 return []
2995 s = ['hooks_os = {']
2996 for hook_os, os_hooks in hooks_os.items():
2997 s.append(' "%s": [' % hook_os)
2998 for dep, hook in os_hooks:
2999 s.extend([
3000 ' # %s' % dep.hierarchy(include_url=False),
3001 ' {',
3002 ])
3003 if hook.name is not None:
3004 s.append(' "name": "%s",' % hook.name)
3005 if hook.pattern is not None:
3006 s.append(' "pattern": "%s",' % hook.pattern)
3007 if hook.condition is not None:
3008 s.append(' "condition": %r,' % hook.condition)
3009 # Flattened hooks need to be written relative to the root gclient
3010 # dir
3011 cwd = os.path.relpath(os.path.normpath(hook.effective_cwd))
3012 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
3013 [' "%s",' % arg
3014 for arg in hook.action] + [' ]', ' },', ''])
3015 s.extend([' ],', ''])
3016 s.extend(['}', ''])
3017 return s
Paweł Hajdan, Jr96e1d782017-06-27 11:12:25 +02003018
3019
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003020def _VarsToLines(variables):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003021 """Converts |variables| dict to list of lines for output."""
3022 if not variables:
3023 return []
3024 s = ['vars = {']
3025 for key, tup in sorted(variables.items()):
3026 hierarchy, value = tup
3027 s.extend([
3028 ' # %s' % hierarchy,
3029 ' "%s": %r,' % (key, value),
3030 '',
3031 ])
3032 s.extend(['}', ''])
3033 return s
Paweł Hajdan, Jrfb022012017-07-06 18:00:08 +02003034
3035
Edward Lemur3298e7b2018-07-17 18:21:27 +00003036@metrics.collector.collect_metrics('gclient grep')
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003037def CMDgrep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003038 """Greps through git repos managed by gclient.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003039
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003040 Runs 'git grep [args...]' for each module.
3041 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003042 # We can't use optparse because it will try to parse arguments sent
3043 # to git grep and throw an error. :-(
3044 if not args or re.match('(-h|--help)$', args[0]):
3045 print(
3046 'Usage: gclient grep [-j <N>] git-grep-args...\n\n'
3047 'Example: "gclient grep -j10 -A2 RefCountedBase" runs\n"git grep '
3048 '-A2 RefCountedBase" on each of gclient\'s git\nrepos with up to '
3049 '10 jobs.\n\nBonus: page output by appending "|& less -FRSX" to the'
3050 ' end of your query.',
3051 file=sys.stderr)
3052 return 1
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003053
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003054 jobs_arg = ['--jobs=1']
3055 if re.match(r'(-j|--jobs=)\d+$', args[0]):
3056 jobs_arg, args = args[:1], args[1:]
3057 elif re.match(r'(-j|--jobs)$', args[0]):
3058 jobs_arg, args = args[:2], args[2:]
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +00003059
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003060 return CMDrecurse(
3061 parser, jobs_arg + [
3062 '--ignore', '--prepend-dir', '--no-progress', '--scm=git', 'git',
3063 'grep', '--null', '--color=Always'
3064 ] + args)
davidbarr@chromium.org12f944e2012-03-01 02:18:31 +00003065
3066
Edward Lemur3298e7b2018-07-17 18:21:27 +00003067@metrics.collector.collect_metrics('gclient root')
stip@chromium.orga735da22015-04-29 23:18:20 +00003068def CMDroot(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003069 """Outputs the solution root (or current dir if there isn't one)."""
3070 (options, args) = parser.parse_args(args)
3071 client = GClient.LoadCurrentConfig(options)
3072 if client:
3073 print(os.path.abspath(client.root_dir))
3074 else:
3075 print(os.path.abspath('.'))
stip@chromium.orga735da22015-04-29 23:18:20 +00003076
3077
agablea98a6cd2016-11-15 14:30:10 -08003078@subcommand.usage('[url]')
Edward Lemur3298e7b2018-07-17 18:21:27 +00003079@metrics.collector.collect_metrics('gclient config')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003080def CMDconfig(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003081 """Creates a .gclient file in the current directory.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003082
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003083 This specifies the configuration for further commands. After update/sync,
3084 top-level DEPS files in each module are read to determine dependent
3085 modules to operate on as well. If optional [url] parameter is
3086 provided, then configuration is read from a specified Subversion server
3087 URL.
3088 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003089 # We do a little dance with the --gclientfile option. 'gclient config' is
3090 # the only command where it's acceptable to have both '--gclientfile' and
3091 # '--spec' arguments. So, we temporarily stash any --gclientfile parameter
3092 # into options.output_config_file until after the (gclientfile xor spec)
3093 # error check.
3094 parser.remove_option('--gclientfile')
3095 parser.add_option('--gclientfile',
3096 dest='output_config_file',
3097 help='Specify an alternate .gclient file')
3098 parser.add_option('--name',
3099 help='overrides the default name for the solution')
3100 parser.add_option(
3101 '--deps-file',
3102 default='DEPS',
3103 help='overrides the default name for the DEPS file for the '
3104 'main solutions and all sub-dependencies')
3105 parser.add_option('--unmanaged',
3106 action='store_true',
3107 default=False,
3108 help='overrides the default behavior to make it possible '
3109 'to have the main solution untouched by gclient '
3110 '(gclient will check out unmanaged dependencies but '
3111 'will never sync them)')
3112 parser.add_option('--cache-dir',
3113 default=UNSET_CACHE_DIR,
3114 help='Cache all git repos into this dir and do shared '
3115 'clones from the cache, instead of cloning directly '
3116 'from the remote. Pass "None" to disable cache, even '
3117 'if globally enabled due to $GIT_CACHE_PATH.')
3118 parser.add_option('--custom-var',
3119 action='append',
3120 dest='custom_vars',
3121 default=[],
3122 help='overrides variables; key=value syntax')
3123 parser.set_defaults(config_filename=None)
3124 (options, args) = parser.parse_args(args)
3125 if options.output_config_file:
3126 setattr(options, 'config_filename',
3127 getattr(options, 'output_config_file'))
3128 if ((options.spec and args) or len(args) > 2
3129 or (not options.spec and not args)):
3130 parser.error(
3131 'Inconsistent arguments. Use either --spec or one or 2 args')
maruel@chromium.org5fc2a332010-05-26 19:37:15 +00003132
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003133 if (options.cache_dir is not UNSET_CACHE_DIR
3134 and options.cache_dir.lower() == 'none'):
3135 options.cache_dir = None
Robert Iannuccia19649b2018-06-29 16:31:45 +00003136
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003137 custom_vars = {}
3138 for arg in options.custom_vars:
3139 kv = arg.split('=', 1)
3140 if len(kv) != 2:
3141 parser.error('Invalid --custom-var argument: %r' % arg)
3142 custom_vars[kv[0]] = gclient_eval.EvaluateCondition(kv[1], {})
Paweł Hajdan, Jr3ba2a7c2017-10-04 19:24:46 +02003143
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003144 client = GClient('.', options)
3145 if options.spec:
3146 client.SetConfig(options.spec)
iposva@chromium.org8cf7a392010-04-07 17:20:26 +00003147 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003148 base_url = args[0].rstrip('/')
3149 if not options.name:
3150 name = base_url.split('/')[-1]
3151 if name.endswith('.git'):
3152 name = name[:-4]
3153 else:
3154 # specify an alternate relpath for the given URL.
3155 name = options.name
3156 if not os.path.abspath(os.path.join(os.getcwd(), name)).startswith(
3157 os.getcwd()):
3158 parser.error('Do not pass a relative path for --name.')
3159 if any(x in ('..', '.', '/', '\\') for x in name.split(os.sep)):
3160 parser.error(
3161 'Do not include relative path components in --name.')
agable@chromium.orgf2214672015-10-27 21:02:48 +00003162
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003163 deps_file = options.deps_file
3164 client.SetDefaultConfig(name,
3165 deps_file,
3166 base_url,
3167 managed=not options.unmanaged,
3168 cache_dir=options.cache_dir,
3169 custom_vars=custom_vars)
3170 client.SaveConfig()
3171 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003172
3173
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003174@subcommand.epilog("""Example:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003175 gclient pack > patch.txt
3176 generate simple patch for configured client and dependences
3177""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003178@metrics.collector.collect_metrics('gclient pack')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003179def CMDpack(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003180 """Generates a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00003181
agabled437d762016-10-17 09:35:11 -07003182 Internally, runs 'git diff' on each checked out module and
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003183 dependencies, and performs minimal postprocessing of the output. The
3184 resulting patch is printed to stdout and can be applied to a freshly
3185 checked out tree via 'patch -p0 < patchfile'.
3186 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003187 parser.add_option('--deps',
3188 dest='deps_os',
3189 metavar='OS_LIST',
3190 help='override deps for the specified (comma-separated) '
3191 'platform(s); \'all\' will process all deps_os '
3192 'references')
3193 parser.remove_option('--jobs')
3194 (options, args) = parser.parse_args(args)
3195 # Force jobs to 1 so the stdout is not annotated with the thread ids
3196 options.jobs = 1
3197 client = GClient.LoadCurrentConfig(options)
3198 if not client:
3199 raise gclient_utils.Error(
3200 'client not configured; see \'gclient config\'')
3201 if options.verbose:
3202 client.PrintLocationAndContents()
3203 return client.RunOnDeps('pack', args)
kbr@google.comab318592009-09-04 00:54:55 +00003204
3205
Edward Lemur3298e7b2018-07-17 18:21:27 +00003206@metrics.collector.collect_metrics('gclient status')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003207def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003208 """Shows modification status for every dependencies."""
3209 parser.add_option('--deps',
3210 dest='deps_os',
3211 metavar='OS_LIST',
3212 help='override deps for the specified (comma-separated) '
3213 'platform(s); \'all\' will process all deps_os '
3214 'references')
3215 (options, args) = parser.parse_args(args)
3216 client = GClient.LoadCurrentConfig(options)
3217 if not client:
3218 raise gclient_utils.Error(
3219 'client not configured; see \'gclient config\'')
3220 if options.verbose:
3221 client.PrintLocationAndContents()
3222 return client.RunOnDeps('status', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003223
3224
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003225@subcommand.epilog("""Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +00003226 gclient sync
3227 update files from SCM according to current configuration,
3228 *for modules which have changed since last update or sync*
3229 gclient sync --force
3230 update files from SCM according to current configuration, for
3231 all modules (useful for recovering files deleted from local copy)
Edward Lesmes3ffca4b2021-05-19 19:36:17 +00003232 gclient sync --revision src@GIT_COMMIT_OR_REF
3233 update src directory to GIT_COMMIT_OR_REF
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003234
3235JSON output format:
3236If the --output-json option is specified, the following document structure will
3237be emitted to the provided file. 'null' entries may occur for subprojects which
3238are present in the gclient solution, but were not processed (due to custom_deps,
3239os_deps, etc.)
3240
3241{
3242 "solutions" : {
3243 "<name>": { # <name> is the posix-normalized path to the solution.
agabled437d762016-10-17 09:35:11 -07003244 "revision": [<git id hex string>|null],
3245 "scm": ["git"|null],
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00003246 }
3247 }
3248}
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003249""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003250@metrics.collector.collect_metrics('gclient sync')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003251def CMDsync(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003252 """Checkout/update all modules."""
3253 parser.add_option('-f',
3254 '--force',
3255 action='store_true',
3256 help='force update even for unchanged modules')
3257 parser.add_option('-n',
3258 '--nohooks',
3259 action='store_true',
3260 help='don\'t run hooks after the update is complete')
3261 parser.add_option('-p',
3262 '--noprehooks',
3263 action='store_true',
3264 help='don\'t run pre-DEPS hooks',
3265 default=False)
3266 parser.add_option('-r',
3267 '--revision',
3268 action='append',
3269 dest='revisions',
3270 metavar='REV',
3271 default=[],
3272 help='Enforces git ref/hash for the solutions with the '
3273 'format src@rev. The src@ part is optional and can be '
3274 'skipped. You can also specify URLs instead of paths '
3275 'and gclient will find the solution corresponding to '
3276 'the given URL. If a path is also specified, the URL '
3277 'takes precedence. -r can be used multiple times when '
3278 '.gclient has multiple solutions configured, and will '
3279 'work even if the src@ part is skipped. Revision '
3280 'numbers (e.g. 31000 or r31000) are not supported.')
3281 parser.add_option('--patch-ref',
3282 action='append',
3283 dest='patch_refs',
3284 metavar='GERRIT_REF',
3285 default=[],
3286 help='Patches the given reference with the format '
3287 'dep@target-ref:patch-ref. '
3288 'For |dep|, you can specify URLs as well as paths, '
3289 'with URLs taking preference. '
3290 '|patch-ref| will be applied to |dep|, rebased on top '
3291 'of what |dep| was synced to, and a soft reset will '
3292 'be done. Use --no-rebase-patch-ref and '
3293 '--no-reset-patch-ref to disable this behavior. '
3294 '|target-ref| is the target branch against which a '
3295 'patch was created, it is used to determine which '
3296 'commits from the |patch-ref| actually constitute a '
3297 'patch.')
3298 parser.add_option(
3299 '-t',
3300 '--download-topics',
3301 action='store_true',
3302 help='Downloads and patches locally changes from all open '
3303 'Gerrit CLs that have the same topic as the changes '
3304 'in the specified patch_refs. Only works if atleast '
3305 'one --patch-ref is specified.')
3306 parser.add_option('--with_branch_heads',
3307 action='store_true',
3308 help='Clone git "branch_heads" refspecs in addition to '
3309 'the default refspecs. This adds about 1/2GB to a '
3310 'full checkout. (git only)')
3311 parser.add_option(
3312 '--with_tags',
3313 action='store_true',
3314 help='Clone git tags in addition to the default refspecs.')
3315 parser.add_option('-H',
3316 '--head',
3317 action='store_true',
3318 help='DEPRECATED: only made sense with safesync urls.')
3319 parser.add_option(
3320 '-D',
3321 '--delete_unversioned_trees',
3322 action='store_true',
3323 help='Deletes from the working copy any dependencies that '
3324 'have been removed since the last sync, as long as '
3325 'there are no local modifications. When used with '
3326 '--force, such dependencies are removed even if they '
3327 'have local modifications. When used with --reset, '
3328 'all untracked directories are removed from the '
3329 'working copy, excluding those which are explicitly '
3330 'ignored in the repository.')
3331 parser.add_option(
3332 '-R',
3333 '--reset',
3334 action='store_true',
3335 help='resets any local changes before updating (git only)')
3336 parser.add_option('-M',
3337 '--merge',
3338 action='store_true',
3339 help='merge upstream changes instead of trying to '
3340 'fast-forward or rebase')
3341 parser.add_option('-A',
3342 '--auto_rebase',
3343 action='store_true',
3344 help='Automatically rebase repositories against local '
3345 'checkout during update (git only).')
3346 parser.add_option('--deps',
3347 dest='deps_os',
3348 metavar='OS_LIST',
3349 help='override deps for the specified (comma-separated) '
3350 'platform(s); \'all\' will process all deps_os '
3351 'references')
3352 parser.add_option('--process-all-deps',
3353 action='store_true',
3354 help='Check out all deps, even for different OS-es, '
3355 'or with conditions evaluating to false')
3356 parser.add_option('--upstream',
3357 action='store_true',
3358 help='Make repo state match upstream branch.')
3359 parser.add_option('--output-json',
3360 help='Output a json document to this path containing '
3361 'summary information about the sync.')
3362 parser.add_option(
3363 '--no-history',
3364 action='store_true',
3365 help='GIT ONLY - Reduces the size/time of the checkout at '
3366 'the cost of no history. Requires Git 1.9+')
3367 parser.add_option('--shallow',
3368 action='store_true',
3369 help='GIT ONLY - Do a shallow clone into the cache dir. '
3370 'Requires Git 1.9+')
3371 parser.add_option('--no_bootstrap',
3372 '--no-bootstrap',
3373 action='store_true',
3374 help='Don\'t bootstrap from Google Storage.')
3375 parser.add_option('--ignore_locks',
3376 action='store_true',
3377 help='No longer used.')
3378 parser.add_option('--break_repo_locks',
3379 action='store_true',
3380 help='No longer used.')
3381 parser.add_option('--lock_timeout',
3382 type='int',
3383 default=5000,
3384 help='GIT ONLY - Deadline (in seconds) to wait for git '
3385 'cache lock to become available. Default is %default.')
3386 parser.add_option('--no-rebase-patch-ref',
3387 action='store_false',
3388 dest='rebase_patch_ref',
3389 default=True,
3390 help='Bypass rebase of the patch ref after checkout.')
3391 parser.add_option('--no-reset-patch-ref',
3392 action='store_false',
3393 dest='reset_patch_ref',
3394 default=True,
3395 help='Bypass calling reset after patching the ref.')
3396 parser.add_option('--experiment',
3397 action='append',
3398 dest='experiments',
3399 default=[],
3400 help='Which experiments should be enabled.')
3401 (options, args) = parser.parse_args(args)
3402 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003403
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003404 if not client:
3405 raise gclient_utils.Error(
3406 'client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003407
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003408 if options.download_topics and not options.rebase_patch_ref:
3409 raise gclient_utils.Error(
3410 'Warning: You cannot download topics and not rebase each patch ref')
Ravi Mistryecda7822022-02-28 16:22:20 +00003411
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003412 if options.ignore_locks:
3413 print(
3414 'Warning: ignore_locks is no longer used. Please remove its usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003416 if options.break_repo_locks:
3417 print('Warning: break_repo_locks is no longer used. Please remove its '
3418 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003419
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003420 if options.revisions and options.head:
3421 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
3422 print('Warning: you cannot use both --head and --revision')
smutae7ea312016-07-18 11:59:41 -07003423
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003424 if options.verbose:
3425 client.PrintLocationAndContents()
3426 ret = client.RunOnDeps('update', args)
3427 if options.output_json:
3428 slns = {}
3429 for d in client.subtree(True):
3430 normed = d.name.replace('\\', '/').rstrip('/') + '/'
3431 slns[normed] = {
3432 'revision': d.got_revision,
3433 'scm': d.used_scm.name if d.used_scm else None,
3434 'url': str(d.url) if d.url else None,
3435 'was_processed': d.should_process,
3436 'was_synced': d._should_sync,
3437 }
3438 with open(options.output_json, 'w') as f:
3439 json.dump({'solutions': slns}, f)
3440 return ret
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003441
3442
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003443CMDupdate = CMDsync
3444
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003445
Edward Lemur3298e7b2018-07-17 18:21:27 +00003446@metrics.collector.collect_metrics('gclient validate')
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003447def CMDvalidate(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003448 """Validates the .gclient and DEPS syntax."""
3449 options, args = parser.parse_args(args)
3450 client = GClient.LoadCurrentConfig(options)
3451 if not client:
3452 raise gclient_utils.Error(
3453 'client not configured; see \'gclient config\'')
3454 rv = client.RunOnDeps('validate', args)
3455 if rv == 0:
3456 print('validate: SUCCESS')
3457 else:
3458 print('validate: FAILURE')
3459 return rv
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02003460
3461
Edward Lemur3298e7b2018-07-17 18:21:27 +00003462@metrics.collector.collect_metrics('gclient diff')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003463def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003464 """Displays local diff for every dependencies."""
3465 parser.add_option('--deps',
3466 dest='deps_os',
3467 metavar='OS_LIST',
3468 help='override deps for the specified (comma-separated) '
3469 'platform(s); \'all\' will process all deps_os '
3470 'references')
3471 (options, args) = parser.parse_args(args)
3472 client = GClient.LoadCurrentConfig(options)
3473 if not client:
3474 raise gclient_utils.Error(
3475 'client not configured; see \'gclient config\'')
3476 if options.verbose:
3477 client.PrintLocationAndContents()
3478 return client.RunOnDeps('diff', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003479
3480
Edward Lemur3298e7b2018-07-17 18:21:27 +00003481@metrics.collector.collect_metrics('gclient revert')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003482def CMDrevert(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003483 """Reverts all modifications in every dependencies.
maruel@chromium.org28d14bd2010-11-11 20:37:09 +00003484
3485 That's the nuclear option to get back to a 'clean' state. It removes anything
agabled437d762016-10-17 09:35:11 -07003486 that shows up in git status."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003487 parser.add_option('--deps',
3488 dest='deps_os',
3489 metavar='OS_LIST',
3490 help='override deps for the specified (comma-separated) '
3491 'platform(s); \'all\' will process all deps_os '
3492 'references')
3493 parser.add_option('-n',
3494 '--nohooks',
3495 action='store_true',
3496 help='don\'t run hooks after the revert is complete')
3497 parser.add_option('-p',
3498 '--noprehooks',
3499 action='store_true',
3500 help='don\'t run pre-DEPS hooks',
3501 default=False)
3502 parser.add_option('--upstream',
3503 action='store_true',
3504 help='Make repo state match upstream branch.')
3505 parser.add_option('--break_repo_locks',
3506 action='store_true',
3507 help='No longer used.')
3508 (options, args) = parser.parse_args(args)
3509 if options.break_repo_locks:
3510 print(
3511 'Warning: break_repo_locks is no longer used. Please remove its ' +
3512 'usage.')
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00003513
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003514 # --force is implied.
3515 options.force = True
3516 options.reset = False
3517 options.delete_unversioned_trees = False
3518 options.merge = False
3519 client = GClient.LoadCurrentConfig(options)
3520 if not client:
3521 raise gclient_utils.Error(
3522 'client not configured; see \'gclient config\'')
3523 return client.RunOnDeps('revert', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003524
3525
Edward Lemur3298e7b2018-07-17 18:21:27 +00003526@metrics.collector.collect_metrics('gclient runhooks')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003527def CMDrunhooks(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003528 """Runs hooks for files that have been modified in the local working copy."""
3529 parser.add_option('--deps',
3530 dest='deps_os',
3531 metavar='OS_LIST',
3532 help='override deps for the specified (comma-separated) '
3533 'platform(s); \'all\' will process all deps_os '
3534 'references')
3535 parser.add_option('-f',
3536 '--force',
3537 action='store_true',
3538 default=True,
3539 help='Deprecated. No effect.')
3540 (options, args) = parser.parse_args(args)
3541 client = GClient.LoadCurrentConfig(options)
3542 if not client:
3543 raise gclient_utils.Error(
3544 'client not configured; see \'gclient config\'')
3545 if options.verbose:
3546 client.PrintLocationAndContents()
3547 options.force = True
3548 options.nohooks = False
3549 return client.RunOnDeps('runhooks', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003550
3551
Edward Lemur3298e7b2018-07-17 18:21:27 +00003552@metrics.collector.collect_metrics('gclient revinfo')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00003553def CMDrevinfo(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003554 """Outputs revision info mapping for the client and its dependencies.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003555
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00003556 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003557 can be used to reproduce the same tree in the future. It is only useful for
agabled437d762016-10-17 09:35:11 -07003558 'unpinned dependencies', i.e. DEPS/deps references without a git hash.
3559 A git branch name isn't 'pinned' since the actual commit can change.
maruel@chromium.org9eda4112010-06-11 18:56:10 +00003560 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003561 parser.add_option('--deps',
3562 dest='deps_os',
3563 metavar='OS_LIST',
3564 help='override deps for the specified (comma-separated) '
3565 'platform(s); \'all\' will process all deps_os '
3566 'references')
3567 parser.add_option(
3568 '-a',
3569 '--actual',
3570 action='store_true',
3571 help='gets the actual checked out revisions instead of the '
3572 'ones specified in the DEPS and .gclient files')
3573 parser.add_option('-s',
3574 '--snapshot',
3575 action='store_true',
3576 help='creates a snapshot .gclient file of the current '
3577 'version of all repositories to reproduce the tree, '
3578 'implies -a')
3579 parser.add_option(
3580 '--filter',
3581 action='append',
3582 dest='filter',
3583 help='Display revision information only for the specified '
3584 'dependencies (filtered by URL or path).')
3585 parser.add_option('--output-json',
3586 help='Output a json document to this path containing '
3587 'information about the revisions.')
3588 parser.add_option(
3589 '--ignore-dep-type',
3590 choices=['git', 'cipd'],
3591 help='Specify to skip processing of a certain type of dep.')
3592 (options, args) = parser.parse_args(args)
3593 client = GClient.LoadCurrentConfig(options)
3594 if not client:
3595 raise gclient_utils.Error(
3596 'client not configured; see \'gclient config\'')
3597 client.PrintRevInfo()
3598 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003599
3600
Edward Lemur3298e7b2018-07-17 18:21:27 +00003601@metrics.collector.collect_metrics('gclient getdep')
Edward Lesmes411041f2018-04-05 20:12:55 -04003602def CMDgetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003603 """Gets revision information and variable values from a DEPS file.
Josip Sokcevic7b5e3d72023-06-13 00:28:23 +00003604
3605 If key doesn't exist or is incorrectly declared, this script exits with exit
3606 code 2."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003607 parser.add_option('--var',
3608 action='append',
3609 dest='vars',
3610 metavar='VAR',
3611 default=[],
3612 help='Gets the value of a given variable.')
3613 parser.add_option(
3614 '-r',
3615 '--revision',
3616 action='append',
3617 dest='getdep_revisions',
3618 metavar='DEP',
3619 default=[],
3620 help='Gets the revision/version for the given dependency. '
3621 'If it is a git dependency, dep must be a path. If it '
3622 'is a CIPD dependency, dep must be of the form '
3623 'path:package.')
3624 parser.add_option(
3625 '--deps-file',
3626 default='DEPS',
3627 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3628 # .gclient first.
3629 help='The DEPS file to be edited. Defaults to the DEPS '
3630 'file in the current directory.')
3631 (options, args) = parser.parse_args(args)
Edward Lesmes411041f2018-04-05 20:12:55 -04003632
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003633 if not os.path.isfile(options.deps_file):
3634 raise gclient_utils.Error('DEPS file %s does not exist.' %
3635 options.deps_file)
3636 with open(options.deps_file) as f:
3637 contents = f.read()
3638 client = GClient.LoadCurrentConfig(options)
3639 if client is not None:
3640 builtin_vars = client.get_builtin_vars()
Edward Lesmes411041f2018-04-05 20:12:55 -04003641 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003642 logging.warning(
3643 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3644 'file without support for built-in variables.')
3645 builtin_vars = None
3646 local_scope = gclient_eval.Exec(contents,
3647 options.deps_file,
3648 builtin_vars=builtin_vars)
3649
3650 for var in options.vars:
3651 print(gclient_eval.GetVar(local_scope, var))
3652
3653 commits = {}
3654 if local_scope.get(
3655 'git_dependencies'
3656 ) == gclient_eval.SUBMODULES and options.getdep_revisions:
3657 commits.update(
3658 scm_git.GIT.GetSubmoduleCommits(
3659 os.getcwd(),
3660 [path for path in options.getdep_revisions if ':' not in path]))
3661
3662 for name in options.getdep_revisions:
3663 if ':' in name:
3664 name, _, package = name.partition(':')
3665 if not name or not package:
3666 parser.error(
3667 'Wrong CIPD format: %s:%s should be of the form path:pkg.' %
3668 (name, package))
3669 print(gclient_eval.GetCIPD(local_scope, name, package))
3670 elif commits:
3671 print(commits[name])
3672 else:
3673 try:
3674 print(gclient_eval.GetRevision(local_scope, name))
3675 except KeyError as e:
3676 print(repr(e), file=sys.stderr)
3677 sys.exit(2)
Edward Lesmes411041f2018-04-05 20:12:55 -04003678
3679
Edward Lemur3298e7b2018-07-17 18:21:27 +00003680@metrics.collector.collect_metrics('gclient setdep')
Edward Lesmes6f64a052018-03-20 17:35:49 -04003681def CMDsetdep(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003682 """Modifies dependency revisions and variable values in a DEPS file"""
3683 parser.add_option('--var',
3684 action='append',
3685 dest='vars',
3686 metavar='VAR=VAL',
3687 default=[],
3688 help='Sets a variable to the given value with the format '
3689 'name=value.')
3690 parser.add_option('-r',
3691 '--revision',
3692 action='append',
3693 dest='setdep_revisions',
3694 metavar='DEP@REV',
3695 default=[],
3696 help='Sets the revision/version for the dependency with '
3697 'the format dep@rev. If it is a git dependency, dep '
3698 'must be a path and rev must be a git hash or '
3699 'reference (e.g. src/dep@deadbeef). If it is a CIPD '
3700 'dependency, dep must be of the form path:package and '
3701 'rev must be the package version '
3702 '(e.g. src/pkg:chromium/pkg@2.1-cr0).')
3703 parser.add_option(
3704 '--deps-file',
3705 default='DEPS',
3706 # TODO(ehmaldonado): Try to find the DEPS file pointed by
3707 # .gclient first.
3708 help='The DEPS file to be edited. Defaults to the DEPS '
3709 'file in the current directory.')
3710 (options, args) = parser.parse_args(args)
3711 if args:
3712 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3713 if not options.setdep_revisions and not options.vars:
Edward Lesmes0ecf6d62018-04-05 18:28:55 -04003714 parser.error(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003715 'You must specify at least one variable or revision to modify.')
3716
3717 if not os.path.isfile(options.deps_file):
3718 raise gclient_utils.Error('DEPS file %s does not exist.' %
3719 options.deps_file)
3720 with open(options.deps_file) as f:
3721 contents = f.read()
3722
3723 client = GClient.LoadCurrentConfig(options)
3724 if client is not None:
3725 builtin_vars = client.get_builtin_vars()
Edward Lesmes6f64a052018-03-20 17:35:49 -04003726 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003727 logging.warning(
3728 'Couldn\'t find a valid gclient config. Will attempt to parse the DEPS '
3729 'file without support for built-in variables.')
3730 builtin_vars = None
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003731
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003732 local_scope = gclient_eval.Exec(contents,
3733 options.deps_file,
3734 builtin_vars=builtin_vars)
Aravind Vasudevancb8023d2023-07-07 00:03:45 +00003735
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003736 # Create a set of all git submodules.
3737 cwd = os.path.dirname(options.deps_file) or os.getcwd()
3738 git_modules = None
3739 if 'git_dependencies' in local_scope and local_scope[
3740 'git_dependencies'] in (gclient_eval.SUBMODULES, gclient_eval.SYNC):
3741 try:
3742 submodule_status = subprocess2.check_output(
3743 ['git', 'submodule', 'status'], cwd=cwd).decode('utf-8')
3744 git_modules = {l.split()[1] for l in submodule_status.splitlines()}
3745 except subprocess2.CalledProcessError as e:
3746 print('Warning: gitlinks won\'t be updated: ', e)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003747
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003748 for var in options.vars:
3749 name, _, value = var.partition('=')
3750 if not name or not value:
3751 parser.error(
3752 'Wrong var format: %s should be of the form name=value.' % var)
3753 if name in local_scope['vars']:
3754 gclient_eval.SetVar(local_scope, name, value)
3755 else:
3756 gclient_eval.AddVar(local_scope, name, value)
Edward Lesmes6f64a052018-03-20 17:35:49 -04003757
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003758 for revision in options.setdep_revisions:
3759 name, _, value = revision.partition('@')
3760 if not name or not value:
3761 parser.error('Wrong dep format: %s should be of the form dep@rev.' %
3762 revision)
3763 if ':' in name:
3764 name, _, package = name.partition(':')
3765 if not name or not package:
3766 parser.error(
3767 'Wrong CIPD format: %s:%s should be of the form path:pkg@version.'
3768 % (name, package))
3769 gclient_eval.SetCIPD(local_scope, name, package, value)
3770 else:
3771 # Update DEPS only when `git_dependencies` == DEPS or SYNC.
3772 # git_dependencies is defaulted to DEPS when not set.
3773 if 'git_dependencies' not in local_scope or local_scope[
3774 'git_dependencies'] in (gclient_eval.DEPS,
3775 gclient_eval.SYNC):
3776 gclient_eval.SetRevision(local_scope, name, value)
3777
3778 # Update git submodules when `git_dependencies` == SYNC or
3779 # SUBMODULES.
3780 if git_modules and 'git_dependencies' in local_scope and local_scope[
3781 'git_dependencies'] in (gclient_eval.SUBMODULES,
3782 gclient_eval.SYNC):
3783 git_module_name = name
3784 if not 'use_relative_paths' in local_scope or \
3785 local_scope['use_relative_paths'] != True:
3786 deps_dir = os.path.dirname(
3787 os.path.abspath(options.deps_file))
3788 gclient_path = gclient_paths.FindGclientRoot(deps_dir)
3789 delta_path = None
3790 if gclient_path:
3791 delta_path = os.path.relpath(
3792 deps_dir, os.path.abspath(gclient_path))
3793 if delta_path:
3794 prefix_length = len(delta_path.replace(
3795 os.path.sep, '/')) + 1
3796 git_module_name = name[prefix_length:]
3797 # gclient setdep should update the revision, i.e., the gitlink
3798 # only when the submodule entry is already present within
3799 # .gitmodules.
3800 if git_module_name not in git_modules:
3801 raise KeyError(
3802 f'Could not find any dependency called "{git_module_name}" in '
3803 f'.gitmodules.')
3804
3805 # Update the gitlink for the submodule.
3806 subprocess2.call([
3807 'git', 'update-index', '--add', '--cacheinfo',
3808 f'160000,{value},{git_module_name}'
3809 ],
3810 cwd=cwd)
3811
3812 with open(options.deps_file, 'wb') as f:
3813 f.write(gclient_eval.RenderDEPSFile(local_scope).encode('utf-8'))
3814
3815 if git_modules:
3816 subprocess2.call(['git', 'add', options.deps_file], cwd=cwd)
3817 print('Changes have been staged. See changes with `git status`.\n'
3818 'Use `git commit -m "Manual roll"` to commit your changes. \n'
3819 'Run gclient sync to update your local dependency checkout.')
Josip Sokcevic5561f8b2023-08-21 16:00:42 +00003820
Edward Lesmes6f64a052018-03-20 17:35:49 -04003821
Edward Lemur3298e7b2018-07-17 18:21:27 +00003822@metrics.collector.collect_metrics('gclient verify')
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003823def CMDverify(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003824 """Verifies the DEPS file deps are only from allowed_hosts."""
3825 (options, args) = parser.parse_args(args)
3826 client = GClient.LoadCurrentConfig(options)
3827 if not client:
3828 raise gclient_utils.Error(
3829 'client not configured; see \'gclient config\'')
3830 client.RunOnDeps(None, [])
3831 # Look at each first-level dependency of this gclient only.
3832 for dep in client.dependencies:
3833 bad_deps = dep.findDepsFromNotAllowedHosts()
3834 if not bad_deps:
3835 continue
3836 print("There are deps from not allowed hosts in file %s" %
3837 dep.deps_file)
3838 for bad_dep in bad_deps:
3839 print("\t%s at %s" % (bad_dep.name, bad_dep.url))
3840 print("allowed_hosts:", ', '.join(dep.allowed_hosts))
3841 sys.stdout.flush()
3842 raise gclient_utils.Error(
3843 'dependencies from disallowed hosts; check your DEPS file.')
3844 return 0
tandrii@chromium.orgc137c1a2014-09-23 11:49:52 +00003845
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003846
3847@subcommand.epilog("""For more information on what metrics are we collecting and
Edward Lemur8a2e3312018-07-12 21:15:09 +00003848why, please read metrics.README.md or visit https://bit.ly/2ufRS4p""")
Edward Lemur3298e7b2018-07-17 18:21:27 +00003849@metrics.collector.collect_metrics('gclient metrics')
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003850def CMDmetrics(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003851 """Reports, and optionally modifies, the status of metric collection."""
3852 parser.add_option('--opt-in',
3853 action='store_true',
3854 dest='enable_metrics',
3855 help='Opt-in to metrics collection.',
3856 default=None)
3857 parser.add_option('--opt-out',
3858 action='store_false',
3859 dest='enable_metrics',
3860 help='Opt-out of metrics collection.')
3861 options, args = parser.parse_args(args)
3862 if args:
3863 parser.error('Unused arguments: "%s"' % '" "'.join(args))
3864 if not metrics.collector.config.is_googler:
3865 print("You're not a Googler. Metrics collection is disabled for you.")
3866 return 0
3867
3868 if options.enable_metrics is not None:
3869 metrics.collector.config.opted_in = options.enable_metrics
3870
3871 if metrics.collector.config.opted_in is None:
3872 print("You haven't opted in or out of metrics collection.")
3873 elif metrics.collector.config.opted_in:
3874 print("You have opted in. Thanks!")
3875 else:
3876 print("You have opted out. Please consider opting in.")
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003877 return 0
3878
Edward Lemur32e3d1e2018-07-12 00:54:05 +00003879
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003880class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003881 gclientfile_default = os.environ.get('GCLIENT_FILE', '.gclient')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003882
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003883 def __init__(self, **kwargs):
3884 optparse.OptionParser.__init__(self,
3885 version='%prog ' + __version__,
3886 **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003887
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003888 # Some arm boards have issues with parallel sync.
3889 if platform.machine().startswith('arm'):
3890 jobs = 1
3891 else:
3892 jobs = max(8, gclient_utils.NumLocalCpus())
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003893
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003894 self.add_option(
3895 '-j',
3896 '--jobs',
3897 default=jobs,
3898 type='int',
3899 help='Specify how many SCM commands can run in parallel; defaults to '
3900 '%default on this machine')
3901 self.add_option(
3902 '-v',
3903 '--verbose',
3904 action='count',
3905 default=0,
3906 help='Produces additional output for diagnostics. Can be used up to '
3907 'three times for more logging info.')
3908 self.add_option('--gclientfile',
3909 dest='config_filename',
3910 help='Specify an alternate %s file' %
3911 self.gclientfile_default)
3912 self.add_option(
3913 '--spec',
3914 help='create a gclient file containing the provided string. Due to '
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003915 'Cygwin/Python brokenness, it can\'t contain any newlines.')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003916 self.add_option('--no-nag-max',
3917 default=False,
3918 action='store_true',
3919 help='Ignored for backwards compatibility.')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003920
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003921 def parse_args(self, args=None, _values=None):
3922 """Integrates standard options processing."""
3923 # Create an optparse.Values object that will store only the actual
3924 # passed options, without the defaults.
3925 actual_options = optparse.Values()
3926 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
3927 # Create an optparse.Values object with the default options.
3928 options = optparse.Values(self.get_default_values().__dict__)
3929 # Update it with the options passed by the user.
3930 options._update_careful(actual_options.__dict__)
3931 # Store the options passed by the user in an _actual_options attribute.
3932 # We store only the keys, and not the values, since the values can
3933 # contain arbitrary information, which might be PII.
3934 metrics.collector.add('arguments', list(actual_options.__dict__))
Edward Lemur3298e7b2018-07-17 18:21:27 +00003935
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003936 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
3937 logging.basicConfig(
3938 level=levels[min(options.verbose,
3939 len(levels) - 1)],
3940 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
3941 if options.config_filename and options.spec:
3942 self.error('Cannot specify both --gclientfile and --spec')
3943 if (options.config_filename and options.config_filename !=
3944 os.path.basename(options.config_filename)):
3945 self.error('--gclientfile target must be a filename, not a path')
3946 if not options.config_filename:
3947 options.config_filename = self.gclientfile_default
3948 options.entries_filename = options.config_filename + '_entries'
3949 if options.jobs < 1:
3950 self.error('--jobs must be 1 or higher')
maruel@chromium.org0895b752011-08-26 20:40:33 +00003951
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003952 # These hacks need to die.
3953 if not hasattr(options, 'revisions'):
3954 # GClient.RunOnDeps expects it even if not applicable.
3955 options.revisions = []
3956 if not hasattr(options, 'experiments'):
3957 options.experiments = []
3958 if not hasattr(options, 'head'):
3959 options.head = None
3960 if not hasattr(options, 'nohooks'):
3961 options.nohooks = True
3962 if not hasattr(options, 'noprehooks'):
3963 options.noprehooks = True
3964 if not hasattr(options, 'deps_os'):
3965 options.deps_os = None
3966 if not hasattr(options, 'force'):
3967 options.force = None
3968 return (options, args)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003969
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003970
3971def disable_buffering():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003972 # Make stdout auto-flush so buildbot doesn't kill us during lengthy
3973 # operations. Python as a strong tendency to buffer sys.stdout.
3974 sys.stdout = gclient_utils.MakeFileAutoFlush(sys.stdout)
3975 # Make stdout annotated with the thread ids.
3976 sys.stdout = gclient_utils.MakeFileAnnotated(sys.stdout)
maruel@chromium.org0895b752011-08-26 20:40:33 +00003977
3978
Elly Fong-Jones7b294392019-04-18 18:32:10 +00003979def path_contains_tilde():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003980 for element in os.environ['PATH'].split(os.pathsep):
3981 if element.startswith('~') and os.path.abspath(
3982 os.path.realpath(
3983 os.path.expanduser(element))) == DEPOT_TOOLS_DIR:
3984 return True
3985 return False
Elly Fong-Jones7b294392019-04-18 18:32:10 +00003986
3987
3988def can_run_gclient_and_helpers():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003989 if sys.hexversion < 0x02060000:
3990 print('\nYour python version %s is unsupported, please upgrade.\n' %
3991 sys.version.split(' ', 1)[0],
3992 file=sys.stderr)
3993 return False
3994 if not sys.executable:
3995 print('\nPython cannot find the location of it\'s own executable.\n',
3996 file=sys.stderr)
3997 return False
3998 if path_contains_tilde():
3999 print(
4000 '\nYour PATH contains a literal "~", which works in some shells ' +
4001 'but will break when python tries to run subprocesses. ' +
4002 'Replace the "~" with $HOME.\n' + 'See https://crbug.com/952865.\n',
4003 file=sys.stderr)
4004 return False
4005 return True
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004006
4007
4008def main(argv):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004009 """Doesn't parse the arguments here, just find the right subcommand to
Elly Fong-Jones7b294392019-04-18 18:32:10 +00004010 execute."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004011 if not can_run_gclient_and_helpers():
4012 return 2
4013 fix_encoding.fix_encoding()
4014 disable_buffering()
4015 setup_color.init()
4016 dispatcher = subcommand.CommandDispatcher(__name__)
4017 try:
4018 return dispatcher.execute(OptionParser(), argv)
4019 except KeyboardInterrupt:
4020 gclient_utils.GClientChildren.KillAllRemainingChildren()
4021 raise
4022 except (gclient_utils.Error, subprocess2.CalledProcessError) as e:
4023 print('Error: %s' % str(e), file=sys.stderr)
4024 return 1
4025 finally:
4026 gclient_utils.PrintWarnings()
4027 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004028
4029
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00004030if '__main__' == __name__:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004031 with metrics.collector.print_notice_and_exit():
4032 sys.exit(main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00004033
4034# vim: ts=2:sw=2:tw=80:et: