blob: 737d5557b33104e4d2a73d9ada2574748a8104e2 [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08002# Copyright 2018 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Gclient utility."""
6
7from __future__ import print_function
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +08008import collections
9import itertools
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080010import logging
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080011import operator
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080012import os
Kuang-che Wu6948ecc2018-09-11 17:43:49 +080013import pprint
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +080014import queue
Kuang-che Wu067ff292019-02-14 18:16:23 +080015import shutil
Kuang-che Wub17b3b92018-09-04 18:12:11 +080016import sys
Kuang-che Wu1e49f512018-12-06 15:27:42 +080017import tempfile
Kuang-che Wu999893c2020-04-13 22:06:22 +080018import urllib.parse
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +080019
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +080020# from third_party
21from depot_tools import gclient_eval
22
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080023from bisect_kit import codechange
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080024from bisect_kit import git_util
Kuang-che Wu1e49f512018-12-06 15:27:42 +080025from bisect_kit import locking
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080026from bisect_kit import util
27
28logger = logging.getLogger(__name__)
Kuang-che Wuced2dbf2019-01-30 23:13:24 +080029emitted_warnings = set()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080030
Kuang-che Wu88e96312020-10-20 16:21:11 +080031# If the dependency is not pinned in DEPS file, the default branch.
32# ref: gclient_scm.py GitWrapper.update default_rev
33# TODO(kcwu): follow gclient to change the default branch name
34DEFAULT_BRANCH_NAME = 'master'
35
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080036
Kuang-che Wu41e8b592018-09-25 17:01:30 +080037def config(gclient_dir,
38 url=None,
39 cache_dir=None,
40 deps_file=None,
41 custom_var=None,
Kuang-che Wucb8a80e2021-01-18 15:22:27 +080042 gclientfile=None,
43 spec=None,
44 target_os=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080045 """Simply wrapper of `gclient config`.
46
47 Args:
48 gclient_dir: root directory of gclient project
49 url: URL of gclient configuration files
50 cache_dir: gclient's git cache folder
51 deps_file: override the default DEPS file name
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080052 custom_var: custom variables
Kuang-che Wucb8a80e2021-01-18 15:22:27 +080053 gclientfile: alternate .gclient file name
Kuang-che Wu41e8b592018-09-25 17:01:30 +080054 spec: content of gclient file
Kuang-che Wucb8a80e2021-01-18 15:22:27 +080055 target_os: target OS
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080056 """
57 cmd = ['gclient', 'config']
58 if deps_file:
59 cmd += ['--deps-file', deps_file]
60 if cache_dir:
61 cmd += ['--cache-dir', cache_dir]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080062 if custom_var:
63 cmd += ['--custom-var', custom_var]
Kuang-che Wucb8a80e2021-01-18 15:22:27 +080064 if gclientfile:
65 cmd += ['--gclientfile', gclientfile]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080066 if spec:
67 cmd += ['--spec', spec]
68 if url:
69 cmd.append(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080070
71 util.check_call(*cmd, cwd=gclient_dir)
72
Kuang-che Wucb8a80e2021-01-18 15:22:27 +080073 # 'target_os' is mandatory for chromeos build, but 'gclient config' doesn't
74 # recognize it. Here add it to gclient file explicitly.
75 if target_os:
76 if not gclientfile:
77 gclientfile = '.gclient'
78 with open(os.path.join(gclient_dir, gclientfile), 'a') as f:
79 f.write('target_os = ["%s"]\n' % target_os)
80
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080081
Kuang-che Wu6ee24b52020-11-02 11:33:49 +080082def sync(gclient_dir, with_branch_heads=False, with_tags=False, jobs=8):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080083 """Simply wrapper of `gclient sync`.
84
85 Args:
86 gclient_dir: root directory of gclient project
87 with_branch_heads: whether to clone git `branch_heads` refspecs
88 with_tags: whether to clone git tags
89 jobs: how many workers running in parallel
90 """
Kuang-che Wua9639eb2019-03-19 17:15:08 +080091 # Work around gclient issue crbug/943430
92 # gclient rejected to sync if there are untracked symlink even with --force
93 for path in [
94 'src/chromeos/assistant/libassistant/src/deps',
95 'src/chromeos/assistant/libassistant/src/libassistant',
96 ]:
97 if os.path.islink(os.path.join(gclient_dir, path)):
98 os.unlink(os.path.join(gclient_dir, path))
99
100 cmd = [
101 'gclient',
102 'sync',
103 '--jobs=%d' % jobs,
104 '--delete_unversioned_trees',
105 # --force is necessary because runhook may generate some untracked files.
106 '--force',
107 ]
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800108 if with_branch_heads:
109 cmd.append('--with_branch_heads')
110 if with_tags:
111 cmd.append('--with_tags')
Kuang-che Wudc714412018-10-17 16:06:39 +0800112
Kuang-che Wu067ff292019-02-14 18:16:23 +0800113 try:
114 old_projects = load_gclient_entries(gclient_dir)
115 except IOError:
116 old_projects = {}
117
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800118 util.check_call(*cmd, cwd=gclient_dir)
119
Kuang-che Wu067ff292019-02-14 18:16:23 +0800120 # Remove dead .git folder after sync.
121 # Ideally, this should be handled by gclient but sometimes gclient didn't
122 # (crbug/930047).
123 new_projects = load_gclient_entries(gclient_dir)
124 for path in old_projects:
125 if path in new_projects:
126 continue
127 old_git_dir = os.path.join(gclient_dir, path)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800128 if not os.path.exists(old_git_dir):
129 continue
130
131 if git_util.is_git_root(old_git_dir):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800132 logger.warning(
133 '%s was removed from .gclient_entries but %s still exists; remove it',
134 path, old_git_dir)
135 shutil.rmtree(old_git_dir)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800136 else:
137 logger.warning(
138 '%s was removed from .gclient_entries but %s still exists;'
139 ' keep it because it is not git root', path, old_git_dir)
140
141
Kuang-che Wucb8a80e2021-01-18 15:22:27 +0800142def runhook(gclient_dir, jobs=8, gclientfile=None):
Kuang-che Wucbe12432019-03-18 19:35:03 +0800143 """Simply wrapper of `gclient runhook`.
144
145 Args:
146 gclient_dir: root directory of gclient project
147 jobs: how many workers running in parallel
148 """
Kuang-che Wucb8a80e2021-01-18 15:22:27 +0800149 cmd = ['gclient', 'runhook', '--job', str(jobs)]
150 if gclientfile:
151 cmd += ['--gclientfile', gclientfile]
152 util.check_call(*cmd, cwd=gclient_dir)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800153
154
155def load_gclient_entries(gclient_dir):
156 """Loads .gclient_entries."""
157 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
158 scope = {}
Kuang-che Wud2646f42019-11-27 18:31:08 +0800159 with open(repo_project_list) as f:
160 code = compile(f.read(), repo_project_list, 'exec')
Kuang-che Wub1b18ea2021-01-18 15:03:30 +0800161 # pylint: disable=exec-used
162 exec(code, scope)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800163 entries = scope.get('entries', {})
164
165 # normalize path: remove trailing slash
166 entries = dict((os.path.normpath(path), url) for path, url in entries.items())
167
168 return entries
Kuang-che Wu067ff292019-02-14 18:16:23 +0800169
170
171def write_gclient_entries(gclient_dir, projects):
172 """Writes .gclient_entries."""
173 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
174 content = 'entries = {\n'
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800175 for path, repo_url in sorted(projects.items()):
176 content += ' %s: %s,\n' % (pprint.pformat(path), pprint.pformat(repo_url))
Kuang-che Wu067ff292019-02-14 18:16:23 +0800177 content += '}\n'
178 with open(repo_project_list, 'w') as f:
179 f.write(content)
180
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800181
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800182def mirror(code_storage, repo_url):
183 """Mirror git repo.
184
185 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
186
187 Args:
188 code_storage: CodeStorage object
189 repo_url: remote repo url
190 """
191 logger.info('mirror %s', repo_url)
192 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
193 git_root = code_storage.cached_git_root(repo_url)
194 assert not os.path.exists(git_root)
195
196 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
197
198 # These config parameters are copied from gclient.
199 git_util.config(tmp_dir, 'gc.autodetach', '0')
200 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
201 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
202 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
203 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
204 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
205 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
206 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
207 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
208 '+refs/branch-heads/*:refs/branch-heads/*',
209 r'\+refs/branch-heads/\*:.*')
210
211 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
212 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
213 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
214
215 # Rename to correct name atomically.
216 os.rename(tmp_dir, git_root)
217
218
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800219# Copied from depot_tools' gclient.py
220_PLATFORM_MAPPING = {
221 'cygwin': 'win',
222 'darwin': 'mac',
223 'linux2': 'linux',
Kuang-che Wud2646f42019-11-27 18:31:08 +0800224 'linux': 'linux',
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800225 'win32': 'win',
226 'aix6': 'aix',
227}
228
229
230def _detect_host_os():
231 return _PLATFORM_MAPPING[sys.platform]
232
233
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800234class Dep:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800235 """Represent one entry of DEPS's deps.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800236
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800237 One Dep object means one subproject inside DEPS file. It recorded what to
238 checkout (like git or cipd) content of each subproject.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800239
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800240 Attributes:
241 path: subproject path, relative to project root
242 variables: the variables of the containing DEPS file; these variables will
243 be applied to fields of this object (like 'url' and 'condition') and
244 children projects.
245 condition: whether to checkout this subproject
246 dep_type: 'git' or 'cipd'
247 url: if dep_type='git', the url of remote repo and associated branch/commit
248 packages: if dep_type='cipd', cipd package version and location
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800249 """
250
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800251 def __init__(self, path, variables, entry):
252 self.path = path
253 self.variables = variables
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800254
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800255 self.url = None # only valid for dep_type='git'
256 self.packages = None # only valid for dep_type='cipd'
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800257
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800258 if isinstance(entry, str):
259 self.dep_type = 'git'
260 self.url = entry
261 self.condition = None
262 else:
263 self.dep_type = entry.get('dep_type', 'git')
264 self.condition = entry.get('condition')
265 if self.dep_type == 'git':
266 self.url = entry['url']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800267 else:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800268 assert self.dep_type == 'cipd', 'unknown dep_type:' + self.dep_type
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800269 self.packages = entry['packages']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800270
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800271 if self.dep_type == 'git':
272 self.url = self.url.format(**self.variables)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800273
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800274 def __eq__(self, rhs):
275 return vars(self) == vars(rhs)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800276
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800277 def __ne__(self, rhs):
278 return not self.__eq__(rhs)
279
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800280 def set_url(self, repo_url, at):
281 assert self.dep_type == 'git'
282 self.url = '%s@%s' % (repo_url, at)
283
284 def set_revision(self, at):
285 assert self.dep_type == 'git'
286 repo_url, _ = self.parse_url()
287 self.set_url(repo_url, at)
288
289 def parse_url(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800290 assert self.dep_type == 'git'
291
292 if '@' in self.url:
293 repo_url, at = self.url.split('@')
294 else:
Kuang-che Wu88e96312020-10-20 16:21:11 +0800295 # If the dependency is not pinned, the default branch.
296 repo_url, at = self.url, DEFAULT_BRANCH_NAME
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800297 return repo_url, at
298
299 def as_path_spec(self):
300 repo_url, at = self.parse_url()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800301 return codechange.PathSpec(self.path, repo_url, at)
302
303 def eval_condition(self):
304 """Evaluate condition for DEPS parsing.
305
306 Returns:
307 eval result
308 """
309 if not self.condition:
310 return True
311
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800312 # Currently, we only support chromeos as target_os.
313 # TODO(kcwu): make it configurable if we need to bisect for other os.
Kuang-che Wu0d7409c2019-03-18 12:29:03 +0800314 # We don't specify `target_os_only`, so `unix` will be considered by
315 # gclient as well.
316 target_os = ['chromeos', 'unix']
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800317
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800318 vars_dict = {
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800319 'checkout_android': 'android' in target_os,
320 'checkout_chromeos': 'chromeos' in target_os,
321 'checkout_fuchsia': 'fuchsia' in target_os,
322 'checkout_ios': 'ios' in target_os,
323 'checkout_linux': 'unix' in target_os,
324 'checkout_mac': 'mac' in target_os,
325 'checkout_win': 'win' in target_os,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800326 # default cpu: x64
327 'checkout_arm64': False,
328 'checkout_arm': False,
329 'checkout_mips': False,
330 'checkout_ppc': False,
331 'checkout_s390': False,
332 'checkout_x64': True,
333 'checkout_x86': False,
334 'host_os': _detect_host_os(),
335 'False': False,
336 'None': None,
337 'True': True,
338 }
339 vars_dict.update(self.variables)
Kuang-che Wu6532ffd2021-03-17 12:23:42 +0800340
341 # Work around b/182219439
342 # TODO(kcwu): mimic gclient's behavior, which convert undefined variables
343 # to strings
344 if 'use_rts' not in vars_dict:
345 vars_dict['use_rts'] = True
346
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800347 # pylint: disable=eval-used
348 return eval(self.condition, vars_dict)
349
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800350 def to_lines(self):
351 s = []
352 condition_part = ([' "condition": %r,' %
353 self.condition] if self.condition else [])
354 if self.dep_type == 'cipd':
355 s.extend([
356 ' "%s": {' % (self.path.split(':')[0],),
357 ' "packages": [',
358 ])
359 for p in sorted(self.packages, key=lambda x: x['package']):
360 s.extend([
361 ' {',
362 ' "package": "%s",' % p['package'],
363 ' "version": "%s",' % p['version'],
364 ' },',
365 ])
366 s.extend([
367 ' ],',
368 ' "dep_type": "cipd",',
369 ] + condition_part + [
370 ' },',
371 '',
372 ])
373 else:
374 s.extend([
375 ' "%s": {' % (self.path,),
376 ' "url": "%s",' % (self.url,),
377 ] + condition_part + [
378 ' },',
379 '',
380 ])
381 return s
382
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800383
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800384class Deps:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800385 """DEPS parsed result.
386
387 Attributes:
388 variables: 'vars' dict in DEPS file; these variables will be applied
389 recursively to children.
390 entries: dict of Dep objects
391 recursedeps: list of recursive projects
392 """
393
394 def __init__(self):
395 self.variables = {}
396 self.entries = {}
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800397 self.ignored_entries = {}
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800398 self.recursedeps = []
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800399 self.allowed_hosts = set()
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800400 self.gn_args_from = None
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800401 self.gn_args_file = None
402 self.gn_args = []
403 self.hooks = []
404 self.pre_deps_hooks = []
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800405 self.modified = set()
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800406
407 def _gn_settings_to_lines(self):
408 s = []
409 if self.gn_args_file:
410 s.extend([
411 'gclient_gn_args_file = "%s"' % self.gn_args_file,
412 'gclient_gn_args = %r' % self.gn_args,
413 ])
414 return s
415
416 def _allowed_hosts_to_lines(self):
417 """Converts |allowed_hosts| set to list of lines for output."""
418 if not self.allowed_hosts:
419 return []
420 s = ['allowed_hosts = [']
421 for h in sorted(self.allowed_hosts):
422 s.append(' "%s",' % h)
423 s.extend([']', ''])
424 return s
425
426 def _entries_to_lines(self):
427 """Converts |entries| dict to list of lines for output."""
428 entries = self.ignored_entries
429 entries.update(self.entries)
430 if not entries:
431 return []
432 s = ['deps = {']
433 for _, dep in sorted(entries.items()):
434 s.extend(dep.to_lines())
435 s.extend(['}', ''])
436 return s
437
438 def _vars_to_lines(self):
439 """Converts |variables| dict to list of lines for output."""
440 if not self.variables:
441 return []
442 s = ['vars = {']
443 for key, value in sorted(self.variables.items()):
444 s.extend([
445 ' "%s": %r,' % (key, value),
446 '',
447 ])
448 s.extend(['}', ''])
449 return s
450
451 def _hooks_to_lines(self, name, hooks):
452 """Converts |hooks| list to list of lines for output."""
453 if not hooks:
454 return []
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800455 hooks.sort(key=lambda x: x.get('name', ''))
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800456 s = ['%s = [' % name]
457 for hook in hooks:
458 s.extend([
459 ' {',
460 ])
461 if hook.get('name') is not None:
462 s.append(' "name": "%s",' % hook.get('name'))
463 if hook.get('pattern') is not None:
464 s.append(' "pattern": "%s",' % hook.get('pattern'))
465 if hook.get('condition') is not None:
466 s.append(' "condition": %r,' % hook.get('condition'))
467 # Flattened hooks need to be written relative to the root gclient dir
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800468 cwd = os.path.relpath(os.path.normpath(hook.get('cwd', '.')))
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800469 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
470 [' "%s",' % arg for arg in hook.get('action', [])] +
471 [' ]', ' },', ''])
472 s.extend([']', ''])
473 return s
474
475 def to_string(self):
476 """Return flatten DEPS string."""
477 return '\n'.join(
478 self._gn_settings_to_lines() + self._allowed_hosts_to_lines() +
479 self._entries_to_lines() + self._hooks_to_lines('hooks', self.hooks) +
480 self._hooks_to_lines('pre_deps_hooks', self.pre_deps_hooks) +
481 self._vars_to_lines() + ['']) # Ensure newline at end of file.
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800482
Zheng-Jie Changc0d3cd72020-06-03 02:46:43 +0800483 def remove_src(self):
484 """Return src_revision for buildbucket use."""
485 assert 'src' in self.entries
486 _, src_rev = self.entries['src'].parse_url()
487 del self.entries['src']
488 return src_rev
489
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800490 def apply_commit(self, path, revision, overwrite=True):
491 """Set revision to a project by path.
492
493 Args:
494 path: A project's path.
495 revision: A git commit id.
496 overwrite: Overwrite flag, the project won't change if overwrite=False
497 and it was modified before.
498 """
499 if path in self.modified and not overwrite:
500 return
501 self.modified.add(path)
502
503 if path not in self.entries:
504 logger.warning('path: %s not found in DEPS', path)
505 return
506 self.entries[path].set_revision(revision)
507
508 def apply_action_groups(self, action_groups):
509 """Apply multiple action groups to DEPS.
510
511 If there are multiple actions in one repo, only last one is applied.
512
513 Args:
514 action_groups: A list of action groups.
515 """
516 # Apply in reversed order with overwrite=False,
517 # so each repo is on the state of last action.
518 for action_group in reversed(action_groups):
519 for action in reversed(action_group.actions):
520 if isinstance(action, codechange.GitCheckoutCommit):
521 self.apply_commit(action.path, action.rev, overwrite=False)
522 if isinstance(action, codechange.GitAddRepo):
523 self.apply_commit(action.path, action.rev, overwrite=False)
524 if isinstance(action, codechange.GitRemoveRepo):
525 assert action.path not in self.entries
526 self.modified.add(action.path)
527
528 def apply_deps(self, deps):
529 for path, dep in deps.entries.items():
530 if path in self.entries:
531 _, rev = dep.parse_url()
532 self.apply_commit(path, rev, overwrite=False)
533
534 # hooks, vars, ignored_entries are ignored and should be set by float_spec
535
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800536
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800537class TimeSeriesTree:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800538 """Data structure for generating snapshots of historical dependency tree.
539
540 This is a tree structure with time information. Each tree node represents not
541 only typical tree data and tree children information, but also historical
542 value of those tree data and tree children.
543
544 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
545 represent a DEPS file. The caller will add_snapshot() to add parsed result of
546 historical DEPS instances. After that, the tree root of this class can
547 reconstruct the every historical moment of the project dependency state.
548
549 This class is slight abstraction of git_util.get_history_recursively() to
550 support more than single git repo and be version control system independent.
551 """
552
553 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
554
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800555 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800556 """TimeSeriesTree constructor.
557
558 Args:
559 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800560 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800561 start_time: start time
562 end_time: end time
563 """
564 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800565 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800566 self.snapshots = {}
567 self.start_time = start_time
568 self.end_time = end_time
569
570 # Intermediate dict to keep track alive children for the time being.
571 # Maintained by add_snapshot() and no_more_snapshot().
572 self.alive_children = {}
573
574 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800575 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800576 # once in this list because they are removed and added back to the DEPS
577 # file.
578 self.subtrees = []
579
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800580 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800581 """Compares subtree of two Deps.
582
583 Args:
584 deps_a: Deps object
585 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800586 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800587
588 Returns:
589 True if the said subtree of these two Deps equal
590 """
591 # Need to compare variables because they may influence subtree parsing
592 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800593 path = child_entry[0]
594 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800595 deps_a.variables == deps_b.variables)
596
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800597 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800598 """Adds parsed DEPS result and children.
599
600 For example, if a given DEPS file has N revisions between start_time and
601 end_time, the caller should call this method N times to feed all parsed
602 results in order (timestamp increasing).
603
604 Args:
605 timestamp: timestamp of `deps`
606 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800607 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800608 """
609 assert timestamp not in self.snapshots
610 self.snapshots[timestamp] = deps
611
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800612 for child_entry in set(list(self.alive_children.keys()) + children_entries):
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800613 # `child_entry` is added at `timestamp`
614 if child_entry not in self.alive_children:
615 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800616
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800617 # `child_entry` is removed at `timestamp`
618 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800619 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800620 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
621 self.alive_children[child_entry][0], timestamp))
622 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800623
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800624 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800625 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800626 last_deps = self.alive_children[child_entry][1]
627 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800628 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800629 TimeSeriesTree(last_deps, child_entry,
630 self.alive_children[child_entry][0], timestamp))
631 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800632
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800633 def no_more_snapshot(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800634 """Indicates all snapshots are added.
635
636 add_snapshot() should not be invoked after no_more_snapshot().
637 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800638 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800639 if timestamp == self.end_time:
640 continue
641 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800642 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800643 self.alive_children = None
644
645 def events(self):
646 """Gets children added/removed events of this subtree.
647
648 Returns:
649 list of (timestamp, deps_name, deps, end_flag):
650 timestamp: timestamp of event
651 deps_name: name of this subtree
652 deps: Deps object of given project
653 end_flag: True indicates this is the last event of this deps tree
654 """
655 assert self.snapshots
656 assert self.alive_children is None, ('events() is valid only after '
657 'no_more_snapshot() is invoked')
658
659 result = []
660
661 last_deps = None
662 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800663 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800664 last_deps = deps
665
666 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800667 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800668
669 for subtree in self.subtrees:
670 for event in subtree.events():
671 result.append(event)
672
Kuang-che Wu1ad2c0e2019-02-26 00:41:10 +0800673 result.sort(key=lambda x: x[0])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800674
675 return result
676
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800677 def iter_forest(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800678 """Iterates snapshots of project dependency state.
679
680 Yields:
681 (timestamp, path_specs):
682 timestamp: time of snapshot
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800683 forest: A dict indicates path => deps mapping
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800684 """
685 forest = {}
686 # Group by timestamp
687 for timestamp, events in itertools.groupby(self.events(),
688 operator.itemgetter(0)):
689 # It's possible that one deps is removed and added at the same timestamp,
690 # i.e. modification, so use counter to track.
691 end_counter = collections.Counter()
692
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800693 for timestamp, entry, deps, end in events:
694 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800695 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800696 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800697 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800698 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800699
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800700 yield timestamp, forest
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800701
702 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800703 for entry, count in end_counter.items():
704 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800705 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800706 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800707
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800708 def iter_path_specs(self):
709 """Iterates snapshots of project dependency state.
710
711 Yields:
712 (timestamp, path_specs):
713 timestamp: time of snapshot
714 path_specs: dict of path_spec entries
715 """
716 for timestamp, forest in self.iter_forest():
717 path_specs = {}
718 # Merge Deps at time `timestamp` into single path_specs.
719 for deps in forest.values():
720 for path, dep in deps.entries.items():
721 path_specs[path] = dep.as_path_spec()
722 yield timestamp, path_specs
723
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800724
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800725class DepsParser:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800726 """Gclient DEPS file parser."""
727
728 def __init__(self, project_root, code_storage):
729 self.project_root = project_root
730 self.code_storage = code_storage
731
Kuang-che Wu067ff292019-02-14 18:16:23 +0800732 def load_single_deps(self, content):
733
734 def var_function(name):
735 return '{%s}' % name
736
Zheng-Jie Chang381e4162020-08-03 14:26:54 +0800737 def str_function(name):
738 return str(name)
739
740 global_scope = dict(Var=var_function, Str=str_function)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800741 local_scope = {}
Kuang-che Wub1b18ea2021-01-18 15:03:30 +0800742 # pylint: disable=exec-used
743 exec(content, global_scope, local_scope)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800744 return local_scope
745
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800746 def parse_single_deps(self,
747 content,
748 parent_vars=None,
749 parent_path='',
750 parent_dep=None):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800751 """Parses DEPS file without recursion.
752
753 Args:
754 content: file content of DEPS file
755 parent_vars: variables inherent from parent DEPS
756 parent_path: project path of parent DEPS file
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800757 parent_dep: A corresponding Dep object in parent DEPS
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800758
759 Returns:
760 Deps object
761 """
762
Kuang-che Wu067ff292019-02-14 18:16:23 +0800763 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800764 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800765
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800766 local_scope.setdefault('vars', {})
767 if parent_vars:
768 local_scope['vars'].update(parent_vars)
769 deps.variables = local_scope['vars']
770
771 # Warnings for old usages which we don't support.
772 for name in deps.variables:
773 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
774 logger.warning('%s is deprecated and not supported recursion syntax',
775 name)
776 if 'deps_os' in local_scope:
777 logger.warning('deps_os is no longer supported')
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800778
779 if 'allowed_hosts' in local_scope:
780 deps.allowed_hosts = set(local_scope.get('allowed_hosts'))
781 deps.hooks = local_scope.get('hooks', [])
782 deps.pre_deps_hooks = local_scope.get('pre_deps_hooks', [])
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800783 deps.gn_args_from = local_scope.get('gclient_gn_args_from')
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800784 deps.gn_args_file = local_scope.get('gclient_gn_args_file')
785 deps.gn_args = local_scope.get('gclient_gn_args', [])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800786
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800787 # recalculate hook path
788 use_relative_hooks = local_scope.get('use_relative_hooks', False)
789 if use_relative_hooks:
790 assert local_scope.get('use_relative_paths', False)
791 for hook in deps.hooks:
792 hook['cwd'] = os.path.join(parent_path, hook.get('cwd', ''))
793 for pre_deps_hook in deps.pre_deps_hooks:
794 pre_deps_hook['cwd'] = os.path.join(parent_path,
795 pre_deps_hook.get('cwd', ''))
796
Kuang-che Wu25fec6f2021-01-28 12:40:43 +0800797 for path, dep_entry in local_scope.get('deps', {}).items():
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800798 # recalculate path
Kuang-che Wu058ac042019-03-18 14:14:53 +0800799 path = path.format(**deps.variables)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800800 if local_scope.get('use_relative_paths', False):
801 path = os.path.join(parent_path, path)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800802 path = os.path.normpath(path)
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800803
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800804 dep = Dep(path, deps.variables, dep_entry)
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800805 eval_condition = dep.eval_condition()
806
807 # update condition
808 if parent_dep and parent_dep.condition:
809 tmp_dict = {'condition': dep.condition}
810 gclient_eval.UpdateCondition(tmp_dict, 'and', parent_dep.condition)
811 dep.condition = tmp_dict['condition']
812
813 if not eval_condition:
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800814 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800815 continue
816
817 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
818 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800819 warning_key = ('dep_type', dep.dep_type, path)
820 if warning_key not in emitted_warnings:
821 emitted_warnings.add(warning_key)
822 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
823 path)
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800824 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800825 continue
826
827 deps.entries[path] = dep
828
829 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800830 for recurse_entry in local_scope.get('recursedeps', []):
831 # Normalize entries.
832 if isinstance(recurse_entry, tuple):
833 path, deps_file = recurse_entry
834 else:
835 assert isinstance(path, str)
836 path, deps_file = recurse_entry, 'DEPS'
837
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800838 if local_scope.get('use_relative_paths', False):
839 path = os.path.join(parent_path, path)
840 path = path.format(**deps.variables)
841 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800842 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800843 deps.recursedeps = recursedeps
844
845 return deps
846
847 def construct_deps_tree(self,
848 tstree,
849 repo_url,
850 at,
851 after,
852 before,
853 parent_vars=None,
854 parent_path='',
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800855 parent_dep=None,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800856 deps_file='DEPS'):
857 """Processes DEPS recursively of given time period.
858
859 This method parses all commits of DEPS between time `after` and `before`,
860 segments recursive dependencies into subtrees if they are changed, and
861 processes subtrees recursively.
862
863 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
864
865 Args:
866 tstree: TimeSeriesTree object
867 repo_url: remote repo url
868 at: branch or git commit id
869 after: begin of period
870 before: end of period
871 parent_vars: DEPS variables inherit from parent DEPS (including
872 custom_vars)
873 parent_path: the path of parent project of current DEPS file
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800874 parent_dep: A corresponding Dep object in parent DEPS
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800875 deps_file: filename of DEPS file, relative to the git repo, repo_rul
876 """
877 if '://' in repo_url:
878 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800879 if not os.path.exists(git_repo):
880 with locking.lock_file(
881 os.path.join(self.code_storage.cache_dir,
882 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
883 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800884 else:
885 git_repo = repo_url
886
887 if git_util.is_git_rev(at):
888 history = [
889 (after, at),
890 (before, at),
891 ]
892 else:
893 history = git_util.get_history(
894 git_repo,
895 deps_file,
896 branch=at,
897 after=after,
898 before=before,
Zheng-Jie Chang313eec32020-02-18 16:17:07 +0800899 padding_begin=True,
900 padding_end=True)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800901 assert history
902
903 # If not equal, it means the file was deleted but is still referenced by
904 # its parent.
905 assert history[-1][0] == before
906
907 # TODO(kcwu): optimization: history[-1] is unused
908 for timestamp, git_rev in history[:-1]:
909 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
910
911 deps = self.parse_single_deps(
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800912 content,
913 parent_vars=parent_vars,
914 parent_path=parent_path,
915 parent_dep=parent_dep)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800916 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
917
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800918 tstree.no_more_snapshot()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800919
920 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800921 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800922 path_spec = subtree.parent_deps.entries[path].as_path_spec()
923 self.construct_deps_tree(
924 subtree,
925 path_spec.repo_url,
926 path_spec.at,
927 subtree.start_time,
928 subtree.end_time,
929 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800930 parent_path=path,
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800931 parent_dep=subtree.parent_deps.entries[path],
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800932 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800933
934 def enumerate_path_specs(self, start_time, end_time, path):
935 tstree = TimeSeriesTree(None, path, start_time, end_time)
Kuang-che Wu88e96312020-10-20 16:21:11 +0800936 self.construct_deps_tree(tstree, path, DEFAULT_BRANCH_NAME, start_time,
937 end_time)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800938 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800939
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800940 def enumerate_gclient_solutions(self, start_time, end_time, path):
941 tstree = TimeSeriesTree(None, path, start_time, end_time)
Kuang-che Wu88e96312020-10-20 16:21:11 +0800942 self.construct_deps_tree(tstree, path, DEFAULT_BRANCH_NAME, start_time,
943 end_time)
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800944 return tstree.iter_forest()
945
946 def flatten(self, solutions, entry_point):
947 """Flatten all given Deps
948
949 Args:
950 solutions: A name => Deps dict, name can be either a str or a tuple.
951 entry_point (str): An entry_point name of solutions.
952
953 Returns:
954 Deps: A flatten Deps.
955 """
956
957 def _add_unvisited_recursedeps(deps_queue, visited, deps):
958 for name in deps.recursedeps:
959 if name not in visited:
960 visited.add(name)
961 deps_queue.put(name)
962
963 result = solutions[entry_point]
964 deps_queue = queue.SimpleQueue()
965 visited = set()
966 visited.add(entry_point)
967 _add_unvisited_recursedeps(deps_queue, visited, solutions[entry_point])
968
969 # BFS to merge `deps` into `result`
970 while not deps_queue.empty():
971 deps_name = deps_queue.get()
972 deps = solutions[deps_name]
973
974 result.allowed_hosts.update(deps.allowed_hosts)
975 for key, value in deps.variables.items():
976 assert key not in result.variables or deps.variables[key] == value
977 result.variables[key] = value
978 result.pre_deps_hooks += deps.pre_deps_hooks
979 result.hooks += deps.hooks
980
981 for dep in deps.entries.values():
982 assert dep.path not in result.entries or result.entries.get(
983 dep.path) == dep
984 result.entries[dep.path] = dep
985
986 for dep in deps.ignored_entries.values():
987 assert (dep.path not in result.ignored_entries or
988 result.ignored_entries.get(dep.path) == dep)
989 result.ignored_entries[dep.path] = dep
990
991 _add_unvisited_recursedeps(deps_queue, visited, deps)
992
993 # If gn_args_from is set in root DEPS, overwrite gn arguments
994 if solutions[entry_point].gn_args_from:
995 gn_args_dep = solutions[(solutions[entry_point].gn_args_from, 'DEPS')]
996 result.gn_args = gn_args_dep.gn_args
997 result.gn_args_file = gn_args_dep.gn_args_file
998
999 return result
1000
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001001
1002class GclientCache(codechange.CodeStorage):
1003 """Gclient git cache."""
1004
1005 def __init__(self, cache_dir):
1006 self.cache_dir = cache_dir
1007
1008 def _url_to_cache_dir(self, url):
1009 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +08001010 parsed = urllib.parse.urlparse(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001011 norm_url = parsed.netloc + parsed.path
1012 if norm_url.endswith('.git'):
1013 norm_url = norm_url[:-len('.git')]
Kuang-che Wu5fef2912019-04-15 16:18:29 +08001014 norm_url = norm_url.replace('googlesource.com/a/', 'googlesource.com/')
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001015 return norm_url.replace('-', '--').replace('/', '-').lower()
1016
1017 def cached_git_root(self, repo_url):
1018 cache_path = self._url_to_cache_dir(repo_url)
1019 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001020
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001021 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +08001022 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001023
1024 projects[path] = repo_url
1025
Kuang-che Wu067ff292019-02-14 18:16:23 +08001026 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001027
1028 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +08001029 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001030
1031 if path in projects:
1032 del projects[path]
1033
Kuang-che Wu067ff292019-02-14 18:16:23 +08001034 write_gclient_entries(project_root, projects)