blob: 99bee5a0a8d09c149e390e58ff28a61d2d6dfc76 [file] [log] [blame]
#!/usr/bin/python
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Script that syncs the repo manifest with .DEPS.git. Designed to be run
periodically from a host machine."""
import filecmp
import optparse
import os
import re
import shutil
import StringIO
import tempfile
from chromite.buildbot import constants
from chromite.buildbot import manifest_version
from chromite.buildbot import repository
from chromite.lib import cros_build_lib as cros_lib
from chromite.scripts import chrome_set_ver
_CHROMIUM_ROOT = 'chromium'
_CHROMIUM_SRC_ROOT = os.path.join(_CHROMIUM_ROOT, 'src')
_CHROMIUM_SRC_INTERNAL = os.path.join(_CHROMIUM_ROOT, 'src-internal')
_CHROMIUM_CROS_DEPS = os.path.join(_CHROMIUM_SRC_ROOT, 'tools/cros.DEPS/DEPS')
_BEGIN_MARKER = """\
<!-- @@@@ BEGIN AUTOGENERATED BROWSER PROJECTS - DON'T MODIFY! @@@@ -->\n\n"""
_END_MARKER = """\
<!-- @@@@ END AUTOGENERATED BROWSER PROJECTS - DON'T MODIFY! @@@@ -->\n\n"""
_EXTERNAL_HEADER = """\
<!-- Begin Chromium (browser) projects -->
<!-- Hardcoded revision="refs/heads/master" is intentional here -->\n\n"""
_EXTERNAL_PROJECT = """\
<project path="%(path)s"
name="%(name)s"
revision="refs/heads/master" />\n"""
_CROS_HEADER = """\
<!-- Begin CrOS-specific Chromium (browser) projects -->
<!-- Hardcoded revision="refs/heads/master" is intentional here -->\n\n"""
_INTERNAL_HEADER = """\
<!-- Begin Chrome browser (PRIVATE) projects -->
<!-- Hardcoded revision="refs/heads/master" is intentional here -->\n\n"""
_INTERNAL_PROJECT = """\
<project remote="cros-internal"
path="%(path)s"
name="%(name)s"
revision="refs/heads/master" />\n"""
_EXTERNAL_MANIFEST_DIR = 'update-manifest'
_INTERNAL_MANIFEST_DIR = 'update-manifest-internal'
_EXTERNAL_MANIFEST_PROJECT = 'chromiumos/manifest'
_INTERNAL_MANIFEST_PROJECT = 'chromeos/manifest-internal'
_EXTERNAL_TEST_DIR = 'external'
_INTERNAL_TEST_DIR = 'internal'
_CHROMIUM_SRC_PROJECT = 'chromium/src'
_CHROMIUM_SRC_INTERNAL_PROJECT = 'chrome/src-internal'
_TEST_MANIFEST_TEMPLATE = """\
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
%(content)s
</manifest>"""
def ConvertDepsToManifest(deps_file, manifest_file, template, blacklist=None):
"""Convert dependencies in .DEPS.git files to manifest entries.
Arguments:
deps_file: The path to the .DEPS.git file.
manifest_file: The file object to write manifest entries to.
template: The template to use for manifest projects. One of
(_EXTERNAL_PROJECT, _INTERNAL_PROJECT)
blacklist: A list of projects to ignore.
"""
_, merged_deps = chrome_set_ver.GetParsedDeps(deps_file)
mappings = chrome_set_ver.GetPathToProjectMappings(merged_deps)
# Check for double checkouts and blacklisted projects.
previous_projects = set([])
for rel_path, project in sorted(mappings.items(),
key=lambda mapping: mapping[1]):
rel_path = os.path.join('chromium', rel_path)
if blacklist and project in blacklist:
cros_lib.Warning('Skipping project %s in %s' % (project, deps_file))
elif project in previous_projects:
cros_lib.Warning('Found double checkout of %s to %s'
% (project, rel_path))
else:
manifest_file.write(template % {'path' : rel_path, 'name' : project})
previous_projects.add(project)
class ManifestException(Exception):
pass
def ParseManifestString(manifest_xml):
"""Returns a cros_lib.ManifestHandler object containing the parsed XML.
Arguments:
manifest_xml: The well-formed XML snippet to parse.
Returns:
A cros_build_lib.ManifestHandler object.
"""
return cros_lib.ManifestHandler.ParseManifest(StringIO.StringIO(manifest_xml))
def CheckForNonChromeProjects(manifest_xml):
"""Make sure we didn't remove non-chrome projects from the manifest.
Arguments:
xml_snippet_object: The file object containing manifest entries to examine.
"""
handler = ParseManifestString(manifest_xml)
for project, attributes in handler.projects.iteritems():
if not attributes.get('path', '').startswith('chromium/'):
raise ManifestException('Project %s was about to be accidentally removed!'
% project)
def GetListOfProjects(manifest_xml):
"""Returns a list of projects specified in the manifest.
Arguments:
manifest_xml: The well-formed XML snippet to parse.
Returns:
A list of projects.
"""
return ParseManifestString(manifest_xml).projects.keys()
class Manifest(object):
"""Encapsulates manifest update logic for an external or internal manifest."""
def __init__(self, repo_root, manifest_path, testroot, internal, dryrun=True):
"""Initializes a new Manifest object.
Arguments:
repo_root: Absolute path to a repo root.
manifest_path: The path to the manifest. This manifest must already be up
up to date.
testroot: Directory where test checkout is stored.
internal: Whether this is an internal manifest.
dryrun: If set, don't actually push the changes.
"""
self.repo_root = repo_root
self.testroot = testroot
self.manifest_path = manifest_path
self.manifest_dir = os.path.dirname(manifest_path)
self.new_manifest_path = os.path.join(self.manifest_dir,
'new_update_manifest.xml')
self.internal = internal
self.dryrun = dryrun
def _PartitionManifest(self):
"""Partitions the manifest around the autogenerated section.
Returns:
A 3-tuple containing the contents of the manifest that come before the
auto-generated section, the autogenerated section itself, and the contents
that come after the auto-generated section.
"""
with open(self.manifest_path, 'r') as f:
lines = f.read()
regex = r'(.*)(%s)(.*)(%s)(.*)' % (
re.escape(_BEGIN_MARKER.strip()),
re.escape(_END_MARKER.strip()))
result = re.match(regex, lines, re.DOTALL)
if result:
return result.group(1), result.group(3), result.group(5)
else:
raise ManifestException('Chromium projects begin/end markers not found!')
def CreateNewManifest(self):
"""Generates a new manifest with updated Chrome entries."""
# Prepare git repo for push
manifest_version.CreatePushBranch(self.manifest_dir)
top_part, overwritten, bottom_part = self._PartitionManifest()
CheckForNonChromeProjects(_TEST_MANIFEST_TEMPLATE
% {'content' : overwritten})
with open(self.new_manifest_path, 'w') as new_manifest:
new_manifest.write(top_part)
new_manifest.write(_BEGIN_MARKER + _EXTERNAL_HEADER)
new_manifest.write(_EXTERNAL_PROJECT % {'path' : 'chromium/src',
'name' : 'chromium/src'})
src_root = os.path.join(self.repo_root, _CHROMIUM_SRC_ROOT)
ConvertDepsToManifest(os.path.join(src_root, '.DEPS.git'), new_manifest,
_EXTERNAL_PROJECT)
new_manifest.write('\n')
# Convert cros.DEPS, skipping projects already in the manifest.
new_manifest.write(_CROS_HEADER)
ConvertDepsToManifest(os.path.join(src_root, 'tools/cros.DEPS/DEPS'),
new_manifest, _EXTERNAL_PROJECT,
blacklist=GetListOfProjects(top_part + bottom_part))
new_manifest.write('\n')
if self.internal:
new_manifest.write(_INTERNAL_HEADER)
new_manifest.write(_INTERNAL_PROJECT
% {'path' : 'chromium/src-internal',
'name' : 'chrome/src-internal'})
internal_deps = os.path.join(self.repo_root, _CHROMIUM_SRC_INTERNAL,
'.DEPS.git')
ConvertDepsToManifest(internal_deps, new_manifest, _INTERNAL_PROJECT)
new_manifest.write('\n')
new_manifest.write(_END_MARKER)
# Strip out the empty lines at the top of the bottom block.
new_manifest.write(re.match(r'\s*(^\s*\S+.*)', bottom_part,
re.S | re.M).group(1))
def IsNewManifestDifferent(self):
"""Checks if newly generated manifest is different from old manifest."""
return not filecmp.cmp(self.new_manifest_path, self.manifest_path,
shallow=False)
def TestNewManifest(self):
"""Runs a 'repo sync' off of new manifest to verify things aren't broken."""
# Copy to .repo/manifest.xml and run repo sync
test_dir = os.path.join(
self.testroot,
_INTERNAL_TEST_DIR if self.internal else _EXTERNAL_TEST_DIR)
if not os.path.isdir(test_dir):
os.makedirs(test_dir)
git_url = (constants.MANIFEST_INT_URL if self.internal
else constants.MANIFEST_URL)
repo = repository.RepoRepository(git_url, test_dir)
try:
repo.Sync(local_manifest=self.new_manifest_path, jobs=12)
except Exception:
cros_lib.Error('Failed to sync with new manifest!')
raise
finally:
# Sync back to original manifest
repo.Sync(local_manifest=repository.RepoRepository.DEFAULT_MANIFEST,
jobs=12)
def PushChanges(self):
"""Push changes to manifest live."""
shutil.move(self.new_manifest_path, self.manifest_path)
cros_lib.RunCommand(['git', 'add', self.manifest_path],
cwd=self.manifest_dir)
cros_lib.RunCommand(['git', 'commit',
'-m',
'Auto-updating manifest to match .DEPS.git file'],
cwd=self.manifest_dir)
cros_lib.GitPushWithRetry(manifest_version.PUSH_BRANCH,
cwd=self.manifest_dir,
dryrun=self.dryrun)
def PerformUpdate(self):
try:
self.CreateNewManifest()
if self.IsNewManifestDifferent():
self.TestNewManifest()
self.PushChanges()
except ManifestException:
cros_lib.Error('Errors encountered while updating manifest!')
raise
def GetSource(repo_path, project_name, internal=False):
"""Checks out and updates a gerrit project.
Arguments:
repo_path: Absolute path of checkout.
project_name: Name of Gerrit project to pull down.
internal: Whether the project is an internal project.
"""
if not os.path.isdir(repo_path):
if internal:
project_url = os.path.join(constants.GERRIT_INT_SSH_URL, project_name)
else:
project_url = os.path.join(constants.GERRIT_SSH_URL, project_name)
cros_lib.RunCommand(['git', 'clone', project_url, repo_path])
cros_lib.RunCommand(['git', 'pull', '--ff-only'], cwd=repo_path)
def _CheckTestRootOption(_option, _opt_str, value, parser):
"""Validate and convert buildroot to full-path form."""
value = value.strip()
if not value or value == '/':
raise optparse.OptionValueError('Invalid buildroot specified')
parser.values.testroot = os.path.realpath(os.path.expanduser(value))
def main(argv):
usage = 'usage: %prog'
parser = optparse.OptionParser(usage=usage)
parser.add_option('-r', '--testroot', action='callback', dest='testroot',
type='string', callback=_CheckTestRootOption,
help=('Directory where test checkout is stored.'))
parser.add_option('-f', '--force', default=False, action='store_true',
help='Actually push manifest changes.')
parser.add_option('-v', '--verbose', default=False, action='store_true',
help='Run with debug output.')
(options, _inputs) = parser.parse_args(argv)
if not options.testroot:
cros_lib.Die('Please specify a test root!')
# Set cros_build_lib debug level to hide RunCommand spew.
if options.verbose:
cros_lib.DebugLevel.SetDebugLevel(cros_lib.DebugLevel.DEBUG)
else:
cros_lib.DebugLevel.SetDebugLevel(cros_lib.DebugLevel.WARNING)
repo_root = cros_lib.FindRepoCheckoutRoot()
chromium_src_root = os.path.join(repo_root, _CHROMIUM_SRC_ROOT)
if not os.path.isdir(chromium_src_root):
cros_lib.Die('chromium src/ dir not found')
external_manifest_dir = os.path.join(tempfile.gettempdir(),
_EXTERNAL_MANIFEST_DIR)
internal_manifest_dir = os.path.join(tempfile.gettempdir(),
_INTERNAL_MANIFEST_DIR)
# Sync manifest and .DEPS.git files.
GetSource(external_manifest_dir, _EXTERNAL_MANIFEST_PROJECT)
GetSource(internal_manifest_dir, _INTERNAL_MANIFEST_PROJECT,
internal=True)
project_list = [_CHROMIUM_SRC_PROJECT, _CHROMIUM_SRC_INTERNAL_PROJECT]
cros_lib.RunCommand(['repo', 'sync'] + project_list, cwd=repo_root)
# Update external manifest.
Manifest(repo_root, os.path.join(external_manifest_dir, 'oldlayout.xml'),
options.testroot, internal=False,
dryrun=not options.force).PerformUpdate()
# Update internal manifest.
Manifest(repo_root, os.path.join(internal_manifest_dir, 'oldlayout.xml'),
options.testroot, internal=True,
dryrun=not options.force).PerformUpdate()