1#!/usr/bin/env python
2# Copyright (c) 2015 The WebRTC 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"""Script to automatically roll dependencies in the WebRTC DEPS file."""
11
12import argparse
13import base64
14import collections
15import logging
16import os
17import re
18import subprocess
19import sys
20import urllib2
21
22def FindSrcDirPath():
23  """Returns the abs path to the src/ dir of the project."""
24  src_dir = os.path.dirname(os.path.abspath(__file__))
25  while os.path.basename(src_dir) != 'src':
26    src_dir = os.path.normpath(os.path.join(src_dir, os.pardir))
27  return src_dir
28
29# Skip these dependencies (list without solution name prefix).
30DONT_AUTOROLL_THESE = [
31  'src/examples/androidtests/third_party/gradle',
32]
33
34# These dependencies are missing in chromium/src/DEPS, either unused or already
35# in-tree. For instance, src/base is a part of the Chromium source git repo,
36# but we pull it through a subtree mirror, so therefore it isn't listed in
37# Chromium's deps but it is in ours.
38WEBRTC_ONLY_DEPS = [
39  'src/base',
40  'src/build',
41  'src/buildtools',
42  'src/ios',
43  'src/testing',
44  'src/third_party',
45  'src/third_party/findbugs',
46  'src/third_party/gtest-parallel',
47  'src/third_party/yasm/binaries',
48  'src/tools',
49]
50
51
52WEBRTC_URL = 'https://webrtc.googlesource.com/src'
53CHROMIUM_SRC_URL = 'https://chromium.googlesource.com/chromium/src'
54CHROMIUM_COMMIT_TEMPLATE = CHROMIUM_SRC_URL + '/+/%s'
55CHROMIUM_LOG_TEMPLATE = CHROMIUM_SRC_URL + '/+log/%s'
56CHROMIUM_FILE_TEMPLATE = CHROMIUM_SRC_URL + '/+/%s/%s'
57
58COMMIT_POSITION_RE = re.compile('^Cr-Commit-Position: .*#([0-9]+).*$')
59CLANG_REVISION_RE = re.compile(r'^CLANG_REVISION = \'([0-9a-z]+)\'$')
60ROLL_BRANCH_NAME = 'roll_chromium_revision'
61
62SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
63CHECKOUT_SRC_DIR = FindSrcDirPath()
64CHECKOUT_ROOT_DIR = os.path.realpath(os.path.join(CHECKOUT_SRC_DIR, os.pardir))
65
66# Copied from tools/android/roll/android_deps/.../BuildConfigGenerator.groovy.
67ANDROID_DEPS_START = r'=== ANDROID_DEPS Generated Code Start ==='
68ANDROID_DEPS_END = r'=== ANDROID_DEPS Generated Code End ==='
69# Location of automically gathered android deps.
70ANDROID_DEPS_PATH = 'src/third_party/android_deps/'
71
72NOTIFY_EMAIL = 'webrtc-trooper@grotations.appspotmail.com'
73
74
75sys.path.append(os.path.join(CHECKOUT_SRC_DIR, 'build'))
76import find_depot_tools
77
78find_depot_tools.add_depot_tools_to_path()
79
80CLANG_UPDATE_SCRIPT_URL_PATH = 'tools/clang/scripts/update.py'
81CLANG_UPDATE_SCRIPT_LOCAL_PATH = os.path.join(CHECKOUT_SRC_DIR, 'tools',
82                                              'clang', 'scripts', 'update.py')
83
84DepsEntry = collections.namedtuple('DepsEntry', 'path url revision')
85ChangedDep = collections.namedtuple(
86    'ChangedDep', 'path url current_rev new_rev')
87CipdDepsEntry = collections.namedtuple('CipdDepsEntry', 'path packages')
88ChangedCipdPackage = collections.namedtuple(
89    'ChangedCipdPackage', 'path package current_version new_version')
90
91ChromiumRevisionUpdate = collections.namedtuple('ChromiumRevisionUpdate',
92                                                ('current_chromium_rev '
93                                                 'new_chromium_rev '))
94
95
96class RollError(Exception):
97  pass
98
99
100def StrExpansion():
101  return lambda str_value: str_value
102
103
104def VarLookup(local_scope):
105  return lambda var_name: local_scope['vars'][var_name]
106
107
108def ParseDepsDict(deps_content):
109  local_scope = {}
110  global_scope = {
111    'Str': StrExpansion(),
112    'Var': VarLookup(local_scope),
113    'deps_os': {},
114  }
115  exec (deps_content, global_scope, local_scope)
116  return local_scope
117
118
119def ParseLocalDepsFile(filename):
120  with open(filename, 'rb') as f:
121    deps_content = f.read()
122  return ParseDepsDict(deps_content)
123
124
125def ParseCommitPosition(commit_message):
126  for line in reversed(commit_message.splitlines()):
127    m = COMMIT_POSITION_RE.match(line.strip())
128    if m:
129      return int(m.group(1))
130  logging.error('Failed to parse commit position id from:\n%s\n',
131                commit_message)
132  sys.exit(-1)
133
134
135def _RunCommand(command, working_dir=None, ignore_exit_code=False,
136    extra_env=None, input_data=None):
137  """Runs a command and returns the output from that command.
138
139  If the command fails (exit code != 0), the function will exit the process.
140
141  Returns:
142    A tuple containing the stdout and stderr outputs as strings.
143  """
144  working_dir = working_dir or CHECKOUT_SRC_DIR
145  logging.debug('CMD: %s CWD: %s', ' '.join(command), working_dir)
146  env = os.environ.copy()
147  if extra_env:
148    assert all(isinstance(value, str) for value in extra_env.values())
149    logging.debug('extra env: %s', extra_env)
150    env.update(extra_env)
151  p = subprocess.Popen(command,
152                       stdin=subprocess.PIPE,
153                       stdout=subprocess.PIPE,
154                       stderr=subprocess.PIPE, env=env,
155                       cwd=working_dir, universal_newlines=True)
156  std_output, err_output = p.communicate(input_data)
157  p.stdout.close()
158  p.stderr.close()
159  if not ignore_exit_code and p.returncode != 0:
160    logging.error('Command failed: %s\n'
161                  'stdout:\n%s\n'
162                  'stderr:\n%s\n', ' '.join(command), std_output, err_output)
163    sys.exit(p.returncode)
164  return std_output, err_output
165
166
167def _GetBranches():
168  """Returns a tuple of active,branches.
169
170  The 'active' is the name of the currently active branch and 'branches' is a
171  list of all branches.
172  """
173  lines = _RunCommand(['git', 'branch'])[0].split('\n')
174  branches = []
175  active = ''
176  for line in lines:
177    if '*' in line:
178      # The assumption is that the first char will always be the '*'.
179      active = line[1:].strip()
180      branches.append(active)
181    else:
182      branch = line.strip()
183      if branch:
184        branches.append(branch)
185  return active, branches
186
187
188def _ReadGitilesContent(url):
189  # Download and decode BASE64 content until
190  # https://code.google.com/p/gitiles/issues/detail?id=7 is fixed.
191  base64_content = ReadUrlContent(url + '?format=TEXT')
192  return base64.b64decode(base64_content[0])
193
194
195def ReadRemoteCrFile(path_below_src, revision):
196  """Reads a remote Chromium file of a specific revision. Returns a string."""
197  return _ReadGitilesContent(CHROMIUM_FILE_TEMPLATE % (revision,
198                                                       path_below_src))
199
200
201def ReadRemoteCrCommit(revision):
202  """Reads a remote Chromium commit message. Returns a string."""
203  return _ReadGitilesContent(CHROMIUM_COMMIT_TEMPLATE % revision)
204
205
206def ReadUrlContent(url):
207  """Connect to a remote host and read the contents. Returns a list of lines."""
208  conn = urllib2.urlopen(url)
209  try:
210    return conn.readlines()
211  except IOError as e:
212    logging.exception('Error connecting to %s. Error: %s', url, e)
213    raise
214  finally:
215    conn.close()
216
217
218def GetMatchingDepsEntries(depsentry_dict, dir_path):
219  """Gets all deps entries matching the provided path.
220
221  This list may contain more than one DepsEntry object.
222  Example: dir_path='src/testing' would give results containing both
223  'src/testing/gtest' and 'src/testing/gmock' deps entries for Chromium's DEPS.
224  Example 2: dir_path='src/build' should return 'src/build' but not
225  'src/buildtools'.
226
227  Returns:
228    A list of DepsEntry objects.
229  """
230  result = []
231  for path, depsentry in depsentry_dict.iteritems():
232    if path == dir_path:
233      result.append(depsentry)
234    else:
235      parts = path.split('/')
236      if all(part == parts[i]
237             for i, part in enumerate(dir_path.split('/'))):
238        result.append(depsentry)
239  return result
240
241
242def BuildDepsentryDict(deps_dict):
243  """Builds a dict of paths to DepsEntry objects from a raw parsed deps dict."""
244  result = {}
245
246  def AddDepsEntries(deps_subdict):
247    for path, dep in deps_subdict.iteritems():
248      if path in result:
249        continue
250      if not isinstance(dep, dict):
251        dep = {'url': dep}
252      if dep.get('dep_type') == 'cipd':
253        result[path] = CipdDepsEntry(path, dep['packages'])
254      else:
255        if '@' not in dep['url']:
256          continue
257        url, revision = dep['url'].split('@')
258        result[path] = DepsEntry(path, url, revision)
259
260  AddDepsEntries(deps_dict['deps'])
261  for deps_os in ['win', 'mac', 'unix', 'android', 'ios', 'unix']:
262    AddDepsEntries(deps_dict.get('deps_os', {}).get(deps_os, {}))
263  return result
264
265
266def _FindChangedCipdPackages(path, old_pkgs, new_pkgs):
267  pkgs_equal = ({p['package'] for p in old_pkgs} ==
268      {p['package'] for p in new_pkgs})
269  assert pkgs_equal, ('Old: %s\n New: %s.\nYou need to do a manual roll '
270                      'and remove/add entries in DEPS so the old and new '
271                      'list match.' % (old_pkgs, new_pkgs))
272  for old_pkg in old_pkgs:
273    for new_pkg in new_pkgs:
274      old_version = old_pkg['version']
275      new_version = new_pkg['version']
276      if (old_pkg['package'] == new_pkg['package'] and
277          old_version != new_version):
278        logging.debug('Roll dependency %s to %s', path, new_version)
279        yield ChangedCipdPackage(path, old_pkg['package'],
280                                 old_version, new_version)
281
282
283def _FindNewDeps(old, new):
284  """ Gather dependencies only in |new| and return corresponding paths. """
285  old_entries = set(BuildDepsentryDict(old))
286  new_entries = set(BuildDepsentryDict(new))
287  return [path for path in new_entries - old_entries
288          if path not in DONT_AUTOROLL_THESE]
289
290
291def FindAddedDeps(webrtc_deps, new_cr_deps):
292  """
293  Calculate new deps entries of interest.
294
295  Ideally, that would mean: only appearing in chromium DEPS
296  but transitively used in WebRTC.
297
298  Since it's hard to compute, we restrict ourselves to a well defined subset:
299  deps sitting in |ANDROID_DEPS_PATH|.
300  Otherwise, assumes that's a Chromium-only dependency.
301
302  Args:
303    webrtc_deps: dict of deps as defined in the WebRTC DEPS file.
304    new_cr_deps: dict of deps as defined in the chromium DEPS file.
305
306  Caveat: Doesn't detect a new package in existing dep.
307
308  Returns:
309    A tuple consisting of:
310      A list of paths added dependencies sitting in |ANDROID_DEPS_PATH|.
311      A list of paths for other added dependencies.
312  """
313  all_added_deps = _FindNewDeps(webrtc_deps, new_cr_deps)
314  generated_android_deps = [path for path in all_added_deps
315                            if path.startswith(ANDROID_DEPS_PATH)]
316  other_deps = [path for path in all_added_deps
317                if path not in generated_android_deps]
318  return generated_android_deps, other_deps
319
320
321def FindRemovedDeps(webrtc_deps, new_cr_deps):
322  """
323  Calculate obsolete deps entries.
324
325  Ideally, that would mean: no more appearing in chromium DEPS
326  and not used in WebRTC.
327
328  Since it's hard to compute:
329   1/ We restrict ourselves to a well defined subset:
330      deps sitting in |ANDROID_DEPS_PATH|.
331   2/ We rely on existing behavior of CalculateChangeDeps.
332      I.e. Assumes non-CIPD dependencies are WebRTC-only, don't remove them.
333
334  Args:
335    webrtc_deps: dict of deps as defined in the WebRTC DEPS file.
336    new_cr_deps: dict of deps as defined in the chromium DEPS file.
337
338  Caveat: Doesn't detect a deleted package in existing dep.
339
340  Returns:
341    A tuple consisting of:
342      A list of paths of dependencies removed from |ANDROID_DEPS_PATH|.
343      A list of paths of unexpected disappearing dependencies.
344  """
345  all_removed_deps = _FindNewDeps(new_cr_deps, webrtc_deps)
346  generated_android_deps = [path for path in all_removed_deps
347                            if path.startswith(ANDROID_DEPS_PATH)]
348  # Webrtc-only dependencies are handled in CalculateChangedDeps.
349  other_deps = [path for path in all_removed_deps
350                if path not in generated_android_deps and
351                   path not in WEBRTC_ONLY_DEPS]
352  return generated_android_deps, other_deps
353
354
355def CalculateChangedDeps(webrtc_deps, new_cr_deps):
356  """
357  Calculate changed deps entries based on entries defined in the WebRTC DEPS
358  file:
359     - If a shared dependency with the Chromium DEPS file: roll it to the same
360       revision as Chromium (i.e. entry in the new_cr_deps dict)
361     - If it's a Chromium sub-directory, roll it to the HEAD revision (notice
362       this means it may be ahead of the chromium_revision, but generally these
363       should be close).
364     - If it's another DEPS entry (not shared with Chromium), roll it to HEAD
365       unless it's configured to be skipped.
366
367  Returns:
368    A list of ChangedDep objects representing the changed deps.
369  """
370  result = []
371  webrtc_entries = BuildDepsentryDict(webrtc_deps)
372  new_cr_entries = BuildDepsentryDict(new_cr_deps)
373  for path, webrtc_deps_entry in webrtc_entries.iteritems():
374    if path in DONT_AUTOROLL_THESE:
375      continue
376    cr_deps_entry = new_cr_entries.get(path)
377    if cr_deps_entry:
378      assert type(cr_deps_entry) is type(webrtc_deps_entry)
379
380      if isinstance(cr_deps_entry, CipdDepsEntry):
381        result.extend(_FindChangedCipdPackages(path, webrtc_deps_entry.packages,
382                                               cr_deps_entry.packages))
383        continue
384
385      # Use the revision from Chromium's DEPS file.
386      new_rev = cr_deps_entry.revision
387      assert webrtc_deps_entry.url == cr_deps_entry.url, (
388          'WebRTC DEPS entry %s has a different URL (%s) than Chromium (%s).' %
389          (path, webrtc_deps_entry.url, cr_deps_entry.url))
390    else:
391      if isinstance(webrtc_deps_entry, DepsEntry):
392        # Use the HEAD of the deps repo.
393        stdout, _ = _RunCommand(['git', 'ls-remote', webrtc_deps_entry.url,
394                                'HEAD'])
395        new_rev = stdout.strip().split('\t')[0]
396      else:
397        # The dependency has been removed from chromium.
398        # This is handled by FindRemovedDeps.
399        continue
400
401    # Check if an update is necessary.
402    if webrtc_deps_entry.revision != new_rev:
403      logging.debug('Roll dependency %s to %s', path, new_rev)
404      result.append(ChangedDep(path, webrtc_deps_entry.url,
405                               webrtc_deps_entry.revision, new_rev))
406  return sorted(result)
407
408
409def CalculateChangedClang(new_cr_rev):
410  def GetClangRev(lines):
411    for line in lines:
412      match = CLANG_REVISION_RE.match(line)
413      if match:
414        return match.group(1)
415    raise RollError('Could not parse Clang revision!')
416
417  with open(CLANG_UPDATE_SCRIPT_LOCAL_PATH, 'rb') as f:
418    current_lines = f.readlines()
419  current_rev = GetClangRev(current_lines)
420
421  new_clang_update_py = ReadRemoteCrFile(CLANG_UPDATE_SCRIPT_URL_PATH,
422                                         new_cr_rev).splitlines()
423  new_rev = GetClangRev(new_clang_update_py)
424  return ChangedDep(CLANG_UPDATE_SCRIPT_LOCAL_PATH, None, current_rev, new_rev)
425
426
427def GenerateCommitMessage(rev_update, current_commit_pos, new_commit_pos,
428                          changed_deps_list,
429                          added_deps_paths=None,
430                          removed_deps_paths=None,
431                          clang_change=None,
432                          ):
433  current_cr_rev = rev_update.current_chromium_rev[0:10]
434  new_cr_rev = rev_update.new_chromium_rev[0:10]
435  rev_interval = '%s..%s' % (current_cr_rev, new_cr_rev)
436  git_number_interval = '%s:%s' % (current_commit_pos, new_commit_pos)
437
438  commit_msg = ['Roll chromium_revision %s (%s)\n' % (rev_interval,
439                                                      git_number_interval),
440                'Change log: %s' % (CHROMIUM_LOG_TEMPLATE % rev_interval),
441                'Full diff: %s\n' % (CHROMIUM_COMMIT_TEMPLATE %
442                                     rev_interval)]
443
444  def Section(adjective, deps):
445    noun = 'dependency' if len(deps) == 1 else 'dependencies'
446    commit_msg.append('%s %s' % (adjective, noun))
447
448  tbr_authors = ''
449  if changed_deps_list:
450    Section('Changed', changed_deps_list)
451
452    for c in changed_deps_list:
453      if isinstance(c, ChangedCipdPackage):
454        commit_msg.append('* %s: %s..%s' % (c.path, c.current_version,
455                                            c.new_version))
456      else:
457        commit_msg.append('* %s: %s/+log/%s..%s' % (c.path, c.url,
458                                                    c.current_rev[0:10],
459                                                    c.new_rev[0:10]))
460      if 'libvpx' in c.path:
461        tbr_authors += 'marpan@webrtc.org, jianj@chromium.org, '
462
463  if added_deps_paths:
464    Section('Added', added_deps_paths)
465    commit_msg.extend('* %s' % p for p in added_deps_paths)
466
467  if removed_deps_paths:
468    Section('Removed', removed_deps_paths)
469    commit_msg.extend('* %s' % p for p in removed_deps_paths)
470
471  if any([changed_deps_list,
472          added_deps_paths,
473          removed_deps_paths]):
474    change_url = CHROMIUM_FILE_TEMPLATE % (rev_interval, 'DEPS')
475    commit_msg.append('DEPS diff: %s\n' % change_url)
476  else:
477    commit_msg.append('No dependencies changed.')
478
479  if clang_change and clang_change.current_rev != clang_change.new_rev:
480    commit_msg.append('Clang version changed %s:%s' %
481                      (clang_change.current_rev, clang_change.new_rev))
482    change_url = CHROMIUM_FILE_TEMPLATE % (rev_interval,
483                                           CLANG_UPDATE_SCRIPT_URL_PATH)
484    commit_msg.append('Details: %s\n' % change_url)
485  else:
486    commit_msg.append('No update to Clang.\n')
487
488  # TBR needs to be non-empty for Gerrit to process it.
489  git_author = _RunCommand(['git', 'config', 'user.email'],
490                           working_dir=CHECKOUT_SRC_DIR)[0].splitlines()[0]
491  tbr_authors = git_author + ',' + tbr_authors
492
493  commit_msg.append('TBR=%s' % tbr_authors)
494  commit_msg.append('BUG=None')
495  return '\n'.join(commit_msg)
496
497
498def UpdateDepsFile(deps_filename, rev_update, changed_deps, new_cr_content):
499  """Update the DEPS file with the new revision."""
500
501  with open(deps_filename, 'rb') as deps_file:
502    deps_content = deps_file.read()
503
504  # Update the chromium_revision variable.
505  deps_content = deps_content.replace(rev_update.current_chromium_rev,
506                                      rev_update.new_chromium_rev)
507
508  # Add and remove dependencies. For now: only generated android deps.
509  # Since gclient cannot add or remove deps, we on the fact that
510  # these android deps are located in one place we can copy/paste.
511  deps_re = re.compile(ANDROID_DEPS_START + '.*' + ANDROID_DEPS_END,
512                       re.DOTALL)
513  new_deps = deps_re.search(new_cr_content)
514  old_deps = deps_re.search(deps_content)
515  if not new_deps or not old_deps:
516    faulty = 'Chromium' if not new_deps else 'WebRTC'
517    raise RollError('Was expecting to find "%s" and "%s"\n'
518                    'in %s DEPS'
519                    % (ANDROID_DEPS_START, ANDROID_DEPS_END, faulty))
520  deps_content = deps_re.sub(new_deps.group(0), deps_content)
521
522  with open(deps_filename, 'wb') as deps_file:
523    deps_file.write(deps_content)
524
525  # Update each individual DEPS entry.
526  for dep in changed_deps:
527    local_dep_dir = os.path.join(CHECKOUT_ROOT_DIR, dep.path)
528    if not os.path.isdir(local_dep_dir):
529      raise RollError(
530          'Cannot find local directory %s. Either run\n'
531          'gclient sync --deps=all\n'
532          'or make sure the .gclient file for your solution contains all '
533          'platforms in the target_os list, i.e.\n'
534          'target_os = ["android", "unix", "mac", "ios", "win"];\n'
535          'Then run "gclient sync" again.' % local_dep_dir)
536    if isinstance(dep, ChangedCipdPackage):
537      package = dep.package.format()  # Eliminate double curly brackets
538      update = '%s:%s@%s' % (dep.path, package, dep.new_version)
539    else:
540      update = '%s@%s' % (dep.path, dep.new_rev)
541    _RunCommand(['gclient', 'setdep', '--revision', update],
542                working_dir=CHECKOUT_SRC_DIR)
543
544
545def _IsTreeClean():
546  stdout, _ = _RunCommand(['git', 'status', '--porcelain'])
547  if len(stdout) == 0:
548    return True
549
550  logging.error('Dirty/unversioned files:\n%s', stdout)
551  return False
552
553
554def _EnsureUpdatedMasterBranch(dry_run):
555  current_branch = _RunCommand(
556      ['git', 'rev-parse', '--abbrev-ref', 'HEAD'])[0].splitlines()[0]
557  if current_branch != 'master':
558    logging.error('Please checkout the master branch and re-run this script.')
559    if not dry_run:
560      sys.exit(-1)
561
562  logging.info('Updating master branch...')
563  _RunCommand(['git', 'pull'])
564
565
566def _CreateRollBranch(dry_run):
567  logging.info('Creating roll branch: %s', ROLL_BRANCH_NAME)
568  if not dry_run:
569    _RunCommand(['git', 'checkout', '-b', ROLL_BRANCH_NAME])
570
571
572def _RemovePreviousRollBranch(dry_run):
573  active_branch, branches = _GetBranches()
574  if active_branch == ROLL_BRANCH_NAME:
575    active_branch = 'master'
576  if ROLL_BRANCH_NAME in branches:
577    logging.info('Removing previous roll branch (%s)', ROLL_BRANCH_NAME)
578    if not dry_run:
579      _RunCommand(['git', 'checkout', active_branch])
580      _RunCommand(['git', 'branch', '-D', ROLL_BRANCH_NAME])
581
582
583def _LocalCommit(commit_msg, dry_run):
584  logging.info('Committing changes locally.')
585  if not dry_run:
586    _RunCommand(['git', 'add', '--update', '.'])
587    _RunCommand(['git', 'commit', '-m', commit_msg])
588
589
590def ChooseCQMode(skip_cq, cq_over, current_commit_pos, new_commit_pos):
591  if skip_cq:
592    return 0
593  if (new_commit_pos - current_commit_pos) < cq_over:
594    return 1
595  return 2
596
597
598def _UploadCL(commit_queue_mode):
599  """Upload the committed changes as a changelist to Gerrit.
600
601  commit_queue_mode:
602    - 2: Submit to commit queue.
603    - 1: Run trybots but do not submit to CQ.
604    - 0: Skip CQ, upload only.
605  """
606  cmd = ['git', 'cl', 'upload', '--force', '--bypass-hooks']
607  if commit_queue_mode >= 2:
608    logging.info('Sending the CL to the CQ...')
609    cmd.extend(['--use-commit-queue'])
610    cmd.extend(['--send-mail', '--cc', NOTIFY_EMAIL])
611  elif commit_queue_mode >= 1:
612    logging.info('Starting CQ dry run...')
613    cmd.extend(['--cq-dry-run'])
614  extra_env = {
615      'EDITOR': 'true',
616      'SKIP_GCE_AUTH_FOR_GIT': '1',
617  }
618  stdout, stderr = _RunCommand(cmd, extra_env=extra_env)
619  logging.debug('Output from "git cl upload":\nstdout:\n%s\n\nstderr:\n%s',
620      stdout, stderr)
621
622
623def GetRollRevisionRanges(opts, webrtc_deps):
624  current_cr_rev = webrtc_deps['vars']['chromium_revision']
625  new_cr_rev = opts.revision
626  if not new_cr_rev:
627    stdout, _ = _RunCommand(['git', 'ls-remote', CHROMIUM_SRC_URL, 'HEAD'])
628    head_rev = stdout.strip().split('\t')[0]
629    logging.info('No revision specified. Using HEAD: %s', head_rev)
630    new_cr_rev = head_rev
631
632  return ChromiumRevisionUpdate(current_cr_rev, new_cr_rev)
633
634
635def main():
636  p = argparse.ArgumentParser()
637  p.add_argument('--clean', action='store_true', default=False,
638                 help='Removes any previous local roll branch.')
639  p.add_argument('-r', '--revision',
640                 help=('Chromium Git revision to roll to. Defaults to the '
641                       'Chromium HEAD revision if omitted.'))
642  p.add_argument('--dry-run', action='store_true', default=False,
643                 help=('Calculate changes and modify DEPS, but don\'t create '
644                       'any local branch, commit, upload CL or send any '
645                       'tryjobs.'))
646  p.add_argument('-i', '--ignore-unclean-workdir', action='store_true',
647                 default=False,
648                 help=('Ignore if the current branch is not master or if there '
649                       'are uncommitted changes (default: %(default)s).'))
650  grp = p.add_mutually_exclusive_group()
651  grp.add_argument('--skip-cq', action='store_true', default=False,
652                   help='Skip sending the CL to the CQ (default: %(default)s)')
653  grp.add_argument('--cq-over', type=int, default=1,
654                   help=('Commit queue dry run if the revision difference '
655                         'is below this number (default: %(default)s)'))
656  p.add_argument('-v', '--verbose', action='store_true', default=False,
657                 help='Be extra verbose in printing of log messages.')
658  opts = p.parse_args()
659
660  if opts.verbose:
661    logging.basicConfig(level=logging.DEBUG)
662  else:
663    logging.basicConfig(level=logging.INFO)
664
665  if not opts.ignore_unclean_workdir and not _IsTreeClean():
666    logging.error('Please clean your local checkout first.')
667    return 1
668
669  if opts.clean:
670    _RemovePreviousRollBranch(opts.dry_run)
671
672  if not opts.ignore_unclean_workdir:
673    _EnsureUpdatedMasterBranch(opts.dry_run)
674
675  deps_filename = os.path.join(CHECKOUT_SRC_DIR, 'DEPS')
676  webrtc_deps = ParseLocalDepsFile(deps_filename)
677
678  rev_update = GetRollRevisionRanges(opts, webrtc_deps)
679
680  current_commit_pos = ParseCommitPosition(
681      ReadRemoteCrCommit(rev_update.current_chromium_rev))
682  new_commit_pos = ParseCommitPosition(
683      ReadRemoteCrCommit(rev_update.new_chromium_rev))
684
685  new_cr_content = ReadRemoteCrFile('DEPS', rev_update.new_chromium_rev)
686  new_cr_deps = ParseDepsDict(new_cr_content)
687  changed_deps = CalculateChangedDeps(webrtc_deps, new_cr_deps)
688  # Discard other deps, assumed to be chromium-only dependencies.
689  new_generated_android_deps, _ = FindAddedDeps(webrtc_deps, new_cr_deps)
690  removed_generated_android_deps, other_deps = FindRemovedDeps(webrtc_deps,
691                                                               new_cr_deps)
692  if other_deps:
693    raise RollError('WebRTC DEPS entries are missing from Chromium: %s.\n'
694          'Remove them or add them to either '
695          'WEBRTC_ONLY_DEPS or DONT_AUTOROLL_THESE.' % other_deps)
696  clang_change = CalculateChangedClang(rev_update.new_chromium_rev)
697  commit_msg = GenerateCommitMessage(
698      rev_update, current_commit_pos, new_commit_pos, changed_deps,
699      added_deps_paths=new_generated_android_deps,
700      removed_deps_paths=removed_generated_android_deps,
701      clang_change=clang_change)
702  logging.debug('Commit message:\n%s', commit_msg)
703
704  _CreateRollBranch(opts.dry_run)
705  if not opts.dry_run:
706    UpdateDepsFile(deps_filename, rev_update, changed_deps, new_cr_content)
707  if _IsTreeClean():
708    logging.info("No DEPS changes detected, skipping CL creation.")
709  else:
710    _LocalCommit(commit_msg, opts.dry_run)
711    commit_queue_mode = ChooseCQMode(opts.skip_cq, opts.cq_over,
712                                     current_commit_pos, new_commit_pos)
713    logging.info('Uploading CL...')
714    if not opts.dry_run:
715      _UploadCL(commit_queue_mode)
716  return 0
717
718
719if __name__ == '__main__':
720  sys.exit(main())
721