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