blob: 4597bde6d7377825dfe5756ac11ea94a30852b62 [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
447
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800448class TimeSeriesTree:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800449 """Data structure for generating snapshots of historical dependency tree.
450
451 This is a tree structure with time information. Each tree node represents not
452 only typical tree data and tree children information, but also historical
453 value of those tree data and tree children.
454
455 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
456 represent a DEPS file. The caller will add_snapshot() to add parsed result of
457 historical DEPS instances. After that, the tree root of this class can
458 reconstruct the every historical moment of the project dependency state.
459
460 This class is slight abstraction of git_util.get_history_recursively() to
461 support more than single git repo and be version control system independent.
462 """
463
464 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
465
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800466 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800467 """TimeSeriesTree constructor.
468
469 Args:
470 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800471 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800472 start_time: start time
473 end_time: end time
474 """
475 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800476 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800477 self.snapshots = {}
478 self.start_time = start_time
479 self.end_time = end_time
480
481 # Intermediate dict to keep track alive children for the time being.
482 # Maintained by add_snapshot() and no_more_snapshot().
483 self.alive_children = {}
484
485 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800486 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800487 # once in this list because they are removed and added back to the DEPS
488 # file.
489 self.subtrees = []
490
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800491 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800492 """Compares subtree of two Deps.
493
494 Args:
495 deps_a: Deps object
496 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800497 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800498
499 Returns:
500 True if the said subtree of these two Deps equal
501 """
502 # Need to compare variables because they may influence subtree parsing
503 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800504 path = child_entry[0]
505 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800506 deps_a.variables == deps_b.variables)
507
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800508 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800509 """Adds parsed DEPS result and children.
510
511 For example, if a given DEPS file has N revisions between start_time and
512 end_time, the caller should call this method N times to feed all parsed
513 results in order (timestamp increasing).
514
515 Args:
516 timestamp: timestamp of `deps`
517 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800518 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800519 """
520 assert timestamp not in self.snapshots
521 self.snapshots[timestamp] = deps
522
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800523 for child_entry in set(list(self.alive_children.keys()) + children_entries):
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800524 # `child_entry` is added at `timestamp`
525 if child_entry not in self.alive_children:
526 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800527
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800528 # `child_entry` is removed at `timestamp`
529 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800530 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800531 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
532 self.alive_children[child_entry][0], timestamp))
533 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800534
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800535 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800536 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800537 last_deps = self.alive_children[child_entry][1]
538 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800539 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800540 TimeSeriesTree(last_deps, child_entry,
541 self.alive_children[child_entry][0], timestamp))
542 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800543
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800544 def no_more_snapshot(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800545 """Indicates all snapshots are added.
546
547 add_snapshot() should not be invoked after no_more_snapshot().
548 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800549 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800550 if timestamp == self.end_time:
551 continue
552 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800553 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800554 self.alive_children = None
555
556 def events(self):
557 """Gets children added/removed events of this subtree.
558
559 Returns:
560 list of (timestamp, deps_name, deps, end_flag):
561 timestamp: timestamp of event
562 deps_name: name of this subtree
563 deps: Deps object of given project
564 end_flag: True indicates this is the last event of this deps tree
565 """
566 assert self.snapshots
567 assert self.alive_children is None, ('events() is valid only after '
568 'no_more_snapshot() is invoked')
569
570 result = []
571
572 last_deps = None
573 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800574 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800575 last_deps = deps
576
577 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800578 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800579
580 for subtree in self.subtrees:
581 for event in subtree.events():
582 result.append(event)
583
Kuang-che Wu1ad2c0e2019-02-26 00:41:10 +0800584 result.sort(key=lambda x: x[0])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800585
586 return result
587
588 def iter_path_specs(self):
589 """Iterates snapshots of project dependency state.
590
591 Yields:
592 (timestamp, path_specs):
593 timestamp: time of snapshot
594 path_specs: dict of path_spec entries
595 """
596 forest = {}
597 # Group by timestamp
598 for timestamp, events in itertools.groupby(self.events(),
599 operator.itemgetter(0)):
600 # It's possible that one deps is removed and added at the same timestamp,
601 # i.e. modification, so use counter to track.
602 end_counter = collections.Counter()
603
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800604 for timestamp, entry, deps, end in events:
605 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800606 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800607 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800608 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800609 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800610
611 # Merge Deps at time `timestamp` into single path_specs.
612 path_specs = {}
613 for deps in forest.values():
614 for path, dep in deps.entries.items():
615 path_specs[path] = dep.as_path_spec()
616
617 yield timestamp, path_specs
618
619 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800620 for entry, count in end_counter.items():
621 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800622 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800623 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800624
625
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800626class DepsParser:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800627 """Gclient DEPS file parser."""
628
629 def __init__(self, project_root, code_storage):
630 self.project_root = project_root
631 self.code_storage = code_storage
632
Kuang-che Wu067ff292019-02-14 18:16:23 +0800633 def load_single_deps(self, content):
634
635 def var_function(name):
636 return '{%s}' % name
637
638 global_scope = dict(Var=var_function)
639 local_scope = {}
640 try:
Kuang-che Wud2646f42019-11-27 18:31:08 +0800641 six.exec_(content, global_scope, local_scope)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800642 except SyntaxError:
643 raise
644
645 return local_scope
646
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800647 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
648 """Parses DEPS file without recursion.
649
650 Args:
651 content: file content of DEPS file
652 parent_vars: variables inherent from parent DEPS
653 parent_path: project path of parent DEPS file
654
655 Returns:
656 Deps object
657 """
658
Kuang-che Wu067ff292019-02-14 18:16:23 +0800659 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800660 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800661
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800662 local_scope.setdefault('vars', {})
663 if parent_vars:
664 local_scope['vars'].update(parent_vars)
665 deps.variables = local_scope['vars']
666
667 # Warnings for old usages which we don't support.
668 for name in deps.variables:
669 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
670 logger.warning('%s is deprecated and not supported recursion syntax',
671 name)
672 if 'deps_os' in local_scope:
673 logger.warning('deps_os is no longer supported')
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800674 if local_scope.get('gclient_gn_args_from'):
675 logger.warning('gclient_gn_args_from is not supported')
676
677 if 'allowed_hosts' in local_scope:
678 deps.allowed_hosts = set(local_scope.get('allowed_hosts'))
679 deps.hooks = local_scope.get('hooks', [])
680 deps.pre_deps_hooks = local_scope.get('pre_deps_hooks', [])
681 deps.gn_args_file = local_scope.get('gclient_gn_args_file')
682 deps.gn_args = local_scope.get('gclient_gn_args', [])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800683
684 for path, dep_entry in local_scope['deps'].items():
Kuang-che Wu058ac042019-03-18 14:14:53 +0800685 path = path.format(**deps.variables)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800686 if local_scope.get('use_relative_paths', False):
687 path = os.path.join(parent_path, path)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800688 path = os.path.normpath(path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800689 dep = Dep(path, deps.variables, dep_entry)
690 if not dep.eval_condition():
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800691 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800692 continue
693
694 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
695 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800696 warning_key = ('dep_type', dep.dep_type, path)
697 if warning_key not in emitted_warnings:
698 emitted_warnings.add(warning_key)
699 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
700 path)
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800701 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800702 continue
703
704 deps.entries[path] = dep
705
706 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800707 for recurse_entry in local_scope.get('recursedeps', []):
708 # Normalize entries.
709 if isinstance(recurse_entry, tuple):
710 path, deps_file = recurse_entry
711 else:
712 assert isinstance(path, str)
713 path, deps_file = recurse_entry, 'DEPS'
714
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800715 if local_scope.get('use_relative_paths', False):
716 path = os.path.join(parent_path, path)
717 path = path.format(**deps.variables)
718 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800719 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800720 deps.recursedeps = recursedeps
721
722 return deps
723
724 def construct_deps_tree(self,
725 tstree,
726 repo_url,
727 at,
728 after,
729 before,
730 parent_vars=None,
731 parent_path='',
732 deps_file='DEPS'):
733 """Processes DEPS recursively of given time period.
734
735 This method parses all commits of DEPS between time `after` and `before`,
736 segments recursive dependencies into subtrees if they are changed, and
737 processes subtrees recursively.
738
739 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
740
741 Args:
742 tstree: TimeSeriesTree object
743 repo_url: remote repo url
744 at: branch or git commit id
745 after: begin of period
746 before: end of period
747 parent_vars: DEPS variables inherit from parent DEPS (including
748 custom_vars)
749 parent_path: the path of parent project of current DEPS file
750 deps_file: filename of DEPS file, relative to the git repo, repo_rul
751 """
752 if '://' in repo_url:
753 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800754 if not os.path.exists(git_repo):
755 with locking.lock_file(
756 os.path.join(self.code_storage.cache_dir,
757 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
758 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800759 else:
760 git_repo = repo_url
761
762 if git_util.is_git_rev(at):
763 history = [
764 (after, at),
765 (before, at),
766 ]
767 else:
768 history = git_util.get_history(
769 git_repo,
770 deps_file,
771 branch=at,
772 after=after,
773 before=before,
Zheng-Jie Chang313eec32020-02-18 16:17:07 +0800774 padding_begin=True,
775 padding_end=True)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800776 assert history
777
778 # If not equal, it means the file was deleted but is still referenced by
779 # its parent.
780 assert history[-1][0] == before
781
782 # TODO(kcwu): optimization: history[-1] is unused
783 for timestamp, git_rev in history[:-1]:
784 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
785
786 deps = self.parse_single_deps(
787 content, parent_vars=parent_vars, parent_path=parent_path)
788 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
789
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800790 tstree.no_more_snapshot()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800791
792 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800793 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800794 path_spec = subtree.parent_deps.entries[path].as_path_spec()
795 self.construct_deps_tree(
796 subtree,
797 path_spec.repo_url,
798 path_spec.at,
799 subtree.start_time,
800 subtree.end_time,
801 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800802 parent_path=path,
803 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800804
805 def enumerate_path_specs(self, start_time, end_time, path):
806 tstree = TimeSeriesTree(None, path, start_time, end_time)
807 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
808 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800809
810
811class GclientCache(codechange.CodeStorage):
812 """Gclient git cache."""
813
814 def __init__(self, cache_dir):
815 self.cache_dir = cache_dir
816
817 def _url_to_cache_dir(self, url):
818 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800819 parsed = urllib.parse.urlparse(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800820 norm_url = parsed.netloc + parsed.path
821 if norm_url.endswith('.git'):
822 norm_url = norm_url[:-len('.git')]
Kuang-che Wu5fef2912019-04-15 16:18:29 +0800823 norm_url = norm_url.replace('googlesource.com/a/', 'googlesource.com/')
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800824 return norm_url.replace('-', '--').replace('/', '-').lower()
825
826 def cached_git_root(self, repo_url):
827 cache_path = self._url_to_cache_dir(repo_url)
828 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800829
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800830 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800831 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800832
833 projects[path] = repo_url
834
Kuang-che Wu067ff292019-02-14 18:16:23 +0800835 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800836
837 def remove_from_project_list(self, project_root, path):
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 if path in projects:
841 del projects[path]
842
Kuang-che Wu067ff292019-02-14 18:16:23 +0800843 write_gclient_entries(project_root, projects)