| #!/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() |