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