• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2# Copyright 2017 The LibYuv Project Authors. All rights reserved.
3#
4# Use of this source code is governed by a BSD-style license
5# that can be found in the LICENSE file in the root of the source
6# tree. An additional intellectual property rights grant can be found
7# in the file PATENTS. All contributing project authors may
8# be found in the AUTHORS file in the root of the source tree.
9
10# This is a modified copy of the script in
11# https://webrtc.googlesource.com/src/+/master/tools_webrtc/autoroller/roll_deps.py
12# customized for libyuv.
13
14
15"""Script to automatically roll dependencies in the libyuv DEPS file."""
16
17import argparse
18import base64
19import collections
20import logging
21import os
22import re
23import subprocess
24import sys
25import urllib2
26
27
28# Skip these dependencies (list without solution name prefix).
29DONT_AUTOROLL_THESE = [
30  'src/third_party/gflags/src',
31]
32
33LIBYUV_URL = 'https://chromium.googlesource.com/libyuv/libyuv'
34CHROMIUM_SRC_URL = 'https://chromium.googlesource.com/chromium/src'
35CHROMIUM_COMMIT_TEMPLATE = CHROMIUM_SRC_URL + '/+/%s'
36CHROMIUM_LOG_TEMPLATE = CHROMIUM_SRC_URL + '/+log/%s'
37CHROMIUM_FILE_TEMPLATE = CHROMIUM_SRC_URL + '/+/%s/%s'
38
39COMMIT_POSITION_RE = re.compile('^Cr-Commit-Position: .*#([0-9]+).*$')
40CLANG_REVISION_RE = re.compile(r'^CLANG_REVISION = \'(\d+)\'$')
41ROLL_BRANCH_NAME = 'roll_chromium_revision'
42
43SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
44CHECKOUT_SRC_DIR = os.path.realpath(os.path.join(SCRIPT_DIR, os.pardir,
45                                                 os.pardir))
46CHECKOUT_ROOT_DIR = os.path.realpath(os.path.join(CHECKOUT_SRC_DIR, os.pardir))
47
48sys.path.append(os.path.join(CHECKOUT_SRC_DIR, 'build'))
49import find_depot_tools
50find_depot_tools.add_depot_tools_to_path()
51
52CLANG_UPDATE_SCRIPT_URL_PATH = 'tools/clang/scripts/update.py'
53CLANG_UPDATE_SCRIPT_LOCAL_PATH = os.path.join(CHECKOUT_SRC_DIR, 'tools',
54                                              'clang', 'scripts', 'update.py')
55
56DepsEntry = collections.namedtuple('DepsEntry', 'path url revision')
57ChangedDep = collections.namedtuple('ChangedDep',
58                                    'path url current_rev new_rev')
59
60class RollError(Exception):
61  pass
62
63
64def VarLookup(local_scope):
65  return lambda var_name: local_scope['vars'][var_name]
66
67
68def ParseDepsDict(deps_content):
69  local_scope = {}
70  global_scope = {
71    'Var': VarLookup(local_scope),
72    'deps_os': {},
73  }
74  exec(deps_content, global_scope, local_scope)
75  return local_scope
76
77
78def ParseLocalDepsFile(filename):
79  with open(filename, 'rb') as f:
80    deps_content = f.read()
81  return ParseDepsDict(deps_content)
82
83
84def ParseRemoteCrDepsFile(revision):
85  deps_content = ReadRemoteCrFile('DEPS', revision)
86  return ParseDepsDict(deps_content)
87
88
89def ParseCommitPosition(commit_message):
90  for line in reversed(commit_message.splitlines()):
91    m = COMMIT_POSITION_RE.match(line.strip())
92    if m:
93      return int(m.group(1))
94  logging.error('Failed to parse commit position id from:\n%s\n',
95                commit_message)
96  sys.exit(-1)
97
98
99def _RunCommand(command, working_dir=None, ignore_exit_code=False,
100                extra_env=None):
101  """Runs a command and returns the output from that command.
102
103  If the command fails (exit code != 0), the function will exit the process.
104
105  Returns:
106    A tuple containing the stdout and stderr outputs as strings.
107  """
108  working_dir = working_dir or CHECKOUT_SRC_DIR
109  logging.debug('CMD: %s CWD: %s', ' '.join(command), working_dir)
110  env = os.environ.copy()
111  if extra_env:
112    assert all(isinstance(value, str) for value in extra_env.values())
113    logging.debug('extra env: %s', extra_env)
114    env.update(extra_env)
115  p = subprocess.Popen(command, stdout=subprocess.PIPE,
116                       stderr=subprocess.PIPE, env=env,
117                       cwd=working_dir, universal_newlines=True)
118  std_output = p.stdout.read()
119  err_output = p.stderr.read()
120  p.wait()
121  p.stdout.close()
122  p.stderr.close()
123  if not ignore_exit_code and p.returncode != 0:
124    logging.error('Command failed: %s\n'
125                  'stdout:\n%s\n'
126                  'stderr:\n%s\n', ' '.join(command), std_output, err_output)
127    sys.exit(p.returncode)
128  return std_output, err_output
129
130
131def _GetBranches():
132  """Returns a tuple of active,branches.
133
134  The 'active' is the name of the currently active branch and 'branches' is a
135  list of all branches.
136  """
137  lines = _RunCommand(['git', 'branch'])[0].split('\n')
138  branches = []
139  active = ''
140  for line in lines:
141    if '*' in line:
142      # The assumption is that the first char will always be the '*'.
143      active = line[1:].strip()
144      branches.append(active)
145    else:
146      branch = line.strip()
147      if branch:
148        branches.append(branch)
149  return active, branches
150
151
152def _ReadGitilesContent(url):
153  # Download and decode BASE64 content until
154  # https://code.google.com/p/gitiles/issues/detail?id=7 is fixed.
155  base64_content = ReadUrlContent(url + '?format=TEXT')
156  return base64.b64decode(base64_content[0])
157
158
159def ReadRemoteCrFile(path_below_src, revision):
160  """Reads a remote Chromium file of a specific revision. Returns a string."""
161  return _ReadGitilesContent(CHROMIUM_FILE_TEMPLATE % (revision,
162                                                       path_below_src))
163
164
165def ReadRemoteCrCommit(revision):
166  """Reads a remote Chromium commit message. Returns a string."""
167  return _ReadGitilesContent(CHROMIUM_COMMIT_TEMPLATE % revision)
168
169
170def ReadUrlContent(url):
171  """Connect to a remote host and read the contents. Returns a list of lines."""
172  conn = urllib2.urlopen(url)
173  try:
174    return conn.readlines()
175  except IOError as e:
176    logging.exception('Error connecting to %s. Error: %s', url, e)
177    raise
178  finally:
179    conn.close()
180
181
182def GetMatchingDepsEntries(depsentry_dict, dir_path):
183  """Gets all deps entries matching the provided path.
184
185  This list may contain more than one DepsEntry object.
186  Example: dir_path='src/testing' would give results containing both
187  'src/testing/gtest' and 'src/testing/gmock' deps entries for Chromium's DEPS.
188  Example 2: dir_path='src/build' should return 'src/build' but not
189  'src/buildtools'.
190
191  Returns:
192    A list of DepsEntry objects.
193  """
194  result = []
195  for path, depsentry in depsentry_dict.iteritems():
196    if path == dir_path:
197      result.append(depsentry)
198    else:
199      parts = path.split('/')
200      if all(part == parts[i]
201             for i, part in enumerate(dir_path.split('/'))):
202        result.append(depsentry)
203  return result
204
205
206def BuildDepsentryDict(deps_dict):
207  """Builds a dict of paths to DepsEntry objects from a raw parsed deps dict."""
208  result = {}
209  def AddDepsEntries(deps_subdict):
210    for path, deps_url_spec in deps_subdict.iteritems():
211      # The deps url is either an URL and a condition, or just the URL.
212      if isinstance(deps_url_spec, dict):
213        if deps_url_spec.get('dep_type') == 'cipd':
214          continue
215        deps_url = deps_url_spec['url']
216      else:
217        deps_url = deps_url_spec
218
219      if not result.has_key(path):
220        url, revision = deps_url.split('@') if deps_url else (None, None)
221        result[path] = DepsEntry(path, url, revision)
222
223  AddDepsEntries(deps_dict['deps'])
224  for deps_os in ['win', 'mac', 'unix', 'android', 'ios', 'unix']:
225    AddDepsEntries(deps_dict.get('deps_os', {}).get(deps_os, {}))
226  return result
227
228
229def CalculateChangedDeps(libyuv_deps, new_cr_deps):
230  """
231  Calculate changed deps entries based on entries defined in the libyuv DEPS
232  file:
233     - If a shared dependency with the Chromium DEPS file: roll it to the same
234       revision as Chromium (i.e. entry in the new_cr_deps dict)
235     - If it's a Chromium sub-directory, roll it to the HEAD revision (notice
236       this means it may be ahead of the chromium_revision, but generally these
237       should be close).
238     - If it's another DEPS entry (not shared with Chromium), roll it to HEAD
239       unless it's configured to be skipped.
240
241  Returns:
242    A list of ChangedDep objects representing the changed deps.
243  """
244  result = []
245  libyuv_entries = BuildDepsentryDict(libyuv_deps)
246  new_cr_entries = BuildDepsentryDict(new_cr_deps)
247  for path, libyuv_deps_entry in libyuv_entries.iteritems():
248    if path in DONT_AUTOROLL_THESE:
249      continue
250    cr_deps_entry = new_cr_entries.get(path)
251    if cr_deps_entry:
252      # Use the revision from Chromium's DEPS file.
253      new_rev = cr_deps_entry.revision
254      assert libyuv_deps_entry.url == cr_deps_entry.url, (
255        'Libyuv DEPS entry %s has a different URL (%s) than Chromium (%s).' %
256        (path, libyuv_deps_entry.url, cr_deps_entry.url))
257    else:
258      # Use the HEAD of the deps repo.
259      stdout, _ = _RunCommand(['git', 'ls-remote', libyuv_deps_entry.url,
260                               'HEAD'])
261      new_rev = stdout.strip().split('\t')[0]
262
263    # Check if an update is necessary.
264    if libyuv_deps_entry.revision != new_rev:
265      logging.debug('Roll dependency %s to %s', path, new_rev)
266      result.append(ChangedDep(path, libyuv_deps_entry.url,
267                               libyuv_deps_entry.revision, new_rev))
268  return sorted(result)
269
270
271def CalculateChangedClang(new_cr_rev):
272  def GetClangRev(lines):
273    for line in lines:
274      match = CLANG_REVISION_RE.match(line)
275      if match:
276        return match.group(1)
277    raise RollError('Could not parse Clang revision!')
278
279  with open(CLANG_UPDATE_SCRIPT_LOCAL_PATH, 'rb') as f:
280    current_lines = f.readlines()
281  current_rev = GetClangRev(current_lines)
282
283  new_clang_update_py = ReadRemoteCrFile(CLANG_UPDATE_SCRIPT_URL_PATH,
284                                             new_cr_rev).splitlines()
285  new_rev = GetClangRev(new_clang_update_py)
286  return ChangedDep(CLANG_UPDATE_SCRIPT_LOCAL_PATH, None, current_rev, new_rev)
287
288
289def GenerateCommitMessage(current_cr_rev, new_cr_rev, current_commit_pos,
290                          new_commit_pos, changed_deps_list, clang_change):
291  current_cr_rev = current_cr_rev[0:10]
292  new_cr_rev = new_cr_rev[0:10]
293  rev_interval = '%s..%s' % (current_cr_rev, new_cr_rev)
294  git_number_interval = '%s:%s' % (current_commit_pos, new_commit_pos)
295
296  commit_msg = ['Roll chromium_revision %s (%s)\n' % (rev_interval,
297                                                    git_number_interval)]
298  commit_msg.append('Change log: %s' % (CHROMIUM_LOG_TEMPLATE % rev_interval))
299  commit_msg.append('Full diff: %s\n' % (CHROMIUM_COMMIT_TEMPLATE %
300                                         rev_interval))
301  if changed_deps_list:
302    commit_msg.append('Changed dependencies:')
303
304    for c in changed_deps_list:
305      commit_msg.append('* %s: %s/+log/%s..%s' % (c.path, c.url,
306                                                  c.current_rev[0:10],
307                                                  c.new_rev[0:10]))
308    change_url = CHROMIUM_FILE_TEMPLATE % (rev_interval, 'DEPS')
309    commit_msg.append('DEPS diff: %s\n' % change_url)
310  else:
311    commit_msg.append('No dependencies changed.')
312
313  if clang_change.current_rev != clang_change.new_rev:
314    commit_msg.append('Clang version changed %s:%s' %
315                      (clang_change.current_rev, clang_change.new_rev))
316    change_url = CHROMIUM_FILE_TEMPLATE % (rev_interval,
317                                           CLANG_UPDATE_SCRIPT_URL_PATH)
318    commit_msg.append('Details: %s\n' % change_url)
319  else:
320    commit_msg.append('No update to Clang.\n')
321
322  # TBR needs to be non-empty for Gerrit to process it.
323  git_author = _RunCommand(['git', 'config', 'user.email'],
324                           working_dir=CHECKOUT_SRC_DIR)[0].strip()
325  commit_msg.append('TBR=%s' % git_author)
326
327  commit_msg.append('BUG=None')
328  return '\n'.join(commit_msg)
329
330
331def UpdateDepsFile(deps_filename, old_cr_revision, new_cr_revision,
332                   changed_deps):
333  """Update the DEPS file with the new revision."""
334
335  # Update the chromium_revision variable.
336  with open(deps_filename, 'rb') as deps_file:
337    deps_content = deps_file.read()
338  deps_content = deps_content.replace(old_cr_revision, new_cr_revision)
339  with open(deps_filename, 'wb') as deps_file:
340    deps_file.write(deps_content)
341
342  # Update each individual DEPS entry.
343  for dep in changed_deps:
344    local_dep_dir = os.path.join(CHECKOUT_ROOT_DIR, dep.path)
345    if not os.path.isdir(local_dep_dir):
346      raise RollError(
347          'Cannot find local directory %s. Make sure the .gclient file\n'
348          'contains all platforms in the target_os list, i.e.\n'
349          'target_os = ["android", "unix", "mac", "ios", "win"];\n'
350          'Then run "gclient sync" again.' % local_dep_dir)
351    _RunCommand(
352      ['gclient', 'setdep', '--revision', '%s@%s' % (dep.path, dep.new_rev)],
353      working_dir=CHECKOUT_SRC_DIR)
354
355
356def _IsTreeClean():
357  stdout, _ = _RunCommand(['git', 'status', '--porcelain'])
358  if len(stdout) == 0:
359    return True
360
361  logging.error('Dirty/unversioned files:\n%s', stdout)
362  return False
363
364
365def _EnsureUpdatedMasterBranch(dry_run):
366  current_branch = _RunCommand(
367      ['git', 'rev-parse', '--abbrev-ref', 'HEAD'])[0].splitlines()[0]
368  if current_branch != 'master':
369    logging.error('Please checkout the master branch and re-run this script.')
370    if not dry_run:
371      sys.exit(-1)
372
373  logging.info('Updating master branch...')
374  _RunCommand(['git', 'pull'])
375
376
377def _CreateRollBranch(dry_run):
378  logging.info('Creating roll branch: %s', ROLL_BRANCH_NAME)
379  if not dry_run:
380    _RunCommand(['git', 'checkout', '-b', ROLL_BRANCH_NAME])
381
382
383def _RemovePreviousRollBranch(dry_run):
384  active_branch, branches = _GetBranches()
385  if active_branch == ROLL_BRANCH_NAME:
386    active_branch = 'master'
387  if ROLL_BRANCH_NAME in branches:
388    logging.info('Removing previous roll branch (%s)', ROLL_BRANCH_NAME)
389    if not dry_run:
390      _RunCommand(['git', 'checkout', active_branch])
391      _RunCommand(['git', 'branch', '-D', ROLL_BRANCH_NAME])
392
393
394def _LocalCommit(commit_msg, dry_run):
395  logging.info('Committing changes locally.')
396  if not dry_run:
397    _RunCommand(['git', 'add', '--update', '.'])
398    _RunCommand(['git', 'commit', '-m', commit_msg])
399
400
401def ChooseCQMode(skip_cq, cq_over, current_commit_pos, new_commit_pos):
402  if skip_cq:
403    return 0
404  if (new_commit_pos - current_commit_pos) < cq_over:
405    return 1
406  return 2
407
408
409def _UploadCL(commit_queue_mode):
410  """Upload the committed changes as a changelist to Gerrit.
411
412  commit_queue_mode:
413    - 2: Submit to commit queue.
414    - 1: Run trybots but do not submit to CQ.
415    - 0: Skip CQ, upload only.
416  """
417  cmd = ['git', 'cl', 'upload', '--force', '--bypass-hooks', '--send-mail']
418  if commit_queue_mode >= 2:
419    logging.info('Sending the CL to the CQ...')
420    cmd.extend(['--use-commit-queue'])
421  elif commit_queue_mode >= 1:
422    logging.info('Starting CQ dry run...')
423    cmd.extend(['--cq-dry-run'])
424  extra_env = {
425      'EDITOR': 'true',
426      'SKIP_GCE_AUTH_FOR_GIT': '1',
427  }
428  stdout, stderr = _RunCommand(cmd, extra_env=extra_env)
429  logging.debug('Output from "git cl upload":\nstdout:\n%s\n\nstderr:\n%s',
430      stdout, stderr)
431
432
433def main():
434  p = argparse.ArgumentParser()
435  p.add_argument('--clean', action='store_true', default=False,
436                 help='Removes any previous local roll branch.')
437  p.add_argument('-r', '--revision',
438                 help=('Chromium Git revision to roll to. Defaults to the '
439                       'Chromium HEAD revision if omitted.'))
440  p.add_argument('--dry-run', action='store_true', default=False,
441                 help=('Calculate changes and modify DEPS, but don\'t create '
442                       'any local branch, commit, upload CL or send any '
443                       'tryjobs.'))
444  p.add_argument('-i', '--ignore-unclean-workdir', action='store_true',
445                 default=False,
446                 help=('Ignore if the current branch is not master or if there '
447                       'are uncommitted changes (default: %(default)s).'))
448  grp = p.add_mutually_exclusive_group()
449  grp.add_argument('--skip-cq', action='store_true', default=False,
450                   help='Skip sending the CL to the CQ (default: %(default)s)')
451  grp.add_argument('--cq-over', type=int, default=1,
452                   help=('Commit queue dry run if the revision difference '
453                         'is below this number (default: %(default)s)'))
454  p.add_argument('-v', '--verbose', action='store_true', default=False,
455                 help='Be extra verbose in printing of log messages.')
456  opts = p.parse_args()
457
458  if opts.verbose:
459    logging.basicConfig(level=logging.DEBUG)
460  else:
461    logging.basicConfig(level=logging.INFO)
462
463  if not opts.ignore_unclean_workdir and not _IsTreeClean():
464    logging.error('Please clean your local checkout first.')
465    return 1
466
467  if opts.clean:
468    _RemovePreviousRollBranch(opts.dry_run)
469
470  if not opts.ignore_unclean_workdir:
471    _EnsureUpdatedMasterBranch(opts.dry_run)
472
473  new_cr_rev = opts.revision
474  if not new_cr_rev:
475    stdout, _ = _RunCommand(['git', 'ls-remote', CHROMIUM_SRC_URL, 'HEAD'])
476    head_rev = stdout.strip().split('\t')[0]
477    logging.info('No revision specified. Using HEAD: %s', head_rev)
478    new_cr_rev = head_rev
479
480  deps_filename = os.path.join(CHECKOUT_SRC_DIR, 'DEPS')
481  libyuv_deps = ParseLocalDepsFile(deps_filename)
482  current_cr_rev = libyuv_deps['vars']['chromium_revision']
483
484  current_commit_pos = ParseCommitPosition(ReadRemoteCrCommit(current_cr_rev))
485  new_commit_pos = ParseCommitPosition(ReadRemoteCrCommit(new_cr_rev))
486
487  new_cr_deps = ParseRemoteCrDepsFile(new_cr_rev)
488  changed_deps = CalculateChangedDeps(libyuv_deps, new_cr_deps)
489  clang_change = CalculateChangedClang(new_cr_rev)
490  commit_msg = GenerateCommitMessage(current_cr_rev, new_cr_rev,
491                                     current_commit_pos, new_commit_pos,
492                                     changed_deps, clang_change)
493  logging.debug('Commit message:\n%s', commit_msg)
494
495  _CreateRollBranch(opts.dry_run)
496  UpdateDepsFile(deps_filename, current_cr_rev, new_cr_rev, changed_deps)
497  _LocalCommit(commit_msg, opts.dry_run)
498  commit_queue_mode = ChooseCQMode(opts.skip_cq, opts.cq_over,
499                                   current_commit_pos, new_commit_pos)
500  logging.info('Uploading CL...')
501  if not opts.dry_run:
502    _UploadCL(commit_queue_mode)
503  return 0
504
505
506if __name__ == '__main__':
507  sys.exit(main())
508