blob: 7fdc9dc89ff01d51d43f0e315bf9f8ce2cbfc31d [file] [log] [blame]
# 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.
"""Routines and classes for working with Portage overlays and ebuilds."""
import filecmp
import fileinput
import os
import re
import shutil
import sys
from chromite.lib import cros_build_lib
_PUBLIC_OVERLAY = '%(build_root)s/src/third_party/chromiumos-overlay'
_OVERLAY_LIST_CMD = '%(build_root)s/src/platform/dev/host/cros_overlay_list'
# Takes two strings, package_name and commit_id.
_GIT_COMMIT_MESSAGE = 'Marking 9999 ebuild for %s with commit %s as stable.'
def FindOverlays(srcroot, overlay_type):
"""Return the list of overlays to use for a given buildbot.
Args:
overlay_type: A string describing which overlays you want.
'private': Just the private overlay.
'public': Just the public overlay.
'both': Both the public and private overlays.
"""
# we use a dictionary to allow tests to override _OVERLAY_LIST_CMD;
# see the cbuildbot_stages and portage_utilities unit tests.
format_args = { 'build_root' : srcroot }
cmd = _OVERLAY_LIST_CMD % format_args
# Check in case we haven't checked out the source yet.
if not os.path.exists(cmd):
return []
cmd_argv = [cmd, '--all_boards']
if overlay_type == 'private':
cmd_argv.append('--nopublic')
elif overlay_type == 'public':
cmd_argv.append('--noprivate')
elif overlay_type != 'both':
return []
overlays = cros_build_lib.RunCommand(
cmd_argv, redirect_stdout=True, print_cmd=False).output.split()
if overlay_type != 'private':
# TODO(davidjames): cros_overlay_list should include chromiumos-overlay in
# its list of public overlays. But it doesn't yet...
overlays.append(_PUBLIC_OVERLAY % format_args)
return overlays
class _BlackListManager(object):
"""Small wrapper class to manage black lists for marking all packages."""
BLACK_LIST_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)),
'cros_mark_as_stable_blacklist')
def __init__(self):
"""Initializes the black list manager."""
self.black_list_re_array = None
self._Initialize()
def _Initialize(self):
"""Initializes the black list manager from a black list file."""
self.black_list_re_array = []
with open(self.BLACK_LIST_FILE) as file_handle:
for line in file_handle.readlines():
line = line.strip()
# Ignore comment lines.
if line and not line.startswith('#'):
line = line.rstrip()
package_array = line.split('/')
assert len(package_array) == 2, \
'Line %s does not match package format.' % line
category, package_name = package_array
self.black_list_re_array.append(
re.compile('.*/%s/%s/%s-.*\.ebuild' % (category, package_name,
package_name)))
def IsPackageBlackListed(self, path_to_ebuild):
"""Returns True if the package given by the path is blacklisted."""
assert self.black_list_re_array != None, 'Black list not initialized.'
for pattern in self.black_list_re_array:
if pattern.match(path_to_ebuild):
return True
return False
class EBuildVersionFormatException(Exception):
def __init__(self, filename):
self.filename = filename
message = ('Ebuild file name %s '
'does not match expected format.' % filename)
super(EBuildVersionFormatException, self).__init__(message)
class EBuild(object):
"""Wrapper class for information about an ebuild."""
verbose = False
@classmethod
def _Print(cls, message):
"""Verbose print function."""
if cls.verbose:
cros_build_lib.Info(message)
@classmethod
def _RunCommand(cls, command):
command_result = cros_build_lib.RunCommand(
command, redirect_stdout=True, print_cmd=cls.verbose, shell=True)
return command_result.output
@classmethod
def MarkAsStable(cls, unstable_ebuild_path, new_stable_ebuild_path,
commit_keyword, commit_value, redirect_file=None,
make_stable=True):
"""Static function that creates a revved stable ebuild.
This function assumes you have already figured out the name of the new
stable ebuild path and then creates that file from the given unstable
ebuild and marks it as stable. If the commit_value is set, it also
set the commit_keyword=commit_value pair in the ebuild.
Args:
unstable_ebuild_path: The path to the unstable ebuild.
new_stable_ebuild_path: The path you want to use for the new stable
ebuild.
commit_keyword: Optional keyword to set in the ebuild to mark it as
stable.
commit_value: Value to set the above keyword to.
redirect_file: Optionally redirect output of new ebuild somewhere else.
make_stable: Actually make the ebuild stable.
"""
shutil.copyfile(unstable_ebuild_path, new_stable_ebuild_path)
for line in fileinput.input(new_stable_ebuild_path, inplace=1):
# Has to be done here to get changes to sys.stdout from fileinput.input.
if not redirect_file:
redirect_file = sys.stdout
if line.startswith('KEYWORDS'):
# Actually mark this file as stable by removing ~'s.
if make_stable:
redirect_file.write(line.replace('~', ''))
else:
redirect_file.write(line)
elif line.startswith('EAPI'):
# Always add new commit_id after EAPI definition.
redirect_file.write(line)
if commit_keyword and commit_value:
redirect_file.write('%s="%s"\n' % (commit_keyword, commit_value))
elif not line.startswith(commit_keyword):
# Skip old commit_keyword definition.
redirect_file.write(line)
fileinput.close()
@classmethod
def CommitChange(cls, message):
"""Commits current changes in git locally with given commit message.
Args:
message: the commit string to write when committing to git.
Raises:
OSError: Error occurred while committing.
"""
cros_build_lib.Info('Committing changes with commit message: %s' % message)
git_commit_cmd = 'git commit -am "%s"' % message
cls._RunCommand(git_commit_cmd)
_package_version_re = re.compile(
r'.*-(([0-9][0-9a-z_.]*)(-r[0-9]+)?)[.]ebuild')
def __init__(self, path):
"""Sets up data about an ebuild from its path."""
_path, self._category, self._pkgname, filename = path.rsplit('/', 3)
m = self._package_version_re.match(filename)
if not m:
raise EBuildVersionFormatException(filename)
self.version, self._version_no_rev, revision = m.groups()
if revision is not None:
self.current_revision = int(revision.replace('-r', ''))
else:
self.current_revision = 0
self.package = '%s/%s' % (self._category, self._pkgname)
self._ebuild_path_no_version = os.path.join(
os.path.dirname(path), self._pkgname)
self.ebuild_path_no_revision = '%s-%s' % (
self._ebuild_path_no_version, self._version_no_rev)
self._unstable_ebuild_path = '%s-9999.ebuild' % (
self._ebuild_path_no_version)
self.ebuild_path = path
self.is_workon = False
self.is_stable = False
self._ReadEBuild(path)
def _ReadEBuild(self, path):
"""Determine the settings of `is_workon` and `is_stable`.
`is_workon` is determined by whether the ebuild inherits from
the 'cros-workon' eclass. `is_stable` is determined by whether
there's a '~' in the KEYWORDS setting in the ebuild.
This function is separate from __init__() to allow unit tests to
stub it out.
"""
for line in fileinput.input(path):
if line.startswith('inherit ') and 'cros-workon' in line:
self.is_workon = True
elif (line.startswith('KEYWORDS=') and '~' not in line and
('amd64' in line or 'x86' in line or 'arm' in line)):
self.is_stable = True
fileinput.close()
def _GetCommitId(self, srcroot):
"""Get the commit id for this ebuild."""
# Grab and evaluate CROS_WORKON variables from this ebuild.
cmd = ('export CROS_WORKON_LOCALNAME="%s" CROS_WORKON_PROJECT="%s"; '
'eval $(grep -E "^CROS_WORKON" %s) && '
'echo $CROS_WORKON_PROJECT '
'$CROS_WORKON_LOCALNAME/$CROS_WORKON_SUBDIR'
% (self._pkgname, self._pkgname, self._unstable_ebuild_path))
project, subdir = self._RunCommand(cmd).split()
# Calculate srcdir.
if self._category == 'chromeos-base':
dir_ = 'platform'
else:
dir_ = 'third_party'
srcdir = os.path.join(srcroot, dir_, subdir)
if not os.path.isdir(srcdir):
cros_build_lib.Die('Cannot find commit id for %s' % self.ebuild_path)
# Verify that we're grabbing the commit id from the right project name.
# NOTE: chromeos-kernel has the wrong project name, so it fails this
# check.
# TODO(davidjames): Fix the project name in the chromeos-kernel ebuild.
cmd = ('cd %s && ( git config --get remote.cros.projectname || '
'git config --get remote.cros-internal.projectname )') % srcdir
actual_project = self._RunCommand(cmd).rstrip()
if project not in (actual_project, 'chromeos-kernel'):
cros_build_lib.Die('Project name mismatch for %s (%s != %s)' % (
self._unstable_ebuild_path, project, actual_project))
# Get commit id.
output = self._RunCommand('cd %s && git rev-parse HEAD' % srcdir)
if not output:
cros_build_lib.Die('Missing commit id for %s' % self.ebuild_path)
return output.rstrip()
def RevWorkOnEBuild(self, srcroot, redirect_file=None):
"""Revs a workon ebuild given the git commit hash.
By default this class overwrites a new ebuild given the normal
ebuild rev'ing logic. However, a user can specify a redirect_file
to redirect the new stable ebuild to another file.
Args:
srcroot: full path to the 'src' subdirectory in the source
repository.
redirect_file: Optional file to write the new ebuild. By default
it is written using the standard rev'ing logic. This file must be
opened and closed by the caller.
Raises:
OSError: Error occurred while creating a new ebuild.
IOError: Error occurred while writing to the new revved ebuild file.
Returns:
If the revved package is different than the old ebuild, return the full
revved package name, including the version number. Otherwise, return None.
"""
if self.is_stable:
stable_version_no_rev = self._version_no_rev
else:
# If given unstable ebuild, use 0.0.1 rather than 9999.
stable_version_no_rev = '0.0.1'
new_version = '%s-r%d' % (
stable_version_no_rev, self.current_revision + 1)
new_stable_ebuild_path = '%s-%s.ebuild' % (
self._ebuild_path_no_version, new_version)
self._Print('Creating new stable ebuild %s' % new_stable_ebuild_path)
if not os.path.exists(self._unstable_ebuild_path):
cros_build_lib.Die('Missing unstable ebuild: %s' %
self._unstable_ebuild_path)
commit_id = self._GetCommitId(srcroot)
self.MarkAsStable(self._unstable_ebuild_path,
new_stable_ebuild_path,
'CROS_WORKON_COMMIT', commit_id, redirect_file)
old_ebuild_path = self.ebuild_path
if filecmp.cmp(old_ebuild_path, new_stable_ebuild_path, shallow=False):
os.unlink(new_stable_ebuild_path)
return None
else:
self._Print('Adding new stable ebuild to git')
self._RunCommand('git add %s' % new_stable_ebuild_path)
if self.is_stable:
self._Print('Removing old ebuild from git')
self._RunCommand('git rm %s' % old_ebuild_path)
message = _GIT_COMMIT_MESSAGE % (self.package, commit_id)
self.CommitChange(message)
return '%s-%s' % (self.package, new_version)
def BestEBuild(ebuilds):
"""Returns the newest EBuild from a list of EBuild objects."""
from portage.versions import vercmp
winner = ebuilds[0]
for ebuild in ebuilds[1:]:
if vercmp(winner.version, ebuild.version) < 0:
winner = ebuild
return winner
def _FindUprevCandidates(files, blacklist):
"""Return the uprev candidate ebuild from a specified list of files.
Usually an uprev candidate is a the stable ebuild in a cros_workon
directory. However, if no such stable ebuild exists (someone just
checked in the 9999 ebuild), this is the unstable ebuild.
If the package isn't a cros_workon package, return None.
Args:
files: List of files in a package directory.
"""
stable_ebuilds = []
unstable_ebuilds = []
for path in files:
if not path.endswith('.ebuild') or os.path.islink(path):
continue
ebuild = EBuild(path)
if not ebuild.is_workon or blacklist.IsPackageBlackListed(path):
continue
if ebuild.is_stable:
stable_ebuilds.append(ebuild)
else:
unstable_ebuilds.append(ebuild)
# If both ebuild lists are empty, the passed in file list was for
# a non-workon package.
if not unstable_ebuilds:
if stable_ebuilds:
path = os.path.dirname(stable_ebuilds[0].ebuild_path)
cros_build_lib.Die('Missing 9999 ebuild in %s' % path)
return None
path = os.path.dirname(unstable_ebuilds[0].ebuild_path)
if len(unstable_ebuilds) > 1:
cros_build_lib.Die('Found multiple unstable ebuilds in %s' % path)
if not stable_ebuilds:
cros_build_lib.Warning('Missing stable ebuild in %s' % path)
return unstable_ebuilds[0]
if len(stable_ebuilds) == 1:
return stable_ebuilds[0]
stable_versions = set(ebuild._version_no_rev for ebuild in stable_ebuilds)
if len(stable_versions) > 1:
package = stable_ebuilds[0].package
message = 'Found multiple stable ebuild versions in %s:' % path
for version in stable_versions:
message += '\n %s-%s' % (package, version)
cros_build_lib.Die(message)
uprev_ebuild = max(stable_ebuilds, key=lambda eb: eb.current_revision)
for ebuild in stable_ebuilds:
if ebuild != uprev_ebuild:
cros_build_lib.Warning('Ignoring stable ebuild revision %s in %s' %
(ebuild.version, path))
return uprev_ebuild
def BuildEBuildDictionary(overlays, use_all, packages):
"""Build a dictionary of the ebuilds in the specified overlays.
overlays: A map which maps overlay directories to arrays of stable EBuilds
inside said directories.
use_all: Whether to include all ebuilds in the specified directories.
If true, then we gather all packages in the directories regardless
of whether they are in our set of packages.
packages: A set of the packages we want to gather.
"""
blacklist = _BlackListManager()
for overlay in overlays:
for package_dir, _dirs, files in os.walk(overlay):
# Add stable ebuilds to overlays[overlay].
paths = [os.path.join(package_dir, path) for path in files]
ebuild = _FindUprevCandidates(paths, blacklist)
# If the --all option isn't used, we only want to update packages that
# are in packages.
if ebuild and (use_all or ebuild.package in packages):
overlays[overlay].append(ebuild)