blob: 752b6ada4e1b143fd5958b370693653c87d70b66 [file] [log] [blame]
#!/usr/bin/python
#
# Copyright (C) 2012 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Merge Chromium into the Android tree."""
import contextlib
import logging
import optparse
import os
import re
import sys
import urllib2
import merge_common
# We need to import this *after* merging from upstream to get the latest
# version. Set it to none here to catch uses before it's imported.
webview_licenses = None
AUTOGEN_MESSAGE = 'This commit was generated by merge_from_chromium.py.'
SRC_GIT_BRANCH = 'refs/remotes/history/upstream-master'
def _ReadGitFile(sha1, path, git_url=None, git_branch=None):
"""Reads a file from a (possibly remote) git project at a specific revision.
Args:
sha1: The SHA1 at which to read.
path: The relative path of the file to read.
git_url: The URL of the git server, if reading a remote project.
git_branch: The branch to fetch, if reading a remote project.
Returns:
The contents of the specified file.
"""
if git_url:
merge_common.GetCommandStdout(['git', 'fetch', '-f', git_url, git_branch])
return merge_common.GetCommandStdout(['git', 'show', '%s:%s' % (sha1, path)])
def _ParseDEPS(deps_content):
"""Parses the .DEPS.git file from Chromium and returns its contents.
Args:
deps_content: The contents of the .DEPS.git file as text.
Returns:
A dictionary of the contents of .DEPS.git at the specified revision
"""
class FromImpl(object):
"""Used to implement the From syntax."""
def __init__(self, module_name):
self.module_name = module_name
def __str__(self):
return 'From("%s")' % self.module_name
class _VarImpl(object):
def __init__(self, custom_vars, local_scope):
self._custom_vars = custom_vars
self._local_scope = local_scope
def Lookup(self, var_name):
"""Implements the Var syntax."""
if var_name in self._custom_vars:
return self._custom_vars[var_name]
elif var_name in self._local_scope.get('vars', {}):
return self._local_scope['vars'][var_name]
raise Exception('Var is not defined: %s' % var_name)
tmp_locals = {}
var = _VarImpl({}, tmp_locals)
tmp_globals = {'From': FromImpl, 'Var': var.Lookup, 'deps_os': {}}
exec(deps_content) in tmp_globals, tmp_locals
return tmp_locals
def _GetProjectMergeInfo(projects, deps_vars):
"""Gets the git URL and SHA1 for each project based on .DEPS.git.
Args:
projects: The list of projects to consider.
deps_vars: The dictionary of dependencies from .DEPS.git.
Returns:
A dictionary from project to git URL and SHA1 - 'path: (url, sha1)'
Raises:
TemporaryMergeError: if a project to be merged is not found in .DEPS.git.
"""
deps_fallback_order = [
deps_vars['deps'],
deps_vars['deps_os']['unix'],
deps_vars['deps_os']['android'],
]
result = {}
for path in projects:
for deps in deps_fallback_order:
if len(path) > 0:
upstream_path = os.path.join('src', path)
else:
upstream_path = 'src'
url_plus_sha1 = deps.get(upstream_path)
if url_plus_sha1:
break
else:
raise merge_common.TemporaryMergeError(
'Could not find .DEPS.git entry for project %s. This probably '
'means that the project list in merge_from_chromium.py needs to be '
'updated.' % path)
match = re.match('(.*?)@(.*)', url_plus_sha1)
url = match.group(1)
sha1 = match.group(2)
logging.debug(' Got URL %s and SHA1 %s for project %s', url, sha1, path)
result[path] = {'url': url, 'sha1': sha1}
return result
def _MergeProjects(version, root_sha1, target, unattended, buildspec_url):
"""Merges each required Chromium project into the Android repository.
.DEPS.git is consulted to determine which revision each project must be merged
at. Only a whitelist of required projects are merged.
Args:
version: The version to mention in generated commit messages.
root_sha1: The git hash to merge in the root repository.
target: The target branch to merge to.
unattended: Run in unattended mode.
buildspec_url: URL for buildspec repository, when merging a branch.
Raises:
TemporaryMergeError: If incompatibly licensed code is left after pruning.
"""
# The logic for this step lives here, in the Android tree, as it makes no
# sense for a Chromium tree to know about this merge.
if unattended:
branch_create_flag = '-B'
else:
branch_create_flag = '-b'
branch_name = 'merge-from-chromium-%s' % version
logging.debug('Parsing DEPS ...')
if root_sha1:
deps_content = _ReadGitFile(root_sha1, '.DEPS.git')
else:
deps_content = _ReadGitFile('FETCH_HEAD', version + '/DEPS',
buildspec_url, 'master')
deps_vars = _ParseDEPS(deps_content)
merge_info = _GetProjectMergeInfo(merge_common.THIRD_PARTY_PROJECTS,
deps_vars)
for path in merge_info:
# webkit needs special handling as we have a local mirror
local_mirrored = path == 'third_party/WebKit'
url = merge_info[path]['url']
sha1 = merge_info[path]['sha1']
dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
if local_mirrored:
remote = 'history'
else:
remote = 'goog'
merge_common.GetCommandStdout(['git', 'checkout',
branch_create_flag, branch_name,
'-t', remote + '/' + target],
cwd=dest_dir)
if not local_mirrored or not root_sha1:
logging.debug('Fetching project %s at %s ...', path, sha1)
fetch_args = ['git', 'fetch', url, sha1]
merge_common.GetCommandStdout(fetch_args, cwd=dest_dir)
if merge_common.GetCommandStdout(['git', 'rev-list', '-1', 'HEAD..' + sha1],
cwd=dest_dir):
logging.debug('Merging project %s at %s ...', path, sha1)
# Merge conflicts make git merge return 1, so ignore errors
merge_common.GetCommandStdout(['git', 'merge', '--no-commit', sha1],
cwd=dest_dir, ignore_errors=True)
merge_common.CheckNoConflictsAndCommitMerge(
'Merge %s from %s at %s\n\n%s' % (path, url, sha1, AUTOGEN_MESSAGE),
cwd=dest_dir, unattended=unattended)
else:
logging.debug('No new commits to merge in project %s', path)
# Handle root repository separately.
merge_common.GetCommandStdout(['git', 'checkout',
branch_create_flag, branch_name,
'-t', 'history/' + target])
if not root_sha1:
merge_info = _GetProjectMergeInfo([''], deps_vars)
url = merge_info['']['url']
root_sha1 = merge_info['']['sha1']
merge_common.GetCommandStdout(['git', 'fetch', url, root_sha1])
logging.debug('Merging Chromium at %s ...', root_sha1)
# Merge conflicts make git merge return 1, so ignore errors
merge_common.GetCommandStdout(['git', 'merge', '--no-commit', root_sha1],
ignore_errors=True)
merge_common.CheckNoConflictsAndCommitMerge(
'Merge Chromium at %s (%s)\n\n%s'
% (version, root_sha1, AUTOGEN_MESSAGE), unattended=unattended)
logging.debug('Getting directories to exclude ...')
# We import this now that we have merged the latest version.
# It imports to a global in order that it can be used to generate NOTICE
# later. We also disable writing bytecode to keep the source tree clean.
sys.path.append(os.path.join(merge_common.REPOSITORY_ROOT, 'android_webview',
'tools'))
sys.dont_write_bytecode = True
global webview_licenses
import webview_licenses
import known_issues
for path, exclude_list in known_issues.KNOWN_INCOMPATIBLE.iteritems():
logging.debug(' %s', '\n '.join(os.path.join(path, x) for x in
exclude_list))
dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
merge_common.GetCommandStdout(['git', 'rm', '-rf', '--ignore-unmatch'] +
exclude_list, cwd=dest_dir)
if _ModifiedFilesInIndex(dest_dir):
merge_common.GetCommandStdout(['git', 'commit', '-m',
'Exclude unwanted directories'],
cwd=dest_dir)
def _CheckLicenses():
"""Check that no incompatibly licensed directories exist."""
directories_left_over = webview_licenses.GetIncompatibleDirectories()
if directories_left_over:
raise merge_common.TemporaryMergeError(
'Incompatibly licensed directories remain: ' +
'\n'.join(directories_left_over))
def _GenerateMakefiles(version, unattended):
"""Run gyp to generate the Android build system makefiles.
Args:
version: The version to mention in generated commit messages.
unattended: Run in unattended mode.
"""
logging.debug('Generating makefiles ...')
# TODO(torne): come up with a way to deal with hooks from DEPS properly
# TODO(torne): The .tmp files are generated by
# third_party/WebKit/Source/WebCore/WebCore.gyp/WebCore.gyp into the source
# tree. We should avoid this, or at least use a more specific name to avoid
# accidentally removing or adding other files.
for path in merge_common.ALL_PROJECTS:
dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
merge_common.GetCommandStdout(['git', 'rm', '--ignore-unmatch',
'GypAndroid.*.mk', '*.target.*.mk',
'*.host.*.mk', '*.tmp'], cwd=dest_dir)
try:
merge_common.GetCommandStdout(['android_webview/tools/gyp_webview', 'all'])
except merge_common.MergeError as e:
if not unattended:
raise
else:
for path in merge_common.ALL_PROJECTS:
merge_common.GetCommandStdout(
['git', 'reset', '--hard'],
cwd=os.path.join(merge_common.REPOSITORY_ROOT, path))
raise merge_common.TemporaryMergeError('Makefile generation failed: ' +
str(e))
for path in merge_common.ALL_PROJECTS:
dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
# git add doesn't have an --ignore-unmatch so we have to do this instead:
merge_common.GetCommandStdout(['git', 'add', '-f', 'GypAndroid.*.mk'],
ignore_errors=True, cwd=dest_dir)
merge_common.GetCommandStdout(['git', 'add', '-f', '*.target.*.mk'],
ignore_errors=True, cwd=dest_dir)
merge_common.GetCommandStdout(['git', 'add', '-f', '*.host.*.mk'],
ignore_errors=True, cwd=dest_dir)
merge_common.GetCommandStdout(['git', 'add', '-f', '*.tmp'],
ignore_errors=True, cwd=dest_dir)
# Only try to commit the makefiles if something has actually changed.
if _ModifiedFilesInIndex(dest_dir):
merge_common.GetCommandStdout(
['git', 'commit', '-m',
'Update makefiles after merge of Chromium at %s\n\n%s' %
(version, AUTOGEN_MESSAGE)], cwd=dest_dir)
def _ModifiedFilesInIndex(cwd=merge_common.REPOSITORY_ROOT):
"""Returns true if git's index contains any changes."""
status = merge_common.GetCommandStdout(['git', 'status', '--porcelain'],
cwd=cwd)
return re.search(r'^[MADRC]', status, flags=re.MULTILINE) is not None
def _GenerateNoticeFile(version):
"""Generates and commits a NOTICE file containing code licenses.
This covers all third-party code (from Android's perspective) that lives in
the Chromium tree.
Args:
version: The version to mention in generated commit messages.
"""
logging.debug('Regenerating NOTICE file ...')
contents = webview_licenses.GenerateNoticeFile()
with open(os.path.join(merge_common.REPOSITORY_ROOT, 'NOTICE'), 'w') as f:
f.write(contents)
merge_common.GetCommandStdout(['git', 'add', 'NOTICE'])
# Only try to commit the NOTICE update if the file has actually changed.
if _ModifiedFilesInIndex():
merge_common.GetCommandStdout([
'git', 'commit', '-m',
'Update NOTICE file after merge of Chromium at %s\n\n%s'
% (version, AUTOGEN_MESSAGE)])
def _GenerateLastChange(version):
"""Write a build/util/LASTCHANGE file containing the current revision.
The revision number is compiled into the binary at build time from this file.
Args:
version: The version to mention in generated commit messages.
"""
logging.debug('Updating LASTCHANGE ...')
svn_revision, sha1 = _GetSVNRevisionAndSHA1('HEAD', 'HEAD')
with open(os.path.join(merge_common.REPOSITORY_ROOT, 'build/util/LASTCHANGE'),
'w') as f:
f.write('LASTCHANGE=%s\n' % svn_revision)
merge_common.GetCommandStdout(['git', 'add', '-f', 'build/util/LASTCHANGE'])
logging.debug('Updating LASTCHANGE.blink ...')
with open(os.path.join(merge_common.REPOSITORY_ROOT,
'build/util/LASTCHANGE.blink'), 'w') as f:
f.write('LASTCHANGE=%s\n' % _GetBlinkRevision())
merge_common.GetCommandStdout(['git', 'add', '-f',
'build/util/LASTCHANGE.blink'])
if _ModifiedFilesInIndex():
merge_common.GetCommandStdout([
'git', 'commit', '-m',
'Update LASTCHANGE file after merge of Chromium at %s\n\n%s'
% (version, AUTOGEN_MESSAGE)])
def GetLKGR():
"""Fetch the last known good release from Chromium's dashboard.
Returns:
The last known good SVN revision.
"""
with contextlib.closing(
urllib2.urlopen('https://chromium-status.appspot.com/lkgr')) as lkgr:
return int(lkgr.read())
def GetHEAD():
"""Fetch the latest HEAD revision from the git mirror of the Chromium svn
repo.
Returns:
The latest HEAD SVN revision.
"""
(svn_revision, root_sha1) = _GetSVNRevisionAndSHA1(SRC_GIT_BRANCH,
'HEAD')
return int(svn_revision)
def _ParseSvnRevisionFromGitCommitMessage(commit_message):
return re.search(r'^git-svn-id: .*@([0-9]+)', commit_message,
flags=re.MULTILINE).group(1)
def _GetSVNRevisionFromSha(sha1):
commit = merge_common.GetCommandStdout([
'git', 'show', '--format=%H%n%b', sha1])
return _ParseSvnRevisionFromGitCommitMessage(commit)
def _GetSVNRevisionAndSHA1(git_branch, svn_revision):
logging.debug('Getting SVN revision and SHA1 ...')
if svn_revision == 'HEAD':
# Just use the latest commit.
commit = merge_common.GetCommandStdout([
'git', 'log', '-n1', '--grep=git-svn-id:', '--format=%H%n%b',
git_branch])
sha1 = commit.split()[0]
svn_revision = _ParseSvnRevisionFromGitCommitMessage(commit)
return (svn_revision, sha1)
if svn_revision is None:
# Fetch LKGR from upstream.
svn_revision = GetLKGR()
output = merge_common.GetCommandStdout([
'git', 'log', '--grep=git-svn-id: .*@%s' % svn_revision,
'--format=%H', git_branch])
if not output:
raise merge_common.TemporaryMergeError('Revision %s not found in git repo.'
% svn_revision)
# The log grep will sometimes match reverts/reapplies of commits. We take the
# oldest (last) match because the first time it appears in history is
# overwhelmingly likely to be the correct commit.
sha1 = output.split()[-1]
return (svn_revision, sha1)
def _GetBlinkRevision():
commit = merge_common.GetCommandStdout([
'git', 'log', '-n1', '--grep=git-svn-id:', '--format=%H%n%b'],
cwd=os.path.join(merge_common.REPOSITORY_ROOT, 'third_party', 'WebKit'))
return _ParseSvnRevisionFromGitCommitMessage(commit)
def Snapshot(svn_revision, root_sha1, release, target, unattended,
buildspec_url):
"""Takes a snapshot of the Chromium tree and merges it into Android.
Android makefiles and a top-level NOTICE file are generated and committed
after the merge.
Args:
svn_revision: The SVN revision in the Chromium repository to merge from.
root_sha1: The sha1 in the Chromium git mirror to merge from.
release: The Chromium release version to merge from (e.g. "30.0.1599.20").
Only one of svn_revision, root_sha1 and release should be
specified.
target: The target branch to merge to.
unattended: Run in unattended mode.
buildspec_url: URL for buildspec repository, used when merging a release.
Returns:
True if new commits were merged; False if no new commits were present.
"""
if svn_revision:
svn_revision, root_sha1 = _GetSVNRevisionAndSHA1(SRC_GIT_BRANCH,
svn_revision)
elif root_sha1:
svn_revision = _GetSVNRevisionFromSha(root_sha1)
if svn_revision and root_sha1:
version = svn_revision
if not merge_common.GetCommandStdout(['git', 'rev-list', '-1',
'HEAD..' + root_sha1]):
logging.info('No new commits to merge at %s (%s)',
svn_revision, root_sha1)
return False
elif release:
version = release
root_sha1 = None
else:
raise merge_common.MergeError('No merge source specified')
logging.info('Snapshotting Chromium at %s (%s)', version, root_sha1)
# 1. Merge, accounting for excluded directories
_MergeProjects(version, root_sha1, target, unattended, buildspec_url)
# 2. Generate Android makefiles
_GenerateMakefiles(version, unattended)
# 3. Check for incompatible licenses
_CheckLicenses()
# 4. Generate Android NOTICE file
_GenerateNoticeFile(version)
# 5. Generate LASTCHANGE file
_GenerateLastChange(version)
return True
def Push(version, target):
"""Push the finished snapshot to the Android repository."""
src = 'merge-from-chromium-%s' % version
# Use forced pushes ('+' prefix) for the temporary and archive branches in
# case they already got updated by a previous (possibly failed?) merge, but
# do not force push to the real master-chromium branch as this could erase
# downstream changes.
refspecs = ['%s:%s' % (src, target),
'+%s:refs/archive/chromium-%s' % (src, version)]
if target == 'master-chromium':
refspecs.insert(0, '+%s:master-chromium-merge' % src)
for refspec in refspecs:
logging.debug('Pushing to server (%s) ...' % refspec)
for path in merge_common.ALL_PROJECTS:
if path in merge_common.PROJECTS_WITH_FLAT_HISTORY:
remote = 'history'
else:
remote = 'goog'
logging.debug('Pushing %s', path)
dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
merge_common.GetCommandStdout(['git', 'push', remote, refspec],
cwd=dest_dir)
def main():
parser = optparse.OptionParser(usage='%prog [options]')
parser.epilog = ('Takes a snapshot of the Chromium tree at the specified '
'Chromium SVN revision and merges it into this repository. '
'Paths marked as excluded for license reasons are removed '
'as part of the merge. Also generates Android makefiles and '
'generates a top-level NOTICE file suitable for use in the '
'Android build.')
parser.add_option(
'', '--svn_revision',
default=None,
help=('Merge to the specified chromium SVN revision, rather than using '
'the current LKGR. Can also pass HEAD to merge from tip of tree. '
'Only one of svn_revision, sha1 and release should be specified'))
parser.add_option(
'', '--sha1',
default=None,
help=('Merge to the specified chromium sha1 revision from ' + SRC_GIT_BRANCH
+ ' branch, rather than using the current LKGR. Only one of'
'svn_revision, sha1 and release should be specified.'))
parser.add_option(
'', '--release',
default=None,
help=('Merge to the specified chromium release buildspec (e.g. '
'"30.0.1599.20"). Only one of svn_revision, sha1 and release '
'should be specified.'))
parser.add_option(
'', '--buildspec_url',
default=None,
help=('Git URL for buildspec repository.'))
parser.add_option(
'', '--target',
default='master-chromium', metavar='BRANCH',
help=('Target branch to push to. Defaults to master-chromium.'))
parser.add_option(
'', '--push',
default=False, action='store_true',
help=('Push the result of a previous merge to the server. Note '
'svn_revision must be given.'))
parser.add_option(
'', '--get_lkgr',
default=False, action='store_true',
help=('Just print the current LKGR on stdout and exit.'))
parser.add_option(
'', '--get_head',
default=False, action='store_true',
help=('Just print the current HEAD revision on stdout and exit.'))
parser.add_option(
'', '--unattended',
default=False, action='store_true',
help=('Run in unattended mode.'))
parser.add_option(
'', '--no_changes_exit',
default=0, type='int',
help=('Exit code to use if there are no changes to merge, for scripts.'))
(options, args) = parser.parse_args()
if args:
parser.print_help()
return 1
if 'ANDROID_BUILD_TOP' not in os.environ:
print >>sys.stderr, 'You need to run the Android envsetup.sh and lunch.'
return 1
logging.basicConfig(format='%(message)s', level=logging.DEBUG,
stream=sys.stdout)
if options.get_lkgr:
print GetLKGR()
elif options.get_head:
logging.disable(logging.CRITICAL) # Prevent log messages
print GetHEAD()
elif options.push:
if options.release:
Push(options.release, options.target)
elif options.svn_revision:
Push(options.svn_revision, options.target)
else:
print >>sys.stderr, 'You need to pass the version to push.'
return 1
else:
if not Snapshot(options.svn_revision, options.sha1, options.release,
options.target, options.unattended, options.buildspec_url):
return options.no_changes_exit
return 0
if __name__ == '__main__':
sys.exit(main())