blob: 93a3d768fc3498a11feead32d2657029cff18167 [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
Kuang-che Wu067ff292019-02-14 18:16:23 +080014import shutil
Kuang-che Wub17b3b92018-09-04 18:12:11 +080015import sys
Kuang-che Wu1e49f512018-12-06 15:27:42 +080016import tempfile
Kuang-che Wu999893c2020-04-13 22:06:22 +080017import urllib.parse
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +080018
Kuang-che Wud2646f42019-11-27 18:31:08 +080019import six
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080020
21from bisect_kit import codechange
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080022from bisect_kit import git_util
Kuang-che Wu1e49f512018-12-06 15:27:42 +080023from bisect_kit import locking
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080024from bisect_kit import util
25
26logger = logging.getLogger(__name__)
Kuang-che Wuced2dbf2019-01-30 23:13:24 +080027emitted_warnings = set()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080028
29
Kuang-che Wu41e8b592018-09-25 17:01:30 +080030def config(gclient_dir,
31 url=None,
32 cache_dir=None,
33 deps_file=None,
34 custom_var=None,
35 spec=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080036 """Simply wrapper of `gclient config`.
37
38 Args:
39 gclient_dir: root directory of gclient project
40 url: URL of gclient configuration files
41 cache_dir: gclient's git cache folder
42 deps_file: override the default DEPS file name
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080043 custom_var: custom variables
Kuang-che Wu41e8b592018-09-25 17:01:30 +080044 spec: content of gclient file
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080045 """
46 cmd = ['gclient', 'config']
47 if deps_file:
48 cmd += ['--deps-file', deps_file]
49 if cache_dir:
50 cmd += ['--cache-dir', cache_dir]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080051 if custom_var:
52 cmd += ['--custom-var', custom_var]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080053 if spec:
54 cmd += ['--spec', spec]
55 if url:
56 cmd.append(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080057
58 util.check_call(*cmd, cwd=gclient_dir)
59
60
Kuang-che Wudc714412018-10-17 16:06:39 +080061def sync(gclient_dir,
62 with_branch_heads=False,
63 with_tags=False,
64 ignore_locks=False,
65 jobs=8):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080066 """Simply wrapper of `gclient sync`.
67
68 Args:
69 gclient_dir: root directory of gclient project
70 with_branch_heads: whether to clone git `branch_heads` refspecs
71 with_tags: whether to clone git tags
Kuang-che Wudc714412018-10-17 16:06:39 +080072 ignore_locks: bypass gclient's lock
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080073 jobs: how many workers running in parallel
74 """
Kuang-che Wua9639eb2019-03-19 17:15:08 +080075 # Work around gclient issue crbug/943430
76 # gclient rejected to sync if there are untracked symlink even with --force
77 for path in [
78 'src/chromeos/assistant/libassistant/src/deps',
79 'src/chromeos/assistant/libassistant/src/libassistant',
80 ]:
81 if os.path.islink(os.path.join(gclient_dir, path)):
82 os.unlink(os.path.join(gclient_dir, path))
83
84 cmd = [
85 'gclient',
86 'sync',
87 '--jobs=%d' % jobs,
88 '--delete_unversioned_trees',
89 # --force is necessary because runhook may generate some untracked files.
90 '--force',
91 ]
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080092 if with_branch_heads:
93 cmd.append('--with_branch_heads')
94 if with_tags:
95 cmd.append('--with_tags')
Kuang-che Wudc714412018-10-17 16:06:39 +080096
97 # If 'gclient sync' is interrupted by ctrl-c or terminated with whatever
98 # reasons, it will leave annoying lock files on disk and thus unfriendly to
99 # bot tasks. In bisect-kit, we will use our own lock mechanism (in caller of
100 # this function) and bypass gclient's.
101 if ignore_locks:
102 cmd.append('--ignore_locks')
103
Kuang-che Wu067ff292019-02-14 18:16:23 +0800104 try:
105 old_projects = load_gclient_entries(gclient_dir)
106 except IOError:
107 old_projects = {}
108
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800109 util.check_call(*cmd, cwd=gclient_dir)
110
Kuang-che Wu067ff292019-02-14 18:16:23 +0800111 # Remove dead .git folder after sync.
112 # Ideally, this should be handled by gclient but sometimes gclient didn't
113 # (crbug/930047).
114 new_projects = load_gclient_entries(gclient_dir)
115 for path in old_projects:
116 if path in new_projects:
117 continue
118 old_git_dir = os.path.join(gclient_dir, path)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800119 if not os.path.exists(old_git_dir):
120 continue
121
122 if git_util.is_git_root(old_git_dir):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800123 logger.warning(
124 '%s was removed from .gclient_entries but %s still exists; remove it',
125 path, old_git_dir)
126 shutil.rmtree(old_git_dir)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800127 else:
128 logger.warning(
129 '%s was removed from .gclient_entries but %s still exists;'
130 ' keep it because it is not git root', path, old_git_dir)
131
132
133def runhook(gclient_dir, jobs=8):
134 """Simply wrapper of `gclient runhook`.
135
136 Args:
137 gclient_dir: root directory of gclient project
138 jobs: how many workers running in parallel
139 """
140 util.check_call('gclient', 'runhook', '--jobs', str(jobs), cwd=gclient_dir)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800141
142
143def load_gclient_entries(gclient_dir):
144 """Loads .gclient_entries."""
145 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
146 scope = {}
Kuang-che Wud2646f42019-11-27 18:31:08 +0800147 with open(repo_project_list) as f:
148 code = compile(f.read(), repo_project_list, 'exec')
149 six.exec_(code, scope)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800150 entries = scope.get('entries', {})
151
152 # normalize path: remove trailing slash
153 entries = dict((os.path.normpath(path), url) for path, url in entries.items())
154
155 return entries
Kuang-che Wu067ff292019-02-14 18:16:23 +0800156
157
158def write_gclient_entries(gclient_dir, projects):
159 """Writes .gclient_entries."""
160 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
161 content = 'entries = {\n'
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800162 for path, repo_url in sorted(projects.items()):
163 content += ' %s: %s,\n' % (pprint.pformat(path), pprint.pformat(repo_url))
Kuang-che Wu067ff292019-02-14 18:16:23 +0800164 content += '}\n'
165 with open(repo_project_list, 'w') as f:
166 f.write(content)
167
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800168
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800169def mirror(code_storage, repo_url):
170 """Mirror git repo.
171
172 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
173
174 Args:
175 code_storage: CodeStorage object
176 repo_url: remote repo url
177 """
178 logger.info('mirror %s', repo_url)
179 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
180 git_root = code_storage.cached_git_root(repo_url)
181 assert not os.path.exists(git_root)
182
183 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
184
185 # These config parameters are copied from gclient.
186 git_util.config(tmp_dir, 'gc.autodetach', '0')
187 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
188 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
189 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
190 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
191 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
192 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
193 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
194 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
195 '+refs/branch-heads/*:refs/branch-heads/*',
196 r'\+refs/branch-heads/\*:.*')
197
198 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
199 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
200 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
201
202 # Rename to correct name atomically.
203 os.rename(tmp_dir, git_root)
204
205
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800206# Copied from depot_tools' gclient.py
207_PLATFORM_MAPPING = {
208 'cygwin': 'win',
209 'darwin': 'mac',
210 'linux2': 'linux',
Kuang-che Wud2646f42019-11-27 18:31:08 +0800211 'linux': 'linux',
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800212 'win32': 'win',
213 'aix6': 'aix',
214}
215
216
217def _detect_host_os():
218 return _PLATFORM_MAPPING[sys.platform]
219
220
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800221class Dep:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800222 """Represent one entry of DEPS's deps.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800223
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800224 One Dep object means one subproject inside DEPS file. It recorded what to
225 checkout (like git or cipd) content of each subproject.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800226
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800227 Attributes:
228 path: subproject path, relative to project root
229 variables: the variables of the containing DEPS file; these variables will
230 be applied to fields of this object (like 'url' and 'condition') and
231 children projects.
232 condition: whether to checkout this subproject
233 dep_type: 'git' or 'cipd'
234 url: if dep_type='git', the url of remote repo and associated branch/commit
235 packages: if dep_type='cipd', cipd package version and location
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800236 """
237
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800238 def __init__(self, path, variables, entry):
239 self.path = path
240 self.variables = variables
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800241
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800242 self.url = None # only valid for dep_type='git'
243 self.packages = None # only valid for dep_type='cipd'
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800244
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800245 if isinstance(entry, str):
246 self.dep_type = 'git'
247 self.url = entry
248 self.condition = None
249 else:
250 self.dep_type = entry.get('dep_type', 'git')
251 self.condition = entry.get('condition')
252 if self.dep_type == 'git':
253 self.url = entry['url']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800254 else:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800255 assert self.dep_type == 'cipd', 'unknown dep_type:' + self.dep_type
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800256 self.packages = entry['packages']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800257
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800258 if self.dep_type == 'git':
259 self.url = self.url.format(**self.variables)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800260
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800261 def __eq__(self, rhs):
262 return vars(self) == vars(rhs)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800263
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800264 def __ne__(self, rhs):
265 return not self.__eq__(rhs)
266
267 def as_path_spec(self):
268 assert self.dep_type == 'git'
269
270 if '@' in self.url:
271 repo_url, at = self.url.split('@')
272 else:
273 # If the dependency is not pinned, the default is master branch.
274 repo_url, at = self.url, 'master'
275 return codechange.PathSpec(self.path, repo_url, at)
276
277 def eval_condition(self):
278 """Evaluate condition for DEPS parsing.
279
280 Returns:
281 eval result
282 """
283 if not self.condition:
284 return True
285
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800286 # Currently, we only support chromeos as target_os.
287 # TODO(kcwu): make it configurable if we need to bisect for other os.
Kuang-che Wu0d7409c2019-03-18 12:29:03 +0800288 # We don't specify `target_os_only`, so `unix` will be considered by
289 # gclient as well.
290 target_os = ['chromeos', 'unix']
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800291
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800292 vars_dict = {
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800293 'checkout_android': 'android' in target_os,
294 'checkout_chromeos': 'chromeos' in target_os,
295 'checkout_fuchsia': 'fuchsia' in target_os,
296 'checkout_ios': 'ios' in target_os,
297 'checkout_linux': 'unix' in target_os,
298 'checkout_mac': 'mac' in target_os,
299 'checkout_win': 'win' in target_os,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800300 # default cpu: x64
301 'checkout_arm64': False,
302 'checkout_arm': False,
303 'checkout_mips': False,
304 'checkout_ppc': False,
305 'checkout_s390': False,
306 'checkout_x64': True,
307 'checkout_x86': False,
308 'host_os': _detect_host_os(),
309 'False': False,
310 'None': None,
311 'True': True,
312 }
313 vars_dict.update(self.variables)
314 # pylint: disable=eval-used
315 return eval(self.condition, vars_dict)
316
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800317 def to_lines(self):
318 s = []
319 condition_part = ([' "condition": %r,' %
320 self.condition] if self.condition else [])
321 if self.dep_type == 'cipd':
322 s.extend([
323 ' "%s": {' % (self.path.split(':')[0],),
324 ' "packages": [',
325 ])
326 for p in sorted(self.packages, key=lambda x: x['package']):
327 s.extend([
328 ' {',
329 ' "package": "%s",' % p['package'],
330 ' "version": "%s",' % p['version'],
331 ' },',
332 ])
333 s.extend([
334 ' ],',
335 ' "dep_type": "cipd",',
336 ] + condition_part + [
337 ' },',
338 '',
339 ])
340 else:
341 s.extend([
342 ' "%s": {' % (self.path,),
343 ' "url": "%s",' % (self.url,),
344 ] + condition_part + [
345 ' },',
346 '',
347 ])
348 return s
349
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800350
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800351class Deps:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800352 """DEPS parsed result.
353
354 Attributes:
355 variables: 'vars' dict in DEPS file; these variables will be applied
356 recursively to children.
357 entries: dict of Dep objects
358 recursedeps: list of recursive projects
359 """
360
361 def __init__(self):
362 self.variables = {}
363 self.entries = {}
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800364 self.ignored_entries = {}
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800365 self.recursedeps = []
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800366 self.allowed_hosts = set()
367 self.gn_args_file = None
368 self.gn_args = []
369 self.hooks = []
370 self.pre_deps_hooks = []
371
372 def _gn_settings_to_lines(self):
373 s = []
374 if self.gn_args_file:
375 s.extend([
376 'gclient_gn_args_file = "%s"' % self.gn_args_file,
377 'gclient_gn_args = %r' % self.gn_args,
378 ])
379 return s
380
381 def _allowed_hosts_to_lines(self):
382 """Converts |allowed_hosts| set to list of lines for output."""
383 if not self.allowed_hosts:
384 return []
385 s = ['allowed_hosts = [']
386 for h in sorted(self.allowed_hosts):
387 s.append(' "%s",' % h)
388 s.extend([']', ''])
389 return s
390
391 def _entries_to_lines(self):
392 """Converts |entries| dict to list of lines for output."""
393 entries = self.ignored_entries
394 entries.update(self.entries)
395 if not entries:
396 return []
397 s = ['deps = {']
398 for _, dep in sorted(entries.items()):
399 s.extend(dep.to_lines())
400 s.extend(['}', ''])
401 return s
402
403 def _vars_to_lines(self):
404 """Converts |variables| dict to list of lines for output."""
405 if not self.variables:
406 return []
407 s = ['vars = {']
408 for key, value in sorted(self.variables.items()):
409 s.extend([
410 ' "%s": %r,' % (key, value),
411 '',
412 ])
413 s.extend(['}', ''])
414 return s
415
416 def _hooks_to_lines(self, name, hooks):
417 """Converts |hooks| list to list of lines for output."""
418 if not hooks:
419 return []
420 s = ['%s = [' % name]
421 for hook in hooks:
422 s.extend([
423 ' {',
424 ])
425 if hook.get('name') is not None:
426 s.append(' "name": "%s",' % hook.get('name'))
427 if hook.get('pattern') is not None:
428 s.append(' "pattern": "%s",' % hook.get('pattern'))
429 if hook.get('condition') is not None:
430 s.append(' "condition": %r,' % hook.get('condition'))
431 # Flattened hooks need to be written relative to the root gclient dir
432 cwd = os.path.relpath(os.path.normpath(hook.get('cwd')))
433 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
434 [' "%s",' % arg for arg in hook.get('action', [])] +
435 [' ]', ' },', ''])
436 s.extend([']', ''])
437 return s
438
439 def to_string(self):
440 """Return flatten DEPS string."""
441 return '\n'.join(
442 self._gn_settings_to_lines() + self._allowed_hosts_to_lines() +
443 self._entries_to_lines() + self._hooks_to_lines('hooks', self.hooks) +
444 self._hooks_to_lines('pre_deps_hooks', self.pre_deps_hooks) +
445 self._vars_to_lines() + ['']) # Ensure newline at end of file.
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800446
Zheng-Jie Changc0d3cd72020-06-03 02:46:43 +0800447 def remove_src(self):
448 """Return src_revision for buildbucket use."""
449 assert 'src' in self.entries
450 _, src_rev = self.entries['src'].parse_url()
451 del self.entries['src']
452 return src_rev
453
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800454
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800455class TimeSeriesTree:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800456 """Data structure for generating snapshots of historical dependency tree.
457
458 This is a tree structure with time information. Each tree node represents not
459 only typical tree data and tree children information, but also historical
460 value of those tree data and tree children.
461
462 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
463 represent a DEPS file. The caller will add_snapshot() to add parsed result of
464 historical DEPS instances. After that, the tree root of this class can
465 reconstruct the every historical moment of the project dependency state.
466
467 This class is slight abstraction of git_util.get_history_recursively() to
468 support more than single git repo and be version control system independent.
469 """
470
471 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
472
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800473 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800474 """TimeSeriesTree constructor.
475
476 Args:
477 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800478 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800479 start_time: start time
480 end_time: end time
481 """
482 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800483 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800484 self.snapshots = {}
485 self.start_time = start_time
486 self.end_time = end_time
487
488 # Intermediate dict to keep track alive children for the time being.
489 # Maintained by add_snapshot() and no_more_snapshot().
490 self.alive_children = {}
491
492 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800493 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800494 # once in this list because they are removed and added back to the DEPS
495 # file.
496 self.subtrees = []
497
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800498 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800499 """Compares subtree of two Deps.
500
501 Args:
502 deps_a: Deps object
503 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800504 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800505
506 Returns:
507 True if the said subtree of these two Deps equal
508 """
509 # Need to compare variables because they may influence subtree parsing
510 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800511 path = child_entry[0]
512 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800513 deps_a.variables == deps_b.variables)
514
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800515 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800516 """Adds parsed DEPS result and children.
517
518 For example, if a given DEPS file has N revisions between start_time and
519 end_time, the caller should call this method N times to feed all parsed
520 results in order (timestamp increasing).
521
522 Args:
523 timestamp: timestamp of `deps`
524 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800525 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800526 """
527 assert timestamp not in self.snapshots
528 self.snapshots[timestamp] = deps
529
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800530 for child_entry in set(list(self.alive_children.keys()) + children_entries):
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800531 # `child_entry` is added at `timestamp`
532 if child_entry not in self.alive_children:
533 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800534
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800535 # `child_entry` is removed at `timestamp`
536 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800537 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800538 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
539 self.alive_children[child_entry][0], timestamp))
540 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800541
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800542 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800543 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800544 last_deps = self.alive_children[child_entry][1]
545 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800546 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800547 TimeSeriesTree(last_deps, child_entry,
548 self.alive_children[child_entry][0], timestamp))
549 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800550
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800551 def no_more_snapshot(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800552 """Indicates all snapshots are added.
553
554 add_snapshot() should not be invoked after no_more_snapshot().
555 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800556 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800557 if timestamp == self.end_time:
558 continue
559 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800560 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800561 self.alive_children = None
562
563 def events(self):
564 """Gets children added/removed events of this subtree.
565
566 Returns:
567 list of (timestamp, deps_name, deps, end_flag):
568 timestamp: timestamp of event
569 deps_name: name of this subtree
570 deps: Deps object of given project
571 end_flag: True indicates this is the last event of this deps tree
572 """
573 assert self.snapshots
574 assert self.alive_children is None, ('events() is valid only after '
575 'no_more_snapshot() is invoked')
576
577 result = []
578
579 last_deps = None
580 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800581 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800582 last_deps = deps
583
584 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800585 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800586
587 for subtree in self.subtrees:
588 for event in subtree.events():
589 result.append(event)
590
Kuang-che Wu1ad2c0e2019-02-26 00:41:10 +0800591 result.sort(key=lambda x: x[0])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800592
593 return result
594
595 def iter_path_specs(self):
596 """Iterates snapshots of project dependency state.
597
598 Yields:
599 (timestamp, path_specs):
600 timestamp: time of snapshot
601 path_specs: dict of path_spec entries
602 """
603 forest = {}
604 # Group by timestamp
605 for timestamp, events in itertools.groupby(self.events(),
606 operator.itemgetter(0)):
607 # It's possible that one deps is removed and added at the same timestamp,
608 # i.e. modification, so use counter to track.
609 end_counter = collections.Counter()
610
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800611 for timestamp, entry, deps, end in events:
612 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800613 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800614 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800615 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800616 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800617
618 # Merge Deps at time `timestamp` into single path_specs.
619 path_specs = {}
620 for deps in forest.values():
621 for path, dep in deps.entries.items():
622 path_specs[path] = dep.as_path_spec()
623
624 yield timestamp, path_specs
625
626 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800627 for entry, count in end_counter.items():
628 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800629 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800630 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800631
632
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800633class DepsParser:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800634 """Gclient DEPS file parser."""
635
636 def __init__(self, project_root, code_storage):
637 self.project_root = project_root
638 self.code_storage = code_storage
639
Kuang-che Wu067ff292019-02-14 18:16:23 +0800640 def load_single_deps(self, content):
641
642 def var_function(name):
643 return '{%s}' % name
644
645 global_scope = dict(Var=var_function)
646 local_scope = {}
Kuang-che Wuc0baf752020-06-29 11:32:26 +0800647 six.exec_(content, global_scope, local_scope)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800648 return local_scope
649
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800650 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
651 """Parses DEPS file without recursion.
652
653 Args:
654 content: file content of DEPS file
655 parent_vars: variables inherent from parent DEPS
656 parent_path: project path of parent DEPS file
657
658 Returns:
659 Deps object
660 """
661
Kuang-che Wu067ff292019-02-14 18:16:23 +0800662 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800663 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800664
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800665 local_scope.setdefault('vars', {})
666 if parent_vars:
667 local_scope['vars'].update(parent_vars)
668 deps.variables = local_scope['vars']
669
670 # Warnings for old usages which we don't support.
671 for name in deps.variables:
672 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
673 logger.warning('%s is deprecated and not supported recursion syntax',
674 name)
675 if 'deps_os' in local_scope:
676 logger.warning('deps_os is no longer supported')
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800677 if local_scope.get('gclient_gn_args_from'):
678 logger.warning('gclient_gn_args_from is not supported')
679
680 if 'allowed_hosts' in local_scope:
681 deps.allowed_hosts = set(local_scope.get('allowed_hosts'))
682 deps.hooks = local_scope.get('hooks', [])
683 deps.pre_deps_hooks = local_scope.get('pre_deps_hooks', [])
684 deps.gn_args_file = local_scope.get('gclient_gn_args_file')
685 deps.gn_args = local_scope.get('gclient_gn_args', [])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800686
687 for path, dep_entry in local_scope['deps'].items():
Kuang-che Wu058ac042019-03-18 14:14:53 +0800688 path = path.format(**deps.variables)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800689 if local_scope.get('use_relative_paths', False):
690 path = os.path.join(parent_path, path)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800691 path = os.path.normpath(path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800692 dep = Dep(path, deps.variables, dep_entry)
693 if not dep.eval_condition():
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800694 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800695 continue
696
697 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
698 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800699 warning_key = ('dep_type', dep.dep_type, path)
700 if warning_key not in emitted_warnings:
701 emitted_warnings.add(warning_key)
702 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
703 path)
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800704 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800705 continue
706
707 deps.entries[path] = dep
708
709 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800710 for recurse_entry in local_scope.get('recursedeps', []):
711 # Normalize entries.
712 if isinstance(recurse_entry, tuple):
713 path, deps_file = recurse_entry
714 else:
715 assert isinstance(path, str)
716 path, deps_file = recurse_entry, 'DEPS'
717
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800718 if local_scope.get('use_relative_paths', False):
719 path = os.path.join(parent_path, path)
720 path = path.format(**deps.variables)
721 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800722 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800723 deps.recursedeps = recursedeps
724
725 return deps
726
727 def construct_deps_tree(self,
728 tstree,
729 repo_url,
730 at,
731 after,
732 before,
733 parent_vars=None,
734 parent_path='',
735 deps_file='DEPS'):
736 """Processes DEPS recursively of given time period.
737
738 This method parses all commits of DEPS between time `after` and `before`,
739 segments recursive dependencies into subtrees if they are changed, and
740 processes subtrees recursively.
741
742 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
743
744 Args:
745 tstree: TimeSeriesTree object
746 repo_url: remote repo url
747 at: branch or git commit id
748 after: begin of period
749 before: end of period
750 parent_vars: DEPS variables inherit from parent DEPS (including
751 custom_vars)
752 parent_path: the path of parent project of current DEPS file
753 deps_file: filename of DEPS file, relative to the git repo, repo_rul
754 """
755 if '://' in repo_url:
756 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800757 if not os.path.exists(git_repo):
758 with locking.lock_file(
759 os.path.join(self.code_storage.cache_dir,
760 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
761 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800762 else:
763 git_repo = repo_url
764
765 if git_util.is_git_rev(at):
766 history = [
767 (after, at),
768 (before, at),
769 ]
770 else:
771 history = git_util.get_history(
772 git_repo,
773 deps_file,
774 branch=at,
775 after=after,
776 before=before,
Zheng-Jie Chang313eec32020-02-18 16:17:07 +0800777 padding_begin=True,
778 padding_end=True)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800779 assert history
780
781 # If not equal, it means the file was deleted but is still referenced by
782 # its parent.
783 assert history[-1][0] == before
784
785 # TODO(kcwu): optimization: history[-1] is unused
786 for timestamp, git_rev in history[:-1]:
787 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
788
789 deps = self.parse_single_deps(
790 content, parent_vars=parent_vars, parent_path=parent_path)
791 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
792
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800793 tstree.no_more_snapshot()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800794
795 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800796 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800797 path_spec = subtree.parent_deps.entries[path].as_path_spec()
798 self.construct_deps_tree(
799 subtree,
800 path_spec.repo_url,
801 path_spec.at,
802 subtree.start_time,
803 subtree.end_time,
804 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800805 parent_path=path,
806 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800807
808 def enumerate_path_specs(self, start_time, end_time, path):
809 tstree = TimeSeriesTree(None, path, start_time, end_time)
810 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
811 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800812
813
814class GclientCache(codechange.CodeStorage):
815 """Gclient git cache."""
816
817 def __init__(self, cache_dir):
818 self.cache_dir = cache_dir
819
820 def _url_to_cache_dir(self, url):
821 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800822 parsed = urllib.parse.urlparse(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800823 norm_url = parsed.netloc + parsed.path
824 if norm_url.endswith('.git'):
825 norm_url = norm_url[:-len('.git')]
Kuang-che Wu5fef2912019-04-15 16:18:29 +0800826 norm_url = norm_url.replace('googlesource.com/a/', 'googlesource.com/')
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800827 return norm_url.replace('-', '--').replace('/', '-').lower()
828
829 def cached_git_root(self, repo_url):
830 cache_path = self._url_to_cache_dir(repo_url)
831 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800832
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800833 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800834 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800835
836 projects[path] = repo_url
837
Kuang-che Wu067ff292019-02-14 18:16:23 +0800838 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800839
840 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800841 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800842
843 if path in projects:
844 del projects[path]
845
Kuang-che Wu067ff292019-02-14 18:16:23 +0800846 write_gclient_entries(project_root, projects)