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