merge_from_chromium.py revision a611c7e365068e2130b6de87b2b14e3b75ae1d4e
1#!/usr/bin/python 2# 3# Copyright (C) 2012 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""Merge Chromium into the Android tree.""" 18 19import contextlib 20import logging 21import optparse 22import os 23import re 24import sys 25import urllib2 26 27import merge_common 28 29 30# We need to import this *after* merging from upstream to get the latest 31# version. Set it to none here to catch uses before it's imported. 32webview_licenses = None 33 34 35AUTOGEN_MESSAGE = 'This commit was generated by merge_from_chromium.py.' 36SRC_GIT_URL = 'http://chromium.googlesource.com/chromium/src.git' 37SRC_GIT_BRANCH = 'git-svn' 38 39 40def _ReadGitFile(git_url, git_branch, sha1, path): 41 """Reads a file from a remote git project at a specific revision. 42 43 Args: 44 git_url: The URL of the git server. 45 git_branch: The branch to fetch. 46 sha1: The SHA1 at which to read. 47 path: The relative path of the file to read. 48 Returns: 49 The contents of the specified file. 50 """ 51 # We fetch the branch to a temporary head so that we don't download the same 52 # commits multiple times. 53 merge_common.GetCommandStdout(['git', 'fetch', '-f', git_url, 54 git_branch + ':cached_upstream']) 55 return merge_common.GetCommandStdout(['git', 'show', '%s:%s' % (sha1, path)]) 56 57 58def _ParseDEPS(git_url, git_branch, sha1): 59 """Parses the .DEPS.git file from Chromium and returns its contents. 60 61 Args: 62 git_url: The URL of the git server. 63 git_branch: The branch to read. 64 sha1: The SHA1 at which to read. 65 Returns: 66 A dictionary of the contents of .DEPS.git at the specified revision 67 """ 68 69 class FromImpl(object): 70 """Used to implement the From syntax.""" 71 72 def __init__(self, module_name): 73 self.module_name = module_name 74 75 def __str__(self): 76 return 'From("%s")' % self.module_name 77 78 class _VarImpl(object): 79 def __init__(self, custom_vars, local_scope): 80 self._custom_vars = custom_vars 81 self._local_scope = local_scope 82 83 def Lookup(self, var_name): 84 """Implements the Var syntax.""" 85 if var_name in self._custom_vars: 86 return self._custom_vars[var_name] 87 elif var_name in self._local_scope.get('vars', {}): 88 return self._local_scope['vars'][var_name] 89 raise Exception('Var is not defined: %s' % var_name) 90 91 tmp_locals = {} 92 var = _VarImpl({}, tmp_locals) 93 tmp_globals = {'From': FromImpl, 'Var': var.Lookup, 'deps_os': {}} 94 deps_content = _ReadGitFile(git_url, git_branch, sha1, '.DEPS.git') 95 exec(deps_content) in tmp_globals, tmp_locals 96 return tmp_locals 97 98 99def _GetThirdPartyProjectMergeInfo(third_party_projects, deps_vars): 100 """Gets the git URL and SHA1 for each project based on .DEPS.git. 101 102 Args: 103 third_party_projects: The list of projects to consider. 104 deps_vars: The dictionary of dependencies from .DEPS.git. 105 Returns: 106 A dictionary from project to git URL and SHA1 - 'path: (url, sha1)' 107 Raises: 108 TemporaryMergeError: if a project to be merged is not found in .DEPS.git. 109 """ 110 deps_fallback_order = [ 111 deps_vars['deps'], 112 deps_vars['deps_os']['unix'], 113 deps_vars['deps_os']['android'], 114 ] 115 result = {} 116 for path in third_party_projects: 117 for deps in deps_fallback_order: 118 url_plus_sha1 = deps.get(os.path.join('src', path)) 119 if url_plus_sha1: 120 break 121 else: 122 raise merge_common.TemporaryMergeError( 123 'Could not find .DEPS.git entry for project %s. This probably ' 124 'means that the project list in merge_from_chromium.py needs to be ' 125 'updated.' % path) 126 match = re.match('(.*?)@(.*)', url_plus_sha1) 127 url = match.group(1) 128 sha1 = match.group(2) 129 logging.debug(' Got URL %s and SHA1 %s for project %s', url, sha1, path) 130 result[path] = {'url': url, 'sha1': sha1} 131 return result 132 133 134def _MergeProjects(git_url, git_branch, svn_revision, root_sha1, unattended): 135 """Merges each required Chromium project into the Android repository. 136 137 .DEPS.git is consulted to determine which revision each project must be merged 138 at. Only a whitelist of required projects are merged. 139 140 Args: 141 git_url: The URL of the Chromium repository to merge from. 142 git_branch: The branch in the Chromium repository to merge from. 143 svn_revision: The SVN revision in the Chromium repository to merge from. 144 root_sha1: The git hash corresponding to svn_revision. 145 unattended: Run in unattended mode. 146 Raises: 147 TemporaryMergeError: If incompatibly licensed code is left after pruning. 148 """ 149 # The logic for this step lives here, in the Android tree, as it makes no 150 # sense for a Chromium tree to know about this merge. 151 152 if unattended: 153 branch_create_flag = '-B' 154 else: 155 branch_create_flag = '-b' 156 branch_name = 'merge-from-chromium-%s' % svn_revision 157 158 logging.debug('Parsing DEPS ...') 159 deps_vars = _ParseDEPS(git_url, git_branch, root_sha1) 160 161 merge_info = _GetThirdPartyProjectMergeInfo(merge_common.THIRD_PARTY_PROJECTS, 162 deps_vars) 163 164 for path in merge_info: 165 url = merge_info[path]['url'] 166 sha1 = merge_info[path]['sha1'] 167 dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path) 168 merge_common.GetCommandStdout(['git', 'checkout', 169 branch_create_flag, branch_name, 170 '-t', 'goog/master-chromium'], cwd=dest_dir) 171 logging.debug('Fetching project %s at %s ...', path, sha1) 172 merge_common.GetCommandStdout(['git', 'fetch', url], cwd=dest_dir) 173 if merge_common.GetCommandStdout(['git', 'rev-list', '-1', 'HEAD..' + sha1], 174 cwd=dest_dir): 175 logging.debug('Merging project %s at %s ...', path, sha1) 176 # Merge conflicts make git merge return 1, so ignore errors 177 merge_common.GetCommandStdout(['git', 'merge', '--no-commit', sha1], 178 cwd=dest_dir, ignore_errors=True) 179 merge_common.CheckNoConflictsAndCommitMerge( 180 'Merge %s from %s at %s\n\n%s' % (path, url, sha1, AUTOGEN_MESSAGE), 181 cwd=dest_dir, unattended=unattended) 182 else: 183 logging.debug('No new commits to merge in project %s', path) 184 185 # Handle root repository separately. 186 merge_common.GetCommandStdout(['git', 'checkout', 187 branch_create_flag, branch_name, 188 '-t', 'goog/master-chromium']) 189 logging.debug('Fetching Chromium at %s ...', root_sha1) 190 merge_common.GetCommandStdout(['git', 'fetch', git_url, git_branch]) 191 logging.debug('Merging Chromium at %s ...', root_sha1) 192 # Merge conflicts make git merge return 1, so ignore errors 193 merge_common.GetCommandStdout(['git', 'merge', '--no-commit', root_sha1], 194 ignore_errors=True) 195 merge_common.CheckNoConflictsAndCommitMerge( 196 'Merge Chromium from %s branch %s at r%s (%s)\n\n%s' 197 % (git_url, git_branch, svn_revision, root_sha1, AUTOGEN_MESSAGE), 198 unattended=unattended) 199 200 logging.debug('Getting directories to exclude ...') 201 202 # We import this now that we have merged the latest version. 203 # It imports to a global in order that it can be used to generate NOTICE 204 # later. We also disable writing bytecode to keep the source tree clean. 205 sys.path.append(os.path.join(merge_common.REPOSITORY_ROOT, 'android_webview', 206 'tools')) 207 sys.dont_write_bytecode = True 208 global webview_licenses 209 import webview_licenses 210 import known_issues 211 212 for path, exclude_list in known_issues.KNOWN_INCOMPATIBLE.iteritems(): 213 logging.debug(' %s', '\n '.join(os.path.join(path, x) for x in 214 exclude_list)) 215 dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path) 216 merge_common.GetCommandStdout(['git', 'rm', '-rf', '--ignore-unmatch'] + 217 exclude_list, cwd=dest_dir) 218 if _ModifiedFilesInIndex(dest_dir): 219 merge_common.GetCommandStdout(['git', 'commit', '-m', 220 'Exclude incompatible directories'], 221 cwd=dest_dir) 222 223 directories_left_over = webview_licenses.GetIncompatibleDirectories() 224 if directories_left_over: 225 raise merge_common.TemporaryMergeError( 226 'Incompatibly licensed directories remain: ' + 227 '\n'.join(directories_left_over)) 228 229 230def _GenerateMakefiles(svn_revision, unattended): 231 """Run gyp to generate the Android build system makefiles. 232 233 Args: 234 svn_revision: The SVN revision to mention in generated commit messages. 235 unattended: Run in unattended mode. 236 """ 237 logging.debug('Generating makefiles ...') 238 239 # TODO(torne): The .tmp files are generated by 240 # third_party/WebKit/Source/WebCore/WebCore.gyp/WebCore.gyp into the source 241 # tree. We should avoid this, or at least use a more specific name to avoid 242 # accidentally removing or adding other files. 243 for path in merge_common.ALL_PROJECTS: 244 dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path) 245 merge_common.GetCommandStdout(['git', 'rm', '--ignore-unmatch', 246 'GypAndroid.*.mk', '*.target.*.mk', 247 '*.host.*.mk', '*.tmp'], cwd=dest_dir) 248 249 try: 250 merge_common.GetCommandStdout(['android_webview/tools/gyp_webview']) 251 except merge_common.MergeError as e: 252 if not unattended: 253 raise 254 else: 255 for path in merge_common.ALL_PROJECTS: 256 merge_common.GetCommandStdout( 257 ['git', 'reset', '--hard'], 258 cwd=os.path.join(merge_common.REPOSITORY_ROOT, path)) 259 raise merge_common.TemporaryMergeError('Makefile generation failed: ' + 260 str(e)) 261 262 for path in merge_common.ALL_PROJECTS: 263 dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path) 264 # git add doesn't have an --ignore-unmatch so we have to do this instead: 265 merge_common.GetCommandStdout(['git', 'add', '-f', 'GypAndroid.*.mk'], 266 ignore_errors=True, cwd=dest_dir) 267 merge_common.GetCommandStdout(['git', 'add', '-f', '*.target.*.mk'], 268 ignore_errors=True, cwd=dest_dir) 269 merge_common.GetCommandStdout(['git', 'add', '-f', '*.host.*.mk'], 270 ignore_errors=True, cwd=dest_dir) 271 merge_common.GetCommandStdout(['git', 'add', '-f', '*.tmp'], 272 ignore_errors=True, cwd=dest_dir) 273 # Only try to commit the makefiles if something has actually changed. 274 if _ModifiedFilesInIndex(dest_dir): 275 merge_common.GetCommandStdout( 276 ['git', 'commit', '-m', 277 'Update makefiles after merge of Chromium at r%s\n\n%s' % 278 (svn_revision, AUTOGEN_MESSAGE)], cwd=dest_dir) 279 280 281def _ModifiedFilesInIndex(cwd=merge_common.REPOSITORY_ROOT): 282 """Returns true if git's index contains any changes.""" 283 status = merge_common.GetCommandStdout(['git', 'status', '--porcelain'], 284 cwd=cwd) 285 return re.search(r'^[MADRC]', status, flags=re.MULTILINE) is not None 286 287 288def _GenerateNoticeFile(svn_revision): 289 """Generates and commits a NOTICE file containing code licenses. 290 291 This covers all third-party code (from Android's perspective) that lives in 292 the Chromium tree. 293 294 Args: 295 svn_revision: The SVN revision for the main Chromium repository. 296 """ 297 logging.debug('Regenerating NOTICE file ...') 298 299 contents = webview_licenses.GenerateNoticeFile() 300 301 with open(os.path.join(merge_common.REPOSITORY_ROOT, 'NOTICE'), 'w') as f: 302 f.write(contents) 303 merge_common.GetCommandStdout(['git', 'add', 'NOTICE']) 304 # Only try to commit the NOTICE update if the file has actually changed. 305 if _ModifiedFilesInIndex(): 306 merge_common.GetCommandStdout([ 307 'git', 'commit', '-m', 308 'Update NOTICE file after merge of Chromium at r%s\n\n%s' 309 % (svn_revision, AUTOGEN_MESSAGE)]) 310 311 312def _GenerateLastChange(svn_revision): 313 """Write a build/util/LASTCHANGE file containing the current revision. 314 315 The revision number is compiled into the binary at build time from this file. 316 317 Args: 318 svn_revision: The SVN revision for the main Chromium repository. 319 """ 320 logging.debug('Updating LASTCHANGE ...') 321 with open(os.path.join(merge_common.REPOSITORY_ROOT, 'build/util/LASTCHANGE'), 322 'w') as f: 323 f.write('LASTCHANGE=%s\n' % svn_revision) 324 merge_common.GetCommandStdout(['git', 'add', '-f', 'build/util/LASTCHANGE']) 325 if _ModifiedFilesInIndex(): 326 merge_common.GetCommandStdout([ 327 'git', 'commit', '-m', 328 'Update LASTCHANGE file after merge of Chromium at r%s\n\n%s' 329 % (svn_revision, AUTOGEN_MESSAGE)]) 330 331 332def GetLKGR(): 333 """Fetch the last known good release from Chromium's dashboard. 334 335 Returns: 336 The last known good SVN revision. 337 """ 338 with contextlib.closing( 339 urllib2.urlopen('https://chromium-status.appspot.com/lkgr')) as lkgr: 340 return int(lkgr.read()) 341 342 343def GetHEAD(): 344 """Fetch the latest HEAD revision from the git mirror of the Chromium svn 345 repo. 346 347 Returns: 348 The latest HEAD SVN revision. 349 """ 350 (svn_revision, root_sha1) = _GetSVNRevisionAndSHA1(SRC_GIT_URL, 351 SRC_GIT_BRANCH, 352 'HEAD') 353 return int(svn_revision) 354 355 356def _GetSVNRevisionAndSHA1(git_url, git_branch, svn_revision): 357 logging.debug('Getting SVN revision and SHA1 ...') 358 merge_common.GetCommandStdout(['git', 'fetch', '-f', git_url, 359 git_branch + ':cached_upstream']) 360 if svn_revision == 'HEAD': 361 # Just use the latest commit. 362 commit = merge_common.GetCommandStdout([ 363 'git', 'log', '-n1', '--grep=git-svn-id:', '--format=%H%n%b', 364 'cached_upstream']) 365 sha1 = commit.split()[0] 366 svn_revision = re.search(r'^git-svn-id: .*@([0-9]+)', commit, 367 flags=re.MULTILINE).group(1) 368 return (svn_revision, sha1) 369 370 if svn_revision is None: 371 # Fetch LKGR from upstream. 372 svn_revision = GetLKGR() 373 output = merge_common.GetCommandStdout([ 374 'git', 'log', '--grep=git-svn-id: .*@%s' % svn_revision, 375 '--format=%H', 'cached_upstream']) 376 if not output: 377 raise merge_common.TemporaryMergeError('Revision %s not found in git repo.' 378 % svn_revision) 379 # The log grep will sometimes match reverts/reapplies of commits. We take the 380 # oldest (last) match because the first time it appears in history is 381 # overwhelmingly likely to be the correct commit. 382 sha1 = output.split()[-1] 383 return (svn_revision, sha1) 384 385 386def Snapshot(svn_revision, unattended): 387 """Takes a snapshot of the Chromium tree and merges it into Android. 388 389 Android makefiles and a top-level NOTICE file are generated and committed 390 after the merge. 391 392 Args: 393 svn_revision: The SVN revision in the Chromium repository to merge from. 394 unattended: Run in unattended mode. 395 396 Returns: 397 True if new commits were merged; False if no new commits were present. 398 """ 399 git_url = SRC_GIT_URL 400 git_branch = SRC_GIT_BRANCH 401 (svn_revision, root_sha1) = _GetSVNRevisionAndSHA1(git_url, git_branch, 402 svn_revision) 403 if not merge_common.GetCommandStdout(['git', 'rev-list', '-1', 404 'HEAD..' + root_sha1]): 405 logging.info('No new commits to merge from %s branch %s at r%s (%s)', 406 git_url, git_branch, svn_revision, root_sha1) 407 return False 408 409 logging.info('Snapshotting Chromium from %s branch %s at r%s (%s)', 410 git_url, git_branch, svn_revision, root_sha1) 411 412 # 1. Merge, accounting for excluded directories 413 _MergeProjects(git_url, git_branch, svn_revision, root_sha1, unattended) 414 415 # 2. Generate Android NOTICE file 416 _GenerateNoticeFile(svn_revision) 417 418 # 3. Generate LASTCHANGE file 419 _GenerateLastChange(svn_revision) 420 421 # 4. Generate Android makefiles 422 _GenerateMakefiles(svn_revision, unattended) 423 424 return True 425 426 427def Push(svn_revision): 428 """Push the finished snapshot to the Android repository.""" 429 merge_common.PushToServer('merge-from-chromium-%s' % svn_revision, 430 'master-chromium', 'master-chromium-merge') 431 432 433def main(): 434 parser = optparse.OptionParser(usage='%prog [options]') 435 parser.epilog = ('Takes a snapshot of the Chromium tree at the specified ' 436 'Chromium SVN revision and merges it into this repository. ' 437 'Paths marked as excluded for license reasons are removed ' 438 'as part of the merge. Also generates Android makefiles and ' 439 'generates a top-level NOTICE file suitable for use in the ' 440 'Android build.') 441 parser.add_option( 442 '', '--svn_revision', 443 default=None, 444 help=('Merge to the specified chromium SVN revision, rather than using ' 445 'the current LKGR. Can also pass HEAD to merge from tip of tree.')) 446 parser.add_option( 447 '', '--push', 448 default=False, action='store_true', 449 help=('Push the result of a previous merge to the server.')) 450 parser.add_option( 451 '', '--get_lkgr', 452 default=False, action='store_true', 453 help=('Just print the current LKGR on stdout and exit.')) 454 parser.add_option( 455 '', '--get_head', 456 default=False, action='store_true', 457 help=('Just print the current HEAD revision on stdout and exit.')) 458 parser.add_option( 459 '', '--unattended', 460 default=False, action='store_true', 461 help=('Run in unattended mode.')) 462 parser.add_option( 463 '', '--no_changes_exit', 464 default=0, type='int', 465 help=('Exit code to use if there are no changes to merge, for scripts.')) 466 (options, args) = parser.parse_args() 467 if args: 468 parser.print_help() 469 return 1 470 471 if 'ANDROID_BUILD_TOP' not in os.environ: 472 print >>sys.stderr, 'You need to run the Android envsetup.sh and lunch.' 473 return 1 474 475 logging.basicConfig(format='%(message)s', level=logging.DEBUG, 476 stream=sys.stdout) 477 478 if options.get_lkgr: 479 print GetLKGR() 480 elif options.get_head: 481 logging.disable(logging.CRITICAL) # Prevent log messages 482 print GetHEAD() 483 elif options.push: 484 if options.svn_revision is None: 485 print >>sys.stderr, 'You need to pass the SVN revision to push.' 486 return 1 487 else: 488 Push(options.svn_revision) 489 else: 490 if not Snapshot(options.svn_revision, options.unattended): 491 return options.no_changes_exit 492 493 return 0 494 495if __name__ == '__main__': 496 sys.exit(main()) 497