blob: 6a817ce21595b803aaa9ef82485104f8adf44722 [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 = {}
647 try:
Kuang-che Wud2646f42019-11-27 18:31:08 +0800648 six.exec_(content, global_scope, local_scope)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800649 except SyntaxError:
650 raise
651
652 return local_scope
653
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800654 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
655 """Parses DEPS file without recursion.
656
657 Args:
658 content: file content of DEPS file
659 parent_vars: variables inherent from parent DEPS
660 parent_path: project path of parent DEPS file
661
662 Returns:
663 Deps object
664 """
665
Kuang-che Wu067ff292019-02-14 18:16:23 +0800666 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800667 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800668
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800669 local_scope.setdefault('vars', {})
670 if parent_vars:
671 local_scope['vars'].update(parent_vars)
672 deps.variables = local_scope['vars']
673
674 # Warnings for old usages which we don't support.
675 for name in deps.variables:
676 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
677 logger.warning('%s is deprecated and not supported recursion syntax',
678 name)
679 if 'deps_os' in local_scope:
680 logger.warning('deps_os is no longer supported')
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800681 if local_scope.get('gclient_gn_args_from'):
682 logger.warning('gclient_gn_args_from is not supported')
683
684 if 'allowed_hosts' in local_scope:
685 deps.allowed_hosts = set(local_scope.get('allowed_hosts'))
686 deps.hooks = local_scope.get('hooks', [])
687 deps.pre_deps_hooks = local_scope.get('pre_deps_hooks', [])
688 deps.gn_args_file = local_scope.get('gclient_gn_args_file')
689 deps.gn_args = local_scope.get('gclient_gn_args', [])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800690
691 for path, dep_entry in local_scope['deps'].items():
Kuang-che Wu058ac042019-03-18 14:14:53 +0800692 path = path.format(**deps.variables)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800693 if local_scope.get('use_relative_paths', False):
694 path = os.path.join(parent_path, path)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800695 path = os.path.normpath(path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800696 dep = Dep(path, deps.variables, dep_entry)
697 if not dep.eval_condition():
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800698 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800699 continue
700
701 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
702 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800703 warning_key = ('dep_type', dep.dep_type, path)
704 if warning_key not in emitted_warnings:
705 emitted_warnings.add(warning_key)
706 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
707 path)
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800708 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800709 continue
710
711 deps.entries[path] = dep
712
713 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800714 for recurse_entry in local_scope.get('recursedeps', []):
715 # Normalize entries.
716 if isinstance(recurse_entry, tuple):
717 path, deps_file = recurse_entry
718 else:
719 assert isinstance(path, str)
720 path, deps_file = recurse_entry, 'DEPS'
721
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800722 if local_scope.get('use_relative_paths', False):
723 path = os.path.join(parent_path, path)
724 path = path.format(**deps.variables)
725 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800726 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800727 deps.recursedeps = recursedeps
728
729 return deps
730
731 def construct_deps_tree(self,
732 tstree,
733 repo_url,
734 at,
735 after,
736 before,
737 parent_vars=None,
738 parent_path='',
739 deps_file='DEPS'):
740 """Processes DEPS recursively of given time period.
741
742 This method parses all commits of DEPS between time `after` and `before`,
743 segments recursive dependencies into subtrees if they are changed, and
744 processes subtrees recursively.
745
746 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
747
748 Args:
749 tstree: TimeSeriesTree object
750 repo_url: remote repo url
751 at: branch or git commit id
752 after: begin of period
753 before: end of period
754 parent_vars: DEPS variables inherit from parent DEPS (including
755 custom_vars)
756 parent_path: the path of parent project of current DEPS file
757 deps_file: filename of DEPS file, relative to the git repo, repo_rul
758 """
759 if '://' in repo_url:
760 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800761 if not os.path.exists(git_repo):
762 with locking.lock_file(
763 os.path.join(self.code_storage.cache_dir,
764 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
765 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800766 else:
767 git_repo = repo_url
768
769 if git_util.is_git_rev(at):
770 history = [
771 (after, at),
772 (before, at),
773 ]
774 else:
775 history = git_util.get_history(
776 git_repo,
777 deps_file,
778 branch=at,
779 after=after,
780 before=before,
Zheng-Jie Chang313eec32020-02-18 16:17:07 +0800781 padding_begin=True,
782 padding_end=True)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800783 assert history
784
785 # If not equal, it means the file was deleted but is still referenced by
786 # its parent.
787 assert history[-1][0] == before
788
789 # TODO(kcwu): optimization: history[-1] is unused
790 for timestamp, git_rev in history[:-1]:
791 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
792
793 deps = self.parse_single_deps(
794 content, parent_vars=parent_vars, parent_path=parent_path)
795 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
796
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800797 tstree.no_more_snapshot()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800798
799 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800800 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800801 path_spec = subtree.parent_deps.entries[path].as_path_spec()
802 self.construct_deps_tree(
803 subtree,
804 path_spec.repo_url,
805 path_spec.at,
806 subtree.start_time,
807 subtree.end_time,
808 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800809 parent_path=path,
810 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800811
812 def enumerate_path_specs(self, start_time, end_time, path):
813 tstree = TimeSeriesTree(None, path, start_time, end_time)
814 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
815 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800816
817
818class GclientCache(codechange.CodeStorage):
819 """Gclient git cache."""
820
821 def __init__(self, cache_dir):
822 self.cache_dir = cache_dir
823
824 def _url_to_cache_dir(self, url):
825 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800826 parsed = urllib.parse.urlparse(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800827 norm_url = parsed.netloc + parsed.path
828 if norm_url.endswith('.git'):
829 norm_url = norm_url[:-len('.git')]
Kuang-che Wu5fef2912019-04-15 16:18:29 +0800830 norm_url = norm_url.replace('googlesource.com/a/', 'googlesource.com/')
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800831 return norm_url.replace('-', '--').replace('/', '-').lower()
832
833 def cached_git_root(self, repo_url):
834 cache_path = self._url_to_cache_dir(repo_url)
835 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800836
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800837 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800838 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800839
840 projects[path] = repo_url
841
Kuang-che Wu067ff292019-02-14 18:16:23 +0800842 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800843
844 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800845 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800846
847 if path in projects:
848 del projects[path]
849
Kuang-che Wu067ff292019-02-14 18:16:23 +0800850 write_gclient_entries(project_root, projects)