blob: aea4f623b35ffb813610546379caf5712246274a [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright (c) 2012 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.
"""This module uprevs a given package's ebuild to the next revision."""
from __future__ import print_function
import os
from chromite.lib import constants
from chromite.lib import commandline
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import git
from chromite.lib import osutils
from chromite.lib import parallel
from chromite.lib import portage_util
from chromite.lib import repo_util
from chromite.cbuildbot import manifest_version
# Commit message subject for uprevving Portage packages.
GIT_COMMIT_SUBJECT = 'Marking set of ebuilds as stable'
# Commit message for uprevving Portage packages.
_GIT_COMMIT_MESSAGE = 'Marking 9999 ebuild for %s as stable.'
# Dictionary of valid commands with usage information.
COMMAND_DICTIONARY = {
'commit': 'Marks given ebuilds as stable locally',
'push': 'Pushes previous marking of ebuilds to remote repo',
}
# ======================= Global Helper Functions ========================
def CleanStalePackages(srcroot, boards, package_atoms):
"""Cleans up stale package info from a previous build.
Args:
srcroot: Root directory of the source tree.
boards: Boards to clean the packages from.
package_atoms: A list of package atoms to unmerge.
"""
if package_atoms:
logging.info('Cleaning up stale packages %s.', package_atoms)
# First unmerge all the packages for a board, then eclean it.
# We need these two steps to run in order (unmerge/eclean),
# but we can let all the boards run in parallel.
def _CleanStalePackages(board):
if board:
suffix = '-' + board
runcmd = cros_build_lib.run
else:
suffix = ''
runcmd = cros_build_lib.sudo_run
emerge, eclean = 'emerge' + suffix, 'eclean' + suffix
if not osutils.FindMissingBinaries([emerge, eclean]):
if package_atoms:
# If nothing was found to be unmerged, emerge will exit(1).
result = runcmd([emerge, '-q', '--unmerge'] + list(package_atoms),
enter_chroot=True, extra_env={'CLEAN_DELAY': '0'},
error_code_ok=True, cwd=srcroot)
if result.returncode not in (0, 1):
raise cros_build_lib.RunCommandError('unexpected error', result)
runcmd([eclean, '-d', 'packages'],
cwd=srcroot, enter_chroot=True,
redirect_stdout=True, redirect_stderr=True)
tasks = []
for board in boards:
tasks.append([board])
tasks.append([None])
parallel.RunTasksInProcessPool(_CleanStalePackages, tasks)
# TODO(build): This code needs to be gutted and rebased to cros_build_lib.
def _DoWeHaveLocalCommits(stable_branch, tracking_branch, cwd):
"""Returns true if there are local commits."""
output = git.RunGit(
cwd, ['rev-parse', stable_branch, tracking_branch]).output.split()
return output[0] != output[1]
# ======================= End Global Helper Functions ========================
def PushChange(stable_branch, tracking_branch, dryrun, cwd,
staging_branch=None):
"""Pushes commits in the stable_branch to the remote git repository.
Pushes local commits from calls to CommitChange to the remote git
repository specified by current working directory. If changes are
found to commit, they will be merged to the merge branch and pushed.
In that case, the local repository will be left on the merge branch.
Args:
stable_branch: The local branch with commits we want to push.
tracking_branch: The tracking branch of the local branch.
dryrun: Use git push --dryrun to emulate a push.
cwd: The directory to run commands in.
staging_branch: The staging branch to push for a failed PFQ run
Raises:
OSError: Error occurred while pushing.
"""
if not git.DoesCommitExistInRepo(cwd, stable_branch):
logging.debug('No branch created for %s. Exiting', cwd)
return
if not _DoWeHaveLocalCommits(stable_branch, tracking_branch, cwd):
logging.debug('No work found to push in %s. Exiting', cwd)
return
# For the commit queue, our local branch may contain commits that were
# just tested and pushed during the CommitQueueCompletion stage. Sync
# and rebase our local branch on top of the remote commits.
remote_ref = git.GetTrackingBranch(cwd,
branch=stable_branch,
for_push=True)
# SyncPushBranch rebases HEAD onto the updated remote. We need to checkout
# stable_branch here in order to update it.
git.RunGit(cwd, ['checkout', stable_branch])
git.SyncPushBranch(cwd, remote_ref.remote, remote_ref.ref)
# Check whether any local changes remain after the sync.
if not _DoWeHaveLocalCommits(stable_branch, remote_ref.ref, cwd):
logging.info('All changes already pushed for %s. Exiting', cwd)
return
# Add a failsafe check here. Only CLs from the 'chrome-bot' or
# 'chromeos-ci-prod' user should be involved here. If any other CLs are
# found then complain. In dryruns extra CLs are normal, though, and can
# be ignored.
bad_cl_cmd = [
'log', '--format=short', '--perl-regexp', '--author',
'^(?!chrome-bot|chromeos-ci-prod)',
'%s..%s' % (remote_ref.ref, stable_branch)
]
bad_cls = git.RunGit(cwd, bad_cl_cmd).output
if bad_cls.strip() and not dryrun:
logging.error(
'The Uprev stage found changes from users other than '
'chrome-bot or chromeos-ci-prod:\n\n%s', bad_cls)
raise AssertionError('Unexpected CLs found during uprev stage.')
if staging_branch is not None:
logging.info('PFQ FAILED. Pushing uprev change to staging branch %s',
staging_branch)
description = git.RunGit(
cwd,
['log', '--format=format:%s%n%n%b',
'%s..%s' % (remote_ref.ref, stable_branch)]).output
description = '%s\n\n%s' % (GIT_COMMIT_SUBJECT, description)
logging.info('For %s, using description %s', cwd, description)
git.CreatePushBranch(constants.MERGE_BRANCH, cwd,
remote_push_branch=remote_ref)
git.RunGit(cwd, ['merge', '--squash', stable_branch])
git.RunGit(cwd, ['commit', '-m', description])
git.RunGit(cwd, ['config', 'push.default', 'tracking'])
git.PushBranch(constants.MERGE_BRANCH, cwd, dryrun=dryrun,
staging_branch=staging_branch)
class GitBranch(object):
"""Wrapper class for a git branch."""
def __init__(self, branch_name, tracking_branch, cwd):
"""Sets up variables but does not create the branch.
Args:
branch_name: The name of the branch.
tracking_branch: The associated tracking branch.
cwd: The git repository to work in.
"""
self.branch_name = branch_name
self.tracking_branch = tracking_branch
self.cwd = cwd
def CreateBranch(self):
self.Checkout()
def Checkout(self, branch=None):
"""Function used to check out to another GitBranch."""
if not branch:
branch = self.branch_name
if branch == self.tracking_branch or self.Exists(branch):
git.RunGit(self.cwd, ['checkout', '-f', branch], quiet=True)
else:
repo = repo_util.Repository.MustFind(self.cwd)
repo.StartBranch(branch, projects=['.'], cwd=self.cwd)
def Exists(self, branch=None):
"""Returns True if the branch exists."""
if not branch:
branch = self.branch_name
branches = git.RunGit(self.cwd, ['branch']).output
return branch in branches.split()
def GetParser():
"""Creates the argparse parser."""
parser = commandline.ArgumentParser()
parser.add_argument('--all', action='store_true',
help='Mark all packages as stable.')
parser.add_argument('-b', '--boards', default='',
help='Colon-separated list of boards.')
parser.add_argument('--drop_file',
help='File to list packages that were revved.')
parser.add_argument('--dryrun', action='store_true',
help='Passes dry-run to git push if pushing a change.')
parser.add_argument('--force', action='store_true',
help='Force the stabilization of blacklisted packages. '
'(only compatible with -p)')
parser.add_argument('--list_revisions', action='store_true',
help='List all revisions included in the commit message.')
parser.add_argument('-o', '--overlays',
help='Colon-separated list of overlays to modify.')
parser.add_argument('--overlay-type',
help='Populates --overlays based on "public", "private"'
', or "both".')
parser.add_argument('-p', '--packages',
help='Colon separated list of packages to rev.')
parser.add_argument('--buildroot', type='path',
help='Path to buildroot.')
parser.add_argument('-r', '--srcroot', type='path',
help='Path to root src. Deprecated in favor of '
'--buildroot')
parser.add_argument('--verbose', action='store_true',
help='Prints out debug info.')
parser.add_argument('--staging_branch',
help='The staging branch to push changes')
parser.add_argument('command', choices=sorted(COMMAND_DICTIONARY),
help='Command to run.')
return parser
def main(argv):
parser = GetParser()
options = parser.parse_args(argv)
# TODO: Remove this code in favor of a simple default on buildroot when
# srcroot is removed.
if options.srcroot and not options.buildroot:
# Convert /<repo>/src -> <repo>
options.buildroot = os.path.dirname(options.srcroot)
if not options.buildroot:
options.buildroot = constants.SOURCE_ROOT
options.srcroot = None
options.Freeze()
if options.command == 'commit':
if not options.packages and not options.all:
parser.error('Please specify at least one package (--packages)')
if options.force and options.all:
parser.error('Cannot use --force with --all. You must specify a list of '
'packages you want to force uprev.')
if not os.path.isdir(options.buildroot):
parser.error('buildroot is not a valid path: %s' % options.buildroot)
if options.overlay_type and options.overlays:
parser.error('Cannot use --overlay-type with --overlays.')
portage_util.EBuild.VERBOSE = options.verbose
package_list = None
if options.packages:
package_list = options.packages.split(':')
overlays = []
if options.overlays:
for path in options.overlays.split(':'):
if not os.path.isdir(path):
cros_build_lib.Die('Cannot find overlay: %s' % path)
overlays.append(os.path.realpath(path))
elif options.overlay_type:
overlays = portage_util.FindOverlays(
options.overlay_type, buildroot=options.buildroot)
else:
logging.warning('Missing --overlays argument')
overlays.extend([
'%s/src/private-overlays/chromeos-overlay' % options.buildroot,
'%s/src/third_party/chromiumos-overlay' % options.buildroot])
manifest = git.ManifestCheckout.Cached(options.buildroot)
# Dict mapping from each overlay to its tracking branch.
overlay_tracking_branch = {}
# Dict mapping from each git repository (project) to a list of its overlays.
git_project_overlays = {}
for overlay in overlays:
remote_ref = git.GetTrackingBranchViaManifest(overlay, manifest=manifest)
overlay_tracking_branch[overlay] = remote_ref.ref
git_project_overlays.setdefault(remote_ref.project_name, []).append(overlay)
if options.command == 'push':
_WorkOnPush(options, overlay_tracking_branch, git_project_overlays)
elif options.command == 'commit':
_WorkOnCommit(options, overlays, overlay_tracking_branch,
git_project_overlays, manifest, package_list)
def _WorkOnPush(options, overlay_tracking_branch, git_project_overlays):
"""Push uprevs of overlays belonging to differet git projects in parallel.
Args:
options: The options object returned by the argument parser.
overlay_tracking_branch: A dict mapping from each overlay to its tracking
branch.
git_project_overlays: A dict mapping from each git repository to a list of
its overlays.
"""
inputs = [[options, overlays_per_project, overlay_tracking_branch]
for overlays_per_project in git_project_overlays.values()]
parallel.RunTasksInProcessPool(_PushOverlays, inputs)
def _PushOverlays(options, overlays, overlay_tracking_branch):
"""Push uprevs for overlays in sequence.
Args:
options: The options object returned by the argument parser.
overlays: A list of overlays to push uprevs in sequence.
overlay_tracking_branch: A dict mapping from each overlay to its tracking
branch.
"""
for overlay in overlays:
if not os.path.isdir(overlay):
logging.warning('Skipping %s, which is not a directory.', overlay)
continue
tracking_branch = overlay_tracking_branch[overlay]
PushChange(constants.STABLE_EBUILD_BRANCH, tracking_branch, options.dryrun,
cwd=overlay, staging_branch=options.staging_branch)
def _WorkOnCommit(options, overlays, overlay_tracking_branch,
git_project_overlays, manifest, package_list):
"""Commit uprevs of overlays belonging to different git projects in parallel.
Args:
options: The options object returned by the argument parser.
overlays: A list of overlays to work on.
overlay_tracking_branch: A dict mapping from each overlay to its tracking
branch.
git_project_overlays: A dict mapping from each git repository to a list of
its overlays.
manifest: The manifest of the given source root.
package_list: A list of packages passed from commandline to work on.
"""
overlay_ebuilds = _GetOverlayToEbuildsMap(options, overlays, package_list)
with parallel.Manager() as manager:
# Contains the array of packages we actually revved.
revved_packages = manager.list()
new_package_atoms = manager.list()
inputs = [[options, manifest, overlays_per_project, overlay_tracking_branch,
overlay_ebuilds, revved_packages, new_package_atoms]
for overlays_per_project in git_project_overlays.values()]
parallel.RunTasksInProcessPool(_CommitOverlays, inputs)
chroot_path = os.path.join(options.buildroot, constants.DEFAULT_CHROOT_DIR)
if os.path.exists(chroot_path):
CleanStalePackages(options.buildroot, options.boards.split(':'),
new_package_atoms)
if options.drop_file:
osutils.WriteFile(options.drop_file, ' '.join(revved_packages))
def _GetOverlayToEbuildsMap(options, overlays, package_list):
"""Get ebuilds for overlays.
Args:
options: The options object returned by the argument parser.
overlays: A list of overlays to work on.
package_list: A list of packages passed from commandline to work on.
Returns:
A dict mapping each overlay to a list of ebuilds belonging to it.
"""
root_version = manifest_version.VersionInfo.from_repo(options.buildroot)
subdir_removal = manifest_version.VersionInfo('10363.0.0')
require_subdir_support = root_version < subdir_removal
overlay_ebuilds = {}
inputs = [[overlay, options.all, package_list, options.force,
require_subdir_support]
for overlay in overlays]
result = parallel.RunTasksInProcessPool(
portage_util.GetOverlayEBuilds, inputs)
for idx, ebuilds in enumerate(result):
overlay_ebuilds[overlays[idx]] = ebuilds
return overlay_ebuilds
def _CommitOverlays(options, manifest, overlays, overlay_tracking_branch,
overlay_ebuilds, revved_packages, new_package_atoms):
"""Commit uprevs for overlays in sequence.
Args:
options: The options object returned by the argument parser.
manifest: The manifest of the given source root.
overlays: A list over overlays to commit.
overlay_tracking_branch: A dict mapping from each overlay to its tracking
branch.
overlay_ebuilds: A dict mapping overlays to their ebuilds.
revved_packages: A shared list of revved packages.
new_package_atoms: A shared list of new package atoms.
"""
for overlay in overlays:
if not os.path.isdir(overlay):
logging.warning('Skipping %s, which is not a directory.', overlay)
continue
# Note we intentionally work from the non push tracking branch;
# everything built thus far has been against it (meaning, http mirrors),
# thus we should honor that. During the actual push, the code switches
# to the correct urls, and does an appropriate rebasing.
tracking_branch = overlay_tracking_branch[overlay]
existing_commit = git.GetGitRepoRevision(overlay)
# Make sure we run in the top-level git directory in case we are
# adding/removing an overlay in existing_commit.
git_root = git.FindGitTopLevel(overlay)
if git_root is None:
cros_build_lib.Die('No git repo at overlay directory %s.', overlay)
work_branch = GitBranch(constants.STABLE_EBUILD_BRANCH, tracking_branch,
cwd=git_root)
work_branch.CreateBranch()
if not work_branch.Exists():
cros_build_lib.Die('Unable to create stabilizing branch in %s' %
overlay)
# In the case of uprevving overlays that have patches applied to them,
# include the patched changes in the stabilizing branch.
git.RunGit(git_root, ['rebase', existing_commit])
ebuilds = overlay_ebuilds.get(overlay, [])
if ebuilds:
with parallel.Manager() as manager:
# Contains the array of packages we actually revved.
messages = manager.list()
ebuild_paths_to_add = manager.list()
ebuild_paths_to_remove = manager.list()
inputs = [[overlay, ebuild, manifest, options, ebuild_paths_to_add,
ebuild_paths_to_remove, messages, revved_packages,
new_package_atoms] for ebuild in ebuilds]
parallel.RunTasksInProcessPool(_WorkOnEbuild, inputs)
if ebuild_paths_to_add:
logging.info('Adding new stable ebuild paths %s in overlay %s.',
ebuild_paths_to_add, overlay)
git.RunGit(overlay, ['add'] + list(ebuild_paths_to_add))
if ebuild_paths_to_remove:
logging.info('Removing old ebuild paths %s in overlay %s.',
ebuild_paths_to_remove, overlay)
git.RunGit(overlay, ['rm', '-f'] + list(ebuild_paths_to_remove))
if messages:
portage_util.EBuild.CommitChange('\n\n'.join(messages), overlay)
def _WorkOnEbuild(overlay, ebuild, manifest, options, ebuild_paths_to_add,
ebuild_paths_to_remove, messages, revved_packages,
new_package_atoms):
"""Work on a single ebuild.
Args:
overlay: The overlay where the ebuild belongs to.
ebuild: The ebuild to work on.
manifest: The manifest of the given source root.
options: The options object returned by the argument parser.
ebuild_paths_to_add: New stable ebuild paths to add to git.
ebuild_paths_to_remove: Old ebuild paths to remove from git.
messages: A share list of commit messages.
revved_packages: A shared list of revved packages.
new_package_atoms: A shared list of new package atoms.
"""
if options.verbose:
logging.info('Working on %s, info %s', ebuild.package,
ebuild.cros_workon_vars)
try:
result = ebuild.RevWorkOnEBuild(os.path.join(options.buildroot, 'src'),
manifest)
if result:
new_package, ebuild_path_to_add, ebuild_path_to_remove = result
if ebuild_path_to_add:
ebuild_paths_to_add.append(ebuild_path_to_add)
if ebuild_path_to_remove:
ebuild_paths_to_remove.append(ebuild_path_to_remove)
messages.append(_GIT_COMMIT_MESSAGE % ebuild.package)
if options.list_revisions:
info = ebuild.GetSourceInfo(os.path.join(options.buildroot, 'src'),
manifest)
srcdirs = [os.path.join(options.buildroot, 'src', srcdir)
for srcdir in ebuild.cros_workon_vars.localname]
old_commit_ids = dict(
zip(srcdirs, ebuild.cros_workon_vars.commit.split(',')))
git_log = []
for srcdir in info.srcdirs:
old_commit_id = old_commit_ids.get(srcdir)
new_commit_id = ebuild.GetCommitId(srcdir)
if not old_commit_id or old_commit_id == new_commit_id:
continue
logs = git.RunGit(srcdir, [
'log', '%s..%s' % (old_commit_id[:8], new_commit_id[:8]),
'--pretty=format:%h %<(63,trunc)%s'])
git_log.append('$ ' + logs.cmdstr)
git_log.extend(line.strip() for line in logs.output.splitlines())
if git_log:
messages.append('\n'.join(git_log))
revved_packages.append(ebuild.package)
new_package_atoms.append('=%s' % new_package)
except portage_util.EbuildVersionError as e:
logging.warning('Unable to rev %s: %s', ebuild.package, e)
raise
except (OSError, IOError):
logging.warning(
'Cannot rev %s\n'
'Note you will have to go into %s '
'and reset the git repo yourself.', ebuild.package, overlay)
raise