merge_from_chromium.py revision 1c263f2b522bcb9159547d14f9f57abac46967c7
1#!/usr/bin/env 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""" 18Merge Chromium into the Android tree. See the output of --help for details. 19 20""" 21import optparse 22import os 23import re 24import sys 25 26import merge_common 27 28 29# We need to import this *after* merging from upstream to get the latest 30# version. Set it to none here to catch uses before it's imported. 31webview_licenses = None 32 33 34AUTOGEN_MESSAGE = 'This commit was generated by merge_from_chromium.py.' 35 36 37def _ReadGitFile(git_url, git_branch, sha1, path): 38 """Reads a file from a remote git project at a specific revision. 39 Args: 40 git_url: The URL of the git server. 41 git_branch: The branch to read. 42 sha1: The SHA1 at which to read. 43 path: The relative path of the file to read. 44 Returns: 45 The contents of the specified file. 46 """ 47 48 # We fetch the branch to a temporary head so that we don't download the same 49 # commits multiple times. 50 merge_common.GetCommandStdout(['git', 'fetch', '-f', git_url, 51 git_branch + ':cached_upstream']) 52 53 args = ['git', 'show', '%s:%s' % (sha1, path)] 54 return merge_common.GetCommandStdout(args) 55 56 57def _ParseDEPS(git_url, git_branch, sha1): 58 """Parses the .DEPS.git file from Chromium and returns its contents. 59 Args: 60 git_url: The URL of the git server. 61 git_branch: The branch to read. 62 sha1: The SHA1 at which to read. 63 Returns: 64 A dictionary of the contents of .DEPS.git at the specified revision 65 """ 66 67 class FromImpl(object): 68 """Used to implement the From syntax.""" 69 70 def __init__(self, module_name): 71 self.module_name = module_name 72 73 def __str__(self): 74 return 'From("%s")' % self.module_name 75 76 class _VarImpl(object): 77 def __init__(self, custom_vars, local_scope): 78 self._custom_vars = custom_vars 79 self._local_scope = local_scope 80 81 def Lookup(self, var_name): 82 """Implements the Var syntax.""" 83 if var_name in self._custom_vars: 84 return self._custom_vars[var_name] 85 elif var_name in self._local_scope.get('vars', {}): 86 return self._local_scope['vars'][var_name] 87 raise Exception('Var is not defined: %s' % var_name) 88 89 tmp_locals = {} 90 var = _VarImpl({}, tmp_locals) 91 tmp_globals = {'From': FromImpl, 'Var': var.Lookup, 'deps_os': {}} 92 deps_content = _ReadGitFile(git_url, git_branch, sha1, '.DEPS.git') 93 exec(deps_content) in tmp_globals, tmp_locals 94 return tmp_locals 95 96 97def _GetThirdPartyProjectMergeInfo(third_party_projects, deps_vars): 98 """Gets the git URL for each project and the SHA1 at which it should be 99 merged. 100 Args: 101 third_party_projects: The list of projects to consider. 102 deps_vars: The dictionary of dependencies from .DEPS.git. 103 Returns: 104 A dictionary from project to git URL and SHA1 - 'path: (url, sha1)' 105 """ 106 107 deps_fallback_order = [ 108 deps_vars['deps'], 109 deps_vars['deps_os']['unix'], 110 deps_vars['deps_os']['android'], 111 ] 112 result = {} 113 for path in third_party_projects: 114 for deps in deps_fallback_order: 115 url_plus_sha1 = deps.get(os.path.join('src', path)) 116 if url_plus_sha1: 117 break 118 else: 119 raise RuntimeError( 120 ('Could not find .DEPS.git entry for project %s. This probably ' 121 'means that the project list in merge_from_chromium.py needs to be ' 122 'updated.') % 123 path) 124 match = re.match('(.*?)@(.*)', url_plus_sha1) 125 url = match.group(1) 126 sha1 = match.group(2) 127 print ' Got URL %s and SHA1 %s for project %s' % (url, sha1, path) 128 result[path] = {'url': url, 'sha1': sha1} 129 return result 130 131 132def _MergeProjects(git_url, git_branch, svn_revision, root_sha1): 133 """Merges into this repository all projects required by the specified branch 134 of Chromium, at the SVN revision. Uses a git subtree merge for each project. 135 Directories in the main Chromium repository which are not needed by Clank are 136 not merged. 137 Args: 138 git_url: The URL of the git server for the Chromium branch to merge to 139 git_branch: The branch name to merge to 140 svn_revision: The SVN revision for the main Chromium repository 141 root_sha1: The git SHA1 for the main Chromium repository 142 """ 143 144 # The logic for this step lives here, in the Android tree, as it makes no 145 # sense for a Chromium tree to know about this merge. 146 147 print 'Parsing DEPS ...' 148 deps_vars = _ParseDEPS(git_url, git_branch, root_sha1) 149 150 merge_info = _GetThirdPartyProjectMergeInfo(merge_common.THIRD_PARTY_PROJECTS, 151 deps_vars) 152 153 for path in merge_info: 154 url = merge_info[path]['url'] 155 sha1 = merge_info[path]['sha1'] 156 dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path) 157 merge_common.GetCommandStdout(['git', 'checkout', 158 '-b', 'merge-from-chromium', 159 '-t', 'goog/master-chromium'], cwd=dest_dir) 160 print 'Fetching project %s at %s ...' % (path, sha1) 161 merge_common.GetCommandStdout(['git', 'fetch', url], cwd=dest_dir) 162 if merge_common.GetCommandStdout(['git', 'rev-list', '-1', 'HEAD..' + sha1], 163 cwd=dest_dir): 164 print 'Merging project %s at %s ...' % (path, sha1) 165 # Merge conflicts make git merge return 1, so ignore errors 166 merge_common.GetCommandStdout(['git', 'merge', '--no-commit', sha1], 167 cwd=dest_dir, ignore_errors=True) 168 merge_common.CheckNoConflictsAndCommitMerge( 169 'Merge %s from %s at %s\n\n%s' % (path, url, sha1, AUTOGEN_MESSAGE), 170 cwd=dest_dir) 171 else: 172 print 'No new commits to merge in project %s' % path 173 174 # Handle root repository separately. 175 merge_common.GetCommandStdout(['git', 'checkout', '-b', 'merge-from-chromium', 176 '-t', 'goog/master-chromium']) 177 print 'Fetching Chromium at %s ...' % root_sha1 178 merge_common.GetCommandStdout(['git', 'fetch', git_url, git_branch]) 179 print 'Merging Chromium at %s ...' % root_sha1 180 # Merge conflicts make git merge return 1, so ignore errors 181 merge_common.GetCommandStdout(['git', 'merge', '--no-commit', root_sha1], 182 ignore_errors=True) 183 merge_common.CheckNoConflictsAndCommitMerge( 184 'Merge Chromium from %s branch %s at r%s (%s)\n\n%s' 185 % (git_url, git_branch, svn_revision, root_sha1, AUTOGEN_MESSAGE)) 186 187 print 'Getting directories to exclude ...' 188 189 # We import this now that we have merged the latest version. 190 # It imports to a global in order that it can be used to generate NOTICE 191 # later. We also disable writing bytecode to keep the source tree clean. 192 sys.path.append(os.path.join(merge_common.REPOSITORY_ROOT, 'android_webview', 193 'tools')) 194 sys.dont_write_bytecode = True 195 global webview_licenses 196 import webview_licenses 197 import known_incompatible 198 199 for path, exclude_list in known_incompatible.KNOWN_INCOMPATIBLE.iteritems(): 200 print ' %s' % '\n '.join(os.path.join(path, x) for x in exclude_list) 201 dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path) 202 merge_common.GetCommandStdout(['git', 'rm', '-rf', '--ignore-unmatch'] + 203 exclude_list, cwd=dest_dir) 204 if _ModifiedFilesInIndex(dest_dir): 205 merge_common.GetCommandStdout(['git', 'commit', '-m', 206 'Exclude incompatible directories'], 207 cwd=dest_dir) 208 209 directories_left_over = webview_licenses.GetIncompatibleDirectories() 210 if directories_left_over: 211 raise RuntimeError('Incompatibly licensed directories remain: ' + 212 '\n'.join(directories_left_over)) 213 return True 214 215 216def _GenerateMakefiles(svn_revision): 217 """Run gyp to generate the makefiles required to build Chromium in the 218 Android build system. 219 """ 220 221 print 'Regenerating makefiles ...' 222 # TODO(torne): The .tmp files are generated by 223 # third_party/WebKit/Source/WebCore/WebCore.gyp/WebCore.gyp into the source 224 # tree. We should avoid this, or at least use a more specific name to avoid 225 # accidentally removing or adding other files. 226 for path in merge_common.ALL_PROJECTS: 227 dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path) 228 merge_common.GetCommandStdout(['git', 'rm', '--ignore-unmatch', 229 'GypAndroid.mk', '*.target.mk', '*.host.mk', 230 '*.tmp'], cwd=dest_dir) 231 232 merge_common.GetCommandStdout(['bash', '-c', 233 'export CHROME_ANDROID_BUILD_WEBVIEW=1 && ' 234 'export CHROME_SRC=`pwd` && ' 235 'export PYTHONDONTWRITEBYTECODE=1 && ' 236 '. build/android/envsetup.sh && ' 237 'android_gyp']) 238 239 for path in merge_common.ALL_PROJECTS: 240 dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path) 241 # git add doesn't have an --ignore-unmatch so we have to do this instead: 242 merge_common.GetCommandStdout(['git', 'add', '-f', 'GypAndroid.mk'], 243 ignore_errors=True, cwd=dest_dir) 244 merge_common.GetCommandStdout(['git', 'add', '-f', '*.target.mk'], 245 ignore_errors=True, cwd=dest_dir) 246 merge_common.GetCommandStdout(['git', 'add', '-f', '*.host.mk'], 247 ignore_errors=True, cwd=dest_dir) 248 merge_common.GetCommandStdout(['git', 'add', '-f', '*.tmp'], 249 ignore_errors=True, cwd=dest_dir) 250 # Only try to commit the makefiles if something has actually changed. 251 if _ModifiedFilesInIndex(dest_dir): 252 merge_common.GetCommandStdout(['git', 'commit', '-m', 253 'Update makefiles after merge of Chromium at r%s\n\n%s' % 254 (svn_revision, AUTOGEN_MESSAGE)], cwd=dest_dir) 255 256 257def _ModifiedFilesInIndex(cwd=merge_common.REPOSITORY_ROOT): 258 """Returns whether git's index includes modified files, ie 'added' changes. 259 """ 260 status = merge_common.GetCommandStdout(['git', 'status', '--porcelain'], 261 cwd=cwd) 262 return re.search(r'^[MADRC]', status, flags=re.MULTILINE) != None 263 264 265def _GenerateNoticeFile(svn_revision): 266 """Generates a NOTICE file for all third-party code (from Android's 267 perspective) that lives in the Chromium tree and commits it to the root of 268 the repository. 269 Args: 270 svn_revision: The SVN revision for the main Chromium repository 271 """ 272 273 print 'Regenerating NOTICE file ...' 274 275 contents = webview_licenses.GenerateNoticeFile() 276 277 with open(os.path.join(merge_common.REPOSITORY_ROOT, 'NOTICE'), 'w') as f: 278 f.write(contents) 279 merge_common.GetCommandStdout(['git', 'add', 'NOTICE']) 280 # Only try to commit the NOTICE update if the file has actually changed. 281 if _ModifiedFilesInIndex(): 282 merge_common.GetCommandStdout([ 283 'git', 'commit', '-m', 284 'Update NOTICE file after merge of Chromium at r%s\n\n%s' 285 % (svn_revision, AUTOGEN_MESSAGE)]) 286 287 288def _GenerateLastChange(svn_revision): 289 """Write a build/util/LASTCHANGE file containing the current revision. This is 290 used in the Chromium build to include the version number. 291 Args: 292 svn_revision: The SVN revision for the main Chromium repository 293 """ 294 295 print 'Updating LASTCHANGE ...' 296 with open(os.path.join(merge_common.REPOSITORY_ROOT, 'build/util/LASTCHANGE'), 297 'w') as f: 298 f.write("LASTCHANGE=%s\n" % svn_revision) 299 merge_common.GetCommandStdout(['git', 'add', '-f', 'build/util/LASTCHANGE']) 300 if _ModifiedFilesInIndex(): 301 merge_common.GetCommandStdout([ 302 'git', 'commit', '-m', 303 'Update LASTCHANGE file after merge of Chromium at r%s\n\n%s' 304 % (svn_revision, AUTOGEN_MESSAGE)]) 305 306 307def _GetSVNRevisionAndSHA1(git_url, git_branch, svn_revision): 308 print 'Getting SVN revision and SHA1 ...' 309 merge_common.GetCommandStdout(['git', 'fetch', '-f', git_url, 310 git_branch + ':cached_upstream']) 311 if svn_revision: 312 # Sometimes, we see multiple commits with the same git SVN ID. No idea why. 313 # We take the most recent. 314 sha1 = merge_common.GetCommandStdout([ 315 'git', 'log', '--grep=git-svn-id: .*@%s' % svn_revision, 316 '--format=%H', 'cached_upstream']).split()[0] 317 else: 318 # Just use the latest commit. 319 # TODO: We may be able to use a LKGR? 320 commit = merge_common.GetCommandStdout([ 321 'git', 'log', '-n1', '--grep=git-svn-id:', '--format=%H%n%b', 322 'cached_upstream']) 323 sha1 = commit.split()[0] 324 svn_revision = re.search(r'^git-svn-id: .*@([0-9]+)', commit, 325 flags=re.MULTILINE).group(1) 326 return (svn_revision, sha1) 327 328 329def _Snapshot(git_url, git_branch, svn_revision, autopush): 330 """Takes a snapshot of the specified Chromium tree at the specified SVN 331 revision and merges it into this repository. Also generates Android makefiles 332 and generates a top-level NOTICE file suitable for use in the Android build. 333 Args: 334 git_url: The URL of the git server for the Chromium branch to merge to 335 svn_revision: The SVN revision for the main Chromium repository 336 """ 337 338 (svn_revision, root_sha1) = _GetSVNRevisionAndSHA1(git_url, git_branch, 339 svn_revision) 340 if not merge_common.GetCommandStdout(['git', 'rev-list', '-1', 341 'HEAD..' + root_sha1]): 342 print ('No new commits to merge from %s branch %s at r%s (%s)' % 343 (git_url, git_branch, svn_revision, root_sha1)) 344 return 345 346 print ('Snapshotting Chromium from %s branch %s at r%s (%s)' % 347 (git_url, git_branch, svn_revision, root_sha1)) 348 349 # 1. Merge, accounting for excluded directories 350 _MergeProjects(git_url, git_branch, svn_revision, root_sha1) 351 352 # 2. Generate Android NOTICE file 353 _GenerateNoticeFile(svn_revision) 354 355 # 3. Generate LASTCHANGE file 356 _GenerateLastChange(svn_revision) 357 358 # 4. Generate Android makefiles 359 _GenerateMakefiles(svn_revision) 360 361 # 5. Push result to server 362 merge_common.PushToServer(autopush, 'merge-from-chromium', 'master-chromium') 363 364 return True 365 366 367def main(): 368 parser = optparse.OptionParser(usage='%prog [options]') 369 parser.epilog = ('Takes a snapshot of the Chromium tree at the specified ' 370 'Chromium SVN revision and merges it into this repository. ' 371 'Paths marked as excluded for license reasons are removed ' 372 'as part of the merge. Also generates Android makefiles and ' 373 'generates a top-level NOTICE file suitable for use in the ' 374 'Android build.') 375 parser.add_option( 376 '', '--git_url', 377 default='http://git.chromium.org/chromium/src.git', 378 help=('The URL of the git server for the Chromium branch to merge. ' 379 'Defaults to upstream.')) 380 parser.add_option( 381 '', '--git_branch', 382 default='git-svn', 383 help=('The name of the upstream branch to merge. Defaults to git-svn.')) 384 parser.add_option( 385 '', '--svn_revision', 386 default=None, 387 help=('Merge to the specified chromium SVN revision, rather than using ' 388 'the current latest revision.')) 389 parser.add_option( 390 '', '--autopush', 391 default=False, action='store_true', 392 help=('Automatically push the result to the server without prompting if' 393 'the merge was successful.')) 394 (options, args) = parser.parse_args() 395 if args: 396 parser.print_help() 397 return 1 398 399 if 'ANDROID_BUILD_TOP' not in os.environ: 400 print >>sys.stderr, 'You need to run the Android envsetup.sh and lunch.' 401 return 1 402 403 if not _Snapshot(options.git_url, options.git_branch, options.svn_revision, 404 options.autopush): 405 return 1 406 407 return 0 408 409if __name__ == '__main__': 410 sys.exit(main()) 411