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