merge_from_chromium.py revision 9fb672be51066b51a372885377cc0eff78e1a34c
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.'
36
37
38def _ReadGitFile(git_url, git_branch, sha1, path):
39  """Reads a file from a remote git project at a specific revision.
40
41  Args:
42    git_url: The URL of the git server.
43    git_branch: The branch to fetch.
44    sha1: The SHA1 at which to read.
45    path: The relative path of the file to read.
46  Returns:
47    The contents of the specified file.
48  """
49  # We fetch the branch to a temporary head so that we don't download the same
50  # commits multiple times.
51  merge_common.GetCommandStdout(['git', 'fetch', '-f', git_url,
52                                 git_branch + ':cached_upstream'])
53  return merge_common.GetCommandStdout(['git', 'show', '%s:%s' % (sha1, path)])
54
55
56def _ParseDEPS(git_url, git_branch, sha1):
57  """Parses the .DEPS.git file from Chromium and returns its contents.
58
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 and SHA1 for each project based on .DEPS.git.
99
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  Raises:
106    TemporaryMergeError: if a project to be merged is not found in .DEPS.git.
107  """
108  deps_fallback_order = [
109      deps_vars['deps'],
110      deps_vars['deps_os']['unix'],
111      deps_vars['deps_os']['android'],
112  ]
113  result = {}
114  for path in third_party_projects:
115    for deps in deps_fallback_order:
116      url_plus_sha1 = deps.get(os.path.join('src', path))
117      if url_plus_sha1:
118        break
119    else:
120      raise merge_common.TemporaryMergeError(
121          'Could not find .DEPS.git entry for project %s. This probably '
122          'means that the project list in merge_from_chromium.py needs to be '
123          'updated.' % path)
124    match = re.match('(.*?)@(.*)', url_plus_sha1)
125    url = match.group(1)
126    sha1 = match.group(2)
127    logging.debug('  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, unattended):
133  """Merges each required Chromium project into the Android repository.
134
135  .DEPS.git is consulted to determine which revision each project must be merged
136  at. Only a whitelist of required projects are merged.
137
138  Args:
139    git_url: The URL of the Chromium repository to merge from.
140    git_branch: The branch in the Chromium repository to merge from.
141    svn_revision: The SVN revision in the Chromium repository to merge from.
142    root_sha1: The git hash corresponding to svn_revision.
143    unattended: Run in unattended mode.
144  Raises:
145    TemporaryMergeError: If incompatibly licensed code is left after pruning.
146  """
147  # The logic for this step lives here, in the Android tree, as it makes no
148  # sense for a Chromium tree to know about this merge.
149
150  if unattended:
151    branch_create_flag = '-B'
152  else:
153    branch_create_flag = '-b'
154  branch_name = 'merge-from-chromium-%s' % svn_revision
155
156  logging.debug('Parsing DEPS ...')
157  deps_vars = _ParseDEPS(git_url, git_branch, root_sha1)
158
159  merge_info = _GetThirdPartyProjectMergeInfo(merge_common.THIRD_PARTY_PROJECTS,
160                                              deps_vars)
161
162  for path in merge_info:
163    url = merge_info[path]['url']
164    sha1 = merge_info[path]['sha1']
165    dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
166    merge_common.GetCommandStdout(['git', 'checkout',
167                                   branch_create_flag, branch_name,
168                                   '-t', 'goog/master-chromium'], cwd=dest_dir)
169    logging.debug('Fetching project %s at %s ...', path, sha1)
170    merge_common.GetCommandStdout(['git', 'fetch', url], cwd=dest_dir)
171    if merge_common.GetCommandStdout(['git', 'rev-list', '-1', 'HEAD..' + sha1],
172                                     cwd=dest_dir):
173      logging.debug('Merging project %s at %s ...', path, sha1)
174      # Merge conflicts make git merge return 1, so ignore errors
175      merge_common.GetCommandStdout(['git', 'merge', '--no-commit', sha1],
176                                    cwd=dest_dir, ignore_errors=True)
177      merge_common.CheckNoConflictsAndCommitMerge(
178          'Merge %s from %s at %s\n\n%s' % (path, url, sha1, AUTOGEN_MESSAGE),
179          cwd=dest_dir, unattended=unattended)
180    else:
181      logging.debug('No new commits to merge in project %s', path)
182
183  # Handle root repository separately.
184  merge_common.GetCommandStdout(['git', 'checkout',
185                                 branch_create_flag, branch_name,
186                                 '-t', 'goog/master-chromium'])
187  logging.debug('Fetching Chromium at %s ...', root_sha1)
188  merge_common.GetCommandStdout(['git', 'fetch', git_url, git_branch])
189  logging.debug('Merging Chromium at %s ...', root_sha1)
190  # Merge conflicts make git merge return 1, so ignore errors
191  merge_common.GetCommandStdout(['git', 'merge', '--no-commit', root_sha1],
192                                ignore_errors=True)
193  merge_common.CheckNoConflictsAndCommitMerge(
194      'Merge Chromium from %s branch %s at r%s (%s)\n\n%s'
195      % (git_url, git_branch, svn_revision, root_sha1, AUTOGEN_MESSAGE),
196      unattended=unattended)
197
198  logging.debug('Getting directories to exclude ...')
199
200  # We import this now that we have merged the latest version.
201  # It imports to a global in order that it can be used to generate NOTICE
202  # later. We also disable writing bytecode to keep the source tree clean.
203  sys.path.append(os.path.join(merge_common.REPOSITORY_ROOT, 'android_webview',
204                               'tools'))
205  sys.dont_write_bytecode = True
206  global webview_licenses
207  import webview_licenses
208  import known_issues
209
210  for path, exclude_list in known_issues.KNOWN_INCOMPATIBLE.iteritems():
211    logging.debug('  %s', '\n  '.join(os.path.join(path, x) for x in
212                                      exclude_list))
213    dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
214    merge_common.GetCommandStdout(['git', 'rm', '-rf', '--ignore-unmatch'] +
215                                  exclude_list, cwd=dest_dir)
216    if _ModifiedFilesInIndex(dest_dir):
217      merge_common.GetCommandStdout(['git', 'commit', '-m',
218                                     'Exclude incompatible directories'],
219                                    cwd=dest_dir)
220
221  directories_left_over = webview_licenses.GetIncompatibleDirectories()
222  if directories_left_over:
223    raise merge_common.TemporaryMergeError(
224        'Incompatibly licensed directories remain: ' +
225        '\n'.join(directories_left_over))
226
227
228def _GenerateMakefiles(svn_revision, unattended):
229  """Run gyp to generate the Android build system makefiles.
230
231  Args:
232    svn_revision: The SVN revision to mention in generated commit messages.
233    unattended: Run in unattended mode.
234  """
235  # TODO(torne): The .tmp files are generated by
236  # third_party/WebKit/Source/WebCore/WebCore.gyp/WebCore.gyp into the source
237  # tree. We should avoid this, or at least use a more specific name to avoid
238  # accidentally removing or adding other files.
239  for path in merge_common.ALL_PROJECTS:
240    dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
241    merge_common.GetCommandStdout(['git', 'rm', '--ignore-unmatch',
242                                   'GypAndroid.*.mk', '*.target.*.mk',
243                                   '*.host.*.mk', '*.tmp'], cwd=dest_dir)
244
245  try:
246    merge_common.GetCommandStdout(['android_webview/tools/gyp_webview'])
247  except merge_common.MergeError as e:
248    if not unattended:
249      raise
250    else:
251      for path in merge_common.ALL_PROJECTS:
252        merge_common.GetCommandStdout(
253            ['git', 'reset', '--hard'],
254            cwd=os.path.join(merge_common.REPOSITORY_ROOT, path))
255      raise merge_common.TemporaryMergeError('Makefile generation failed: ' +
256                                             str(e))
257
258  for path in merge_common.ALL_PROJECTS:
259    dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
260    # git add doesn't have an --ignore-unmatch so we have to do this instead:
261    merge_common.GetCommandStdout(['git', 'add', '-f', 'GypAndroid.*.mk'],
262                                  ignore_errors=True, cwd=dest_dir)
263    merge_common.GetCommandStdout(['git', 'add', '-f', '*.target.*.mk'],
264                                  ignore_errors=True, cwd=dest_dir)
265    merge_common.GetCommandStdout(['git', 'add', '-f', '*.host.*.mk'],
266                                  ignore_errors=True, cwd=dest_dir)
267    merge_common.GetCommandStdout(['git', 'add', '-f', '*.tmp'],
268                                  ignore_errors=True, cwd=dest_dir)
269    # Only try to commit the makefiles if something has actually changed.
270    if _ModifiedFilesInIndex(dest_dir):
271      merge_common.GetCommandStdout(
272          ['git', 'commit', '-m',
273           'Update makefiles after merge of Chromium at r%s\n\n%s' %
274           (svn_revision, AUTOGEN_MESSAGE)], cwd=dest_dir)
275
276
277def _ModifiedFilesInIndex(cwd=merge_common.REPOSITORY_ROOT):
278  """Returns true if git's index contains any changes."""
279  status = merge_common.GetCommandStdout(['git', 'status', '--porcelain'],
280                                         cwd=cwd)
281  return re.search(r'^[MADRC]', status, flags=re.MULTILINE) is not None
282
283
284def _GenerateNoticeFile(svn_revision):
285  """Generates and commits a NOTICE file containing code licenses.
286
287  This covers all third-party code (from Android's perspective) that lives in
288  the Chromium tree.
289
290  Args:
291    svn_revision: The SVN revision for the main Chromium repository.
292  """
293  logging.debug('Regenerating NOTICE file ...')
294
295  contents = webview_licenses.GenerateNoticeFile()
296
297  with open(os.path.join(merge_common.REPOSITORY_ROOT, 'NOTICE'), 'w') as f:
298    f.write(contents)
299  merge_common.GetCommandStdout(['git', 'add', 'NOTICE'])
300  # Only try to commit the NOTICE update if the file has actually changed.
301  if _ModifiedFilesInIndex():
302    merge_common.GetCommandStdout([
303        'git', 'commit', '-m',
304        'Update NOTICE file after merge of Chromium at r%s\n\n%s'
305        % (svn_revision, AUTOGEN_MESSAGE)])
306
307
308def _GenerateLastChange(svn_revision):
309  """Write a build/util/LASTCHANGE file containing the current revision.
310
311  The revision number is compiled into the binary at build time from this file.
312
313  Args:
314    svn_revision: The SVN revision for the main Chromium repository.
315  """
316  logging.debug('Updating LASTCHANGE ...')
317  with open(os.path.join(merge_common.REPOSITORY_ROOT, 'build/util/LASTCHANGE'),
318            'w') as f:
319    f.write('LASTCHANGE=%s\n' % svn_revision)
320  merge_common.GetCommandStdout(['git', 'add', '-f', 'build/util/LASTCHANGE'])
321  if _ModifiedFilesInIndex():
322    merge_common.GetCommandStdout([
323        'git', 'commit', '-m',
324        'Update LASTCHANGE file after merge of Chromium at r%s\n\n%s'
325        % (svn_revision, AUTOGEN_MESSAGE)])
326
327
328def GetLKGR():
329  """Fetch the last known good release from Chromium's dashboard.
330
331  Returns:
332    The last known good SVN revision.
333  """
334  with contextlib.closing(
335      urllib2.urlopen('https://chromium-status.appspot.com/lkgr')) as lkgr:
336    return int(lkgr.read())
337
338
339def _GetSVNRevisionAndSHA1(git_url, git_branch, svn_revision):
340  logging.debug('Getting SVN revision and SHA1 ...')
341  merge_common.GetCommandStdout(['git', 'fetch', '-f', git_url,
342                                 git_branch + ':cached_upstream'])
343  if svn_revision == 'HEAD':
344    # Just use the latest commit.
345    commit = merge_common.GetCommandStdout([
346        'git', 'log', '-n1', '--grep=git-svn-id:', '--format=%H%n%b',
347        'cached_upstream'])
348    sha1 = commit.split()[0]
349    svn_revision = re.search(r'^git-svn-id: .*@([0-9]+)', commit,
350                             flags=re.MULTILINE).group(1)
351    return (svn_revision, sha1)
352
353  if svn_revision is None:
354    # Fetch LKGR from upstream.
355    svn_revision = GetLKGR()
356  output = merge_common.GetCommandStdout([
357      'git', 'log', '--grep=git-svn-id: .*@%s' % svn_revision,
358      '--format=%H', 'cached_upstream'])
359  if not output:
360    raise merge_common.TemporaryMergeError('Revision %s not found in git repo.'
361                                           % svn_revision)
362  # The log grep will sometimes match reverts/reapplies of commits. We take the
363  # oldest (last) match because the first time it appears in history is
364  # overwhelmingly likely to be the correct commit.
365  sha1 = output.split()[-1]
366  return (svn_revision, sha1)
367
368
369def Snapshot(svn_revision, unattended):
370  """Takes a snapshot of the Chromium tree and merges it into Android.
371
372  Android makefiles and a top-level NOTICE file are generated and committed
373  after the merge.
374
375  Args:
376    svn_revision: The SVN revision in the Chromium repository to merge from.
377    unattended: Run in unattended mode.
378
379  Returns:
380    True if new commits were merged; False if no new commits were present.
381  """
382  git_url = 'http://chromium.googlesource.com/chromium/src.git'
383  git_branch = 'git-svn'
384  (svn_revision, root_sha1) = _GetSVNRevisionAndSHA1(git_url, git_branch,
385                                                     svn_revision)
386  if not merge_common.GetCommandStdout(['git', 'rev-list', '-1',
387                                        'HEAD..' + root_sha1]):
388    logging.info('No new commits to merge from %s branch %s at r%s (%s)',
389                 git_url, git_branch, svn_revision, root_sha1)
390    return False
391
392  logging.info('Snapshotting Chromium from %s branch %s at r%s (%s)',
393               git_url, git_branch, svn_revision, root_sha1)
394
395  # 1. Merge, accounting for excluded directories
396  _MergeProjects(git_url, git_branch, svn_revision, root_sha1, unattended)
397
398  # 2. Generate Android NOTICE file
399  _GenerateNoticeFile(svn_revision)
400
401  # 3. Generate LASTCHANGE file
402  _GenerateLastChange(svn_revision)
403
404  # 4. Generate Android makefiles
405  _GenerateMakefiles(svn_revision, unattended)
406
407  return True
408
409
410def Push(svn_revision):
411  """Push the finished snapshot to the Android repository."""
412  merge_common.PushToServer('merge-from-chromium-%s' % svn_revision,
413                            'master-chromium', 'master-chromium-merge')
414
415
416def main():
417  parser = optparse.OptionParser(usage='%prog [options]')
418  parser.epilog = ('Takes a snapshot of the Chromium tree at the specified '
419                   'Chromium SVN revision and merges it into this repository. '
420                   'Paths marked as excluded for license reasons are removed '
421                   'as part of the merge. Also generates Android makefiles and '
422                   'generates a top-level NOTICE file suitable for use in the '
423                   'Android build.')
424  parser.add_option(
425      '', '--svn_revision',
426      default=None,
427      help=('Merge to the specified chromium SVN revision, rather than using '
428            'the current LKGR. Can also pass HEAD to merge from tip of tree.'))
429  parser.add_option(
430      '', '--push',
431      default=False, action='store_true',
432      help=('Push the result of a previous merge to the server.'))
433  parser.add_option(
434      '', '--get_lkgr',
435      default=False, action='store_true',
436      help=('Just print the current LKGR on stdout and exit.'))
437  parser.add_option(
438      '', '--unattended',
439      default=False, action='store_true',
440      help=('Run in unattended mode.'))
441  (options, args) = parser.parse_args()
442  if args:
443    parser.print_help()
444    return 1
445
446  if 'ANDROID_BUILD_TOP' not in os.environ:
447    print >>sys.stderr, 'You need to run the Android envsetup.sh and lunch.'
448    return 1
449
450  logging.basicConfig(format='%(message)s', level=logging.DEBUG,
451                      stream=sys.stdout)
452
453  if options.get_lkgr:
454    print GetLKGR()
455  elif options.push:
456    if options.svn_revision is None:
457      print >>sys.stderr, 'You need to pass the SVN revision to push.'
458      return 1
459    else:
460      Push(options.svn_revision)
461  else:
462    Snapshot(options.svn_revision, options.unattended)
463
464  return 0
465
466if __name__ == '__main__':
467  sys.exit(main())
468